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:
parent
86f0c3a54c
commit
dafb17b5fa
|
@ -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 (!openEncryptedDatabaseAtLatestVersion(hexKey)) {
|
||||
close();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
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 (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";
|
||||
close();
|
||||
return false;
|
||||
}
|
||||
|
||||
// #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;
|
||||
}
|
||||
|
||||
if (!execNow("SELECT count(*) FROM sqlite_master")) {
|
||||
qWarning() << "Database is unusable, check that the password is correct";
|
||||
close();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
||||
if (!decryptDatabase()) {
|
||||
close();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
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 = newHexKey;
|
||||
currentHexKey = hexKey;
|
||||
if (!open(path, currentHexKey)) {
|
||||
qWarning() << "Failed to open encrypted database";
|
||||
qCritical() << "Failed to swap db";
|
||||
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";
|
||||
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.clear();
|
||||
if (!open(path)) {
|
||||
qCritical() << "Failed to open decrypted database";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in New Issue
Block a user