Update SQLCipher error handling.

fork-5.53.8
Greyson Parrelli 2021-11-29 16:56:29 -05:00
rodzic e363bac1a3
commit 638bae6de3
2 zmienionych plików z 166 dodań i 115 usunięć

Wyświetl plik

@ -1,115 +0,0 @@
package org.thoughtcrime.securesms.database;
import android.database.Cursor;
import androidx.annotation.NonNull;
import net.zetetic.database.DatabaseErrorHandler;
import net.zetetic.database.sqlcipher.SQLiteDatabase;
import org.signal.core.util.ExceptionUtil;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.util.CursorUtil;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* The default error handler wipes the file. This one instead prints some diagnostics and then crashes so the original corrupt file isn't lost.
*/
public final class SqlCipherErrorHandler implements DatabaseErrorHandler {
private static final String TAG = Log.tag(SqlCipherErrorHandler.class);
private final String databaseName;
public SqlCipherErrorHandler(@NonNull String databaseName) {
this.databaseName = databaseName;
}
@Override
@SuppressWarnings("ConstantConditions")
public void onCorruption(SQLiteDatabase db) {
StringBuilder output = new StringBuilder();
output.append("Database '").append(databaseName).append("' corrupted! Going to try to run some diagnostics.").append("\n");
boolean pragma1Passes = false;
boolean pragma2Passes = true;
output.append(" ===== PRAGMA integrity_check =====").append("\n");
try (Cursor cursor = db.rawQuery("PRAGMA integrity_check", null)) {
while (cursor.moveToNext()) {
String row = CursorUtil.readRowAsString(cursor);
output.append(row).append("\n");
if (row.toLowerCase().contains("ok")) {
pragma1Passes = true;
}
}
} catch (Throwable t) {
pragma1Passes = false;
output.append("Failed to do integrity_check!").append("\n")
.append(ExceptionUtil.convertThrowableToString(t));
}
output.append("\n").append("===== PRAGMA cipher_integrity_check =====").append("\n");
try (Cursor cursor = db.rawQuery("PRAGMA cipher_integrity_check", null)) {
while (cursor.moveToNext()) {
output.append(CursorUtil.readRowAsString(cursor)).append("\n");
pragma2Passes = false;
}
} catch (Throwable t) {
pragma2Passes = false;
output.append("Failed to do cipher_integrity_check!").append("\n")
.append(ExceptionUtil.convertThrowableToString(t));
}
Log.e(TAG, output.toString());
List<String> lines = new ArrayList<>(Arrays.asList(output.toString().split("\n")));
if (pragma1Passes && pragma2Passes) {
throw new DatabaseCorruptedError_BothChecksPass(lines);
} else if (!pragma1Passes && pragma2Passes) {
throw new DatabaseCorruptedError_NormalCheckFailsCipherCheckPasses(lines);
} else if (pragma1Passes && !pragma2Passes) {
throw new DatabaseCorruptedError_NormalCheckPassesCipherCheckFails(lines);
} else {
throw new DatabaseCorruptedError_BothChecksFail(lines);
}
}
public static class CustomTraceError extends Error {
CustomTraceError(@NonNull List<String> lines) {
StackTraceElement[] custom = lines.stream().map(line -> new StackTraceElement(line, "", "", 0)).toArray(StackTraceElement[]::new);
setStackTrace(ExceptionUtil.joinStackTrace(getStackTrace(), custom));
}
}
public static final class DatabaseCorruptedError_BothChecksPass extends CustomTraceError {
DatabaseCorruptedError_BothChecksPass(@NonNull List<String> lines) {
super(lines);
}
}
public static final class DatabaseCorruptedError_BothChecksFail extends CustomTraceError {
DatabaseCorruptedError_BothChecksFail(@NonNull List<String> lines) {
super(lines);
}
}
public static final class DatabaseCorruptedError_NormalCheckFailsCipherCheckPasses extends CustomTraceError {
DatabaseCorruptedError_NormalCheckFailsCipherCheckPasses(@NonNull List<String> lines) {
super(lines);
}
}
public static final class DatabaseCorruptedError_NormalCheckPassesCipherCheckFails extends CustomTraceError {
DatabaseCorruptedError_NormalCheckPassesCipherCheckFails(@NonNull List<String> lines) {
super(lines);
}
}
}

Wyświetl plik

