update: encryption/decryption logic, backup encryption on save and decryption on restore

This commit is contained in:
MrMirDan
2025-12-18 12:25:55 +02:00
parent a7ae0bc65e
commit ba580891cd
12 changed files with 308 additions and 215 deletions
-4
View File
@@ -1,4 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 12C2 12 5 5 12 5C19 5 22 12 22 12C22 12 19 19 12 19C5 19 2 12 2 12Z" stroke="#D7D8DB" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 15C13.6569 15 15 13.6569 15 12C15 10.3431 13.6569 9 12 9C10.3431 9 9 10.3431 9 12C9 13.6569 10.3431 15 12 15Z" stroke="#D7D8DB" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 474 B

-6
View File
@@ -1,6 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.88061 9.87891C9.58587 10.1536 9.34946 10.4848 9.18549 10.8528C9.02152 11.2208 8.93336 11.618 8.92625 12.0208C8.91914 12.4236 8.99324 12.8237 9.14412 13.1973C9.29501 13.5708 9.51959 13.9102 9.80446 14.1951C10.0893 14.4799 10.4287 14.7045 10.8022 14.8554C11.1758 15.0063 11.5759 15.0804 11.9787 15.0733C12.3815 15.0662 12.7788 14.978 13.1468 14.814C13.5148 14.6501 13.846 14.4137 14.1206 14.1189" stroke="#D7D8DB" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.7305 5.08C11.1518 5.02751 11.5759 5.00079 12.0005 5C19.0005 5 22.0005 12 22.0005 12C21.5534 12.9571 20.9927 13.8569 20.3305 14.68" stroke="#D7D8DB" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.61 6.60938C4.62125 7.964 3.02987 9.82463 2 11.9994C2 11.9994 5 18.9994 12 18.9994C13.9159 19.0045 15.7908 18.4445 17.39 17.3894" stroke="#D7D8DB" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2 2L22 22" stroke="#D7D8DB" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

