From fb9844cab857c6d8db12561627e5b3bcd20343e2 Mon Sep 17 00:00:00 2001 From: MrMirDan Date: Wed, 22 Oct 2025 12:35:14 +0300 Subject: [PATCH 01/24] feat: file password protection v1 --- client/protection.cpp | 147 ++++++++++++++++++++++++++++++++++++++++++ client/protection.h | 23 +++++++ 2 files changed, 170 insertions(+) create mode 100644 client/protection.cpp create mode 100644 client/protection.h diff --git a/client/protection.cpp b/client/protection.cpp new file mode 100644 index 000000000..5fd3324c9 --- /dev/null +++ b/client/protection.cpp @@ -0,0 +1,147 @@ +#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(); + } +} + +void Protector::resetPassword(const QString &filePath, const QString &password) +{ + try { + decryptFile(filePath, password); + qInfo() << "Password removed from file:" << filePath; + } catch (const std::exception &e) { + qWarning() << "Reset failed:" << e.what(); + } +} diff --git a/client/protection.h b/client/protection.h new file mode 100644 index 000000000..751ecbdb4 --- /dev/null +++ b/client/protection.h @@ -0,0 +1,23 @@ +#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); + Q_INVOKABLE static void resetPassword(const QString &filePath, const QString &password); + +private: + static QByteArray deriveKey(const QString &password, const QByteArray &salt); + static QByteArray generateSalt(); +}; + +#endif // PROTECTOR_H From a9ab28122194d2bdab8d0285e7145bfaa5763ab9 Mon Sep 17 00:00:00 2001 From: MrMirDan Date: Wed, 22 Oct 2025 13:17:22 +0300 Subject: [PATCH 02/24] removed unnecessary method --- client/protection.cpp | 10 ---------- client/protection.h | 1 - 2 files changed, 11 deletions(-) diff --git a/client/protection.cpp b/client/protection.cpp index 5fd3324c9..b3f8db141 100644 --- a/client/protection.cpp +++ b/client/protection.cpp @@ -135,13 +135,3 @@ void Protector::decryptFile(const QString &filePath, const QString &password) qWarning() << "Decryption failed:" << e.what(); } } - -void Protector::resetPassword(const QString &filePath, const QString &password) -{ - try { - decryptFile(filePath, password); - qInfo() << "Password removed from file:" << filePath; - } catch (const std::exception &e) { - qWarning() << "Reset failed:" << e.what(); - } -} diff --git a/client/protection.h b/client/protection.h index 751ecbdb4..679995da9 100644 --- a/client/protection.h +++ b/client/protection.h @@ -13,7 +13,6 @@ public: Q_INVOKABLE static void encryptFile(const QString &filePath, const QString &password); Q_INVOKABLE static void decryptFile(const QString &filePath, const QString &password); - Q_INVOKABLE static void resetPassword(const QString &filePath, const QString &password); private: static QByteArray deriveKey(const QString &password, const QByteArray &salt); From a868702be0b45487d05f5436901b15473b8d0e6e Mon Sep 17 00:00:00 2001 From: MrMirDan Date: Tue, 9 Dec 2025 13:50:55 +0200 Subject: [PATCH 03/24] update: changed logic, added to settingsController --- client/protection.cpp | 137 ------------ client/protection.h | 22 -- client/secure_qsettings.cpp | 206 +++++++++++++++++++ client/secure_qsettings.h | 12 ++ client/settings.h | 27 +++ client/ui/controllers/settingsController.cpp | 30 +++ client/ui/controllers/settingsController.h | 8 + 7 files changed, 283 insertions(+), 159 deletions(-) delete mode 100644 client/protection.cpp delete mode 100644 client/protection.h 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); From 9ed920b71536386f62ffcd469aeb4a3aa7091ba3 Mon Sep 17 00:00:00 2001 From: MrMirDan Date: Thu, 11 Dec 2025 14:20:13 +0200 Subject: [PATCH 04/24] update: changed password and hint saving --- client/ui/controllers/pageController.h | 2 ++ client/ui/controllers/settingsController.cpp | 1 + client/ui/controllers/settingsController.h | 2 ++ 3 files changed, 5 insertions(+) diff --git a/client/ui/controllers/pageController.h b/client/ui/controllers/pageController.h index 529106343..83948f500 100644 --- a/client/ui/controllers/pageController.h +++ b/client/ui/controllers/pageController.h @@ -34,6 +34,8 @@ namespace PageLoader PageSettingsSplitTunneling, PageSettingsAppSplitTunneling, PageSettingsKillSwitch, + PageSettingsAppPassword, + PageSettingsAppPasswordConfirm, PageSettingsApiServerInfo, PageSettingsApiAvailableCountries, PageSettingsApiSupport, diff --git a/client/ui/controllers/settingsController.cpp b/client/ui/controllers/settingsController.cpp index 55a2b3844..2409fe61f 100644 --- a/client/ui/controllers/settingsController.cpp +++ b/client/ui/controllers/settingsController.cpp @@ -304,6 +304,7 @@ bool SettingsController::isFileEncryptionEnabled() void SettingsController::toggleFileEncryption(bool enable) { m_settings->setFileEncryption(enable); + emit fileEncryptionStateChanged(); } void SettingsController::setPassword(QString pwd) diff --git a/client/ui/controllers/settingsController.h b/client/ui/controllers/settingsController.h index 95549cfa7..2452cea7c 100644 --- a/client/ui/controllers/settingsController.h +++ b/client/ui/controllers/settingsController.h @@ -136,6 +136,8 @@ signals: void isHomeAdLabelVisibleChanged(bool visible); void startMinimizedChanged(); + void fileEncryptionStateChanged(); + private: QSharedPointer m_serversModel; QSharedPointer m_containersModel; From ad5d60f915e8f767363713e7fd1cd57a50e4102b Mon Sep 17 00:00:00 2001 From: MrMirDan Date: Fri, 12 Dec 2025 16:32:16 +0200 Subject: [PATCH 05/24] update: ui to set/change password and hint --- client/images/controls/eye-new.svg | 4 + client/images/controls/eye-off-new.svg | 6 + client/images/controls/lock-locked.svg | 3 + client/images/controls/lock-unlocked.svg | 3 + client/resources.qrc | 11 +- client/secure_qsettings.cpp | 20 -- client/secure_qsettings.h | 3 - client/settings.h | 8 +- client/ui/controllers/pageController.h | 1 + client/ui/controllers/settingsController.h | 1 + .../ui/qml/Components/EncryptionIndicator.qml | 66 ++++++ client/ui/qml/Components/PasswordDrawer.qml | 89 +++++++++ .../qml/Pages2/PageSettingsAppEncryption.qml | 148 ++++++++++++++ .../ui/qml/Pages2/PageSettingsAppPassword.qml | 189 ++++++++++++++++++ .../Pages2/PageSettingsAppPasswordConfirm.qml | 148 ++++++++++++++ .../ui/qml/Pages2/PageSettingsApplication.qml | 17 ++ 16 files changed, 689 insertions(+), 28 deletions(-) create mode 100644 client/images/controls/eye-new.svg create mode 100644 client/images/controls/eye-off-new.svg create mode 100644 client/images/controls/lock-locked.svg create mode 100644 client/images/controls/lock-unlocked.svg create mode 100644 client/ui/qml/Components/EncryptionIndicator.qml create mode 100644 client/ui/qml/Components/PasswordDrawer.qml create mode 100644 client/ui/qml/Pages2/PageSettingsAppEncryption.qml create mode 100644 client/ui/qml/Pages2/PageSettingsAppPassword.qml create mode 100644 client/ui/qml/Pages2/PageSettingsAppPasswordConfirm.qml diff --git a/client/images/controls/eye-new.svg b/client/images/controls/eye-new.svg new file mode 100644 index 000000000..84811e489 --- /dev/null +++ b/client/images/controls/eye-new.svg @@ -0,0 +1,4 @@ + + + + diff --git a/client/images/controls/eye-off-new.svg b/client/images/controls/eye-off-new.svg new file mode 100644 index 000000000..d9310af7e --- /dev/null +++ b/client/images/controls/eye-off-new.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/client/images/controls/lock-locked.svg b/client/images/controls/lock-locked.svg new file mode 100644 index 000000000..99d25f7a7 --- /dev/null +++ b/client/images/controls/lock-locked.svg @@ -0,0 +1,3 @@ + + + diff --git a/client/images/controls/lock-unlocked.svg b/client/images/controls/lock-unlocked.svg new file mode 100644 index 000000000..739624cd3 --- /dev/null +++ b/client/images/controls/lock-unlocked.svg @@ -0,0 +1,3 @@ + + + diff --git a/client/resources.qrc b/client/resources.qrc index a293d4c69..d5e6fc3bb 100644 --- a/client/resources.qrc +++ b/client/resources.qrc @@ -19,6 +19,8 @@ images/controls/delete.svg images/controls/download.svg images/controls/edit-3.svg + images/controls/eye-off-new.svg + images/controls/eye-new.svg images/controls/eye-off.svg images/controls/eye.svg images/controls/external-link.svg @@ -32,6 +34,8 @@ images/controls/history.svg images/controls/home.svg images/controls/info.svg + images/controls/lock-locked.svg + images/controls/lock-unlocked.svg images/controls/mail.svg images/controls/map-pin.svg images/controls/more-vertical.svg @@ -127,11 +131,13 @@ ui/qml/Components/HomeContainersListView.qml ui/qml/Components/HomeSplitTunnelingDrawer.qml ui/qml/Components/InstalledAppsDrawer.qml + ui/qml/Components/PasswordDrawer.qml ui/qml/Components/QuestionDrawer.qml ui/qml/Components/SelectLanguageDrawer.qml ui/qml/Components/ServersListView.qml ui/qml/Components/SettingsContainersListView.qml - + ui/qml/Components/EncryptionIndicator.qml + ui/qml/Components/TransportProtoSelector.qml ui/qml/Components/AddSitePanel.qml ui/qml/Config/GlobalConfig.qml @@ -203,6 +209,9 @@ ui/qml/Pages2/PageSettingsApiServerInfo.qml ui/qml/Pages2/PageSettingsApplication.qml ui/qml/Pages2/PageSettingsAppSplitTunneling.qml + ui/qml/Pages2/PageSettingsAppEncryption.qml + ui/qml/Pages2/PageSettingsAppPassword.qml + ui/qml/Pages2/PageSettingsAppPasswordConfirm.qml ui/qml/Pages2/PageSettingsBackup.qml ui/qml/Pages2/PageSettingsConnection.qml ui/qml/Pages2/PageSettingsDns.qml diff --git a/client/secure_qsettings.cpp b/client/secure_qsettings.cpp index 918964a62..b5f838f3c 100644 --- a/client/secure_qsettings.cpp +++ b/client/secure_qsettings.cpp @@ -305,26 +305,6 @@ 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(); diff --git a/client/secure_qsettings.h b/client/secure_qsettings.h index 017c4447e..ca59b959a 100644 --- a/client/secure_qsettings.h +++ b/client/secure_qsettings.h @@ -57,9 +57,6 @@ 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 fddf1183b..ba21ff541 100644 --- a/client/settings.h +++ b/client/settings.h @@ -105,20 +105,20 @@ public: QString getPassword() const { - return m_settings.getPassword(); + return value("Sec/password", "").toString(); } void setPassword(const QString &pwd) { - m_settings.setPassword(pwd); + setValue("Sec/password", pwd); } QString getHint() const { - return m_settings.getHint(); + return value("Sec/hint", "").toString(); } void setHint(const QString &hint) { - m_settings.setHint(hint); + setValue("Sec/hint", hint); } bool isSaveLogs() const diff --git a/client/ui/controllers/pageController.h b/client/ui/controllers/pageController.h index 83948f500..c3e4f8dc4 100644 --- a/client/ui/controllers/pageController.h +++ b/client/ui/controllers/pageController.h @@ -34,6 +34,7 @@ namespace PageLoader PageSettingsSplitTunneling, PageSettingsAppSplitTunneling, PageSettingsKillSwitch, + PageSettingsAppEncryption, PageSettingsAppPassword, PageSettingsAppPasswordConfirm, PageSettingsApiServerInfo, diff --git a/client/ui/controllers/settingsController.h b/client/ui/controllers/settingsController.h index 2452cea7c..62ea98a63 100644 --- a/client/ui/controllers/settingsController.h +++ b/client/ui/controllers/settingsController.h @@ -137,6 +137,7 @@ signals: void startMinimizedChanged(); void fileEncryptionStateChanged(); + void changingPassword(); private: QSharedPointer m_serversModel; diff --git a/client/ui/qml/Components/EncryptionIndicator.qml b/client/ui/qml/Components/EncryptionIndicator.qml new file mode 100644 index 000000000..ca4c29a72 --- /dev/null +++ b/client/ui/qml/Components/EncryptionIndicator.qml @@ -0,0 +1,66 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Qt5Compat.GraphicalEffects + +import Style 1.0 + +import "../Controls2" +import "../Controls2/TextTypes" + +import "../Config" + +Rectangle { + id: root + + property string textColor: AmneziaStyle.color.paleGray + + property string textString + property int textFormat: Text.PlainText + + property string iconPath + property real iconWidth: 16 + property real iconHeight: 16 + + color: AmneziaStyle.color.onyxBlack + radius: 32 + implicitHeight: iconHeight + 8 + + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + + RowLayout { + id: content + width: parent.width + anchors.fill: parent + + anchors.leftMargin: content.width / 4 + anchors.rightMargin: content.width / 4 + anchors.topMargin: 4 + anchors.bottomMargin: 4 + + spacing: 0 + + Image { + Layout.alignment: Qt.AlignTop + + width: iconWidth + height: iconHeight + + source: iconPath + } + + CaptionTextType { + id: supportingText + + Layout.fillWidth: true + Layout.leftMargin: 8 + + text: textString + textFormat: root.textFormat + color: textColor + } + } +} \ No newline at end of file diff --git a/client/ui/qml/Components/PasswordDrawer.qml b/client/ui/qml/Components/PasswordDrawer.qml new file mode 100644 index 000000000..652a8d525 --- /dev/null +++ b/client/ui/qml/Components/PasswordDrawer.qml @@ -0,0 +1,89 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import Style 1.0 + +import "../Controls2" +import "../Controls2/TextTypes" + +import "../Config" + +DrawerType2 { + id: root + objectName: "passwordDrawer" + + property var securedFunc + + expandedStateContent: ColumnLayout { + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 16 + anchors.leftMargin: 16 + anchors.rightMargin: 16 + + Header2TextType { + Layout.fillWidth: true + Layout.bottomMargin: 8 + + text: qsTr("Enter password to continue") + } + + TextFieldWithHeaderType { + id: passwordField + + property bool hideContent: true + + Layout.fillWidth: true + + headerText: qsTr("Password") + textField.echoMode: hideContent ? TextInput.Password : TextInput.Normal + textField.text: textField.text + + rightButtonClickedOnEnter: true + + clickedFunc: function () { + hideContent = !hideContent + buttonImageSource = textField.text !== "" ? (hideContent ? "qrc:/images/controls/eye.svg" : "qrc:/images/controls/eye-off.svg") : "" + } + + textField.onFocusChanged: { + textField.text = textField.text.replace(/^\s+|\s+$/g, '') + } + + textField.onTextChanged: { + buttonImageSource = textField.text !== "" ? (hideContent ? "qrc:/images/controls/eye.svg" : "qrc:/images/controls/eye-off.svg") : "" + } + } + + LabelTextType { + Layout.fillWidth: true + Layout.topMargin: 8 + Layout.bottomMargin: 16 + + text: SettingsController.getHint() + } + + BasicButtonType { + id: doneButton + + Layout.fillWidth: true + + text: qsTr("Done") + + clickedFunc: function() { + if (passwordField.textField.text !== SettingsController.getPassword()) { + passwordField.errorText = qsTr("Incorrect password") + return + } + + if (root.securedFunc && typeof root.securedFunc === "function") { + root.securedFunc() + } + + root.closeTriggered() + } + } + } +} diff --git a/client/ui/qml/Pages2/PageSettingsAppEncryption.qml b/client/ui/qml/Pages2/PageSettingsAppEncryption.qml new file mode 100644 index 000000000..8c5900b8c --- /dev/null +++ b/client/ui/qml/Pages2/PageSettingsAppEncryption.qml @@ -0,0 +1,148 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import PageEnum 1.0 +import Style 1.0 + +import "./" +import "../Controls2" +import "../Config" +import "../Controls2/TextTypes" +import "../Components" + +PageType { + id: root + + property bool isChangingPassword: false + + Connections { + target: SettingsController + + function onFileEncryptionStateChanged() { + PageController.showBusyIndicator(true) + PageController.closePage() + PageController.goToPage(PageEnum.PageSettingsAppEncryption) + PageController.showBusyIndicator(false) + } + } + + BackButtonType { + id: backButton + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 20 + + onFocusChanged: { + if (this.activeFocus) { + listView.positionViewAtBeginning() + } + } + + backButtonFunction: function() { + PageController.closePage() + if (root.isChangingPassword) { + root.isChangingPassword = false + } + } + } + + ListViewType { + id: listView + + anchors.top: backButton.bottom + anchors.bottom: parent.bottom + anchors.right: parent.right + anchors.left: parent.left + + header: ColumnLayout { + width: listView.width + + BaseHeaderType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + + headerText: qsTr("File encryption") + descriptionText: qsTr("For encrypting backups, configuration files, subscription keys, and logs") + } + + EncryptionIndicator { + id: indicator + + textString: SettingsController.isFileEncryptionEnabled() ? qsTr("Password set. Encryption on") : qsTr("Password not set. Encryption off") + iconPath: SettingsController.isFileEncryptionEnabled() ? "qrc:/images/controls/lock-locked.svg" : "qrc:/images/controls/lock-unlocked.svg" + } + + + BasicButtonType { + id: switchEncryptionButton + + Layout.fillWidth: true + Layout.topMargin: 16 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + + text: SettingsController.isFileEncryptionEnabled() ? qsTr("Turn off encryption") : qsTr("Turn on encryption") + + clickedFunc: function() { + SettingsController.isFileEncryptionEnabled() ? SettingsController.toggleFileEncryption(false) + : SettingsController.toggleFileEncryption(true) + } + } + + BasicButtonType { + id: changePasswordButton + + hoveredColor: AmneziaStyle.color.slateGray + defaultColor: AmneziaStyle.color.midnightBlack + textColor: AmneziaStyle.color.paleGray + borderWidth: 1 + + Layout.fillWidth: true + Layout.topMargin: 8 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + + text: qsTr("Change password") + + signal changingPassword + + clickedFunc: function() { + passwordDrawer.openTriggered() + } + } + + PasswordDrawer { + id: passwordDrawer + + parent: root + + anchors.fill: parent + expandedHeight: root.height * 0.45 + + securedFunc: function() { + root.isChangingPassword = true + + PageController.showBusyIndicator(true) + PageController.closePage() + PageController.goToPage(PageEnum.PageSettingsAppPassword) + PageController.showBusyIndicator(false) + + SettingsController.changingPassword() + } + } + } + + spacing: 16 + + footer: ColumnLayout { + width: listView.width + + // TODO: add text + } + } +} \ No newline at end of file diff --git a/client/ui/qml/Pages2/PageSettingsAppPassword.qml b/client/ui/qml/Pages2/PageSettingsAppPassword.qml new file mode 100644 index 000000000..bac19b956 --- /dev/null +++ b/client/ui/qml/Pages2/PageSettingsAppPassword.qml @@ -0,0 +1,189 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import PageEnum 1.0 +import Style 1.0 + +import "./" +import "../Controls2" +import "../Config" +import "../Controls2/TextTypes" +import "../Components" + +PageType { + id: root + + property bool isChangingPassword: false + + Connections { + target: SettingsController + + function onChangingPassword() { + root.isChangingPassword = true + } + } + + BackButtonType { + id: backButton + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 20 + + onFocusChanged: { + if (this.activeFocus) { + listView.positionViewAtBeginning() + } + } + + backButtonFunction: function() { + PageController.closePage() + if (root.isChangingPassword) { + root.isChangingPassword = false + } + } + } + + ListViewType { + id: listView + + anchors.top: backButton.bottom + anchors.bottom: parent.bottom + anchors.right: parent.right + anchors.left: parent.left + + header: ColumnLayout { + width: listView.width + + BaseHeaderType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 16 + + headerText: root.isChangingPassword ? qsTr("Password changing") : qsTr("File encryption") + descriptionText: root.isChangingPassword ? qsTr("Files encrypted with old password will stay encrypted with old password") + : qsTr("For encrypting backups, configuration files, subscription keys, and logs") + } + } + + model: inputFields + spacing: 16 + + delegate: ColumnLayout { + width: listView.width + + TextFieldWithHeaderType { + id: delegate + + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + + headerText: title + textField.echoMode: hideContent ? TextInput.Password : TextInput.Normal + textField.placeholderText: placeholderContent + textField.text: textField.text + + rightButtonClickedOnEnter: true + + clickedFunc: function () { + clickedHandler() + buttonImageSource = textField.text !== "" ? imageSource : "" + } + + textField.onFocusChanged: { + textField.text = textField.text.replace(/^\s+|\s+$/g, '') + } + + textField.onTextChanged: { + buttonImageSource = textField.text !== "" ? imageSource : "" + } + } + } + + footer: ColumnLayout { + width: listView.width + + BasicButtonType { + id: continueButton + + Layout.fillWidth: true + Layout.topMargin: 32 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + + text: qsTr("Continue") + + clickedFunc: function() { + if (!root.isPasswordProperlyFilled()) { + return + } + + var _password = listView.itemAtIndex(vars.passwordIndex).children[0].textField.text + var _hint = listView.itemAtIndex(vars.hintIndex).children[0].textField.text + + SettingsController.setPassword(_password) + SettingsController.setHint(_hint) + + PageController.goToPage(PageEnum.PageSettingsAppPasswordConfirm) + if (root.isChangingPassword) { + SettingsController.changingPassword() + } + } + } + } + } + + function isPasswordProperlyFilled() { + var tooShort = false + + var secretDataItem = listView.itemAtIndex(vars.passwordIndex).children[0] + if (secretDataItem.textField.text === "") { + secretDataItem.errorText = qsTr("Password cannot be empty") + tooShort = true + } else if (secretDataItem.textField.text.length < 4) { + secretDataItem.errorText = qsTr("Password too short") + tooShort = true + } + + return !tooShort + } + + property list inputFields: [ + passwordObject, + hintObject + ] + + QtObject { + id: passwordObject + + property string title: root.isChangingPassword ? qsTr("New password") : qsTr("Set encryption password") + readonly property string placeholderContent: "" + property string imageSource: "qrc:/images/controls/eye-new.svg" + property bool hideContent: true + readonly property var clickedHandler: function() { + hideContent = !hideContent + imageSource = hideContent ? "qrc:/images/controls/eye-new.svg" : "qrc:/images/controls/eye-off-new.svg" + } + } + + QtObject { + id: hintObject + + property string title: qsTr("Hint for password") + readonly property string placeholderContent: "" + property string imageSource: "" + property bool hideContent: false + readonly property var clickedHandler: undefined + } + + QtObject { + id: vars + + readonly property int passwordIndex: 0 + readonly property int hintIndex: 1 + } +} \ No newline at end of file diff --git a/client/ui/qml/Pages2/PageSettingsAppPasswordConfirm.qml b/client/ui/qml/Pages2/PageSettingsAppPasswordConfirm.qml new file mode 100644 index 000000000..2fc1768a2 --- /dev/null +++ b/client/ui/qml/Pages2/PageSettingsAppPasswordConfirm.qml @@ -0,0 +1,148 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +import PageEnum 1.0 +import Style 1.0 + +import "./" +import "../Controls2" +import "../Config" +import "../Controls2/TextTypes" + +PageType { + id: root + + property bool isChangingPassword: false + + Connections { + target: SettingsController + + function onChangingPassword() { + root.isChangingPassword = true + } + } + + BackButtonType { + id: backButton + + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.topMargin: 20 + + onFocusChanged: { + if (this.activeFocus) { + listView.positionViewAtBeginning() + } + } + } + + ListViewType { + id: listView + + anchors.top: backButton.bottom + anchors.bottom: parent.bottom + anchors.right: parent.right + anchors.left: parent.left + + header: ColumnLayout { + width: listView.width + + BaseHeaderType { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + + headerText: root.isChangingPassword ? qsTr("Confirm new password") : qsTr("Confirm password") + descriptionText: qsTr("If you forget your password, you'll have to reset all app settings to reset it." + + " Encrypted files will remain encrypted") + } + } + + model: 1 // fake model + spacing: 16 + + delegate: ColumnLayout { + width: listView.width + + TextFieldWithHeaderType { + id: delegate + + property bool hideContent: true + + Layout.fillWidth: true + Layout.topMargin: 16 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + + headerText: root.isChangingPassword ? qsTr("Enter new password one more time") : qsTr("Enter password one more time") + textField.echoMode: hideContent ? TextInput.Password : TextInput.Normal + textField.placeholderText: "" + textField.text: textField.text + + rightButtonClickedOnEnter: true + + clickedFunc: function () { + hideContent = !hideContent + buttonImageSource = textField.text !== "" ? (hideContent ? "qrc:/images/controls/eye.svg" : "qrc:/images/controls/eye-off.svg") : "" + } + + textField.onFocusChanged: { + textField.text = textField.text.replace(/^\s+|\s+$/g, '') + } + + textField.onTextChanged: { + buttonImageSource = textField.text !== "" ? (hideContent ? "qrc:/images/controls/eye.svg" : "qrc:/images/controls/eye-off.svg") : "" + } + } + } + + footer: ColumnLayout { + width: listView.width + + LabelTextType { + Layout.fillWidth: true + Layout.topMargin: 16 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.bottomMargin: 24 + + text: SettingsController.getHint() + } + + BasicButtonType { + id: continueButton + + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + + text: root.isChangingPassword ? qsTr("Save new password") : qsTr("Turn on encryption") + + clickedFunc: function() { + if (!root.isPasswordProperlyFilled()) { + return + } + + PageController.closePage() + PageController.goToPage(PageEnum.PageSettings) + PageController.goToPage(PageEnum.PageSettingsAppEncryption) + SettingsController.toggleFileEncryption(true) + } + } + } + } + + function isPasswordProperlyFilled() { + var notMatch = false + + var secretDataItem = listView.itemAtIndex(0).children[0] + if (secretDataItem.textField.text !== SettingsController.getPassword()) { + secretDataItem.errorText = qsTr("Passwords not match") + notMatch = true + } + + return !notMatch + } +} \ No newline at end of file diff --git a/client/ui/qml/Pages2/PageSettingsApplication.qml b/client/ui/qml/Pages2/PageSettingsApplication.qml index 6cb48ac9d..206ce3ada 100644 --- a/client/ui/qml/Pages2/PageSettingsApplication.qml +++ b/client/ui/qml/Pages2/PageSettingsApplication.qml @@ -190,6 +190,23 @@ PageType { DividerType {} + LabelWithButtonType { + id: labelWithButtonAppPassword + + Layout.fillWidth: true + + text: qsTr("File encryption") + descriptionText: qsTr("For encrypting backups, configuration files, subscription keys, and logs") + rightImageSource: "qrc:/images/controls/chevron-right.svg" + + clickedFunction: function() { + SettingsController.getPassword() === "" ? PageController.goToPage(PageEnum.PageSettingsAppPassword) + : PageController.goToPage(PageEnum.PageSettingsAppEncryption) + } + } + + DividerType {} + LabelWithButtonType { id: labelWithButtonLogging From 6ebb942466f4aa1ec5b2b5a3ae0c47866249905a Mon Sep 17 00:00:00 2001 From: MrMirDan Date: Thu, 18 Dec 2025 12:25:55 +0200 Subject: [PATCH 06/24] update: encryption/decryption logic, backup encryption on save and decryption on restore --- client/images/controls/eye-new.svg | 4 - client/images/controls/eye-off-new.svg | 6 - client/resources.qrc | 2 - client/secure_qsettings.cpp | 186 ------------- client/secure_qsettings.h | 9 - client/ui/controllers/settingsController.cpp | 6 + client/ui/controllers/systemController.cpp | 254 ++++++++++++++++++ client/ui/controllers/systemController.h | 7 + client/ui/qml/Components/PasswordDrawer.qml | 27 +- .../qml/Pages2/PageSettingsAppEncryption.qml | 2 + .../ui/qml/Pages2/PageSettingsAppPassword.qml | 4 +- client/ui/qml/Pages2/PageSettingsBackup.qml | 16 +- 12 files changed, 308 insertions(+), 215 deletions(-) delete mode 100644 client/images/controls/eye-new.svg delete mode 100644 client/images/controls/eye-off-new.svg diff --git a/client/images/controls/eye-new.svg b/client/images/controls/eye-new.svg deleted file mode 100644 index 84811e489..000000000 --- a/client/images/controls/eye-new.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/client/images/controls/eye-off-new.svg b/client/images/controls/eye-off-new.svg deleted file mode 100644 index d9310af7e..000000000 --- a/client/images/controls/eye-off-new.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/client/resources.qrc b/client/resources.qrc index d5e6fc3bb..8693fb7b6 100644 --- a/client/resources.qrc +++ b/client/resources.qrc @@ -19,8 +19,6 @@ images/controls/delete.svg images/controls/download.svg images/controls/edit-3.svg - images/controls/eye-off-new.svg - images/controls/eye-new.svg images/controls/eye-off.svg images/controls/eye.svg images/controls/external-link.svg diff --git a/client/secure_qsettings.cpp b/client/secure_qsettings.cpp index b5f838f3c..8df24e742 100644 --- a/client/secure_qsettings.cpp +++ b/client/secure_qsettings.cpp @@ -12,11 +12,6 @@ #include #include #include -#include - -#include -#include -#include using namespace QKeychain; @@ -24,11 +19,6 @@ 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) @@ -305,182 +295,6 @@ void SecureQSettings::setSecTag(const QString &tag, const QByteArray &data) } } -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 ca59b959a..8878e1d56 100644 --- a/client/secure_qsettings.h +++ b/client/secure_qsettings.h @@ -16,9 +16,6 @@ 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); @@ -38,12 +35,6 @@ 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: diff --git a/client/ui/controllers/settingsController.cpp b/client/ui/controllers/settingsController.cpp index 2409fe61f..2bbe6bee2 100644 --- a/client/ui/controllers/settingsController.cpp +++ b/client/ui/controllers/settingsController.cpp @@ -123,6 +123,8 @@ void SettingsController::exportLogsFile(const QString &fileName) AndroidController::instance()->exportLogsFile(fileName); #else SystemController::saveFile(fileName, Logger::getLogFile()); + if (m_settings->isFileEncryption()) + SystemController::encryptFile(fileName, m_settings->getPassword(), m_settings->getHint()); #endif } @@ -132,6 +134,8 @@ void SettingsController::exportServiceLogsFile(const QString &fileName) AndroidController::instance()->exportLogsFile(fileName); #else SystemController::saveFile(fileName, Logger::getServiceLogFile()); + if (m_settings->isFileEncryption()) + SystemController::encryptFile(fileName, m_settings->getPassword(), m_settings->getHint()); #endif } @@ -162,6 +166,8 @@ void SettingsController::backupAppConfig(const QString &fileName) config["Conf/useAmneziaDns"] = isAmneziaDnsEnabled(); SystemController::saveFile(fileName, QJsonDocument(config).toJson()); + if (m_settings->isFileEncryption()) + SystemController::encryptFile(fileName, m_settings->getPassword(), m_settings->getHint()); } void SettingsController::restoreAppConfig(const QString &fileName) diff --git a/client/ui/controllers/systemController.cpp b/client/ui/controllers/systemController.cpp index 12b869903..0b2f8cc14 100644 --- a/client/ui/controllers/systemController.cpp +++ b/client/ui/controllers/systemController.cpp @@ -10,6 +10,20 @@ #include #include +#include +#include +#include + +namespace +{ + constexpr int SALT_LEN = 16; + constexpr int IV_LEN = 16; + constexpr int KEY_LEN = 32; + constexpr int PBKDF2_ITER = 100000; + + const QByteArray magicString { "EncData" }; +} + #ifdef Q_OS_ANDROID #include "platforms/android/android_controller.h" #endif @@ -89,6 +103,246 @@ bool SystemController::readFile(const QString &fileName, QString &data) return true; } +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 SystemController::encryptFile(const QString &filePath, const QString &password, const QString &hint, QString *error) +{ + QFile f(filePath); + if (!f.open(QIODevice::ReadOnly)) { + if (error) + *error = QStringLiteral("Cannot open file for read: %1").arg(f.errorString()); + return false; + } + QByteArray content = f.readAll(); + f.close(); + + if (content.startsWith(magicString)) { + if (error) + *error = QStringLiteral("File already encrypted (magic found)"); + return false; + } + + QByteArray qba_hint = hint.toUtf8(); + quint32 qba_hint_len = static_cast(qba_hint.size()); + 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(content, key, iv, cipher, true, error)) + return false; + + out.reserve(magicString.size() + SALT_LEN + IV_LEN + cipher.size()); + out += magicString; + out.append(reinterpret_cast(&qba_hint_len), sizeof(qba_hint_len)); + out += hint.toUtf8(); + 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 SystemController::decryptFile(const QString &filePath, const QString &password, QString *error) +{ + QFile f(filePath); + if (!f.open(QIODevice::ReadOnly)) { + if (error) + *error = QStringLiteral("Cannot open file for read: %1").arg(f.errorString()); + return false; + } + QByteArray content = f.readAll(); + f.close(); + + if (!content.startsWith(magicString)) { + if (error) + *error = QStringLiteral("File is not recognized as encrypted (magic missing)"); + return false; + } + + int pos = magicString.size(); + + quint32 hintLen = 0; + memcpy(&hintLen, content.constData() + pos, sizeof(quint32)); + + pos += sizeof(quint32); + pos += hintLen; + + if (content.size() < pos + SALT_LEN + IV_LEN) { + if (error) + *error = QStringLiteral("Encrypted file too small / corrupted"); + return false; + } + + QByteArray salt = content.mid(pos, SALT_LEN); + pos += SALT_LEN; + QByteArray iv = content.mid(pos, IV_LEN); + pos += IV_LEN; + QByteArray cipher = content.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; +} + +bool SystemController::isFileEncrypted(const QString &filePath) +{ + QFile f(filePath); + if (!f.open(QIODevice::ReadOnly)) { + qDebug() << "Cannot open file for read: %1", f.errorString(); + return false; + } + QByteArray content = f.readAll(); + f.close(); + + if (!content.startsWith(magicString)) { + qDebug() << "File is not recognized as encrypted (magic missing)"; + return false; + } + + return true; +} + +QString SystemController::readHint(const QString &filePath) +{ + QFile f(filePath); + if (!f.open(QIODevice::ReadOnly)) { + qDebug() << f.errorString(); + return {}; + } + + QByteArray data = f.readAll(); + f.close(); + + if (!data.startsWith(magicString)) { + qDebug() << "File is not encrypted"; + return {}; + } + + int pos = magicString.size(); + + if (data.size() < pos + static_cast(sizeof(quint32))) { + qDebug() << "Corrupted file (no hint length)"; + return {}; + } + + quint32 hintLen = 0; + memcpy(&hintLen, data.constData() + pos, sizeof(quint32)); + pos += sizeof(quint32); + + if (data.size() < pos + static_cast(hintLen)) { + qDebug() << "Corrupted file (hint truncated)"; + return {}; + } + + return QString::fromUtf8(data.constData() + pos, hintLen); +} + QString SystemController::getFileName(const QString &acceptLabel, const QString &nameFilter, const QString &selectedFile, const bool isSaveMode, const QString &defaultSuffix) { diff --git a/client/ui/controllers/systemController.h b/client/ui/controllers/systemController.h index 8cb3a0d16..a18fc00f3 100644 --- a/client/ui/controllers/systemController.h +++ b/client/ui/controllers/systemController.h @@ -15,10 +15,17 @@ public: static bool readFile(const QString &fileName, QByteArray &data); static bool readFile(const QString &fileName, QString &data); + static bool encryptFile(const QString &filePath, const QString &password, const QString &hint, QString *error = nullptr); + public slots: QString getFileName(const QString &acceptLabel, const QString &nameFilter, const QString &selectedFile = "", const bool isSaveMode = false, const QString &defaultSuffix = ""); + bool decryptFile(const QString &filePath, const QString &password, QString *error = nullptr); + + bool isFileEncrypted(const QString &filePath); + QString readHint(const QString &filePath); + void setQmlRoot(QObject *qmlRoot); bool isAuthenticated(); diff --git a/client/ui/qml/Components/PasswordDrawer.qml b/client/ui/qml/Components/PasswordDrawer.qml index 652a8d525..ad8c359a9 100644 --- a/client/ui/qml/Components/PasswordDrawer.qml +++ b/client/ui/qml/Components/PasswordDrawer.qml @@ -13,6 +13,8 @@ DrawerType2 { id: root objectName: "passwordDrawer" + property bool fromOutside: true + property string fileName property var securedFunc expandedStateContent: ColumnLayout { @@ -33,13 +35,21 @@ DrawerType2 { TextFieldWithHeaderType { id: passwordField + Connections { + target: root + + function onCloseTriggered() { + passwordField.textField.text = "" + } + } + property bool hideContent: true Layout.fillWidth: true headerText: qsTr("Password") textField.echoMode: hideContent ? TextInput.Password : TextInput.Normal - textField.text: textField.text + textField.text: "" rightButtonClickedOnEnter: true @@ -62,7 +72,7 @@ DrawerType2 { Layout.topMargin: 8 Layout.bottomMargin: 16 - text: SettingsController.getHint() + text: fromOutside ? SystemController.readHint(fileName) : SettingsController.getHint() } BasicButtonType { @@ -73,9 +83,16 @@ DrawerType2 { text: qsTr("Done") clickedFunc: function() { - if (passwordField.textField.text !== SettingsController.getPassword()) { - passwordField.errorText = qsTr("Incorrect password") - return + if (fromOutside) { + if (!SystemController.decryptFile(fileName, passwordField.textField.text)) { + passwordField.errorText = qsTr("Incorrect password") + return + } + } else { + if (passwordField.textField.text !== SettingsController.getPassword()) { + passwordField.errorText = qsTr("Incorrect password") + return + } } if (root.securedFunc && typeof root.securedFunc === "function") { diff --git a/client/ui/qml/Pages2/PageSettingsAppEncryption.qml b/client/ui/qml/Pages2/PageSettingsAppEncryption.qml index 8c5900b8c..d9137d46e 100644 --- a/client/ui/qml/Pages2/PageSettingsAppEncryption.qml +++ b/client/ui/qml/Pages2/PageSettingsAppEncryption.qml @@ -119,6 +119,8 @@ PageType { PasswordDrawer { id: passwordDrawer + fromOutside: false + parent: root anchors.fill: parent diff --git a/client/ui/qml/Pages2/PageSettingsAppPassword.qml b/client/ui/qml/Pages2/PageSettingsAppPassword.qml index bac19b956..84f21224b 100644 --- a/client/ui/qml/Pages2/PageSettingsAppPassword.qml +++ b/client/ui/qml/Pages2/PageSettingsAppPassword.qml @@ -162,11 +162,11 @@ PageType { property string title: root.isChangingPassword ? qsTr("New password") : qsTr("Set encryption password") readonly property string placeholderContent: "" - property string imageSource: "qrc:/images/controls/eye-new.svg" + property string imageSource: "qrc:/images/controls/eye.svg" property bool hideContent: true readonly property var clickedHandler: function() { hideContent = !hideContent - imageSource = hideContent ? "qrc:/images/controls/eye-new.svg" : "qrc:/images/controls/eye-off-new.svg" + imageSource = hideContent ? "qrc:/images/controls/eye.svg" : "qrc:/images/controls/eye-off.svg" } } diff --git a/client/ui/qml/Pages2/PageSettingsBackup.qml b/client/ui/qml/Pages2/PageSettingsBackup.qml index a8ea9da7c..5ec25bc1e 100644 --- a/client/ui/qml/Pages2/PageSettingsBackup.qml +++ b/client/ui/qml/Pages2/PageSettingsBackup.qml @@ -144,10 +144,24 @@ PageType { var filePath = SystemController.getFileName(qsTr("Open backup file"), qsTr("Backup files (*.backup)")) if (filePath !== "") { - restoreBackup(filePath) + passwordDrawer.fileName = filePath + SystemController.isFileEncrypted(filePath) ? passwordDrawer.openTriggered() : passwordDrawer.securedFunc() } } } + + PasswordDrawer { + id: passwordDrawer + + parent: root + + anchors.fill: parent + expandedHeight: root.height * 0.45 + + securedFunc: function() { + restoreBackup(fileName) + } + } } } From d1cd2a9d8da5e61aaa6510868f4c0fb49e65c0cc Mon Sep 17 00:00:00 2001 From: MrMirDan Date: Fri, 19 Dec 2025 12:24:37 +0200 Subject: [PATCH 07/24] update: files encrypt on export and files data decrypt on import --- .../controllers/api/apiConfigsController.cpp | 2 + client/ui/controllers/exportController.cpp | 2 + client/ui/controllers/systemController.cpp | 157 ++++++++---------- client/ui/controllers/systemController.h | 5 +- client/ui/qml/Components/PasswordDrawer.qml | 29 +++- client/ui/qml/Pages2/PageSettingsBackup.qml | 4 +- .../Pages2/PageSetupWizardConfigSource.qml | 26 ++- 7 files changed, 129 insertions(+), 96 deletions(-) diff --git a/client/ui/controllers/api/apiConfigsController.cpp b/client/ui/controllers/api/apiConfigsController.cpp index 026224c5d..b2489ed26 100644 --- a/client/ui/controllers/api/apiConfigsController.cpp +++ b/client/ui/controllers/api/apiConfigsController.cpp @@ -310,6 +310,8 @@ bool ApiConfigsController::exportNativeConfig(const QString &serverCountryCode, nativeConfig.replace("$WIREGUARD_CLIENT_PRIVATE_KEY", protocolData.wireGuardClientPrivKey); SystemController::saveFile(fileName, nativeConfig); + if (m_settings->isFileEncryption()) + SystemController::encryptFile(fileName, m_settings->getPassword(), m_settings->getHint()); return true; } diff --git a/client/ui/controllers/exportController.cpp b/client/ui/controllers/exportController.cpp index 853aacbb3..6f74620f3 100644 --- a/client/ui/controllers/exportController.cpp +++ b/client/ui/controllers/exportController.cpp @@ -282,6 +282,8 @@ QList ExportController::getQrCodes() void ExportController::exportConfig(const QString &fileName) { SystemController::saveFile(fileName, m_config); + if (m_settings->isFileEncryption()) + SystemController::encryptFile(fileName, m_settings->getPassword(), m_settings->getHint()); } void ExportController::updateClientManagementModel(const DockerContainer container, ServerCredentials credentials) diff --git a/client/ui/controllers/systemController.cpp b/client/ui/controllers/systemController.cpp index 0b2f8cc14..70c639671 100644 --- a/client/ui/controllers/systemController.cpp +++ b/client/ui/controllers/systemController.cpp @@ -113,7 +113,7 @@ static QString opensslErrString() return QString::fromUtf8(buf); } -static bool deriveKey(const QByteArray &password, const QByteArray &salt, QByteArray &outKey, QString *err) +static bool deriveKey(const QByteArray &password, const QByteArray &salt, QByteArray &outKey) { outKey.resize(KEY_LEN); const unsigned char *pw = reinterpret_cast(password.constData()); @@ -121,27 +121,23 @@ static bool deriveKey(const QByteArray &password, const QByteArray &salt, QByteA 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(); + qDebug() << opensslErrString(); } return ok == 1; } -static bool aesCrypt(const QByteArray &in, const QByteArray &key, const QByteArray &iv, QByteArray &out, bool encrypt, - QString *err) +static bool aesCrypt(const QByteArray &in, const QByteArray &key, const QByteArray &iv, QByteArray &out, bool encrypt) { EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new(); if (!ctx) { - if (err) - *err = "EVP_CIPHER_CTX_new failed"; + qDebug() << "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(); + qDebug() << opensslErrString(); EVP_CIPHER_CTX_free(ctx); return false; } @@ -152,15 +148,13 @@ static bool aesCrypt(const QByteArray &in, const QByteArray &key, const QByteArr if (1 != EVP_CipherUpdate(ctx, reinterpret_cast(out.data()), &outlen1, reinterpret_cast(in.constData()), in.size())) { - if (err) - *err = opensslErrString(); + qDebug() << 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(); + qDebug() << opensslErrString(); EVP_CIPHER_CTX_free(ctx); return false; } @@ -169,20 +163,18 @@ static bool aesCrypt(const QByteArray &in, const QByteArray &key, const QByteArr return true; } -bool SystemController::encryptFile(const QString &filePath, const QString &password, const QString &hint, QString *error) +bool SystemController::encryptFile(const QString &filePath, const QString &password, const QString &hint) { QFile f(filePath); if (!f.open(QIODevice::ReadOnly)) { - if (error) - *error = QStringLiteral("Cannot open file for read: %1").arg(f.errorString()); + qDebug() << QStringLiteral("Cannot open file for read: %1").arg(f.errorString()); return false; } QByteArray content = f.readAll(); f.close(); if (content.startsWith(magicString)) { - if (error) - *error = QStringLiteral("File already encrypted (magic found)"); + qDebug() << QStringLiteral("File already encrypted (magic found)"); return false; } @@ -196,15 +188,14 @@ bool SystemController::encryptFile(const QString &filePath, const QString &passw if (1 != RAND_bytes(reinterpret_cast(salt.data()), SALT_LEN) || 1 != RAND_bytes(reinterpret_cast(iv.data()), IV_LEN)) { - if (error) - *error = opensslErrString(); + qDebug() << opensslErrString(); return false; } - if (!deriveKey(password.toUtf8(), salt, key, error)) + if (!deriveKey(password.toUtf8(), salt, key)) return false; - if (!aesCrypt(content, key, iv, cipher, true, error)) + if (!aesCrypt(content, key, iv, cipher, true)) return false; out.reserve(magicString.size() + SALT_LEN + IV_LEN + cipher.size()); @@ -216,78 +207,43 @@ bool SystemController::encryptFile(const QString &filePath, const QString &passw out += cipher; if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate)) { - if (error) - *error = QStringLiteral("Cannot open file for write: %1").arg(f.errorString()); + qDebug() << 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"); + qDebug() << QStringLiteral("Write failed or incomplete"); return false; } return true; } -bool SystemController::decryptFile(const QString &filePath, const QString &password, QString *error) +QByteArray SystemController::getDecryptedData(const QString &filePath, const QString &password) { - QFile f(filePath); - if (!f.open(QIODevice::ReadOnly)) { - if (error) - *error = QStringLiteral("Cannot open file for read: %1").arg(f.errorString()); - return false; - } - QByteArray content = f.readAll(); - f.close(); - - if (!content.startsWith(magicString)) { - if (error) - *error = QStringLiteral("File is not recognized as encrypted (magic missing)"); - return false; - } + QByteArray encData; + readFile(filePath, encData); int pos = magicString.size(); quint32 hintLen = 0; - memcpy(&hintLen, content.constData() + pos, sizeof(quint32)); - + memcpy(&hintLen, encData.constData() + pos, sizeof(quint32)); pos += sizeof(quint32); pos += hintLen; - if (content.size() < pos + SALT_LEN + IV_LEN) { - if (error) - *error = QStringLiteral("Encrypted file too small / corrupted"); - return false; - } + QByteArray salt = encData.mid(pos, 16); + pos += 16; + QByteArray iv = encData.mid(pos, 16); + pos += 16; + QByteArray cipher = encData.mid(pos); - QByteArray salt = content.mid(pos, SALT_LEN); - pos += SALT_LEN; - QByteArray iv = content.mid(pos, IV_LEN); - pos += IV_LEN; - QByteArray cipher = content.mid(pos); QByteArray key; - QByteArray plain; + deriveKey(password.toUtf8(), salt, key); - if (!deriveKey(password.toUtf8(), salt, key, error)) - return false; + QByteArray data; + !aesCrypt(cipher, key, iv, data, 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; + return data; } bool SystemController::isFileEncrypted(const QString &filePath) @@ -297,11 +253,49 @@ bool SystemController::isFileEncrypted(const QString &filePath) qDebug() << "Cannot open file for read: %1", f.errorString(); return false; } + QByteArray data = f.readAll(); + f.close(); + + if (!data.startsWith(magicString)) { + qDebug() << "File is not recognized as encrypted (magic missing)"; + return false; + } + + return true; +} + +bool SystemController::isPasswordValid(const QString &filePath, const QString &password) +{ + QFile f(filePath); + if (!f.open(QIODevice::ReadOnly)) { + qDebug() << f.errorString(); + return false; + } + QByteArray content = f.readAll(); f.close(); - if (!content.startsWith(magicString)) { - qDebug() << "File is not recognized as encrypted (magic missing)"; + int pos = magicString.size(); + quint32 hintLen = 0; + memcpy(&hintLen, content.constData() + pos, sizeof(quint32)); + pos += sizeof(quint32); + pos += hintLen; + + QByteArray salt = content.mid(pos, 16); + pos += 16; + QByteArray iv = content.mid(pos, 16); + pos += 16; + QByteArray cipher = content.mid(pos); + + QByteArray key; + if (!deriveKey(password.toUtf8(), salt, key)) + return false; + + QByteArray plain; + bool ok = aesCrypt(cipher, key, iv, plain, false); + + if (!ok) { + qDebug() << "Wrong password"; return false; } @@ -310,19 +304,8 @@ bool SystemController::isFileEncrypted(const QString &filePath) QString SystemController::readHint(const QString &filePath) { - QFile f(filePath); - if (!f.open(QIODevice::ReadOnly)) { - qDebug() << f.errorString(); - return {}; - } - - QByteArray data = f.readAll(); - f.close(); - - if (!data.startsWith(magicString)) { - qDebug() << "File is not encrypted"; - return {}; - } + QByteArray data; + readFile(filePath, data); int pos = magicString.size(); diff --git a/client/ui/controllers/systemController.h b/client/ui/controllers/systemController.h index a18fc00f3..7303a0136 100644 --- a/client/ui/controllers/systemController.h +++ b/client/ui/controllers/systemController.h @@ -15,15 +15,16 @@ public: static bool readFile(const QString &fileName, QByteArray &data); static bool readFile(const QString &fileName, QString &data); - static bool encryptFile(const QString &filePath, const QString &password, const QString &hint, QString *error = nullptr); + static bool encryptFile(const QString &filePath, const QString &password, const QString &hint); public slots: QString getFileName(const QString &acceptLabel, const QString &nameFilter, const QString &selectedFile = "", const bool isSaveMode = false, const QString &defaultSuffix = ""); - bool decryptFile(const QString &filePath, const QString &password, QString *error = nullptr); + QByteArray getDecryptedData(const QString &filePath, const QString &password); bool isFileEncrypted(const QString &filePath); + bool isPasswordValid(const QString &filePath, const QString &password); QString readHint(const QString &filePath); void setQmlRoot(QObject *qmlRoot); diff --git a/client/ui/qml/Components/PasswordDrawer.qml b/client/ui/qml/Components/PasswordDrawer.qml index ad8c359a9..22bade27d 100644 --- a/client/ui/qml/Components/PasswordDrawer.qml +++ b/client/ui/qml/Components/PasswordDrawer.qml @@ -2,6 +2,7 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts +import PageEnum 1.0 import Style 1.0 import "../Controls2" @@ -17,6 +18,9 @@ DrawerType2 { property string fileName property var securedFunc + signal restoreSecuredBackup + signal importSecuredFile + expandedStateContent: ColumnLayout { anchors.top: parent.top anchors.left: parent.left @@ -25,6 +29,27 @@ DrawerType2 { anchors.leftMargin: 16 anchors.rightMargin: 16 + Connections { + target: root + + function onRestoreSecuredBackup() { + root.securedFunc = function() { + SettingsController.restoreAppConfigFromData(SystemController.getDecryptedData(fileName, passwordField.textField.text)) + } + passwordDrawer.openTriggered() + } + + function onImportSecuredFile() { + // TODO: file name not showing on import + root.securedFunc = function() { + if (ImportController.extractConfigFromData(SystemController.getDecryptedData(fileName, passwordField.textField.text))) { + PageController.goToPage(PageEnum.PageSetupWizardViewConfig) + } + } + passwordDrawer.openTriggered() + } + } + Header2TextType { Layout.fillWidth: true Layout.bottomMargin: 8 @@ -49,7 +74,7 @@ DrawerType2 { headerText: qsTr("Password") textField.echoMode: hideContent ? TextInput.Password : TextInput.Normal - textField.text: "" + textField.text: textField.text rightButtonClickedOnEnter: true @@ -84,7 +109,7 @@ DrawerType2 { clickedFunc: function() { if (fromOutside) { - if (!SystemController.decryptFile(fileName, passwordField.textField.text)) { + if (!SystemController.isPasswordValid(fileName, passwordField.textField.text)) { passwordField.errorText = qsTr("Incorrect password") return } diff --git a/client/ui/qml/Pages2/PageSettingsBackup.qml b/client/ui/qml/Pages2/PageSettingsBackup.qml index 5ec25bc1e..4dd6b2fe2 100644 --- a/client/ui/qml/Pages2/PageSettingsBackup.qml +++ b/client/ui/qml/Pages2/PageSettingsBackup.qml @@ -145,7 +145,7 @@ PageType { qsTr("Backup files (*.backup)")) if (filePath !== "") { passwordDrawer.fileName = filePath - SystemController.isFileEncrypted(filePath) ? passwordDrawer.openTriggered() : passwordDrawer.securedFunc() + SystemController.isFileEncrypted(filePath) ? passwordDrawer.openTriggered() : restoreBackup(filePath) } } } @@ -159,7 +159,7 @@ PageType { expandedHeight: root.height * 0.45 securedFunc: function() { - restoreBackup(fileName) + passwordDrawer.restoreSecuredBackup() } } } diff --git a/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml b/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml index 3410842d6..30c28be53 100644 --- a/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml +++ b/client/ui/qml/Pages2/PageSetupWizardConfigSource.qml @@ -12,6 +12,7 @@ import "./" import "../Controls2" import "../Controls2/TextTypes" import "../Config" +import "../Components" PageType { id: root @@ -261,6 +262,15 @@ PageType { } } + PasswordDrawer { + id: passwordDrawer + + parent: root + + anchors.fill: parent + expandedHeight: root.height * 0.45 + } + property list variants: [ amneziaVpn, selfHostVpn, @@ -311,7 +321,12 @@ PageType { qsTr("Backup files (*.backup)")) if (filePath !== "") { PageController.showBusyIndicator(true) - SettingsController.restoreAppConfig(filePath) + if (SystemController.isFileEncrypted(filePath)) { + passwordDrawer.fileName = filePath + passwordDrawer.restoreSecuredBackup() + } else { + SettingsController.restoreAppConfig(filePath) + } PageController.showBusyIndicator(false) } } @@ -329,8 +344,13 @@ PageType { "Config files (*.vpn *.ovpn *.conf *.json)" var fileName = SystemController.getFileName(qsTr("Open config file"), nameFilter) if (fileName !== "") { - if (ImportController.extractConfigFromFile(fileName)) { - PageController.goToPage(PageEnum.PageSetupWizardViewConfig) + if (SystemController.isFileEncrypted(fileName)) { + passwordDrawer.fileName = fileName + passwordDrawer.importSecuredFile() + } else { + if (ImportController.extractConfigFromFile(fileName)) { + PageController.goToPage(PageEnum.PageSetupWizardViewConfig) + } } } } From 4097af8d819b41ca3b1dc2770f6b94f7a6f4c6d3 Mon Sep 17 00:00:00 2001 From: MrMirDan Date: Fri, 19 Dec 2025 12:40:43 +0200 Subject: [PATCH 08/24] update: logging page text --- client/ui/qml/Pages2/PageSettingsLogging.qml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/client/ui/qml/Pages2/PageSettingsLogging.qml b/client/ui/qml/Pages2/PageSettingsLogging.qml index 240e8c8c5..dba1c47d1 100644 --- a/client/ui/qml/Pages2/PageSettingsLogging.qml +++ b/client/ui/qml/Pages2/PageSettingsLogging.qml @@ -48,8 +48,8 @@ PageType { Layout.rightMargin: 16 headerText: qsTr("Logging") - descriptionText: qsTr("Enabling this function will save application's logs automatically. " + - "By default, logging functionality is disabled. Enable log saving in case of application malfunction.") + descriptionText: qsTr("Logs help diagnose app errors and connection issues" + + "Logging is disabled by default. Enable it when troubleshooting or if requested by support") } SwitcherType { @@ -60,7 +60,7 @@ PageType { Layout.leftMargin: 16 Layout.rightMargin: 16 - text: qsTr("Enable logs") + text: qsTr("Enable logging") checked: SettingsController.isLoggingEnabled @@ -77,7 +77,7 @@ PageType { Layout.fillWidth: true Layout.topMargin: -8 - text: qsTr("Clear logs") + text: qsTr("Delete all logs") leftImageSource: "qrc:/images/controls/trash.svg" isSmallLeftImage: true @@ -154,7 +154,7 @@ PageType { Layout.topMargin: -8 Layout.bottomMargin: -8 - text: qsTr("Export logs") + text: qsTr("Save logs to file") leftImageSource: "qrc:/images/controls/save.svg" isSmallLeftImage: true @@ -178,7 +178,7 @@ PageType { QtObject { id: clientLogs - readonly property string title: qsTr("Client logs") + readonly property string title: qsTr("App logs") readonly property string description: qsTr("AmneziaVPN logs") readonly property bool isVisible: true readonly property var openLogsHandler: function() { From d8bf7d4d1ac0362921d84beadfba665fad4e07eb Mon Sep 17 00:00:00 2001 From: MrMirDan Date: Fri, 19 Dec 2025 12:58:16 +0200 Subject: [PATCH 09/24] update: added encryption indicator on backup, share and api configs pages --- client/ui/qml/Pages2/PageSettingsApiNativeConfigs.qml | 9 +++++++++ client/ui/qml/Pages2/PageSettingsBackup.qml | 9 +++++++++ client/ui/qml/Pages2/PageShare.qml | 9 +++++++++ client/ui/qml/Pages2/PageShareFullAccess.qml | 9 +++++++++ 4 files changed, 36 insertions(+) diff --git a/client/ui/qml/Pages2/PageSettingsApiNativeConfigs.qml b/client/ui/qml/Pages2/PageSettingsApiNativeConfigs.qml index dc3f63c6a..3d74f4a52 100644 --- a/client/ui/qml/Pages2/PageSettingsApiNativeConfigs.qml +++ b/client/ui/qml/Pages2/PageSettingsApiNativeConfigs.qml @@ -60,6 +60,15 @@ PageType { headerText: qsTr("Configuration Files") descriptionText: qsTr("For router setup or the AmneziaWG app") } + + EncryptionIndicator { + id: indicator + + visible: SettingsController.isFileEncryptionEnabled() + + textString: qsTr("Encryption enabled. Learn more") + iconPath: "qrc:/images/controls/lock-locked.svg" + } } delegate: ColumnLayout { diff --git a/client/ui/qml/Pages2/PageSettingsBackup.qml b/client/ui/qml/Pages2/PageSettingsBackup.qml index 4dd6b2fe2..d902e082a 100644 --- a/client/ui/qml/Pages2/PageSettingsBackup.qml +++ b/client/ui/qml/Pages2/PageSettingsBackup.qml @@ -71,6 +71,15 @@ PageType { headerText: qsTr("Back up your configuration") descriptionText: qsTr("You can save your settings to a backup file to restore them the next time you install the application.") } + + EncryptionIndicator { + id: indicator + + visible: SettingsController.isFileEncryptionEnabled() + + textString: qsTr("Encryption enabled. Learn more") + iconPath: "qrc:/images/controls/lock-locked.svg" + } } model: 1 // fake model to force the ListView to be created without a model diff --git a/client/ui/qml/Pages2/PageShare.qml b/client/ui/qml/Pages2/PageShare.qml index 67540c6ae..3b3d65297 100644 --- a/client/ui/qml/Pages2/PageShare.qml +++ b/client/ui/qml/Pages2/PageShare.qml @@ -294,6 +294,15 @@ PageType { color: AmneziaStyle.color.mutedGray } + EncryptionIndicator { + id: indicator + + visible: SettingsController.isFileEncryptionEnabled() + + textString: qsTr("Encryption enabled. Learn more") + iconPath: "qrc:/images/controls/lock-locked.svg" + } + TextFieldWithHeaderType { id: clientNameTextField Layout.fillWidth: true diff --git a/client/ui/qml/Pages2/PageShareFullAccess.qml b/client/ui/qml/Pages2/PageShareFullAccess.qml index d79abcc34..7f404eba2 100644 --- a/client/ui/qml/Pages2/PageShareFullAccess.qml +++ b/client/ui/qml/Pages2/PageShareFullAccess.qml @@ -69,6 +69,15 @@ PageType { color: AmneziaStyle.color.mutedGray } + EncryptionIndicator { + id: indicator + + visible: SettingsController.isFileEncryptionEnabled() + + textString: qsTr("Encryption enabled. Learn more") + iconPath: "qrc:/images/controls/lock-locked.svg" + } + DropDownType { id: serverSelector objectName: "serverSelector" From 4c082654f949c45928f8d68b217072534d27bee0 Mon Sep 17 00:00:00 2001 From: MrMirDan Date: Fri, 19 Dec 2025 13:24:51 +0200 Subject: [PATCH 10/24] fix: correctly save password on set or change --- client/ui/controllers/settingsController.cpp | 20 +++++++++++++++++++ client/ui/controllers/settingsController.h | 8 ++++++++ .../ui/qml/Pages2/PageSettingsAppPassword.qml | 4 ++-- .../Pages2/PageSettingsAppPasswordConfirm.qml | 10 ++++++++-- 4 files changed, 38 insertions(+), 4 deletions(-) diff --git a/client/ui/controllers/settingsController.cpp b/client/ui/controllers/settingsController.cpp index 2bbe6bee2..fc3eec8f7 100644 --- a/client/ui/controllers/settingsController.cpp +++ b/client/ui/controllers/settingsController.cpp @@ -333,6 +333,26 @@ QString SettingsController::getHint() return m_settings->getHint(); } +void SettingsController::setTempPassword(QString pwd) +{ + tempPassword = pwd; +} + +QString SettingsController::getTempPassword() +{ + return tempPassword; +} + +void SettingsController::setTempHint(QString hint) +{ + tempHint = hint; +} + +QString SettingsController::getTempHint() +{ + return tempHint; +} + bool SettingsController::isScreenshotsEnabled() { return m_settings->isScreenshotsEnabled(); diff --git a/client/ui/controllers/settingsController.h b/client/ui/controllers/settingsController.h index 62ea98a63..4e7c8fc53 100644 --- a/client/ui/controllers/settingsController.h +++ b/client/ui/controllers/settingsController.h @@ -78,6 +78,11 @@ public slots: void setHint(QString hint); QString getHint(); + void setTempPassword(QString pwd); + QString getTempPassword(); + void setTempHint(QString hint); + QString getTempHint(); + bool isScreenshotsEnabled(); void toggleScreenshotsEnabled(bool enable); @@ -151,6 +156,9 @@ private: QString getPlatform(); + QString tempPassword; + QString tempHint; + QDateTime m_loggingDisableDate; bool m_isDevModeEnabled = false; diff --git a/client/ui/qml/Pages2/PageSettingsAppPassword.qml b/client/ui/qml/Pages2/PageSettingsAppPassword.qml index 84f21224b..ae05a3423 100644 --- a/client/ui/qml/Pages2/PageSettingsAppPassword.qml +++ b/client/ui/qml/Pages2/PageSettingsAppPassword.qml @@ -125,8 +125,8 @@ PageType { var _password = listView.itemAtIndex(vars.passwordIndex).children[0].textField.text var _hint = listView.itemAtIndex(vars.hintIndex).children[0].textField.text - SettingsController.setPassword(_password) - SettingsController.setHint(_hint) + SettingsController.setTempPassword(_password) + SettingsController.setTempHint(_hint) PageController.goToPage(PageEnum.PageSettingsAppPasswordConfirm) if (root.isChangingPassword) { diff --git a/client/ui/qml/Pages2/PageSettingsAppPasswordConfirm.qml b/client/ui/qml/Pages2/PageSettingsAppPasswordConfirm.qml index 2fc1768a2..52886b62c 100644 --- a/client/ui/qml/Pages2/PageSettingsAppPasswordConfirm.qml +++ b/client/ui/qml/Pages2/PageSettingsAppPasswordConfirm.qml @@ -108,7 +108,7 @@ PageType { Layout.rightMargin: 16 Layout.bottomMargin: 24 - text: SettingsController.getHint() + text: SettingsController.getTempHint() } BasicButtonType { @@ -125,6 +125,12 @@ PageType { return } + SettingsController.setPassword(SettingsController.getTempPassword()) + SettingsController.setHint(SettingsController.getTempHint()) + + SettingsController.setTempPassword("") + SettingsController.setTempHint("") + PageController.closePage() PageController.goToPage(PageEnum.PageSettings) PageController.goToPage(PageEnum.PageSettingsAppEncryption) @@ -138,7 +144,7 @@ PageType { var notMatch = false var secretDataItem = listView.itemAtIndex(0).children[0] - if (secretDataItem.textField.text !== SettingsController.getPassword()) { + if (secretDataItem.textField.text !== SettingsController.getTempPassword()) { secretDataItem.errorText = qsTr("Passwords not match") notMatch = true } From 452150bffff31a8fdb6c6e5f4f2f9cfdf2d1e28b Mon Sep 17 00:00:00 2001 From: MrMirDan Date: Tue, 23 Dec 2025 14:10:19 +0200 Subject: [PATCH 11/24] update: text and some fixes --- client/ui/controllers/settingsController.cpp | 4 - .../ui/qml/Components/EncryptionIndicator.qml | 33 +++---- client/ui/qml/Components/PasswordDrawer.qml | 6 +- .../Pages2/PageSettingsApiNativeConfigs.qml | 1 + .../qml/Pages2/PageSettingsAppEncryption.qml | 85 +++++++++++++------ .../ui/qml/Pages2/PageSettingsAppPassword.qml | 34 +++++++- .../Pages2/PageSettingsAppPasswordConfirm.qml | 4 +- .../ui/qml/Pages2/PageSettingsApplication.qml | 4 +- client/ui/qml/Pages2/PageSettingsBackup.qml | 3 +- client/ui/qml/Pages2/PageShare.qml | 1 + 10 files changed, 119 insertions(+), 56 deletions(-) diff --git a/client/ui/controllers/settingsController.cpp b/client/ui/controllers/settingsController.cpp index fc3eec8f7..f0dbfc875 100644 --- a/client/ui/controllers/settingsController.cpp +++ b/client/ui/controllers/settingsController.cpp @@ -123,8 +123,6 @@ void SettingsController::exportLogsFile(const QString &fileName) AndroidController::instance()->exportLogsFile(fileName); #else SystemController::saveFile(fileName, Logger::getLogFile()); - if (m_settings->isFileEncryption()) - SystemController::encryptFile(fileName, m_settings->getPassword(), m_settings->getHint()); #endif } @@ -134,8 +132,6 @@ void SettingsController::exportServiceLogsFile(const QString &fileName) AndroidController::instance()->exportLogsFile(fileName); #else SystemController::saveFile(fileName, Logger::getServiceLogFile()); - if (m_settings->isFileEncryption()) - SystemController::encryptFile(fileName, m_settings->getPassword(), m_settings->getHint()); #endif } diff --git a/client/ui/qml/Components/EncryptionIndicator.qml b/client/ui/qml/Components/EncryptionIndicator.qml index ca4c29a72..35883c7c1 100644 --- a/client/ui/qml/Components/EncryptionIndicator.qml +++ b/client/ui/qml/Components/EncryptionIndicator.qml @@ -7,12 +7,13 @@ import Style 1.0 import "../Controls2" import "../Controls2/TextTypes" - import "../Config" Rectangle { id: root + property bool linkEnabled: false + property string textColor: AmneziaStyle.color.paleGray property string textString @@ -33,23 +34,15 @@ Rectangle { RowLayout { id: content - width: parent.width - anchors.fill: parent - - anchors.leftMargin: content.width / 4 - anchors.rightMargin: content.width / 4 - anchors.topMargin: 4 - anchors.bottomMargin: 4 + anchors.centerIn: parent spacing: 0 Image { - Layout.alignment: Qt.AlignTop + width: root.iconWidth + height: root.iconHeight - width: iconWidth - height: iconHeight - - source: iconPath + source: root.iconPath } CaptionTextType { @@ -58,9 +51,17 @@ Rectangle { Layout.fillWidth: true Layout.leftMargin: 8 - text: textString - textFormat: root.textFormat - color: textColor + text: root.linkEnabled ? root.textString + + qsTr(" Learn more").arg(AmneziaStyle.color.goldenApricot) + : root.textString + textFormat: Text.RichText + color: root.textColor + + onLinkActivated: function(link) { + if (link === "learnMore") { + console.log("Learn more pressed") + } + } } } } \ No newline at end of file diff --git a/client/ui/qml/Components/PasswordDrawer.qml b/client/ui/qml/Components/PasswordDrawer.qml index 22bade27d..55b8f4e9b 100644 --- a/client/ui/qml/Components/PasswordDrawer.qml +++ b/client/ui/qml/Components/PasswordDrawer.qml @@ -54,7 +54,7 @@ DrawerType2 { Layout.fillWidth: true Layout.bottomMargin: 8 - text: qsTr("Enter password to continue") + text: qsTr("Password required") } TextFieldWithHeaderType { @@ -110,12 +110,12 @@ DrawerType2 { clickedFunc: function() { if (fromOutside) { if (!SystemController.isPasswordValid(fileName, passwordField.textField.text)) { - passwordField.errorText = qsTr("Incorrect password") + passwordField.errorText = qsTr("Invalid password") return } } else { if (passwordField.textField.text !== SettingsController.getPassword()) { - passwordField.errorText = qsTr("Incorrect password") + passwordField.errorText = qsTr("Invalid password") return } } diff --git a/client/ui/qml/Pages2/PageSettingsApiNativeConfigs.qml b/client/ui/qml/Pages2/PageSettingsApiNativeConfigs.qml index 3d74f4a52..ca69ef790 100644 --- a/client/ui/qml/Pages2/PageSettingsApiNativeConfigs.qml +++ b/client/ui/qml/Pages2/PageSettingsApiNativeConfigs.qml @@ -65,6 +65,7 @@ PageType { id: indicator visible: SettingsController.isFileEncryptionEnabled() + linkEnabled: true textString: qsTr("Encryption enabled. Learn more") iconPath: "qrc:/images/controls/lock-locked.svg" diff --git a/client/ui/qml/Pages2/PageSettingsAppEncryption.qml b/client/ui/qml/Pages2/PageSettingsAppEncryption.qml index d9137d46e..a24b9f423 100644 --- a/client/ui/qml/Pages2/PageSettingsAppEncryption.qml +++ b/client/ui/qml/Pages2/PageSettingsAppEncryption.qml @@ -22,8 +22,9 @@ PageType { function onFileEncryptionStateChanged() { PageController.showBusyIndicator(true) PageController.closePage() - PageController.goToPage(PageEnum.PageSettingsAppEncryption) + SettingsController.isFileEncryptionEnabled() ? PageController.goToPage(PageEnum.PageSettingsAppEncryption) : PageController.goToPage(PageEnum.PageSettingsAppPassword) PageController.showBusyIndicator(false) + PageController.showNotificationMessage(SettingsController.isFileEncryptionEnabled() ? qsTr("Encryption enabled") : qsTr("Encryption disabled")) } } @@ -64,33 +65,56 @@ PageType { Layout.fillWidth: true Layout.leftMargin: 16 Layout.rightMargin: 16 - Layout.bottomMargin: 16 - headerText: qsTr("File encryption") - descriptionText: qsTr("For encrypting backups, configuration files, subscription keys, and logs") + headerText: qsTr("Password & Encryption") + descriptionText: qsTr("Password protection for backups and configuration files.\nRequired to restore or import encrypted files.") + } + + BasicButtonType { + Layout.leftMargin: 8 + Layout.bottomMargin: 16 + implicitHeight: 16 + + defaultColor: AmneziaStyle.color.transparent + hoveredColor: AmneziaStyle.color.translucentWhite + pressedColor: AmneziaStyle.color.sheerWhite + disabledColor: AmneziaStyle.color.mutedGray + textColor: AmneziaStyle.color.goldenApricot + + text: qsTr("Learn more") + + clickedFunc: function() { + // TODO: add link + } } EncryptionIndicator { id: indicator - textString: SettingsController.isFileEncryptionEnabled() ? qsTr("Password set. Encryption on") : qsTr("Password not set. Encryption off") - iconPath: SettingsController.isFileEncryptionEnabled() ? "qrc:/images/controls/lock-locked.svg" : "qrc:/images/controls/lock-unlocked.svg" + textString: qsTr("Password set. Encryption enabled") + iconPath: "qrc:/images/controls/lock-locked.svg" } BasicButtonType { - id: switchEncryptionButton + id: disableEncryptionButton Layout.fillWidth: true Layout.topMargin: 16 Layout.leftMargin: 16 Layout.rightMargin: 16 - text: SettingsController.isFileEncryptionEnabled() ? qsTr("Turn off encryption") : qsTr("Turn on encryption") + text: qsTr("Disable encryption") clickedFunc: function() { - SettingsController.isFileEncryptionEnabled() ? SettingsController.toggleFileEncryption(false) - : SettingsController.toggleFileEncryption(true) + passwordDrawer.securedFunc = function() { + PageController.showBusyIndicator(true) + SettingsController.toggleFileEncryption(false) + SettingsController.setPassword("") + SettingsController.setHint("") + PageController.showBusyIndicator(false) + } + passwordDrawer.openTriggered() } } @@ -109,9 +133,17 @@ PageType { text: qsTr("Change password") - signal changingPassword - clickedFunc: function() { + passwordDrawer.securedFunc = function() { + root.isChangingPassword = true + + PageController.showBusyIndicator(true) + PageController.closePage() + PageController.goToPage(PageEnum.PageSettingsAppPassword) + PageController.showBusyIndicator(false) + + SettingsController.changingPassword() + } passwordDrawer.openTriggered() } } @@ -125,17 +157,6 @@ PageType { anchors.fill: parent expandedHeight: root.height * 0.45 - - securedFunc: function() { - root.isChangingPassword = true - - PageController.showBusyIndicator(true) - PageController.closePage() - PageController.goToPage(PageEnum.PageSettingsAppPassword) - PageController.showBusyIndicator(false) - - SettingsController.changingPassword() - } } } @@ -143,8 +164,24 @@ PageType { footer: ColumnLayout { width: listView.width + CaptionTextType { + Layout.fillWidth: true + Layout.topMargin: 16 - // TODO: add text + horizontalAlignment: Text.AlignHCenter + textFormat: Text.RichText + + text: qsTr("If the password is forgotten, it can be recovered. To reset the password, " + + "settings must be reset." + + "\nEncrypted files can only be opened with password used to encrypt them").arg(AmneziaStyle.color.goldenApricot) + color: AmneziaStyle.color.mutedGray + + onLinkActivated: function(link) { + if (link === "appSettings") { + PageController.closePage() + } + } + } } } } \ No newline at end of file diff --git a/client/ui/qml/Pages2/PageSettingsAppPassword.qml b/client/ui/qml/Pages2/PageSettingsAppPassword.qml index ae05a3423..b96e61bea 100644 --- a/client/ui/qml/Pages2/PageSettingsAppPassword.qml +++ b/client/ui/qml/Pages2/PageSettingsAppPassword.qml @@ -61,11 +61,37 @@ PageType { Layout.fillWidth: true Layout.leftMargin: 16 Layout.rightMargin: 16 - Layout.bottomMargin: 16 - headerText: root.isChangingPassword ? qsTr("Password changing") : qsTr("File encryption") - descriptionText: root.isChangingPassword ? qsTr("Files encrypted with old password will stay encrypted with old password") - : qsTr("For encrypting backups, configuration files, subscription keys, and logs") + headerText: root.isChangingPassword ? qsTr("Change password") : qsTr("Password & Encryption") + descriptionText: root.isChangingPassword ? qsTr("Existing encrypted files will still require the old password.\nThe new password will be used for new encrypted files.") + : qsTr("Password protection for backups and configuration files.\nRequired to restore or import encrypted files.") + } + + BasicButtonType { + Layout.leftMargin: 8 + Layout.bottomMargin: 16 + implicitHeight: 16 + + defaultColor: AmneziaStyle.color.transparent + hoveredColor: AmneziaStyle.color.translucentWhite + pressedColor: AmneziaStyle.color.sheerWhite + disabledColor: AmneziaStyle.color.mutedGray + textColor: AmneziaStyle.color.goldenApricot + + text: qsTr("Learn more") + + clickedFunc: function() { + // TODO: add link + } + } + + EncryptionIndicator { + id: indicator + + visible: !root.isChangingPassword + + textString: qsTr("Password not set. Encryption disabled") + iconPath: "qrc:/images/controls/lock-unlocked.svg" } } diff --git a/client/ui/qml/Pages2/PageSettingsAppPasswordConfirm.qml b/client/ui/qml/Pages2/PageSettingsAppPasswordConfirm.qml index 52886b62c..cd282f148 100644 --- a/client/ui/qml/Pages2/PageSettingsAppPasswordConfirm.qml +++ b/client/ui/qml/Pages2/PageSettingsAppPasswordConfirm.qml @@ -55,8 +55,8 @@ PageType { Layout.rightMargin: 16 headerText: root.isChangingPassword ? qsTr("Confirm new password") : qsTr("Confirm password") - descriptionText: qsTr("If you forget your password, you'll have to reset all app settings to reset it." - + " Encrypted files will remain encrypted") + descriptionText: root.isChangingPassword ? qsTr("") : qsTr("If you forget your password, you'll have to reset all app settings to reset it." + + " Encrypted files will remain encrypted") } } diff --git a/client/ui/qml/Pages2/PageSettingsApplication.qml b/client/ui/qml/Pages2/PageSettingsApplication.qml index 206ce3ada..d6e53377c 100644 --- a/client/ui/qml/Pages2/PageSettingsApplication.qml +++ b/client/ui/qml/Pages2/PageSettingsApplication.qml @@ -195,8 +195,8 @@ PageType { Layout.fillWidth: true - text: qsTr("File encryption") - descriptionText: qsTr("For encrypting backups, configuration files, subscription keys, and logs") + text: qsTr("Password & Encryption") + descriptionText: qsTr("Password protection for backups and configuration files") rightImageSource: "qrc:/images/controls/chevron-right.svg" clickedFunction: function() { diff --git a/client/ui/qml/Pages2/PageSettingsBackup.qml b/client/ui/qml/Pages2/PageSettingsBackup.qml index d902e082a..e72e51cac 100644 --- a/client/ui/qml/Pages2/PageSettingsBackup.qml +++ b/client/ui/qml/Pages2/PageSettingsBackup.qml @@ -76,8 +76,9 @@ PageType { id: indicator visible: SettingsController.isFileEncryptionEnabled() + linkEnabled: true - textString: qsTr("Encryption enabled. Learn more") + textString: qsTr("Encryption enabled.") iconPath: "qrc:/images/controls/lock-locked.svg" } } diff --git a/client/ui/qml/Pages2/PageShare.qml b/client/ui/qml/Pages2/PageShare.qml index 3b3d65297..19253879b 100644 --- a/client/ui/qml/Pages2/PageShare.qml +++ b/client/ui/qml/Pages2/PageShare.qml @@ -298,6 +298,7 @@ PageType { id: indicator visible: SettingsController.isFileEncryptionEnabled() + linkEnabled: true textString: qsTr("Encryption enabled. Learn more") iconPath: "qrc:/images/controls/lock-locked.svg" From 805d594608b6e0791c1643a624982dbf1d715725 Mon Sep 17 00:00:00 2001 From: MrMirDan Date: Tue, 23 Dec 2025 14:51:34 +0200 Subject: [PATCH 12/24] updated: added encryption docs links --- client/ui/qml/Components/EncryptionIndicator.qml | 2 +- client/ui/qml/Pages2/PageSettingsAppEncryption.qml | 2 +- client/ui/qml/Pages2/PageSettingsAppPassword.qml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ui/qml/Components/EncryptionIndicator.qml b/client/ui/qml/Components/EncryptionIndicator.qml index 35883c7c1..704688319 100644 --- a/client/ui/qml/Components/EncryptionIndicator.qml +++ b/client/ui/qml/Components/EncryptionIndicator.qml @@ -59,7 +59,7 @@ Rectangle { onLinkActivated: function(link) { if (link === "learnMore") { - console.log("Learn more pressed") + Qt.openUrlExternally("https://storage.googleapis.com/amnezia/docs?m-path=/documentation/instructions/encryption") } } } diff --git a/client/ui/qml/Pages2/PageSettingsAppEncryption.qml b/client/ui/qml/Pages2/PageSettingsAppEncryption.qml index a24b9f423..d35b3506b 100644 --- a/client/ui/qml/Pages2/PageSettingsAppEncryption.qml +++ b/client/ui/qml/Pages2/PageSettingsAppEncryption.qml @@ -84,7 +84,7 @@ PageType { text: qsTr("Learn more") clickedFunc: function() { - // TODO: add link + Qt.openUrlExternally("https://storage.googleapis.com/amnezia/docs?m-path=/documentation/instructions/encryption") } } diff --git a/client/ui/qml/Pages2/PageSettingsAppPassword.qml b/client/ui/qml/Pages2/PageSettingsAppPassword.qml index b96e61bea..75d41a757 100644 --- a/client/ui/qml/Pages2/PageSettingsAppPassword.qml +++ b/client/ui/qml/Pages2/PageSettingsAppPassword.qml @@ -81,7 +81,7 @@ PageType { text: qsTr("Learn more") clickedFunc: function() { - // TODO: add link + Qt.openUrlExternally("https://storage.googleapis.com/amnezia/docs?m-path=/documentation/instructions/encryption") } } From f8c80c21c9d7df7eea01909a922c348371c39a76 Mon Sep 17 00:00:00 2001 From: MrMirDan Date: Tue, 23 Dec 2025 16:30:05 +0200 Subject: [PATCH 13/24] update: text --- client/ui/qml/Components/PasswordDrawer.qml | 1 - client/ui/qml/Pages2/PageSettingsApiNativeConfigs.qml | 2 +- client/ui/qml/Pages2/PageSettingsAppPassword.qml | 2 +- client/ui/qml/Pages2/PageSettingsAppPasswordConfirm.qml | 2 +- client/ui/qml/Pages2/PageShare.qml | 2 +- 5 files changed, 4 insertions(+), 5 deletions(-) diff --git a/client/ui/qml/Components/PasswordDrawer.qml b/client/ui/qml/Components/PasswordDrawer.qml index 55b8f4e9b..108f95e5a 100644 --- a/client/ui/qml/Components/PasswordDrawer.qml +++ b/client/ui/qml/Components/PasswordDrawer.qml @@ -40,7 +40,6 @@ DrawerType2 { } function onImportSecuredFile() { - // TODO: file name not showing on import root.securedFunc = function() { if (ImportController.extractConfigFromData(SystemController.getDecryptedData(fileName, passwordField.textField.text))) { PageController.goToPage(PageEnum.PageSetupWizardViewConfig) diff --git a/client/ui/qml/Pages2/PageSettingsApiNativeConfigs.qml b/client/ui/qml/Pages2/PageSettingsApiNativeConfigs.qml index ca69ef790..66eeae1a9 100644 --- a/client/ui/qml/Pages2/PageSettingsApiNativeConfigs.qml +++ b/client/ui/qml/Pages2/PageSettingsApiNativeConfigs.qml @@ -67,7 +67,7 @@ PageType { visible: SettingsController.isFileEncryptionEnabled() linkEnabled: true - textString: qsTr("Encryption enabled. Learn more") + textString: qsTr("Encryption enabled.") iconPath: "qrc:/images/controls/lock-locked.svg" } } diff --git a/client/ui/qml/Pages2/PageSettingsAppPassword.qml b/client/ui/qml/Pages2/PageSettingsAppPassword.qml index 75d41a757..95a03f108 100644 --- a/client/ui/qml/Pages2/PageSettingsAppPassword.qml +++ b/client/ui/qml/Pages2/PageSettingsAppPassword.qml @@ -199,7 +199,7 @@ PageType { QtObject { id: hintObject - property string title: qsTr("Hint for password") + property string title: root.isChangingPassword ? qsTr("New hint password (optional)") : qsTr("Password hint") readonly property string placeholderContent: "" property string imageSource: "" property bool hideContent: false diff --git a/client/ui/qml/Pages2/PageSettingsAppPasswordConfirm.qml b/client/ui/qml/Pages2/PageSettingsAppPasswordConfirm.qml index cd282f148..56c38f661 100644 --- a/client/ui/qml/Pages2/PageSettingsAppPasswordConfirm.qml +++ b/client/ui/qml/Pages2/PageSettingsAppPasswordConfirm.qml @@ -76,7 +76,7 @@ PageType { Layout.leftMargin: 16 Layout.rightMargin: 16 - headerText: root.isChangingPassword ? qsTr("Enter new password one more time") : qsTr("Enter password one more time") + headerText: root.isChangingPassword ? qsTr("Re-enter new password") : qsTr("Re-enter password") textField.echoMode: hideContent ? TextInput.Password : TextInput.Normal textField.placeholderText: "" textField.text: textField.text diff --git a/client/ui/qml/Pages2/PageShare.qml b/client/ui/qml/Pages2/PageShare.qml index 19253879b..15c3ca3c4 100644 --- a/client/ui/qml/Pages2/PageShare.qml +++ b/client/ui/qml/Pages2/PageShare.qml @@ -300,7 +300,7 @@ PageType { visible: SettingsController.isFileEncryptionEnabled() linkEnabled: true - textString: qsTr("Encryption enabled. Learn more") + textString: qsTr("Encryption enabled.") iconPath: "qrc:/images/controls/lock-locked.svg" } From 10de29217b824ba37fe157878d4b8cafa90bedc6 Mon Sep 17 00:00:00 2001 From: MrMirDan Date: Fri, 26 Dec 2025 12:00:30 +0200 Subject: [PATCH 14/24] update: text and RU translation --- client/translations/amneziavpn_ru_RU.ts | 538 +++++++++++++----- .../ui/qml/Pages2/PageSettingsAppPassword.qml | 2 +- .../Pages2/PageSettingsAppPasswordConfirm.qml | 5 +- client/ui/qml/Pages2/PageShareFullAccess.qml | 3 +- 4 files changed, 402 insertions(+), 146 deletions(-) diff --git a/client/translations/amneziavpn_ru_RU.ts b/client/translations/amneziavpn_ru_RU.ts index 5c9869306..f00af2afa 100644 --- a/client/translations/amneziavpn_ru_RU.ts +++ b/client/translations/amneziavpn_ru_RU.ts @@ -89,17 +89,17 @@ ApiConfigsController - + %1 installed successfully. %1 успешно установлен. - + API config reloaded Конфигурация API перезагружена - + Successfully changed the country of connection to %1 Страна подключения изменена на %1 @@ -352,6 +352,14 @@ Выбрать всё + + EncryptionIndicator + + + <a href="learnMore" style="text-decoration:none; color:%1">Learn more</a> + <a href="Подробнее" style="text-decoration:none; color:%1">Learn more</a> + + HomeContainersListView @@ -632,22 +640,27 @@ Thank you for staying with us! Логирование включено - + + Dev gateway enabled + + + + Split tunneling enabled Раздельное туннелирование включено - + Split tunneling disabled Раздельное туннелирование выключено - + VPN protocol VPN-протокол - + Servers Серверы @@ -1579,32 +1592,37 @@ Thank you for staying with us! Настройки - + Servers Серверы - + Connection Соединение - + Application Приложение - + + News & Notifications + + + + Backup Резервное копирование - + About AmneziaVPN Об AmneziaVPN - + Dev console Dev console @@ -1879,67 +1897,72 @@ Thank you for staying with us! Для настройки роутера или приложения AmneziaWG - + + Encryption enabled. + Шифрование включено. + + + The configuration needs to be reissued Необходимо заново скачать конфигурацию и добавить ее в приложение - + configuration file файл конфигурации - + Generate a new configuration file Создать новый файл конфигурации - + The previously created one will stop working Ранее созданный файл перестанет работать - + Revoke the current configuration file Отозвать текущий файл конфигурации - + Config file saved Файл конфигурации сохранен - + The config has been revoked Конфигурация была отозвана - + Generate a new %1 configuration file? Создать новый %1 файл конфигурации? - + Revoke the current %1 configuration file? Отозвать текущий %1 файл конфигурации? - + Your previous configuration file will no longer work, and it will not be possible to connect using it Ваш предыдущий файл конфигурации не будет работать, и вы больше не сможете использовать его для подключения - + Download Скачать - + Continue Продолжить - + Cancel Отменить @@ -2157,6 +2180,180 @@ Thank you for staying with us! Скопировано + + PageSettingsAppEncryption + + + Encryption enabled + Шифрование включено + + + + Encryption disabled + Шифрование выключено + + + + Password & Encryption + Пароль и шифрование + + + + Password protection for backups and configuration files. +Required to restore or import encrypted files. + Пароль используется для защиты резервных копий и файлов конфигурации. +Он потребуется при восстановлении или импорте зашифрованных файлов. + + + + Learn more + Подробнее + + + + Password set. Encryption enabled + Пароль задан. Шифрование включено + + + + Disable encryption + Выключить шифрование + + + + If the password is forgotten, it can be recovered. To reset the password, <a href="appSettings" style="text-decoration:none; color:%1;">settings must be reset</a>. +Encrypted files can only be opened with password used to encrypt them + Если забудете пароль, восстановить его нельзя. Чтобы сбросить пароль, придется <a href="appSettings" style="text-decoration:none; color:%1;">сбросить все настройки</a>. +Зашифрованные файлы открываются только тем паролем, которым были зашифрованы + + + + Change password + Измененить пароль + + + + PageSettingsAppPassword + + + Password & Encryption + Пароль и шифрование + + + + Change password + Изменение пароля + + + + Existing encrypted files will still require the old password. +The new password will be used for new encrypted files. + Ранее зашифрованные файлы по-прежнему будут требовать старый пароль. +Новый пароль будет использоваться для новых зашифрованных файлов + + + + Password protection for backups and configuration files. +Required to restore or import encrypted files. + Пароль используется для защиты резервных копий и файлов конфигурации. +Он потребуется при восстановлении или импорте зашифрованных файлов. + + + + Learn more + Подробнее + + + + Password not set. Encryption disabled + Пароль не задан. Шифрование выключено + + + + Continue + Продолжить + + + + Password cannot be empty + Пароль не может быть пустым + + + + Password too short + Пароль слишком короткий + + + + New password + Новый пароль + + + + Set encryption password + Придумайте пароль шифрования + + + + New password hint (optional) + Новая подсказка (необязательно) + + + + Password hint + Подсказка + + + + PageSettingsAppPasswordConfirm + + + Confirm new password + Введите новый пароль ещё раз + + + + Confirm password + Введите пароль ещё раз + + + + + + + + + If the password is forgotten, it cant be recovered. To reset the password, the app settings must be reset. +Encrypted files can only be opened with the password used to encrypt them + Если забудете пароль, восстановить его нельзя. Чтобы сбросить пароль, придется сбросить все настройки. +Зашифрованные файлы открываются только тем паролем, которым были зашифрованы + + + + Re-enter new password + Введите новый пароль ещё раз + + + + Re-enter password + Введите пароль ещё раз + + + + Save new password + Сохранить новый пароль + + + + Turn on encryption + Включить шифрование + + + + Passwords not match + Пароли не совпадают + + PageSettingsAppSplitTunneling @@ -2279,46 +2476,56 @@ Thank you for staying with us! + Password & Encryption + Пароль и шифрование + + + + Password protection for backups and configuration files + Защита паролем резервных файлов и файлов конфигурации + + + Logging Логирование - + Enabled Включено - + Disabled Отключено - + Reset settings and remove all data from the application Сбросить настройки и удалить все данные из приложения - + Reset settings and remove all data from the application? Сбросить настройки и удалить все данные из приложения? - + All settings will be reset to default. All installed AmneziaVPN services will still remain on the server. Все настройки будут сброшены до значений по умолчанию. Все установленные сервисы AmneziaVPN останутся на сервере. - + Continue Продолжить - + Cancel Отменить - + Cannot reset settings during active connection Невозможно сбросить настройки во время активного соединения @@ -2341,63 +2548,68 @@ Thank you for staying with us! Вы можете сохранить настройки в файл резервной копии, чтобы восстановить их при следующей установке приложения. - + + Encryption enabled. + Шифрование включено. + + + The backup will contain your passwords and private keys for all servers added to AmneziaVPN. Keep this information in a secure place. Резервная копия будет содержать ваши пароли и закрытые ключи для всех серверов, добавленных в AmneziaVPN. Храните эту информацию в надежном месте. - + Make a backup Создать резервную копию - + Save backup file Сохранить резервную копию - - + + Backup files (*.backup) Файлы резервных копий (*.backup) - + Backup file saved Резервная копия сохранена - + Restore from backup Восстановить из резервной копии - + Open backup file Открыть резервную копию - + Import settings from a backup file? Импортировать настройки из резервной копии? - + All current settings will be reset Все текущие настройки будут сброшены - + Continue Продолжить - + Cancel Отменить - + Cannot restore backup settings during active connection Невозможно восстановить настройки из резервной копии во время активного соединения @@ -2679,11 +2891,6 @@ Thank you for staying with us! Logging Логирование - - - Enabling this function will save application's logs automatically. By default, logging functionality is disabled. Enable log saving in case of application malfunction. - Включение этой функции позволяет сохранять логи на вашем устройстве. По умолчанию она отключена. Включите сохранение логов в случае сбоев в работе приложения. - @@ -2702,10 +2909,20 @@ Thank you for staying with us! Logs file saved Файл с логами сохранен + + + Logs help diagnose app errors and connection issuesLogging is disabled by default. Enable it when troubleshooting or if requested by support + + - Enable logs - Включить запись логов + Enable logging + + + + + Delete all logs + @@ -2727,10 +2944,15 @@ Thank you for staying with us! Logs have been cleaned up Логи очищены + + + Save logs to file + + - Client logs - Логи приложения + App logs + @@ -2742,11 +2964,6 @@ Thank you for staying with us! Open logs folder Открыть папку с логами - - - Export logs - Сохранить логи - Service logs @@ -2757,10 +2974,13 @@ Thank you for staying with us! AmneziaVPN-service logs AmneziaVPN-service logs + + + PageSettingsNewsNotifications - - Clear logs - Очистить логи + + News & Notifications + @@ -3012,13 +3232,13 @@ Thank you for staying with us! - + Continue Продолжить - + Cancel Отменить @@ -3059,8 +3279,8 @@ Thank you for staying with us! - - + + Sites files (*.json) Файлы сайтов (*.json) @@ -3070,33 +3290,33 @@ Thank you for staying with us! Очистить список сайтов - + Clear site list? Очистить список сайтов? - + All sites will be removed from list. Все сайты будут удалены из списка. - + Import a list of sites Импортировать список с сайтами - + Replace site list Заменить список с сайтами - - + + Open sites file Открыть список с сайтами - + Add imported sites to existing ones Добавить импортированные сайты к существующим @@ -3150,140 +3370,140 @@ Thank you for staying with us! PageSetupWizardConfigSource - + File with connection settings Файл с настройками подключения - + Connection Соединение - + Settings Настройки - + Enable logs Включить запись логов - + Export client logs Экспорт логов клиента - + Save Сохранить - + Logs files (*.log) Файлы логов (*.log) - + Logs file saved Файл с логами сохранен - + Support tag Support tag - + Copied Скопировано - + Insert the key, add a configuration file or scan the QR-code Вставьте ключ, добавьте файл конфигурации или отсканируйте QR-код - + Insert key Вставьте ключ - + Insert Вставить - + Continue Продолжить - + Other connection options Другие варианты подключения - + Site Amnezia Сайт Amnezia - + VPN by Amnezia VPN от Amnezia - + Connect to classic paid and free VPN services from Amnezia Подключайтесь к классическим платным и бесплатным VPN-сервисам от Amnezia - + Self-hosted VPN Self-hosted VPN - + Configure Amnezia VPN on your own server Настроить VPN на собственном сервере - + Restore from backup Восстановить из резервной копии - - - - + + + + - + Open backup file Открыть резервную копию - + Backup files (*.backup) Файлы резервных копий (*.backup) - + Open config file Открыть файл с конфигурацией - + QR code QR-код - + I have nothing У меня ничего нет @@ -3521,32 +3741,32 @@ Thank you for staying with us! PageSetupWizardViewConfig - + New connection Новое соединение - + Collapse content Свернуть - + Show content Показать - + Enable WireGuard obfuscation. It may be useful if WireGuard is blocked on your provider. Включить обфускацию WireGuard. Это может быть полезно, если WireGuard блокируется вашим провайдером. - + Use connection codes only from sources you trust. Codes from public sources may have been created to intercept your data. Используйте файлы конфигурации только из тех источников, которым вы доверяете. Файлы из общедоступных источников могли быть созданы с целью перехвата ваших личных данных. - + Connect Подключиться @@ -3569,8 +3789,13 @@ Thank you for staying with us! Соединение - - + + Encryption enabled. + Шифрование включено. + + + + Server Сервер @@ -3666,82 +3891,82 @@ Thank you for staying with us! - + Users Пользователи - + User name Имя пользователя - + Search Поиск - + Creation date: %1 Дата создания: %1 - + Latest handshake: %1 Последнее рукопожатие: %1 - + Data received: %1 Получено данных: %1 - + Data sent: %1 Отправлено данных: %1 - + Allowed IPs: %1 Разрешенные подсети: %1 - + Rename Переименовать - + Client name Имя клиента - + Save Сохранить - + Revoke Отозвать - + Revoke the config for a user - %1? Отозвать конфигурацию для пользователя - %1? - + The user will no longer be able to connect to your server. Пользователь больше не сможет подключаться к вашему серверу. - + Continue Продолжить - + Cancel Отменить @@ -3751,20 +3976,20 @@ Thank you for staying with us! Поделиться доступом к VPN без возможности управления сервером - - + + Protocol Протокол - - + + Connection format Формат подключения - + Share Поделиться @@ -3828,28 +4053,33 @@ Thank you for staying with us! Если вы поделитесь полным доступом с другими людьми, то они смогут удалять и добавлять протоколы и сервисы на сервер, что приведет к некорректной работе VPN для всех пользователей. - - + + Encryption enabled. + Шифрование включено. + + + + Server Сервер - + Accessing Доступ - + File with accessing settings to Файл с настройками доступа к - + Share Поделиться - + Access error! Ошибка доступа! @@ -3872,6 +4102,30 @@ Thank you for staying with us! Логирование включено. Обратите внимание, что через 14 дней оно будет автоматически отключено, а все файлы логов будут удалены. + + PasswordDrawer + + + Password required + Для продолжения введите пароль + + + + Password + Пароль + + + + Done + Готово + + + + + Invalid password + Неверный пароль + + PopupType @@ -4950,12 +5204,12 @@ FileZilla или другие SFTP-клиенты, а также смонтир SettingsController - + All settings have been reset to default values Все настройки сброшены до значений по умолчанию - + Backup file is corrupted Файл резервной копии поврежден diff --git a/client/ui/qml/Pages2/PageSettingsAppPassword.qml b/client/ui/qml/Pages2/PageSettingsAppPassword.qml index 95a03f108..d8a713376 100644 --- a/client/ui/qml/Pages2/PageSettingsAppPassword.qml +++ b/client/ui/qml/Pages2/PageSettingsAppPassword.qml @@ -199,7 +199,7 @@ PageType { QtObject { id: hintObject - property string title: root.isChangingPassword ? qsTr("New hint password (optional)") : qsTr("Password hint") + property string title: root.isChangingPassword ? qsTr("New password hint (optional)") : qsTr("Password hint") readonly property string placeholderContent: "" property string imageSource: "" property bool hideContent: false diff --git a/client/ui/qml/Pages2/PageSettingsAppPasswordConfirm.qml b/client/ui/qml/Pages2/PageSettingsAppPasswordConfirm.qml index 56c38f661..f068bd1a9 100644 --- a/client/ui/qml/Pages2/PageSettingsAppPasswordConfirm.qml +++ b/client/ui/qml/Pages2/PageSettingsAppPasswordConfirm.qml @@ -55,8 +55,9 @@ PageType { Layout.rightMargin: 16 headerText: root.isChangingPassword ? qsTr("Confirm new password") : qsTr("Confirm password") - descriptionText: root.isChangingPassword ? qsTr("") : qsTr("If you forget your password, you'll have to reset all app settings to reset it." - + " Encrypted files will remain encrypted") + descriptionText: root.isChangingPassword ? qsTr("") : qsTr("If the password is forgotten, it cant be recovered. " + + "To reset the password, the app settings must be reset.\n" + + "Encrypted files can only be opened with the password used to encrypt them") } } diff --git a/client/ui/qml/Pages2/PageShareFullAccess.qml b/client/ui/qml/Pages2/PageShareFullAccess.qml index 7f404eba2..746f3d242 100644 --- a/client/ui/qml/Pages2/PageShareFullAccess.qml +++ b/client/ui/qml/Pages2/PageShareFullAccess.qml @@ -73,8 +73,9 @@ PageType { id: indicator visible: SettingsController.isFileEncryptionEnabled() + linkEnabled: true - textString: qsTr("Encryption enabled. Learn more") + textString: qsTr("Encryption enabled.") iconPath: "qrc:/images/controls/lock-locked.svg" } From 91307cf49a74f557f4b18ea12526aedb73e89cc7 Mon Sep 17 00:00:00 2001 From: MrMirDan Date: Fri, 13 Mar 2026 13:30:03 +0200 Subject: [PATCH 15/24] update: systemController --- client/ui/controllers/systemController.cpp | 27 ++++++++++++---------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/client/ui/controllers/systemController.cpp b/client/ui/controllers/systemController.cpp index 70c639671..4a7961277 100644 --- a/client/ui/controllers/systemController.cpp +++ b/client/ui/controllers/systemController.cpp @@ -128,38 +128,38 @@ static bool deriveKey(const QByteArray &password, const QByteArray &salt, QByteA static bool aesCrypt(const QByteArray &in, const QByteArray &key, const QByteArray &iv, QByteArray &out, bool encrypt) { - EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new(); + std::unique_ptr ctx { EVP_CIPHER_CTX_new(), EVP_CIPHER_CTX_free }; + if (!ctx) { qDebug() << "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 (1 != EVP_CipherInit_ex(ctx.get(), cipher, nullptr, reinterpret_cast(key.constData()), + reinterpret_cast(iv.constData()), encrypt ? 1 : 0)) { qDebug() << 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 (1 != EVP_CipherUpdate(ctx.get(), reinterpret_cast(out.data()), &outlen1, + reinterpret_cast(in.constData()), in.size())) { qDebug() << opensslErrString(); - EVP_CIPHER_CTX_free(ctx); return false; } + int outlen2 = 0; - if (1 != EVP_CipherFinal_ex(ctx, reinterpret_cast(out.data()) + outlen1, &outlen2)) { + if (1 != EVP_CipherFinal_ex(ctx.get(), reinterpret_cast(out.data()) + outlen1, &outlen2)) { qDebug() << opensslErrString(); - EVP_CIPHER_CTX_free(ctx); return false; } + out.resize(outlen1 + outlen2); - EVP_CIPHER_CTX_free(ctx); return true; } @@ -304,6 +304,9 @@ bool SystemController::isPasswordValid(const QString &filePath, const QString &p QString SystemController::readHint(const QString &filePath) { + if (filePath.isEmpty()) + return ""; + QByteArray data; readFile(filePath, data); From dbce2f796cbe9ab71f7e4af9dc684aa28e14fb76 Mon Sep 17 00:00:00 2001 From: MrMirDan Date: Fri, 13 Mar 2026 13:30:42 +0200 Subject: [PATCH 16/24] update: app password and encription pages margins --- client/ui/qml/Pages2/PageSettingsAppEncryption.qml | 4 ++-- client/ui/qml/Pages2/PageSettingsAppPassword.qml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ui/qml/Pages2/PageSettingsAppEncryption.qml b/client/ui/qml/Pages2/PageSettingsAppEncryption.qml index d35b3506b..26cc90e97 100644 --- a/client/ui/qml/Pages2/PageSettingsAppEncryption.qml +++ b/client/ui/qml/Pages2/PageSettingsAppEncryption.qml @@ -72,7 +72,7 @@ PageType { BasicButtonType { Layout.leftMargin: 8 - Layout.bottomMargin: 16 + Layout.bottomMargin: 32 implicitHeight: 16 defaultColor: AmneziaStyle.color.transparent @@ -166,7 +166,7 @@ PageType { width: listView.width CaptionTextType { Layout.fillWidth: true - Layout.topMargin: 16 + Layout.topMargin: 32 horizontalAlignment: Text.AlignHCenter textFormat: Text.RichText diff --git a/client/ui/qml/Pages2/PageSettingsAppPassword.qml b/client/ui/qml/Pages2/PageSettingsAppPassword.qml index d8a713376..90ee1f238 100644 --- a/client/ui/qml/Pages2/PageSettingsAppPassword.qml +++ b/client/ui/qml/Pages2/PageSettingsAppPassword.qml @@ -69,7 +69,7 @@ PageType { BasicButtonType { Layout.leftMargin: 8 - Layout.bottomMargin: 16 + Layout.bottomMargin: 32 implicitHeight: 16 defaultColor: AmneziaStyle.color.transparent From bfcb7bf979657c43b393c7e8fb06df3d3f2c8d10 Mon Sep 17 00:00:00 2001 From: MrMirDan Date: Tue, 17 Mar 2026 16:15:53 +0200 Subject: [PATCH 17/24] update: changed encryption to aes-256-gcm --- client/ui/controllers/systemController.cpp | 182 +++++++++++++++------ 1 file changed, 133 insertions(+), 49 deletions(-) diff --git a/client/ui/controllers/systemController.cpp b/client/ui/controllers/systemController.cpp index 4a7961277..07a9be141 100644 --- a/client/ui/controllers/systemController.cpp +++ b/client/ui/controllers/systemController.cpp @@ -21,6 +21,9 @@ namespace constexpr int KEY_LEN = 32; constexpr int PBKDF2_ITER = 100000; + constexpr int IV_LEN_GCM = 16; + constexpr int TAG_LEN = 16; + const QByteArray magicString { "EncData" }; } @@ -126,7 +129,8 @@ static bool deriveKey(const QByteArray &password, const QByteArray &salt, QByteA return ok == 1; } -static bool aesCrypt(const QByteArray &in, const QByteArray &key, const QByteArray &iv, QByteArray &out, bool encrypt) +static bool aesCrypt(const QByteArray &in, const QByteArray &key, const QByteArray &iv, QByteArray &out, + QByteArray &tag, bool encrypt) { std::unique_ptr ctx { EVP_CIPHER_CTX_new(), EVP_CIPHER_CTX_free }; @@ -135,31 +139,64 @@ static bool aesCrypt(const QByteArray &in, const QByteArray &key, const QByteArr return false; } - const EVP_CIPHER *cipher = EVP_aes_256_cbc(); + const EVP_CIPHER *cipher = EVP_aes_256_gcm(); - if (1 != EVP_CipherInit_ex(ctx.get(), cipher, nullptr, reinterpret_cast(key.constData()), - reinterpret_cast(iv.constData()), encrypt ? 1 : 0)) { + if (1 != EVP_CipherInit_ex(ctx.get(), cipher, nullptr, nullptr, nullptr, encrypt ? 1 : 0)) { + qDebug() << opensslErrString(); + return false; + } + + if (1 != EVP_CIPHER_CTX_ctrl(ctx.get(), EVP_CTRL_GCM_SET_IVLEN, iv.size(), nullptr)) { + qDebug() << opensslErrString(); + return false; + } + + if (1 != EVP_CipherInit_ex(ctx.get(), nullptr, nullptr, reinterpret_cast(key.constData()), + reinterpret_cast(iv.constData()), -1)) { qDebug() << opensslErrString(); return false; } out.clear(); - out.resize(in.size() + EVP_CIPHER_block_size(cipher)); + out.resize(in.size()); - int outlen1 = 0; - if (1 != EVP_CipherUpdate(ctx.get(), reinterpret_cast(out.data()), &outlen1, - reinterpret_cast(in.constData()), in.size())) { + int outlen = 0; + + if (1 != EVP_CipherUpdate(ctx.get(), reinterpret_cast(out.data()), &outlen, + reinterpret_cast(in.constData()), in.size())) { qDebug() << opensslErrString(); return false; } - int outlen2 = 0; - if (1 != EVP_CipherFinal_ex(ctx.get(), reinterpret_cast(out.data()) + outlen1, &outlen2)) { - qDebug() << opensslErrString(); - return false; + int tmplen = 0; + + if (encrypt) { + if (1 != EVP_CipherFinal_ex(ctx.get(), reinterpret_cast(out.data()) + outlen, &tmplen)) { + qDebug() << opensslErrString(); + return false; + } + + out.resize(outlen + tmplen); + + tag.resize(TAG_LEN); + if (1 != EVP_CIPHER_CTX_ctrl(ctx.get(), EVP_CTRL_GCM_GET_TAG, TAG_LEN, tag.data())) { + qDebug() << opensslErrString(); + return false; + } + } else { + if (1 != EVP_CIPHER_CTX_ctrl(ctx.get(), EVP_CTRL_GCM_SET_TAG, tag.size(), const_cast(tag.constData()))) { + qDebug() << opensslErrString(); + return false; + } + + if (1 != EVP_CipherFinal_ex(ctx.get(), reinterpret_cast(out.data()) + outlen, &tmplen)) { + qDebug() << "Authentication failed: " << opensslErrString(); + return false; + } + + out.resize(outlen + tmplen); } - out.resize(outlen1 + outlen2); return true; } @@ -167,24 +204,23 @@ bool SystemController::encryptFile(const QString &filePath, const QString &passw { QFile f(filePath); if (!f.open(QIODevice::ReadOnly)) { - qDebug() << QStringLiteral("Cannot open file for read: %1").arg(f.errorString()); + qDebug() << "Cannot open file for read: " << f.errorString(); return false; } + QByteArray content = f.readAll(); f.close(); if (content.startsWith(magicString)) { - qDebug() << QStringLiteral("File already encrypted (magic found)"); + qDebug() << "File already encrypted"; return false; } - QByteArray qba_hint = hint.toUtf8(); - quint32 qba_hint_len = static_cast(qba_hint.size()); QByteArray salt(SALT_LEN, 0); QByteArray iv(IV_LEN, 0); QByteArray key; QByteArray cipher; - QByteArray out; + QByteArray tag; if (1 != RAND_bytes(reinterpret_cast(salt.data()), SALT_LEN) || 1 != RAND_bytes(reinterpret_cast(iv.data()), IV_LEN)) { @@ -195,55 +231,94 @@ bool SystemController::encryptFile(const QString &filePath, const QString &passw if (!deriveKey(password.toUtf8(), salt, key)) return false; - if (!aesCrypt(content, key, iv, cipher, true)) + if (!aesCrypt(content, key, iv, cipher, tag, true)) return false; - out.reserve(magicString.size() + SALT_LEN + IV_LEN + cipher.size()); + QByteArray out; + QByteArray hintBytes = hint.toUtf8(); + quint32 hintLen = static_cast(hintBytes.size()); + out += magicString; - out.append(reinterpret_cast(&qba_hint_len), sizeof(qba_hint_len)); - out += hint.toUtf8(); + out.append(reinterpret_cast(&hintLen), sizeof(hintLen)); + out += hintBytes; out += salt; out += iv; + out += tag; out += cipher; if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate)) { - qDebug() << QStringLiteral("Cannot open file for write: %1").arg(f.errorString()); + qDebug() << "Cannot open file for write: " << f.errorString(); return false; } - qint64 written = f.write(out); + + if (f.write(out) != out.size()) { + qDebug() << "Write failed"; + f.close(); + return false; + } + f.close(); - if (written != out.size()) { - qDebug() << QStringLiteral("Write failed or incomplete"); - return false; - } return true; } QByteArray SystemController::getDecryptedData(const QString &filePath, const QString &password) { - QByteArray encData; - readFile(filePath, encData); + QFile f(filePath); + if (!f.open(QIODevice::ReadOnly)) { + qDebug() << "Cannot open file: " << f.errorString(); + return {}; + } + + QByteArray content = f.readAll(); + f.close(); + + if (!content.startsWith(magicString)) { + qDebug() << "Invalid file format (magic missing)"; + return {}; + } int pos = magicString.size(); + if (content.size() < pos + static_cast(sizeof(quint32))) { + qDebug() << "Corrupted file (no hint length)"; + return {}; + } + quint32 hintLen = 0; - memcpy(&hintLen, encData.constData() + pos, sizeof(quint32)); + memcpy(&hintLen, content.constData() + pos, sizeof(quint32)); pos += sizeof(quint32); + + if (content.size() < pos + static_cast(hintLen) + SALT_LEN + IV_LEN + TAG_LEN) { + qDebug() << "Corrupted file (invalid sizes)"; + return {}; + } + pos += hintLen; - QByteArray salt = encData.mid(pos, 16); - pos += 16; - QByteArray iv = encData.mid(pos, 16); - pos += 16; - QByteArray cipher = encData.mid(pos); + QByteArray salt = content.mid(pos, SALT_LEN); + pos += SALT_LEN; + + QByteArray iv = content.mid(pos, IV_LEN); + pos += IV_LEN; + + QByteArray tag = content.mid(pos, TAG_LEN); + pos += TAG_LEN; + + QByteArray cipher = content.mid(pos); QByteArray key; - deriveKey(password.toUtf8(), salt, key); + if (!deriveKey(password.toUtf8(), salt, key)) { + qDebug() << "Key derivation failed"; + return {}; + } - QByteArray data; - !aesCrypt(cipher, key, iv, data, false); + QByteArray plain; + if (!aesCrypt(cipher, key, iv, plain, tag, false)) { + qDebug() << "Decryption failed (wrong password or corrupted data)"; + return {}; + } - return data; + return plain; } bool SystemController::isFileEncrypted(const QString &filePath) @@ -275,16 +350,22 @@ bool SystemController::isPasswordValid(const QString &filePath, const QString &p QByteArray content = f.readAll(); f.close(); + if (!content.startsWith(magicString)) + return false; + int pos = magicString.size(); + quint32 hintLen = 0; memcpy(&hintLen, content.constData() + pos, sizeof(quint32)); pos += sizeof(quint32); pos += hintLen; - QByteArray salt = content.mid(pos, 16); - pos += 16; - QByteArray iv = content.mid(pos, 16); - pos += 16; + QByteArray salt = content.mid(pos, SALT_LEN); + pos += SALT_LEN; + QByteArray iv = content.mid(pos, IV_LEN); + pos += IV_LEN; + QByteArray tag = content.mid(pos, TAG_LEN); + pos += TAG_LEN; QByteArray cipher = content.mid(pos); QByteArray key; @@ -292,14 +373,12 @@ bool SystemController::isPasswordValid(const QString &filePath, const QString &p return false; QByteArray plain; - bool ok = aesCrypt(cipher, key, iv, plain, false); + bool ok = aesCrypt(cipher, key, iv, plain, tag, false); - if (!ok) { + if (!ok) qDebug() << "Wrong password"; - return false; - } - return true; + return ok; } QString SystemController::readHint(const QString &filePath) @@ -310,6 +389,11 @@ QString SystemController::readHint(const QString &filePath) QByteArray data; readFile(filePath, data); + if (!data.startsWith(magicString)) { + qDebug() << "Not an encrypted file"; + return {}; + } + int pos = magicString.size(); if (data.size() < pos + static_cast(sizeof(quint32))) { From ae1bbb2f88a1409e11c6c6952fec0fb9d517361f Mon Sep 17 00:00:00 2001 From: MrMirDan Date: Thu, 19 Mar 2026 14:01:46 +0200 Subject: [PATCH 18/24] fix: restoring from encrypted backup --- client/ui/controllers/systemController.cpp | 6 ++---- client/ui/qml/Pages2/PageSettingsBackup.qml | 6 +----- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/client/ui/controllers/systemController.cpp b/client/ui/controllers/systemController.cpp index 07a9be141..fec30ead1 100644 --- a/client/ui/controllers/systemController.cpp +++ b/client/ui/controllers/systemController.cpp @@ -17,12 +17,10 @@ namespace { constexpr int SALT_LEN = 16; - constexpr int IV_LEN = 16; + constexpr int IV_LEN = 12; constexpr int KEY_LEN = 32; - constexpr int PBKDF2_ITER = 100000; - - constexpr int IV_LEN_GCM = 16; constexpr int TAG_LEN = 16; + constexpr int PBKDF2_ITER = 100000; const QByteArray magicString { "EncData" }; } diff --git a/client/ui/qml/Pages2/PageSettingsBackup.qml b/client/ui/qml/Pages2/PageSettingsBackup.qml index 563c402a0..26f5e1eb1 100644 --- a/client/ui/qml/Pages2/PageSettingsBackup.qml +++ b/client/ui/qml/Pages2/PageSettingsBackup.qml @@ -155,7 +155,7 @@ PageType { qsTr("Backup files (*.backup)")) if (filePath !== "") { passwordDrawer.fileName = filePath - SystemController.isFileEncrypted(filePath) ? passwordDrawer.openTriggered() : restoreBackup(filePath) + SystemController.isFileEncrypted(filePath) ? passwordDrawer.restoreSecuredBackup() : restoreBackup(filePath) } } } @@ -167,10 +167,6 @@ PageType { anchors.fill: parent expandedHeight: root.height * 0.45 - - securedFunc: function() { - passwordDrawer.restoreSecuredBackup() - } } } } From 18b8f50d213cdf7b2b3527a29a064736b99842b5 Mon Sep 17 00:00:00 2001 From: MrMirDan Date: Fri, 20 Mar 2026 15:21:37 +0200 Subject: [PATCH 19/24] fix: Sec/ field added to backup --- client/secure_qsettings.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/secure_qsettings.h b/client/secure_qsettings.h index 8878e1d56..eef057d98 100644 --- a/client/secure_qsettings.h +++ b/client/secure_qsettings.h @@ -45,7 +45,7 @@ private: QStringList encryptedKeys; // encode only key listed here // only this fields need for backup QStringList m_fieldsToBackup = { - "Conf/", "Servers/", + "Conf/", "Servers/", "Sec/", }; mutable QByteArray m_key; From 465b3d4d95d5459b8825f6d9c50c36f261c7adf0 Mon Sep 17 00:00:00 2001 From: MrMirDan Date: Fri, 20 Mar 2026 15:37:51 +0200 Subject: [PATCH 20/24] fix: value and setValue usage --- client/settings.h | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/client/settings.h b/client/settings.h index b0b292195..d4285da30 100644 --- a/client/settings.h +++ b/client/settings.h @@ -96,29 +96,29 @@ public: bool isFileEncryption() const { - return value("Sec/fileEncryption", false).toBool(); + return m_settings.value("Sec/fileEncryption", false).toBool(); } void setFileEncryption(bool enabled) { - setValue("Sec/fileEncryption", enabled); + m_settings.setValue("Sec/fileEncryption", enabled); } QString getPassword() const { - return value("Sec/password", "").toString(); + return m_settings.value("Sec/password", "").toString(); } void setPassword(const QString &pwd) { - setValue("Sec/password", pwd); + m_settings.setValue("Sec/password", pwd); } QString getHint() const { - return value("Sec/hint", "").toString(); + return m_settings.value("Sec/hint", "").toString(); } void setHint(const QString &hint) { - setValue("Sec/hint", hint); + m_settings.setValue("Sec/hint", hint); } bool isNewsNotifications() const From 50801dd559ada05b37fb571a3bc13dfdc41766cb Mon Sep 17 00:00:00 2001 From: MrMirDan Date: Mon, 23 Mar 2026 14:05:06 +0200 Subject: [PATCH 21/24] update: hint text --- client/ui/qml/Components/PasswordDrawer.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ui/qml/Components/PasswordDrawer.qml b/client/ui/qml/Components/PasswordDrawer.qml index 108f95e5a..eba932aa6 100644 --- a/client/ui/qml/Components/PasswordDrawer.qml +++ b/client/ui/qml/Components/PasswordDrawer.qml @@ -96,7 +96,7 @@ DrawerType2 { Layout.topMargin: 8 Layout.bottomMargin: 16 - text: fromOutside ? SystemController.readHint(fileName) : SettingsController.getHint() + text: qsTr("Hint: ") + (fromOutside ? SystemController.readHint(fileName) : SettingsController.getHint()) } BasicButtonType { From 3ddd38f58ee20df4ecd678a5a12fae61d9f88bc7 Mon Sep 17 00:00:00 2001 From: MrMirDan Date: Tue, 24 Mar 2026 13:12:03 +0200 Subject: [PATCH 22/24] fix: textField image become bigger when error text visible --- client/ui/qml/Controls2/TextFieldWithHeaderType.qml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/client/ui/qml/Controls2/TextFieldWithHeaderType.qml b/client/ui/qml/Controls2/TextFieldWithHeaderType.qml index 897584303..006cb0ef7 100644 --- a/client/ui/qml/Controls2/TextFieldWithHeaderType.qml +++ b/client/ui/qml/Controls2/TextFieldWithHeaderType.qml @@ -188,11 +188,10 @@ Item { leftImageSource: root.buttonImageSource anchors.top: content.top - anchors.bottom: content.bottom anchors.right: content.right - height: content.implicitHeight - width: content.implicitHeight + height: root.errorText !== "" ? content.implicitHeight - errorField.height - 5: content.implicitHeight + width: root.errorText !== "" ? content.implicitHeight - errorField.height - 5: content.implicitHeight squareLeftSide: true clickedFunc: function() { From d5616615d37cc40c08d1c69eec73ece97afb93cd Mon Sep 17 00:00:00 2001 From: MrMirDan Date: Tue, 24 Mar 2026 16:05:44 +0200 Subject: [PATCH 23/24] update: remade EncryptionIndicator --- .../ui/qml/Components/EncryptionIndicator.qml | 33 +++++++++++++++---- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/client/ui/qml/Components/EncryptionIndicator.qml b/client/ui/qml/Components/EncryptionIndicator.qml index 704688319..a8654643c 100644 --- a/client/ui/qml/Components/EncryptionIndicator.qml +++ b/client/ui/qml/Components/EncryptionIndicator.qml @@ -51,16 +51,35 @@ Rectangle { Layout.fillWidth: true Layout.leftMargin: 8 - text: root.linkEnabled ? root.textString - + qsTr(" Learn more").arg(AmneziaStyle.color.goldenApricot) - : root.textString + text: root.textString textFormat: Text.RichText color: root.textColor + } - onLinkActivated: function(link) { - if (link === "learnMore") { - Qt.openUrlExternally("https://storage.googleapis.com/amnezia/docs?m-path=/documentation/instructions/encryption") - } + BasicButtonType { + visible: root.linkEnabled + + hoverEnabled: false + + implicitHeight: root.iconHeight + implicitWidth: linkText.width/2 + 8 + + defaultColor: AmneziaStyle.color.transparent + + CaptionTextType { + id: linkText + + leftPadding: 4 + + width: linkText.text.length * linkText.font.pixelSize + + text: qsTr("Learn more") + textFormat: Text.RichText + color: AmneziaStyle.color.goldenApricot + } + + clickedFunc: function() { + Qt.openUrlExternally("https://storage.googleapis.com/amnezia/docs?m-path=/documentation/instructions/encryption") } } } From 6f3c372c9d5939932651e8bd9ae8ad6058541eab Mon Sep 17 00:00:00 2001 From: MrMirDan Date: Wed, 25 Mar 2026 11:08:51 +0200 Subject: [PATCH 24/24] update: added "Hint" text to password confirm page --- client/ui/qml/Pages2/PageSettingsAppPasswordConfirm.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ui/qml/Pages2/PageSettingsAppPasswordConfirm.qml b/client/ui/qml/Pages2/PageSettingsAppPasswordConfirm.qml index f068bd1a9..6f213e601 100644 --- a/client/ui/qml/Pages2/PageSettingsAppPasswordConfirm.qml +++ b/client/ui/qml/Pages2/PageSettingsAppPasswordConfirm.qml @@ -109,7 +109,7 @@ PageType { Layout.rightMargin: 16 Layout.bottomMargin: 24 - text: SettingsController.getTempHint() + text: qsTr("Hint: ") + SettingsController.getTempHint() } BasicButtonType {