@ -0,0 +1,166 @@
package org.thoughtcrime.securesms.database
import android.content.Context
import net.zetetic.database.DatabaseErrorHandler
import net.zetetic.database.sqlcipher.SQLiteConnection
import net.zetetic.database.sqlcipher.SQLiteDatabase
import net.zetetic.database.sqlcipher.SQLiteDatabaseHook
import org.signal.core.util.ExceptionUtil
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.crypto.DatabaseSecretProvider
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.util.CursorUtil
import java.lang.Error
import java.util.concurrent.CountDownLatch
import java.util.concurrent.atomic.AtomicReference
import kotlin.text.StringBuilder
/**
* The default error handler wipes the file. This one instead prints some diagnostics and then crashes so the original corrupt file isn't lost.
*/
class SqlCipherErrorHandler(private val databaseName: String) : DatabaseErrorHandler {
override fun onCorruption(db: SQLiteDatabase) {
val output = StringBuilder()
output.append("Database '$databaseName' corrupted! Going to try to run some diagnostics.\n")
val result: DiagnosticResults = runDiagnostics(ApplicationDependencies.getApplication(), db)
var lines: List<String> = result.logs.split("\n")
lines = listOf("Database '$databaseName' corrupted!. Diagnostics results:\n") + lines
Log.e(TAG, "Database '$databaseName' corrupted!. Diagnostics results:\n ${result.logs}")
if (result is DiagnosticResults.Success) {
if (result.pragma1Passes && result.pragma2Passes) {
throw DatabaseCorruptedError_BothChecksPass(lines)
} else if (!result.pragma1Passes && result.pragma2Passes) {
throw DatabaseCorruptedError_NormalCheckFailsCipherCheckPasses(lines)
} else if (result.pragma1Passes && !result.pragma2Passes) {
throw DatabaseCorruptedError_NormalCheckPassesCipherCheckFails(lines)
} else {
throw DatabaseCorruptedError_BothChecksFail(lines)
}
} else {
throw DatabaseCorruptedError_FailedToRunChecks(lines)
}
}
/**
* Try running diagnostics on the current database instance. If that fails, try opening a new connection and running the diagnostics there.
*/
private fun runDiagnostics(context: Context, db: SQLiteDatabase): DiagnosticResults {
val sameConnectionResult: DiagnosticResults = runDiagnosticsMethod("same-connection", queryDatabase(db))
if (sameConnectionResult is DiagnosticResults.Success) {
return sameConnectionResult
}
val differentConnectionResult: AtomicReference<DiagnosticResults> = AtomicReference()
val databaseFile = context.getDatabasePath(databaseName)
val latch = CountDownLatch(1)
try {
SQLiteDatabase.openOrCreateDatabase(
databaseFile.absolutePath, DatabaseSecretProvider.getOrCreateDatabaseSecret(context).asString(), null, null,
object : SQLiteDatabaseHook {
override fun preKey(connection: SQLiteConnection) {}
override fun postKey(connection: SQLiteConnection) {
if (latch.count > 0) {
val result: DiagnosticResults = runDiagnosticsMethod("different-connection") { query -> connection.executeForString(query, null, null) }
differentConnectionResult.set(result)
latch.countDown()
}
}
}
)
} catch (t: Throwable) {
return DiagnosticResults.Failure(ExceptionUtil.convertThrowableToString(t))
}
latch.await()
return differentConnectionResult.get()!!
}
/**
* Do two different integrity checks and return the results.
*/
private fun runDiagnosticsMethod(descriptor: String, query: (String) -> String): DiagnosticResults {
val output = StringBuilder()
var pragma1Passes = false
var pragma2Passes = true
output.append(" ===== PRAGMA integrity_check ($descriptor) =====\n")
try {
val results = query("PRAGMA integrity_check")
output.append(results)
if (results.toLowerCase().contains("ok")) {
pragma1Passes = true
}
} catch (t: Throwable) {
output.append("Failed to do integrity_check!\n").append(ExceptionUtil.convertThrowableToString(t))
return DiagnosticResults.Failure(output.toString())
}
output.append("\n").append("===== PRAGMA cipher_integrity_check ($descriptor) =====\n")
try {
val results = query("PRAGMA cipher_integrity_check")
output.append(results)
if (results.trim().isNotEmpty()) {
pragma2Passes = false
}
} catch (t: Throwable) {
output.append("Failed to do cipher_integrity_check!\n")
.append(ExceptionUtil.convertThrowableToString(t))
return DiagnosticResults.Failure(output.toString())
}
return DiagnosticResults.Success(
pragma1Passes = pragma1Passes,
pragma2Passes = pragma2Passes,
logs = output.toString()
)
}
private fun queryDatabase(db: SQLiteDatabase): (String) -> String {
return { query ->
val output = StringBuilder()
db.rawQuery(query, null).use { cursor ->
while (cursor.moveToNext()) {
val row = CursorUtil.readRowAsString(cursor)
output.append(row).append("\n")
}
}
output.toString()
}
}
private sealed class DiagnosticResults(val logs: String) {
class Success(
val pragma1Passes: Boolean,
val pragma2Passes: Boolean,
logs: String
) : DiagnosticResults(logs)
class Failure(logs: String) : DiagnosticResults(logs)
}
private open class CustomTraceError constructor(lines: List<String>) : Error() {
init {
val custom: Array<StackTraceElement> = lines.map { line -> StackTraceElement(line, "", "", 0) }.toTypedArray()
stackTrace = ExceptionUtil.joinStackTrace(stackTrace, custom)
}
}
private class DatabaseCorruptedError_BothChecksPass constructor(lines: List<String>) : CustomTraceError(lines)
private class DatabaseCorruptedError_BothChecksFail constructor(lines: List<String>) : CustomTraceError(lines)
private class DatabaseCorruptedError_NormalCheckFailsCipherCheckPasses constructor(lines: List<String>) : CustomTraceError(lines)
private class DatabaseCorruptedError_NormalCheckPassesCipherCheckFails constructor(lines: List<String>) : CustomTraceError(lines)
private class DatabaseCorruptedError_FailedToRunChecks constructor(lines: List<String>) : CustomTraceError(lines)
companion object {
private val TAG = Log.tag(SqlCipherErrorHandler::class.java)
}
}