diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/BackupRecordInputStream.java b/app/src/main/java/org/thoughtcrime/securesms/backup/BackupRecordInputStream.java index 87509aed5a..d57179bd7c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/BackupRecordInputStream.java +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/BackupRecordInputStream.java @@ -4,6 +4,7 @@ import androidx.annotation.NonNull; import org.signal.core.util.Conversions; import org.signal.core.util.StreamUtil; +import org.signal.core.util.logging.Log; import org.signal.libsignal.protocol.kdf.HKDF; import org.signal.libsignal.protocol.util.ByteUtil; import org.thoughtcrime.securesms.backup.proto.BackupFrame; @@ -27,6 +28,9 @@ import javax.crypto.spec.SecretKeySpec; class BackupRecordInputStream extends FullBackupBase.BackupStream { + private final String TAG = Log.tag(BackupRecordInputStream.class); + private final int MAX_BUFFER_SIZE = 8192; + private final int version; private final InputStream in; private final Cipher cipher; @@ -92,6 +96,35 @@ class BackupRecordInputStream extends FullBackupBase.BackupStream { return readFrame(in); } + boolean validateFrame() throws InvalidAlgorithmParameterException, IOException, InvalidKeyException { + int frameLength = decryptFrameLength(in); + if (frameLength <= 0) { + Log.i(TAG, "Backup frame is not valid due to negative frame length. This is likely because the decryption passphrase was wrong."); + return false; + } + + int bufferSize = Math.min(MAX_BUFFER_SIZE, frameLength); + byte[] buffer = new byte[bufferSize]; + byte[] theirMac = new byte[10]; + while (frameLength > 0) { + int read = in.read(buffer, 0, Math.min(buffer.length, frameLength)); + if (read == -1) return false; + + if (read < MAX_BUFFER_SIZE) { + final int frameEndIndex = read - 10; + mac.update(buffer, 0, frameEndIndex); + System.arraycopy(buffer, frameEndIndex, theirMac, 0, theirMac.length); + } else { + mac.update(buffer, 0, read); + } + frameLength -= read; + } + + byte[] ourMac = ByteUtil.trim(mac.doFinal(), 10); + + return MessageDigest.isEqual(ourMac, theirMac); + } + void readAttachmentTo(OutputStream out, int length) throws IOException { try { Conversions.intToByteArray(iv, 0, counter++); @@ -142,24 +175,7 @@ class BackupRecordInputStream extends FullBackupBase.BackupStream { private BackupFrame readFrame(InputStream in) throws IOException { try { - byte[] length = new byte[4]; - StreamUtil.readFully(in, length); - - Conversions.intToByteArray(iv, 0, counter++); - cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(cipherKey, "AES"), new IvParameterSpec(iv)); - - int frameLength; - if (BackupVersions.isFrameLengthEncrypted(version)) { - mac.update(length); - // this depends upon cipher being a stream cipher mode in order to get back the length without needing a full AES block-size input - byte[] decryptedLength = cipher.update(length); - if (decryptedLength.length != length.length) { - throw new IOException("Cipher was not a stream cipher!"); - } - frameLength = Conversions.byteArrayToInt(decryptedLength); - } else { - frameLength = Conversions.byteArrayToInt(length); - } + int frameLength = decryptFrameLength(in); byte[] frame = new byte[frameLength]; StreamUtil.readFully(in, frame); @@ -182,5 +198,27 @@ class BackupRecordInputStream extends FullBackupBase.BackupStream { } } + private int decryptFrameLength(InputStream inputStream) throws IOException, InvalidAlgorithmParameterException, InvalidKeyException { + byte[] length = new byte[4]; + StreamUtil.readFully(inputStream, length); + + Conversions.intToByteArray(iv, 0, counter++); + cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(cipherKey, "AES"), new IvParameterSpec(iv)); + + int frameLength; + if (BackupVersions.isFrameLengthEncrypted(version)) { + mac.update(length); + // this depends upon cipher being a stream cipher mode in order to get back the length without needing a full AES block-size input + byte[] decryptedLength = cipher.update(length); + if (decryptedLength.length != length.length) { + throw new IOException("Cipher was not a stream cipher!"); + } + frameLength = Conversions.byteArrayToInt(decryptedLength); + } else { + frameLength = Conversions.byteArrayToInt(length); + } + return frameLength; + } + static class BadMacException extends IOException {} } diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupImporter.java b/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupImporter.java index 61f480bbb1..408eb7dc72 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupImporter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/FullBackupImporter.java @@ -45,6 +45,8 @@ import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; @@ -63,6 +65,24 @@ public class FullBackupImporter extends FullBackupBase { @SuppressWarnings("unused") private static final String TAG = Log.tag(FullBackupImporter.class); + public static boolean validatePassphrase(@NonNull Context context, + @NonNull Uri uri, + @NonNull String passphrase) + throws IOException + { + + try (InputStream is = getInputStream(context, uri)) { + BackupRecordInputStream inputStream = new BackupRecordInputStream(is, passphrase); + return inputStream.validateFrame(); + } catch (InvalidAlgorithmParameterException e) { + Log.w(TAG, "Invalid algorithm parameter exception in backup passphrase validation.", e); + return false; + } catch (InvalidKeyException e) { + Log.w(TAG, "Invalid key exception in backup passphrase validation.", e); + return false; + } + } + public static void importFile(@NonNull Context context, @NonNull AttachmentSecret attachmentSecret, @NonNull SQLiteDatabase db, @NonNull Uri uri, @NonNull String passphrase) throws IOException diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/RestoreBackupFragment.java b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/RestoreBackupFragment.java index 6231942226..7ee3ac6de9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/RestoreBackupFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/fragments/RestoreBackupFragment.java @@ -289,6 +289,11 @@ public final class RestoreBackupFragment extends LoggingFragment { SQLiteDatabase database = SignalDatabase.getBackupDatabase(); BackupPassphrase.set(context, passphrase); + + if (!FullBackupImporter.validatePassphrase(context, backup.getUri(), passphrase)) { + return BackupImportResult.FAILURE_UNKNOWN; + } + FullBackupImporter.importFile(context, AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret(), database,