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")) {
|
if (!QFile::exists(path) && QFile::exists(path + ".tmp")) {
|
||||||
qWarning() << "Restoring database from temporary export file! Did we crash while changing "
|
qWarning() << "Restoring database from temporary export file! Did we crash while changing "
|
||||||
"the password?";
|
"the password or upgrading?";
|
||||||
QFile::rename(path + ".tmp", path);
|
QFile::rename(path + ".tmp", path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -175,27 +175,127 @@ bool RawDatabase::open(const QString& path, const QString& hexKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hexKey.isEmpty()) {
|
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 + "'\"")) {
|
if (!execNow("PRAGMA key = \"x'" + hexKey + "'\"")) {
|
||||||
qWarning() << "Failed to set encryption key";
|
qWarning() << "Failed to set encryption key";
|
||||||
close();
|
|
||||||
return false;
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -356,52 +456,69 @@ bool RawDatabase::setPassword(const QString& password)
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Need to encrypt the database
|
if (!encryptDatabase(newHexKey)) {
|
||||||
if (!execNow("ATTACH DATABASE '" + path + ".tmp' AS encrypted KEY \"x'" + newHexKey
|
|
||||||
+ "'\";"
|
|
||||||
"SELECT sqlcipher_export('encrypted');"
|
|
||||||
"DETACH DATABASE encrypted;")) {
|
|
||||||
qWarning() << "Failed to export encrypted database";
|
|
||||||
close();
|
close();
|
||||||
return false;
|
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
|
// 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
|
// If we crash or die here, the rename should be atomic, so we can recover no matter
|
||||||
// what
|
// what
|
||||||
close();
|
close();
|
||||||
QFile::remove(path);
|
QFile::remove(path);
|
||||||
QFile::rename(path + ".tmp", path);
|
QFile::rename(path + ".tmp", path);
|
||||||
currentHexKey = newHexKey;
|
currentHexKey = hexKey;
|
||||||
if (!open(path, currentHexKey)) {
|
if (!open(path, currentHexKey)) {
|
||||||
qWarning() << "Failed to open encrypted database";
|
qCritical() << "Failed to swap db";
|
||||||
return false;
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -89,6 +89,15 @@ protected slots:
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QString anonymizeQuery(const QByteArray& query);
|
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:
|
protected:
|
||||||
static QString deriveKey(const QString& password, const QByteArray& salt);
|
static QString deriveKey(const QString& password, const QByteArray& salt);
|
||||||
|
|
Loading…
Reference in New Issue
Block a user