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")
@ -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 try {
// an invalid state so should delete it // Define the location for the new database
String newDbPath = context.getDatabasePath(DATABASE_NAME).getPath(); String newDbPath = context.getDatabasePath(DATABASE_NAME).getPath();
File newDbFile = new File(newDbPath); File newDbFile = new File(newDbPath);
if (newDbFile.exists()) { newDbFile.delete(); } // If the new database file already exists then check if it's valid first, if it's in an
// invalid state we should delete it and try to migrate again
try { if (newDbFile.exists()) {
newDbFile.createNewFile(); // If the old database hasn't been modified since the new database was created, then we can
} // assume the user hasn't downgraded for some reason and made changes to the old database and
catch (Exception e) { // can remove the old database file (it won't be used anymore)
// TODO: Communicate the error somehow??? if (oldDbFile.lastModified() <= newDbFile.lastModified()) {
// TODO: Delete 'CIPHER3_DATABASE_NAME' once enough time has past
// //noinspection ResultOfMethodCallIgnored
// oldDbFile.delete();
return; return;
} }
try { // If the old database does have newer changes then the new database could have stale/invalid
// Open the old database // data and we should re-migrate to avoid losing any data or issues
SQLiteDatabase oldDb = SQLiteDatabase.openOrCreateDatabase(oldDbPath, databaseSecret.asString(), null, null, new SQLiteDatabaseHook() { if (!newDbFile.delete()) {
@Override throw new Exception("Failed to remove invalid new database");
public void preKey(SQLiteConnection connection) { }
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);
} }
@Override if (!newDbFile.createNewFile()) {
public void postKey(SQLiteConnection connection) { throw new Exception("Failed to create new database");
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);
} }
});
// 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>