Added temporary support for downgrading and notify the user upon failure

This commit is contained in:
Morgan Pretty 2023-01-19 15:24:09 +11:00
parent a2fcb3195d
commit cae15a200d
2 changed files with 98 additions and 55 deletions

View File

@ -1,15 +1,17 @@
package org.thoughtcrime.securesms.database.helpers; package org.thoughtcrime.securesms.database.helpers;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.Context; import android.content.Context;
import android.database.Cursor; import android.database.Cursor;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.core.app.NotificationCompat;
import net.zetetic.database.DatabaseErrorHandler;
import net.zetetic.database.DatabaseUtils;
import net.zetetic.database.sqlcipher.SQLiteConnection; import net.zetetic.database.sqlcipher.SQLiteConnection;
import net.zetetic.database.sqlcipher.SQLiteDatabase; import net.zetetic.database.sqlcipher.SQLiteDatabase;
import net.zetetic.database.sqlcipher.SQLiteDatabaseHook; import net.zetetic.database.sqlcipher.SQLiteDatabaseHook;
import net.zetetic.database.sqlcipher.SQLiteException;
import net.zetetic.database.sqlcipher.SQLiteOpenHelper; import net.zetetic.database.sqlcipher.SQLiteOpenHelper;
import org.session.libsession.utilities.TextSecurePreferences; import org.session.libsession.utilities.TextSecurePreferences;
@ -37,9 +39,12 @@ import org.thoughtcrime.securesms.database.SessionContactDatabase;
import org.thoughtcrime.securesms.database.SessionJobDatabase; import org.thoughtcrime.securesms.database.SessionJobDatabase;
import org.thoughtcrime.securesms.database.SmsDatabase; import org.thoughtcrime.securesms.database.SmsDatabase;
import org.thoughtcrime.securesms.database.ThreadDatabase; import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import java.io.File; import java.io.File;
import network.loki.messenger.R;
public class SQLCipherOpenHelper extends SQLiteOpenHelper { public class SQLCipherOpenHelper extends SQLiteOpenHelper {
@SuppressWarnings("unused") @SuppressWarnings("unused")
@ -85,7 +90,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
private static final int DATABASE_VERSION = lokiV39; private static final int DATABASE_VERSION = lokiV39;
private static final int MIN_DATABASE_VERSION = lokiV7; private static final int MIN_DATABASE_VERSION = lokiV7;
private static final String CIPHER3_DATABASE_NAME = "signal.db"; private static final String CIPHER3_DATABASE_NAME = "signal.db";
public static final String DATABASE_NAME = "signal_v4.db"; public static final String DATABASE_NAME = "signal_v4.db";
private final Context context; private final Context context;
private final DatabaseSecret databaseSecret; private final DatabaseSecret databaseSecret;
@ -94,14 +99,13 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
super(context, DATABASE_NAME, databaseSecret.asString(), null, DATABASE_VERSION, MIN_DATABASE_VERSION, null, new SQLiteDatabaseHook() { super(context, DATABASE_NAME, databaseSecret.asString(), null, DATABASE_VERSION, MIN_DATABASE_VERSION, null, new SQLiteDatabaseHook() {
@Override @Override
public void preKey(SQLiteConnection connection) { public void preKey(SQLiteConnection connection) {
connection.execute("PRAGMA cipher_default_kdf_iter = 256000;", null, null); SQLCipherOpenHelper.applySQLCipherPragmas(connection, true);
connection.execute("PRAGMA cipher_default_page_size = 4096;", null, null);
} }
@Override @Override
public void postKey(SQLiteConnection connection) { public void postKey(SQLiteConnection connection) {
connection.execute("PRAGMA kdf_iter = '256000';", null, null); SQLCipherOpenHelper.applySQLCipherPragmas(connection, true);
connection.execute("PRAGMA cipher_page_size = 4096;", null, null);
// if not vacuumed in a while, perform that operation // if not vacuumed in a while, perform that operation
long currentTime = System.currentTimeMillis(); long currentTime = System.currentTimeMillis();
// 7 days // 7 days
@ -116,48 +120,69 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
this.databaseSecret = databaseSecret; this.databaseSecret = databaseSecret;
} }
public static void migrateSqlCipher3To4IfNeeded(@NonNull Context context, @NonNull DatabaseSecret databaseSecret) { private static void applySQLCipherPragmas(SQLiteConnection connection, boolean useSQLCipher4) {
if (useSQLCipher4) {
connection.execute("PRAGMA kdf_iter = '256000';", null, null);
}
else {
connection.execute("PRAGMA cipher_compatibility = 3;", null, null);
connection.execute("PRAGMA kdf_iter = '1';", null, null);
}
connection.execute("PRAGMA cipher_page_size = 4096;", null, null);
}
private static SQLiteDatabase open(String path, DatabaseSecret databaseSecret, boolean useSQLCipher4) throws SQLiteException {
return SQLiteDatabase.openDatabase(path, databaseSecret.asString(), null, SQLiteDatabase.OPEN_READWRITE, new SQLiteDatabaseHook() {
@Override
public void preKey(SQLiteConnection connection) { SQLCipherOpenHelper.applySQLCipherPragmas(connection, useSQLCipher4); }
@Override
public void postKey(SQLiteConnection connection) { SQLCipherOpenHelper.applySQLCipherPragmas(connection, useSQLCipher4); }
});
}
public static void migrateSqlCipher3To4IfNeeded(@NonNull Context context, @NonNull DatabaseSecret databaseSecret) throws Exception {
String oldDbPath = context.getDatabasePath(CIPHER3_DATABASE_NAME).getPath(); String oldDbPath = context.getDatabasePath(CIPHER3_DATABASE_NAME).getPath();
File oldDbFile = new File(oldDbPath); File oldDbFile = new File(oldDbPath);
// If the old SQLCipher3 database file doesn't exist then just return early // If the old SQLCipher3 database file doesn't exist then no need to do anything
if (!oldDbFile.exists()) { return; } if (!oldDbFile.exists()) { return; }
// If the new database file already exists then we probably had a failed migration and it's likely in
// an invalid state so should delete it
String newDbPath = context.getDatabasePath(DATABASE_NAME).getPath();
File newDbFile = new File(newDbPath);
if (newDbFile.exists()) { newDbFile.delete(); }
try { try {
newDbFile.createNewFile(); // Define the location for the new database
} String newDbPath = context.getDatabasePath(DATABASE_NAME).getPath();
catch (Exception e) { File newDbFile = new File(newDbPath);
// TODO: Communicate the error somehow???
return;
}
try { // If the new database file already exists then check if it's valid first, if it's in an
// Open the old database // invalid state we should delete it and try to migrate again
SQLiteDatabase oldDb = SQLiteDatabase.openOrCreateDatabase(oldDbPath, databaseSecret.asString(), null, null, new SQLiteDatabaseHook() { if (newDbFile.exists()) {
@Override // If the old database hasn't been modified since the new database was created, then we can
public void preKey(SQLiteConnection connection) { // assume the user hasn't downgraded for some reason and made changes to the old database and
connection.execute("PRAGMA cipher_compatibility = 3;", null, null); // can remove the old database file (it won't be used anymore)
connection.execute("PRAGMA kdf_iter = '1';", null, null); if (oldDbFile.lastModified() <= newDbFile.lastModified()) {
connection.execute("PRAGMA cipher_page_size = 4096;", null, null); // TODO: Delete 'CIPHER3_DATABASE_NAME' once enough time has past
// //noinspection ResultOfMethodCallIgnored
// oldDbFile.delete();
return;
} }
@Override // If the old database does have newer changes then the new database could have stale/invalid
public void postKey(SQLiteConnection connection) { // data and we should re-migrate to avoid losing any data or issues
connection.execute("PRAGMA cipher_compatibility = 3;", null, null); if (!newDbFile.delete()) {
connection.execute("PRAGMA kdf_iter = '1';", null, null); throw new Exception("Failed to remove invalid new database");
connection.execute("PRAGMA cipher_page_size = 4096;", null, null);
} }
}); }
if (!newDbFile.createNewFile()) {
throw new Exception("Failed to create new database");
}
// Open the old database and extract it's version
SQLiteDatabase oldDb = SQLCipherOpenHelper.open(oldDbPath, databaseSecret, false);
int oldDbVersion = oldDb.getVersion();
// Export the old database to the new one (will have the default 'kdf_iter' and 'page_size' settings) // Export the old database to the new one (will have the default 'kdf_iter' and 'page_size' settings)
int oldDbVersion = oldDb.getVersion();
oldDb.rawExecSQL( oldDb.rawExecSQL(
String.format("ATTACH DATABASE '%s' AS sqlcipher4 KEY '%s'", newDbPath, databaseSecret.asString()) String.format("ATTACH DATABASE '%s' AS sqlcipher4 KEY '%s'", newDbPath, databaseSecret.asString())
); );
@ -167,30 +192,46 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
oldDb.rawExecSQL("DETACH DATABASE sqlcipher4"); oldDb.rawExecSQL("DETACH DATABASE sqlcipher4");
oldDb.close(); oldDb.close();
// TODO: Performance testing // Open the newly migrated database (to ensure it works) and set it's version so we don't try
// to run any of our custom migrations
SQLiteDatabase newDb = SQLiteDatabase.openDatabase(newDbPath, databaseSecret.asString(), null, SQLiteDatabase.OPEN_READWRITE, new SQLiteDatabaseHook() { SQLiteDatabase newDb = SQLCipherOpenHelper.open(newDbPath, databaseSecret, true);
@Override
public void preKey(SQLiteConnection connection) {
connection.execute("PRAGMA cipher_default_kdf_iter = 256000;", null, null);
connection.execute("PRAGMA cipher_default_page_size = 4096;", null, null);
}
@Override
public void postKey(SQLiteConnection connection) {
connection.execute("PRAGMA cipher_default_kdf_iter = 256000;", null, null);
connection.execute("PRAGMA cipher_default_page_size = 4096;", null, null);
}
});
newDb.setVersion(oldDbVersion); newDb.setVersion(oldDbVersion);
newDb.close(); newDb.close();
// TODO: Delete 'CIPHER3_DATABASE_NAME' // TODO: Delete 'CIPHER3_DATABASE_NAME' once enough time has past
// TODO: What do we do if the deletion fails??? (The current logic will end up re-migrating...) // Remove the old database file since it will no longer be used
// //noinspection ResultOfMethodCallIgnored
// oldDbFile.delete(); // oldDbFile.delete();
} }
catch (Exception e) { catch (Exception e) {
// TODO: Communicate the error somehow??? Log.e(TAG, "Migration from SQLCipher3 to SQLCipher4 failed", e);
// Notify the user of the issue so they know they can downgrade until the issue is fixed
NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
String channelId = context.getString(R.string.NotificationChannel_failures);
if (NotificationChannels.supported()) {
NotificationChannel channel = new NotificationChannel(channelId, channelId, NotificationManager.IMPORTANCE_HIGH);
channel.enableVibration(true);
notificationManager.createNotificationChannel(channel);
}
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, channelId)
.setSmallIcon(R.drawable.ic_notification)
.setColor(context.getResources().getColor(R.color.textsecure_primary))
.setCategory(NotificationCompat.CATEGORY_ERROR)
.setContentTitle(context.getString(R.string.ErrorNotifier_migration))
.setContentText(context.getString(R.string.ErrorNotifier_migration_downgrade))
.setAutoCancel(true);
if (!NotificationChannels.supported()) {
builder.setPriority(NotificationCompat.PRIORITY_HIGH);
}
notificationManager.notify(5874, builder.build());
// Throw the error (app will crash but there is nothing else we can do unfortunately)
throw e;
} }
} }

View File

@ -864,4 +864,6 @@
<string name="fragment_enter_community_url_join_button_title">Join</string> <string name="fragment_enter_community_url_join_button_title">Join</string>
<string name="new_conversation_dialog_back_button_content_description">Navigate Back</string> <string name="new_conversation_dialog_back_button_content_description">Navigate Back</string>
<string name="new_conversation_dialog_close_button_content_description">Close Dialog</string> <string name="new_conversation_dialog_close_button_content_description">Close Dialog</string>
<string name="ErrorNotifier_migration">Database Upgrade Failed</string>
<string name="ErrorNotifier_migration_downgrade">Please contact support to report the error and then install an older version to continue using Session.</string>
</resources> </resources>