mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-16 23:13:17 +01:00
Remove legacy ClassicOpenHelper.
This commit is contained in:
committed by
Cody Henthorne
parent
ebccc6db30
commit
2b67b1c44f
@@ -26454,61 +26454,6 @@
|
||||
column="7"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="Recycle"
|
||||
message="This `Cursor` should be freed up after use with `#close()`"
|
||||
errorLine1=" Cursor mmsCursor = db.query("mms", new String[] {"_id"},"
|
||||
errorLine2=" ~~~~~">
|
||||
<location
|
||||
file="src/main/java/org/thoughtcrime/securesms/database/helpers/ClassicOpenHelper.java"
|
||||
line="298"
|
||||
column="38"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="Recycle"
|
||||
message="This `Cursor` should be freed up after use with `#close()`"
|
||||
errorLine1=" Cursor partCursor = db.query("part", new String[] {"_id", "ct", "_data", "encrypted"},"
|
||||
errorLine2=" ~~~~~">
|
||||
<location
|
||||
file="src/main/java/org/thoughtcrime/securesms/database/helpers/ClassicOpenHelper.java"
|
||||
line="310"
|
||||
column="32"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="Recycle"
|
||||
message="This `Cursor` should be freed up after use with `#close()`"
|
||||
errorLine1=" Cursor threadCursor = db.query("thread", new String[] {"_id"}, null, null, null, null, null);"
|
||||
errorLine2=" ~~~~~">
|
||||
<location
|
||||
file="src/main/java/org/thoughtcrime/securesms/database/helpers/ClassicOpenHelper.java"
|
||||
line="708"
|
||||
column="32"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="Recycle"
|
||||
message="This `Cursor` should be freed up after use with `#close()`"
|
||||
errorLine1=" Cursor cursor = db.rawQuery("SELECT DISTINCT date AS date_received, status, " +"
|
||||
errorLine2=" ~~~~~~~~">
|
||||
<location
|
||||
file="src/main/java/org/thoughtcrime/securesms/database/helpers/ClassicOpenHelper.java"
|
||||
line="713"
|
||||
column="28"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="Recycle"
|
||||
message="This `Cursor` should be freed up after use with `#close()`"
|
||||
errorLine1=" cursor = db.query("mms", new String[] {"_id", "network_failures"}, "network_failures IS NOT NULL", null, null, null, null);"
|
||||
errorLine2=" ~~~~~">
|
||||
<location
|
||||
file="src/main/java/org/thoughtcrime/securesms/database/helpers/ClassicOpenHelper.java"
|
||||
line="1037"
|
||||
column="19"/>
|
||||
</issue>
|
||||
|
||||
<issue
|
||||
id="ObsoleteSdkInt"
|
||||
message="Unnecessary; SDK_INT is always >= 21"
|
||||
|
||||
@@ -10,18 +10,8 @@ import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.withinTransaction
|
||||
import org.thoughtcrime.securesms.crypto.AttachmentSecret
|
||||
import org.thoughtcrime.securesms.crypto.DatabaseSecret
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecret
|
||||
import org.thoughtcrime.securesms.database.helpers.ClassicOpenHelper
|
||||
import org.thoughtcrime.securesms.database.helpers.PreKeyMigrationHelper
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherMigrationHelper
|
||||
import org.thoughtcrime.securesms.database.helpers.SessionStoreMigrationHelper
|
||||
import org.thoughtcrime.securesms.database.helpers.SignalDatabaseMigrations
|
||||
import org.thoughtcrime.securesms.database.model.AvatarPickerDatabase
|
||||
import org.thoughtcrime.securesms.jobs.PreKeysSyncJob
|
||||
import org.thoughtcrime.securesms.migrations.LegacyMigrationJob
|
||||
import org.thoughtcrime.securesms.migrations.LegacyMigrationJob.DatabaseUpgradeListener
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import java.io.File
|
||||
import org.thoughtcrime.securesms.database.SQLiteDatabase as SignalSQLiteDatabase
|
||||
|
||||
@@ -95,19 +85,6 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
|
||||
// Requires FTS5
|
||||
executeStatements(signalDb, SearchTable.CREATE_TABLE)
|
||||
executeStatements(signalDb, SearchTable.CREATE_TRIGGERS)
|
||||
|
||||
if (context.getDatabasePath(ClassicOpenHelper.NAME).exists()) {
|
||||
val legacyHelper = ClassicOpenHelper(context)
|
||||
val legacyDb = legacyHelper.writableDatabase
|
||||
SQLCipherMigrationHelper.migratePlaintext(context, legacyDb, db)
|
||||
val masterSecret = KeyCachingService.getMasterSecret(context)
|
||||
if (masterSecret != null) SQLCipherMigrationHelper.migrateCiphertext(context, masterSecret, legacyDb, db, null) else TextSecurePreferences.setNeedsSqlCipherMigration(context, true)
|
||||
if (!PreKeyMigrationHelper.migratePreKeys(context, db)) {
|
||||
PreKeysSyncJob.enqueue()
|
||||
}
|
||||
SessionStoreMigrationHelper.migrateSessions(context, db)
|
||||
PreKeyMigrationHelper.cleanUpPreKeys(context)
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
@@ -348,36 +325,6 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
|
||||
instance!!.signalWritableDatabase
|
||||
}
|
||||
|
||||
@Deprecated("Only used for a legacy migration.")
|
||||
@JvmStatic
|
||||
fun onApplicationLevelUpgrade(
|
||||
context: Context,
|
||||
masterSecret: MasterSecret,
|
||||
fromVersion: Int,
|
||||
listener: DatabaseUpgradeListener?
|
||||
) {
|
||||
instance!!.signalWritableDatabase
|
||||
var legacyOpenHelper: ClassicOpenHelper? = null
|
||||
if (fromVersion < LegacyMigrationJob.ASYMMETRIC_MASTER_SECRET_FIX_VERSION) {
|
||||
legacyOpenHelper = ClassicOpenHelper(context)
|
||||
legacyOpenHelper.onApplicationLevelUpgrade(context, masterSecret, fromVersion, listener)
|
||||
}
|
||||
|
||||
if (fromVersion < LegacyMigrationJob.SQLCIPHER && TextSecurePreferences.getNeedsSqlCipherMigration(context)) {
|
||||
if (legacyOpenHelper == null) {
|
||||
legacyOpenHelper = ClassicOpenHelper(context)
|
||||
}
|
||||
|
||||
SQLCipherMigrationHelper.migrateCiphertext(
|
||||
context,
|
||||
masterSecret,
|
||||
legacyOpenHelper.writableDatabase,
|
||||
instance!!.rawWritableDatabase,
|
||||
listener
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun <T> runInTransaction(block: (SignalSQLiteDatabase) -> T): T {
|
||||
return instance!!.signalWritableDatabase.withinTransaction {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,227 +0,0 @@
|
||||
package org.thoughtcrime.securesms.database.helpers;
|
||||
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||
|
||||
import org.signal.core.util.Conversions;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.libsignal.protocol.InvalidKeyException;
|
||||
import org.signal.libsignal.protocol.InvalidMessageException;
|
||||
import org.signal.libsignal.protocol.state.PreKeyRecord;
|
||||
import org.signal.libsignal.protocol.state.SignedPreKeyRecord;
|
||||
import org.thoughtcrime.securesms.database.OneTimePreKeyTable;
|
||||
import org.thoughtcrime.securesms.database.SignedPreKeyTable;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.signal.core.util.Base64;
|
||||
import org.thoughtcrime.securesms.util.JsonUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
|
||||
public final class PreKeyMigrationHelper {
|
||||
|
||||
private static final String PREKEY_DIRECTORY = "prekeys";
|
||||
private static final String SIGNED_PREKEY_DIRECTORY = "signed_prekeys";
|
||||
|
||||
private static final int PLAINTEXT_VERSION = 2;
|
||||
private static final int CURRENT_VERSION_MARKER = 2;
|
||||
|
||||
private static final String TAG = Log.tag(PreKeyMigrationHelper.class);
|
||||
|
||||
public static boolean migratePreKeys(Context context, SQLiteDatabase database) {
|
||||
File[] preKeyFiles = getPreKeyDirectory(context).listFiles();
|
||||
boolean clean = true;
|
||||
|
||||
if (preKeyFiles != null) {
|
||||
for (File preKeyFile : preKeyFiles) {
|
||||
if (!"index.dat".equals(preKeyFile.getName())) {
|
||||
try {
|
||||
PreKeyRecord preKey = new PreKeyRecord(loadSerializedRecord(preKeyFile));
|
||||
|
||||
ContentValues contentValues = new ContentValues();
|
||||
contentValues.put(OneTimePreKeyTable.KEY_ID, preKey.getId());
|
||||
contentValues.put(OneTimePreKeyTable.PUBLIC_KEY, Base64.encodeWithPadding(preKey.getKeyPair().getPublicKey().serialize()));
|
||||
contentValues.put(OneTimePreKeyTable.PRIVATE_KEY, Base64.encodeWithPadding(preKey.getKeyPair().getPrivateKey().serialize()));
|
||||
database.insert(OneTimePreKeyTable.TABLE_NAME, null, contentValues);
|
||||
Log.i(TAG, "Migrated one-time prekey: " + preKey.getId());
|
||||
} catch (IOException | InvalidMessageException | InvalidKeyException e) {
|
||||
Log.w(TAG, e);
|
||||
clean = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File[] signedPreKeyFiles = getSignedPreKeyDirectory(context).listFiles();
|
||||
|
||||
if (signedPreKeyFiles != null) {
|
||||
for (File signedPreKeyFile : signedPreKeyFiles) {
|
||||
if (!"index.dat".equals(signedPreKeyFile.getName())) {
|
||||
try {
|
||||
SignedPreKeyRecord signedPreKey = new SignedPreKeyRecord(loadSerializedRecord(signedPreKeyFile));
|
||||
|
||||
ContentValues contentValues = new ContentValues();
|
||||
contentValues.put(SignedPreKeyTable.KEY_ID, signedPreKey.getId());
|
||||
contentValues.put(SignedPreKeyTable.PUBLIC_KEY, Base64.encodeWithPadding(signedPreKey.getKeyPair().getPublicKey().serialize()));
|
||||
contentValues.put(SignedPreKeyTable.PRIVATE_KEY, Base64.encodeWithPadding(signedPreKey.getKeyPair().getPrivateKey().serialize()));
|
||||
contentValues.put(SignedPreKeyTable.SIGNATURE, Base64.encodeWithPadding(signedPreKey.getSignature()));
|
||||
contentValues.put(SignedPreKeyTable.TIMESTAMP, signedPreKey.getTimestamp());
|
||||
database.insert(SignedPreKeyTable.TABLE_NAME, null, contentValues);
|
||||
Log.i(TAG, "Migrated signed prekey: " + signedPreKey.getId());
|
||||
} catch (IOException | InvalidMessageException | InvalidKeyException e) {
|
||||
Log.w(TAG, e);
|
||||
clean = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File oneTimePreKeyIndex = new File(getPreKeyDirectory(context), PreKeyIndex.FILE_NAME);
|
||||
File signedPreKeyIndex = new File(getSignedPreKeyDirectory(context), SignedPreKeyIndex.FILE_NAME);
|
||||
|
||||
if (oneTimePreKeyIndex.exists()) {
|
||||
try {
|
||||
InputStreamReader reader = new InputStreamReader(new FileInputStream(oneTimePreKeyIndex));
|
||||
PreKeyIndex index = JsonUtils.fromJson(reader, PreKeyIndex.class);
|
||||
reader.close();
|
||||
|
||||
Log.i(TAG, "Setting next prekey id: " + index.nextPreKeyId);
|
||||
SignalStore.account().aciPreKeys().setNextEcOneTimePreKeyId(index.nextPreKeyId);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
|
||||
if (signedPreKeyIndex.exists()) {
|
||||
try {
|
||||
InputStreamReader reader = new InputStreamReader(new FileInputStream(signedPreKeyIndex));
|
||||
SignedPreKeyIndex index = JsonUtils.fromJson(reader, SignedPreKeyIndex.class);
|
||||
reader.close();
|
||||
|
||||
Log.i(TAG, "Setting next signed prekey id: " + index.nextSignedPreKeyId);
|
||||
Log.i(TAG, "Setting active signed prekey id: " + index.activeSignedPreKeyId);
|
||||
SignalStore.account().aciPreKeys().setNextSignedPreKeyId(index.nextSignedPreKeyId);
|
||||
SignalStore.account().aciPreKeys().setActiveSignedPreKeyId(index.activeSignedPreKeyId);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
|
||||
return clean;
|
||||
}
|
||||
|
||||
public static void cleanUpPreKeys(@NonNull Context context) {
|
||||
File preKeyDirectory = getPreKeyDirectory(context);
|
||||
File[] preKeyFiles = preKeyDirectory.listFiles();
|
||||
|
||||
if (preKeyFiles != null) {
|
||||
for (File preKeyFile : preKeyFiles) {
|
||||
Log.i(TAG, "Deleting: " + preKeyFile.getAbsolutePath());
|
||||
preKeyFile.delete();
|
||||
}
|
||||
|
||||
Log.i(TAG, "Deleting: " + preKeyDirectory.getAbsolutePath());
|
||||
preKeyDirectory.delete();
|
||||
}
|
||||
|
||||
File signedPreKeyDirectory = getSignedPreKeyDirectory(context);
|
||||
File[] signedPreKeyFiles = signedPreKeyDirectory.listFiles();
|
||||
|
||||
if (signedPreKeyFiles != null) {
|
||||
for (File signedPreKeyFile : signedPreKeyFiles) {
|
||||
Log.i(TAG, "Deleting: " + signedPreKeyFile.getAbsolutePath());
|
||||
signedPreKeyFile.delete();
|
||||
}
|
||||
|
||||
Log.i(TAG, "Deleting: " + signedPreKeyDirectory.getAbsolutePath());
|
||||
signedPreKeyDirectory.delete();
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] loadSerializedRecord(File recordFile)
|
||||
throws IOException, InvalidMessageException
|
||||
{
|
||||
FileInputStream fin = new FileInputStream(recordFile);
|
||||
int recordVersion = readInteger(fin);
|
||||
|
||||
if (recordVersion > CURRENT_VERSION_MARKER) {
|
||||
throw new IOException("Invalid version: " + recordVersion);
|
||||
}
|
||||
|
||||
byte[] serializedRecord = readBlob(fin);
|
||||
|
||||
if (recordVersion < PLAINTEXT_VERSION) {
|
||||
throw new IOException("Migration didn't happen! " + recordFile.getAbsolutePath() + ", " + recordVersion);
|
||||
}
|
||||
|
||||
fin.close();
|
||||
return serializedRecord;
|
||||
}
|
||||
|
||||
private static File getPreKeyDirectory(Context context) {
|
||||
return getRecordsDirectory(context, PREKEY_DIRECTORY);
|
||||
}
|
||||
|
||||
private static File getSignedPreKeyDirectory(Context context) {
|
||||
return getRecordsDirectory(context, SIGNED_PREKEY_DIRECTORY);
|
||||
}
|
||||
|
||||
private static File getRecordsDirectory(Context context, String directoryName) {
|
||||
File directory = new File(context.getFilesDir(), directoryName);
|
||||
|
||||
if (!directory.exists()) {
|
||||
if (!directory.mkdirs()) {
|
||||
Log.w(TAG, "PreKey directory creation failed!");
|
||||
}
|
||||
}
|
||||
|
||||
return directory;
|
||||
}
|
||||
|
||||
private static byte[] readBlob(FileInputStream in) throws IOException {
|
||||
int length = readInteger(in);
|
||||
byte[] blobBytes = new byte[length];
|
||||
|
||||
in.read(blobBytes, 0, blobBytes.length);
|
||||
return blobBytes;
|
||||
}
|
||||
|
||||
private static int readInteger(FileInputStream in) throws IOException {
|
||||
byte[] integer = new byte[4];
|
||||
in.read(integer, 0, integer.length);
|
||||
return Conversions.byteArrayToInt(integer);
|
||||
}
|
||||
|
||||
private static class PreKeyIndex {
|
||||
static final String FILE_NAME = "index.dat";
|
||||
|
||||
@JsonProperty
|
||||
private int nextPreKeyId;
|
||||
|
||||
public PreKeyIndex() {}
|
||||
}
|
||||
|
||||
private static class SignedPreKeyIndex {
|
||||
static final String FILE_NAME = "index.dat";
|
||||
|
||||
@JsonProperty
|
||||
private int nextSignedPreKeyId;
|
||||
|
||||
@JsonProperty
|
||||
private int activeSignedPreKeyId = -1;
|
||||
|
||||
public SignedPreKeyIndex() {}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
package org.thoughtcrime.securesms.database.helpers;
|
||||
|
||||
import android.database.Cursor;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.database.SQLiteDatabase;
|
||||
import org.thoughtcrime.securesms.util.DelimiterUtil;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class RecipientIdCleanupHelper {
|
||||
|
||||
private static final String TAG = Log.tag(RecipientIdCleanupHelper.class);
|
||||
|
||||
public static void execute(@NonNull SQLiteDatabase db) {
|
||||
Log.i(TAG, "Beginning migration.");
|
||||
|
||||
long startTime = System.currentTimeMillis();
|
||||
Pattern pattern = Pattern.compile("^[0-9\\-+]+$");
|
||||
Set<String> deletionCandidates = new HashSet<>();
|
||||
|
||||
try (Cursor cursor = db.query("recipient", new String[] { "_id", "phone" }, "group_id IS NULL AND email IS NULL", null, null, null, null)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
String id = cursor.getString(cursor.getColumnIndexOrThrow("_id"));
|
||||
String phone = cursor.getString(cursor.getColumnIndexOrThrow("phone"));
|
||||
|
||||
if (TextUtils.isEmpty(phone) || !pattern.matcher(phone).matches()) {
|
||||
Log.i(TAG, "Recipient ID " + id + " has non-numeric characters and can potentially be deleted.");
|
||||
|
||||
if (!isIdUsed(db, "identities", "address", id) &&
|
||||
!isIdUsed(db, "sessions", "address", id) &&
|
||||
!isIdUsed(db, "thread", "recipient_ids", id) &&
|
||||
!isIdUsed(db, "sms", "address", id) &&
|
||||
!isIdUsed(db, "mms", "address", id) &&
|
||||
!isIdUsed(db, "mms", "quote_author", id) &&
|
||||
!isIdUsed(db, "group_receipts", "address", id) &&
|
||||
!isIdUsed(db, "groups", "recipient_id", id))
|
||||
{
|
||||
Log.i(TAG, "Determined ID " + id + " is unused in non-group membership. Marking for potential deletion.");
|
||||
deletionCandidates.add(id);
|
||||
} else {
|
||||
Log.i(TAG, "Found that ID " + id + " is actually used in another table.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Set<String> deletions = findUnusedInGroupMembership(db, deletionCandidates);
|
||||
|
||||
for (String deletion : deletions) {
|
||||
Log.i(TAG, "Deleting ID " + deletion);
|
||||
db.delete("recipient", "_id = ?", new String[] { String.valueOf(deletion) });
|
||||
}
|
||||
|
||||
Log.i(TAG, "Migration took " + (System.currentTimeMillis() - startTime) + " ms.");
|
||||
}
|
||||
|
||||
private static boolean isIdUsed(@NonNull SQLiteDatabase db, @NonNull String tableName, @NonNull String columnName, String id) {
|
||||
try (Cursor cursor = db.query(tableName, new String[] { columnName }, columnName + " = ?", new String[] { id }, null, null, null, "1")) {
|
||||
boolean used = cursor != null && cursor.moveToFirst();
|
||||
if (used) {
|
||||
Log.i(TAG, "Recipient " + id + " was used in (" + tableName + ", " + columnName + ")");
|
||||
}
|
||||
return used;
|
||||
}
|
||||
}
|
||||
|
||||
private static Set<String> findUnusedInGroupMembership(@NonNull SQLiteDatabase db, Set<String> candidates) {
|
||||
Set<String> unused = new HashSet<>(candidates);
|
||||
|
||||
try (Cursor cursor = db.rawQuery("SELECT members FROM groups", null)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
String serializedMembers = cursor.getString(cursor.getColumnIndexOrThrow("members"));
|
||||
String[] members = DelimiterUtil.split(serializedMembers, ',');
|
||||
|
||||
for (String member : members) {
|
||||
if (unused.remove(member)) {
|
||||
Log.i(TAG, "Recipient " + member + " was found in a group membership list.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return unused;
|
||||
}
|
||||
}
|
||||
@@ -1,282 +0,0 @@
|
||||
package org.thoughtcrime.securesms.database.helpers;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.phonenumbers.NumberUtil;
|
||||
import org.thoughtcrime.securesms.util.DelimiterUtil;
|
||||
import org.signal.core.util.Util;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public class RecipientIdMigrationHelper {
|
||||
|
||||
private static final String TAG = Log.tag(RecipientIdMigrationHelper.class);
|
||||
|
||||
public static void execute(SQLiteDatabase db) {
|
||||
Log.i(TAG, "Starting the recipient ID migration.");
|
||||
|
||||
long insertStart = System.currentTimeMillis();
|
||||
|
||||
Log.i(TAG, "Starting inserts for missing recipients.");
|
||||
db.execSQL(buildInsertMissingRecipientStatement("identities", "address"));
|
||||
db.execSQL(buildInsertMissingRecipientStatement("sessions", "address"));
|
||||
db.execSQL(buildInsertMissingRecipientStatement("thread", "recipient_ids"));
|
||||
db.execSQL(buildInsertMissingRecipientStatement("sms", "address"));
|
||||
db.execSQL(buildInsertMissingRecipientStatement("mms", "address"));
|
||||
db.execSQL(buildInsertMissingRecipientStatement("mms", "quote_author"));
|
||||
db.execSQL(buildInsertMissingRecipientStatement("group_receipts", "address"));
|
||||
db.execSQL(buildInsertMissingRecipientStatement("groups", "group_id"));
|
||||
Log.i(TAG, "Finished inserts for missing recipients in " + (System.currentTimeMillis() - insertStart) + " ms.");
|
||||
|
||||
long updateMissingStart = System.currentTimeMillis();
|
||||
|
||||
Log.i(TAG, "Starting updates for invalid or missing addresses.");
|
||||
db.execSQL(buildMissingAddressUpdateStatement("sms", "address"));
|
||||
db.execSQL(buildMissingAddressUpdateStatement("mms", "address"));
|
||||
db.execSQL(buildMissingAddressUpdateStatement("mms", "quote_author"));
|
||||
Log.i(TAG, "Finished updates for invalid or missing addresses in " + (System.currentTimeMillis() - updateMissingStart) + " ms.");
|
||||
|
||||
db.execSQL("ALTER TABLE groups ADD COLUMN recipient_id INTEGER DEFAULT 0");
|
||||
|
||||
long updateStart = System.currentTimeMillis();
|
||||
|
||||
Log.i(TAG, "Starting recipient ID updates.");
|
||||
db.execSQL(buildUpdateAddressToRecipientIdStatement("identities", "address"));
|
||||
db.execSQL(buildUpdateAddressToRecipientIdStatement("sessions", "address"));
|
||||
db.execSQL(buildUpdateAddressToRecipientIdStatement("thread", "recipient_ids"));
|
||||
db.execSQL(buildUpdateAddressToRecipientIdStatement("sms", "address"));
|
||||
db.execSQL(buildUpdateAddressToRecipientIdStatement("mms", "address"));
|
||||
db.execSQL(buildUpdateAddressToRecipientIdStatement("mms", "quote_author"));
|
||||
db.execSQL(buildUpdateAddressToRecipientIdStatement("group_receipts", "address"));
|
||||
db.execSQL("UPDATE groups SET recipient_id = (SELECT _id FROM recipient_preferences WHERE recipient_preferences.recipient_ids = groups.group_id)");
|
||||
Log.i(TAG, "Finished recipient ID updates in " + (System.currentTimeMillis() - updateStart) + " ms.");
|
||||
|
||||
// NOTE: Because there's an open cursor on the same table, inserts and updates aren't visible
|
||||
// until afterwards, which is why this group stuff is split into multiple loops
|
||||
|
||||
long findGroupStart = System.currentTimeMillis();
|
||||
|
||||
Log.i(TAG, "Starting to find missing group recipients.");
|
||||
Set<String> missingGroupMembers = new HashSet<>();
|
||||
|
||||
try (Cursor cursor = db.rawQuery("SELECT members FROM groups", null)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
String serializedMembers = cursor.getString(cursor.getColumnIndexOrThrow("members"));
|
||||
String[] members = DelimiterUtil.split(serializedMembers, ',');
|
||||
|
||||
for (String rawMember : members) {
|
||||
String member = DelimiterUtil.unescape(rawMember, ',');
|
||||
|
||||
if (!TextUtils.isEmpty(member) && !recipientExists(db, member)) {
|
||||
missingGroupMembers.add(member);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Log.i(TAG, "Finished finding " + missingGroupMembers.size() + " missing group recipients in " + (System.currentTimeMillis() - findGroupStart) + " ms.");
|
||||
|
||||
long insertGroupStart = System.currentTimeMillis();
|
||||
|
||||
Log.i(TAG, "Starting the insert of missing group recipients.");
|
||||
for (String member : missingGroupMembers) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put("recipient_ids", member);
|
||||
db.insert("recipient_preferences", null, values);
|
||||
}
|
||||
Log.i(TAG, "Finished inserting missing group recipients in " + (System.currentTimeMillis() - insertGroupStart) + " ms.");
|
||||
|
||||
long updateGroupStart = System.currentTimeMillis();
|
||||
|
||||
Log.i(TAG, "Starting group recipient ID updates.");
|
||||
try (Cursor cursor = db.rawQuery("SELECT _id, members FROM groups", null)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
long groupId = cursor.getLong(cursor.getColumnIndexOrThrow("_id"));
|
||||
String serializedMembers = cursor.getString(cursor.getColumnIndexOrThrow("members"));
|
||||
String[] members = DelimiterUtil.split(serializedMembers, ',');
|
||||
long[] memberIds = new long[members.length];
|
||||
|
||||
for (int i = 0; i < members.length; i++) {
|
||||
String member = DelimiterUtil.unescape(members[i], ',');
|
||||
memberIds[i] = requireRecipientId(db, member);
|
||||
}
|
||||
|
||||
String serializedMemberIds = Util.join(memberIds, ",");
|
||||
|
||||
db.execSQL("UPDATE groups SET members = ? WHERE _id = ?", new String[]{ serializedMemberIds, String.valueOf(groupId) });
|
||||
}
|
||||
}
|
||||
db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS group_recipient_id_index ON groups (recipient_id)");
|
||||
Log.i(TAG, "Finished group recipient ID updates in " + (System.currentTimeMillis() - updateGroupStart) + " ms.");
|
||||
|
||||
|
||||
long tableCopyStart = System.currentTimeMillis();
|
||||
|
||||
Log.i(TAG, "Starting to copy the recipient table.");
|
||||
db.execSQL("CREATE TABLE recipient (_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
|
||||
"uuid TEXT UNIQUE DEFAULT NULL, " +
|
||||
"phone TEXT UNIQUE DEFAULT NULL, " +
|
||||
"email TEXT UNIQUE DEFAULT NULL, " +
|
||||
"group_id TEXT UNIQUE DEFAULT NULL, " +
|
||||
"blocked INTEGER DEFAULT 0, " +
|
||||
"message_ringtone TEXT DEFAULT NULL, " +
|
||||
"message_vibrate INTEGER DEFAULT 0, " +
|
||||
"call_ringtone TEXT DEFAULT NULL, " +
|
||||
"call_vibrate INTEGER DEFAULT 0, " +
|
||||
"notification_channel TEXT DEFAULT NULL, " +
|
||||
"mute_until INTEGER DEFAULT 0, " +
|
||||
"color TEXT DEFAULT NULL, " +
|
||||
"seen_invite_reminder INTEGER DEFAULT 0, " +
|
||||
"default_subscription_id INTEGER DEFAULT -1, " +
|
||||
"message_expiration_time INTEGER DEFAULT 0, " +
|
||||
"registered INTEGER DEFAULT 0, " +
|
||||
"system_display_name TEXT DEFAULT NULL, " +
|
||||
"system_photo_uri TEXT DEFAULT NULL, " +
|
||||
"system_phone_label TEXT DEFAULT NULL, " +
|
||||
"system_contact_uri TEXT DEFAULT NULL, " +
|
||||
"profile_key TEXT DEFAULT NULL, " +
|
||||
"signal_profile_name TEXT DEFAULT NULL, " +
|
||||
"signal_profile_avatar TEXT DEFAULT NULL, " +
|
||||
"profile_sharing INTEGER DEFAULT 0, " +
|
||||
"unidentified_access_mode INTEGER DEFAULT 0, " +
|
||||
"force_sms_selection INTEGER DEFAULT 0)");
|
||||
|
||||
try (Cursor cursor = db.query("recipient_preferences", null, null, null, null, null, null)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
String address = cursor.getString(cursor.getColumnIndexOrThrow("recipient_ids"));
|
||||
boolean isGroup = GroupId.isEncodedGroup(address);
|
||||
boolean isEmail = !isGroup && NumberUtil.isValidEmail(address);
|
||||
boolean isPhone = !isGroup && !isEmail;
|
||||
|
||||
ContentValues values = new ContentValues();
|
||||
|
||||
values.put("_id", cursor.getLong(cursor.getColumnIndexOrThrow("_id")));
|
||||
values.put("uuid", (String) null);
|
||||
values.put("phone", isPhone ? address : null);
|
||||
values.put("email", isEmail ? address : null);
|
||||
values.put("group_id", isGroup ? address : null);
|
||||
values.put("blocked", cursor.getInt(cursor.getColumnIndexOrThrow("block")));
|
||||
values.put("message_ringtone", cursor.getString(cursor.getColumnIndexOrThrow("notification")));
|
||||
values.put("message_vibrate", cursor.getString(cursor.getColumnIndexOrThrow("vibrate")));
|
||||
values.put("call_ringtone", cursor.getString(cursor.getColumnIndexOrThrow("call_ringtone")));
|
||||
values.put("call_vibrate", cursor.getString(cursor.getColumnIndexOrThrow("call_vibrate")));
|
||||
values.put("notification_channel", cursor.getString(cursor.getColumnIndexOrThrow("notification_channel")));
|
||||
values.put("mute_until", cursor.getLong(cursor.getColumnIndexOrThrow("mute_until")));
|
||||
values.put("color", cursor.getString(cursor.getColumnIndexOrThrow("color")));
|
||||
values.put("seen_invite_reminder", cursor.getInt(cursor.getColumnIndexOrThrow("seen_invite_reminder")));
|
||||
values.put("default_subscription_id", cursor.getInt(cursor.getColumnIndexOrThrow("default_subscription_id")));
|
||||
values.put("message_expiration_time", cursor.getInt(cursor.getColumnIndexOrThrow("expire_messages")));
|
||||
values.put("registered", cursor.getInt(cursor.getColumnIndexOrThrow("registered")));
|
||||
values.put("system_display_name", cursor.getString(cursor.getColumnIndexOrThrow("system_display_name")));
|
||||
values.put("system_photo_uri", cursor.getString(cursor.getColumnIndexOrThrow("system_contact_photo")));
|
||||
values.put("system_phone_label", cursor.getString(cursor.getColumnIndexOrThrow("system_phone_label")));
|
||||
values.put("system_contact_uri", cursor.getString(cursor.getColumnIndexOrThrow("system_contact_uri")));
|
||||
values.put("profile_key", cursor.getString(cursor.getColumnIndexOrThrow("profile_key")));
|
||||
values.put("signal_profile_name", cursor.getString(cursor.getColumnIndexOrThrow("signal_profile_name")));
|
||||
values.put("signal_profile_avatar", cursor.getString(cursor.getColumnIndexOrThrow("signal_profile_avatar")));
|
||||
values.put("profile_sharing", cursor.getInt(cursor.getColumnIndexOrThrow("profile_sharing_approval")));
|
||||
values.put("unidentified_access_mode", cursor.getInt(cursor.getColumnIndexOrThrow("unidentified_access_mode")));
|
||||
values.put("force_sms_selection", cursor.getInt(cursor.getColumnIndexOrThrow("force_sms_selection")));
|
||||
|
||||
db.insert("recipient", null, values);
|
||||
}
|
||||
}
|
||||
|
||||
db.execSQL("DROP TABLE recipient_preferences");
|
||||
Log.i(TAG, "Finished copying the recipient table in " + (System.currentTimeMillis() - tableCopyStart) + " ms.");
|
||||
|
||||
long sanityCheckStart = System.currentTimeMillis();
|
||||
|
||||
Log.i(TAG, "Starting DB integrity sanity checks.");
|
||||
assertEmptyQuery(db, "identities", buildSanityCheckQuery("identities", "address"));
|
||||
assertEmptyQuery(db, "sessions", buildSanityCheckQuery("sessions", "address"));
|
||||
assertEmptyQuery(db, "groups", buildSanityCheckQuery("groups", "recipient_id"));
|
||||
assertEmptyQuery(db, "thread", buildSanityCheckQuery("thread", "recipient_ids"));
|
||||
assertEmptyQuery(db, "sms", buildSanityCheckQuery("sms", "address"));
|
||||
assertEmptyQuery(db, "mms -- address", buildSanityCheckQuery("mms", "address"));
|
||||
assertEmptyQuery(db, "mms -- quote_author", buildSanityCheckQuery("mms", "quote_author"));
|
||||
assertEmptyQuery(db, "group_receipts", buildSanityCheckQuery("group_receipts", "address"));
|
||||
Log.i(TAG, "Finished DB integrity sanity checks in " + (System.currentTimeMillis() - sanityCheckStart) + " ms.");
|
||||
|
||||
Log.i(TAG, "Finished recipient ID migration in " + (System.currentTimeMillis() - insertStart) + " ms.");
|
||||
}
|
||||
|
||||
private static String buildUpdateAddressToRecipientIdStatement(@NonNull String table, @NonNull String addressColumn) {
|
||||
return "UPDATE " + table + " SET " + addressColumn + "=(SELECT _id " +
|
||||
"FROM recipient_preferences " +
|
||||
"WHERE recipient_preferences.recipient_ids = " + table + "." + addressColumn + ")";
|
||||
}
|
||||
|
||||
private static String buildInsertMissingRecipientStatement(@NonNull String table, @NonNull String addressColumn) {
|
||||
return "INSERT INTO recipient_preferences(recipient_ids) SELECT DISTINCT " + addressColumn + " " +
|
||||
"FROM " + table + " " +
|
||||
"WHERE " + addressColumn + " != '' AND " +
|
||||
addressColumn + " != 'insert-address-column' AND " +
|
||||
addressColumn + " NOT NULL AND " +
|
||||
addressColumn + " NOT IN (SELECT recipient_ids FROM recipient_preferences)";
|
||||
}
|
||||
|
||||
private static String buildMissingAddressUpdateStatement(@NonNull String table, @NonNull String addressColumn) {
|
||||
return "UPDATE " + table + " SET " + addressColumn + " = -1 " +
|
||||
"WHERE " + addressColumn + " = '' OR " +
|
||||
addressColumn + " IS NULL OR " +
|
||||
addressColumn + " = 'insert-address-token'";
|
||||
}
|
||||
|
||||
private static boolean recipientExists(@NonNull SQLiteDatabase db, @NonNull String address) {
|
||||
return getRecipientId(db, address) != null;
|
||||
}
|
||||
|
||||
private static @Nullable Long getRecipientId(@NonNull SQLiteDatabase db, @NonNull String address) {
|
||||
try (Cursor cursor = db.rawQuery("SELECT _id FROM recipient_preferences WHERE recipient_ids = ?", new String[]{ address })) {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
return cursor.getLong(cursor.getColumnIndexOrThrow("_id"));
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static long requireRecipientId(@NonNull SQLiteDatabase db, @NonNull String address) {
|
||||
Long id = getRecipientId(db, address);
|
||||
|
||||
if (id != null) {
|
||||
return id;
|
||||
} else {
|
||||
throw new MissingRecipientError(address);
|
||||
}
|
||||
}
|
||||
|
||||
private static String buildSanityCheckQuery(@NonNull String table, @NonNull String idColumn) {
|
||||
return "SELECT " + idColumn + " FROM " + table + " WHERE " + idColumn + " != -1 AND " + idColumn + " NOT IN (SELECT _id FROM recipient)";
|
||||
}
|
||||
|
||||
private static void assertEmptyQuery(@NonNull SQLiteDatabase db, @NonNull String tag, @NonNull String query) {
|
||||
try (Cursor cursor = db.rawQuery(query, null)) {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
throw new FailedSanityCheckError(tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final class MissingRecipientError extends AssertionError {
|
||||
MissingRecipientError(@NonNull String address) {
|
||||
super("Could not find recipient with address " + address);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class FailedSanityCheckError extends AssertionError {
|
||||
FailedSanityCheckError(@NonNull String tableName) {
|
||||
super("Sanity check failed for tag '" + tableName + "'");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,259 +0,0 @@
|
||||
package org.thoughtcrime.securesms.database.helpers;
|
||||
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.text.TextUtils;
|
||||
import kotlin.Pair;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.annimon.stream.function.BiFunction;
|
||||
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.libsignal.protocol.InvalidMessageException;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
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.jobs.UnableToStartException;
|
||||
import org.thoughtcrime.securesms.migrations.LegacyMigrationJob;
|
||||
import org.thoughtcrime.securesms.service.GenericForegroundService;
|
||||
import org.thoughtcrime.securesms.service.NotificationController;
|
||||
import org.signal.core.util.Base64;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public class SQLCipherMigrationHelper {
|
||||
|
||||
private static final String TAG = Log.tag(SQLCipherMigrationHelper.class);
|
||||
|
||||
private static final long ENCRYPTION_SYMMETRIC_BIT = 0x80000000;
|
||||
private static final long ENCRYPTION_ASYMMETRIC_BIT = 0x40000000;
|
||||
|
||||
public static void migratePlaintext(@NonNull Context context,
|
||||
@NonNull android.database.sqlite.SQLiteDatabase legacyDb,
|
||||
@NonNull SQLiteDatabase modernDb)
|
||||
{
|
||||
modernDb.beginTransaction();
|
||||
try (NotificationController controller = GenericForegroundService.startForegroundTask(context, context.getString(R.string.SQLCipherMigrationHelper_migrating_signal_database))) {
|
||||
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();
|
||||
} catch (UnableToStartException e) {
|
||||
throw new IllegalStateException(e);
|
||||
} finally {
|
||||
modernDb.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
public static void migrateCiphertext(@NonNull Context context,
|
||||
@NonNull MasterSecret masterSecret,
|
||||
@NonNull android.database.sqlite.SQLiteDatabase legacyDb,
|
||||
@NonNull SQLiteDatabase modernDb,
|
||||
@Nullable LegacyMigrationJob.DatabaseUpgradeListener listener)
|
||||
{
|
||||
MasterCipher legacyCipher = new MasterCipher(masterSecret);
|
||||
AsymmetricMasterCipher legacyAsymmetricCipher = new AsymmetricMasterCipher(MasterSecretUtil.getAsymmetricMasterSecret(context, masterSecret));
|
||||
|
||||
modernDb.beginTransaction();
|
||||
|
||||
try (NotificationController controller = GenericForegroundService.startForegroundTask(context, context.getString(R.string.SQLCipherMigrationHelper_migrating_signal_database))) {
|
||||
int total = 5000;
|
||||
|
||||
copyTable("sms", legacyDb, modernDb, (row, progress) -> {
|
||||
Pair<Long, String> plaintext = getPlaintextBody(legacyCipher, legacyAsymmetricCipher,
|
||||
row.getAsLong("type"),
|
||||
row.getAsString("body"));
|
||||
|
||||
row.put("body", plaintext.getSecond());
|
||||
row.put("type", plaintext.getFirst());
|
||||
|
||||
if (listener != null && (progress.getFirst() % 1000 == 0)) {
|
||||
listener.setProgress(getTotalProgress(0, progress.getFirst(), progress.getSecond()), total);
|
||||
}
|
||||
|
||||
return row;
|
||||
});
|
||||
|
||||
copyTable("mms", legacyDb, modernDb, (row, progress) -> {
|
||||
Pair<Long, String> plaintext = getPlaintextBody(legacyCipher, legacyAsymmetricCipher,
|
||||
row.getAsLong("msg_box"),
|
||||
row.getAsString("body"));
|
||||
|
||||
row.put("body", plaintext.getSecond());
|
||||
row.put("msg_box", plaintext.getFirst());
|
||||
|
||||
if (listener != null && (progress.getFirst() % 1000 == 0)) {
|
||||
listener.setProgress(getTotalProgress(1000, progress.getFirst(), progress.getSecond()), total);
|
||||
}
|
||||
|
||||
return row;
|
||||
});
|
||||
|
||||
copyTable("part", legacyDb, modernDb, (row, progress) -> {
|
||||
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.encodeWithPadding(plaintext));
|
||||
}
|
||||
} catch (IOException | InvalidMessageException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
|
||||
if (listener != null && (progress.getFirst() % 1000 == 0)) {
|
||||
listener.setProgress(getTotalProgress(2000, progress.getFirst(), progress.getSecond()), total);
|
||||
}
|
||||
|
||||
return row;
|
||||
});
|
||||
|
||||
copyTable("thread", legacyDb, modernDb, (row, progress) -> {
|
||||
Long snippetType = row.getAsLong("snippet_type");
|
||||
if (snippetType == null) snippetType = 0L;
|
||||
|
||||
Pair<Long, String> plaintext = getPlaintextBody(legacyCipher, legacyAsymmetricCipher,
|
||||
snippetType, row.getAsString("snippet"));
|
||||
|
||||
row.put("snippet", plaintext.getSecond());
|
||||
row.put("snippet_type", plaintext.getFirst());
|
||||
|
||||
if (listener != null && (progress.getFirst() % 1000 == 0)) {
|
||||
listener.setProgress(getTotalProgress(3000, progress.getFirst(), progress.getSecond()), total);
|
||||
}
|
||||
|
||||
return row;
|
||||
});
|
||||
|
||||
|
||||
copyTable("drafts", legacyDb, modernDb, (row, progress) -> {
|
||||
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);
|
||||
}
|
||||
|
||||
if (listener != null && (progress.getFirst() % 1000 == 0)) {
|
||||
listener.setProgress(getTotalProgress(4000, progress.getFirst(), progress.getSecond()), total);
|
||||
}
|
||||
|
||||
return row;
|
||||
});
|
||||
|
||||
AttachmentSecretProvider.getInstance(context).setClassicKey(context, masterSecret.getEncryptionKey().getEncoded(), masterSecret.getMacKey().getEncoded());
|
||||
TextSecurePreferences.setNeedsSqlCipherMigration(context, false);
|
||||
modernDb.setTransactionSuccessful();
|
||||
} catch (UnableToStartException e) {
|
||||
throw new IllegalStateException(e);
|
||||
} finally {
|
||||
modernDb.endTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
private static void copyTable(@NonNull String tableName,
|
||||
@NonNull android.database.sqlite.SQLiteDatabase legacyDb,
|
||||
@NonNull SQLiteDatabase modernDb,
|
||||
@Nullable BiFunction<ContentValues, Pair<Integer, Integer>, ContentValues> transformer)
|
||||
{
|
||||
Set<String> destinationColumns = getTableColumns(tableName, modernDb);
|
||||
|
||||
try (Cursor cursor = legacyDb.query(tableName, null, null, null, null, null, null)) {
|
||||
int count = (cursor != null) ? cursor.getCount() : 0;
|
||||
int progress = 1;
|
||||
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
ContentValues row = new ContentValues();
|
||||
|
||||
for (int i=0;i<cursor.getColumnCount();i++) {
|
||||
String columnName = cursor.getColumnName(i);
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (transformer != null) {
|
||||
row = transformer.apply(row, new Pair<>(progress++, count));
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
private static Set<String> getTableColumns(String tableName, 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;
|
||||
}
|
||||
|
||||
private static int getTotalProgress(int sectionOffset, int sectionProgress, int sectionTotal) {
|
||||
double percentOfSectionComplete = ((double)sectionProgress) / ((double)sectionTotal);
|
||||
return sectionOffset + (int)(((double)1000) * percentOfSectionComplete);
|
||||
}
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
package org.thoughtcrime.securesms.database.helpers;
|
||||
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase;
|
||||
|
||||
import org.signal.core.util.Conversions;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.libsignal.protocol.InvalidMessageException;
|
||||
import org.signal.libsignal.protocol.state.SessionRecord;
|
||||
import org.thoughtcrime.securesms.database.SessionTable;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
public final class SessionStoreMigrationHelper {
|
||||
|
||||
private static final String TAG = Log.tag(SessionStoreMigrationHelper.class);
|
||||
|
||||
private static final String SESSIONS_DIRECTORY_V2 = "sessions-v2";
|
||||
private static final Object FILE_LOCK = new Object();
|
||||
|
||||
private static final int SINGLE_STATE_VERSION = 1;
|
||||
private static final int ARCHIVE_STATES_VERSION = 2;
|
||||
private static final int PLAINTEXT_VERSION = 3;
|
||||
private static final int CURRENT_VERSION = 3;
|
||||
|
||||
public static void migrateSessions(Context context, SQLiteDatabase database) {
|
||||
File directory = new File(context.getFilesDir(), SESSIONS_DIRECTORY_V2);
|
||||
|
||||
if (directory.exists()) {
|
||||
File[] sessionFiles = directory.listFiles();
|
||||
|
||||
if (sessionFiles != null) {
|
||||
for (File sessionFile : sessionFiles) {
|
||||
try {
|
||||
String[] parts = sessionFile.getName().split("[.]");
|
||||
String address = parts[0];
|
||||
|
||||
int deviceId;
|
||||
|
||||
if (parts.length > 1) deviceId = Integer.parseInt(parts[1]);
|
||||
else deviceId = SignalServiceAddress.DEFAULT_DEVICE_ID;
|
||||
|
||||
FileInputStream in = new FileInputStream(sessionFile);
|
||||
int versionMarker = readInteger(in);
|
||||
|
||||
if (versionMarker > CURRENT_VERSION) {
|
||||
throw new AssertionError("Unknown version: " + versionMarker + ", " + sessionFile.getAbsolutePath());
|
||||
}
|
||||
|
||||
byte[] serialized = readBlob(in);
|
||||
in.close();
|
||||
|
||||
if (versionMarker < PLAINTEXT_VERSION) {
|
||||
throw new AssertionError("Not plaintext: " + versionMarker + ", " + sessionFile.getAbsolutePath());
|
||||
}
|
||||
|
||||
SessionRecord sessionRecord;
|
||||
|
||||
if (versionMarker == SINGLE_STATE_VERSION) {
|
||||
Log.i(TAG, "Migrating single state version: " + sessionFile.getAbsolutePath());
|
||||
sessionRecord = new SessionRecord(serialized);
|
||||
} else if (versionMarker >= ARCHIVE_STATES_VERSION) {
|
||||
Log.i(TAG, "Migrating session: " + sessionFile.getAbsolutePath());
|
||||
sessionRecord = new SessionRecord(serialized);
|
||||
} else {
|
||||
throw new AssertionError("Unknown version: " + versionMarker + ", " + sessionFile.getAbsolutePath());
|
||||
}
|
||||
|
||||
|
||||
ContentValues contentValues = new ContentValues();
|
||||
contentValues.put(SessionTable.ADDRESS, address);
|
||||
contentValues.put(SessionTable.DEVICE, deviceId);
|
||||
contentValues.put(SessionTable.RECORD, sessionRecord.serialize());
|
||||
|
||||
database.insert(SessionTable.TABLE_NAME, null, contentValues);
|
||||
} catch (NumberFormatException | IOException | InvalidMessageException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] readBlob(FileInputStream in) throws IOException {
|
||||
int length = readInteger(in);
|
||||
byte[] blobBytes = new byte[length];
|
||||
|
||||
in.read(blobBytes, 0, blobBytes.length);
|
||||
return blobBytes;
|
||||
}
|
||||
|
||||
private static int readInteger(FileInputStream in) throws IOException {
|
||||
byte[] integer = new byte[4];
|
||||
in.read(integer, 0, integer.length);
|
||||
return Conversions.byteArrayToInt(integer);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -392,8 +392,5 @@ object SignalDatabaseMigrations {
|
||||
|
||||
@JvmStatic
|
||||
fun migratePostTransaction(context: Context, oldVersion: Int) {
|
||||
if (oldVersion < V149_LegacyMigrations.MIGRATE_PREKEYS_VERSION) {
|
||||
PreKeyMigrationHelper.cleanUpPreKeys(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,10 +31,6 @@ import org.thoughtcrime.securesms.conversation.colors.ChatColorsMapper
|
||||
import org.thoughtcrime.securesms.database.KeyValueDatabase
|
||||
import org.thoughtcrime.securesms.database.RecipientTable
|
||||
import org.thoughtcrime.securesms.database.SQLiteDatabase
|
||||
import org.thoughtcrime.securesms.database.helpers.PreKeyMigrationHelper
|
||||
import org.thoughtcrime.securesms.database.helpers.RecipientIdCleanupHelper
|
||||
import org.thoughtcrime.securesms.database.helpers.RecipientIdMigrationHelper
|
||||
import org.thoughtcrime.securesms.database.helpers.SessionStoreMigrationHelper
|
||||
import org.thoughtcrime.securesms.database.helpers.SignalDatabaseMigrations
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.ReactionList
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
@@ -221,14 +217,11 @@ object V149_LegacyMigrations : SignalDatabaseMigration {
|
||||
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 (!PreKeyMigrationHelper.migratePreKeys(context, db.sqlCipherDatabase)) {
|
||||
PreKeysSyncJob.enqueue()
|
||||
}
|
||||
PreKeysSyncJob.enqueue()
|
||||
}
|
||||
|
||||
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.sqlCipherDatabase)
|
||||
}
|
||||
|
||||
if (oldVersion < NO_MORE_IMAGE_THUMBNAILS_VERSION) {
|
||||
@@ -615,7 +608,7 @@ object V149_LegacyMigrations : SignalDatabaseMigration {
|
||||
}
|
||||
|
||||
if (oldVersion < RECIPIENT_IDS) {
|
||||
RecipientIdMigrationHelper.execute(db.sqlCipherDatabase)
|
||||
// RecipientIdMigrationHelper was removed -- migration from this old version is no longer supported
|
||||
}
|
||||
|
||||
if (oldVersion < RECIPIENT_SEARCH) {
|
||||
@@ -638,7 +631,7 @@ object V149_LegacyMigrations : SignalDatabaseMigration {
|
||||
}
|
||||
|
||||
if (oldVersion < RECIPIENT_CLEANUP) {
|
||||
RecipientIdCleanupHelper.execute(db)
|
||||
// RecipientIdCleanupHelper was removed -- migration from this old version is no longer supported
|
||||
}
|
||||
|
||||
if (oldVersion < MMS_RECIPIENT_CLEANUP) {
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
package org.thoughtcrime.securesms.jobmanager.impl;
|
||||
|
||||
import android.app.Application;
|
||||
import android.app.job.JobInfo;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.jobmanager.Constraint;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
public class SqlCipherMigrationConstraint implements Constraint {
|
||||
|
||||
public static final String KEY = "SqlCipherMigrationConstraint";
|
||||
|
||||
private final Application application;
|
||||
|
||||
private SqlCipherMigrationConstraint(@NonNull Application application) {
|
||||
this.application = application;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isMet() {
|
||||
return !TextSecurePreferences.getNeedsSqlCipherMigration(application);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String getFactoryKey() {
|
||||
return KEY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void applyToJobInfo(@NonNull JobInfo.Builder jobInfoBuilder) {
|
||||
}
|
||||
|
||||
public static final class Factory implements Constraint.Factory<SqlCipherMigrationConstraint> {
|
||||
|
||||
private final Application application;
|
||||
|
||||
public Factory(@NonNull Application application) {
|
||||
this.application = application;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SqlCipherMigrationConstraint create() {
|
||||
return new SqlCipherMigrationConstraint(application);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package org.thoughtcrime.securesms.jobmanager.impl;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.greenrobot.eventbus.Subscribe;
|
||||
import org.greenrobot.eventbus.ThreadMode;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.jobmanager.ConstraintObserver;
|
||||
|
||||
public class SqlCipherMigrationConstraintObserver implements ConstraintObserver {
|
||||
|
||||
private static final String REASON = Log.tag(SqlCipherMigrationConstraintObserver.class);
|
||||
|
||||
private Notifier notifier;
|
||||
|
||||
public SqlCipherMigrationConstraintObserver() {
|
||||
EventBus.getDefault().register(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void register(@NonNull Notifier notifier) {
|
||||
this.notifier = notifier;
|
||||
}
|
||||
|
||||
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||
public void onEvent(SqlCipherNeedsMigrationEvent event) {
|
||||
if (notifier != null) notifier.onConstraintMet(REASON);
|
||||
}
|
||||
|
||||
public static class SqlCipherNeedsMigrationEvent {
|
||||
}
|
||||
}
|
||||
@@ -34,8 +34,6 @@ import org.thoughtcrime.securesms.jobmanager.impl.NotInCallConstraintObserver;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.RegisteredConstraint;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.RestoreAttachmentConstraint;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.RestoreAttachmentConstraintObserver;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.SqlCipherMigrationConstraint;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.SqlCipherMigrationConstraintObserver;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.StickersNotDownloadingConstraint;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.WifiConstraint;
|
||||
import org.thoughtcrime.securesms.jobmanager.migrations.DeprecatedJobMigration;
|
||||
@@ -454,7 +452,6 @@ public final class JobManagerFactories {
|
||||
put(RegisteredConstraint.KEY, new RegisteredConstraint.Factory());
|
||||
put(RestoreAttachmentConstraint.KEY, new RestoreAttachmentConstraint.Factory(application));
|
||||
put(SealedSenderConstraint.KEY, new SealedSenderConstraint.Factory());
|
||||
put(SqlCipherMigrationConstraint.KEY, new SqlCipherMigrationConstraint.Factory(application));
|
||||
put(StickersNotDownloadingConstraint.KEY, new StickersNotDownloadingConstraint.Factory());
|
||||
put(WifiConstraint.KEY, new WifiConstraint.Factory(application));
|
||||
}};
|
||||
@@ -464,7 +461,6 @@ public final class JobManagerFactories {
|
||||
return Arrays.asList(CellServiceConstraintObserver.getInstance(application),
|
||||
new ChargingAndBatteryIsNotLowConstraintObserver(application),
|
||||
new NetworkConstraintObserver(application),
|
||||
new SqlCipherMigrationConstraintObserver(),
|
||||
new DecryptionsDrainedConstraintObserver(),
|
||||
new NotInCallConstraintObserver(),
|
||||
ChangeNumberConstraintObserver.INSTANCE,
|
||||
|
||||
@@ -10,7 +10,6 @@ import com.bumptech.glide.Glide;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
import org.thoughtcrime.securesms.database.AttachmentTable;
|
||||
import org.thoughtcrime.securesms.database.MessageTable;
|
||||
import org.thoughtcrime.securesms.database.MessageTable.MmsReader;
|
||||
@@ -23,7 +22,6 @@ import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob;
|
||||
import org.thoughtcrime.securesms.jobs.PreKeysSyncJob;
|
||||
import org.thoughtcrime.securesms.jobs.RefreshAttributesJob;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
import org.thoughtcrime.securesms.transport.RetryLaterException;
|
||||
import org.thoughtcrime.securesms.util.FileUtils;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
@@ -98,15 +96,6 @@ public class LegacyMigrationJob extends MigrationJob {
|
||||
void performMigration() throws RetryLaterException {
|
||||
Log.i(TAG, "Running background upgrade..");
|
||||
int lastSeenVersion = VersionTracker.getLastSeenVersion(context);
|
||||
MasterSecret masterSecret = KeyCachingService.getMasterSecret(context);
|
||||
|
||||
if (lastSeenVersion < SQLCIPHER && masterSecret != null) {
|
||||
SignalDatabase.onApplicationLevelUpgrade(context, masterSecret, lastSeenVersion, (progress, total) -> {
|
||||
Log.i(TAG, "onApplicationLevelUpgrade: " + progress + "/" + total);
|
||||
});
|
||||
} else if (lastSeenVersion < SQLCIPHER) {
|
||||
throw new RetryLaterException();
|
||||
}
|
||||
|
||||
if (lastSeenVersion < NO_V1_VERSION) {
|
||||
File v1sessions = new File(context.getFilesDir(), "sessions");
|
||||
|
||||
@@ -26,7 +26,6 @@ import org.thoughtcrime.securesms.backup.proto.SharedPreference;
|
||||
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.SqlCipherMigrationConstraintObserver;
|
||||
import org.thoughtcrime.securesms.keyvalue.SettingsValues;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.lock.RegistrationLockReminders;
|
||||
@@ -97,7 +96,6 @@ public class TextSecurePreferences {
|
||||
private static final String DATABASE_UNENCRYPTED_SECRET = "pref_database_unencrypted_secret";
|
||||
private static final String ATTACHMENT_ENCRYPTED_SECRET = "pref_attachment_encrypted_secret";
|
||||
private static final String ATTACHMENT_UNENCRYPTED_SECRET = "pref_attachment_unencrypted_secret";
|
||||
private static final String NEEDS_SQLCIPHER_MIGRATION = "pref_needs_sql_cipher_migration";
|
||||
|
||||
public static final String CALL_NOTIFICATIONS_PREF = "pref_call_notifications";
|
||||
public static final String CALL_RINGTONE_PREF = "pref_call_ringtone";
|
||||
@@ -336,15 +334,6 @@ public class TextSecurePreferences {
|
||||
return getLongPreference(context, BACKUP_TIME, -1);
|
||||
}
|
||||
|
||||
public static void setNeedsSqlCipherMigration(@NonNull Context context, boolean value) {
|
||||
setBooleanPreference(context, NEEDS_SQLCIPHER_MIGRATION, value);
|
||||
EventBus.getDefault().post(new SqlCipherMigrationConstraintObserver.SqlCipherNeedsMigrationEvent());
|
||||
}
|
||||
|
||||
public static boolean getNeedsSqlCipherMigration(@NonNull Context context) {
|
||||
return getBooleanPreference(context, NEEDS_SQLCIPHER_MIGRATION, false);
|
||||
}
|
||||
|
||||
public static void setAttachmentEncryptedSecret(@NonNull Context context, @NonNull String secret) {
|
||||
setStringPreference(context, ATTACHMENT_ENCRYPTED_SECRET, secret);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user