mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-21 09:20:19 +01:00
Verify backup can be decrypted as part of creation flow.
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
package org.thoughtcrime.securesms.backup;
|
||||
|
||||
public class BackupEvent {
|
||||
public enum Type {
|
||||
PROGRESS,
|
||||
PROGRESS_VERIFYING,
|
||||
FINISHED
|
||||
}
|
||||
|
||||
private final Type type;
|
||||
private final long count;
|
||||
private final long estimatedTotalCount;
|
||||
|
||||
BackupEvent(Type type, long count, long estimatedTotalCount) {
|
||||
this.type = type;
|
||||
this.count = count;
|
||||
this.estimatedTotalCount = estimatedTotalCount;
|
||||
}
|
||||
|
||||
public Type getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public long getCount() {
|
||||
return count;
|
||||
}
|
||||
|
||||
public long getEstimatedTotalCount() {
|
||||
return estimatedTotalCount;
|
||||
}
|
||||
|
||||
public double getCompletionPercentage() {
|
||||
if (estimatedTotalCount == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Math.min(99.9f, (double) count * 100L / (double) estimatedTotalCount);
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ public enum BackupFileIOError {
|
||||
ACCESS_ERROR(R.string.LocalBackupJobApi29_backup_failed, R.string.LocalBackupJobApi29_your_backup_directory_has_been_deleted_or_moved),
|
||||
FILE_TOO_LARGE(R.string.LocalBackupJobApi29_backup_failed, R.string.LocalBackupJobApi29_your_backup_file_is_too_large),
|
||||
NOT_ENOUGH_SPACE(R.string.LocalBackupJobApi29_backup_failed, R.string.LocalBackupJobApi29_there_is_not_enough_space),
|
||||
VERIFICATION_FAILED(R.string.LocalBackupJobApi29_backup_failed, R.string.LocalBackupJobApi29_your_backup_could_not_be_verified),
|
||||
UNKNOWN(R.string.LocalBackupJobApi29_backup_failed, R.string.LocalBackupJobApi29_tap_to_manage_backups);
|
||||
|
||||
private static final short BACKUP_FAILED_ID = 31321;
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
package org.thoughtcrime.securesms.backup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.signal.core.util.Conversions;
|
||||
import org.signal.core.util.StreamUtil;
|
||||
import org.signal.libsignal.protocol.kdf.HKDF;
|
||||
import org.signal.libsignal.protocol.util.ByteUtil;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
class BackupRecordInputStream extends FullBackupBase.BackupStream {
|
||||
|
||||
private final InputStream in;
|
||||
private final Cipher cipher;
|
||||
private final Mac mac;
|
||||
|
||||
private final byte[] cipherKey;
|
||||
|
||||
private final byte[] iv;
|
||||
private int counter;
|
||||
|
||||
BackupRecordInputStream(@NonNull InputStream in, @NonNull String passphrase) throws IOException {
|
||||
try {
|
||||
this.in = in;
|
||||
|
||||
byte[] headerLengthBytes = new byte[4];
|
||||
StreamUtil.readFully(in, headerLengthBytes);
|
||||
|
||||
int headerLength = Conversions.byteArrayToInt(headerLengthBytes);
|
||||
byte[] headerFrame = new byte[headerLength];
|
||||
StreamUtil.readFully(in, headerFrame);
|
||||
|
||||
BackupProtos.BackupFrame frame = BackupProtos.BackupFrame.parseFrom(headerFrame);
|
||||
|
||||
if (!frame.hasHeader()) {
|
||||
throw new IOException("Backup stream does not start with header!");
|
||||
}
|
||||
|
||||
BackupProtos.Header header = frame.getHeader();
|
||||
|
||||
this.iv = header.getIv().toByteArray();
|
||||
|
||||
if (iv.length != 16) {
|
||||
throw new IOException("Invalid IV length!");
|
||||
}
|
||||
|
||||
byte[] key = getBackupKey(passphrase, header.hasSalt() ? header.getSalt().toByteArray() : null);
|
||||
byte[] derived = HKDF.deriveSecrets(key, "Backup Export".getBytes(), 64);
|
||||
byte[][] split = ByteUtil.split(derived, 32, 32);
|
||||
|
||||
this.cipherKey = split[0];
|
||||
byte[] macKey = split[1];
|
||||
|
||||
this.cipher = Cipher.getInstance("AES/CTR/NoPadding");
|
||||
this.mac = Mac.getInstance("HmacSHA256");
|
||||
this.mac.init(new SecretKeySpec(macKey, "HmacSHA256"));
|
||||
|
||||
this.counter = Conversions.byteArrayToInt(iv);
|
||||
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
BackupProtos.BackupFrame readFrame() throws IOException {
|
||||
return readFrame(in);
|
||||
}
|
||||
|
||||
void readAttachmentTo(OutputStream out, int length) throws IOException {
|
||||
try {
|
||||
Conversions.intToByteArray(iv, 0, counter++);
|
||||
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(cipherKey, "AES"), new IvParameterSpec(iv));
|
||||
mac.update(iv);
|
||||
|
||||
byte[] buffer = new byte[8192];
|
||||
|
||||
while (length > 0) {
|
||||
int read = in.read(buffer, 0, Math.min(buffer.length, length));
|
||||
if (read == -1) throw new IOException("File ended early!");
|
||||
|
||||
mac.update(buffer, 0, read);
|
||||
|
||||
byte[] plaintext = cipher.update(buffer, 0, read);
|
||||
|
||||
if (plaintext != null) {
|
||||
out.write(plaintext, 0, plaintext.length);
|
||||
}
|
||||
|
||||
length -= read;
|
||||
}
|
||||
|
||||
byte[] plaintext = cipher.doFinal();
|
||||
|
||||
if (plaintext != null) {
|
||||
out.write(plaintext, 0, plaintext.length);
|
||||
}
|
||||
|
||||
out.close();
|
||||
|
||||
byte[] ourMac = ByteUtil.trim(mac.doFinal(), 10);
|
||||
byte[] theirMac = new byte[10];
|
||||
|
||||
try {
|
||||
StreamUtil.readFully(in, theirMac);
|
||||
} catch (IOException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
|
||||
if (!MessageDigest.isEqual(ourMac, theirMac)) {
|
||||
throw new BadMacException();
|
||||
}
|
||||
} catch (InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private BackupProtos.BackupFrame readFrame(InputStream in) throws IOException {
|
||||
try {
|
||||
byte[] length = new byte[4];
|
||||
StreamUtil.readFully(in, length);
|
||||
|
||||
byte[] frame = new byte[Conversions.byteArrayToInt(length)];
|
||||
StreamUtil.readFully(in, frame);
|
||||
|
||||
byte[] theirMac = new byte[10];
|
||||
System.arraycopy(frame, frame.length - 10, theirMac, 0, theirMac.length);
|
||||
|
||||
mac.update(frame, 0, frame.length - 10);
|
||||
byte[] ourMac = ByteUtil.trim(mac.doFinal(), 10);
|
||||
|
||||
if (!MessageDigest.isEqual(ourMac, theirMac)) {
|
||||
throw new IOException("Bad MAC");
|
||||
}
|
||||
|
||||
Conversions.intToByteArray(iv, 0, counter++);
|
||||
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(cipherKey, "AES"), new IvParameterSpec(iv));
|
||||
|
||||
byte[] plaintext = cipher.doFinal(frame, 0, frame.length - 10);
|
||||
|
||||
return BackupProtos.BackupFrame.parseFrom(plaintext);
|
||||
} catch (InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
static class BadMacException extends IOException {}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package org.thoughtcrime.securesms.backup
|
||||
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.backup.BackupProtos.BackupFrame
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
|
||||
/**
|
||||
* Given a backup file, run over it and verify it will decrypt properly when attempting to import it.
|
||||
*/
|
||||
object BackupVerifier {
|
||||
|
||||
private val TAG = Log.tag(BackupVerifier::class.java)
|
||||
|
||||
@JvmStatic
|
||||
fun verifyFile(cipherStream: InputStream, passphrase: String, expectedCount: Long): Boolean {
|
||||
val inputStream = BackupRecordInputStream(cipherStream, passphrase)
|
||||
|
||||
var count = 0L
|
||||
var frame: BackupFrame = inputStream.readFrame()
|
||||
|
||||
while (!frame.end) {
|
||||
val verified = when {
|
||||
frame.hasAttachment() -> verifyAttachment(frame.attachment, inputStream)
|
||||
frame.hasSticker() -> verifySticker(frame.sticker, inputStream)
|
||||
frame.hasAvatar() -> verifyAvatar(frame.avatar, inputStream)
|
||||
else -> true
|
||||
}
|
||||
|
||||
if (!verified) {
|
||||
return false
|
||||
}
|
||||
|
||||
EventBus.getDefault().post(BackupEvent(BackupEvent.Type.PROGRESS_VERIFYING, ++count, expectedCount))
|
||||
|
||||
frame = inputStream.readFrame()
|
||||
}
|
||||
|
||||
cipherStream.close()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private fun verifyAttachment(attachment: BackupProtos.Attachment, inputStream: BackupRecordInputStream): Boolean {
|
||||
try {
|
||||
inputStream.readAttachmentTo(NullOutputStream, attachment.length)
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, "Bad attachment: ${attachment.attachmentId}", e)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private fun verifySticker(sticker: BackupProtos.Sticker, inputStream: BackupRecordInputStream): Boolean {
|
||||
try {
|
||||
inputStream.readAttachmentTo(NullOutputStream, sticker.length)
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, "Bad sticker: ${sticker.rowId}", e)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun verifyAvatar(avatar: BackupProtos.Avatar, inputStream: BackupRecordInputStream): Boolean {
|
||||
try {
|
||||
inputStream.readAttachmentTo(NullOutputStream, avatar.length)
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, "Bad sticker: ${avatar.recipientId}", e)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private object NullOutputStream : OutputStream() {
|
||||
override fun write(b: Int) = Unit
|
||||
override fun write(b: ByteArray?) = Unit
|
||||
override fun write(b: ByteArray?, off: Int, len: Int) = Unit
|
||||
}
|
||||
}
|
||||
@@ -17,8 +17,6 @@ public abstract class FullBackupBase {
|
||||
static class BackupStream {
|
||||
static @NonNull byte[] getBackupKey(@NonNull String passphrase, @Nullable byte[] salt) {
|
||||
try {
|
||||
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, 0, 0));
|
||||
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-512");
|
||||
byte[] input = passphrase.replace(" ", "").getBytes();
|
||||
byte[] hash = input;
|
||||
@@ -26,7 +24,6 @@ public abstract class FullBackupBase {
|
||||
if (salt != null) digest.update(salt);
|
||||
|
||||
for (int i = 0; i < DIGEST_ROUNDS; i++) {
|
||||
if (i % 1000 == 0) EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, 0, 0));
|
||||
digest.update(hash);
|
||||
hash = digest.digest(input);
|
||||
}
|
||||
@@ -37,42 +34,4 @@ public abstract class FullBackupBase {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class BackupEvent {
|
||||
public enum Type {
|
||||
PROGRESS,
|
||||
FINISHED
|
||||
}
|
||||
|
||||
private final Type type;
|
||||
private final long count;
|
||||
private final long estimatedTotalCount;
|
||||
|
||||
BackupEvent(Type type, long count, long estimatedTotalCount) {
|
||||
this.type = type;
|
||||
this.count = count;
|
||||
this.estimatedTotalCount = estimatedTotalCount;
|
||||
}
|
||||
|
||||
public Type getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public long getCount() {
|
||||
return count;
|
||||
}
|
||||
|
||||
public long getEstimatedTotalCount() {
|
||||
return estimatedTotalCount;
|
||||
}
|
||||
|
||||
public double getCompletionPercentage() {
|
||||
if (estimatedTotalCount == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Math.min(99.9f, (double) count * 100L / (double) estimatedTotalCount);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ public class FullBackupExporter extends FullBackupBase {
|
||||
AvatarPickerDatabase.TABLE_NAME
|
||||
);
|
||||
|
||||
public static void export(@NonNull Context context,
|
||||
public static BackupEvent export(@NonNull Context context,
|
||||
@NonNull AttachmentSecret attachmentSecret,
|
||||
@NonNull SQLiteDatabase input,
|
||||
@NonNull File output,
|
||||
@@ -105,12 +105,12 @@ public class FullBackupExporter extends FullBackupBase {
|
||||
throws IOException
|
||||
{
|
||||
try (OutputStream outputStream = new FileOutputStream(output)) {
|
||||
internalExport(context, attachmentSecret, input, outputStream, passphrase, true, cancellationSignal);
|
||||
return internalExport(context, attachmentSecret, input, outputStream, passphrase, true, cancellationSignal);
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(29)
|
||||
public static void export(@NonNull Context context,
|
||||
public static BackupEvent export(@NonNull Context context,
|
||||
@NonNull AttachmentSecret attachmentSecret,
|
||||
@NonNull SQLiteDatabase input,
|
||||
@NonNull DocumentFile output,
|
||||
@@ -119,7 +119,7 @@ public class FullBackupExporter extends FullBackupBase {
|
||||
throws IOException
|
||||
{
|
||||
try (OutputStream outputStream = Objects.requireNonNull(context.getContentResolver().openOutputStream(output.getUri()))) {
|
||||
internalExport(context, attachmentSecret, input, outputStream, passphrase, true, cancellationSignal);
|
||||
return internalExport(context, attachmentSecret, input, outputStream, passphrase, true, cancellationSignal);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,16 +130,16 @@ public class FullBackupExporter extends FullBackupBase {
|
||||
@NonNull String passphrase)
|
||||
throws IOException
|
||||
{
|
||||
internalExport(context, attachmentSecret, input, outputStream, passphrase, false, () -> false);
|
||||
EventBus.getDefault().post(internalExport(context, attachmentSecret, input, outputStream, passphrase, false, () -> false));
|
||||
}
|
||||
|
||||
private static void internalExport(@NonNull Context context,
|
||||
@NonNull AttachmentSecret attachmentSecret,
|
||||
@NonNull SQLiteDatabase input,
|
||||
@NonNull OutputStream fileOutputStream,
|
||||
@NonNull String passphrase,
|
||||
boolean closeOutputStream,
|
||||
@NonNull BackupCancellationSignal cancellationSignal)
|
||||
private static BackupEvent internalExport(@NonNull Context context,
|
||||
@NonNull AttachmentSecret attachmentSecret,
|
||||
@NonNull SQLiteDatabase input,
|
||||
@NonNull OutputStream fileOutputStream,
|
||||
@NonNull String passphrase,
|
||||
boolean closeOutputStream,
|
||||
@NonNull BackupCancellationSignal cancellationSignal)
|
||||
throws IOException
|
||||
{
|
||||
BackupFrameOutputStream outputStream = new BackupFrameOutputStream(fileOutputStream, passphrase);
|
||||
@@ -210,8 +210,8 @@ public class FullBackupExporter extends FullBackupBase {
|
||||
if (closeOutputStream) {
|
||||
outputStream.close();
|
||||
}
|
||||
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.FINISHED, ++count, estimatedCountOutside));
|
||||
}
|
||||
return new BackupEvent(BackupEvent.Type.FINISHED, ++count, estimatedCountOutside);
|
||||
}
|
||||
|
||||
private static long calculateCount(@NonNull Context context, @NonNull SQLiteDatabase input, List<String> tables) {
|
||||
|
||||
@@ -14,11 +14,8 @@ import androidx.annotation.NonNull;
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.signal.core.util.Conversions;
|
||||
import org.signal.core.util.StreamUtil;
|
||||
import org.signal.core.util.SqlUtil;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.libsignal.protocol.kdf.HKDFv3;
|
||||
import org.signal.libsignal.protocol.util.ByteUtil;
|
||||
import org.thoughtcrime.securesms.backup.BackupProtos.Attachment;
|
||||
import org.thoughtcrime.securesms.backup.BackupProtos.BackupFrame;
|
||||
import org.thoughtcrime.securesms.backup.BackupProtos.DatabaseVersion;
|
||||
@@ -38,7 +35,6 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.profiles.AvatarHelper;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.BackupUtil;
|
||||
import org.signal.core.util.SqlUtil;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
@@ -46,24 +42,12 @@ 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.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
public class FullBackupImporter extends FullBackupBase {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@@ -185,7 +169,7 @@ public class FullBackupImporter extends FullBackupBase {
|
||||
|
||||
contentValues.put(AttachmentDatabase.DATA, dataFile.getAbsolutePath());
|
||||
contentValues.put(AttachmentDatabase.DATA_RANDOM, output.first);
|
||||
} catch (BadMacException e) {
|
||||
} catch (BackupRecordInputStream.BadMacException e) {
|
||||
Log.w(TAG, "Bad MAC for attachment " + attachment.getAttachmentId() + "! Can't restore it.", e);
|
||||
dataFile.delete();
|
||||
contentValues.put(AttachmentDatabase.DATA, (String) null);
|
||||
@@ -301,144 +285,6 @@ public class FullBackupImporter extends FullBackupBase {
|
||||
}
|
||||
}
|
||||
|
||||
private static class BackupRecordInputStream extends BackupStream {
|
||||
|
||||
private final InputStream in;
|
||||
private final Cipher cipher;
|
||||
private final Mac mac;
|
||||
|
||||
private final byte[] cipherKey;
|
||||
private final byte[] macKey;
|
||||
|
||||
private byte[] iv;
|
||||
private int counter;
|
||||
|
||||
private BackupRecordInputStream(@NonNull InputStream in, @NonNull String passphrase) throws IOException {
|
||||
try {
|
||||
this.in = in;
|
||||
|
||||
byte[] headerLengthBytes = new byte[4];
|
||||
StreamUtil.readFully(in, headerLengthBytes);
|
||||
|
||||
int headerLength = Conversions.byteArrayToInt(headerLengthBytes);
|
||||
byte[] headerFrame = new byte[headerLength];
|
||||
StreamUtil.readFully(in, headerFrame);
|
||||
|
||||
BackupFrame frame = BackupFrame.parseFrom(headerFrame);
|
||||
|
||||
if (!frame.hasHeader()) {
|
||||
throw new IOException("Backup stream does not start with header!");
|
||||
}
|
||||
|
||||
BackupProtos.Header header = frame.getHeader();
|
||||
|
||||
this.iv = header.getIv().toByteArray();
|
||||
|
||||
if (iv.length != 16) {
|
||||
throw new IOException("Invalid IV length!");
|
||||
}
|
||||
|
||||
byte[] key = getBackupKey(passphrase, header.hasSalt() ? header.getSalt().toByteArray() : null);
|
||||
byte[] derived = new HKDFv3().deriveSecrets(key, "Backup Export".getBytes(), 64);
|
||||
byte[][] split = ByteUtil.split(derived, 32, 32);
|
||||
|
||||
this.cipherKey = split[0];
|
||||
this.macKey = split[1];
|
||||
|
||||
this.cipher = Cipher.getInstance("AES/CTR/NoPadding");
|
||||
this.mac = Mac.getInstance("HmacSHA256");
|
||||
this.mac.init(new SecretKeySpec(macKey, "HmacSHA256"));
|
||||
|
||||
this.counter = Conversions.byteArrayToInt(iv);
|
||||
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
BackupFrame readFrame() throws IOException {
|
||||
return readFrame(in);
|
||||
}
|
||||
|
||||
void readAttachmentTo(OutputStream out, int length) throws IOException {
|
||||
try {
|
||||
Conversions.intToByteArray(iv, 0, counter++);
|
||||
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(cipherKey, "AES"), new IvParameterSpec(iv));
|
||||
mac.update(iv);
|
||||
|
||||
byte[] buffer = new byte[8192];
|
||||
|
||||
while (length > 0) {
|
||||
int read = in.read(buffer, 0, Math.min(buffer.length, length));
|
||||
if (read == -1) throw new IOException("File ended early!");
|
||||
|
||||
mac.update(buffer, 0, read);
|
||||
|
||||
byte[] plaintext = cipher.update(buffer, 0, read);
|
||||
|
||||
if (plaintext != null) {
|
||||
out.write(plaintext, 0, plaintext.length);
|
||||
}
|
||||
|
||||
length -= read;
|
||||
}
|
||||
|
||||
byte[] plaintext = cipher.doFinal();
|
||||
|
||||
if (plaintext != null) {
|
||||
out.write(plaintext, 0, plaintext.length);
|
||||
}
|
||||
|
||||
out.close();
|
||||
|
||||
byte[] ourMac = ByteUtil.trim(mac.doFinal(), 10);
|
||||
byte[] theirMac = new byte[10];
|
||||
|
||||
try {
|
||||
StreamUtil.readFully(in, theirMac);
|
||||
} catch (IOException e) {
|
||||
throw new IOException(e);
|
||||
}
|
||||
|
||||
if (!MessageDigest.isEqual(ourMac, theirMac)) {
|
||||
throw new BadMacException();
|
||||
}
|
||||
} catch (InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private BackupFrame readFrame(InputStream in) throws IOException {
|
||||
try {
|
||||
byte[] length = new byte[4];
|
||||
StreamUtil.readFully(in, length);
|
||||
|
||||
byte[] frame = new byte[Conversions.byteArrayToInt(length)];
|
||||
StreamUtil.readFully(in, frame);
|
||||
|
||||
byte[] theirMac = new byte[10];
|
||||
System.arraycopy(frame, frame.length - 10, theirMac, 0, theirMac.length);
|
||||
|
||||
mac.update(frame, 0, frame.length - 10);
|
||||
byte[] ourMac = ByteUtil.trim(mac.doFinal(), 10);
|
||||
|
||||
if (!MessageDigest.isEqual(ourMac, theirMac)) {
|
||||
throw new IOException("Bad MAC");
|
||||
}
|
||||
|
||||
Conversions.intToByteArray(iv, 0, counter++);
|
||||
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(cipherKey, "AES"), new IvParameterSpec(iv));
|
||||
|
||||
byte[] plaintext = cipher.doFinal(frame, 0, frame.length - 10);
|
||||
|
||||
return BackupFrame.parseFrom(plaintext);
|
||||
} catch (InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class BadMacException extends IOException {}
|
||||
|
||||
public static class DatabaseDowngradeException extends IOException {
|
||||
DatabaseDowngradeException(int currentVersion, int backupVersion) {
|
||||
super("Tried to import a backup with version " + backupVersion + " into a database with version " + currentVersion);
|
||||
|
||||
Reference in New Issue
Block a user