2021-11-29 21:56:29 +00:00
|
|
|
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
|
2022-03-23 16:56:11 +00:00
|
|
|
import org.signal.core.util.CursorUtil
|
2021-11-29 21:56:29 +00:00
|
|
|
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 java.util.concurrent.CountDownLatch
|
|
|
|
import java.util.concurrent.atomic.AtomicReference
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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(
|
2023-02-06 15:04:40 +00:00
|
|
|
databaseFile.absolutePath,
|
|
|
|
DatabaseSecretProvider.getOrCreateDatabaseSecret(context).asString(),
|
|
|
|
null,
|
|
|
|
null,
|
2021-11-29 21:56:29 +00:00
|
|
|
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)
|
2022-05-18 18:05:17 +00:00
|
|
|
if (results.lowercase().contains("ok")) {
|
2021-11-29 21:56:29 +00:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|