2018-01-25 03:17:44 +00:00
|
|
|
package org.thoughtcrime.securesms.database.helpers;
|
|
|
|
|
|
|
|
|
|
|
|
import android.content.ContentValues;
|
|
|
|
import android.content.Context;
|
|
|
|
import android.database.Cursor;
|
|
|
|
import android.text.TextUtils;
|
|
|
|
import android.util.Pair;
|
|
|
|
|
2020-12-04 23:31:58 +00:00
|
|
|
import androidx.annotation.NonNull;
|
|
|
|
import androidx.annotation.Nullable;
|
|
|
|
|
2018-02-02 00:01:24 +00:00
|
|
|
import com.annimon.stream.function.BiFunction;
|
2018-01-25 03:17:44 +00:00
|
|
|
|
2020-12-04 23:31:58 +00:00
|
|
|
import org.signal.core.util.logging.Log;
|
2018-02-02 00:01:24 +00:00
|
|
|
import org.thoughtcrime.securesms.R;
|
2018-01-25 03:17:44 +00:00
|
|
|
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;
|
2019-07-29 23:02:40 +00:00
|
|
|
import org.thoughtcrime.securesms.migrations.LegacyMigrationJob;
|
2018-02-02 00:01:24 +00:00
|
|
|
import org.thoughtcrime.securesms.service.GenericForegroundService;
|
2018-01-25 03:17:44 +00:00
|
|
|
import org.thoughtcrime.securesms.util.Base64;
|
|
|
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
|
|
|
import org.whispersystems.libsignal.InvalidMessageException;
|
|
|
|
|
|
|
|
import java.io.IOException;
|
2018-02-05 22:04:56 +00:00
|
|
|
import java.util.HashSet;
|
|
|
|
import java.util.Set;
|
2018-01-25 03:17:44 +00:00
|
|
|
|
|
|
|
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;
|
|
|
|
|
2018-02-02 00:01:24 +00:00
|
|
|
static void migratePlaintext(@NonNull Context context,
|
|
|
|
@NonNull android.database.sqlite.SQLiteDatabase legacyDb,
|
2018-01-25 03:17:44 +00:00
|
|
|
@NonNull net.sqlcipher.database.SQLiteDatabase modernDb)
|
|
|
|
{
|
|
|
|
modernDb.beginTransaction();
|
2019-06-27 16:18:52 +00:00
|
|
|
int foregroundId = GenericForegroundService.startForegroundTask(context, context.getString(R.string.SQLCipherMigrationHelper_migrating_signal_database)).getId();
|
2018-01-25 03:17:44 +00:00
|
|
|
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();
|
2019-06-27 16:18:52 +00:00
|
|
|
GenericForegroundService.stopForegroundTask(context, foregroundId);
|
2018-01-25 03:17:44 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public static void migrateCiphertext(@NonNull Context context,
|
|
|
|
@NonNull MasterSecret masterSecret,
|
|
|
|
@NonNull android.database.sqlite.SQLiteDatabase legacyDb,
|
2018-02-02 00:01:24 +00:00
|
|
|
@NonNull net.sqlcipher.database.SQLiteDatabase modernDb,
|
2019-07-29 23:02:40 +00:00
|
|
|
@Nullable LegacyMigrationJob.DatabaseUpgradeListener listener)
|
2018-01-25 03:17:44 +00:00
|
|
|
{
|
|
|
|
MasterCipher legacyCipher = new MasterCipher(masterSecret);
|
|
|
|
AsymmetricMasterCipher legacyAsymmetricCipher = new AsymmetricMasterCipher(MasterSecretUtil.getAsymmetricMasterSecret(context, masterSecret));
|
|
|
|
|
|
|
|
modernDb.beginTransaction();
|
|
|
|
|
2019-06-27 16:18:52 +00:00
|
|
|
int foregroundId = GenericForegroundService.startForegroundTask(context, context.getString(R.string.SQLCipherMigrationHelper_migrating_signal_database)).getId();
|
2018-01-25 03:17:44 +00:00
|
|
|
try {
|
2018-02-02 00:01:24 +00:00
|
|
|
int total = 5000;
|
|
|
|
|
|
|
|
copyTable("sms", legacyDb, modernDb, (row, progress) -> {
|
2018-01-25 03:17:44 +00:00
|
|
|
Pair<Long, String> plaintext = getPlaintextBody(legacyCipher, legacyAsymmetricCipher,
|
|
|
|
row.getAsLong("type"),
|
|
|
|
row.getAsString("body"));
|
|
|
|
|
|
|
|
row.put("body", plaintext.second);
|
|
|
|
row.put("type", plaintext.first);
|
|
|
|
|
2018-02-02 00:01:24 +00:00
|
|
|
if (listener != null && (progress.first % 1000 == 0)) {
|
|
|
|
listener.setProgress(getTotalProgress(0, progress.first, progress.second), total);
|
|
|
|
}
|
|
|
|
|
2018-01-25 03:17:44 +00:00
|
|
|
return row;
|
|
|
|
});
|
|
|
|
|
2018-02-02 00:01:24 +00:00
|
|
|
copyTable("mms", legacyDb, modernDb, (row, progress) -> {
|
2018-01-25 03:17:44 +00:00
|
|
|
Pair<Long, String> plaintext = getPlaintextBody(legacyCipher, legacyAsymmetricCipher,
|
|
|
|
row.getAsLong("msg_box"),
|
|
|
|
row.getAsString("body"));
|
|
|
|
|
|
|
|
row.put("body", plaintext.second);
|
|
|
|
row.put("msg_box", plaintext.first);
|
|
|
|
|
2018-02-02 00:01:24 +00:00
|
|
|
if (listener != null && (progress.first % 1000 == 0)) {
|
|
|
|
listener.setProgress(getTotalProgress(1000, progress.first, progress.second), total);
|
|
|
|
}
|
|
|
|
|
2018-01-25 03:17:44 +00:00
|
|
|
return row;
|
|
|
|
});
|
|
|
|
|
2018-02-02 00:01:24 +00:00
|
|
|
copyTable("part", legacyDb, modernDb, (row, progress) -> {
|
2018-01-25 03:17:44 +00:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2018-02-02 00:01:24 +00:00
|
|
|
if (listener != null && (progress.first % 1000 == 0)) {
|
|
|
|
listener.setProgress(getTotalProgress(2000, progress.first, progress.second), total);
|
|
|
|
}
|
2018-01-25 03:17:44 +00:00
|
|
|
|
|
|
|
return row;
|
|
|
|
});
|
|
|
|
|
2018-02-02 00:01:24 +00:00
|
|
|
copyTable("thread", legacyDb, modernDb, (row, progress) -> {
|
2018-04-03 15:59:19 +00:00
|
|
|
Long snippetType = row.getAsLong("snippet_type");
|
|
|
|
if (snippetType == null) snippetType = 0L;
|
|
|
|
|
2018-01-25 03:17:44 +00:00
|
|
|
Pair<Long, String> plaintext = getPlaintextBody(legacyCipher, legacyAsymmetricCipher,
|
2018-04-03 15:59:19 +00:00
|
|
|
snippetType, row.getAsString("snippet"));
|
2018-01-25 03:17:44 +00:00
|
|
|
|
|
|
|
row.put("snippet", plaintext.second);
|
|
|
|
row.put("snippet_type", plaintext.first);
|
|
|
|
|
2018-02-02 00:01:24 +00:00
|
|
|
if (listener != null && (progress.first % 1000 == 0)) {
|
|
|
|
listener.setProgress(getTotalProgress(3000, progress.first, progress.second), total);
|
|
|
|
}
|
|
|
|
|
2018-01-25 03:17:44 +00:00
|
|
|
return row;
|
|
|
|
});
|
|
|
|
|
|
|
|
|
2018-02-02 00:01:24 +00:00
|
|
|
copyTable("drafts", legacyDb, modernDb, (row, progress) -> {
|
2018-01-25 03:17:44 +00:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2018-02-02 00:01:24 +00:00
|
|
|
if (listener != null && (progress.first % 1000 == 0)) {
|
|
|
|
listener.setProgress(getTotalProgress(4000, progress.first, progress.second), total);
|
|
|
|
}
|
|
|
|
|
2018-01-25 03:17:44 +00:00
|
|
|
return row;
|
|
|
|
});
|
|
|
|
|
2018-02-02 00:01:24 +00:00
|
|
|
AttachmentSecretProvider.getInstance(context).setClassicKey(context, masterSecret.getEncryptionKey().getEncoded(), masterSecret.getMacKey().getEncoded());
|
2018-01-25 03:17:44 +00:00
|
|
|
TextSecurePreferences.setNeedsSqlCipherMigration(context, false);
|
|
|
|
modernDb.setTransactionSuccessful();
|
|
|
|
} finally {
|
|
|
|
modernDb.endTransaction();
|
2019-06-27 16:18:52 +00:00
|
|
|
GenericForegroundService.stopForegroundTask(context, foregroundId);
|
2018-01-25 03:17:44 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private static void copyTable(@NonNull String tableName,
|
|
|
|
@NonNull android.database.sqlite.SQLiteDatabase legacyDb,
|
|
|
|
@NonNull net.sqlcipher.database.SQLiteDatabase modernDb,
|
2018-02-02 00:01:24 +00:00
|
|
|
@Nullable BiFunction<ContentValues, Pair<Integer, Integer>, ContentValues> transformer)
|
2018-01-25 03:17:44 +00:00
|
|
|
{
|
2018-02-05 22:04:56 +00:00
|
|
|
Set<String> destinationColumns = getTableColumns(tableName, modernDb);
|
|
|
|
|
2018-01-25 03:17:44 +00:00
|
|
|
try (Cursor cursor = legacyDb.query(tableName, null, null, null, null, null, null)) {
|
2018-02-02 00:01:24 +00:00
|
|
|
int count = (cursor != null) ? cursor.getCount() : 0;
|
|
|
|
int progress = 1;
|
|
|
|
|
2018-01-25 03:17:44 +00:00
|
|
|
while (cursor != null && cursor.moveToNext()) {
|
|
|
|
ContentValues row = new ContentValues();
|
|
|
|
|
|
|
|
for (int i=0;i<cursor.getColumnCount();i++) {
|
|
|
|
String columnName = cursor.getColumnName(i);
|
|
|
|
|
2018-02-05 22:04:56 +00:00
|
|
|
if (destinationColumns.contains(columnName)) {
|
|
|
|
switch (cursor.getType(i)) {
|
|
|
|
case Cursor.FIELD_TYPE_STRING: row.put(columnName, cursor.getString(i)); break;
|
|
|
|
case Cursor.FIELD_TYPE_FLOAT: row.put(columnName, cursor.getFloat(i)); break;
|
|
|
|
case Cursor.FIELD_TYPE_INTEGER: row.put(columnName, cursor.getLong(i)); break;
|
|
|
|
case Cursor.FIELD_TYPE_BLOB: row.put(columnName, cursor.getBlob(i)); break;
|
|
|
|
}
|
2018-01-25 03:17:44 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (transformer != null) {
|
2018-02-02 00:01:24 +00:00
|
|
|
row = transformer.apply(row, new Pair<>(progress++, count));
|
2018-01-25 03:17:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
modernDb.insert(tableName, null, row);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private static Pair<Long, String> 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);
|
|
|
|
}
|
2018-02-02 00:01:24 +00:00
|
|
|
|
2018-02-05 22:04:56 +00:00
|
|
|
private static Set<String> getTableColumns(String tableName, net.sqlcipher.database.SQLiteDatabase database) {
|
|
|
|
Set<String> results = new HashSet<>();
|
|
|
|
|
|
|
|
try (Cursor cursor = database.rawQuery("PRAGMA table_info(" + tableName + ")", null)) {
|
|
|
|
while (cursor != null && cursor.moveToNext()) {
|
|
|
|
results.add(cursor.getString(1));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return results;
|
|
|
|
}
|
|
|
|
|
2018-02-02 00:01:24 +00:00
|
|
|
private static int getTotalProgress(int sectionOffset, int sectionProgress, int sectionTotal) {
|
|
|
|
double percentOfSectionComplete = ((double)sectionProgress) / ((double)sectionTotal);
|
|
|
|
return sectionOffset + (int)(((double)1000) * percentOfSectionComplete);
|
|
|
|
}
|
2018-01-25 03:17:44 +00:00
|
|
|
}
|