package org.thoughtcrime.securesms.database; import android.app.Application; import android.content.Context; import android.database.Cursor; import android.text.TextUtils; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.facebook.flipper.plugins.databases.DatabaseDescriptor; import com.facebook.flipper.plugins.databases.DatabaseDriver; import net.zetetic.database.DatabaseUtils; import net.zetetic.database.sqlcipher.SQLiteDatabase; import net.zetetic.database.sqlcipher.SQLiteStatement; import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.util.Hex; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Arrays; 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} * and made to work with SqlCipher. Unfortunately I couldn't use it directly, nor subclass it. */ public class FlipperSqlCipherAdapter extends DatabaseDriver { private static final String TAG = Log.tag(FlipperSqlCipherAdapter.class); public FlipperSqlCipherAdapter(Context context) { super(context); } @Override public List getDatabases() { try { SignalDatabaseOpenHelper mainOpenHelper = Objects.requireNonNull(SignalDatabase.getInstance()); SignalDatabaseOpenHelper keyValueOpenHelper = KeyValueDatabase.getInstance((Application) getContext()); SignalDatabaseOpenHelper megaphoneOpenHelper = MegaphoneDatabase.getInstance((Application) getContext()); SignalDatabaseOpenHelper jobManagerOpenHelper = JobDatabase.getInstance((Application) getContext()); SignalDatabaseOpenHelper metricsOpenHelper = LocalMetricsDatabase.getInstance((Application) getContext()); return Arrays.asList(new Descriptor(mainOpenHelper), new Descriptor(keyValueOpenHelper), new Descriptor(megaphoneOpenHelper), new Descriptor(jobManagerOpenHelper), new Descriptor(metricsOpenHelper)); } catch (Exception e) { Log.i(TAG, "Unable to use reflection to access raw database.", e); } return Collections.emptyList(); } @Override public List getTableNames(Descriptor descriptor) { SQLiteDatabase db = descriptor.getReadable(); List tableNames = new ArrayList<>(); try (Cursor cursor = db.rawQuery("SELECT name FROM sqlite_master WHERE type IN (?, ?)", new String[] { "table", "view" })) { while (cursor != null && cursor.moveToNext()) { tableNames.add(cursor.getString(0)); } } return tableNames; } @Override public DatabaseGetTableDataResponse getTableData(Descriptor descriptor, String table, String order, boolean reverse, int start, int count) { SQLiteDatabase db = descriptor.getReadable(); long total = DatabaseUtils.queryNumEntries(db, table); String orderBy = order != null ? order + (reverse ? " DESC" : " ASC") : null; String limitBy = start + ", " + count; try (Cursor cursor = db.query(table, null, null, null, null, null, orderBy, limitBy)) { String[] columnNames = cursor.getColumnNames(); List> rows = cursorToList(cursor); return new DatabaseGetTableDataResponse(Arrays.asList(columnNames), rows, start, rows.size(), total); } } @Override public DatabaseGetTableStructureResponse getTableStructure(Descriptor descriptor, String table) { SQLiteDatabase db = descriptor.getReadable(); Map foreignKeyValues = new HashMap<>(); try(Cursor cursor = db.rawQuery("PRAGMA foreign_key_list(" + table + ")", null)) { while (cursor != null && cursor.moveToNext()) { String from = cursor.getString(cursor.getColumnIndex("from")); String to = cursor.getString(cursor.getColumnIndex("to")); String tableName = cursor.getString(cursor.getColumnIndex("table")) + "(" + to + ")"; foreignKeyValues.put(from, tableName); } } List structureColumns = Arrays.asList("column_name", "data_type", "nullable", "default", "primary_key", "foreign_key"); List> structureValues = new ArrayList<>(); try (Cursor cursor = db.rawQuery("PRAGMA table_info(" + table + ")", null)) { while (cursor != null && cursor.moveToNext()) { String columnName = cursor.getString(cursor.getColumnIndex("name")); String foreignKey = foreignKeyValues.containsKey(columnName) ? foreignKeyValues.get(columnName) : null; structureValues.add(Arrays.asList(columnName, cursor.getString(cursor.getColumnIndex("type")), cursor.getInt(cursor.getColumnIndex("notnull")) == 0, getObjectFromColumnIndex(cursor, cursor.getColumnIndex("dflt_value")), cursor.getInt(cursor.getColumnIndex("pk")) == 1, foreignKey)); } } List indexesColumns = Arrays.asList("index_name", "unique", "indexed_column_name"); List> indexesValues = new ArrayList<>(); try (Cursor indexesCursor = db.rawQuery("PRAGMA index_list(" + table + ")", null)) { List indexedColumnNames = new ArrayList<>(); String indexName = indexesCursor.getString(indexesCursor.getColumnIndex("name")); try(Cursor indexInfoCursor = db.rawQuery("PRAGMA index_info(" + indexName + ")", null)) { while (indexInfoCursor.moveToNext()) { indexedColumnNames.add(indexInfoCursor.getString(indexInfoCursor.getColumnIndex("name"))); } } indexesValues.add(Arrays.asList(indexName, indexesCursor.getInt(indexesCursor.getColumnIndex("unique")) == 1, TextUtils.join(",", indexedColumnNames))); } return new DatabaseGetTableStructureResponse(structureColumns, structureValues, indexesColumns, indexesValues); } @Override public DatabaseGetTableInfoResponse getTableInfo(Descriptor databaseDescriptor, String table) { SQLiteDatabase db = databaseDescriptor.getReadable(); try (Cursor cursor = db.rawQuery("SELECT sql FROM sqlite_master WHERE name = ?", new String[] { table })) { cursor.moveToFirst(); return new DatabaseGetTableInfoResponse(cursor.getString(cursor.getColumnIndex("sql"))); } } @Override public DatabaseExecuteSqlResponse executeSQL(Descriptor descriptor, String query) { SQLiteDatabase db = descriptor.getWritable(); String firstWordUpperCase = getFirstWord(query).toUpperCase(); switch (firstWordUpperCase) { case "UPDATE": case "DELETE": return executeUpdateDelete(db, query); case "INSERT": return executeInsert(db, query); case "SELECT": case "PRAGMA": case "EXPLAIN": return executeSelect(db, query); default: return executeRawQuery(db, query); } } private static String getFirstWord(String s) { s = s.trim(); int firstSpace = s.indexOf(' '); return firstSpace >= 0 ? s.substring(0, firstSpace) : s; } private static DatabaseExecuteSqlResponse executeUpdateDelete(SQLiteDatabase database, String query) { SQLiteStatement statement = database.compileStatement(query); int count = statement.executeUpdateDelete(); return DatabaseExecuteSqlResponse.successfulUpdateDelete(count); } private static DatabaseExecuteSqlResponse executeInsert(SQLiteDatabase database, String query) { SQLiteStatement statement = database.compileStatement(query); long insertedId = statement.executeInsert(); return DatabaseExecuteSqlResponse.successfulInsert(insertedId); } private static DatabaseExecuteSqlResponse executeSelect(SQLiteDatabase database, String query) { try (Cursor cursor = database.rawQuery(query, null)) { String[] columnNames = cursor.getColumnNames(); List> rows = cursorToList(cursor); return DatabaseExecuteSqlResponse.successfulSelect(Arrays.asList(columnNames), rows); } } private static DatabaseExecuteSqlResponse executeRawQuery(SQLiteDatabase database, String query) { database.execSQL(query); return DatabaseExecuteSqlResponse.successfulRawQuery(); } private static @NonNull List> cursorToList(Cursor cursor) { List> rows = new ArrayList<>(); int numColumns = cursor.getColumnCount(); while (cursor.moveToNext()) { List values = new ArrayList<>(numColumns); for (int column = 0; column < numColumns; column++) { values.add(getObjectFromColumnIndex(cursor, column)); } rows.add(values); } return rows; } private static @Nullable Object getObjectFromColumnIndex(Cursor cursor, int column) { switch (cursor.getType(column)) { case Cursor.FIELD_TYPE_NULL: return null; case Cursor.FIELD_TYPE_INTEGER: return cursor.getLong(column); case Cursor.FIELD_TYPE_FLOAT: return cursor.getDouble(column); case Cursor.FIELD_TYPE_BLOB: byte[] blob = cursor.getBlob(column); String bytes = blob != null ? "(blob) " + Hex.toStringCondensed(Arrays.copyOf(blob, Math.min(blob.length, 32))) : null; if (bytes != null && bytes.length() == 32 && blob.length > 32) { bytes += "..."; } return bytes; case Cursor.FIELD_TYPE_STRING: default: return cursor.getString(column); } } static class Descriptor implements DatabaseDescriptor { private final SignalDatabaseOpenHelper sqlCipherOpenHelper; Descriptor(@NonNull SignalDatabaseOpenHelper sqlCipherOpenHelper) { this.sqlCipherOpenHelper = sqlCipherOpenHelper; } @Override public String name() { return sqlCipherOpenHelper.getDatabaseName(); } public @NonNull SQLiteDatabase getReadable() { return sqlCipherOpenHelper.getSqlCipherDatabase(); } public @NonNull SQLiteDatabase getWritable() { return sqlCipherOpenHelper.getSqlCipherDatabase(); } } }