mirror of
https://github.com/qTox/qTox.git
synced 2024-03-22 14:00:36 +08:00
feat(database): use own public key as salt to encrypt the database
fixes #3583 BREAKING CHANGE: The database will be encrypted with a new key, qTox versions before this commit won't be able to decrypt the database.
This commit is contained in:
parent
949e3cb830
commit
c4b9d302d0
|
@ -91,15 +91,43 @@
|
|||
* @param password If empty, the database will be opened unencrypted.
|
||||
* Otherwise we will use toxencryptsave to derive a key and encrypt the database.
|
||||
*/
|
||||
RawDatabase::RawDatabase(const QString &path, const QString& password)
|
||||
: workerThread{new QThread}, path{path}, currentHexKey{deriveKey(password)}
|
||||
RawDatabase::RawDatabase(const QString& path, const QString& password, const QByteArray& salt)
|
||||
: workerThread{new QThread}
|
||||
, path{path}
|
||||
, currentSalt{salt} // we need the salt later if a new password should be set
|
||||
, currentHexKey{deriveKey(password, salt)}
|
||||
{
|
||||
workerThread->setObjectName("qTox Database");
|
||||
moveToThread(workerThread.get());
|
||||
workerThread->start();
|
||||
|
||||
if (!open(path, currentHexKey))
|
||||
// first try with the new salt
|
||||
if (open(path, currentHexKey))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// avoid opening the same db twice
|
||||
close();
|
||||
|
||||
// fall back to the old salt
|
||||
currentHexKey = deriveKey(password);
|
||||
if(open(path, currentHexKey))
|
||||
{
|
||||
// still using old salt, upgrade
|
||||
if(setPassword(password))
|
||||
{
|
||||
qDebug() << "Successfully upgraded to dynamic salt";
|
||||
}
|
||||
else
|
||||
{
|
||||
qWarning() << "Failed to set password with new salt";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
qDebug() << "Failed to open database with old salt";
|
||||
}
|
||||
}
|
||||
|
||||
RawDatabase::~RawDatabase()
|
||||
|
@ -313,7 +341,7 @@ bool RawDatabase::setPassword(const QString& password)
|
|||
|
||||
if (!password.isEmpty())
|
||||
{
|
||||
QString newHexKey = deriveKey(password);
|
||||
QString newHexKey = deriveKey(password, currentSalt);
|
||||
if (!currentHexKey.isEmpty())
|
||||
{
|
||||
if (!execNow("PRAGMA rekey = \"x'"+newHexKey+"'\""))
|
||||
|
@ -445,6 +473,7 @@ bool RawDatabase::remove()
|
|||
* @brief Derives a 256bit key from the password and returns it hex-encoded
|
||||
* @param password Password to decrypt database
|
||||
* @return String representation of key
|
||||
* @deprecated deprecated on 2016-11-06, kept for compatibility, replaced by the salted version
|
||||
*/
|
||||
QString RawDatabase::deriveKey(const QString &password)
|
||||
{
|
||||
|
@ -457,8 +486,37 @@ QString RawDatabase::deriveKey(const QString &password)
|
|||
|
||||
static const uint8_t expandConstant[TOX_PASS_SALT_LENGTH+1] = "L'ignorance est le pire des maux";
|
||||
TOX_PASS_KEY key;
|
||||
tox_derive_key_with_salt((uint8_t*)passData.data(), passData.size(), expandConstant, &key, nullptr);
|
||||
return QByteArray((char*)key.key, 32).toHex();
|
||||
tox_derive_key_with_salt(reinterpret_cast<uint8_t*>(passData.data()), static_cast<std::size_t>(passData.size()), expandConstant, &key, nullptr);
|
||||
return QByteArray(reinterpret_cast<char*>(key.key), 32).toHex();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Derives a 256bit key from the password and returns it hex-encoded
|
||||
* @param password Password to decrypt database
|
||||
* @param salt Salt to improve password strength, must be TOX_PASS_SALT_LENGTH bytes
|
||||
* @return String representation of key
|
||||
*/
|
||||
QString RawDatabase::deriveKey(const QString& password, const QByteArray& salt)
|
||||
{
|
||||
if (password.isEmpty())
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
if (salt.length() != TOX_PASS_SALT_LENGTH)
|
||||
{
|
||||
qWarning() << "Salt length doesn't match toxencryptsave expections";
|
||||
return {};
|
||||
}
|
||||
|
||||
QByteArray passData = password.toUtf8();
|
||||
|
||||
static_assert(TOX_PASS_KEY_LENGTH >= 32, "toxcore must provide 256bit or longer keys");
|
||||
|
||||
TOX_PASS_KEY key;
|
||||
tox_derive_key_with_salt(reinterpret_cast<uint8_t*>(passData.data()), static_cast<std::size_t>(passData.size()),
|
||||
reinterpret_cast<const uint8_t*>(salt.constData()), &key, nullptr);
|
||||
return QByteArray(reinterpret_cast<char*>(key.key), 32).toHex();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -41,7 +41,7 @@ public:
|
|||
};
|
||||
|
||||
public:
|
||||
RawDatabase(const QString& path, const QString& password);
|
||||
RawDatabase(const QString& path, const QString& password, const QByteArray& salt);
|
||||
~RawDatabase();
|
||||
bool isOpen();
|
||||
|
||||
|
@ -68,7 +68,8 @@ private:
|
|||
QString anonymizeQuery(const QByteArray& query);
|
||||
|
||||
protected:
|
||||
static QString deriveKey(const QString &password);
|
||||
static QString deriveKey(const QString& password, const QByteArray& salt);
|
||||
static QString deriveKey(const QString& password);
|
||||
static QVariant extractData(sqlite3_stmt* stmt, int col);
|
||||
|
||||
private:
|
||||
|
@ -85,6 +86,7 @@ private:
|
|||
QQueue<Transaction> pendingTransactions;
|
||||
QMutex transactionsMutex;
|
||||
QString path;
|
||||
QByteArray currentSalt;
|
||||
QString currentHexKey;
|
||||
};
|
||||
|
||||
|
|
|
@ -56,7 +56,6 @@ QVector<QString> Profile::profiles;
|
|||
|
||||
Profile::Profile(QString name, const QString& password, bool isNewProfile)
|
||||
: name{name}, password{password}
|
||||
, database(std::make_shared<RawDatabase>(getDbPath(name), password))
|
||||
, newProfile{isNewProfile}, isRemoved{false}
|
||||
{
|
||||
if (!password.isEmpty())
|
||||
|
@ -68,22 +67,10 @@ Profile::Profile(QString name, const QString& password, bool isNewProfile)
|
|||
s.setCurrentProfile(name);
|
||||
s.saveGlobal();
|
||||
|
||||
// At this point it's too early to load the personal settings (Nexus will do
|
||||
// it), so we always load the history, and if it fails we can't change the
|
||||
// setting now, but we keep a nullptr
|
||||
if (database->isOpen())
|
||||
{
|
||||
history.reset(new History(database));
|
||||
}
|
||||
else
|
||||
{
|
||||
qWarning() << "Failed to open history for profile" << name;
|
||||
GUI::showError(QObject::tr("Error"), QObject::tr("qTox couldn't open your chat logs, they will be disabled."));
|
||||
}
|
||||
|
||||
coreThread = new QThread();
|
||||
coreThread->setObjectName("qTox Core");
|
||||
core = new Core(coreThread, *this);
|
||||
QObject::connect(core, &Core::idSet, this, &Profile::loadDatabase, Qt::QueuedConnection);
|
||||
core->moveToThread(coreThread);
|
||||
QObject::connect(coreThread, &QThread::started, core, &Core::start);
|
||||
}
|
||||
|
@ -512,6 +499,34 @@ QByteArray Profile::loadAvatarData(const QString& ownerId, const QString& passwo
|
|||
return pic;
|
||||
}
|
||||
|
||||
void Profile::loadDatabase(const QString& id)
|
||||
{
|
||||
if(isRemoved)
|
||||
{
|
||||
qDebug() << "Can't load database of removed profile";
|
||||
return;
|
||||
}
|
||||
|
||||
QByteArray salt = QByteArray::fromHex(ToxId {id}.publicKey.toUtf8());
|
||||
if(salt.size() != TOX_PASS_SALT_LENGTH)
|
||||
{
|
||||
qWarning() << "Couldn't compute salt from public key" << name;
|
||||
GUI::showError(QObject::tr("Error"), QObject::tr("qTox couldn't open your chat logs, they will be disabled."));
|
||||
}
|
||||
// At this point it's too early to load the personal settings (Nexus will do it), so we always load
|
||||
// the history, and if it fails we can't change the setting now, but we keep a nullptr
|
||||
database = std::make_shared<RawDatabase>(getDbPath(name), password, salt);
|
||||
if (database && database->isOpen())
|
||||
{
|
||||
history.reset(new History(database));
|
||||
}
|
||||
else
|
||||
{
|
||||
qWarning() << "Failed to open database for profile" << name;
|
||||
GUI::showError(QObject::tr("Error"), QObject::tr("qTox couldn't open your chat logs, they will be disabled."));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Save an avatar to cache.
|
||||
* @param pic Picture to save.
|
||||
|
@ -688,11 +703,12 @@ QVector<QString> Profile::remove()
|
|||
}
|
||||
|
||||
QString dbPath = getDbPath(name);
|
||||
if (database->isOpen() && !database->remove() && QFile::exists(dbPath))
|
||||
if (database && database->isOpen() && !database->remove() && QFile::exists(dbPath))
|
||||
{
|
||||
ret.push_back(dbPath);
|
||||
qWarning() << "Could not remove file " << dbPath;
|
||||
}
|
||||
|
||||
history.release();
|
||||
database.reset();
|
||||
|
||||
|
|
|
@ -25,6 +25,7 @@
|
|||
#include <QString>
|
||||
#include <QByteArray>
|
||||
#include <QPixmap>
|
||||
#include <QObject>
|
||||
#include <tox/toxencryptsave.h>
|
||||
#include <memory>
|
||||
#include "src/persistence/history.h"
|
||||
|
@ -32,8 +33,10 @@
|
|||
class Core;
|
||||
class QThread;
|
||||
|
||||
class Profile
|
||||
class Profile : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
static Profile* loadProfile(QString name, const QString& password = QString());
|
||||
static Profile* createProfile(QString name, QString password);
|
||||
|
@ -78,6 +81,8 @@ public:
|
|||
static bool isEncrypted(QString name);
|
||||
static QString getDbPath(const QString& profileName);
|
||||
|
||||
private slots:
|
||||
void loadDatabase(const QString& id);
|
||||
private:
|
||||
Profile(QString name, const QString& password, bool newProfile);
|
||||
static QVector<QString> getFilesByExt(QString extension);
|
||||
|
|
Loading…
Reference in New Issue
Block a user