package org.thoughtcrime.securesms.database.helpers; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.text.TextUtils; import android.util.Log; import android.util.Pair; import com.annimon.stream.function.Function; import org.thoughtcrime.securesms.crypto.AsymmetricMasterCipher; import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider; import org.thoughtcrime.securesms.crypto.MasterCipher; import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecretUtil; import org.thoughtcrime.securesms.util.Base64; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.whispersystems.libsignal.InvalidMessageException; import java.io.IOException; public class SQLCipherMigrationHelper { private static final String TAG = SQLCipherMigrationHelper.class.getSimpleName(); private static final long ENCRYPTION_SYMMETRIC_BIT = 0x80000000; private static final long ENCRYPTION_ASYMMETRIC_BIT = 0x40000000; static void migratePlaintext(@NonNull android.database.sqlite.SQLiteDatabase legacyDb, @NonNull net.sqlcipher.database.SQLiteDatabase modernDb) { modernDb.beginTransaction(); try { copyTable("identities", legacyDb, modernDb, null); copyTable("push", legacyDb, modernDb, null); copyTable("groups", legacyDb, modernDb, null); copyTable("recipient_preferences", legacyDb, modernDb, null); copyTable("group_receipts", legacyDb, modernDb, null); modernDb.setTransactionSuccessful(); } finally { modernDb.endTransaction(); } } public static void migrateCiphertext(@NonNull Context context, @NonNull MasterSecret masterSecret, @NonNull android.database.sqlite.SQLiteDatabase legacyDb, @NonNull net.sqlcipher.database.SQLiteDatabase modernDb) { MasterCipher legacyCipher = new MasterCipher(masterSecret); AsymmetricMasterCipher legacyAsymmetricCipher = new AsymmetricMasterCipher(MasterSecretUtil.getAsymmetricMasterSecret(context, masterSecret)); modernDb.beginTransaction(); try { copyTable("sms", legacyDb, modernDb, (row) -> { Pair plaintext = getPlaintextBody(legacyCipher, legacyAsymmetricCipher, row.getAsLong("type"), row.getAsString("body")); row.put("body", plaintext.second); row.put("type", plaintext.first); return row; }); copyTable("mms", legacyDb, modernDb, (row) -> { Pair plaintext = getPlaintextBody(legacyCipher, legacyAsymmetricCipher, row.getAsLong("msg_box"), row.getAsString("body")); row.put("body", plaintext.second); row.put("msg_box", plaintext.first); return row; }); copyTable("part", legacyDb, modernDb, (row) -> { String fileName = row.getAsString("file_name"); String mediaKey = row.getAsString("cd"); try { if (!TextUtils.isEmpty(fileName)) { row.put("file_name", legacyCipher.decryptBody(fileName)); } } catch (InvalidMessageException e) { Log.w(TAG, e); } try { if (!TextUtils.isEmpty(mediaKey)) { byte[] plaintext; if (mediaKey.startsWith("?ASYNC-")) { plaintext = legacyAsymmetricCipher.decryptBytes(Base64.decode(mediaKey.substring("?ASYNC-".length()))); } else { plaintext = legacyCipher.decryptBytes(Base64.decode(mediaKey)); } row.put("cd", Base64.encodeBytes(plaintext)); } } catch (IOException | InvalidMessageException e) { Log.w(TAG, e); } return row; }); copyTable("thread", legacyDb, modernDb, (row) -> { Pair plaintext = getPlaintextBody(legacyCipher, legacyAsymmetricCipher, row.getAsLong("snippet_type"), row.getAsString("snippet")); row.put("snippet", plaintext.second); row.put("snippet_type", plaintext.first); return row; }); copyTable("drafts", legacyDb, modernDb, (row) -> { String draftType = row.getAsString("type"); String draft = row.getAsString("value"); try { if (!TextUtils.isEmpty(draftType)) row.put("type", legacyCipher.decryptBody(draftType)); if (!TextUtils.isEmpty(draft)) row.put("value", legacyCipher.decryptBody(draft)); } catch (InvalidMessageException e) { Log.w(TAG, e); } return row; }); TextSecurePreferences.setNeedsSqlCipherMigration(context, false); modernDb.setTransactionSuccessful(); } finally { modernDb.endTransaction(); } AttachmentSecretProvider.getInstance(context).setClassicKey(context, masterSecret.getEncryptionKey().getEncoded(), masterSecret.getMacKey().getEncoded()); } private static void copyTable(@NonNull String tableName, @NonNull android.database.sqlite.SQLiteDatabase legacyDb, @NonNull net.sqlcipher.database.SQLiteDatabase modernDb, @Nullable Function transformer) { try (Cursor cursor = legacyDb.query(tableName, null, null, null, null, null, null)) { while (cursor != null && cursor.moveToNext()) { ContentValues row = new ContentValues(); for (int i=0;i getPlaintextBody(@NonNull MasterCipher legacyCipher, @NonNull AsymmetricMasterCipher legacyAsymmetricCipher, long type, @Nullable String body) { try { if (!TextUtils.isEmpty(body)) { if ((type & ENCRYPTION_SYMMETRIC_BIT) != 0) body = legacyCipher.decryptBody(body); else if ((type & ENCRYPTION_ASYMMETRIC_BIT) != 0) body = legacyAsymmetricCipher.decryptBody(body); } } catch (InvalidMessageException | IOException e) { Log.w(TAG, e); } type &= ~(ENCRYPTION_SYMMETRIC_BIT); type &= ~(ENCRYPTION_ASYMMETRIC_BIT); return new Pair<>(type, body); } }