diff --git a/src/org/thoughtcrime/securesms/DatabaseUpgradeActivity.java b/src/org/thoughtcrime/securesms/DatabaseUpgradeActivity.java index c09e8f8135..0e0ca86e3e 100644 --- a/src/org/thoughtcrime/securesms/DatabaseUpgradeActivity.java +++ b/src/org/thoughtcrime/securesms/DatabaseUpgradeActivity.java @@ -32,8 +32,6 @@ import android.widget.ProgressBar; import org.thoughtcrime.securesms.attachments.DatabaseAttachment; import org.thoughtcrime.securesms.crypto.IdentityKeyUtil; import org.thoughtcrime.securesms.crypto.MasterSecret; -import org.thoughtcrime.securesms.crypto.storage.TextSecurePreKeyStore; -import org.thoughtcrime.securesms.crypto.storage.TextSecureSessionStore; import org.thoughtcrime.securesms.database.AttachmentDatabase; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.MmsDatabase; @@ -222,8 +220,8 @@ public class DatabaseUpgradeActivity extends BaseActivity { } if (params[0] < MIGRATE_SESSION_PLAINTEXT) { - new TextSecureSessionStore(context, masterSecret).migrateSessions(); - new TextSecurePreKeyStore(context, masterSecret).migrateRecords(); +// new TextSecureSessionStore(context, masterSecret).migrateSessions(); +// new TextSecurePreKeyStore(context, masterSecret).migrateRecords(); IdentityKeyUtil.migrateIdentityKeys(context, masterSecret); scheduleMessagesInPushDatabase(context);; diff --git a/src/org/thoughtcrime/securesms/crypto/PreKeyUtil.java b/src/org/thoughtcrime/securesms/crypto/PreKeyUtil.java index 9694d698e8..fd6ef67a29 100644 --- a/src/org/thoughtcrime/securesms/crypto/PreKeyUtil.java +++ b/src/org/thoughtcrime/securesms/crypto/PreKeyUtil.java @@ -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 generatePreKeys(Context context) { + public synchronized static List generatePreKeys(Context context) { PreKeyStore preKeyStore = new TextSecurePreKeyStore(context); List records = new LinkedList<>(); - int preKeyIdOffset = getNextPreKeyId(context); + int preKeyIdOffset = TextSecurePreferences.getNextPreKeyId(context); for (int i=0;i 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 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() {} - - } - - } diff --git a/src/org/thoughtcrime/securesms/crypto/storage/TextSecurePreKeyStore.java b/src/org/thoughtcrime/securesms/crypto/storage/TextSecurePreKeyStore.java index d9f3d2981c..0376fd495c 100644 --- a/src/org/thoughtcrime/securesms/crypto/storage/TextSecurePreKeyStore.java +++ b/src/org/thoughtcrime/securesms/crypto/storage/TextSecurePreKeyStore.java @@ -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 loadSignedPreKeys() { synchronized (FILE_LOCK) { - File directory = getSignedPreKeyDirectory(); - List 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)); - } - - - + } diff --git a/src/org/thoughtcrime/securesms/database/DatabaseFactory.java b/src/org/thoughtcrime/securesms/database/DatabaseFactory.java index fa105bac85..47cd06b1d8 100644 --- a/src/org/thoughtcrime/securesms/database/DatabaseFactory.java +++ b/src/org/thoughtcrime/securesms/database/DatabaseFactory.java @@ -39,20 +39,22 @@ public class DatabaseFactory { private static DatabaseFactory instance; - private final SQLCipherOpenHelper databaseHelper; - private final SmsDatabase sms; - private final MmsDatabase mms; - private final AttachmentDatabase attachments; - private final MediaDatabase media; - private final ThreadDatabase thread; - private final MmsSmsDatabase mmsSmsDatabase; - private final IdentityDatabase identityDatabase; - private final DraftDatabase draftDatabase; - private final PushDatabase pushDatabase; - private final GroupDatabase groupDatabase; - private final RecipientDatabase recipientDatabase; - private final ContactsDatabase contactsDatabase; - private final GroupReceiptDatabase groupReceiptDatabase; + private final SQLCipherOpenHelper databaseHelper; + private final SmsDatabase sms; + private final MmsDatabase mms; + private final AttachmentDatabase attachments; + private final MediaDatabase media; + private final ThreadDatabase thread; + private final MmsSmsDatabase mmsSmsDatabase; + private final IdentityDatabase identityDatabase; + private final DraftDatabase draftDatabase; + private final PushDatabase pushDatabase; + private final GroupDatabase groupDatabase; + private final RecipientDatabase recipientDatabase; + private final ContactsDatabase contactsDatabase; + private final GroupReceiptDatabase groupReceiptDatabase; + private final OneTimePreKeyDatabase preKeyDatabase; + private final SignedPreKeyDatabase signedPreKeyDatabase; public static DatabaseFactory getInstance(Context context) { synchronized (lock) { @@ -115,6 +117,14 @@ public class DatabaseFactory { return getInstance(context).groupReceiptDatabase; } + public static OneTimePreKeyDatabase getPreKeyDatabase(Context context) { + return getInstance(context).preKeyDatabase; + } + + public static SignedPreKeyDatabase getSignedPreKeyDatabase(Context context) { + return getInstance(context).signedPreKeyDatabase; + } + private DatabaseFactory(@NonNull Context context) { SQLiteDatabase.loadLibs(context); @@ -135,6 +145,8 @@ public class DatabaseFactory { this.recipientDatabase = new RecipientDatabase(context, databaseHelper); this.groupReceiptDatabase = new GroupReceiptDatabase(context, databaseHelper); this.contactsDatabase = new ContactsDatabase(context); + this.preKeyDatabase = new OneTimePreKeyDatabase(context, databaseHelper); + this.signedPreKeyDatabase = new SignedPreKeyDatabase(context, databaseHelper); } public void onApplicationLevelUpgrade(@NonNull Context context, @NonNull MasterSecret masterSecret, diff --git a/src/org/thoughtcrime/securesms/database/IdentityDatabase.java b/src/org/thoughtcrime/securesms/database/IdentityDatabase.java index dc8f2a91c4..8f26a84b0f 100644 --- a/src/org/thoughtcrime/securesms/database/IdentityDatabase.java +++ b/src/org/thoughtcrime/securesms/database/IdentityDatabase.java @@ -1,4 +1,4 @@ -/** +/* * Copyright (C) 2011 Whisper Systems * * This program is free software: you can redistribute it and/or modify @@ -35,6 +35,7 @@ import java.io.IOException; public class IdentityDatabase extends Database { + @SuppressWarnings("unused") private static final String TAG = IdentityDatabase.class.getSimpleName(); private static final String TABLE_NAME = "identities"; @@ -218,7 +219,7 @@ public class IdentityDatabase extends Database { public class IdentityReader { private final Cursor cursor; - public IdentityReader(@NonNull Cursor cursor) { + IdentityReader(@NonNull Cursor cursor) { this.cursor = cursor; } diff --git a/src/org/thoughtcrime/securesms/database/OneTimePreKeyDatabase.java b/src/org/thoughtcrime/securesms/database/OneTimePreKeyDatabase.java new file mode 100644 index 0000000000..82f620d0a6 --- /dev/null +++ b/src/org/thoughtcrime/securesms/database/OneTimePreKeyDatabase.java @@ -0,0 +1,86 @@ +package org.thoughtcrime.securesms.database; + + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.support.annotation.Nullable; +import android.util.Log; + +import net.sqlcipher.database.SQLiteDatabase; + +import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; +import org.thoughtcrime.securesms.util.Base64; +import org.whispersystems.libsignal.InvalidKeyException; +import org.whispersystems.libsignal.ecc.Curve; +import org.whispersystems.libsignal.ecc.ECKeyPair; +import org.whispersystems.libsignal.ecc.ECPrivateKey; +import org.whispersystems.libsignal.ecc.ECPublicKey; +import org.whispersystems.libsignal.state.PreKeyRecord; + +import java.io.IOException; + +public class OneTimePreKeyDatabase extends Database { + + private static final String TAG = OneTimePreKeyDatabase.class.getSimpleName(); + + public static final String TABLE_NAME = "one_time_prekeys"; + private static final String ID = "_id"; + public static final String KEY_ID = "key_id"; + public static final String PUBLIC_KEY = "public_key"; + public static final String PRIVATE_KEY = "private_key"; + + public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + + " (" + ID + " INTEGER PRIMARY KEY, " + + KEY_ID + " INTEGER UNIQUE, " + + PUBLIC_KEY + " TEXT NOT NULL, " + + PRIVATE_KEY + " TEXT NOT NULL);"; + + OneTimePreKeyDatabase(Context context, SQLCipherOpenHelper databaseHelper) { + super(context, databaseHelper); + } + + public @Nullable PreKeyRecord getPreKey(int keyId) { + SQLiteDatabase database = databaseHelper.getReadableDatabase(); + + try (Cursor cursor = database.query(TABLE_NAME, null, KEY_ID + " = ?", + new String[] {String.valueOf(keyId)}, + null, null, null)) + { + if (cursor != null && cursor.moveToFirst()) { + try { + ECPublicKey publicKey = Curve.decodePoint(Base64.decode(cursor.getString(cursor.getColumnIndexOrThrow(PUBLIC_KEY))), 0); + ECPrivateKey privateKey = Curve.decodePrivatePoint(Base64.decode(cursor.getString(cursor.getColumnIndexOrThrow(PRIVATE_KEY)))); + + return new PreKeyRecord(keyId, new ECKeyPair(publicKey, privateKey)); + } catch (InvalidKeyException | IOException e) { + Log.w(TAG, e); + } + } + } + + return null; + } + + + public void insertPreKey(int keyId, PreKeyRecord record) { + SQLiteDatabase database = databaseHelper.getWritableDatabase(); + + ContentValues contentValues = new ContentValues(); + contentValues.put(KEY_ID, keyId); + contentValues.put(PUBLIC_KEY, Base64.encodeBytes(record.getKeyPair().getPublicKey().serialize())); + contentValues.put(PRIVATE_KEY, Base64.encodeBytes(record.getKeyPair().getPrivateKey().serialize())); + + database.insert(TABLE_NAME, null, contentValues); + } + + public void removePreKey(int keyId) { + SQLiteDatabase database = databaseHelper.getWritableDatabase(); + database.delete(TABLE_NAME, KEY_ID + " = ?", new String[] {String.valueOf(keyId)}); + } + + + + + +} diff --git a/src/org/thoughtcrime/securesms/database/SignedPreKeyDatabase.java b/src/org/thoughtcrime/securesms/database/SignedPreKeyDatabase.java new file mode 100644 index 0000000000..c8508ab321 --- /dev/null +++ b/src/org/thoughtcrime/securesms/database/SignedPreKeyDatabase.java @@ -0,0 +1,117 @@ +package org.thoughtcrime.securesms.database; + + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.util.Log; + +import net.sqlcipher.database.SQLiteDatabase; + +import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; +import org.thoughtcrime.securesms.util.Base64; +import org.whispersystems.libsignal.InvalidKeyException; +import org.whispersystems.libsignal.ecc.Curve; +import org.whispersystems.libsignal.ecc.ECKeyPair; +import org.whispersystems.libsignal.ecc.ECPrivateKey; +import org.whispersystems.libsignal.ecc.ECPublicKey; +import org.whispersystems.libsignal.state.SignedPreKeyRecord; + +import java.io.IOException; +import java.util.LinkedList; +import java.util.List; + +public class SignedPreKeyDatabase extends Database { + + private static final String TAG = SignedPreKeyDatabase.class.getSimpleName(); + + public static final String TABLE_NAME = "signed_prekeys"; + + private static final String ID = "_id"; + public static final String KEY_ID = "key_id"; + public static final String PUBLIC_KEY = "public_key"; + public static final String PRIVATE_KEY = "private_key"; + public static final String SIGNATURE = "signature"; + public static final String TIMESTAMP = "timestamp"; + + public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + + " (" + ID + " INTEGER PRIMARY KEY, " + + KEY_ID + " INTEGER UNIQUE, " + + PUBLIC_KEY + " TEXT NOT NULL, " + + PRIVATE_KEY + " TEXT NOT NULL, " + + SIGNATURE + " TEXT NOT NULL, " + + TIMESTAMP + " INTEGER DEFAULT 0);"; + + public SignedPreKeyDatabase(Context context, SQLCipherOpenHelper databaseHelper) { + super(context, databaseHelper); + } + + public @Nullable SignedPreKeyRecord getSignedPreKey(int keyId) { + SQLiteDatabase database = databaseHelper.getReadableDatabase(); + + try (Cursor cursor = database.query(TABLE_NAME, null, KEY_ID + " = ?", + new String[] {String.valueOf(keyId)}, + null, null, null)) + { + if (cursor != null && cursor.moveToFirst()) { + try { + ECPublicKey publicKey = Curve.decodePoint(Base64.decode(cursor.getString(cursor.getColumnIndexOrThrow(PUBLIC_KEY))), 0); + ECPrivateKey privateKey = Curve.decodePrivatePoint(Base64.decode(cursor.getString(cursor.getColumnIndexOrThrow(PRIVATE_KEY)))); + byte[] signature = Base64.decode(cursor.getString(cursor.getColumnIndexOrThrow(SIGNATURE))); + long timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(TIMESTAMP)); + + return new SignedPreKeyRecord(keyId, timestamp, new ECKeyPair(publicKey, privateKey), signature); + } catch (InvalidKeyException | IOException e) { + Log.w(TAG, e); + } + } + } + + return null; + } + + public @NonNull List getAllSignedPreKeys() { + SQLiteDatabase database = databaseHelper.getReadableDatabase(); + List results = new LinkedList<>(); + + try (Cursor cursor = database.query(TABLE_NAME, null, null, null, null, null, null)) { + while (cursor != null && cursor.moveToNext()) { + try { + int keyId = cursor.getInt(cursor.getColumnIndexOrThrow(KEY_ID)); + ECPublicKey publicKey = Curve.decodePoint(Base64.decode(cursor.getString(cursor.getColumnIndexOrThrow(PUBLIC_KEY))), 0); + ECPrivateKey privateKey = Curve.decodePrivatePoint(Base64.decode(cursor.getString(cursor.getColumnIndexOrThrow(PRIVATE_KEY)))); + byte[] signature = Base64.decode(cursor.getString(cursor.getColumnIndexOrThrow(SIGNATURE))); + long timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(TIMESTAMP)); + + results.add(new SignedPreKeyRecord(keyId, timestamp, new ECKeyPair(publicKey, privateKey), signature)); + } catch (InvalidKeyException | IOException e) { + Log.w(TAG, e); + } + } + } + + return results; + } + + public void insertSignedPreKey(int keyId, SignedPreKeyRecord record) { + SQLiteDatabase database = databaseHelper.getWritableDatabase(); + + ContentValues contentValues = new ContentValues(); + contentValues.put(KEY_ID, keyId); + contentValues.put(PUBLIC_KEY, Base64.encodeBytes(record.getKeyPair().getPublicKey().serialize())); + contentValues.put(PRIVATE_KEY, Base64.encodeBytes(record.getKeyPair().getPrivateKey().serialize())); + contentValues.put(SIGNATURE, Base64.encodeBytes(record.getSignature())); + contentValues.put(TIMESTAMP, record.getTimestamp()); + + database.insert(TABLE_NAME, null, contentValues); + } + + + public void removeSignedPreKey(int keyId) { + SQLiteDatabase database = databaseHelper.getWritableDatabase(); + database.delete(TABLE_NAME, KEY_ID + " = ? AND " + SIGNATURE + " IS NOT NULL", new String[] {String.valueOf(keyId)}); + } + +} diff --git a/src/org/thoughtcrime/securesms/database/helpers/PreKeyMigrationHelper.java b/src/org/thoughtcrime/securesms/database/helpers/PreKeyMigrationHelper.java new file mode 100644 index 0000000000..5ac68de43a --- /dev/null +++ b/src/org/thoughtcrime/securesms/database/helpers/PreKeyMigrationHelper.java @@ -0,0 +1,225 @@ +package org.thoughtcrime.securesms.database.helpers; + + +import android.content.ContentValues; +import android.content.Context; +import android.support.annotation.NonNull; +import android.util.Log; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import net.sqlcipher.database.SQLiteDatabase; + +import org.thoughtcrime.securesms.database.OneTimePreKeyDatabase; +import org.thoughtcrime.securesms.database.SignedPreKeyDatabase; +import org.thoughtcrime.securesms.util.Base64; +import org.thoughtcrime.securesms.util.Conversions; +import org.thoughtcrime.securesms.util.JsonUtils; +import org.thoughtcrime.securesms.util.TextSecurePreferences; +import org.whispersystems.libsignal.InvalidMessageException; +import org.whispersystems.libsignal.state.PreKeyRecord; +import org.whispersystems.libsignal.state.SignedPreKeyRecord; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStreamReader; + +class PreKeyMigrationHelper { + + private static final String PREKEY_DIRECTORY = "prekeys"; + private static final String SIGNED_PREKEY_DIRECTORY = "signed_prekeys"; + + private static final int PLAINTEXT_VERSION = 2; + private static final int CURRENT_VERSION_MARKER = 2; + + private static final String TAG = PreKeyMigrationHelper.class.getSimpleName(); + + static boolean migratePreKeys(Context context, SQLiteDatabase database) { + File[] preKeyFiles = getPreKeyDirectory(context).listFiles(); + boolean clean = true; + + if (preKeyFiles != null) { + for (File preKeyFile : preKeyFiles) { + if (!"index.dat".equals(preKeyFile.getName())) { + try { + PreKeyRecord preKey = new PreKeyRecord(loadSerializedRecord(preKeyFile)); + + ContentValues contentValues = new ContentValues(); + contentValues.put(OneTimePreKeyDatabase.KEY_ID, preKey.getId()); + contentValues.put(OneTimePreKeyDatabase.PUBLIC_KEY, Base64.encodeBytes(preKey.getKeyPair().getPublicKey().serialize())); + contentValues.put(OneTimePreKeyDatabase.PRIVATE_KEY, Base64.encodeBytes(preKey.getKeyPair().getPrivateKey().serialize())); + database.insert(OneTimePreKeyDatabase.TABLE_NAME, null, contentValues); + Log.w(TAG, "Migrated one-time prekey: " + preKey.getId()); + } catch (IOException | InvalidMessageException e) { + Log.w(TAG, e); + clean = false; + } + } + } + } + + File[] signedPreKeyFiles = getSignedPreKeyDirectory(context).listFiles(); + + if (signedPreKeyFiles != null) { + for (File signedPreKeyFile : signedPreKeyFiles) { + if (!"index.dat".equals(signedPreKeyFile.getName())) { + try { + SignedPreKeyRecord signedPreKey = new SignedPreKeyRecord(loadSerializedRecord(signedPreKeyFile)); + + ContentValues contentValues = new ContentValues(); + contentValues.put(SignedPreKeyDatabase.KEY_ID, signedPreKey.getId()); + contentValues.put(SignedPreKeyDatabase.PUBLIC_KEY, Base64.encodeBytes(signedPreKey.getKeyPair().getPublicKey().serialize())); + contentValues.put(SignedPreKeyDatabase.PRIVATE_KEY, Base64.encodeBytes(signedPreKey.getKeyPair().getPrivateKey().serialize())); + contentValues.put(SignedPreKeyDatabase.SIGNATURE, Base64.encodeBytes(signedPreKey.getSignature())); + contentValues.put(SignedPreKeyDatabase.TIMESTAMP, signedPreKey.getTimestamp()); + database.insert(SignedPreKeyDatabase.TABLE_NAME, null, contentValues); + Log.w(TAG, "Migrated signed prekey: " + signedPreKey.getId()); + } catch (IOException | InvalidMessageException e) { + Log.w(TAG, e); + clean = false; + } + } + } + } + + File oneTimePreKeyIndex = new File(getPreKeyDirectory(context), PreKeyIndex.FILE_NAME); + File signedPreKeyIndex = new File(getSignedPreKeyDirectory(context), SignedPreKeyIndex.FILE_NAME); + + if (oneTimePreKeyIndex.exists()) { + try { + InputStreamReader reader = new InputStreamReader(new FileInputStream(oneTimePreKeyIndex)); + PreKeyIndex index = JsonUtils.fromJson(reader, PreKeyIndex.class); + reader.close(); + + Log.w(TAG, "Setting next prekey id: " + index.nextPreKeyId); + TextSecurePreferences.setNextPreKeyId(context, index.nextPreKeyId); + } catch (IOException e) { + Log.w(TAG, e); + } + } + + if (signedPreKeyIndex.exists()) { + try { + InputStreamReader reader = new InputStreamReader(new FileInputStream(signedPreKeyIndex)); + SignedPreKeyIndex index = JsonUtils.fromJson(reader, SignedPreKeyIndex.class); + reader.close(); + + Log.w(TAG, "Setting next signed prekey id: " + index.nextSignedPreKeyId); + Log.w(TAG, "Setting active signed prekey id: " + index.activeSignedPreKeyId); + TextSecurePreferences.setNextSignedPreKeyId(context, index.nextSignedPreKeyId); + TextSecurePreferences.setActiveSignedPreKeyId(context, index.activeSignedPreKeyId); + } catch (IOException e) { + Log.w(TAG, e); + } + } + + return clean; + } + + static void cleanUpPreKeys(@NonNull Context context) { + File preKeyDirectory = getPreKeyDirectory(context); + File[] preKeyFiles = preKeyDirectory.listFiles(); + + if (preKeyFiles != null) { + for (File preKeyFile : preKeyFiles) { + Log.w(TAG, "Deleting: " + preKeyFile.getAbsolutePath()); + preKeyFile.delete(); + } + + Log.w(TAG, "Deleting: " + preKeyDirectory.getAbsolutePath()); + preKeyDirectory.delete(); + } + + File signedPreKeyDirectory = getSignedPreKeyDirectory(context); + File[] signedPreKeyFiles = signedPreKeyDirectory.listFiles(); + + if (signedPreKeyFiles != null) { + for (File signedPreKeyFile : signedPreKeyFiles) { + Log.w(TAG, "Deleting: " + signedPreKeyFile.getAbsolutePath()); + signedPreKeyFile.delete(); + } + + Log.w(TAG, "Deleting: " + signedPreKeyDirectory.getAbsolutePath()); + signedPreKeyDirectory.delete(); + } + } + + private static byte[] loadSerializedRecord(File recordFile) + throws IOException, InvalidMessageException + { + FileInputStream fin = new FileInputStream(recordFile); + int recordVersion = readInteger(fin); + + if (recordVersion > CURRENT_VERSION_MARKER) { + throw new IOException("Invalid version: " + recordVersion); + } + + byte[] serializedRecord = readBlob(fin); + + if (recordVersion < PLAINTEXT_VERSION) { + throw new IOException("Migration didn't happen! " + recordFile.getAbsolutePath() + ", " + recordVersion); + } + + fin.close(); + return serializedRecord; + } + + private static File getPreKeyDirectory(Context context) { + return getRecordsDirectory(context, PREKEY_DIRECTORY); + } + + private static File getSignedPreKeyDirectory(Context context) { + return getRecordsDirectory(context, SIGNED_PREKEY_DIRECTORY); + } + + private static File getRecordsDirectory(Context context, 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 static byte[] readBlob(FileInputStream in) throws IOException { + int length = readInteger(in); + byte[] blobBytes = new byte[length]; + + in.read(blobBytes, 0, blobBytes.length); + return blobBytes; + } + + private static int readInteger(FileInputStream in) throws IOException { + byte[] integer = new byte[4]; + in.read(integer, 0, integer.length); + return Conversions.byteArrayToInt(integer); + } + + private static class PreKeyIndex { + static final String FILE_NAME = "index.dat"; + + @JsonProperty + private int nextPreKeyId; + + public PreKeyIndex() {} + } + + private static class SignedPreKeyIndex { + static final String FILE_NAME = "index.dat"; + + @JsonProperty + private int nextSignedPreKeyId; + + @JsonProperty + private int activeSignedPreKeyId = -1; + + public SignedPreKeyIndex() {} + + } + + +} diff --git a/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java b/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java index 7dda134b89..052e224a89 100644 --- a/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java +++ b/src/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java @@ -9,6 +9,7 @@ import net.sqlcipher.database.SQLiteDatabase; import net.sqlcipher.database.SQLiteDatabaseHook; import net.sqlcipher.database.SQLiteOpenHelper; +import org.thoughtcrime.securesms.ApplicationContext; import org.thoughtcrime.securesms.crypto.DatabaseSecret; import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.database.AttachmentDatabase; @@ -17,10 +18,13 @@ import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.database.GroupReceiptDatabase; import org.thoughtcrime.securesms.database.IdentityDatabase; import org.thoughtcrime.securesms.database.MmsDatabase; +import org.thoughtcrime.securesms.database.OneTimePreKeyDatabase; import org.thoughtcrime.securesms.database.PushDatabase; import org.thoughtcrime.securesms.database.RecipientDatabase; +import org.thoughtcrime.securesms.database.SignedPreKeyDatabase; import org.thoughtcrime.securesms.database.SmsDatabase; import org.thoughtcrime.securesms.database.ThreadDatabase; +import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob; import org.thoughtcrime.securesms.service.KeyCachingService; import org.thoughtcrime.securesms.util.TextSecurePreferences; @@ -29,9 +33,10 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { @SuppressWarnings("unused") private static final String TAG = SQLCipherOpenHelper.class.getSimpleName(); - private static final int RECIPIENT_CALL_RINGTONE_VERSION = 2; + private static final int RECIPIENT_CALL_RINGTONE_VERSION = 2; + private static final int MIGRATE_PREKEYS_VERSION = 3; - private static final int DATABASE_VERSION = 2; + private static final int DATABASE_VERSION = 3; private static final String DATABASE_NAME = "signal.db"; private final Context context; @@ -68,6 +73,8 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { db.execSQL(GroupDatabase.CREATE_TABLE); db.execSQL(RecipientDatabase.CREATE_TABLE); db.execSQL(GroupReceiptDatabase.CREATE_TABLE); + db.execSQL(OneTimePreKeyDatabase.CREATE_TABLE); + db.execSQL(SignedPreKeyDatabase.CREATE_TABLE); executeStatements(db, SmsDatabase.CREATE_INDEXS); executeStatements(db, MmsDatabase.CREATE_INDEXS); @@ -87,6 +94,12 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { if (masterSecret != null) SQLCipherMigrationHelper.migrateCiphertext(context, masterSecret, legacyDb, db, null); else TextSecurePreferences.setNeedsSqlCipherMigration(context, true); + + if (!PreKeyMigrationHelper.migratePreKeys(context, db)) { + ApplicationContext.getInstance(context).getJobManager().add(new RefreshPreKeysJob(context)); + } + + PreKeyMigrationHelper.cleanUpPreKeys(context); } } @@ -94,9 +107,33 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { Log.w(TAG, "Upgrading database: " + oldVersion + ", " + newVersion); - if (oldVersion < RECIPIENT_CALL_RINGTONE_VERSION) { - db.execSQL("ALTER TABLE recipient_preferences ADD COLUMN call_ringtone TEXT DEFAULT NULL"); - db.execSQL("ALTER TABLE recipient_preferences ADD COLUMN call_vibrate INTEGER DEFAULT " + RecipientDatabase.VibrateState.DEFAULT.getId()); + db.beginTransaction(); + + try { + + if (oldVersion < RECIPIENT_CALL_RINGTONE_VERSION) { + db.execSQL("ALTER TABLE recipient_preferences ADD COLUMN call_ringtone TEXT DEFAULT NULL"); + db.execSQL("ALTER TABLE recipient_preferences ADD COLUMN call_vibrate INTEGER DEFAULT " + RecipientDatabase.VibrateState.DEFAULT.getId()); + } + + if (oldVersion < MIGRATE_PREKEYS_VERSION) { + db.execSQL("CREATE TABLE signed_prekeys (_id INTEGER PRIMARY KEY, key_id INTEGER UNIQUE, public_key TEXT NOT NULL, private_key TEXT NOT NULL, signature TEXT NOT NULL, timestamp INTEGER DEFAULT 0)"); + db.execSQL("CREATE TABLE one_time_prekeys (_id INTEGER PRIMARY KEY, key_id INTEGER UNIQUE, public_key TEXT NOT NULL, private_key TEXT NOT NULL)"); + + if (!PreKeyMigrationHelper.migratePreKeys(context, db)) { + ApplicationContext.getInstance(context).getJobManager().add(new RefreshPreKeysJob(context)); + } + + PreKeyMigrationHelper.cleanUpPreKeys(context); + } + + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + + if (oldVersion < MIGRATE_PREKEYS_VERSION) { + PreKeyMigrationHelper.cleanUpPreKeys(context); } } diff --git a/src/org/thoughtcrime/securesms/util/TextSecurePreferences.java b/src/org/thoughtcrime/securesms/util/TextSecurePreferences.java index 289f1c40b9..dcfae8e3af 100644 --- a/src/org/thoughtcrime/securesms/util/TextSecurePreferences.java +++ b/src/org/thoughtcrime/securesms/util/TextSecurePreferences.java @@ -18,8 +18,10 @@ import org.greenrobot.eventbus.EventBus; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.jobs.requirements.SqlCipherMigrationRequirementProvider; import org.thoughtcrime.securesms.preferences.widgets.NotificationPrivacyPreference; +import org.whispersystems.libsignal.util.Medium; import java.io.IOException; +import java.security.SecureRandom; import java.util.Arrays; import java.util.Collections; import java.util.HashSet; @@ -131,6 +133,34 @@ public class TextSecurePreferences { public static final String CALL_RINGTONE_PREF = "pref_call_ringtone"; public static final String CALL_VIBRATE_PREF = "pref_call_vibrate"; + private static final String NEXT_PRE_KEY_ID = "pref_next_pre_key_id"; + private static final String ACTIVE_SIGNED_PRE_KEY_ID = "pref_active_signed_pre_key_id"; + private static final String NEXT_SIGNED_PRE_KEY_ID = "pref_next_signed_pre_key_id"; + + public static int getNextPreKeyId(@NonNull Context context) { + return getIntegerPreference(context, NEXT_PRE_KEY_ID, new SecureRandom().nextInt(Medium.MAX_VALUE)); + } + + public static void setNextPreKeyId(@NonNull Context context, int value) { + setIntegerPrefrence(context, NEXT_PRE_KEY_ID, value); + } + + public static int getNextSignedPreKeyId(@NonNull Context context) { + return getIntegerPreference(context, NEXT_SIGNED_PRE_KEY_ID, new SecureRandom().nextInt(Medium.MAX_VALUE)); + } + + public static void setNextSignedPreKeyId(@NonNull Context context, int value) { + setIntegerPrefrence(context, NEXT_SIGNED_PRE_KEY_ID, value); + } + + public static int getActiveSignedPreKeyId(@NonNull Context context) { + return getIntegerPreference(context, ACTIVE_SIGNED_PRE_KEY_ID, -1); + } + + public static void setActiveSignedPreKeyId(@NonNull Context context, int value) { + setIntegerPrefrence(context, ACTIVE_SIGNED_PRE_KEY_ID, value);; + } + public static void setNeedsSqlCipherMigration(@NonNull Context context, boolean value) { setBooleanPreference(context, NEEDS_SQLCIPHER_MIGRATION, value); EventBus.getDefault().post(new SqlCipherMigrationRequirementProvider.SqlCipherNeedsMigrationEvent());