Add salt to backup header

This commit is contained in:
Moxie Marlinspike 2018-03-14 10:28:41 -07:00
parent f544b7d7b4
commit 90006e81db
5 changed files with 140 additions and 51 deletions

View File

@ -39,7 +39,8 @@ message DatabaseVersion {
} }
message Header { message Header {
optional bytes iv = 1; optional bytes iv = 1;
optional bytes salt = 2;
} }
message BackupFrame { message BackupFrame {

View File

@ -3388,6 +3388,16 @@ public final class BackupProtos {
* <code>optional bytes iv = 1;</code> * <code>optional bytes iv = 1;</code>
*/ */
com.google.protobuf.ByteString getIv(); com.google.protobuf.ByteString getIv();
// optional bytes salt = 2;
/**
* <code>optional bytes salt = 2;</code>
*/
boolean hasSalt();
/**
* <code>optional bytes salt = 2;</code>
*/
com.google.protobuf.ByteString getSalt();
} }
/** /**
* Protobuf type {@code signal.Header} * Protobuf type {@code signal.Header}
@ -3445,6 +3455,11 @@ public final class BackupProtos {
iv_ = input.readBytes(); iv_ = input.readBytes();
break; break;
} }
case 18: {
bitField0_ |= 0x00000002;
salt_ = input.readBytes();
break;
}
} }
} }
} catch (com.google.protobuf.InvalidProtocolBufferException e) { } catch (com.google.protobuf.InvalidProtocolBufferException e) {
@ -3501,8 +3516,25 @@ public final class BackupProtos {
return iv_; return iv_;
} }
// optional bytes salt = 2;
public static final int SALT_FIELD_NUMBER = 2;
private com.google.protobuf.ByteString salt_;
/**
* <code>optional bytes salt = 2;</code>
*/
public boolean hasSalt() {
return ((bitField0_ & 0x00000002) == 0x00000002);
}
/**
* <code>optional bytes salt = 2;</code>
*/
public com.google.protobuf.ByteString getSalt() {
return salt_;
}
private void initFields() { private void initFields() {
iv_ = com.google.protobuf.ByteString.EMPTY; iv_ = com.google.protobuf.ByteString.EMPTY;
salt_ = com.google.protobuf.ByteString.EMPTY;
} }
private byte memoizedIsInitialized = -1; private byte memoizedIsInitialized = -1;
public final boolean isInitialized() { public final boolean isInitialized() {
@ -3519,6 +3551,9 @@ public final class BackupProtos {
if (((bitField0_ & 0x00000001) == 0x00000001)) { if (((bitField0_ & 0x00000001) == 0x00000001)) {
output.writeBytes(1, iv_); output.writeBytes(1, iv_);
} }
if (((bitField0_ & 0x00000002) == 0x00000002)) {
output.writeBytes(2, salt_);
}
getUnknownFields().writeTo(output); getUnknownFields().writeTo(output);
} }
@ -3532,6 +3567,10 @@ public final class BackupProtos {
size += com.google.protobuf.CodedOutputStream size += com.google.protobuf.CodedOutputStream
.computeBytesSize(1, iv_); .computeBytesSize(1, iv_);
} }
if (((bitField0_ & 0x00000002) == 0x00000002)) {
size += com.google.protobuf.CodedOutputStream
.computeBytesSize(2, salt_);
}
size += getUnknownFields().getSerializedSize(); size += getUnknownFields().getSerializedSize();
memoizedSerializedSize = size; memoizedSerializedSize = size;
return size; return size;
@ -3650,6 +3689,8 @@ public final class BackupProtos {
super.clear(); super.clear();
iv_ = com.google.protobuf.ByteString.EMPTY; iv_ = com.google.protobuf.ByteString.EMPTY;
bitField0_ = (bitField0_ & ~0x00000001); bitField0_ = (bitField0_ & ~0x00000001);
salt_ = com.google.protobuf.ByteString.EMPTY;
bitField0_ = (bitField0_ & ~0x00000002);
return this; return this;
} }
@ -3682,6 +3723,10 @@ public final class BackupProtos {
to_bitField0_ |= 0x00000001; to_bitField0_ |= 0x00000001;
} }
result.iv_ = iv_; result.iv_ = iv_;
if (((from_bitField0_ & 0x00000002) == 0x00000002)) {
to_bitField0_ |= 0x00000002;
}
result.salt_ = salt_;
result.bitField0_ = to_bitField0_; result.bitField0_ = to_bitField0_;
onBuilt(); onBuilt();
return result; return result;
@ -3701,6 +3746,9 @@ public final class BackupProtos {
if (other.hasIv()) { if (other.hasIv()) {
setIv(other.getIv()); setIv(other.getIv());
} }
if (other.hasSalt()) {
setSalt(other.getSalt());
}
this.mergeUnknownFields(other.getUnknownFields()); this.mergeUnknownFields(other.getUnknownFields());
return this; return this;
} }
@ -3764,6 +3812,42 @@ public final class BackupProtos {
return this; return this;
} }
// optional bytes salt = 2;
private com.google.protobuf.ByteString salt_ = com.google.protobuf.ByteString.EMPTY;
/**
* <code>optional bytes salt = 2;</code>
*/
public boolean hasSalt() {
return ((bitField0_ & 0x00000002) == 0x00000002);
}
/**
* <code>optional bytes salt = 2;</code>
*/
public com.google.protobuf.ByteString getSalt() {
return salt_;
}
/**
* <code>optional bytes salt = 2;</code>
*/
public Builder setSalt(com.google.protobuf.ByteString value) {
if (value == null) {
throw new NullPointerException();
}
bitField0_ |= 0x00000002;
salt_ = value;
onChanged();
return this;
}
/**
* <code>optional bytes salt = 2;</code>
*/
public Builder clearSalt() {
bitField0_ = (bitField0_ & ~0x00000002);
salt_ = getDefaultInstance().getSalt();
onChanged();
return this;
}
// @@protoc_insertion_point(builder_scope:signal.Header) // @@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" + "\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" + "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", "\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\"" + "\022\017\n\007version\030\001 \001(\r\"\"\n\006Header\022\n\n\002iv\030\001 \001(\014\022" +
"\343\001\n\013BackupFrame\022\036\n\006header\030\001 \001(\0132\016.signal" + "\014\n\004salt\030\002 \001(\014\"\343\001\n\013BackupFrame\022\036\n\006header\030" +
".Header\022\'\n\tstatement\030\002 \001(\0132\024.signal.SqlS" + "\001 \001(\0132\016.signal.Header\022\'\n\tstatement\030\002 \001(\013" +
"tatement\022,\n\npreference\030\003 \001(\0132\030.signal.Sh" + "2\024.signal.SqlStatement\022,\n\npreference\030\003 \001" +
"aredPreference\022&\n\nattachment\030\004 \001(\0132\022.sig" + "(\0132\030.signal.SharedPreference\022&\n\nattachme" +
"nal.Attachment\022(\n\007version\030\005 \001(\0132\027.signal" + "nt\030\004 \001(\0132\022.signal.Attachment\022(\n\007version\030" +
".DatabaseVersion\022\013\n\003end\030\006 \001(\010B1\n!org.tho" + "\005 \001(\0132\027.signal.DatabaseVersion\022\013\n\003end\030\006 " +
"ughtcrime.securesms.backupB\014BackupProtos" "\001(\010B1\n!org.thoughtcrime.securesms.backup" +
"B\014BackupProtos"
}; };
com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner = com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner =
new com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner() { new com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner() {
@ -5234,7 +5319,7 @@ public final class BackupProtos {
internal_static_signal_Header_fieldAccessorTable = new internal_static_signal_Header_fieldAccessorTable = new
com.google.protobuf.GeneratedMessage.FieldAccessorTable( com.google.protobuf.GeneratedMessage.FieldAccessorTable(
internal_static_signal_Header_descriptor, internal_static_signal_Header_descriptor,
new java.lang.String[] { "Iv", }); new java.lang.String[] { "Iv", "Salt", });
internal_static_signal_BackupFrame_descriptor = internal_static_signal_BackupFrame_descriptor =
getDescriptor().getMessageTypes().get(5); getDescriptor().getMessageTypes().get(5);
internal_static_signal_BackupFrame_fieldAccessorTable = new internal_static_signal_BackupFrame_fieldAccessorTable = new

View File

@ -2,7 +2,7 @@ package org.thoughtcrime.securesms.backup;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.util.Log; import android.support.annotation.Nullable;
import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.EventBus;
import org.whispersystems.libsignal.util.ByteUtil; import org.whispersystems.libsignal.util.ByteUtil;
@ -12,27 +12,29 @@ import java.security.NoSuchAlgorithmException;
public abstract class FullBackupBase { public abstract class FullBackupBase {
@SuppressWarnings("unused")
private static final String TAG = FullBackupBase.class.getSimpleName(); private static final String TAG = FullBackupBase.class.getSimpleName();
protected static @NonNull byte[] getBackupKey(@NonNull String passphrase) { static class BackupStream {
try { static @NonNull byte[] getBackupKey(@NonNull String passphrase, @Nullable byte[] salt) {
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, 0)); try {
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, 0));
MessageDigest digest = MessageDigest.getInstance("SHA-512"); MessageDigest digest = MessageDigest.getInstance("SHA-512");
byte[] input = passphrase.replace(" ", "").getBytes(); byte[] input = passphrase.replace(" ", "").getBytes();
byte[] hash = input; byte[] hash = input;
long start = System.currentTimeMillis(); for (int i=0;i<250000;i++) {
for (int i=0;i<250000;i++) { if (i % 1000 == 0) EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, 0));
if (i % 1000 == 0) EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, 0)); digest.update(hash);
digest.update(hash); if (salt != null) digest.update(salt);
hash = digest.digest(input); 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);
} }
} }

