mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-06-22 02:01:08 +07:00
update: encryption/decryption logic, backup encryption on save and decryption on restore
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user