Compare commits

...

54 Commits

Author SHA1 Message Date
MrMirDan 1c291bba0f fix: fixed file encryption invoke 2026-05-25 12:13:49 +03:00
MrMirDan 7ba9013b66 Merge branch 'dev' into feat/file-password-protection 2026-05-25 11:49:14 +03:00
MrMirDan 09a0c933fb Merge branch 'feat/file-password-protection' of https://github.com/amnezia-vpn/amnezia-client into feat/file-password-protection 2026-05-19 16:24:34 +03:00
MrMirDan 14ee66cc58 some fixes 2026-05-19 16:22:22 +03:00
MrMirDan 798d51de34 update: added "Hint" text to password confirm page 2026-05-19 15:21:33 +03:00
MrMirDan d3ee50ea6f update: remade EncryptionIndicator 2026-05-19 15:21:33 +03:00
MrMirDan e095b21911 fix: textField image become bigger when error text visible 2026-05-19 15:21:33 +03:00
MrMirDan 7decbb5a48 update: hint text 2026-05-19 15:21:33 +03:00
MrMirDan 3e240fc9ac fix: Sec/ field added to backup 2026-05-19 15:20:07 +03:00
MrMirDan 161a1eb647 fix: restoring from encrypted backup 2026-05-19 15:20:07 +03:00
MrMirDan a1a13b4428 update: changed encryption to aes-256-gcm 2026-05-19 15:20:07 +03:00
MrMirDan 4675084658 update: app password and encription pages margins 2026-05-19 15:20:07 +03:00
MrMirDan aa37ed1d5e update: systemController 2026-05-19 15:20:07 +03:00
MrMirDan b3e72aecd7 update: text and RU translation 2026-05-19 15:20:07 +03:00
MrMirDan 9108cc6dc8 update: text 2026-05-19 15:19:33 +03:00
MrMirDan dd511de97d updated: added encryption docs links 2026-05-19 15:18:54 +03:00
MrMirDan ecc94ef48a update: text and some fixes 2026-05-19 15:18:53 +03:00
MrMirDan 10abaf5e33 fix: correctly save password on set or change 2026-05-19 15:16:26 +03:00
MrMirDan 6426516365 update: added encryption indicator on backup, share and api configs pages 2026-05-19 15:11:15 +03:00
MrMirDan d0bd28defb update: logging page text 2026-05-19 15:10:06 +03:00
MrMirDan 4ae3bac65a update: files encrypt on export and files data decrypt on import 2026-05-19 15:10:06 +03:00
MrMirDan ba580891cd update: encryption/decryption logic, backup encryption on save and decryption on restore 2026-05-19 14:45:05 +03:00
MrMirDan a7ae0bc65e update: ui to set/change password and hint 2026-05-19 14:22:40 +03:00
MrMirDan fca062ba3c update: changed password and hint saving 2026-05-19 14:12:45 +03:00
MrMirDan a8f084f8ef update: changed logic, added to settingsController 2026-05-19 14:09:23 +03:00
MrMirDan 0b91219c51 removed unnecessary method 2026-05-19 13:22:21 +03:00
MrMirDan d3c12b95ec feat: file password protection v1 2026-05-19 13:22:20 +03:00
MrMirDan 6f3c372c9d update: added "Hint" text to password confirm page 2026-03-25 11:08:51 +02:00
MrMirDan d5616615d3 update: remade EncryptionIndicator 2026-03-24 16:05:44 +02:00
MrMirDan 3ddd38f58e fix: textField image become bigger when error text visible 2026-03-24 13:12:03 +02:00
MrMirDan 50801dd559 update: hint text 2026-03-23 14:05:06 +02:00
MrMirDan 465b3d4d95 fix: value and setValue usage 2026-03-20 15:37:51 +02:00
MrMirDan 23de0c7ed1 Merge branch 'feat/file-password-protection' of https://github.com/amnezia-vpn/amnezia-client into feat/file-password-protection 2026-03-20 15:22:04 +02:00
MrMirDan 18b8f50d21 fix: Sec/ field added to backup 2026-03-20 15:21:37 +02:00
MrMirDan 23dc5e7999 Merge branch 'dev' into feat/file-password-protection 2026-03-19 14:07:27 +02:00
MrMirDan ae1bbb2f88 fix: restoring from encrypted backup 2026-03-19 14:01:46 +02:00
MrMirDan bfcb7bf979 update: changed encryption to aes-256-gcm 2026-03-17 16:15:53 +02:00
MrMirDan dbce2f796c update: app password and encription pages margins 2026-03-13 13:30:42 +02:00
MrMirDan 91307cf49a update: systemController 2026-03-13 13:30:03 +02:00
MrMirDan e044507b94 Merge branch 'dev' into feat/file-password-protection 2026-01-30 14:45:36 +02:00
MrMirDan 10de29217b update: text and RU translation 2025-12-26 12:00:30 +02:00
MrMirDan f8c80c21c9 update: text 2025-12-23 16:30:05 +02:00
MrMirDan 805d594608 updated: added encryption docs links 2025-12-23 14:51:34 +02:00
MrMirDan 452150bfff update: text and some fixes 2025-12-23 14:10:19 +02:00
MrMirDan 4c082654f9 fix: correctly save password on set or change 2025-12-19 13:24:51 +02:00
MrMirDan d8bf7d4d1a update: added encryption indicator on backup, share and api configs pages 2025-12-19 12:58:16 +02:00
MrMirDan 4097af8d81 update: logging page text 2025-12-19 12:40:43 +02:00
MrMirDan d1cd2a9d8d update: files encrypt on export and files data decrypt on import 2025-12-19 12:24:37 +02:00
MrMirDan 6ebb942466 update: encryption/decryption logic, backup encryption on save and decryption on restore 2025-12-18 12:25:55 +02:00
MrMirDan ad5d60f915 update: ui to set/change password and hint 2025-12-12 16:32:16 +02:00
MrMirDan 9ed920b715 update: changed password and hint saving 2025-12-11 14:20:13 +02:00
MrMirDan a868702be0 update: changed logic, added to settingsController 2025-12-09 13:50:55 +02:00
MrMirDan a9ab281221 removed unnecessary method 2025-10-22 13:17:22 +03:00
MrMirDan fb9844cab8 feat: file password protection v1 2025-10-22 12:35:14 +03:00
29 changed files with 1373 additions and 14 deletions
@@ -192,6 +192,36 @@ void SettingsController::clearSettings()
toggleAutoStart(false);
}
bool SettingsController::isFileEncryptionEnabled()
{
return m_appSettingsRepository->isFileEncryption();
}
void SettingsController::toggleFileEncryption(bool enable)
{
m_appSettingsRepository->setFileEncryption(enable);
}
void SettingsController::setPassword(QString pwd)
{
m_appSettingsRepository->setPassword(pwd);
}
QString SettingsController::getPassword()
{
return m_appSettingsRepository->getPassword();
}
void SettingsController::setHint(QString hint)
{
m_appSettingsRepository->setHint(hint);
}
QString SettingsController::getHint()
{
return m_appSettingsRepository->getHint();
}
bool SettingsController::isAutoConnectEnabled() const
{
return m_appSettingsRepository->isAutoConnect();
@@ -44,6 +44,15 @@ public:
void clearSettings();
bool isFileEncryptionEnabled();
void toggleFileEncryption(bool enable);
void setPassword(QString pwd);
QString getPassword();
void setHint(QString hint);
QString getHint();
bool isAutoConnectEnabled() const;
void toggleAutoConnect(bool enable);
@@ -300,6 +300,33 @@ void SecureAppSettingsRepository::setStrictKillSwitchEnabled(bool enabled)
setValue("Conf/strictKillSwitchEnabled", enabled);
}
bool SecureAppSettingsRepository::isFileEncryption() const
{
return value("Sec/fileEncryption", false).toBool();
}
void SecureAppSettingsRepository::setFileEncryption(bool enabled)
{
setValue("Sec/fileEncryption", enabled);
}
QString SecureAppSettingsRepository::getPassword() const
{
return value("Sec/password", "").toString();
}
void SecureAppSettingsRepository::setPassword(const QString &pwd)
{
setValue("Sec/password", pwd);
}
QString SecureAppSettingsRepository::getHint() const
{
return value("Sec/hint", "").toString();
}
void SecureAppSettingsRepository::setHint(const QString &hint)
{
setValue("Sec/hint", hint);
}
bool SecureAppSettingsRepository::isAutoConnect() const
{
return value("Conf/autoConnect", false).toBool();
@@ -64,6 +64,13 @@ public:
void setKillSwitchEnabled(bool enabled);
bool isStrictKillSwitchEnabled() const;
void setStrictKillSwitchEnabled(bool enabled);
bool isFileEncryption() const;
void setFileEncryption(bool enabled);
QString getPassword() const;
void setPassword(const QString &pwd);
QString getHint() const;
void setHint(const QString &hint);
bool isAutoConnect() const;
void setAutoConnect(bool enabled);
+3
View File
@@ -0,0 +1,3 @@
<svg width="10" height="11" viewBox="0 0 10 11" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.5 5V3C2.5 2.33696 2.76339 1.70107 3.23223 1.23223C3.70107 0.763392 4.33696 0.5 5 0.5C5.66304 0.5 6.29893 0.763392 6.76777 1.23223C7.23661 1.70107 7.5 2.33696 7.5 3V5M1.5 5H8.5C9.05229 5 9.5 5.44772 9.5 6V9.5C9.5 10.0523 9.05229 10.5 8.5 10.5H1.5C0.947715 10.5 0.5 10.0523 0.5 9.5V6C0.5 5.44772 0.947715 5 1.5 5Z" stroke="#EB5757" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 494 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="10" height="11" viewBox="0 0 10 11" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.5 5.00252V3.00252C2.49938 2.38254 2.72914 1.78445 3.14469 1.32435C3.56023 0.864252 4.13191 0.574969 4.74875 0.512663C5.36559 0.450356 5.98357 0.61947 6.48274 0.987175C6.9819 1.35488 7.32663 1.89494 7.45 2.50252M1.5 5.00252H8.5C9.05229 5.00252 9.5 5.45023 9.5 6.00252V9.50252C9.5 10.0548 9.05229 10.5025 8.5 10.5025H1.5C0.947715 10.5025 0.5 10.0548 0.5 9.50252V6.00252C0.5 5.45023 0.947715 5.00252 1.5 5.00252Z" stroke="#5CAEE7" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 592 B

+2
View File
@@ -36,6 +36,8 @@
<file>controls/home.svg</file>
<file>controls/infinity.svg</file>
<file>controls/info.svg</file>
<file>controls/lock-locked.svg</file>
<file>controls/lock-unlocked.svg</file>
<file>controls/mail.svg</file>
<file>controls/map-pin.svg</file>
<file>controls/more-vertical.svg</file>
+1 -1
View File
@@ -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;
@@ -117,6 +117,8 @@ bool SubscriptionUiController::exportNativeConfig(const QString &serverId, const
}
const bool saved = SystemController::saveFile(fileName, nativeConfig);
if (m_settingsController->isFileEncryptionEnabled())
SystemController::encryptFile(fileName, m_settingsController->getPassword(), m_settingsController->getHint());
getAccountInfo(serverId, true);
return saved;
}
@@ -37,6 +37,9 @@ namespace PageLoader
PageSettingsSplitTunneling,
PageSettingsAppSplitTunneling,
PageSettingsKillSwitch,
PageSettingsAppEncryption,
PageSettingsAppPassword,
PageSettingsAppPasswordConfirm,
PageSettingsApiServerInfo,
PageSettingsApiAvailableCountries,
PageSettingsApiSupport,
@@ -139,6 +139,8 @@ void SettingsUiController::backupAppConfig(const QString &fileName)
if (!SystemController::saveFile(fileName, data)) {
qInfo() << "SettingsUiController::backupAppConfig: save or share was cancelled or failed";
}
if (isFileEncryptionEnabled())
SystemController::encryptFile(fileName, getPassword(), getHint());
}
void SettingsUiController::restoreAppConfig(const QString &fileName)
@@ -186,6 +188,57 @@ void SettingsUiController::clearSettings()
#endif
}
bool SettingsUiController::isFileEncryptionEnabled()
{
return m_settingsController->isFileEncryptionEnabled();
}
void SettingsUiController::toggleFileEncryption(bool enable)
{
m_settingsController->toggleFileEncryption(enable);
emit fileEncryptionStateChanged();
}
void SettingsUiController::setPassword(QString pwd)
{
m_settingsController->setPassword(pwd);
}
QString SettingsUiController::getPassword()
{
return m_settingsController->getPassword();
}
void SettingsUiController::setHint(QString hint)
{
m_settingsController->setHint(hint);
}
QString SettingsUiController::getHint()
{
return m_settingsController->getHint();
}
void SettingsUiController::setTempPassword(QString pwd)
{
tempPassword = pwd;
}
QString SettingsUiController::getTempPassword()
{
return tempPassword;
}
void SettingsUiController::setTempHint(QString hint)
{
tempHint = hint;
}
QString SettingsUiController::getTempHint()
{
return tempHint;
}
bool SettingsUiController::isAutoConnectEnabled()
{
return m_settingsController->isAutoConnectEnabled();
@@ -61,6 +61,14 @@ public slots:
void clearSettings();
bool isFileEncryptionEnabled();
void toggleFileEncryption(bool enable);
void setPassword(QString pwd);
QString getPassword();
void setHint(QString hint);
QString getHint();
bool isAutoConnectEnabled();
void toggleAutoConnect(bool enable);
@@ -73,6 +81,11 @@ public slots:
bool isNewsNotificationsEnabled();
void toggleNewsNotificationsEnabled(bool enable);
void setTempPassword(QString pwd);
QString getTempPassword();
void setTempHint(QString hint);
QString getTempHint();
bool isScreenshotsEnabled();
void toggleScreenshotsEnabled(bool enable);
@@ -137,7 +150,13 @@ signals:
void isHomeAdLabelVisibleChanged(bool visible);
void startMinimizedChanged();
void fileEncryptionStateChanged();
void changingPassword();
private:
QString tempPassword;
QString tempHint;
SettingsController* m_settingsController;
ServersController* m_serversController;
LanguageUiController* m_languageUiController;
+322
View File
@@ -11,6 +11,21 @@
#include <QUrl>
#include <QtConcurrent>
#include <openssl/err.h>
#include <openssl/evp.h>
#include <openssl/rand.h>
namespace
{
constexpr int SALT_LEN = 16;
constexpr int IV_LEN = 12;
constexpr int KEY_LEN = 32;
constexpr int TAG_LEN = 16;
constexpr int PBKDF2_ITER = 100000;
const QByteArray magicString { "EncData" };
}
#ifdef Q_OS_ANDROID
#include "platforms/android/android_controller.h"
#endif
@@ -104,6 +119,313 @@ 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)
{
outKey.resize(KEY_LEN);
const unsigned char *pw = reinterpret_cast<const unsigned char *>(password.constData());
const unsigned char *s = reinterpret_cast<const unsigned char *>(salt.constData());
int ok = PKCS5_PBKDF2_HMAC(reinterpret_cast<const char *>(pw), password.size(), s, salt.size(), PBKDF2_ITER,
EVP_sha256(), KEY_LEN, reinterpret_cast<unsigned char *>(outKey.data()));
if (!ok) {
qDebug() << opensslErrString();
}
return ok == 1;
}
static bool aesCrypt(const QByteArray &in, const QByteArray &key, const QByteArray &iv, QByteArray &out,
QByteArray &tag, bool encrypt)
{
std::unique_ptr<EVP_CIPHER_CTX, void (*)(EVP_CIPHER_CTX *)> 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_gcm();
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<const unsigned char *>(key.constData()),
reinterpret_cast<const unsigned char *>(iv.constData()), -1)) {
qDebug() << opensslErrString();
return false;
}
out.clear();
out.resize(in.size());
int outlen = 0;
if (1 != EVP_CipherUpdate(ctx.get(), reinterpret_cast<unsigned char *>(out.data()), &outlen,
reinterpret_cast<const unsigned char *>(in.constData()), in.size())) {
qDebug() << opensslErrString();
return false;
}
int tmplen = 0;
if (encrypt) {
if (1 != EVP_CipherFinal_ex(ctx.get(), reinterpret_cast<unsigned char *>(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<char *>(tag.constData()))) {
qDebug() << opensslErrString();
return false;
}
if (1 != EVP_CipherFinal_ex(ctx.get(), reinterpret_cast<unsigned char *>(out.data()) + outlen, &tmplen)) {
qDebug() << "Authentication failed: " << opensslErrString();
return false;
}
out.resize(outlen + tmplen);
}
return true;
}
bool SystemController::encryptFile(const QString &filePath, const QString &password, const QString &hint)
{
QFile f(filePath);
if (!f.open(QIODevice::ReadOnly)) {
qDebug() << "Cannot open file for read: " << f.errorString();
return false;
}
QByteArray content = f.readAll();
f.close();
if (content.startsWith(magicString)) {
qDebug() << "File already encrypted";
return false;
}
QByteArray salt(SALT_LEN, 0);
QByteArray iv(IV_LEN, 0);
QByteArray key;
QByteArray cipher;
QByteArray tag;
if (1 != RAND_bytes(reinterpret_cast<unsigned char *>(salt.data()), SALT_LEN)
|| 1 != RAND_bytes(reinterpret_cast<unsigned char *>(iv.data()), IV_LEN)) {
qDebug() << opensslErrString();
return false;
}
if (!deriveKey(password.toUtf8(), salt, key))
return false;
if (!aesCrypt(content, key, iv, cipher, tag, true))
return false;
QByteArray out;
QByteArray hintBytes = hint.toUtf8();
quint32 hintLen = static_cast<quint32>(hintBytes.size());
out += magicString;
out.append(reinterpret_cast<const char *>(&hintLen), sizeof(hintLen));
out += hintBytes;
out += salt;
out += iv;
out += tag;
out += cipher;
if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
qDebug() << "Cannot open file for write: " << f.errorString();
return false;
}
if (f.write(out) != out.size()) {
qDebug() << "Write failed";
f.close();
return false;
}
f.close();
return true;
}
QByteArray SystemController::getDecryptedData(const QString &filePath, const QString &password)
{
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<int>(sizeof(quint32))) {
qDebug() << "Corrupted file (no hint length)";
return {};
}
quint32 hintLen = 0;
memcpy(&hintLen, content.constData() + pos, sizeof(quint32));
pos += sizeof(quint32);
if (content.size() < pos + static_cast<int>(hintLen) + SALT_LEN + IV_LEN + TAG_LEN) {
qDebug() << "Corrupted file (invalid sizes)";
return {};
}
pos += hintLen;
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;
if (!deriveKey(password.toUtf8(), salt, key)) {
qDebug() << "Key derivation failed";
return {};
}
QByteArray plain;
if (!aesCrypt(cipher, key, iv, plain, tag, false)) {
qDebug() << "Decryption failed (wrong password or corrupted data)";
return {};
}
return plain;
}
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 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))
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, 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;
if (!deriveKey(password.toUtf8(), salt, key))
return false;
QByteArray plain;
bool ok = aesCrypt(cipher, key, iv, plain, tag, false);
if (!ok)
qDebug() << "Wrong password";
return ok;
}
QString SystemController::readHint(const QString &filePath)
{
if (filePath.isEmpty())
return "";
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<int>(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<int>(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)
{
+13
View File
@@ -15,10 +15,23 @@ 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);
Q_INVOKABLE bool QEncryptFile(const QString &filePath, const QString &password, const QString &hint)
{
return encryptFile(filePath, password, hint);
}
public slots:
QString getFileName(const QString &acceptLabel, const QString &nameFilter, const QString &selectedFile = "",
const bool isSaveMode = false, const QString &defaultSuffix = "");
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);
bool isAuthenticated();
@@ -0,0 +1,86 @@
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 bool linkEnabled: false
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
anchors.centerIn: parent
spacing: 0
Image {
width: root.iconWidth
height: root.iconHeight
source: root.iconPath
}
CaptionTextType {
id: supportingText
Layout.fillWidth: true
Layout.leftMargin: 8
text: root.textString
textFormat: Text.RichText
color: root.textColor
}
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")
}
}
}
}
+130
View File
@@ -0,0 +1,130 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import PageEnum 1.0
import Style 1.0
import "../Controls2"
import "../Controls2/TextTypes"
import "../Config"
DrawerType2 {
id: root
objectName: "passwordDrawer"
property bool fromOutside: true
property string fileName
property var securedFunc
signal restoreSecuredBackup
signal importSecuredFile
expandedStateContent: ColumnLayout {
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.topMargin: 16
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() {
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
text: qsTr("Password required")
}
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
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: qsTr("Hint: ") + (fromOutside ? SystemController.readHint(fileName) : SettingsController.getHint())
}
BasicButtonType {
id: doneButton
Layout.fillWidth: true
text: qsTr("Done")
clickedFunc: function() {
if (fromOutside) {
if (!SystemController.isPasswordValid(fileName, passwordField.textField.text)) {
passwordField.errorText = qsTr("Invalid password")
return
}
} else {
if (passwordField.textField.text !== SettingsController.getPassword()) {
passwordField.errorText = qsTr("Invalid password")
return
}
}
if (root.securedFunc && typeof root.securedFunc === "function") {
root.securedFunc()
}
root.closeTriggered()
}
}
}
}
@@ -199,11 +199,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() {
@@ -60,6 +60,16 @@ PageType {
headerText: qsTr("Configuration Files")
descriptionText: qsTr("For router setup or the AmneziaWG app")
}
EncryptionIndicator {
id: indicator
visible: SettingsController.isFileEncryptionEnabled()
linkEnabled: true
textString: qsTr("Encryption enabled.")
iconPath: "qrc:/images/controls/lock-locked.svg"
}
}
delegate: ColumnLayout {
@@ -0,0 +1,187 @@
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()
SettingsController.isFileEncryptionEnabled() ? PageController.goToPage(PageEnum.PageSettingsAppEncryption) : PageController.goToPage(PageEnum.PageSettingsAppPassword)
PageController.showBusyIndicator(false)
PageController.showNotificationMessage(SettingsController.isFileEncryptionEnabled() ? qsTr("Encryption enabled") : qsTr("Encryption disabled"))
}
}
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
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: 32
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() {
Qt.openUrlExternally("https://storage.googleapis.com/amnezia/docs?m-path=/documentation/instructions/encryption")
}
}
EncryptionIndicator {
id: indicator
textString: qsTr("Password set. Encryption enabled")
iconPath: "qrc:/images/controls/lock-locked.svg"
}
BasicButtonType {
id: disableEncryptionButton
Layout.fillWidth: true
Layout.topMargin: 16
Layout.leftMargin: 16
Layout.rightMargin: 16
text: qsTr("Disable encryption")
clickedFunc: function() {
passwordDrawer.securedFunc = function() {
PageController.showBusyIndicator(true)
SettingsController.toggleFileEncryption(false)
SettingsController.setPassword("")
SettingsController.setHint("")
PageController.showBusyIndicator(false)
}
passwordDrawer.openTriggered()
}
}
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")
clickedFunc: function() {
passwordDrawer.securedFunc = function() {
root.isChangingPassword = true
PageController.showBusyIndicator(true)
PageController.closePage()
PageController.goToPage(PageEnum.PageSettingsAppPassword)
PageController.showBusyIndicator(false)
SettingsController.changingPassword()
}
passwordDrawer.openTriggered()
}
}
PasswordDrawer {
id: passwordDrawer
fromOutside: false
parent: root
anchors.fill: parent
expandedHeight: root.height * 0.45
}
}
spacing: 16
footer: ColumnLayout {
width: listView.width
CaptionTextType {
Layout.fillWidth: true
Layout.topMargin: 32
horizontalAlignment: Text.AlignHCenter
textFormat: Text.RichText
text: qsTr("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>."
+ "\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()
}
}
}
}
}
}
@@ -0,0 +1,215 @@
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
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: 32
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() {
Qt.openUrlExternally("https://storage.googleapis.com/amnezia/docs?m-path=/documentation/instructions/encryption")
}
}
EncryptionIndicator {
id: indicator
visible: !root.isChangingPassword
textString: qsTr("Password not set. Encryption disabled")
iconPath: "qrc:/images/controls/lock-unlocked.svg"
}
}
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.setTempPassword(_password)
SettingsController.setTempHint(_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<QtObject> 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.svg"
property bool hideContent: true
readonly property var clickedHandler: function() {
hideContent = !hideContent
imageSource = hideContent ? "qrc:/images/controls/eye.svg" : "qrc:/images/controls/eye-off.svg"
}
}
QtObject {
id: hintObject
property string title: root.isChangingPassword ? qsTr("New password hint (optional)") : qsTr("Password hint")
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
}
}
@@ -0,0 +1,155 @@
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: 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")
}
}
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("Re-enter new password") : qsTr("Re-enter password")
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: qsTr("Hint: ") + SettingsController.getTempHint()
}
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
}
SettingsController.setPassword(SettingsController.getTempPassword())
SettingsController.setHint(SettingsController.getTempHint())
SettingsController.setTempPassword("")
SettingsController.setTempHint("")
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.getTempPassword()) {
secretDataItem.errorText = qsTr("Passwords not match")
notMatch = true
}
return !notMatch
}
}
@@ -213,6 +213,23 @@ PageType {
DividerType {}
LabelWithButtonType {
id: labelWithButtonAppPassword
Layout.fillWidth: true
text: qsTr("Password & Encryption")
descriptionText: qsTr("Password protection for backups and configuration files")
rightImageSource: "qrc:/images/controls/chevron-right.svg"
clickedFunction: function() {
SettingsController.getPassword() === "" ? PageController.goToPage(PageEnum.PageSettingsAppPassword)
: PageController.goToPage(PageEnum.PageSettingsAppEncryption)
}
}
DividerType {}
LabelWithButtonType {
id: labelWithButtonLogging
+21 -1
View File
@@ -67,6 +67,16 @@ 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()
linkEnabled: true
textString: qsTr("Encryption enabled.")
iconPath: "qrc:/images/controls/lock-locked.svg"
}
}
model: 1 // fake model to force the ListView to be created without a model
@@ -140,10 +150,20 @@ PageType {
var filePath = SystemController.getFileName(qsTr("Open backup file"),
qsTr("Backup files (*.backup)"))
if (filePath !== "") {
restoreBackup(filePath)
passwordDrawer.fileName = filePath
SystemController.isFileEncrypted(filePath) ? passwordDrawer.restoreSecuredBackup() : restoreBackup(filePath)
}
}
}
PasswordDrawer {
id: passwordDrawer
parent: root
anchors.fill: parent
expandedHeight: root.height * 0.45
}
}
}
+6 -6
View File
@@ -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() {
@@ -12,6 +12,7 @@ import "./"
import "../Controls2"
import "../Controls2/TextTypes"
import "../Config"
import "../Components"
PageType {
id: root
@@ -264,6 +265,15 @@ PageType {
}
}
PasswordDrawer {
id: passwordDrawer
parent: root
anchors.fill: parent
expandedHeight: root.height * 0.45
}
property list<QtObject> variants: [
amneziaVpn,
selfHostVpn,
@@ -318,7 +328,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)
}
}
@@ -336,8 +351,13 @@ PageType {
var nameFilter = "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)
}
}
}
}
+10
View File
@@ -271,6 +271,16 @@ PageType {
color: AmneziaStyle.color.mutedGray
}
EncryptionIndicator {
id: indicator
visible: SettingsController.isFileEncryptionEnabled()
linkEnabled: true
textString: qsTr("Encryption enabled.")
iconPath: "qrc:/images/controls/lock-locked.svg"
}
TextFieldWithHeaderType {
id: clientNameTextField
Layout.fillWidth: true
@@ -108,6 +108,8 @@ PageType {
if (fileName !== "") {
PageController.showBusyIndicator(true)
ExportController.exportConfig(fileName)
if (SettingsController.isFileEncryptionEnabled())
SystemController.QEncryptFile(fileName, SettingsController.getPassword(), SettingsController.getHint())
PageController.showBusyIndicator(false)
}
}
@@ -69,6 +69,16 @@ PageType {
color: AmneziaStyle.color.mutedGray
}
EncryptionIndicator {
id: indicator
visible: SettingsController.isFileEncryptionEnabled()
linkEnabled: true
textString: qsTr("Encryption enabled.")
iconPath: "qrc:/images/controls/lock-locked.svg"
}
DropDownType {
id: serverSelector
objectName: "serverSelector"
+5
View File
@@ -7,11 +7,13 @@
<file>Components/HomeContainersListView.qml</file>
<file>Components/HomeSplitTunnelingDrawer.qml</file>
<file>Components/InstalledAppsDrawer.qml</file>
<file>Components/PasswordDrawer.qml</file>
<file>Components/ChangelogDrawer.qml</file>
<file>Components/QuestionDrawer.qml</file>
<file>Components/SelectLanguageDrawer.qml</file>
<file>Components/ServersListView.qml</file>
<file>Components/SettingsContainersListView.qml</file>
<file>Components/EncryptionIndicator.qml</file>
<file>Components/BenefitRow.qml</file>
<file>Components/BenefitsPanel.qml</file>
<file>Components/SubscriptionExpiredDrawer.qml</file>
@@ -99,6 +101,9 @@
<file>Pages2/PageSettingsApiServerInfo.qml</file>
<file>Pages2/PageSettingsApplication.qml</file>
<file>Pages2/PageSettingsAppSplitTunneling.qml</file>
<file>Pages2/PageSettingsAppEncryption.qml</file>
<file>Pages2/PageSettingsAppPassword.qml</file>
<file>Pages2/PageSettingsAppPasswordConfirm.qml</file>
<file>Pages2/PageSettingsBackup.qml</file>
<file>Pages2/PageSettingsConnection.qml</file>
<file>Pages2/PageSettingsDns.qml</file>