mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-05-08 14:33:23 +00:00
511 lines
14 KiB
C++
511 lines
14 KiB
C++
#include "secure_qsettings.h"
|
|
|
|
#include "../client/3rd/QSimpleCrypto/src/include/QAead.h"
|
|
#include "../client/3rd/QSimpleCrypto/src/include/QBlockCipher.h"
|
|
#include "utilities.h"
|
|
#include <QDataStream>
|
|
#include <QDebug>
|
|
#include <QEventLoop>
|
|
#include <QIODevice>
|
|
#include <QJsonDocument>
|
|
#include <QJsonObject>
|
|
#include <QRandomGenerator>
|
|
#include <QSharedPointer>
|
|
#include <QTimer>
|
|
#include <QFile>
|
|
|
|
#include <openssl/evp.h>
|
|
#include <openssl/rand.h>
|
|
#include <openssl/err.h>
|
|
|
|
using namespace QKeychain;
|
|
|
|
namespace {
|
|
constexpr const char *settingsKeyTag = "settingsKeyTag";
|
|
constexpr const char *settingsIvTag = "settingsIvTag";
|
|
constexpr const char *keyChainName = "AmneziaVPN-Keychain";
|
|
|
|
constexpr int SALT_LEN = 16;
|
|
constexpr int IV_LEN = 16;
|
|
constexpr int KEY_LEN = 32;
|
|
constexpr int PBKDF2_ITER = 100000;
|
|
}
|
|
|
|
SecureQSettings::SecureQSettings(const QString &organization, const QString &application, QObject *parent)
|
|
: QObject { parent }, m_settings(organization, application, parent), encryptedKeys({ "Servers/serversList" })
|
|
{
|
|
bool encrypted = m_settings.value("Conf/encrypted").toBool();
|
|
|
|
// convert settings to encrypted for if updated to >= 2.1.0
|
|
if (encryptionRequired() && !encrypted) {
|
|
for (const QString &key : m_settings.allKeys()) {
|
|
if (encryptedKeys.contains(key)) {
|
|
const QVariant &val = value(key);
|
|
setValue(key, val);
|
|
}
|
|
}
|
|
m_settings.setValue("Conf/encrypted", true);
|
|
m_settings.sync();
|
|
}
|
|
}
|
|
|
|
QVariant SecureQSettings::value(const QString &key, const QVariant &defaultValue) const
|
|
{
|
|
QMutexLocker locker(&mutex);
|
|
|
|
if (m_cache.contains(key)) {
|
|
return m_cache.value(key);
|
|
}
|
|
|
|
if (!m_settings.contains(key))
|
|
return defaultValue;
|
|
|
|
QVariant retVal;
|
|
|
|
// check if value is not encrypted, v. < 2.0.x
|
|
retVal = m_settings.value(key);
|
|
if (retVal.isValid()) {
|
|
if (retVal.userType() == QMetaType::QByteArray && retVal.toByteArray().mid(0, magicString.size()) == magicString) {
|
|
|
|
if (getEncKey().isEmpty() || getEncIv().isEmpty()) {
|
|
qCritical() << "SecureQSettings::setValue Decryption requested, but key is empty";
|
|
return {};
|
|
}
|
|
|
|
QByteArray encryptedValue = retVal.toByteArray().mid(magicString.size());
|
|
|
|
QByteArray decryptedValue = decryptText(encryptedValue);
|
|
QDataStream ds(&decryptedValue, QIODevice::ReadOnly);
|
|
|
|
ds >> retVal;
|
|
|
|
if (!retVal.isValid()) {
|
|
qWarning() << "SecureQSettings::value settings decryption failed";
|
|
retVal = QVariant();
|
|
}
|
|
}
|
|
} else {
|
|
qWarning() << "SecureQSettings::value invalid QVariant value";
|
|
retVal = QVariant();
|
|
}
|
|
|
|
m_cache.insert(key, retVal);
|
|
return retVal;
|
|
}
|
|
|
|
void SecureQSettings::setValue(const QString &key, const QVariant &value)
|
|
{
|
|
QMutexLocker locker(&mutex);
|
|
|
|
if (encryptionRequired() && encryptedKeys.contains(key)) {
|
|
if (!getEncKey().isEmpty() && !getEncIv().isEmpty()) {
|
|
QByteArray decryptedValue;
|
|
{
|
|
QDataStream ds(&decryptedValue, QIODevice::WriteOnly);
|
|
ds << value;
|
|
}
|
|
|
|
QByteArray encryptedValue = encryptText(decryptedValue);
|
|
m_settings.setValue(key, magicString + encryptedValue);
|
|
} else {
|
|
qCritical() << "SecureQSettings::setValue Encryption required, but key is empty";
|
|
return;
|
|
}
|
|
|
|
} else {
|
|
m_settings.setValue(key, value);
|
|
}
|
|
|
|
m_cache.insert(key, value);
|
|
sync();
|
|
}
|
|
|
|
void SecureQSettings::remove(const QString &key)
|
|
{
|
|
QMutexLocker locker(&mutex);
|
|
|
|
m_settings.remove(key);
|
|
m_cache.remove(key);
|
|
|
|
sync();
|
|
}
|
|
|
|
void SecureQSettings::sync()
|
|
{
|
|
m_settings.sync();
|
|
}
|
|
|
|
QByteArray SecureQSettings::backupAppConfig() const
|
|
{
|
|
QJsonObject cfg;
|
|
|
|
const auto needToBackup = [this](const auto &key) {
|
|
for (const auto &item : m_fieldsToBackup)
|
|
{
|
|
if (key == "Conf/installationUuid")
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (key.startsWith(item))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
for (const QString &key : m_settings.allKeys()) {
|
|
|
|
if (!needToBackup(key))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
cfg.insert(key, QJsonValue::fromVariant(value(key)));
|
|
}
|
|
|
|
return QJsonDocument(cfg).toJson();
|
|
}
|
|
|
|
bool SecureQSettings::restoreAppConfig(const QByteArray &json)
|
|
{
|
|
QJsonObject cfg = QJsonDocument::fromJson(json).object();
|
|
if (cfg.isEmpty())
|
|
return false;
|
|
|
|
for (const QString &key : cfg.keys()) {
|
|
if (key == "Conf/installationUuid") {
|
|
continue;
|
|
}
|
|
|
|
setValue(key, cfg.value(key).toVariant());
|
|
}
|
|
|
|
sync();
|
|
return true;
|
|
}
|
|
|
|
QByteArray SecureQSettings::encryptText(const QByteArray &value) const
|
|
{
|
|
QSimpleCrypto::QBlockCipher cipher;
|
|
QByteArray result;
|
|
try {
|
|
result = cipher.encryptAesBlockCipher(value, getEncKey(), getEncIv());
|
|
} catch (...) { // todo change error handling in QSimpleCrypto?
|
|
qCritical() << "error when encrypting the settings value";
|
|
}
|
|
return result;
|
|
}
|
|
|
|
QByteArray SecureQSettings::decryptText(const QByteArray &ba) const
|
|
{
|
|
QSimpleCrypto::QBlockCipher cipher;
|
|
QByteArray result;
|
|
try {
|
|
result = cipher.decryptAesBlockCipher(ba, getEncKey(), getEncIv());
|
|
} catch (...) { // todo change error handling in QSimpleCrypto?
|
|
qCritical() << "error when decrypting the settings value";
|
|
}
|
|
return result;
|
|
}
|
|
|
|
bool SecureQSettings::encryptionRequired() const
|
|
{
|
|
#if defined(Q_OS_LINUX) && !defined(Q_OS_ANDROID)
|
|
// QtKeyChain failing on Linux
|
|
return false;
|
|
#endif
|
|
return true;
|
|
}
|
|
|
|
QByteArray SecureQSettings::getEncKey() const
|
|
{
|
|
// load keys from system key storage
|
|
m_key = getSecTag(settingsKeyTag);
|
|
|
|
if (m_key.isEmpty()) {
|
|
// Create new key
|
|
QSimpleCrypto::QBlockCipher cipher;
|
|
QByteArray key = cipher.generatePrivateSalt(32);
|
|
if (key.isEmpty()) {
|
|
qCritical() << "SecureQSettings::getEncKey Unable to generate new enc key";
|
|
}
|
|
|
|
setSecTag(settingsKeyTag, key);
|
|
|
|
// check
|
|
m_key = getSecTag(settingsKeyTag);
|
|
if (key != m_key) {
|
|
qCritical() << "SecureQSettings::getEncKey Unable to store key in keychain" << key.size() << m_key.size();
|
|
return {};
|
|
}
|
|
}
|
|
|
|
return m_key;
|
|
}
|
|
|
|
QByteArray SecureQSettings::getEncIv() const
|
|
{
|
|
// load keys from system key storage
|
|
m_iv = getSecTag(settingsIvTag);
|
|
|
|
if (m_iv.isEmpty()) {
|
|
// Create new IV
|
|
QSimpleCrypto::QBlockCipher cipher;
|
|
QByteArray iv = cipher.generatePrivateSalt(32);
|
|
if (iv.isEmpty()) {
|
|
qCritical() << "SecureQSettings::getEncIv Unable to generate new enc IV";
|
|
}
|
|
setSecTag(settingsIvTag, iv);
|
|
|
|
// check
|
|
m_iv = getSecTag(settingsIvTag);
|
|
if (iv != m_iv) {
|
|
qCritical() << "SecureQSettings::getEncIv Unable to store IV in keychain" << iv.size() << m_iv.size();
|
|
return {};
|
|
}
|
|
}
|
|
|
|
return m_iv;
|
|
}
|
|
|
|
QByteArray SecureQSettings::getSecTag(const QString &tag)
|
|
{
|
|
auto job = QSharedPointer<ReadPasswordJob>(new ReadPasswordJob(keyChainName), &QObject::deleteLater);
|
|
job->setAutoDelete(false);
|
|
job->setKey(tag);
|
|
QEventLoop loop;
|
|
job->connect(job.data(), &ReadPasswordJob::finished, job.data(), [&loop]() { loop.quit(); });
|
|
job->start();
|
|
loop.exec();
|
|
|
|
if (job->error()) {
|
|
qCritical() << "SecureQSettings::getSecTag Error:" << job->errorString();
|
|
}
|
|
|
|
return job->binaryData();
|
|
}
|
|
|
|
void SecureQSettings::setSecTag(const QString &tag, const QByteArray &data)
|
|
{
|
|
auto job = QSharedPointer<WritePasswordJob>(new WritePasswordJob(keyChainName), &QObject::deleteLater);
|
|
job->setAutoDelete(false);
|
|
job->setKey(tag);
|
|
job->setBinaryData(data);
|
|
QEventLoop loop;
|
|
QTimer::singleShot(1000, &loop, SLOT(quit()));
|
|
job->connect(job.data(), &WritePasswordJob::finished, job.data(), [&loop]() { loop.quit(); });
|
|
job->start();
|
|
loop.exec();
|
|
|
|
if (job->error()) {
|
|
qCritical() << "SecureQSettings::setSecTag Error:" << job->errorString();
|
|
}
|
|
}
|
|
|
|
void SecureQSettings::setPassword(const QString &pwd)
|
|
{
|
|
m_password = pwd;
|
|
}
|
|
|
|
void SecureQSettings::setHint(const QString &hint)
|
|
{
|
|
m_hint = hint;
|
|
}
|
|
|
|
QString SecureQSettings::getPassword() const
|
|
{
|
|
return m_password;
|
|
}
|
|
|
|
QString SecureQSettings::getHint() const
|
|
{
|
|
return m_hint;
|
|
}
|
|
|
|
static QString opensslErrString()
|
|
{
|
|
unsigned long e = ERR_get_error();
|
|
if (!e)
|
|
return QStringLiteral("Unknown OpenSSL error");
|
|
char buf[256];
|
|
ERR_error_string_n(e, buf, sizeof(buf));
|
|
return QString::fromUtf8(buf);
|
|
}
|
|
|
|
static bool deriveKey(const QByteArray &password, const QByteArray &salt, QByteArray &outKey, QString *err)
|
|
{
|
|
outKey.resize(KEY_LEN);
|
|
const unsigned char *pw = reinterpret_cast<const unsigned char *>(password.constData());
|
|
const unsigned char *s = reinterpret_cast<const unsigned char *>(salt.constData());
|
|
int ok = PKCS5_PBKDF2_HMAC(reinterpret_cast<const char *>(pw), password.size(), s, salt.size(), PBKDF2_ITER,
|
|
EVP_sha256(), KEY_LEN, reinterpret_cast<unsigned char *>(outKey.data()));
|
|
if (!ok) {
|
|
if (err)
|
|
*err = opensslErrString();
|
|
}
|
|
return ok == 1;
|
|
}
|
|
|
|
static bool aesCrypt(const QByteArray &in, const QByteArray &key, const QByteArray &iv, QByteArray &out, bool encrypt,
|
|
QString *err)
|
|
{
|
|
EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
|
|
if (!ctx) {
|
|
if (err)
|
|
*err = "EVP_CIPHER_CTX_new failed";
|
|
return false;
|
|
}
|
|
const EVP_CIPHER *cipher = EVP_aes_256_cbc();
|
|
if (1
|
|
!= EVP_CipherInit_ex(ctx, cipher, nullptr, reinterpret_cast<const unsigned char *>(key.constData()),
|
|
reinterpret_cast<const unsigned char *>(iv.constData()), encrypt ? 1 : 0)) {
|
|
if (err)
|
|
*err = opensslErrString();
|
|
EVP_CIPHER_CTX_free(ctx);
|
|
return false;
|
|
}
|
|
|
|
out.clear();
|
|
out.resize(in.size() + EVP_CIPHER_block_size(cipher));
|
|
int outlen1 = 0;
|
|
if (1
|
|
!= EVP_CipherUpdate(ctx, reinterpret_cast<unsigned char *>(out.data()), &outlen1,
|
|
reinterpret_cast<const unsigned char *>(in.constData()), in.size())) {
|
|
if (err)
|
|
*err = opensslErrString();
|
|
EVP_CIPHER_CTX_free(ctx);
|
|
return false;
|
|
}
|
|
int outlen2 = 0;
|
|
if (1 != EVP_CipherFinal_ex(ctx, reinterpret_cast<unsigned char *>(out.data()) + outlen1, &outlen2)) {
|
|
if (err)
|
|
*err = opensslErrString();
|
|
EVP_CIPHER_CTX_free(ctx);
|
|
return false;
|
|
}
|
|
out.resize(outlen1 + outlen2);
|
|
EVP_CIPHER_CTX_free(ctx);
|
|
return true;
|
|
}
|
|
|
|
bool SecureQSettings::encryptFile(const QString &filePath, const QString &password, QString *error) const
|
|
{
|
|
QFile f(filePath);
|
|
if (!f.open(QIODevice::ReadOnly)) {
|
|
if (error)
|
|
*error = QStringLiteral("Cannot open file for read: %1").arg(f.errorString());
|
|
return false;
|
|
}
|
|
QByteArray plain = f.readAll();
|
|
f.close();
|
|
|
|
if (plain.startsWith(magicString)) {
|
|
if (error)
|
|
*error = QStringLiteral("File already encrypted (magic found)");
|
|
return false;
|
|
}
|
|
|
|
QByteArray salt(SALT_LEN, 0);
|
|
QByteArray iv(IV_LEN, 0);
|
|
QByteArray key;
|
|
QByteArray cipher;
|
|
QByteArray out;
|
|
|
|
if (1 != RAND_bytes(reinterpret_cast<unsigned char *>(salt.data()), SALT_LEN)
|
|
|| 1 != RAND_bytes(reinterpret_cast<unsigned char *>(iv.data()), IV_LEN)) {
|
|
if (error)
|
|
*error = opensslErrString();
|
|
return false;
|
|
}
|
|
|
|
if (!deriveKey(password.toUtf8(), salt, key, error))
|
|
return false;
|
|
|
|
if (!aesCrypt(plain, key, iv, cipher, true, error))
|
|
return false;
|
|
|
|
out.reserve(magicString.size() + SALT_LEN + IV_LEN + cipher.size());
|
|
out += magicString;
|
|
out += salt;
|
|
out += iv;
|
|
out += cipher;
|
|
|
|
if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
|
|
if (error)
|
|
*error = QStringLiteral("Cannot open file for write: %1").arg(f.errorString());
|
|
return false;
|
|
}
|
|
qint64 written = f.write(out);
|
|
f.close();
|
|
if (written != out.size()) {
|
|
if (error)
|
|
*error = QStringLiteral("Write failed or incomplete");
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool SecureQSettings::decryptFile(const QString &filePath, const QString &password, QString *error) const
|
|
{
|
|
QFile f(filePath);
|
|
if (!f.open(QIODevice::ReadOnly)) {
|
|
if (error)
|
|
*error = QStringLiteral("Cannot open file for read: %1").arg(f.errorString());
|
|
return false;
|
|
}
|
|
QByteArray blob = f.readAll();
|
|
f.close();
|
|
|
|
if (!blob.startsWith(magicString)) {
|
|
if (error)
|
|
*error = QStringLiteral("File is not recognized as encrypted (magic missing)");
|
|
return false;
|
|
}
|
|
|
|
int pos = magicString.size();
|
|
if (blob.size() < pos + SALT_LEN + IV_LEN) {
|
|
if (error)
|
|
*error = QStringLiteral("Encrypted file too small / corrupted");
|
|
return false;
|
|
}
|
|
|
|
QByteArray salt = blob.mid(pos, SALT_LEN);
|
|
pos += SALT_LEN;
|
|
QByteArray iv = blob.mid(pos, IV_LEN);
|
|
pos += IV_LEN;
|
|
QByteArray cipher = blob.mid(pos);
|
|
QByteArray key;
|
|
QByteArray plain;
|
|
|
|
if (!deriveKey(password.toUtf8(), salt, key, error))
|
|
return false;
|
|
|
|
if (!aesCrypt(cipher, key, iv, plain, false, error))
|
|
return false;
|
|
|
|
if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
|
|
if (error)
|
|
*error = QStringLiteral("Cannot open file for write: %1").arg(f.errorString());
|
|
return false;
|
|
}
|
|
qint64 written = f.write(plain);
|
|
f.close();
|
|
if (written != plain.size()) {
|
|
if (error)
|
|
*error = QStringLiteral("Write failed or incomplete");
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void SecureQSettings::clearSettings()
|
|
{
|
|
QMutexLocker locker(&mutex);
|
|
m_settings.clear();
|
|
m_cache.clear();
|
|
sync();
|
|
}
|