mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-23 20:48:43 +00:00
Read first frame of backup to validate before proceeding.
Addresses #11952.
This commit is contained in:
@@ -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 {}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user