Move the KeyValueDatabase to a separate physical database.

fork-5.53.8
Greyson Parrelli 2021-01-05 11:50:20 -05:00 zatwierdzone przez Alan Evans
rodzic 46d412a6c3
commit c466dba8c4
10 zmienionych plików z 228 dodań i 50 usunięć

Wyświetl plik

@ -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<FlipperSqlCipherAdap
try {
Field databaseHelperField = DatabaseFactory.class.getDeclaredField("databaseHelper");
databaseHelperField.setAccessible(true);
SQLCipherOpenHelper sqlCipherOpenHelper = (SQLCipherOpenHelper) databaseHelperField.get(DatabaseFactory.getInstance(getContext()));
return Collections.singletonList(new Descriptor(sqlCipherOpenHelper));
SignalDatabase mainOpenHelper = Objects.requireNonNull((SQLCipherOpenHelper) databaseHelperField.get(DatabaseFactory.getInstance(getContext())));
SignalDatabase keyValueOpenHelper = KeyValueDatabase.getInstance((Application) getContext());
return Arrays.asList(new Descriptor(mainOpenHelper), new Descriptor(keyValueOpenHelper));
} catch (Exception e) {
Log.i(TAG, "Unable to use reflection to access raw database.", e);
}
@ -235,9 +241,9 @@ public class FlipperSqlCipherAdapter extends DatabaseDriver<FlipperSqlCipherAdap
}
static class Descriptor implements DatabaseDescriptor {
private final SQLCipherOpenHelper sqlCipherOpenHelper;
private final SignalDatabase sqlCipherOpenHelper;
Descriptor(@NonNull SQLCipherOpenHelper sqlCipherOpenHelper) {
Descriptor(@NonNull SignalDatabase sqlCipherOpenHelper) {
this.sqlCipherOpenHelper = sqlCipherOpenHelper;
}
@ -247,11 +253,11 @@ public class FlipperSqlCipherAdapter extends DatabaseDriver<FlipperSqlCipherAdap
}
public @NonNull SQLiteDatabase getReadable() {
return sqlCipherOpenHelper.getReadableDatabase().getSqlCipherDatabase();
return sqlCipherOpenHelper.getSqlCipherDatabase();
}
public @NonNull SQLiteDatabase getWritable() {
return sqlCipherOpenHelper.getWritableDatabase().getSqlCipherDatabase();
return sqlCipherOpenHelper.getSqlCipherDatabase();
}
}
}

Wyświetl plik

@ -77,8 +77,7 @@ public class FullBackupExporter extends FullBackupBase {
SearchDatabase.MMS_FTS_TABLE_NAME,
JobDatabase.JOBS_TABLE_NAME,
JobDatabase.CONSTRAINTS_TABLE_NAME,
JobDatabase.DEPENDENCIES_TABLE_NAME,
KeyValueDatabase.TABLE_NAME
JobDatabase.DEPENDENCIES_TABLE_NAME
);
public static void export(@NonNull Context context,

Wyświetl plik

@ -11,18 +11,30 @@ import org.thoughtcrime.securesms.util.TextSecurePreferences;
import java.io.IOException;
import java.security.SecureRandom;
public class DatabaseSecretProvider {
/**
* It can be rather expensive to read from the keystore, so this class caches the key in memory
* after it is created.
*/
public final class DatabaseSecretProvider {
@SuppressWarnings("unused")
private static final String TAG = DatabaseSecretProvider.class.getSimpleName();
private static volatile DatabaseSecret instance;
private final Context context;
public static DatabaseSecret getOrCreateDatabaseSecret(@NonNull Context context) {
if (instance == null) {
synchronized (DatabaseSecretProvider.class) {
if (instance == null) {
instance = getOrCreate(context);
}
}
}
public DatabaseSecretProvider(@NonNull Context context) {
this.context = context.getApplicationContext();
return instance;
}
public DatabaseSecret getOrCreateDatabaseSecret() {
private DatabaseSecretProvider() {
}
private static @NonNull DatabaseSecret getOrCreate(@NonNull Context context) {
String unencryptedSecret = TextSecurePreferences.getDatabaseUnencryptedSecret(context);
String encryptedSecret = TextSecurePreferences.getDatabaseEncryptedSecret(context);
@ -31,12 +43,12 @@ public class DatabaseSecretProvider {
else return createAndStoreDatabaseSecret(context);
}
private DatabaseSecret getUnencryptedDatabaseSecret(@NonNull Context context, @NonNull String unencryptedSecret)
private static @NonNull DatabaseSecret getUnencryptedDatabaseSecret(@NonNull Context context, @NonNull String unencryptedSecret)
{
try {
DatabaseSecret databaseSecret = new DatabaseSecret(unencryptedSecret);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
if (Build.VERSION.SDK_INT < 23) {
return databaseSecret;
} else {
KeyStoreHelper.SealedData encryptedSecret = KeyStoreHelper.seal(databaseSecret.asBytes());
@ -51,8 +63,8 @@ public class DatabaseSecretProvider {
}
}
private DatabaseSecret getEncryptedDatabaseSecret(@NonNull String serializedEncryptedSecret) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
private static @NonNull DatabaseSecret getEncryptedDatabaseSecret(@NonNull String serializedEncryptedSecret) {
if (Build.VERSION.SDK_INT < 23) {
throw new AssertionError("OS downgrade not supported. KeyStore sealed data exists on platform < M!");
} else {
KeyStoreHelper.SealedData encryptedSecret = KeyStoreHelper.SealedData.fromString(serializedEncryptedSecret);
@ -60,14 +72,14 @@ public class DatabaseSecretProvider {
}
}
private DatabaseSecret createAndStoreDatabaseSecret(@NonNull Context context) {
private static @NonNull DatabaseSecret createAndStoreDatabaseSecret(@NonNull Context context) {
SecureRandom random = new SecureRandom();
byte[] secret = new byte[32];
random.nextBytes(secret);
DatabaseSecret databaseSecret = new DatabaseSecret(secret);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (Build.VERSION.SDK_INT >= 23) {
KeyStoreHelper.SealedData encryptedSecret = KeyStoreHelper.seal(databaseSecret.asBytes());
TextSecurePreferences.setDatabaseEncryptedSecret(context, encryptedSecret.serialize());
} else {

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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