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);