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.Conversions;
|
||||||
import org.signal.core.util.StreamUtil;
|
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.kdf.HKDF;
|
||||||
import org.signal.libsignal.protocol.util.ByteUtil;
|
import org.signal.libsignal.protocol.util.ByteUtil;
|
||||||
import org.thoughtcrime.securesms.backup.proto.BackupFrame;
|
import org.thoughtcrime.securesms.backup.proto.BackupFrame;
|
||||||
@@ -27,6 +28,9 @@ import javax.crypto.spec.SecretKeySpec;
|
|||||||
|
|
||||||
class BackupRecordInputStream extends FullBackupBase.BackupStream {
|
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 int version;
|
||||||
private final InputStream in;
|
private final InputStream in;
|
||||||
private final Cipher cipher;
|
private final Cipher cipher;
|
||||||
@@ -92,6 +96,35 @@ class BackupRecordInputStream extends FullBackupBase.BackupStream {
|
|||||||
return readFrame(in);
|
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 {
|
void readAttachmentTo(OutputStream out, int length) throws IOException {
|
||||||
try {
|
try {
|
||||||
Conversions.intToByteArray(iv, 0, counter++);
|
Conversions.intToByteArray(iv, 0, counter++);
|
||||||
@@ -142,24 +175,7 @@ class BackupRecordInputStream extends FullBackupBase.BackupStream {
|
|||||||
|
|
||||||
private BackupFrame readFrame(InputStream in) throws IOException {
|
private BackupFrame readFrame(InputStream in) throws IOException {
|
||||||
try {
|
try {
|
||||||
byte[] length = new byte[4];
|
int frameLength = decryptFrameLength(in);
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[] frame = new byte[frameLength];
|
byte[] frame = new byte[frameLength];
|
||||||
StreamUtil.readFully(in, frame);
|
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 {}
|
static class BadMacException extends IOException {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ import java.io.FileInputStream;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
|
import java.security.InvalidAlgorithmParameterException;
|
||||||
|
import java.security.InvalidKeyException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
@@ -63,6 +65,24 @@ public class FullBackupImporter extends FullBackupBase {
|
|||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
private static final String TAG = Log.tag(FullBackupImporter.class);
|
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,
|
public static void importFile(@NonNull Context context, @NonNull AttachmentSecret attachmentSecret,
|
||||||
@NonNull SQLiteDatabase db, @NonNull Uri uri, @NonNull String passphrase)
|
@NonNull SQLiteDatabase db, @NonNull Uri uri, @NonNull String passphrase)
|
||||||
throws IOException
|
throws IOException
|
||||||
|
|||||||
@@ -289,6 +289,11 @@ public final class RestoreBackupFragment extends LoggingFragment {
|
|||||||
SQLiteDatabase database = SignalDatabase.getBackupDatabase();
|
SQLiteDatabase database = SignalDatabase.getBackupDatabase();
|
||||||
|
|
||||||
BackupPassphrase.set(context, passphrase);
|
BackupPassphrase.set(context, passphrase);
|
||||||
|
|
||||||
|
if (!FullBackupImporter.validatePassphrase(context, backup.getUri(), passphrase)) {
|
||||||
|
return BackupImportResult.FAILURE_UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
FullBackupImporter.importFile(context,
|
FullBackupImporter.importFile(context,
|
||||||
AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret(),
|
AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret(),
|
||||||
database,
|
database,
|
||||||
|
|||||||
Reference in New Issue
Block a user