1
0
mirror of https://github.com/qTox/qTox.git synced 2024-03-22 14:00:36 +08:00

fix(db): support databases with either SQLCipher 3.x or 4.x defaults

Fix #5451
This commit is contained in:
Anthony Bilinski 2019-06-25 20:06:22 -07:00
parent 86f0c3a54c
commit dafb17b5fa
No known key found for this signature in database
GPG Key ID: 2AA8E0DA1B31FB3C
2 changed files with 172 additions and 46 deletions

View File

@ -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<QVariant>& 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;
}

View File

@ -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);