View File

@ -63,8 +63,7 @@ public class FullBackupExporter extends FullBackupBase {
@NonNull String passphrase) @NonNull String passphrase)
throws IOException throws IOException
{ {
byte[] key = getBackupKey(passphrase); BackupFrameOutputStream outputStream = new BackupFrameOutputStream(output, passphrase);
BackupFrameOutputStream outputStream = new BackupFrameOutputStream(output, key);
outputStream.writeDatabaseVersion(input.getVersion()); outputStream.writeDatabaseVersion(input.getVersion());
List<String> tables = exportSchema(input, outputStream); List<String> 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 OutputStream outputStream;
private final Cipher cipher; private final Cipher cipher;
@ -208,10 +207,12 @@ public class FullBackupExporter extends FullBackupBase {
private byte[] iv; private byte[] iv;
private int counter; private int counter;
private BackupFrameOutputStream(@NonNull File output, @NonNull byte[] key) throws IOException { private BackupFrameOutputStream(@NonNull File output, @NonNull String passphrase) throws IOException {
try { try {
byte[] derived = new HKDFv3().deriveSecrets(key, "Backup Export".getBytes(), 64); byte[] salt = Util.getSecretBytes(32);
byte[][] split = ByteUtil.split(derived, 32, 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.cipherKey = split[0];
this.macKey = split[1]; this.macKey = split[1];
@ -224,7 +225,10 @@ public class FullBackupExporter extends FullBackupBase {
mac.init(new SecretKeySpec(macKey, "HmacSHA256")); 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(Conversions.intToByteArray(header.length));
outputStream.write(header); outputStream.write(header);

View File

@ -6,12 +6,8 @@ import android.content.ContentValues;
import android.content.Context; import android.content.Context;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.util.Log;
import android.util.Pair; import android.util.Pair;
import com.annimon.stream.Collectors;
import com.annimon.stream.Stream;
import net.sqlcipher.database.SQLiteDatabase; import net.sqlcipher.database.SQLiteDatabase;
import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.EventBus;
@ -57,8 +53,7 @@ public class FullBackupImporter extends FullBackupBase {
@NonNull SQLiteDatabase db, @NonNull File file, @NonNull String passphrase) @NonNull SQLiteDatabase db, @NonNull File file, @NonNull String passphrase)
throws IOException throws IOException
{ {
byte[] key = getBackupKey(passphrase); BackupRecordInputStream inputStream = new BackupRecordInputStream(file, passphrase);
BackupRecordInputStream inputStream = new BackupRecordInputStream(file, key);
int count = 0; int count = 0;
try { try {
@ -128,7 +123,7 @@ public class FullBackupImporter extends FullBackupBase {
preferences.edit().putString(preference.getKey(), preference.getValue()).commit(); preferences.edit().putString(preference.getKey(), preference.getValue()).commit();
} }
private static class BackupRecordInputStream { private static class BackupRecordInputStream extends BackupStream {
private final InputStream in; private final InputStream in;
private final Cipher cipher; private final Cipher cipher;
@ -140,18 +135,9 @@ public class FullBackupImporter extends FullBackupBase {
private byte[] iv; private byte[] iv;
private int counter; private int counter;
private BackupRecordInputStream(@NonNull File file, @NonNull byte[] key) throws IOException { private BackupRecordInputStream(@NonNull File file, @NonNull String passphrase) throws IOException {
try { 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.in = new FileInputStream(file);
this.mac.init(new SecretKeySpec(macKey, "HmacSHA256"));
byte[] headerLengthBytes = new byte[4]; byte[] headerLengthBytes = new byte[4];
Util.readFully(in, headerLengthBytes); Util.readFully(in, headerLengthBytes);
@ -174,6 +160,17 @@ public class FullBackupImporter extends FullBackupBase {
throw new IOException("Invalid IV length!"); 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); this.counter = Conversions.byteArrayToInt(iv);
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException e) { } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException e) {
throw new AssertionError(e); throw new AssertionError(e);