package org.thoughtcrime.securesms.database.helpers; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.os.SystemClock; import android.support.annotation.NonNull; import android.text.TextUtils; import android.util.Log; import net.sqlcipher.database.SQLiteDatabase; import net.sqlcipher.database.SQLiteDatabaseHook; import net.sqlcipher.database.SQLiteOpenHelper; import org.thoughtcrime.securesms.crypto.DatabaseSecret; import java.io.File; 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 MIGRATE_PREKEYS_VERSION = 3; private static final int MIGRATE_SESSIONS_VERSION = 4; private static final int NO_MORE_IMAGE_THUMBNAILS_VERSION = 5; private static final int ATTACHMENT_DIMENSIONS = 6; private static final int QUOTED_REPLIES = 7; private static final int SHARED_CONTACTS = 8; private static final int FULL_TEXT_SEARCH = 9; private static final int BAD_IMPORT_CLEANUP = 10; private static final int DATABASE_VERSION = 10; private static final String DATABASE_NAME = "signal.db"; private final Context context; private final DatabaseSecret databaseSecret; public SQLCipherOpenHelper(@NonNull Context context, @NonNull DatabaseSecret databaseSecret) { super(context, 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.context = context.getApplicationContext(); this.databaseSecret = databaseSecret; } @Override public void onCreate(SQLiteDatabase db) { } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { Log.w(TAG, "Upgrading database: " + oldVersion + ", " + newVersion); 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 0"); } 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 (oldVersion < MIGRATE_SESSIONS_VERSION) { db.execSQL("CREATE TABLE sessions (_id INTEGER PRIMARY KEY, address TEXT NOT NULL, device INTEGER NOT NULL, record BLOB NOT NULL, UNIQUE(address, device) ON CONFLICT REPLACE)"); // SessionStoreMigrationHelper.migrateSessions(context, db); } if (oldVersion < NO_MORE_IMAGE_THUMBNAILS_VERSION) { ContentValues update = new ContentValues(); update.put("thumbnail", (String)null); update.put("aspect_ratio", (String)null); update.put("thumbnail_random", (String)null); try (Cursor cursor = db.query("part", new String[] {"_id", "ct", "thumbnail"}, "thumbnail IS NOT NULL", null, null, null, null)) { while (cursor != null && cursor.moveToNext()) { long id = cursor.getLong(cursor.getColumnIndexOrThrow("_id")); String contentType = cursor.getString(cursor.getColumnIndexOrThrow("ct")); if (contentType != null && !contentType.startsWith("video")) { String thumbnailPath = cursor.getString(cursor.getColumnIndexOrThrow("thumbnail")); File thumbnailFile = new File(thumbnailPath); thumbnailFile.delete(); db.update("part", update, "_id = ?", new String[] {String.valueOf(id)}); } } } } if (oldVersion < ATTACHMENT_DIMENSIONS) { db.execSQL("ALTER TABLE part ADD COLUMN width INTEGER DEFAULT 0"); db.execSQL("ALTER TABLE part ADD COLUMN height INTEGER DEFAULT 0"); } if (oldVersion < QUOTED_REPLIES) { db.execSQL("ALTER TABLE mms ADD COLUMN quote_id INTEGER DEFAULT 0"); db.execSQL("ALTER TABLE mms ADD COLUMN quote_author TEXT"); db.execSQL("ALTER TABLE mms ADD COLUMN quote_body TEXT"); db.execSQL("ALTER TABLE mms ADD COLUMN quote_attachment INTEGER DEFAULT -1"); db.execSQL("ALTER TABLE part ADD COLUMN quote INTEGER DEFAULT 0"); } if (oldVersion < SHARED_CONTACTS) { db.execSQL("ALTER TABLE mms ADD COLUMN shared_contacts TEXT"); } if (oldVersion < FULL_TEXT_SEARCH) { Log.i(TAG, "Beginning to build search index."); long start = SystemClock.elapsedRealtime(); db.execSQL("INSERT INTO sms_fts (rowid, body) SELECT _id, body FROM sms"); long smsFinished = SystemClock.elapsedRealtime(); Log.i(TAG, "Indexing SMS completed in " + (smsFinished - start) + " ms"); db.execSQL("INSERT INTO mms_fts (rowid, body) SELECT _id, body FROM mms"); long mmsFinished = SystemClock.elapsedRealtime(); Log.i(TAG, "Indexing MMS completed in " + (mmsFinished - smsFinished) + " ms"); Log.i(TAG, "Indexing finished. Total time: " + (mmsFinished - start) + " ms"); } if (oldVersion < BAD_IMPORT_CLEANUP) { String trimmedCondition = " NOT IN (SELECT _id FROM mms)"; db.delete("group_receipts", "mms_id" + trimmedCondition, null); String[] columns = new String[] { "_id", "unique_id", "_data", "thumbnail"}; try (Cursor cursor = db.query("part", columns, "mid" + trimmedCondition, null, null, null, null)) { while (cursor != null && cursor.moveToNext()) { db.delete("part", "_id = ? AND unique_id = ?", new String[] { String.valueOf(cursor.getLong(0)), String.valueOf(cursor.getLong(1)) }); String data = cursor.getString(2); String thumbnail = cursor.getString(3); if (!TextUtils.isEmpty(data)) { new File(data).delete(); } if (!TextUtils.isEmpty(thumbnail)) { new File(thumbnail).delete(); } } } } db.setTransactionSuccessful(); } finally { db.endTransaction(); } } public SQLiteDatabase getReadableDatabase() { return getReadableDatabase(databaseSecret.asString()); } public SQLiteDatabase getWritableDatabase() { return getWritableDatabase(databaseSecret.asString()); } private void executeStatements(SQLiteDatabase db, String[] statements) { for (String statement : statements) db.execSQL(statement); } }