diff --git a/app/src/flipper/java/org/thoughtcrime/securesms/database/FlipperSqlCipherAdapter.java b/app/src/flipper/java/org/thoughtcrime/securesms/database/FlipperSqlCipherAdapter.java index 7d6dd0106..a15dfb129 100644 --- a/app/src/flipper/java/org/thoughtcrime/securesms/database/FlipperSqlCipherAdapter.java +++ b/app/src/flipper/java/org/thoughtcrime/securesms/database/FlipperSqlCipherAdapter.java @@ -1,5 +1,6 @@ package org.thoughtcrime.securesms.database; +import android.app.Application; import android.content.Context; import android.database.Cursor; import android.text.TextUtils; @@ -24,6 +25,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; /** * A lot of this code is taken from {@link com.facebook.flipper.plugins.databases.impl.SqliteDatabaseDriver} @@ -42,8 +44,12 @@ public class FlipperSqlCipherAdapter extends DatabaseDriver= Build.VERSION_CODES.M) { + if (Build.VERSION.SDK_INT >= 23) { KeyStoreHelper.SealedData encryptedSecret = KeyStoreHelper.seal(databaseSecret.asBytes()); TextSecurePreferences.setDatabaseEncryptedSecret(context, encryptedSecret.serialize()); } else { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/DatabaseFactory.java b/app/src/main/java/org/thoughtcrime/securesms/database/DatabaseFactory.java index c54e0a778..0c7613a29 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/DatabaseFactory.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/DatabaseFactory.java @@ -32,6 +32,7 @@ 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.migrations.LegacyMigrationJob; +import org.thoughtcrime.securesms.util.SqlUtil; import org.thoughtcrime.securesms.util.TextSecurePreferences; public class DatabaseFactory { @@ -61,7 +62,6 @@ public class DatabaseFactory { private final JobDatabase jobDatabase; private final StickerDatabase stickerDatabase; private final StorageKeyDatabase storageKeyDatabase; - private final KeyValueDatabase keyValueDatabase; private final MegaphoneDatabase megaphoneDatabase; private final RemappedRecordsDatabase remappedRecordsDatabase; private final MentionDatabase mentionDatabase; @@ -157,10 +157,6 @@ public class DatabaseFactory { return getInstance(context).storageKeyDatabase; } - public static KeyValueDatabase getKeyValueDatabase(Context context) { - return getInstance(context).keyValueDatabase; - } - public static MegaphoneDatabase getMegaphoneDatabase(Context context) { return getInstance(context).megaphoneDatabase; } @@ -182,6 +178,7 @@ public class DatabaseFactory { getInstance(context).databaseHelper.onUpgrade(database, database.getVersion(), -1); getInstance(context).databaseHelper.markCurrent(database); getInstance(context).mms.trimEntriesForExpiredMessages(); + getInstance(context).getRawDatabase().rawExecSQL("DROP TABLE IF EXISTS key_value"); instance.databaseHelper.close(); instance = null; @@ -195,7 +192,7 @@ public class DatabaseFactory { private DatabaseFactory(@NonNull Context context) { SQLiteDatabase.loadLibs(context); - DatabaseSecret databaseSecret = new DatabaseSecretProvider(context).getOrCreateDatabaseSecret(); + DatabaseSecret databaseSecret = DatabaseSecretProvider.getOrCreateDatabaseSecret(context); AttachmentSecret attachmentSecret = AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret(); this.databaseHelper = new SQLCipherOpenHelper(context, databaseSecret); @@ -219,7 +216,6 @@ public class DatabaseFactory { this.jobDatabase = new JobDatabase(context, databaseHelper); this.stickerDatabase = new StickerDatabase(context, databaseHelper, attachmentSecret); this.storageKeyDatabase = new StorageKeyDatabase(context, databaseHelper); - this.keyValueDatabase = new KeyValueDatabase(context, databaseHelper); this.megaphoneDatabase = new MegaphoneDatabase(context, databaseHelper); this.remappedRecordsDatabase = new RemappedRecordsDatabase(context, databaseHelper); this.mentionDatabase = new MentionDatabase(context, databaseHelper); @@ -252,4 +248,12 @@ public class DatabaseFactory { public void triggerDatabaseAccess() { databaseHelper.getWritableDatabase(); } + + public SQLiteDatabase getRawDatabase() { + return databaseHelper.getWritableDatabase().getSqlCipherDatabase(); + } + + public boolean hasTable(String table) { + return SqlUtil.tableExists(databaseHelper.getReadableDatabase().getSqlCipherDatabase(), table); + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/KeyValueDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/KeyValueDatabase.java index a4981c039..47275558e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/KeyValueDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/KeyValueDatabase.java @@ -1,42 +1,118 @@ package org.thoughtcrime.securesms.database; +import android.app.Application; import android.content.ContentValues; -import android.content.Context; import android.database.Cursor; import androidx.annotation.NonNull; -import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; +import net.sqlcipher.database.SQLiteDatabase; +import net.sqlcipher.database.SQLiteDatabaseHook; +import net.sqlcipher.database.SQLiteOpenHelper; + +import org.signal.core.util.logging.Log; +import org.thoughtcrime.securesms.crypto.DatabaseSecret; +import org.thoughtcrime.securesms.crypto.DatabaseSecretProvider; import org.thoughtcrime.securesms.keyvalue.KeyValueDataSet; +import org.thoughtcrime.securesms.util.CursorUtil; +import org.thoughtcrime.securesms.util.SqlUtil; import java.util.Collection; import java.util.Map; -public class KeyValueDatabase extends Database { +/** + * Persists data for the {@link org.thoughtcrime.securesms.keyvalue.KeyValueStore}. + * + * This is it's own separate physical database, so it cannot do joins or queries with any other + * tables. + */ +public class KeyValueDatabase extends SQLiteOpenHelper implements SignalDatabase { - public static final String TABLE_NAME = "key_value"; + private static final String TAG = Log.tag(KeyValueDatabase.class); - private static final String ID = "_id"; - private static final String KEY = "key"; - private static final String VALUE = "value"; - private static final String TYPE = "type"; + private static final int DATABASE_VERSION = 1; + private static final String DATABASE_NAME = "signal-key-value.db"; - public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + "(" + ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + - KEY + " TEXT UNIQUE, " + - VALUE + " TEXT, " + - TYPE + " INTEGER)"; + private static final String TABLE_NAME = "key_value"; + private static final String ID = "_id"; + private static final String KEY = "key"; + private static final String VALUE = "value"; + private static final String TYPE = "type"; - KeyValueDatabase(Context context, SQLCipherOpenHelper databaseHelper) { - super(context, databaseHelper); + private static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + "(" + ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " + + KEY + " TEXT UNIQUE, " + + VALUE + " TEXT, " + + TYPE + " INTEGER)"; + + private static volatile KeyValueDatabase instance; + + private final Application application; + private final DatabaseSecret databaseSecret; + + public static @NonNull KeyValueDatabase getInstance(@NonNull Application context) { + if (instance == null) { + synchronized (KeyValueDatabase.class) { + if (instance == null) { + instance = new KeyValueDatabase(context, DatabaseSecretProvider.getOrCreateDatabaseSecret(context)); + } + } + } + return instance; + } + + public KeyValueDatabase(@NonNull Application application, @NonNull DatabaseSecret databaseSecret) { + super(application, DATABASE_NAME, null, DATABASE_VERSION, new SQLiteDatabaseHook() { + @Override + public void preKey(SQLiteDatabase db) { + db.rawExecSQL("PRAGMA cipher_default_kdf_iter = 1;"); + db.rawExecSQL("PRAGMA cipher_default_page_size = 4096;"); + } + + @Override + public void postKey(SQLiteDatabase db) { + db.rawExecSQL("PRAGMA kdf_iter = '1';"); + db.rawExecSQL("PRAGMA cipher_page_size = 4096;"); + } + }); + + this.application = application; + this.databaseSecret = databaseSecret; + } + + @Override + public void onCreate(SQLiteDatabase db) { + Log.i(TAG, "onCreate()"); + + db.execSQL(CREATE_TABLE); + + if (DatabaseFactory.getInstance(application).hasTable("key_value")) { + Log.i(TAG, "Found old key_value table. Migrating data."); + migrateDataFromPreviousDatabase(DatabaseFactory.getInstance(application).getRawDatabase(), db); + } + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + Log.i(TAG, "onUpgrade(" + oldVersion + ", " + newVersion + ")"); + } + + @Override + public void onOpen(SQLiteDatabase db) { + Log.i(TAG, "onOpen()"); + + if (DatabaseFactory.getInstance(application).hasTable("key_value")) { + Log.i(TAG, "Dropping original key_value table from the main database."); + DatabaseFactory.getInstance(application).getRawDatabase().rawExecSQL("DROP TABLE key_value"); + } } public @NonNull KeyValueDataSet getDataSet() { KeyValueDataSet dataSet = new KeyValueDataSet(); - try (Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, null, null, null, null, null, null)){ + try (Cursor cursor = getReadableDatabase().query(TABLE_NAME, null, null, null, null, null, null)){ while (cursor != null && cursor.moveToNext()) { - Type type = Type.fromId(cursor.getInt(cursor.getColumnIndexOrThrow(TYPE))); - String key = cursor.getString(cursor.getColumnIndexOrThrow(KEY)); + Type type = Type.fromId(cursor.getInt(cursor.getColumnIndexOrThrow(TYPE))); + String key = cursor.getString(cursor.getColumnIndexOrThrow(KEY)); switch (type) { case BLOB: @@ -65,7 +141,7 @@ public class KeyValueDatabase extends Database { } public void writeDataSet(@NonNull KeyValueDataSet dataSet, @NonNull Collection removes) { - SQLiteDatabase db = databaseHelper.getWritableDatabase(); + SQLiteDatabase db = getWritableDatabase(); db.beginTransaction(); try { @@ -113,6 +189,53 @@ public class KeyValueDatabase extends Database { } } + private @NonNull SQLiteDatabase getReadableDatabase() { + return getReadableDatabase(databaseSecret.asString()); + } + + private @NonNull SQLiteDatabase getWritableDatabase() { + return getWritableDatabase(databaseSecret.asString()); + } + + @Override + public @NonNull SQLiteDatabase getSqlCipherDatabase() { + return getWritableDatabase(); + } + + private static void migrateDataFromPreviousDatabase(@NonNull SQLiteDatabase oldDb, @NonNull SQLiteDatabase newDb) { + try (Cursor cursor = oldDb.rawQuery("SELECT * FROM key_value", null)) { + while (cursor.moveToNext()) { + int type = CursorUtil.requireInt(cursor, "type"); + ContentValues values = new ContentValues(); + values.put(KEY, CursorUtil.requireString(cursor, "key")); + values.put(TYPE, type); + + switch (type) { + case 0: + values.put(VALUE, CursorUtil.requireBlob(cursor, "value")); + break; + case 1: + values.put(VALUE, CursorUtil.requireBoolean(cursor, "value")); + break; + case 2: + values.put(VALUE, CursorUtil.requireFloat(cursor, "value")); + break; + case 3: + values.put(VALUE, CursorUtil.requireInt(cursor, "value")); + break; + case 4: + values.put(VALUE, CursorUtil.requireLong(cursor, "value")); + break; + case 5: + values.put(VALUE, CursorUtil.requireString(cursor, "value")); + break; + } + + newDb.insert(TABLE_NAME, null, values); + } + } + } + private enum Type { BLOB(0), BOOLEAN(1), FLOAT(2), INTEGER(3), LONG(4), STRING(5); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SignalDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/SignalDatabase.java new file mode 100644 index 000000000..46130e8b9 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SignalDatabase.java @@ -0,0 +1,14 @@ +package org.thoughtcrime.securesms.database; + +import androidx.annotation.NonNull; + +import net.sqlcipher.database.SQLiteDatabase; + +/** + * Simple interface for common methods across our various + * {@link net.sqlcipher.database.SQLiteOpenHelper}s. + */ +public interface SignalDatabase { + SQLiteDatabase getSqlCipherDatabase(); + String getDatabaseName(); +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java index d744550bf..1348aaa62 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java @@ -41,6 +41,7 @@ import org.thoughtcrime.securesms.database.RecipientDatabase; import org.thoughtcrime.securesms.database.RemappedRecordsDatabase; import org.thoughtcrime.securesms.database.SearchDatabase; import org.thoughtcrime.securesms.database.SessionDatabase; +import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.database.SignedPreKeyDatabase; import org.thoughtcrime.securesms.database.SmsDatabase; import org.thoughtcrime.securesms.database.StickerDatabase; @@ -76,7 +77,7 @@ import java.util.List; import java.util.Locale; import java.util.Set; -public class SQLCipherOpenHelper extends SQLiteOpenHelper { +public class SQLCipherOpenHelper extends SQLiteOpenHelper implements SignalDatabase { @SuppressWarnings("unused") private static final String TAG = SQLCipherOpenHelper.class.getSimpleName(); @@ -208,7 +209,6 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { db.execSQL(SessionDatabase.CREATE_TABLE); db.execSQL(StickerDatabase.CREATE_TABLE); db.execSQL(StorageKeyDatabase.CREATE_TABLE); - db.execSQL(KeyValueDatabase.CREATE_TABLE); db.execSQL(MegaphoneDatabase.CREATE_TABLE); db.execSQL(MentionDatabase.CREATE_TABLE); executeStatements(db, SearchDatabase.CREATE_TABLE); @@ -1264,6 +1264,11 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { return new org.thoughtcrime.securesms.database.SQLiteDatabase(getWritableDatabase(databaseSecret.asString())); } + @Override + public @NonNull SQLiteDatabase getSqlCipherDatabase() { + return getWritableDatabase().getSqlCipherDatabase(); + } + public void markCurrent(SQLiteDatabase db) { db.setVersion(DATABASE_VERSION); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/KeyValueStore.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/KeyValueStore.java index a37146724..953e500e2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/KeyValueStore.java +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/KeyValueStore.java @@ -1,5 +1,6 @@ package org.thoughtcrime.securesms.keyvalue; +import android.app.Application; import android.content.Context; import androidx.annotation.AnyThread; @@ -38,9 +39,9 @@ public final class KeyValueStore implements KeyValueReader { private KeyValueDataSet dataSet; - public KeyValueStore(@NonNull Context context) { + public KeyValueStore(@NonNull Application application) { this.executor = SignalExecutors.newCachedSingleThreadExecutor("signal-KeyValueStore"); - this.database = DatabaseFactory.getKeyValueDatabase(context); + this.database = KeyValueDatabase.getInstance(application); } @AnyThread diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/CursorUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/CursorUtil.java index 67fcc1625..83665faa6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/CursorUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/CursorUtil.java @@ -18,6 +18,10 @@ public final class CursorUtil { return cursor.getInt(cursor.getColumnIndexOrThrow(column)); } + public static float requireFloat(@NonNull Cursor cursor, @NonNull String column) { + return cursor.getFloat(cursor.getColumnIndexOrThrow(column)); + } + public static long requireLong(@NonNull Cursor cursor, @NonNull String column) { return cursor.getLong(cursor.getColumnIndexOrThrow(column)); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/SqlUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/SqlUtil.java index c7f425899..506df0fdd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/SqlUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/SqlUtil.java @@ -27,6 +27,16 @@ public final class SqlUtil { } } + public static boolean isEmpty(@NonNull SQLiteDatabase db, @NonNull String table) { + try (Cursor cursor = db.rawQuery("SELECT COUNT(*) FROM " + table, null)) { + if (cursor.moveToFirst()) { + return cursor.getInt(0) == 0; + } else { + return true; + } + } + } + public static boolean columnExists(@NonNull SQLiteDatabase db, @NonNull String table, @NonNull String column) { try (Cursor cursor = db.rawQuery("PRAGMA table_info(" + table + ")", null)) { int nameColumnIndex = cursor.getColumnIndexOrThrow("name");