|
|
|
@ -151,7 +151,7 @@ bool RawDatabase::open(const QString& path, const QString& hexKey)
@@ -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)
@@ -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(); |
|
|
|
|
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,52 +456,69 @@ bool RawDatabase::setPassword(const QString& password)
@@ -356,52 +456,69 @@ 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; |
|
|
|
|
} |
|
|
|
|
currentHexKey = newHexKey; |
|
|
|
|
} |
|
|
|
|
} else { |
|
|
|
|
if (currentHexKey.isEmpty()) |
|
|
|
|
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
|
|
|
|
|
if (!decryptDatabase()) { |
|
|
|
|
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
|
|
|
|
|
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');" |
|
|
|
|
"DETACH DATABASE plaintext;")) { |
|
|
|
|
"SELECT sqlcipher_export('plaintext');")) { |
|
|
|
|
qWarning() << "Failed to export decrypted database"; |
|
|
|
|
close(); |
|
|
|
|
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
|
|
|
|
|
// 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"; |
|
|
|
|
currentHexKey = hexKey; |
|
|
|
|
if (!open(path, currentHexKey)) { |
|
|
|
|
qCritical() << "Failed to swap db"; |
|
|
|
|
return false; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
return true; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|