From dafb17b5fa81259a7a70804b56c233b05e76c3b9 Mon Sep 17 00:00:00 2001 From: Anthony Bilinski Date: Tue, 25 Jun 2019 20:06:22 -0700 Subject: [PATCH] fix(db): support databases with either SQLCipher 3.x or 4.x defaults Fix #5451 --- src/persistence/db/rawdatabase.cpp | 209 ++++++++++++++++++++++------- src/persistence/db/rawdatabase.h | 9 ++ 2 files changed, 172 insertions(+), 46 deletions(-) diff --git a/src/persistence/db/rawdatabase.cpp b/src/persistence/db/rawdatabase.cpp index 84e0058b3..2695f3140 100644 --- a/src/persistence/db/rawdatabase.cpp +++ b/src/persistence/db/rawdatabase.cpp @@ -151,7 +151,7 @@ bool RawDatabase::open(const QString& path, const QString& hexKey) if (!QFile::exists(path) && QFile::exists(path + ".tmp")) { qWarning() << "Restoring database from temporary export file! Did we crash while changing " - "the password?"; + "the password or upgrading?"; QFile::rename(path + ".tmp", path); } @@ -175,27 +175,127 @@ bool RawDatabase::open(const QString& path, const QString& hexKey) } if (!hexKey.isEmpty()) { - if (!execNow("PRAGMA key = \"x'" + hexKey + "'\"")) { - qWarning() << "Failed to set encryption key"; + if (!openEncryptedDatabaseAtLatestVersion(hexKey)) { close(); return false; } + } + return true; +} - // #5451 SQLCipher 4.x has new crypto defaults that won't work with DBs saved with 3.x. - // manually use existing 3.x defaults for now, until SQLCipher upgrade is functional. - if (!execNow("PRAGMA cipher_page_size = 1024; PRAGMA kdf_iter = 64000;" - " PRAGMA cipher_hmac_algorithm = HMAC_SHA1; PRAGMA cipher_kdf_algorithm = PBKDF2_HMAC_SHA1;")) { - qWarning() << "Failed to prepare SQLCipher for version 3.x"; - close(); - return false; - } +bool RawDatabase::openEncryptedDatabaseAtLatestVersion(const QString& hexKey) +{ + // old qTox database are saved with SQLCipher 3.x defaults. New qTox (and for a period during 1.16.3 master) are stored + // with 4.x defaults. We need to support opening both databases saved with 3.x defaults and 4.x defaults + // so upgrade from 3.x default to 4.x defaults while we're at it + if (!setKey(hexKey)) { + return false; + } - if (!execNow("SELECT count(*) FROM sqlite_master")) { - qWarning() << "Database is unusable, check that the password is correct"; - close(); + if (setCipherParameters(4)) { + if (testUsable()) { + qInfo() << "Opened database with SQLCipher 4.x parameters"; + return true; + } else { + return updateSavedCipherParameters(hexKey); + } + } else { + // setKey again to clear old bad cipher settings + if (setKey(hexKey) && setCipherParameters(3) && testUsable()) { + qInfo() << "Opened database with SQLCipher 3.x parameters"; + return true; + } else { + qCritical() << "Failed to open database with SQLCipher 3.x parameters"; return false; } } +} + +bool RawDatabase::testUsable() +{ + // this will unfortunately log a warning if it fails, even though we may expect failure + return execNow("SELECT count(*) FROM sqlite_master;"); +} + +/** + * @brief Changes stored db encryption from SQLCipher 3.x defaults to 4.x defaults + */ +bool RawDatabase::updateSavedCipherParameters(const QString& hexKey) +{ + setKey(hexKey); // setKey again because a SELECT has already been run, causing crypto settings to take effect + if (!setCipherParameters(3)) { + return false; + } + + int64_t user_version; + if (!execNow(RawDatabase::Query("PRAGMA user_version", [&](const QVector& row) { + user_version = row[0].toLongLong(); + }))) { + qCritical() << "Failed to read user_version during cipher upgrade"; + return false; + } + if (!execNow("ATTACH DATABASE '" + path + ".tmp' AS sqlcipher4 KEY \"x'" + hexKey + "'\";")) { + return false; + } + if (!setCipherParameters(4, "sqlcipher4")) { + return false; + } + if (!execNow("SELECT sqlcipher_export('sqlcipher4');")) { + return false; + } + if (!execNow(QString("PRAGMA sqlcipher4.user_version = %1;").arg(user_version))) { + return false; + } + if (!execNow("DETACH DATABASE sqlcipher4;")) { + return false; + } + if (!commitDbSwap(hexKey)) { + return false; + } + qInfo() << "Upgraded database from SQLCipher 3.x defaults to SQLCipher 4.x defaults"; + return true; +} + +bool RawDatabase::setCipherParameters(int majorVersion, const QString& database) +{ + QString prefix; + if (!database.isNull()) { + prefix = database + "."; + } + // from https://www.zetetic.net/blog/2018/11/30/sqlcipher-400-release/ + const QString default3_xParams{"PRAGMA database.cipher_page_size = 1024; PRAGMA database.kdf_iter = 64000;" + "PRAGMA database.cipher_hmac_algorithm = HMAC_SHA1;" + "PRAGMA database.cipher_kdf_algorithm = PBKDF2_HMAC_SHA1;"}; + const QString default4_xParams{"PRAGMA database.cipher_page_size = 4096; PRAGMA database.kdf_iter = 256000;" + "PRAGMA database.cipher_hmac_algorithm = HMAC_SHA512;" + "PRAGMA database.cipher_kdf_algorithm = PBKDF2_HMAC_SHA512;"}; + + QString defaultParams; + switch(majorVersion) { + case 3: { + defaultParams = default3_xParams; + break; + } + case 4: { + defaultParams = default4_xParams; + break; + } + default: { + qCritical() << __FUNCTION__ << "called with unsupported SQLCipher major version" << majorVersion; + return false; + } + } + qDebug() << "Setting SQLCipher 4.x parameters"; + return execNow(defaultParams.replace("database.", prefix)); +} + +bool RawDatabase::setKey(const QString& hexKey) +{ + // setKey again to clear old bad cipher settings + if (!execNow("PRAGMA key = \"x'" + hexKey + "'\"")) { + qWarning() << "Failed to set encryption key"; + return false; + } return true; } @@ -356,51 +456,68 @@ bool RawDatabase::setPassword(const QString& password) return false; } } else { - // Need to encrypt the database - if (!execNow("ATTACH DATABASE '" + path + ".tmp' AS encrypted KEY \"x'" + newHexKey - + "'\";" - "SELECT sqlcipher_export('encrypted');" - "DETACH DATABASE encrypted;")) { - qWarning() << "Failed to export encrypted database"; + if (!encryptDatabase(newHexKey)) { close(); return false; } - - // This is racy as hell, but nobody will race with us since we hold the profile lock - // If we crash or die here, the rename should be atomic, so we can recover no matter - // what - close(); - QFile::remove(path); - QFile::rename(path + ".tmp", path); currentHexKey = newHexKey; - if (!open(path, currentHexKey)) { - qWarning() << "Failed to open encrypted database"; - return false; - } } } else { if (currentHexKey.isEmpty()) return true; - // Need to decrypt the database - if (!execNow("ATTACH DATABASE '" + path + ".tmp' AS plaintext KEY '';" - "SELECT sqlcipher_export('plaintext');" - "DETACH DATABASE plaintext;")) { - qWarning() << "Failed to export decrypted database"; + if (!decryptDatabase()) { close(); return false; } + } + return true; +} - // This is racy as hell, but nobody will race with us since we hold the profile lock - // If we crash or die here, the rename should be atomic, so we can recover no matter what - close(); - QFile::remove(path); - QFile::rename(path + ".tmp", path); - currentHexKey.clear(); - if (!open(path)) { - qCritical() << "Failed to open decrypted database"; - return false; - } +bool RawDatabase::encryptDatabase(const QString& newHexKey) +{ + if (!execNow("ATTACH DATABASE '" + path + ".tmp' AS encrypted KEY \"x'" + newHexKey + + "'\";")) { + qWarning() << "Failed to export encrypted database"; + return false; + } + if (!setCipherParameters(4, "encrypted")) { + return false; + } + if (!execNow("SELECT sqlcipher_export('encrypted');")) { + return false; + } + if (!execNow("DETACH DATABASE encrypted;")) { + return false; + } + return commitDbSwap(newHexKey); +} + +bool RawDatabase::decryptDatabase() +{ + if (!execNow("ATTACH DATABASE '" + path + ".tmp' AS plaintext KEY '';" + "SELECT sqlcipher_export('plaintext');")) { + qWarning() << "Failed to export decrypted database"; + return false; + } + if (!execNow("DETACH DATABASE plaintext;")) { + return false; + } + return commitDbSwap({}); +} + +bool RawDatabase::commitDbSwap(const QString& hexKey) +{ + // This is racy as hell, but nobody will race with us since we hold the profile lock + // If we crash or die here, the rename should be atomic, so we can recover no matter + // what + close(); + QFile::remove(path); + QFile::rename(path + ".tmp", path); + currentHexKey = hexKey; + if (!open(path, currentHexKey)) { + qCritical() << "Failed to swap db"; + return false; } return true; } diff --git a/src/persistence/db/rawdatabase.h b/src/persistence/db/rawdatabase.h index 9ffcfe67d..318978064 100644 --- a/src/persistence/db/rawdatabase.h +++ b/src/persistence/db/rawdatabase.h @@ -89,6 +89,15 @@ protected slots: private: QString anonymizeQuery(const QByteArray& query); + bool openEncryptedDatabaseAtLatestVersion(const QString& hexKey); + bool updateSavedCipherParameters(const QString& hexKey); + bool setCipherParameters(int majorVersion, const QString& database = {}); + bool setKey(const QString& hexKey); + int getUserVersion(); + bool encryptDatabase(const QString& newHexKey); + bool decryptDatabase(); + bool commitDbSwap(const QString& hexKey); + bool testUsable(); protected: static QString deriveKey(const QString& password, const QByteArray& salt);