Read first frame of backup to validate before proceeding.

Addresses #11952.
This commit is contained in:
Nicholas
2023-10-02 20:30:39 -04:00
committed by GitHub
parent e9fbce4e28
commit da84cde6da
3 changed files with 81 additions and 18 deletions

View File

@@ -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 {}
}

View File

@@ -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

View File

@@ -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,