mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-06-21 02:01:03 +07:00
Compare commits
54 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1c291bba0f | |||
| 7ba9013b66 | |||
| 09a0c933fb | |||
| 14ee66cc58 | |||
| 798d51de34 | |||
| d3ee50ea6f | |||
| e095b21911 | |||
| 7decbb5a48 | |||
| 3e240fc9ac | |||
| 161a1eb647 | |||
| a1a13b4428 | |||
| 4675084658 | |||
| aa37ed1d5e | |||
| b3e72aecd7 | |||
| 9108cc6dc8 | |||
| dd511de97d | |||
| ecc94ef48a | |||
| 10abaf5e33 | |||
| 6426516365 | |||
| d0bd28defb | |||
| 4ae3bac65a | |||
| ba580891cd | |||
| a7ae0bc65e | |||
| fca062ba3c | |||
| a8f084f8ef | |||
| 0b91219c51 | |||
| d3c12b95ec | |||
| 6f3c372c9d | |||
| d5616615d3 | |||
| 3ddd38f58e | |||
| 50801dd559 | |||
| 465b3d4d95 | |||
| 23de0c7ed1 | |||
| 18b8f50d21 | |||
| 23dc5e7999 | |||
| ae1bbb2f88 | |||
| bfcb7bf979 | |||
| dbce2f796c | |||
| 91307cf49a | |||
| e044507b94 | |||
| 10de29217b | |||
| f8c80c21c9 | |||
| 805d594608 | |||
| 452150bfff | |||
| 4c082654f9 | |||
| d8bf7d4d1a | |||
| 4097af8d81 | |||
| d1cd2a9d8d | |||
| 6ebb942466 | |||
| ad5d60f915 | |||
| 9ed920b715 | |||
| a868702be0 | |||
| a9ab281221 | |||
| fb9844cab8 |
@@ -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);
|
||||
|
||||
@@ -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 |
@@ -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 |
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user