From bdc62a96455bb60411cf1322c91a16672cc21828 Mon Sep 17 00:00:00 2001 From: Mikunj Date: Tue, 4 Jun 2019 12:12:40 +1000 Subject: [PATCH] Added more loki databases. --- .../securesms/crypto/PreKeyUtil.java | 11 ++ .../securesms/database/DatabaseFactory.java | 21 +++ .../securesms/loki/LokiAPIDatabase.kt | 26 ++-- .../loki/LokiContactPreKeyDatabase.kt | 57 +++++++++ .../securesms/loki/LokiDatabaseUtils.kt | 30 +++++ .../loki/LokiPreKeyBundleDatabase.kt | 121 ++++++++++++++++++ 6 files changed, 248 insertions(+), 18 deletions(-) create mode 100644 src/org/thoughtcrime/securesms/loki/LokiContactPreKeyDatabase.kt create mode 100644 src/org/thoughtcrime/securesms/loki/LokiDatabaseUtils.kt create mode 100644 src/org/thoughtcrime/securesms/loki/LokiPreKeyBundleDatabase.kt diff --git a/src/org/thoughtcrime/securesms/crypto/PreKeyUtil.java b/src/org/thoughtcrime/securesms/crypto/PreKeyUtil.java index 400a96147d..d2e68f49a8 100644 --- a/src/org/thoughtcrime/securesms/crypto/PreKeyUtil.java +++ b/src/org/thoughtcrime/securesms/crypto/PreKeyUtil.java @@ -19,6 +19,7 @@ package org.thoughtcrime.securesms.crypto; import android.content.Context; +import org.jetbrains.annotations.Nullable; import org.thoughtcrime.securesms.crypto.storage.TextSecurePreKeyStore; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.whispersystems.libsignal.IdentityKeyPair; @@ -92,6 +93,16 @@ public class PreKeyUtil { // region - Loki + public static synchronized @Nullable SignedPreKeyRecord getActiveSignedPreKey(Context context) { + SignedPreKeyStore signedPreKeyStore = new TextSecurePreKeyStore(context); + try { + return signedPreKeyStore.loadSignedPreKey(getActiveSignedPreKeyId(context)); + } catch (InvalidKeyIdException e) { + // Fall through + } + return null; + } + public synchronized static List generatePreKeys(Context context, int amount) { PreKeyStore preKeyStore = new TextSecurePreKeyStore(context); List records = new LinkedList<>(); diff --git a/src/org/thoughtcrime/securesms/database/DatabaseFactory.java b/src/org/thoughtcrime/securesms/database/DatabaseFactory.java index 4935584987..46c1b25375 100644 --- a/src/org/thoughtcrime/securesms/database/DatabaseFactory.java +++ b/src/org/thoughtcrime/securesms/database/DatabaseFactory.java @@ -31,6 +31,8 @@ import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.database.helpers.ClassicOpenHelper; import org.thoughtcrime.securesms.database.helpers.SQLCipherMigrationHelper; import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; +import org.thoughtcrime.securesms.loki.LokiContactPreKeyDatabase; +import org.thoughtcrime.securesms.loki.LokiPreKeyBundleDatabase; import org.thoughtcrime.securesms.util.TextSecurePreferences; public class DatabaseFactory { @@ -59,6 +61,10 @@ public class DatabaseFactory { private final SearchDatabase searchDatabase; private final JobDatabase jobDatabase; + // Loki + private final LokiContactPreKeyDatabase lokiContactPreKeyDatabase; + private final LokiPreKeyBundleDatabase lokiPreKeyBundleDatabase; + public static DatabaseFactory getInstance(Context context) { synchronized (lock) { if (instance == null) @@ -144,6 +150,18 @@ public class DatabaseFactory { return getInstance(context).databaseHelper.getReadableDatabase(); } + // region Loki + + public static LokiContactPreKeyDatabase getLokiContactPreKeyDatabase(Context context) { + return getInstance(context).lokiContactPreKeyDatabase; + } + + public static LokiPreKeyBundleDatabase getLokiPreKeyBundleDatabase(Context context) { + return getInstance(context).lokiPreKeyBundleDatabase; + } + + // endregion + public static void upgradeRestored(Context context, SQLiteDatabase database){ getInstance(context).databaseHelper.onUpgrade(database, database.getVersion(), -1); getInstance(context).databaseHelper.markCurrent(database); @@ -174,6 +192,9 @@ public class DatabaseFactory { this.sessionDatabase = new SessionDatabase(context, databaseHelper); this.searchDatabase = new SearchDatabase(context, databaseHelper); this.jobDatabase = new JobDatabase(context, databaseHelper); + + this.lokiContactPreKeyDatabase = new LokiContactPreKeyDatabase(context, databaseHelper); + this.lokiPreKeyBundleDatabase = new LokiPreKeyBundleDatabase(context, databaseHelper); } public void onApplicationLevelUpgrade(@NonNull Context context, @NonNull MasterSecret masterSecret, diff --git a/src/org/thoughtcrime/securesms/loki/LokiAPIDatabase.kt b/src/org/thoughtcrime/securesms/loki/LokiAPIDatabase.kt index 9c847adc16..37cafa6b4b 100644 --- a/src/org/thoughtcrime/securesms/loki/LokiAPIDatabase.kt +++ b/src/org/thoughtcrime/securesms/loki/LokiAPIDatabase.kt @@ -29,7 +29,8 @@ class LokiAPIDatabase(private val userPublicKey: String, context: Context, helpe } override fun getSwarmCache(hexEncodedPublicKey: String): List? { - return get(swarmCache, "${Companion.hexEncodedPublicKey} = ?", wrap(hexEncodedPublicKey)) { cursor -> + val database = databaseHelper.readableDatabase + return database.get(swarmCache, "${Companion.hexEncodedPublicKey} = ?", wrap(hexEncodedPublicKey)) { cursor -> val swarmAsString = cursor.getString(cursor.getColumnIndexOrThrow(swarm)) swarmAsString.split(",").map { targetAsString -> val components = targetAsString.split("?port=") @@ -47,7 +48,8 @@ class LokiAPIDatabase(private val userPublicKey: String, context: Context, helpe } override fun getLastMessageHashValue(target: LokiAPITarget): String? { - return get(lastMessageHashValueCache, "${Companion.target} = ?", wrap(target.address)) { cursor -> + val database = databaseHelper.readableDatabase + return database.get(lastMessageHashValueCache, "${Companion.target} = ?", wrap(target.address)) { cursor -> cursor.getString(cursor.getColumnIndexOrThrow(lastMessageHashValue)) } } @@ -58,7 +60,8 @@ class LokiAPIDatabase(private val userPublicKey: String, context: Context, helpe } override fun getReceivedMessageHashValues(): Set? { - return get(receivedMessageHashValuesCache, "$userID = ?", wrap(userPublicKey)) { cursor -> + val database = databaseHelper.readableDatabase + return database.get(receivedMessageHashValuesCache, "$userID = ?", wrap(userPublicKey)) { cursor -> val receivedMessageHashValuesAsString = cursor.getString(cursor.getColumnIndexOrThrow(receivedMessageHashValues)) receivedMessageHashValuesAsString.split(",").toSet() } @@ -69,23 +72,10 @@ class LokiAPIDatabase(private val userPublicKey: String, context: Context, helpe val receivedMessageHashValuesAsString = newValue.joinToString(",") database.update(receivedMessageHashValuesCache, wrap(mapOf( receivedMessageHashValues to receivedMessageHashValuesAsString )), "$userID = ?", wrap(userPublicKey)) } - - // region Convenience - private fun get(table: String, query: String, arguments: Array, get: (Cursor) -> T): T? { - val database = databaseHelper.readableDatabase - var cursor: Cursor? = null - try { - cursor = database.query(table, null, query, arguments, null, null, null) - if (cursor != null && cursor.moveToFirst()) { return get(cursor) } - } catch (e: Exception) { - // Do nothing - } finally { - cursor?.close() - } - return null - } } +// region Convenience + private inline fun wrap(x: T): Array { return Array(1) { x } } diff --git a/src/org/thoughtcrime/securesms/loki/LokiContactPreKeyDatabase.kt b/src/org/thoughtcrime/securesms/loki/LokiContactPreKeyDatabase.kt new file mode 100644 index 0000000000..5f82fe20f4 --- /dev/null +++ b/src/org/thoughtcrime/securesms/loki/LokiContactPreKeyDatabase.kt @@ -0,0 +1,57 @@ +package org.thoughtcrime.securesms.loki + +import android.content.ContentValues +import android.content.Context +import net.sqlcipher.database.SQLiteDatabase +import org.thoughtcrime.securesms.crypto.PreKeyUtil +import org.thoughtcrime.securesms.database.Database +import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper +import org.whispersystems.libsignal.state.PreKeyRecord + +/** + * A database for associating a `PreKeyRecord` id to a contact public key. + */ +class LokiContactPreKeyDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper) { + companion object { + private val tableName = "loki_contact_pre_key_database" + private val pubKey = "pub_key" + private val preKeyId = "pre_key_id" + + @JvmStatic + val createTableCommand = "CREATE TABLE $tableName ($preKeyId INTEGER PRIMARY KEY, $pubKey TEXT);" + } + + fun hasPreKey(pubKey: String): Boolean { + val database = databaseHelper.readableDatabase + return database.get(tableName, "${Companion.pubKey} = ?", arrayOf(pubKey)) { cursor -> + cursor.count > 0 + } ?: false + } + + fun getPreKey(pubKey: String): PreKeyRecord? { + val database = databaseHelper.readableDatabase + return database.get(tableName, "${Companion.pubKey} = ?", arrayOf(pubKey)) { cursor -> + val preKeyId = cursor.getInt(preKeyId) + PreKeyUtil.loadPreKey(context, preKeyId) + } + } + + fun getOrCreatePreKey(pubKey: String): PreKeyRecord { + return getPreKey(pubKey) ?: generateAndStorePreKey(pubKey) + } + + private fun generateAndStorePreKey(pubKey: String): PreKeyRecord { + val records = PreKeyUtil.generatePreKeys(context, 1) + PreKeyUtil.storePreKeyRecords(context, records) + + val record = records.first() + val database = databaseHelper.writableDatabase + + val values = ContentValues() + values.put(Companion.pubKey, pubKey) + values.put(preKeyId, record.id) + + database.insertWithOnConflict(tableName, null, values, SQLiteDatabase.CONFLICT_REPLACE) + return record + } +} \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/loki/LokiDatabaseUtils.kt b/src/org/thoughtcrime/securesms/loki/LokiDatabaseUtils.kt new file mode 100644 index 0000000000..312702307f --- /dev/null +++ b/src/org/thoughtcrime/securesms/loki/LokiDatabaseUtils.kt @@ -0,0 +1,30 @@ +package org.thoughtcrime.securesms.loki + +import net.sqlcipher.Cursor +import net.sqlcipher.database.SQLiteDatabase +import org.thoughtcrime.securesms.util.Base64 + +fun SQLiteDatabase.get(table: String, query: String, arguments: Array, get: (Cursor) -> T): T? { + var cursor: Cursor? = null + try { + cursor = this.query(table, null, query, arguments, null, null, null) + if (cursor != null && cursor.moveToFirst()) { return get(cursor) } + } catch (e: Exception) { + // Do nothing + } finally { + cursor?.close() + } + return null +} + +fun Cursor.getInt(columnName: String): Int { + return this.getInt(this.getColumnIndexOrThrow(columnName)) +} + +fun Cursor.getString(columnName: String): String { + return this.getString(this.getColumnIndexOrThrow(columnName)) +} + +fun Cursor.getBase64Bytes(columnName: String): ByteArray { + return Base64.decode(this.getString(columnName)) +} \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/loki/LokiPreKeyBundleDatabase.kt b/src/org/thoughtcrime/securesms/loki/LokiPreKeyBundleDatabase.kt new file mode 100644 index 0000000000..a16ea4819b --- /dev/null +++ b/src/org/thoughtcrime/securesms/loki/LokiPreKeyBundleDatabase.kt @@ -0,0 +1,121 @@ +package org.thoughtcrime.securesms.loki + +import android.content.ContentValues +import android.content.Context +import net.sqlcipher.database.SQLiteDatabase +import org.thoughtcrime.securesms.crypto.IdentityKeyUtil +import org.thoughtcrime.securesms.crypto.PreKeyUtil +import org.thoughtcrime.securesms.database.Database +import org.thoughtcrime.securesms.database.DatabaseFactory +import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper +import org.thoughtcrime.securesms.util.Base64 +import org.thoughtcrime.securesms.util.TextSecurePreferences +import org.whispersystems.libsignal.IdentityKey +import org.whispersystems.libsignal.ecc.Curve +import org.whispersystems.libsignal.state.PreKeyBundle + +/** + * A database for associating a `PreKeyBundle` to a contact public key. + */ +class LokiPreKeyBundleDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper) { + companion object { + private val tableName = "loki_pre_key_bundle_database" + private val pubKey = "pub_key" + private val preKeyId = "pre_key_id" + private val preKeyPublic = "pre_key_public" + private val signedPreKeyId = "signed_pre_key_id" + private val signedPreKeyPublic = "signed_pre_key_public" + private val signedPreKeySignature = "signed_pre_key_signature" + private val identityKey = "identity_key" + private val deviceId = "device_id" + private val registrationId = "registration_id" + + @JvmStatic + val createTableCommand = "CREATE TABLE $tableName (" + + "$pubKey TEXT PRIMARY KEY" + + "$preKeyId INTEGER," + + "$preKeyPublic TEXT NOT NULL" + + "$signedPreKeyId INTEGER" + + "$signedPreKeyPublic TEXT NOT NULL" + + "$signedPreKeySignature TEXT" + + "$identityKey TEXT NOT NULL" + + "$deviceId INTEGER" + + "$registrationId INTEGER" + + ");" + } + + /** + * Generate a `PreKeyBundle` for the given contact. + * This generated bundle shouldn't be stored locally since this is used to generate bundles to send to other users. + * @param pubKey String The hex encoded public key of the contact + * @return PreKeyBundle? A bundle or null if something went wrong + */ + fun generatePreKeyBundle(pubKey: String): PreKeyBundle? { + // TODO: Check if we have pre keys + val identityKeyPair = IdentityKeyUtil.getIdentityKeyPair(context) + + val signedPreKey = PreKeyUtil.getActiveSignedPreKey(context) + if (signedPreKey == null) return null + + val preKeyRecord = DatabaseFactory.getLokiContactPreKeyDatabase(context).getOrCreatePreKey(pubKey) + val registrationId = TextSecurePreferences.getLocalRegistrationId(context) + if (registrationId == 0) return null + + // TODO: This is the primary device id, we may want to change it to support multiple devices? + val deviceId = 1 + + return PreKeyBundle(registrationId, deviceId,preKeyRecord.id, preKeyRecord.keyPair.publicKey, signedPreKey.id, signedPreKey.keyPair.publicKey, signedPreKey.signature, identityKeyPair.publicKey) + } + + /** + * Get the `PreKeyBundle` associated with the given contact. + * @param pubKey String The hex encoded public key of the contact. + * @return PreKeyBundle? The prekey bundle or null if it doesn't exist + */ + fun getPreKeyBundle(pubKey: String): PreKeyBundle? { + val database = databaseHelper.readableDatabase + return database.get(tableName, "${Companion.pubKey} = ?", arrayOf(pubKey)) { cursor -> + val registrationId = cursor.getInt(registrationId) + val deviceId = cursor.getInt(deviceId) + val preKeyId = cursor.getInt(preKeyId) + val preKey = Curve.decodePoint(cursor.getBase64Bytes(preKeyPublic), 0) + val signedPreKeyId = cursor.getInt(signedPreKeyId) + val signedPreKey = Curve.decodePoint(cursor.getBase64Bytes(signedPreKeyPublic), 0) + val signedPreKeySignature = cursor.getBase64Bytes(signedPreKeySignature) + val identityKey = IdentityKey(cursor.getBase64Bytes(identityKey), 0) + + PreKeyBundle(registrationId, deviceId, preKeyId, preKey, signedPreKeyId, signedPreKey, signedPreKeySignature, identityKey) + } + } + + /** + * Set the `PreKeyBundle` fore the given contact. + * @param pubKey String The hex encoded public key of the contact + * @param preKeyBundle PreKeyBundle The pre key bundle + */ + fun setPreKeyBundle(pubKey: String, preKeyBundle: PreKeyBundle) { + val database = databaseHelper.writableDatabase + val contentValues = ContentValues() + contentValues.put(registrationId, preKeyBundle.registrationId) + contentValues.put(deviceId, preKeyBundle.deviceId) + contentValues.put(preKeyId, preKeyBundle.preKeyId) + contentValues.put(preKeyPublic, Base64.encodeBytes(preKeyBundle.preKey.serialize())) + contentValues.put(signedPreKeyId, preKeyBundle.signedPreKeyId) + contentValues.put(signedPreKeyPublic, Base64.encodeBytes(preKeyBundle.signedPreKey.serialize())) + contentValues.put(signedPreKeySignature, Base64.encodeBytes(preKeyBundle.signedPreKeySignature)) + contentValues.put(identityKey, Base64.encodeBytes(preKeyBundle.identityKey.serialize())) + contentValues.put(Companion.pubKey, pubKey) + + database.insertWithOnConflict(tableName, null, contentValues, SQLiteDatabase.CONFLICT_REPLACE) + } + + /** + * Remove the `PreKeyBundle` for the given contact. + * @param pubKey String The hex encoded public key of the contact + */ + fun removePreKeyBundle(pubKey: String) { + val database = databaseHelper.writableDatabase + database.delete(tableName, "${Companion.pubKey} = ?", arrayOf(pubKey)) + } + +} \ No newline at end of file