diff --git a/client/protection.cpp b/client/protection.cpp deleted file mode 100644 index b3f8db141..000000000 --- a/client/protection.cpp +++ /dev/null @@ -1,137 +0,0 @@ -#include "protection.h" -#include -#include -#include -#include -#include -#include - -static constexpr int KEY_SIZE = 32; -static constexpr int SALT_SIZE = 16; -static constexpr int IV_SIZE = 16; -static constexpr int ITERATIONS = 100000; - -Protector::Protector(QObject *parent) : QObject(parent) -{ -} - -QByteArray Protector::generateSalt() -{ - QByteArray salt(SALT_SIZE, 0); - RAND_bytes(reinterpret_cast(salt.data()), SALT_SIZE); - return salt; -} - -QByteArray Protector::deriveKey(const QString &password, const QByteArray &salt) -{ - QByteArray key(KEY_SIZE, 0); - if (!PKCS5_PBKDF2_HMAC(password.toUtf8().constData(), password.size(), - reinterpret_cast(salt.constData()), salt.size(), ITERATIONS, - EVP_sha256(), KEY_SIZE, reinterpret_cast(key.data()))) { - throw std::runtime_error("PBKDF2 key derivation failed"); - } - return key; -} - -void Protector::encryptFile(const QString &filePath, const QString &password) -{ - try { - QFile file(filePath); - if (!file.open(QIODevice::ReadOnly)) - throw std::runtime_error("Cannot open file for reading"); - - QByteArray plain = file.readAll(); - file.close(); - - QByteArray salt = generateSalt(); - QByteArray iv(IV_SIZE, 0); - RAND_bytes(reinterpret_cast(iv.data()), IV_SIZE); - QByteArray key = deriveKey(password, salt); - - EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new(); - QByteArray encrypted(plain.size() + EVP_MAX_BLOCK_LENGTH, 0); - int len = 0, total = 0; - - if (!EVP_EncryptInit_ex(ctx, EVP_aes_256_cbc(), nullptr, reinterpret_cast(key.constData()), - reinterpret_cast(iv.constData()))) - throw std::runtime_error("EncryptInit failed"); - - if (!EVP_EncryptUpdate(ctx, reinterpret_cast(encrypted.data()), &len, - reinterpret_cast(plain.constData()), plain.size())) - throw std::runtime_error("EncryptUpdate failed"); - - total = len; - - if (!EVP_EncryptFinal_ex(ctx, reinterpret_cast(encrypted.data()) + len, &len)) - throw std::runtime_error("EncryptFinal failed"); - - total += len; - EVP_CIPHER_CTX_free(ctx); - encrypted.truncate(total); - - QByteArray finalData = salt + iv + encrypted; - - if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) - throw std::runtime_error("Cannot open file for writing"); - - file.write(finalData); - file.close(); - - qInfo() << "File encrypted successfully:" << filePath; - } catch (const std::exception &e) { - qWarning() << "Encryption failed:" << e.what(); - } -} - -void Protector::decryptFile(const QString &filePath, const QString &password) -{ - try { - QFile file(filePath); - if (!file.open(QIODevice::ReadOnly)) - throw std::runtime_error("Cannot open file for reading"); - - QByteArray data = file.readAll(); - file.close(); - - if (data.size() < SALT_SIZE + IV_SIZE) - throw std::runtime_error("Corrupted or invalid encrypted file"); - - QByteArray salt = data.left(SALT_SIZE); - QByteArray iv = data.mid(SALT_SIZE, IV_SIZE); - QByteArray cipher = data.mid(SALT_SIZE + IV_SIZE); - - QByteArray key = deriveKey(password, salt); - EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new(); - QByteArray plain(cipher.size(), 0); - int len = 0, total = 0; - - if (!EVP_DecryptInit_ex(ctx, EVP_aes_256_cbc(), nullptr, reinterpret_cast(key.constData()), - reinterpret_cast(iv.constData()))) - throw std::runtime_error("DecryptInit failed"); - - if (!EVP_DecryptUpdate(ctx, reinterpret_cast(plain.data()), &len, - reinterpret_cast(cipher.constData()), cipher.size())) - throw std::runtime_error("DecryptUpdate failed"); - - total = len; - - if (!EVP_DecryptFinal_ex(ctx, reinterpret_cast(plain.data()) + len, &len)) { - EVP_CIPHER_CTX_free(ctx); - throw std::runtime_error("Incorrect password or corrupted file"); - } - - total += len; - EVP_CIPHER_CTX_free(ctx); - plain.truncate(total); - - if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) - throw std::runtime_error("Cannot open file for writing"); - - file.write(plain); - file.close(); - - qInfo() << "File decrypted successfully:" << filePath; - } catch (const std::exception &e) { - qWarning() << "Decryption failed:" << e.what(); - } -} diff --git a/client/protection.h b/client/protection.h deleted file mode 100644 index 679995da9..000000000 --- a/client/protection.h +++ /dev/null @@ -1,22 +0,0 @@ -#ifndef PROTECTOR_H -#define PROTECTOR_H - -#include -#include -#include - -class Protector : public QObject -{ - Q_OBJECT -public: - explicit Protector(QObject *parent = nullptr); - - Q_INVOKABLE static void encryptFile(const QString &filePath, const QString &password); - Q_INVOKABLE static void decryptFile(const QString &filePath, const QString &password); - -private: - static QByteArray deriveKey(const QString &password, const QByteArray &salt); - static QByteArray generateSalt(); -}; - -#endif // PROTECTOR_H diff --git a/client/secure_qsettings.cpp b/client/secure_qsettings.cpp index 8df24e742..918964a62 100644 --- a/client/secure_qsettings.cpp +++ b/client/secure_qsettings.cpp @@ -12,6 +12,11 @@ #include #include #include +#include + +#include +#include +#include using namespace QKeychain; @@ -19,6 +24,11 @@ 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) @@ -295,6 +305,202 @@ void SecureQSettings::setSecTag(const QString &tag, const QByteArray &data) } } +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(password.constData()); + const unsigned char *s = reinterpret_cast(salt.constData()); + int ok = PKCS5_PBKDF2_HMAC(reinterpret_cast(pw), password.size(), s, salt.size(), PBKDF2_ITER, + EVP_sha256(), KEY_LEN, reinterpret_cast(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(key.constData()), + reinterpret_cast(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(out.data()), &outlen1, + reinterpret_cast(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(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(salt.data()), SALT_LEN) + || 1 != RAND_bytes(reinterpret_cast(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); diff --git a/client/secure_qsettings.h b/client/secure_qsettings.h index 8878e1d56..017c4447e 100644 --- a/client/secure_qsettings.h +++ b/client/secure_qsettings.h @@ -16,6 +16,9 @@ public: explicit SecureQSettings(const QString &organization, const QString &application = QString(), QObject *parent = nullptr); + Q_INVOKABLE bool encryptFile(const QString &filePath, const QString &password, QString *error = nullptr) const; + Q_INVOKABLE bool decryptFile(const QString &filePath, const QString &password, QString *error = nullptr) const; + Q_INVOKABLE QVariant value(const QString &key, const QVariant &defaultValue = QVariant()) const; Q_INVOKABLE void setValue(const QString &key, const QVariant &value); void remove(const QString &key); @@ -35,6 +38,12 @@ public: static QByteArray getSecTag(const QString &tag); static void setSecTag(const QString &tag, const QByteArray &data); + void setPassword(const QString &pwd); + void setHint(const QString &hint); + + QString getPassword() const; + QString getHint() const; + void clearSettings(); private: @@ -48,6 +57,9 @@ private: "Conf/", "Servers/", }; + mutable QString m_password; + mutable QString m_hint; + mutable QByteArray m_key; mutable QByteArray m_iv; diff --git a/client/settings.h b/client/settings.h index 249506277..fddf1183b 100644 --- a/client/settings.h +++ b/client/settings.h @@ -94,6 +94,33 @@ public: setValue("Conf/startMinimized", enabled); } + bool isFileEncryption() const + { + return value("Sec/fileEncryption", false).toBool(); + } + void setFileEncryption(bool enabled) + { + setValue("Sec/fileEncryption", enabled); + } + + QString getPassword() const + { + return m_settings.getPassword(); + } + void setPassword(const QString &pwd) + { + m_settings.setPassword(pwd); + } + + QString getHint() const + { + return m_settings.getHint(); + } + void setHint(const QString &hint) + { + m_settings.setHint(hint); + } + bool isSaveLogs() const { return value("Conf/saveLogs", false).toBool(); diff --git a/client/ui/controllers/settingsController.cpp b/client/ui/controllers/settingsController.cpp index d1bc84b94..55a2b3844 100644 --- a/client/ui/controllers/settingsController.cpp +++ b/client/ui/controllers/settingsController.cpp @@ -296,6 +296,36 @@ void SettingsController::toggleStartMinimized(bool enable) emit startMinimizedChanged(); } +bool SettingsController::isFileEncryptionEnabled() +{ + return m_settings->isFileEncryption(); +} + +void SettingsController::toggleFileEncryption(bool enable) +{ + m_settings->setFileEncryption(enable); +} + +void SettingsController::setPassword(QString pwd) +{ + m_settings->setPassword(pwd); +} + +QString SettingsController::getPassword() +{ + return m_settings->getPassword(); +} + +void SettingsController::setHint(QString hint) +{ + m_settings->setHint(hint); +} + +QString SettingsController::getHint() +{ + return m_settings->getHint(); +} + bool SettingsController::isScreenshotsEnabled() { return m_settings->isScreenshotsEnabled(); diff --git a/client/ui/controllers/settingsController.h b/client/ui/controllers/settingsController.h index a5c65642b..95549cfa7 100644 --- a/client/ui/controllers/settingsController.h +++ b/client/ui/controllers/settingsController.h @@ -70,6 +70,14 @@ public slots: bool isStartMinimizedEnabled(); void toggleStartMinimized(bool enable); + bool isFileEncryptionEnabled(); + void toggleFileEncryption(bool enable); + + void setPassword(QString pwd); + QString getPassword(); + void setHint(QString hint); + QString getHint(); + bool isScreenshotsEnabled(); void toggleScreenshotsEnabled(bool enable);