Migrate prekeys into database

This commit is contained in:
Moxie Marlinspike
2018-02-15 20:33:10 -08:00
parent 6239508b39
commit 9f3c04dfb5
10 changed files with 570 additions and 381 deletions

View File

@@ -1,5 +1,5 @@
/**
* Copyright (C) 2013 Open Whisper Systems
/*
* Copyright (C) 2013-2018 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -18,16 +18,11 @@
package org.thoughtcrime.securesms.crypto;
import android.content.Context;
import android.util.Log;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.thoughtcrime.securesms.crypto.storage.TextSecurePreKeyStore;
import org.thoughtcrime.securesms.util.JsonUtils;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.libsignal.IdentityKeyPair;
import org.whispersystems.libsignal.InvalidKeyException;
import org.whispersystems.libsignal.InvalidKeyIdException;
import org.whispersystems.libsignal.ecc.Curve;
import org.whispersystems.libsignal.ecc.ECKeyPair;
import org.whispersystems.libsignal.state.PreKeyRecord;
@@ -35,26 +30,21 @@ import org.whispersystems.libsignal.state.PreKeyStore;
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
import org.whispersystems.libsignal.state.SignedPreKeyStore;
import org.whispersystems.libsignal.util.Medium;
import org.whispersystems.libsignal.util.guava.Optional;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.LinkedList;
import java.util.List;
public class PreKeyUtil {
@SuppressWarnings("unused")
private static final String TAG = PreKeyUtil.class.getName();
private static final int BATCH_SIZE = 100;
public static List<PreKeyRecord> generatePreKeys(Context context) {
public synchronized static List<PreKeyRecord> generatePreKeys(Context context) {
PreKeyStore preKeyStore = new TextSecurePreKeyStore(context);
List<PreKeyRecord> records = new LinkedList<>();
int preKeyIdOffset = getNextPreKeyId(context);
int preKeyIdOffset = TextSecurePreferences.getNextPreKeyId(context);
for (int i=0;i<BATCH_SIZE;i++) {
int preKeyId = (preKeyIdOffset + i) % Medium.MAX_VALUE;
@@ -65,24 +55,24 @@ public class PreKeyUtil {
records.add(record);
}
setNextPreKeyId(context, (preKeyIdOffset + BATCH_SIZE + 1) % Medium.MAX_VALUE);
TextSecurePreferences.setNextPreKeyId(context, (preKeyIdOffset + BATCH_SIZE + 1) % Medium.MAX_VALUE);
return records;
}
public static SignedPreKeyRecord generateSignedPreKey(Context context, IdentityKeyPair identityKeyPair, boolean active)
{
public synchronized static SignedPreKeyRecord generateSignedPreKey(Context context, IdentityKeyPair identityKeyPair, boolean active) {
try {
SignedPreKeyStore signedPreKeyStore = new TextSecurePreKeyStore(context);
int signedPreKeyId = getNextSignedPreKeyId(context);
int signedPreKeyId = TextSecurePreferences.getNextSignedPreKeyId(context);
ECKeyPair keyPair = Curve.generateKeyPair();
byte[] signature = Curve.calculateSignature(identityKeyPair.getPrivateKey(), keyPair.getPublicKey().serialize());
SignedPreKeyRecord record = new SignedPreKeyRecord(signedPreKeyId, System.currentTimeMillis(), keyPair, signature);
signedPreKeyStore.storeSignedPreKey(signedPreKeyId, record);
setNextSignedPreKeyId(context, (signedPreKeyId + 1) % Medium.MAX_VALUE);
TextSecurePreferences.setNextSignedPreKeyId(context, (signedPreKeyId + 1) % Medium.MAX_VALUE);
if (active) {
setActiveSignedPreKeyId(context, signedPreKeyId);
TextSecurePreferences.setActiveSignedPreKeyId(context, signedPreKeyId);
}
return record;
@@ -91,150 +81,12 @@ public class PreKeyUtil {
}
}
private static synchronized void setNextPreKeyId(Context context, int id) {
try {
File nextFile = new File(getPreKeysDirectory(context), PreKeyIndex.FILE_NAME);
FileOutputStream fout = new FileOutputStream(nextFile);
fout.write(JsonUtils.toJson(new PreKeyIndex(id)).getBytes());
fout.close();
} catch (IOException e) {
Log.w("PreKeyUtil", e);
}
}
private static synchronized void setNextSignedPreKeyId(Context context, int id) {
try {
SignedPreKeyIndex index = getSignedPreKeyIndex(context).or(new SignedPreKeyIndex());
index.nextSignedPreKeyId = id;
setSignedPreKeyIndex(context, index);
} catch (IOException e) {
Log.w(TAG, e);
}
}
public static synchronized void setActiveSignedPreKeyId(Context context, int id) {
try {
SignedPreKeyIndex index = getSignedPreKeyIndex(context).or(new SignedPreKeyIndex());
index.activeSignedPreKeyId = id;
setSignedPreKeyIndex(context, index);
} catch (IOException e) {
Log.w(TAG, e);
}
TextSecurePreferences.setActiveSignedPreKeyId(context, id);
}
public static synchronized int getActiveSignedPreKeyId(Context context) {
Optional<SignedPreKeyIndex> index = getSignedPreKeyIndex(context);
if (index.isPresent()) return index.get().activeSignedPreKeyId;
else return -1;
return TextSecurePreferences.getActiveSignedPreKeyId(context);
}
private static synchronized int getNextPreKeyId(Context context) {
try {
File nextFile = new File(getPreKeysDirectory(context), PreKeyIndex.FILE_NAME);
if (!nextFile.exists()) {
return Util.getSecureRandom().nextInt(Medium.MAX_VALUE);
} else {
InputStreamReader reader = new InputStreamReader(new FileInputStream(nextFile));
PreKeyIndex index = JsonUtils.fromJson(reader, PreKeyIndex.class);
reader.close();
return index.nextPreKeyId;
}
} catch (IOException e) {
Log.w("PreKeyUtil", e);
return Util.getSecureRandom().nextInt(Medium.MAX_VALUE);
}
}
private static synchronized int getNextSignedPreKeyId(Context context) {
try {
File nextFile = new File(getSignedPreKeysDirectory(context), SignedPreKeyIndex.FILE_NAME);
if (!nextFile.exists()) {
return Util.getSecureRandom().nextInt(Medium.MAX_VALUE);
} else {
InputStreamReader reader = new InputStreamReader(new FileInputStream(nextFile));
SignedPreKeyIndex index = JsonUtils.fromJson(reader, SignedPreKeyIndex.class);
reader.close();
return index.nextSignedPreKeyId;
}
} catch (IOException e) {
Log.w("PreKeyUtil", e);
return Util.getSecureRandom().nextInt(Medium.MAX_VALUE);
}
}
private static synchronized Optional<SignedPreKeyIndex> getSignedPreKeyIndex(Context context) {
File indexFile = new File(getSignedPreKeysDirectory(context), SignedPreKeyIndex.FILE_NAME);
if (!indexFile.exists()) {
return Optional.absent();
}
try {
InputStreamReader reader = new InputStreamReader(new FileInputStream(indexFile));
SignedPreKeyIndex index = JsonUtils.fromJson(reader, SignedPreKeyIndex.class);
reader.close();
return Optional.of(index);
} catch (IOException e) {
Log.w(TAG, e);
return Optional.absent();
}
}
private static synchronized void setSignedPreKeyIndex(Context context, SignedPreKeyIndex index) throws IOException {
File indexFile = new File(getSignedPreKeysDirectory(context), SignedPreKeyIndex.FILE_NAME);
FileOutputStream fout = new FileOutputStream(indexFile);
fout.write(JsonUtils.toJson(index).getBytes());
fout.close();
}
private static File getPreKeysDirectory(Context context) {
return getKeysDirectory(context, TextSecurePreKeyStore.PREKEY_DIRECTORY);
}
private static File getSignedPreKeysDirectory(Context context) {
return getKeysDirectory(context, TextSecurePreKeyStore.SIGNED_PREKEY_DIRECTORY);
}
private static File getKeysDirectory(Context context, String name) {
File directory = new File(context.getFilesDir(), name);
if (!directory.exists())
directory.mkdirs();
return directory;
}
private static class PreKeyIndex {
public static final String FILE_NAME = "index.dat";
@JsonProperty
private int nextPreKeyId;
public PreKeyIndex() {}
public PreKeyIndex(int nextPreKeyId) {
this.nextPreKeyId = nextPreKeyId;
}
}
private static class SignedPreKeyIndex {
public static final String FILE_NAME = "index.dat";
@JsonProperty
private int nextSignedPreKeyId;
@JsonProperty
private int activeSignedPreKeyId = -1;
public SignedPreKeyIndex() {}
}
}

View File

@@ -2,258 +2,89 @@ package org.thoughtcrime.securesms.crypto.storage;
import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import org.thoughtcrime.securesms.crypto.MasterCipher;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.whispersystems.libsignal.InvalidKeyIdException;
import org.whispersystems.libsignal.InvalidMessageException;
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
import org.whispersystems.libsignal.state.SignedPreKeyStore;
import org.whispersystems.libsignal.state.PreKeyRecord;
import org.whispersystems.libsignal.state.PreKeyStore;
import org.thoughtcrime.securesms.util.Conversions;
import org.whispersystems.libsignal.state.SignedPreKeyRecord;
import org.whispersystems.libsignal.state.SignedPreKeyStore;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.util.LinkedList;
import java.util.List;
public class TextSecurePreKeyStore implements PreKeyStore, SignedPreKeyStore {
public static final String PREKEY_DIRECTORY = "prekeys";
public static final String SIGNED_PREKEY_DIRECTORY = "signed_prekeys";
@SuppressWarnings("unused")
private static final String TAG = TextSecurePreKeyStore.class.getSimpleName();
private static final Object FILE_LOCK = new Object();
private static final int PLAINTEXT_VERSION = 2;
private static final int CURRENT_VERSION_MARKER = 2;
private static final Object FILE_LOCK = new Object();
private static final String TAG = TextSecurePreKeyStore.class.getSimpleName();
@NonNull private final Context context;
@Nullable private final MasterSecret masterSecret;
@NonNull
private final Context context;
public TextSecurePreKeyStore(@NonNull Context context) {
this(context, null);
}
public TextSecurePreKeyStore(@NonNull Context context, @Nullable MasterSecret masterSecret) {
this.context = context;
this.masterSecret = masterSecret;
this.context = context;
}
@Override
public PreKeyRecord loadPreKey(int preKeyId) throws InvalidKeyIdException {
synchronized (FILE_LOCK) {
try {
return new PreKeyRecord(loadSerializedRecord(getPreKeyFile(preKeyId)));
} catch (IOException | InvalidMessageException e) {
Log.w(TAG, e);
throw new InvalidKeyIdException(e);
}
PreKeyRecord preKeyRecord = DatabaseFactory.getPreKeyDatabase(context).getPreKey(preKeyId);
if (preKeyRecord == null) throw new InvalidKeyIdException("No such key: " + preKeyId);
else return preKeyRecord;
}
}
@Override
public SignedPreKeyRecord loadSignedPreKey(int signedPreKeyId) throws InvalidKeyIdException {
synchronized (FILE_LOCK) {
try {
return new SignedPreKeyRecord(loadSerializedRecord(getSignedPreKeyFile(signedPreKeyId)));
} catch (IOException | InvalidMessageException e) {
Log.w(TAG, e);
throw new InvalidKeyIdException(e);
}
SignedPreKeyRecord signedPreKeyRecord = DatabaseFactory.getSignedPreKeyDatabase(context).getSignedPreKey(signedPreKeyId);
if (signedPreKeyRecord == null) throw new InvalidKeyIdException("No such signed prekey: " + signedPreKeyId);
else return signedPreKeyRecord;
}
}
@Override
public List<SignedPreKeyRecord> loadSignedPreKeys() {
synchronized (FILE_LOCK) {
File directory = getSignedPreKeyDirectory();
List<SignedPreKeyRecord> results = new LinkedList<>();
for (File signedPreKeyFile : directory.listFiles()) {
try {
if (!"index.dat".equals(signedPreKeyFile.getName())) {
results.add(new SignedPreKeyRecord(loadSerializedRecord(signedPreKeyFile)));
}
} catch (IOException | InvalidMessageException e) {
Log.w(TAG, signedPreKeyFile.getAbsolutePath(), e);
}
}
return results;
return DatabaseFactory.getSignedPreKeyDatabase(context).getAllSignedPreKeys();
}
}
@Override
public void storePreKey(int preKeyId, PreKeyRecord record) {
synchronized (FILE_LOCK) {
try {
storeSerializedRecord(getPreKeyFile(preKeyId), record.serialize());
} catch (IOException e) {
throw new AssertionError(e);
}
DatabaseFactory.getPreKeyDatabase(context).insertPreKey(preKeyId, record);
}
}
@Override
public void storeSignedPreKey(int signedPreKeyId, SignedPreKeyRecord record) {
synchronized (FILE_LOCK) {
try {
storeSerializedRecord(getSignedPreKeyFile(signedPreKeyId), record.serialize());
} catch (IOException e) {
throw new AssertionError(e);
}
DatabaseFactory.getSignedPreKeyDatabase(context).insertSignedPreKey(signedPreKeyId, record);
}
}
@Override
public boolean containsPreKey(int preKeyId) {
File record = getPreKeyFile(preKeyId);
return record.exists();
return DatabaseFactory.getPreKeyDatabase(context).getPreKey(preKeyId) != null;
}
@Override
public boolean containsSignedPreKey(int signedPreKeyId) {
File record = getSignedPreKeyFile(signedPreKeyId);
return record.exists();
return DatabaseFactory.getSignedPreKeyDatabase(context).getSignedPreKey(signedPreKeyId) != null;
}
@Override
public void removePreKey(int preKeyId) {
File record = getPreKeyFile(preKeyId);
record.delete();
DatabaseFactory.getPreKeyDatabase(context).removePreKey(preKeyId);
}
@Override
public void removeSignedPreKey(int signedPreKeyId) {
File record = getSignedPreKeyFile(signedPreKeyId);
record.delete();
DatabaseFactory.getSignedPreKeyDatabase(context).removeSignedPreKey(signedPreKeyId);
}
public void migrateRecords() {
synchronized (FILE_LOCK) {
File preKeyRecords = getPreKeyDirectory();
for (File preKeyRecord : preKeyRecords.listFiles()) {
try {
int preKeyId = Integer.parseInt(preKeyRecord.getName());
PreKeyRecord record = loadPreKey(preKeyId);
storePreKey(preKeyId, record);
} catch (InvalidKeyIdException | NumberFormatException e) {
Log.w(TAG, e);
}
}
File signedPreKeyRecords = getSignedPreKeyDirectory();
for (File signedPreKeyRecord : signedPreKeyRecords.listFiles()) {
try {
int signedPreKeyId = Integer.parseInt(signedPreKeyRecord.getName());
SignedPreKeyRecord record = loadSignedPreKey(signedPreKeyId);
storeSignedPreKey(signedPreKeyId, record);
} catch (InvalidKeyIdException | NumberFormatException e) {
Log.w(TAG, e);
}
}
}
}
private byte[] loadSerializedRecord(File recordFile)
throws IOException, InvalidMessageException
{
FileInputStream fin = new FileInputStream(recordFile);
int recordVersion = readInteger(fin);
if (recordVersion > CURRENT_VERSION_MARKER) {
throw new AssertionError("Invalid version: " + recordVersion);
}
byte[] serializedRecord = readBlob(fin);
if (recordVersion < PLAINTEXT_VERSION && masterSecret != null) {
MasterCipher masterCipher = new MasterCipher(masterSecret);
serializedRecord = masterCipher.decryptBytes(serializedRecord);
} else if (recordVersion < PLAINTEXT_VERSION) {
throw new AssertionError("Migration didn't happen! " + recordFile.getAbsolutePath() + ", " + recordVersion);
}
fin.close();
return serializedRecord;
}
private void storeSerializedRecord(File file, byte[] serialized) throws IOException {
RandomAccessFile recordFile = new RandomAccessFile(file, "rw");
FileChannel out = recordFile.getChannel();
out.position(0);
writeInteger(CURRENT_VERSION_MARKER, out);
writeBlob(serialized, out);
out.truncate(out.position());
recordFile.close();
}
private File getPreKeyFile(int preKeyId) {
return new File(getPreKeyDirectory(), String.valueOf(preKeyId));
}
private File getSignedPreKeyFile(int signedPreKeyId) {
return new File(getSignedPreKeyDirectory(), String.valueOf(signedPreKeyId));
}
private File getPreKeyDirectory() {
return getRecordsDirectory(PREKEY_DIRECTORY);
}
private File getSignedPreKeyDirectory() {
return getRecordsDirectory(SIGNED_PREKEY_DIRECTORY);
}
private File getRecordsDirectory(String directoryName) {
File directory = new File(context.getFilesDir(), directoryName);
if (!directory.exists()) {
if (!directory.mkdirs()) {
Log.w(TAG, "PreKey directory creation failed!");
}
}
return directory;
}
private byte[] readBlob(FileInputStream in) throws IOException {
int length = readInteger(in);
byte[] blobBytes = new byte[length];
in.read(blobBytes, 0, blobBytes.length);
return blobBytes;
}
private void writeBlob(byte[] blobBytes, FileChannel out) throws IOException {
writeInteger(blobBytes.length, out);
out.write(ByteBuffer.wrap(blobBytes));
}
private int readInteger(FileInputStream in) throws IOException {
byte[] integer = new byte[4];
in.read(integer, 0, integer.length);
return Conversions.byteArrayToInt(integer);
}
private void writeInteger(int value, FileChannel out) throws IOException {
byte[] valueBytes = Conversions.intToByteArray(value);
out.write(ByteBuffer.wrap(valueBytes));
}
}