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

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

View File

@ -3388,6 +3388,16 @@ public final class BackupProtos {
* <code>optional bytes iv = 1;</code>
*/
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}
@ -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_;
/**
* <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() {
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;
/**
* <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)
}
@ -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

View File

@ -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,9 +12,11 @@ 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) {
static class BackupStream {
static @NonNull byte[] getBackupKey(@NonNull String passphrase, @Nullable byte[] salt) {
try {
EventBus.getDefault().post(new BackupEvent(BackupEvent.Type.PROGRESS, 0));
@ -22,19 +24,19 @@ public abstract class FullBackupBase {
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);
if (salt != null) digest.update(salt);
hash = digest.digest(input);
}
Log.w(TAG, "Generated: " + (System.currentTimeMillis()- start));
return ByteUtil.trim(hash, 32);
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
}
}
public static class BackupEvent {
public enum Type {

View File

@ -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<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 Cipher cipher;
@ -208,8 +207,10 @@ 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[] 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);
@ -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);

View File

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