diff --git a/protobuf/Backups.proto b/protobuf/Backups.proto index 4abd01befc..df62b5ae40 100644 --- a/protobuf/Backups.proto +++ b/protobuf/Backups.proto @@ -39,7 +39,8 @@ message DatabaseVersion { } message Header { - optional bytes iv = 1; + optional bytes iv = 1; + optional bytes salt = 2; } message BackupFrame { diff --git a/src/org/thoughtcrime/securesms/backup/BackupProtos.java b/src/org/thoughtcrime/securesms/backup/BackupProtos.java index 1bdcd5493e..b0a418338b 100644 --- a/src/org/thoughtcrime/securesms/backup/BackupProtos.java +++ b/src/org/thoughtcrime/securesms/backup/BackupProtos.java @@ -3388,6 +3388,16 @@ public final class BackupProtos { * optional bytes iv = 1; */ com.google.protobuf.ByteString getIv(); + + // optional bytes salt = 2; + /** + * optional bytes salt = 2; + */ + boolean hasSalt(); + /** + * optional bytes salt = 2; + */ + com.google.protobuf.ByteString getSalt(); } /** * Protobuf type {@code signal.Header} @@ -3445,6 +3455,11 @@ public final class BackupProtos { iv_ = input.readBytes(); break; } + case 18: { + bitField0_ |= 0x00000002; + salt_ = input.readBytes(); + break; + } } } } catch (com.google.protobuf.InvalidProtocolBufferException e) { @@ -3501,8 +3516,25 @@ public final class BackupProtos { return iv_; } + // optional bytes salt = 2; + public static final int SALT_FIELD_NUMBER = 2; + private com.google.protobuf.ByteString salt_; + /** + * optional bytes salt = 2; + */ + public boolean hasSalt() { + return ((bitField0_ & 0x00000002) == 0x00000002); + } + /** + * optional bytes salt = 2; + */ + public com.google.protobuf.ByteString getSalt() { + return salt_; + } + private void initFields() { iv_ = com.google.protobuf.ByteString.EMPTY; + salt_ = com.google.protobuf.ByteString.EMPTY; } private byte memoizedIsInitialized = -1; public final boolean isInitialized() { @@ -3519,6 +3551,9 @@ public final class BackupProtos { if (((bitField0_ & 0x00000001) == 0x00000001)) { output.writeBytes(1, iv_); } + if (((bitField0_ & 0x00000002) == 0x00000002)) { + output.writeBytes(2, salt_); + } getUnknownFields().writeTo(output); } @@ -3532,6 +3567,10 @@ public final class BackupProtos { size += com.google.protobuf.CodedOutputStream .computeBytesSize(1, iv_); } + if (((bitField0_ & 0x00000002) == 0x00000002)) { + size += com.google.protobuf.CodedOutputStream + .computeBytesSize(2, salt_); + } size += getUnknownFields().getSerializedSize(); memoizedSerializedSize = size; return size; @@ -3650,6 +3689,8 @@ public final class BackupProtos { super.clear(); iv_ = com.google.protobuf.ByteString.EMPTY; bitField0_ = (bitField0_ & ~0x00000001); + salt_ = com.google.protobuf.ByteString.EMPTY; + bitField0_ = (bitField0_ & ~0x00000002); return this; } @@ -3682,6 +3723,10 @@ public final class BackupProtos { to_bitField0_ |= 0x00000001; } result.iv_ = iv_; + if (((from_bitField0_ & 0x00000002) == 0x00000002)) { + to_bitField0_ |= 0x00000002; + } + result.salt_ = salt_; result.bitField0_ = to_bitField0_; onBuilt(); return result; @@ -3701,6 +3746,9 @@ public final class BackupProtos { if (other.hasIv()) { setIv(other.getIv()); } + if (other.hasSalt()) { + setSalt(other.getSalt()); + } this.mergeUnknownFields(other.getUnknownFields()); return this; } @@ -3764,6 +3812,42 @@ public final class BackupProtos { return this; } + // optional bytes salt = 2; + private com.google.protobuf.ByteString salt_ = com.google.protobuf.ByteString.EMPTY; + /** + * optional bytes salt = 2; + */ + public boolean hasSalt() { + return ((bitField0_ & 0x00000002) == 0x00000002); + } + /** + * optional bytes salt = 2; + */ + public com.google.protobuf.ByteString getSalt() { + return salt_; + } + /** + * optional bytes salt = 2; + */ + public Builder setSalt(com.google.protobuf.ByteString value) { + if (value == null) { + throw new NullPointerException(); + } + bitField0_ |= 0x00000002; + salt_ = value; + onChanged(); + return this; + } + /** + * optional bytes salt = 2; + */ + public Builder clearSalt() { + bitField0_ = (bitField0_ & ~0x00000002); + salt_ = getDefaultInstance().getSalt(); + onChanged(); + return this; + } + // @@protoc_insertion_point(builder_scope:signal.Header) } @@ -5185,14 +5269,15 @@ public final class BackupProtos { "\030\001 \001(\t\022\013\n\003key\030\002 \001(\t\022\r\n\005value\030\003 \001(\t\"A\n\nAt" + "tachment\022\r\n\005rowId\030\001 \001(\004\022\024\n\014attachmentId\030" + "\002 \001(\004\022\016\n\006length\030\003 \001(\r\"\"\n\017DatabaseVersion", - "\022\017\n\007version\030\001 \001(\r\"\024\n\006Header\022\n\n\002iv\030\001 \001(\014\"" + - "\343\001\n\013BackupFrame\022\036\n\006header\030\001 \001(\0132\016.signal" + - ".Header\022\'\n\tstatement\030\002 \001(\0132\024.signal.SqlS" + - "tatement\022,\n\npreference\030\003 \001(\0132\030.signal.Sh" + - "aredPreference\022&\n\nattachment\030\004 \001(\0132\022.sig" + - "nal.Attachment\022(\n\007version\030\005 \001(\0132\027.signal" + - ".DatabaseVersion\022\013\n\003end\030\006 \001(\010B1\n!org.tho" + - "ughtcrime.securesms.backupB\014BackupProtos" + "\022\017\n\007version\030\001 \001(\r\"\"\n\006Header\022\n\n\002iv\030\001 \001(\014\022" + + "\014\n\004salt\030\002 \001(\014\"\343\001\n\013BackupFrame\022\036\n\006header\030" + + "\001 \001(\0132\016.signal.Header\022\'\n\tstatement\030\002 \001(\013" + + "2\024.signal.SqlStatement\022,\n\npreference\030\003 \001" + + "(\0132\030.signal.SharedPreference\022&\n\nattachme" + + "nt\030\004 \001(\0132\022.signal.Attachment\022(\n\007version\030" + + "\005 \001(\0132\027.signal.DatabaseVersion\022\013\n\003end\030\006 " + + "\001(\010B1\n!org.thoughtcrime.securesms.backup" + + "B\014BackupProtos" }; com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner = new com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner() { @@ -5234,7 +5319,7 @@ public final class BackupProtos { internal_static_signal_Header_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_signal_Header_descriptor, - new java.lang.String[] { "Iv", }); + new java.lang.String[] { "Iv", "Salt", }); internal_static_signal_BackupFrame_descriptor = getDescriptor().getMessageTypes().get(5); internal_static_signal_BackupFrame_fieldAccessorTable = new diff --git a/src/org/thoughtcrime/securesms/backup/FullBackupBase.java b/src/org/thoughtcrime/securesms/backup/FullBackupBase.java index de475436bd..c6d881fdf2 100644 --- a/src/org/thoughtcrime/securesms/backup/FullBackupBase.java +++ b/src/org/thoughtcrime/securesms/backup/FullBackupBase.java @@ -2,7 +2,7 @@ package org.thoughtcrime.securesms.backup; import android.support.annotation.NonNull; -import android.util.Log; +import android.support.annotation.Nullable; import org.greenrobot.eventbus.EventBus; import org.whispersystems.libsignal.util.ByteUtil; @@ -12,27 +12,29 @@ import java.security.NoSuchAlgorithmException; public abstract class FullBackupBase { + @SuppressWarnings("unused") private static final String TAG = FullBackupBase.class.getSimpleName(); - protected static @NonNull byte[] getBackupKey(@NonNull String passphrase) { - try { - EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, 0)); + static class BackupStream { + static @NonNull byte[] getBackupKey(@NonNull String passphrase, @Nullable byte[] salt) { + try { + EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, 0)); - MessageDigest digest = MessageDigest.getInstance("SHA-512"); - byte[] input = passphrase.replace(" ", "").getBytes(); - byte[] hash = input; + MessageDigest digest = MessageDigest.getInstance("SHA-512"); + byte[] input = passphrase.replace(" ", "").getBytes(); + byte[] hash = input; - long start = System.currentTimeMillis(); - for (int i=0;i<250000;i++) { - if (i % 1000 == 0) EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, 0)); - digest.update(hash); - hash = digest.digest(input); + for (int i=0;i<250000;i++) { + if (i % 1000 == 0) EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, 0)); + digest.update(hash); + if (salt != null) digest.update(salt); + hash = digest.digest(input); + } + + return ByteUtil.trim(hash, 32); + } catch (NoSuchAlgorithmException e) { + throw new AssertionError(e); } - Log.w(TAG, "Generated: " + (System.currentTimeMillis()- start)); - - return ByteUtil.trim(hash, 32); - } catch (NoSuchAlgorithmException e) { - throw new AssertionError(e); } } diff --git a/src/org/thoughtcrime/securesms/backup/FullBackupExporter.java b/src/org/thoughtcrime/securesms/backup/FullBackupExporter.java index bb1faaf3e4..8577b0465f 100644 --- a/src/org/thoughtcrime/securesms/backup/FullBackupExporter.java +++ b/src/org/thoughtcrime/securesms/backup/FullBackupExporter.java @@ -63,8 +63,7 @@ public class FullBackupExporter extends FullBackupBase { @NonNull String passphrase) throws IOException { - byte[] key = getBackupKey(passphrase); - BackupFrameOutputStream outputStream = new BackupFrameOutputStream(output, key); + BackupFrameOutputStream outputStream = new BackupFrameOutputStream(output, passphrase); outputStream.writeDatabaseVersion(input.getVersion()); List tables = exportSchema(input, outputStream); @@ -196,7 +195,7 @@ public class FullBackupExporter extends FullBackupBase { } } - private static class BackupFrameOutputStream { + private static class BackupFrameOutputStream extends BackupStream { private final OutputStream outputStream; private final Cipher cipher; @@ -208,10 +207,12 @@ public class FullBackupExporter extends FullBackupBase { private byte[] iv; private int counter; - private BackupFrameOutputStream(@NonNull File output, @NonNull byte[] key) throws IOException { + private BackupFrameOutputStream(@NonNull File output, @NonNull String passphrase) throws IOException { try { - byte[] derived = new HKDFv3().deriveSecrets(key, "Backup Export".getBytes(), 64); - byte[][] split = ByteUtil.split(derived, 32, 32); + byte[] salt = Util.getSecretBytes(32); + byte[] key = getBackupKey(passphrase, salt); + 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]; @@ -224,7 +225,10 @@ public class FullBackupExporter extends FullBackupBase { mac.init(new SecretKeySpec(macKey, "HmacSHA256")); - byte[] header = BackupProtos.BackupFrame.newBuilder().setHeader(BackupProtos.Header.newBuilder().setIv(ByteString.copyFrom(iv))).build().toByteArray(); + byte[] header = BackupProtos.BackupFrame.newBuilder().setHeader(BackupProtos.Header.newBuilder() + .setIv(ByteString.copyFrom(iv)) + .setSalt(ByteString.copyFrom(salt))) + .build().toByteArray(); outputStream.write(Conversions.intToByteArray(header.length)); outputStream.write(header); diff --git a/src/org/thoughtcrime/securesms/backup/FullBackupImporter.java b/src/org/thoughtcrime/securesms/backup/FullBackupImporter.java index c9864f3048..0b81eb1c58 100644 --- a/src/org/thoughtcrime/securesms/backup/FullBackupImporter.java +++ b/src/org/thoughtcrime/securesms/backup/FullBackupImporter.java @@ -6,12 +6,8 @@ import android.content.ContentValues; import android.content.Context; import android.content.SharedPreferences; import android.support.annotation.NonNull; -import android.util.Log; import android.util.Pair; -import com.annimon.stream.Collectors; -import com.annimon.stream.Stream; - import net.sqlcipher.database.SQLiteDatabase; import org.greenrobot.eventbus.EventBus; @@ -57,8 +53,7 @@ public class FullBackupImporter extends FullBackupBase { @NonNull SQLiteDatabase db, @NonNull File file, @NonNull String passphrase) throws IOException { - byte[] key = getBackupKey(passphrase); - BackupRecordInputStream inputStream = new BackupRecordInputStream(file, key); + BackupRecordInputStream inputStream = new BackupRecordInputStream(file, passphrase); int count = 0; try { @@ -128,7 +123,7 @@ public class FullBackupImporter extends FullBackupBase { preferences.edit().putString(preference.getKey(), preference.getValue()).commit(); } - private static class BackupRecordInputStream { + private static class BackupRecordInputStream extends BackupStream { private final InputStream in; private final Cipher cipher; @@ -140,18 +135,9 @@ public class FullBackupImporter extends FullBackupBase { private byte[] iv; private int counter; - private BackupRecordInputStream(@NonNull File file, @NonNull byte[] key) throws IOException { + private BackupRecordInputStream(@NonNull File file, @NonNull String passphrase) throws IOException { try { - 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.in = new FileInputStream(file); - this.mac.init(new SecretKeySpec(macKey, "HmacSHA256")); byte[] headerLengthBytes = new byte[4]; Util.readFully(in, headerLengthBytes); @@ -174,6 +160,17 @@ public class FullBackupImporter extends FullBackupBase { 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);