-2
View File
@@ -21,8 +21,6 @@
<file>controls/delete.svg</file>
<file>controls/download.svg</file>
<file>controls/edit-3.svg</file>
<file>controls/eye-off-new.svg</file>
<file>controls/eye-new.svg</file>
<file>controls/eye-off.svg</file>
<file>controls/eye.svg</file>
<file>controls/external-link.svg</file>
-186
View File
@@ -12,11 +12,6 @@
#include <QRandomGenerator>
#include <QSharedPointer>
#include <QTimer>
#include <QFile>
#include <openssl/evp.h>
#include <openssl/rand.h>
#include <openssl/err.h>
using namespace QKeychain;
@@ -24,11 +19,6 @@ namespace {
constexpr const char *settingsKeyTag = "settingsKeyTag";
constexpr const char *settingsIvTag = "settingsIvTag";
constexpr const char *keyChainName = "AmneziaVPN-Keychain";
constexpr int SALT_LEN = 16;
constexpr int IV_LEN = 16;
constexpr int KEY_LEN = 32;
constexpr int PBKDF2_ITER = 100000;
}
SecureQSettings::SecureQSettings(const QString &organization, const QString &application, QObject *parent, bool enableEncryption)
@@ -315,179 +305,3 @@ void SecureQSettings::setSecTag(const QString &tag, const QByteArray &data)
qCritical() << "SecureQSettings::setSecTag Error:" << job->errorString();
}
}
static QString opensslErrString()
{
unsigned long e = ERR_get_error();
if (!e)
return QStringLiteral("Unknown OpenSSL error");
char buf[256];
ERR_error_string_n(e, buf, sizeof(buf));
return QString::fromUtf8(buf);
}
static bool deriveKey(const QByteArray &password, const QByteArray &salt, QByteArray &outKey, QString *err)
{
outKey.resize(KEY_LEN);
const unsigned char *pw = reinterpret_cast<const unsigned char *>(password.constData());
const unsigned char *s = reinterpret_cast<const unsigned char *>(salt.constData());
int ok = PKCS5_PBKDF2_HMAC(reinterpret_cast<const char *>(pw), password.size(), s, salt.size(), PBKDF2_ITER,
EVP_sha256(), KEY_LEN, reinterpret_cast<unsigned char *>(outKey.data()));
if (!ok) {
if (err)
*err = opensslErrString();
}
return ok == 1;
}
static bool aesCrypt(const QByteArray &in, const QByteArray &key, const QByteArray &iv, QByteArray &out, bool encrypt,
QString *err)
{
EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
if (!ctx) {
if (err)
*err = "EVP_CIPHER_CTX_new failed";
return false;
}
const EVP_CIPHER *cipher = EVP_aes_256_cbc();
if (1
!= EVP_CipherInit_ex(ctx, cipher, nullptr, reinterpret_cast<const unsigned char *>(key.constData()),
reinterpret_cast<const unsigned char *>(iv.constData()), encrypt ? 1 : 0)) {
if (err)
*err = opensslErrString();
EVP_CIPHER_CTX_free(ctx);
return false;
}
out.clear();
out.resize(in.size() + EVP_CIPHER_block_size(cipher));
int outlen1 = 0;
if (1
!= EVP_CipherUpdate(ctx, reinterpret_cast<unsigned char *>(out.data()), &outlen1,
reinterpret_cast<const unsigned char *>(in.constData()), in.size())) {
if (err)
*err = opensslErrString();
EVP_CIPHER_CTX_free(ctx);
return false;
}
int outlen2 = 0;
if (1 != EVP_CipherFinal_ex(ctx, reinterpret_cast<unsigned char *>(out.data()) + outlen1, &outlen2)) {
if (err)
*err = opensslErrString();
EVP_CIPHER_CTX_free(ctx);
return false;
}
out.resize(outlen1 + outlen2);
EVP_CIPHER_CTX_free(ctx);
return true;
}
bool SecureQSettings::encryptFile(const QString &filePath, const QString &password, QString *error) const
{
QFile f(filePath);
if (!f.open(QIODevice::ReadOnly)) {
if (error)
*error = QStringLiteral("Cannot open file for read: %1").arg(f.errorString());
return false;
}
QByteArray plain = f.readAll();
f.close();
if (plain.startsWith(magicString)) {
if (error)
*error = QStringLiteral("File already encrypted (magic found)");
return false;
}
QByteArray salt(SALT_LEN, 0);
QByteArray iv(IV_LEN, 0);
QByteArray key;
QByteArray cipher;
QByteArray out;
if (1 != RAND_bytes(reinterpret_cast<unsigned char *>(salt.data()), SALT_LEN)
|| 1 != RAND_bytes(reinterpret_cast<unsigned char *>(iv.data()), IV_LEN)) {
if (error)
*error = opensslErrString();
return false;
}
if (!deriveKey(password.toUtf8(), salt, key, error))
return false;
if (!aesCrypt(plain, key, iv, cipher, true, error))
return false;
out.reserve(magicString.size() + SALT_LEN + IV_LEN + cipher.size());
out += magicString;
out += salt;
out += iv;
out += cipher;
if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
if (error)
*error = QStringLiteral("Cannot open file for write: %1").arg(f.errorString());
return false;
}
qint64 written = f.write(out);
f.close();
if (written != out.size()) {
if (error)
*error = QStringLiteral("Write failed or incomplete");
return false;
}
return true;
}
bool SecureQSettings::decryptFile(const QString &filePath, const QString &password, QString *error) const
{
QFile f(filePath);
if (!f.open(QIODevice::ReadOnly)) {
if (error)
*error = QStringLiteral("Cannot open file for read: %1").arg(f.errorString());
return false;
}
QByteArray blob = f.readAll();
f.close();
if (!blob.startsWith(magicString)) {
if (error)
*error = QStringLiteral("File is not recognized as encrypted (magic missing)");
return false;
}
int pos = magicString.size();
if (blob.size() < pos + SALT_LEN + IV_LEN) {
if (error)
*error = QStringLiteral("Encrypted file too small / corrupted");
return false;
}
QByteArray salt = blob.mid(pos, SALT_LEN);
pos += SALT_LEN;
QByteArray iv = blob.mid(pos, IV_LEN);
pos += IV_LEN;
QByteArray cipher = blob.mid(pos);
QByteArray key;
QByteArray plain;
if (!deriveKey(password.toUtf8(), salt, key, error))
return false;
if (!aesCrypt(cipher, key, iv, plain, false, error))
return false;
if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
if (error)
*error = QStringLiteral("Cannot open file for write: %1").arg(f.errorString());
return false;
}
qint64 written = f.write(plain);
f.close();
if (written != plain.size()) {
if (error)
*error = QStringLiteral("Write failed or incomplete");
return false;
}
return true;
}
-9
View File
@@ -26,15 +26,6 @@ public:
void clearSettings();
void setPassword(const QString &pwd);
void setHint(const QString &hint);
QString getPassword() const;
QString getHint() const;
bool encryptFile(const QString &filePath, const QString &password, QString *error = nullptr) const;
bool decryptFile(const QString &filePath, const QString &password, QString *error = nullptr) const;
private:
QByteArray encryptText(const QByteArray &value) const;
QByteArray decryptText(const QByteArray &ba) const;
@@ -110,6 +110,8 @@ void SettingsUiController::exportLogsFile(const QString &fileName)
if (!SystemController::saveFile(fileName, Logger::getLogFile())) {
qInfo() << "SettingsUiController::exportLogsFile: save or share was cancelled or failed";
}
if (isFileEncryptionEnabled())
SystemController::encryptFile(fileName, getPassword(), getHint());
#endif
}
@@ -121,6 +123,8 @@ void SettingsUiController::exportServiceLogsFile(const QString &fileName)
if (!SystemController::saveFile(fileName, Logger::getServiceLogFile())) {
qInfo() << "SettingsUiController::exportServiceLogsFile: save or share was cancelled or failed";
}
if (isFileEncryptionEnabled())
SystemController::encryptFile(fileName, getPassword(), getHint());
#endif
}
@@ -139,6 +143,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)
+254
View File
@@ -11,6 +11,20 @@
#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 = 16;
constexpr int KEY_LEN = 32;
constexpr int PBKDF2_ITER = 100000;
const QByteArray magicString { "EncData" };
}
#ifdef Q_OS_ANDROID
#include "platforms/android/android_controller.h"
#endif
@@ -104,6 +118,246 @@ bool SystemController::readFile(const QString &fileName, QString &data)
return true;
}
static QString opensslErrString()
{
unsigned long e = ERR_get_error();
if (!e)
return QStringLiteral("Unknown OpenSSL error");
char buf[256];
ERR_error_string_n(e, buf, sizeof(buf));
return QString::fromUtf8(buf);
}
static bool deriveKey(const QByteArray &password, const QByteArray &salt, QByteArray &outKey, QString *err)
{
outKey.resize(KEY_LEN);
const unsigned char *pw = reinterpret_cast<const unsigned char *>(password.constData());
const unsigned char *s = reinterpret_cast<const unsigned char *>(salt.constData());
int ok = PKCS5_PBKDF2_HMAC(reinterpret_cast<const char *>(pw), password.size(), s, salt.size(), PBKDF2_ITER,
EVP_sha256(), KEY_LEN, reinterpret_cast<unsigned char *>(outKey.data()));
if (!ok) {
if (err)
*err = opensslErrString();
}
return ok == 1;
}
static bool aesCrypt(const QByteArray &in, const QByteArray &key, const QByteArray &iv, QByteArray &out, bool encrypt,
QString *err)
{
EVP_CIPHER_CTX *ctx = EVP_CIPHER_CTX_new();
if (!ctx) {
if (err)
*err = "EVP_CIPHER_CTX_new failed";
return false;
}
const EVP_CIPHER *cipher = EVP_aes_256_cbc();
if (1
!= EVP_CipherInit_ex(ctx, cipher, nullptr, reinterpret_cast<const unsigned char *>(key.constData()),
reinterpret_cast<const unsigned char *>(iv.constData()), encrypt ? 1 : 0)) {
if (err)
*err = opensslErrString();
EVP_CIPHER_CTX_free(ctx);
return false;
}
out.clear();
out.resize(in.size() + EVP_CIPHER_block_size(cipher));
int outlen1 = 0;
if (1
!= EVP_CipherUpdate(ctx, reinterpret_cast<unsigned char *>(out.data()), &outlen1,
reinterpret_cast<const unsigned char *>(in.constData()), in.size())) {
if (err)
*err = opensslErrString();
EVP_CIPHER_CTX_free(ctx);
return false;
}
int outlen2 = 0;
if (1 != EVP_CipherFinal_ex(ctx, reinterpret_cast<unsigned char *>(out.data()) + outlen1, &outlen2)) {
if (err)
*err = opensslErrString();
EVP_CIPHER_CTX_free(ctx);
return false;
}
out.resize(outlen1 + outlen2);
EVP_CIPHER_CTX_free(ctx);
return true;
}
bool SystemController::encryptFile(const QString &filePath, const QString &password, const QString &hint, QString *error)
{
QFile f(filePath);
if (!f.open(QIODevice::ReadOnly)) {
if (error)
*error = QStringLiteral("Cannot open file for read: %1").arg(f.errorString());
return false;
}
QByteArray content = f.readAll();
f.close();
if (content.startsWith(magicString)) {
if (error)
*error = QStringLiteral("File already encrypted (magic found)");
return false;
}
QByteArray qba_hint = hint.toUtf8();
quint32 qba_hint_len = static_cast<quint32>(qba_hint.size());
QByteArray salt(SALT_LEN, 0);
QByteArray iv(IV_LEN, 0);
QByteArray key;
QByteArray cipher;
QByteArray out;
if (1 != RAND_bytes(reinterpret_cast<unsigned char *>(salt.data()), SALT_LEN)
|| 1 != RAND_bytes(reinterpret_cast<unsigned char *>(iv.data()), IV_LEN)) {
if (error)
*error = opensslErrString();
return false;
}
if (!deriveKey(password.toUtf8(), salt, key, error))
return false;
if (!aesCrypt(content, key, iv, cipher, true, error))
return false;
out.reserve(magicString.size() + SALT_LEN + IV_LEN + cipher.size());
out += magicString;
out.append(reinterpret_cast<const char *>(&qba_hint_len), sizeof(qba_hint_len));
out += hint.toUtf8();
out += salt;
out += iv;
out += cipher;
if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
if (error)
*error = QStringLiteral("Cannot open file for write: %1").arg(f.errorString());
return false;
}
qint64 written = f.write(out);
f.close();
if (written != out.size()) {
if (error)
*error = QStringLiteral("Write failed or incomplete");
return false;
}
return true;
}
bool SystemController::decryptFile(const QString &filePath, const QString &password, QString *error)
{
QFile f(filePath);
if (!f.open(QIODevice::ReadOnly)) {
if (error)
*error = QStringLiteral("Cannot open file for read: %1").arg(f.errorString());
return false;
}
QByteArray content = f.readAll();
f.close();
if (!content.startsWith(magicString)) {
if (error)
*error = QStringLiteral("File is not recognized as encrypted (magic missing)");
return false;
}
int pos = magicString.size();
quint32 hintLen = 0;
memcpy(&hintLen, content.constData() + pos, sizeof(quint32));
pos += sizeof(quint32);
pos += hintLen;
if (content.size() < pos + SALT_LEN + IV_LEN) {
if (error)
*error = QStringLiteral("Encrypted file too small / corrupted");
return false;
}
QByteArray salt = content.mid(pos, SALT_LEN);
pos += SALT_LEN;
QByteArray iv = content.mid(pos, IV_LEN);
pos += IV_LEN;
QByteArray cipher = content.mid(pos);
QByteArray key;
QByteArray plain;
if (!deriveKey(password.toUtf8(), salt, key, error))
return false;
if (!aesCrypt(cipher, key, iv, plain, false, error))
return false;
if (!f.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
if (error)
*error = QStringLiteral("Cannot open file for write: %1").arg(f.errorString());
return false;
}
qint64 written = f.write(plain);
f.close();
if (written != plain.size()) {
if (error)
*error = QStringLiteral("Write failed or incomplete");
return false;
}
return true;
}
bool SystemController::isFileEncrypted(const QString &filePath)
{
QFile f(filePath);
if (!f.open(QIODevice::ReadOnly)) {
qDebug() << "Cannot open file for read: %1", f.errorString();
return false;
}
QByteArray content = f.readAll();
f.close();
if (!content.startsWith(magicString)) {
qDebug() << "File is not recognized as encrypted (magic missing)";
return false;
}
return true;
}
QString SystemController::readHint(const QString &filePath)
{
QFile f(filePath);
if (!f.open(QIODevice::ReadOnly)) {
qDebug() << f.errorString();
return {};
}
QByteArray data = f.readAll();
f.close();
if (!data.startsWith(magicString)) {
qDebug() << "File is not encrypted";
return {};
}
int pos = magicString.size();
if (data.size() < pos + static_cast<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)
{
+7
View File
@@ -15,10 +15,17 @@ public:
static bool readFile(const QString &fileName, QByteArray &data);
static bool readFile(const QString &fileName, QString &data);
static bool encryptFile(const QString &filePath, const QString &password, const QString &hint, QString *error = nullptr);
public slots:
QString getFileName(const QString &acceptLabel, const QString &nameFilter, const QString &selectedFile = "",
const bool isSaveMode = false, const QString &defaultSuffix = "");
bool decryptFile(const QString &filePath, const QString &password, QString *error = nullptr);
bool isFileEncrypted(const QString &filePath);
QString readHint(const QString &filePath);
void setQmlRoot(QObject *qmlRoot);
bool isAuthenticated();
+22 -5
View File
@@ -13,6 +13,8 @@ DrawerType2 {
id: root
objectName: "passwordDrawer"
property bool fromOutside: true
property string fileName
property var securedFunc
expandedStateContent: ColumnLayout {
@@ -33,13 +35,21 @@ DrawerType2 {
TextFieldWithHeaderType {
id: passwordField
Connections {
target: root
function onCloseTriggered() {
passwordField.textField.text = ""
}
}
property bool hideContent: true
Layout.fillWidth: true
headerText: qsTr("Password")
textField.echoMode: hideContent ? TextInput.Password : TextInput.Normal
textField.text: textField.text
textField.text: ""
rightButtonClickedOnEnter: true
@@ -62,7 +72,7 @@ DrawerType2 {
Layout.topMargin: 8
Layout.bottomMargin: 16
text: SettingsController.getHint()
text: fromOutside ? SystemController.readHint(fileName) : SettingsController.getHint()
}
BasicButtonType {
@@ -73,9 +83,16 @@ DrawerType2 {
text: qsTr("Done")
clickedFunc: function() {
if (passwordField.textField.text !== SettingsController.getPassword()) {
passwordField.errorText = qsTr("Incorrect password")
return
if (fromOutside) {
if (!SystemController.decryptFile(fileName, passwordField.textField.text)) {
passwordField.errorText = qsTr("Incorrect password")
return
}
} else {
if (passwordField.textField.text !== SettingsController.getPassword()) {
passwordField.errorText = qsTr("Incorrect password")
return
}
}
if (root.securedFunc && typeof root.securedFunc === "function") {
@@ -119,6 +119,8 @@ PageType {
PasswordDrawer {
id: passwordDrawer
fromOutside: false
parent: root
anchors.fill: parent
@@ -162,11 +162,11 @@ PageType {
property string title: root.isChangingPassword ? qsTr("New password") : qsTr("Set encryption password")
readonly property string placeholderContent: ""
property string imageSource: "qrc:/images/controls/eye-new.svg"
property string imageSource: "qrc:/images/controls/eye.svg"
property bool hideContent: true
readonly property var clickedHandler: function() {
hideContent = !hideContent
imageSource = hideContent ? "qrc:/images/controls/eye-new.svg" : "qrc:/images/controls/eye-off-new.svg"
imageSource = hideContent ? "qrc:/images/controls/eye.svg" : "qrc:/images/controls/eye-off.svg"
}
}
+15 -1
View File
@@ -140,10 +140,24 @@ PageType {
var filePath = SystemController.getFileName(qsTr("Open backup file"),
qsTr("Backup files (*.backup)"))
if (filePath !== "") {
restoreBackup(filePath)
passwordDrawer.fileName = filePath
SystemController.isFileEncrypted(filePath) ? passwordDrawer.openTriggered() : passwordDrawer.securedFunc()
}
}
}
PasswordDrawer {
id: passwordDrawer
parent: root
anchors.fill: parent
expandedHeight: root.height * 0.45
securedFunc: function() {
restoreBackup(fileName)
}
}
}
}