mirror of
https://github.com/amnezia-vpn/amnezia-client.git
synced 2026-07-10 10:56:16 +03:00
Compare commits
3 Commits
feat/proxy
...
feat/proto
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8417f4d30d | ||
|
|
241ed18f96 | ||
|
|
aadd1e81d1 |
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -11,9 +11,6 @@
|
||||
[submodule "client/3rd/amneziawg-apple"]
|
||||
path = client/3rd/amneziawg-apple
|
||||
url = https://github.com/amnezia-vpn/amneziawg-apple
|
||||
[submodule "client/3rd/QSimpleCrypto"]
|
||||
path = client/3rd/QSimpleCrypto
|
||||
url = https://github.com/amnezia-vpn/QSimpleCrypto.git
|
||||
[submodule "client/3rd/qtgamepad"]
|
||||
path = client/3rd/qtgamepad
|
||||
url = https://github.com/amnezia-vpn/qtgamepad.git
|
||||
|
||||
1
client/3rd/QSimpleCrypto
vendored
1
client/3rd/QSimpleCrypto
vendored
Submodule client/3rd/QSimpleCrypto deleted from c99b33f0e0
@@ -109,6 +109,7 @@ include(${CMAKE_CURRENT_LIST_DIR}/cmake/sources.cmake)
|
||||
include_directories(
|
||||
${CMAKE_CURRENT_LIST_DIR}/../ipc
|
||||
${CMAKE_CURRENT_LIST_DIR}/../common/logger
|
||||
${CMAKE_CURRENT_LIST_DIR}/../common/crypto
|
||||
${CMAKE_CURRENT_LIST_DIR}
|
||||
${CMAKE_CURRENT_BINARY_DIR}
|
||||
)
|
||||
|
||||
@@ -4,7 +4,6 @@ set(CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/Modules;${CMAKE_MODULE_PATH}")
|
||||
|
||||
add_subdirectory(${CLIENT_ROOT_DIR}/3rd/SortFilterProxyModel)
|
||||
set(LIBS ${LIBS} SortFilterProxyModel)
|
||||
include(${CLIENT_ROOT_DIR}/cmake/QSimpleCrypto.cmake)
|
||||
|
||||
include(${CLIENT_ROOT_DIR}/3rd/qrcodegen/qrcodegen.cmake)
|
||||
|
||||
@@ -110,7 +109,6 @@ include_directories(
|
||||
${LIBSSH_INCLUDE_DIR}/include
|
||||
${LIBSSH_ROOT_DIR}/include
|
||||
${CLIENT_ROOT_DIR}/3rd/libssh/include
|
||||
${CLIENT_ROOT_DIR}/3rd/QSimpleCrypto/src/include
|
||||
${CLIENT_ROOT_DIR}/3rd/qtkeychain/qtkeychain
|
||||
${CMAKE_CURRENT_BINARY_DIR}/3rd/qtkeychain
|
||||
${CMAKE_CURRENT_BINARY_DIR}/3rd/libssh/include
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
set(CLIENT_ROOT_DIR ${CMAKE_CURRENT_LIST_DIR}/..)
|
||||
set(QSIMPLECRYPTO_DIR ${CLIENT_ROOT_DIR}/3rd/QSimpleCrypto/src)
|
||||
|
||||
include_directories(${QSIMPLECRYPTO_DIR})
|
||||
|
||||
set(HEADERS ${HEADERS}
|
||||
${QSIMPLECRYPTO_DIR}/include/QAead.h
|
||||
${QSIMPLECRYPTO_DIR}/include/QBlockCipher.h
|
||||
${QSIMPLECRYPTO_DIR}/include/QRsa.h
|
||||
${QSIMPLECRYPTO_DIR}/include/QSimpleCrypto_global.h
|
||||
${QSIMPLECRYPTO_DIR}/include/QX509.h
|
||||
${QSIMPLECRYPTO_DIR}/include/QX509Store.h
|
||||
)
|
||||
|
||||
set(SOURCES ${SOURCES}
|
||||
${QSIMPLECRYPTO_DIR}/sources/QAead.cpp
|
||||
${QSIMPLECRYPTO_DIR}/sources/QBlockCipher.cpp
|
||||
${QSIMPLECRYPTO_DIR}/sources/QRsa.cpp
|
||||
${QSIMPLECRYPTO_DIR}/sources/QX509.cpp
|
||||
${QSIMPLECRYPTO_DIR}/sources/QX509Store.cpp
|
||||
)
|
||||
@@ -23,9 +23,11 @@ set(HEADERS ${HEADERS}
|
||||
${CMAKE_CURRENT_BINARY_DIR}/version.h
|
||||
${CLIENT_ROOT_DIR}/core/sshclient.h
|
||||
${CLIENT_ROOT_DIR}/core/networkUtilities.h
|
||||
${CLIENT_ROOT_DIR}/core/payloadSender.h
|
||||
${CLIENT_ROOT_DIR}/core/serialization/serialization.h
|
||||
${CLIENT_ROOT_DIR}/core/serialization/transfer.h
|
||||
${CLIENT_ROOT_DIR}/../common/logger/logger.h
|
||||
${CLIENT_ROOT_DIR}/../common/crypto/cryptoUtils.h
|
||||
${CLIENT_ROOT_DIR}/utils/qmlUtils.h
|
||||
${CLIENT_ROOT_DIR}/core/api/apiUtils.h
|
||||
${CLIENT_ROOT_DIR}/core/osSignalHandler.h
|
||||
@@ -68,6 +70,7 @@ set(SOURCES ${SOURCES}
|
||||
${CLIENT_ROOT_DIR}/protocols/vpnprotocol.cpp
|
||||
${CLIENT_ROOT_DIR}/core/sshclient.cpp
|
||||
${CLIENT_ROOT_DIR}/core/networkUtilities.cpp
|
||||
${CLIENT_ROOT_DIR}/core/payloadSender.cpp
|
||||
${CLIENT_ROOT_DIR}/core/serialization/outbound.cpp
|
||||
${CLIENT_ROOT_DIR}/core/serialization/inbound.cpp
|
||||
${CLIENT_ROOT_DIR}/core/serialization/ss.cpp
|
||||
@@ -77,6 +80,7 @@ set(SOURCES ${SOURCES}
|
||||
${CLIENT_ROOT_DIR}/core/serialization/vmess.cpp
|
||||
${CLIENT_ROOT_DIR}/core/serialization/vmess_new.cpp
|
||||
${CLIENT_ROOT_DIR}/../common/logger/logger.cpp
|
||||
${CLIENT_ROOT_DIR}/../common/crypto/cryptoUtils.cpp
|
||||
${CLIENT_ROOT_DIR}/utils/qmlUtils.cpp
|
||||
${CLIENT_ROOT_DIR}/core/api/apiUtils.cpp
|
||||
${CLIENT_ROOT_DIR}/core/osSignalHandler.cpp
|
||||
|
||||
@@ -34,7 +34,6 @@ namespace apiDefs
|
||||
constexpr QLatin1String serviceType("service_type");
|
||||
constexpr QLatin1String cliVersion("cli_version");
|
||||
constexpr QLatin1String cliName("cli_name");
|
||||
constexpr QLatin1String supportedProtocols("supported_protocols");
|
||||
|
||||
constexpr QLatin1String vpnKey("vpn_key");
|
||||
constexpr QLatin1String config("config");
|
||||
@@ -52,6 +51,7 @@ namespace apiDefs
|
||||
constexpr QLatin1String appLanguage("app_language");
|
||||
|
||||
constexpr QLatin1String availableCountries("available_countries");
|
||||
constexpr QLatin1String availableProtocols("available_protocols");
|
||||
constexpr QLatin1String activeDeviceCount("active_device_count");
|
||||
constexpr QLatin1String maxDeviceCount("max_device_count");
|
||||
constexpr QLatin1String subscriptionEndDate("subscription_end_date");
|
||||
|
||||
@@ -12,14 +12,13 @@
|
||||
#include <QPromise>
|
||||
#include <QUrl>
|
||||
|
||||
#include "QBlockCipher.h"
|
||||
#include "QRsa.h"
|
||||
#include <openssl/rsa.h>
|
||||
|
||||
#include "amnezia_application.h"
|
||||
#include "cryptoUtils.h"
|
||||
#include "core/api/apiUtils.h"
|
||||
#include "core/networkUtilities.h"
|
||||
#include "settings.h"
|
||||
#include "utilities.h"
|
||||
|
||||
#ifdef AMNEZIA_DESKTOP
|
||||
#include "core/ipcclient.h"
|
||||
@@ -69,27 +68,24 @@ namespace
|
||||
|
||||
bool decryptProxyUrlsPayload(const QByteArray &encryptedPayload, bool isDevEnvironment, QByteArray &decryptedPayload)
|
||||
{
|
||||
try {
|
||||
QByteArray key = isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
|
||||
if (!isDevEnvironment) {
|
||||
QCryptographicHash hash(QCryptographicHash::Sha512);
|
||||
hash.addData(key);
|
||||
QByteArray h = hash.result().toHex();
|
||||
QByteArray key = isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
|
||||
if (!isDevEnvironment) {
|
||||
QCryptographicHash hash(QCryptographicHash::Sha512);
|
||||
hash.addData(key);
|
||||
QByteArray h = hash.result().toHex();
|
||||
|
||||
QByteArray decKey = QByteArray::fromHex(h.left(64));
|
||||
QByteArray iv = QByteArray::fromHex(h.mid(64, 32));
|
||||
QByteArray ba = QByteArray::fromBase64(encryptedPayload);
|
||||
QByteArray decKey = QByteArray::fromHex(h.left(64));
|
||||
QByteArray iv = QByteArray::fromHex(h.mid(64, 32));
|
||||
QByteArray ba = QByteArray::fromBase64(encryptedPayload);
|
||||
|
||||
QSimpleCrypto::QBlockCipher cipher;
|
||||
decryptedPayload = cipher.decryptAesBlockCipher(ba, decKey, iv);
|
||||
} else {
|
||||
decryptedPayload = encryptedPayload;
|
||||
decryptedPayload = CryptoUtils::decryptAes256Cbc(ba, decKey, iv);
|
||||
if (decryptedPayload.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch (...) {
|
||||
Utils::logException();
|
||||
return false;
|
||||
} else {
|
||||
decryptedPayload = encryptedPayload;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
QStringList readCachedProxyUrls(const QByteArray &cachedProxyUrlsEncrypted, bool isDevEnvironment)
|
||||
@@ -157,40 +153,29 @@ GatewayController::EncryptedRequestData GatewayController::prepareRequest(const
|
||||
}
|
||||
#endif
|
||||
|
||||
QSimpleCrypto::QBlockCipher blockCipher;
|
||||
encRequestData.key = blockCipher.generatePrivateSalt(32);
|
||||
encRequestData.iv = blockCipher.generatePrivateSalt(32);
|
||||
encRequestData.salt = blockCipher.generatePrivateSalt(8);
|
||||
encRequestData.key = CryptoUtils::generateRandomBytes(32);
|
||||
encRequestData.iv = CryptoUtils::generateRandomBytes(32);
|
||||
encRequestData.salt = CryptoUtils::generateRandomBytes(8);
|
||||
|
||||
QJsonObject keyPayload;
|
||||
keyPayload[configKey::aesKey] = QString(encRequestData.key.toBase64());
|
||||
keyPayload[configKey::aesIv] = QString(encRequestData.iv.toBase64());
|
||||
keyPayload[configKey::aesSalt] = QString(encRequestData.salt.toBase64());
|
||||
|
||||
QByteArray encryptedKeyPayload;
|
||||
QByteArray encryptedApiPayload;
|
||||
try {
|
||||
QSimpleCrypto::QRsa rsa;
|
||||
QByteArray rsaKey = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
|
||||
EVP_PKEY *publicKey = CryptoUtils::loadPublicKeyFromPem(rsaKey);
|
||||
if (publicKey == nullptr) {
|
||||
qCritical() << "error loading public key from environment variables";
|
||||
encRequestData.errorCode = ErrorCode::ApiMissingAgwPublicKey;
|
||||
return encRequestData;
|
||||
}
|
||||
|
||||
EVP_PKEY *publicKey = nullptr;
|
||||
try {
|
||||
QByteArray rsaKey = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
|
||||
QSimpleCrypto::QRsa rsa;
|
||||
publicKey = rsa.getPublicKeyFromByteArray(rsaKey);
|
||||
} catch (...) {
|
||||
Utils::logException();
|
||||
qCritical() << "error loading public key from environment variables";
|
||||
encRequestData.errorCode = ErrorCode::ApiMissingAgwPublicKey;
|
||||
return encRequestData;
|
||||
}
|
||||
QByteArray encryptedKeyPayload = CryptoUtils::rsaEncrypt(QJsonDocument(keyPayload).toJson(), publicKey, RSA_PKCS1_PADDING);
|
||||
EVP_PKEY_free(publicKey);
|
||||
|
||||
encryptedKeyPayload = rsa.encrypt(QJsonDocument(keyPayload).toJson(), publicKey, RSA_PKCS1_PADDING);
|
||||
EVP_PKEY_free(publicKey);
|
||||
QByteArray encryptedApiPayload = CryptoUtils::encryptAes256Cbc(QJsonDocument(apiPayload).toJson(), encRequestData.key, encRequestData.iv);
|
||||
|
||||
encryptedApiPayload = blockCipher.encryptAesBlockCipher(QJsonDocument(apiPayload).toJson(), encRequestData.key, encRequestData.iv,
|
||||
"", encRequestData.salt);
|
||||
} catch (...) {
|
||||
Utils::logException();
|
||||
if (encryptedKeyPayload.isEmpty() || encryptedApiPayload.isEmpty()) {
|
||||
qCritical() << "error when encrypting the request body";
|
||||
encRequestData.errorCode = ErrorCode::ApiConfigDecryptionError;
|
||||
return encRequestData;
|
||||
@@ -212,11 +197,11 @@ GatewayController::DecryptionResult GatewayController::tryDecryptResponseBody(co
|
||||
result.decryptedBody = encryptedResponseBody;
|
||||
result.isDecryptionSuccessful = false;
|
||||
|
||||
try {
|
||||
QSimpleCrypto::QBlockCipher blockCipher;
|
||||
result.decryptedBody = blockCipher.decryptAesBlockCipher(encryptedResponseBody, key, iv, "", salt);
|
||||
QByteArray decrypted = CryptoUtils::decryptAes256Cbc(encryptedResponseBody, key, iv);
|
||||
if (!decrypted.isEmpty()) {
|
||||
result.decryptedBody = decrypted;
|
||||
result.isDecryptionSuccessful = true;
|
||||
} catch (...) {
|
||||
} else {
|
||||
result.decryptedBody = encryptedResponseBody;
|
||||
result.isDecryptionSuccessful = false;
|
||||
}
|
||||
@@ -335,7 +320,6 @@ QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString
|
||||
}
|
||||
|
||||
if (!decryptionResult.isDecryptionSuccessful) {
|
||||
Utils::logException();
|
||||
qCritical() << "error when decrypting the request body";
|
||||
promise->addResult(qMakePair(ErrorCode::ApiConfigDecryptionError, QByteArray()));
|
||||
promise->finish();
|
||||
|
||||
198
client/core/payloadSender.cpp
Normal file
198
client/core/payloadSender.cpp
Normal file
@@ -0,0 +1,198 @@
|
||||
#include "payloadSender.h"
|
||||
|
||||
#include <QDebug>
|
||||
#include <QEventLoop>
|
||||
#include <QHostAddress>
|
||||
#include <QJsonObject>
|
||||
#include <QRandomGenerator>
|
||||
#include <QRegularExpression>
|
||||
#include <QTcpSocket>
|
||||
#include <QTimer>
|
||||
#include <QUdpSocket>
|
||||
|
||||
#include "core/networkUtilities.h"
|
||||
#include "protocols/protocols_defs.h"
|
||||
|
||||
using namespace amnezia;
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr char protoUdp[] = "udp";
|
||||
constexpr char protoTcp[] = "tcp";
|
||||
|
||||
QByteArray randomBytes(int count)
|
||||
{
|
||||
if (count <= 0) {
|
||||
return {};
|
||||
}
|
||||
QByteArray bytes(count, Qt::Uninitialized);
|
||||
for (int i = 0; i < count; ++i) {
|
||||
bytes[i] = static_cast<char>(QRandomGenerator::global()->bounded(256));
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
bool parseEndpoint(const QString &endpoint, QString &host, quint16 &port)
|
||||
{
|
||||
const int separatorIndex = endpoint.lastIndexOf(QLatin1Char(':'));
|
||||
if (separatorIndex <= 0 || separatorIndex == endpoint.size() - 1) {
|
||||
return false;
|
||||
}
|
||||
host = endpoint.left(separatorIndex);
|
||||
bool ok = false;
|
||||
const uint parsedPort = endpoint.mid(separatorIndex + 1).toUInt(&ok);
|
||||
if (!ok || parsedPort == 0 || parsedPort > 65535) {
|
||||
return false;
|
||||
}
|
||||
port = static_cast<quint16>(parsedPort);
|
||||
return true;
|
||||
}
|
||||
|
||||
enum class ExchangeResult { Sent, Matched, Mismatch, Timeout };
|
||||
|
||||
ExchangeResult waitForResponse(QAbstractSocket &socket, const QByteArray &expectedResponse, int timeoutMs)
|
||||
{
|
||||
if (expectedResponse.isEmpty()) {
|
||||
return ExchangeResult::Sent;
|
||||
}
|
||||
|
||||
QByteArray response;
|
||||
QEventLoop loop;
|
||||
QTimer timer;
|
||||
timer.setSingleShot(true);
|
||||
QObject::connect(&timer, &QTimer::timeout, &loop, &QEventLoop::quit);
|
||||
QObject::connect(&socket, &QIODevice::readyRead, &loop, [&]() {
|
||||
response.append(socket.readAll());
|
||||
if (response.size() >= expectedResponse.size()) {
|
||||
loop.quit();
|
||||
}
|
||||
});
|
||||
|
||||
timer.start(timeoutMs);
|
||||
loop.exec();
|
||||
|
||||
if (response.isEmpty()) {
|
||||
return ExchangeResult::Timeout;
|
||||
}
|
||||
return response == expectedResponse ? ExchangeResult::Matched : ExchangeResult::Mismatch;
|
||||
}
|
||||
}
|
||||
|
||||
QByteArray PayloadSender::buildPayload(const QString &tagString)
|
||||
{
|
||||
QByteArray result;
|
||||
if (tagString.isEmpty()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
static const QRegularExpression tagRegExp(QStringLiteral("<([^>]*)>"));
|
||||
QRegularExpressionMatchIterator it = tagRegExp.globalMatch(tagString);
|
||||
while (it.hasNext()) {
|
||||
const QRegularExpressionMatch match = it.next();
|
||||
const QStringList parts = match.captured(1).simplified().split(QLatin1Char(' '), Qt::SkipEmptyParts);
|
||||
if (parts.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const QString tag = parts.at(0).toLower();
|
||||
if (tag == QLatin1String("b")) {
|
||||
if (parts.size() < 2) {
|
||||
continue;
|
||||
}
|
||||
QString hex = parts.at(1);
|
||||
if (hex.startsWith(QLatin1String("0x"), Qt::CaseInsensitive)) {
|
||||
hex = hex.mid(2);
|
||||
}
|
||||
result.append(QByteArray::fromHex(hex.toLatin1()));
|
||||
} else if (tag == QLatin1String("r")) {
|
||||
if (parts.size() < 2) {
|
||||
continue;
|
||||
}
|
||||
bool ok = false;
|
||||
const int count = parts.at(1).toInt(&ok);
|
||||
if (ok) {
|
||||
result.append(randomBytes(count));
|
||||
}
|
||||
} else {
|
||||
qWarning() << "PayloadSender: unknown payload tag" << tag;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void PayloadSender::sendAll(const QJsonArray &sendPayload)
|
||||
{
|
||||
for (int i = 0; i < sendPayload.size(); ++i) {
|
||||
const QJsonValue value = sendPayload.at(i);
|
||||
if (!value.isObject()) {
|
||||
continue;
|
||||
}
|
||||
sendEntry(value.toObject(), i);
|
||||
}
|
||||
}
|
||||
|
||||
void PayloadSender::sendEntry(const QJsonObject &entry, int index)
|
||||
{
|
||||
const QString endpoint = entry.value(config_key::sendPayloadEndpoint).toString();
|
||||
QString host;
|
||||
quint16 port = 0;
|
||||
if (!parseEndpoint(endpoint, host, port)) {
|
||||
qWarning() << "PayloadSender: skipping entry" << index << "- invalid endpoint" << endpoint;
|
||||
return;
|
||||
}
|
||||
|
||||
const QString protocol = entry.value(config_key::sendPayloadProtocol).toString().toLower();
|
||||
const int timeoutMs = entry.value(config_key::sendPayloadTimeoutMs).toInt();
|
||||
const QByteArray payload = buildPayload(entry.value(config_key::sendPayloadData).toString());
|
||||
const QByteArray expectedResponse = buildPayload(entry.value(config_key::sendPayloadExpectedResponse).toString());
|
||||
|
||||
QUdpSocket udpSocket;
|
||||
QTcpSocket tcpSocket;
|
||||
QAbstractSocket *socket = nullptr;
|
||||
if (protocol == protoUdp) {
|
||||
socket = &udpSocket;
|
||||
} else if (protocol == protoTcp) {
|
||||
socket = &tcpSocket;
|
||||
} else {
|
||||
qWarning() << "PayloadSender: skipping entry" << index << "- unsupported protocol" << protocol;
|
||||
return;
|
||||
}
|
||||
|
||||
const QString resolvedHost = NetworkUtilities::getIPAddress(host);
|
||||
const QHostAddress hostAddress(resolvedHost.isEmpty() ? host : resolvedHost);
|
||||
|
||||
socket->connectToHost(hostAddress, port);
|
||||
if (!socket->waitForConnected(timeoutMs)) {
|
||||
qWarning() << "PayloadSender: entry" << index << "-" << protocol << "connect failed to" << endpoint << socket->errorString();
|
||||
return;
|
||||
}
|
||||
|
||||
bool writeOk = false;
|
||||
if (protocol == protoUdp) {
|
||||
writeOk = udpSocket.writeDatagram(payload, hostAddress, port) >= 0;
|
||||
} else {
|
||||
writeOk = tcpSocket.write(payload) == payload.size();
|
||||
tcpSocket.flush();
|
||||
}
|
||||
|
||||
if (!writeOk) {
|
||||
qWarning() << "PayloadSender: entry" << index << "-" << protocol << "write failed to" << endpoint;
|
||||
return;
|
||||
}
|
||||
|
||||
switch (waitForResponse(*socket, expectedResponse, timeoutMs)) {
|
||||
case ExchangeResult::Sent:
|
||||
qInfo() << "PayloadSender: entry" << index << "-" << protocol << "payload sent to" << endpoint;
|
||||
break;
|
||||
case ExchangeResult::Matched:
|
||||
qInfo() << "PayloadSender: entry" << index << "-" << protocol << "expected response received from" << endpoint;
|
||||
break;
|
||||
case ExchangeResult::Mismatch:
|
||||
qWarning() << "PayloadSender: entry" << index << "-" << protocol << "response mismatch from" << endpoint;
|
||||
break;
|
||||
case ExchangeResult::Timeout:
|
||||
qWarning() << "PayloadSender: entry" << index << "-" << protocol << "response timeout from" << endpoint;
|
||||
break;
|
||||
}
|
||||
}
|
||||
35
client/core/payloadSender.h
Normal file
35
client/core/payloadSender.h
Normal file
@@ -0,0 +1,35 @@
|
||||
#ifndef PAYLOADSENDER_H
|
||||
#define PAYLOADSENDER_H
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QJsonArray>
|
||||
#include <QString>
|
||||
|
||||
/*
|
||||
* Handles the optional "send_payload" block that may be attached to a server
|
||||
* configuration received from the gateway. Before establishing the VPN tunnel
|
||||
* the client sends the described UDP/TCP packet(s) to the given endpoint(s)
|
||||
* (port-knocking / DPI warm-up) and, when an expected response is provided,
|
||||
* waits for it within the given timeout.
|
||||
*
|
||||
* The payload and expected response use the AmneziaWG special-junk tag syntax:
|
||||
* <b 0xHEX> - append the literal bytes decoded from the hex string
|
||||
* <r N> - append N random bytes
|
||||
* Unknown tags are ignored.
|
||||
*/
|
||||
class PayloadSender
|
||||
{
|
||||
public:
|
||||
// Sends every entry of the "send_payload" array. A failed / mismatched /
|
||||
// timed-out entry is skipped and processing continues with the next one.
|
||||
// Always best-effort: never blocks the connection from proceeding.
|
||||
static void sendAll(const QJsonArray &sendPayload);
|
||||
|
||||
// Exposed for reuse/testing: builds a byte buffer from the tag string.
|
||||
static QByteArray buildPayload(const QString &tagString);
|
||||
|
||||
private:
|
||||
static void sendEntry(const QJsonObject &entry, int index);
|
||||
};
|
||||
|
||||
#endif // PAYLOADSENDER_H
|
||||
6
client/images/controls/arrow-left-right.svg
Normal file
6
client/images/controls/arrow-left-right.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M17 11L21 7L17 3" stroke="#D7D8DB" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M21 7H9" stroke="#D7D8DB" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M7 21L3 17L7 13" stroke="#D7D8DB" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M15 17H3" stroke="#D7D8DB" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 525 B |
@@ -98,6 +98,13 @@ namespace amnezia
|
||||
|
||||
constexpr char configVersion[] = "config_version";
|
||||
|
||||
constexpr char sendPayload[] = "send_payload";
|
||||
constexpr char sendPayloadEndpoint[] = "endpoint";
|
||||
constexpr char sendPayloadTimeoutMs[] = "timeout_ms";
|
||||
constexpr char sendPayloadProtocol[] = "protocol";
|
||||
constexpr char sendPayloadData[] = "payload";
|
||||
constexpr char sendPayloadExpectedResponse[] = "expected_response";
|
||||
|
||||
constexpr char splitTunnelSites[] = "splitTunnelSites";
|
||||
constexpr char splitTunnelType[] = "splitTunnelType";
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
<file>images/controls/app.svg</file>
|
||||
<file>images/controls/archive-restore.svg</file>
|
||||
<file>images/controls/arrow-left.svg</file>
|
||||
<file>images/controls/arrow-left-right.svg</file>
|
||||
<file>images/controls/arrow-right.svg</file>
|
||||
<file>images/controls/bug.svg</file>
|
||||
<file>images/controls/check.svg</file>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#include "secure_qsettings.h"
|
||||
|
||||
#include "../client/3rd/QSimpleCrypto/src/include/QAead.h"
|
||||
#include "../client/3rd/QSimpleCrypto/src/include/QBlockCipher.h"
|
||||
#include "cryptoUtils.h"
|
||||
#include "utilities.h"
|
||||
#include <QDataStream>
|
||||
#include <QDebug>
|
||||
@@ -180,11 +179,8 @@ void SecureQSettings::clearSettings()
|
||||
|
||||
QByteArray SecureQSettings::encryptText(const QByteArray &value) const
|
||||
{
|
||||
QSimpleCrypto::QBlockCipher cipher;
|
||||
QByteArray result;
|
||||
try {
|
||||
result = cipher.encryptAesBlockCipher(value, getEncKey(), getEncIv());
|
||||
} catch (...) { // todo change error handling in QSimpleCrypto?
|
||||
QByteArray result = CryptoUtils::encryptAes256Cbc(value, getEncKey(), getEncIv());
|
||||
if (result.isEmpty() && !value.isEmpty()) {
|
||||
qCritical() << "error when encrypting the settings value";
|
||||
}
|
||||
return result;
|
||||
@@ -192,11 +188,8 @@ QByteArray SecureQSettings::encryptText(const QByteArray &value) const
|
||||
|
||||
QByteArray SecureQSettings::decryptText(const QByteArray &ba) const
|
||||
{
|
||||
QSimpleCrypto::QBlockCipher cipher;
|
||||
QByteArray result;
|
||||
try {
|
||||
result = cipher.decryptAesBlockCipher(ba, getEncKey(), getEncIv());
|
||||
} catch (...) { // todo change error handling in QSimpleCrypto?
|
||||
QByteArray result = CryptoUtils::decryptAes256Cbc(ba, getEncKey(), getEncIv());
|
||||
if (result.isEmpty() && !ba.isEmpty()) {
|
||||
qCritical() << "error when decrypting the settings value";
|
||||
}
|
||||
return result;
|
||||
@@ -218,8 +211,7 @@ QByteArray SecureQSettings::getEncKey() const
|
||||
|
||||
if (m_key.isEmpty()) {
|
||||
// Create new key
|
||||
QSimpleCrypto::QBlockCipher cipher;
|
||||
QByteArray key = cipher.generatePrivateSalt(32);
|
||||
QByteArray key = CryptoUtils::generateRandomBytes(32);
|
||||
if (key.isEmpty()) {
|
||||
qCritical() << "SecureQSettings::getEncKey Unable to generate new enc key";
|
||||
}
|
||||
@@ -244,8 +236,7 @@ QByteArray SecureQSettings::getEncIv() const
|
||||
|
||||
if (m_iv.isEmpty()) {
|
||||
// Create new IV
|
||||
QSimpleCrypto::QBlockCipher cipher;
|
||||
QByteArray iv = cipher.generatePrivateSalt(32);
|
||||
QByteArray iv = CryptoUtils::generateRandomBytes(32);
|
||||
if (iv.isEmpty()) {
|
||||
qCritical() << "SecureQSettings::getEncIv Unable to generate new enc IV";
|
||||
}
|
||||
|
||||
@@ -227,6 +227,10 @@ namespace
|
||||
serverConfig[config_key::containers] = newServerConfig.value(config_key::containers);
|
||||
serverConfig[config_key::hostName] = newServerConfig.value(config_key::hostName);
|
||||
|
||||
if (newServerConfig.contains(config_key::sendPayload)) {
|
||||
serverConfig[config_key::sendPayload] = newServerConfig.value(config_key::sendPayload);
|
||||
}
|
||||
|
||||
if (newServerConfig.value(config_key::configVersion).toInt() == apiDefs::ConfigSource::AmneziaGateway) {
|
||||
serverConfig[config_key::configVersion] = newServerConfig.value(config_key::configVersion);
|
||||
serverConfig[config_key::description] = newServerConfig.value(config_key::description);
|
||||
@@ -241,9 +245,6 @@ namespace
|
||||
auto apiConfig = QJsonObject::fromVariantMap(map);
|
||||
|
||||
if (newServerConfig.value(config_key::configVersion).toInt() == apiDefs::ConfigSource::AmneziaGateway) {
|
||||
apiConfig.insert(apiDefs::key::supportedProtocols,
|
||||
QJsonDocument::fromJson(apiResponseBody).object().value(apiDefs::key::supportedProtocols).toArray());
|
||||
|
||||
apiConfig.insert(apiDefs::key::serviceInfo,
|
||||
QJsonDocument::fromJson(apiResponseBody).object().value(apiDefs::key::serviceInfo).toObject());
|
||||
}
|
||||
@@ -936,6 +937,23 @@ bool ApiConfigsController::updateServiceFromGateway(const int serverIndex, const
|
||||
auto serverConfig = m_serversModel->getServerConfig(serverIndex);
|
||||
auto apiConfig = serverConfig.value(configKey::apiConfig).toObject();
|
||||
|
||||
if (!newCountryCode.isEmpty()) {
|
||||
const auto currentProtocol = apiConfig.value(configKey::serviceProtocol).toString();
|
||||
const auto availableCountries = apiConfig.value(apiDefs::key::availableCountries).toArray();
|
||||
for (const auto &country : availableCountries) {
|
||||
const auto countryObject = country.toObject();
|
||||
if (countryObject.value(apiDefs::key::serverCountryCode).toString() != newCountryCode) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto availableProtocols = countryObject.value(apiDefs::key::availableProtocols).toArray();
|
||||
if (!availableProtocols.isEmpty() && !availableProtocols.contains(currentProtocol)) {
|
||||
apiConfig[configKey::serviceProtocol] = availableProtocols.first().toString();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
GatewayRequestData gatewayRequestData { QSysInfo::productType(),
|
||||
QString(APP_VERSION),
|
||||
m_settings->getAppLanguage().name().split("_").first(),
|
||||
|
||||
@@ -6,9 +6,13 @@
|
||||
#include <QApplication>
|
||||
#endif
|
||||
|
||||
#include <QJsonArray>
|
||||
|
||||
#include "amnezia_application.h"
|
||||
#include "utilities.h"
|
||||
#include "core/controllers/vpnConfigurationController.h"
|
||||
#include "core/payloadSender.h"
|
||||
#include "protocols/protocols_defs.h"
|
||||
#include "version.h"
|
||||
|
||||
ConnectionController::ConnectionController(const QSharedPointer<ServersModel> &serversModel,
|
||||
@@ -61,6 +65,12 @@ void ConnectionController::openConnection()
|
||||
auto dns = m_serversModel->getDnsPair(serverIndex);
|
||||
|
||||
auto vpnConfiguration = vpnConfigurationController.createVpnConfiguration(dns, serverConfig, containerConfig, container);
|
||||
|
||||
const QJsonArray sendPayload = serverConfig.value(config_key::sendPayload).toArray();
|
||||
if (!sendPayload.isEmpty()) {
|
||||
PayloadSender::sendAll(sendPayload);
|
||||
}
|
||||
|
||||
emit connectToVpn(serverIndex, credentials, container, vpnConfiguration);
|
||||
}
|
||||
|
||||
|
||||
@@ -73,12 +73,6 @@ QVariant ApiAccountInfoModel::data(const QModelIndex &index, int role) const
|
||||
}
|
||||
return false;
|
||||
}
|
||||
case IsProtocolSelectionSupportedRole: {
|
||||
if (m_accountInfoData.supportedProtocols.size() > 1) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
case IsSubscriptionExpiredRole: {
|
||||
if (m_accountInfoData.configType == apiDefs::ConfigType::AmneziaFreeV3) {
|
||||
return false;
|
||||
@@ -132,10 +126,6 @@ void ApiAccountInfoModel::updateModel(const QJsonObject &accountInfoObject, cons
|
||||
accountInfoData.subscriptionDescription = accountInfoObject.value(apiDefs::key::subscriptionDescription).toString();
|
||||
accountInfoData.isRenewalAvailable = accountInfoObject.value(apiDefs::key::isRenewalAvailable).toBool(false);
|
||||
|
||||
for (const auto &protocol : accountInfoObject.value(apiDefs::key::supportedProtocols).toArray()) {
|
||||
accountInfoData.supportedProtocols.push_back(protocol.toString());
|
||||
}
|
||||
|
||||
m_accountInfoData = accountInfoData;
|
||||
|
||||
m_supportInfo = accountInfoObject.value(apiDefs::key::supportInfo).toObject();
|
||||
@@ -201,7 +191,6 @@ QHash<int, QByteArray> ApiAccountInfoModel::roleNames() const
|
||||
roles[IsComponentVisibleRole] = "isComponentVisible";
|
||||
roles[IsSubscriptionRenewalAvailableRole] = "isSubscriptionRenewalAvailable";
|
||||
roles[HasExpiredWorkerRole] = "hasExpiredWorker";
|
||||
roles[IsProtocolSelectionSupportedRole] = "isProtocolSelectionSupported";
|
||||
roles[IsSubscriptionExpiredRole] = "isSubscriptionExpired";
|
||||
roles[IsSubscriptionExpiringSoonRole] = "isSubscriptionExpiringSoon";
|
||||
roles[IsInAppPurchaseRole] = "isInAppPurchase";
|
||||
|
||||
@@ -20,7 +20,6 @@ public:
|
||||
IsComponentVisibleRole,
|
||||
IsSubscriptionRenewalAvailableRole,
|
||||
HasExpiredWorkerRole,
|
||||
IsProtocolSelectionSupportedRole,
|
||||
IsSubscriptionExpiredRole,
|
||||
IsSubscriptionExpiringSoonRole,
|
||||
IsInAppPurchaseRole
|
||||
@@ -56,8 +55,6 @@ private:
|
||||
|
||||
apiDefs::ConfigType configType;
|
||||
|
||||
QStringList supportedProtocols;
|
||||
|
||||
QString subscriptionDescription;
|
||||
|
||||
bool isInAppPurchase = false;
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
#include "servers_model.h"
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QStringList>
|
||||
|
||||
#include "core/api/apiDefs.h"
|
||||
#include "core/controllers/serverController.h"
|
||||
#include "core/networkUtilities.h"
|
||||
@@ -163,6 +166,26 @@ QVariant ServersModel::data(const QModelIndex &index, int role) const
|
||||
case ApiServerCountryCodeRole: {
|
||||
return apiConfig.value(configKey::serverCountryCode).toString();
|
||||
}
|
||||
case ApiServiceProtocolRole: {
|
||||
return apiConfig.value(configKey::serviceProtocol).toString();
|
||||
}
|
||||
case ApiAvailableProtocolsRole: {
|
||||
const auto currentCountryCode = apiConfig.value(configKey::serverCountryCode).toString();
|
||||
const auto availableCountries = apiConfig.value(configKey::availableCountries).toArray();
|
||||
|
||||
QStringList availableProtocols;
|
||||
for (const auto &country : availableCountries) {
|
||||
const auto countryObject = country.toObject();
|
||||
if (countryObject.value(configKey::serverCountryCode).toString() != currentCountryCode) {
|
||||
continue;
|
||||
}
|
||||
for (const auto &protocol : countryObject.value(apiDefs::key::availableProtocols).toArray()) {
|
||||
availableProtocols.push_back(protocol.toString());
|
||||
}
|
||||
break;
|
||||
}
|
||||
return availableProtocols;
|
||||
}
|
||||
case HasAmneziaDns: {
|
||||
QString primaryDns = server.value(config_key::dns1).toString();
|
||||
return primaryDns == protocols::dns::amneziaDnsIp;
|
||||
@@ -471,6 +494,8 @@ QHash<int, QByteArray> ServersModel::roleNames() const
|
||||
roles[IsCountrySelectionAvailableRole] = "isCountrySelectionAvailable";
|
||||
roles[ApiAvailableCountriesRole] = "apiAvailableCountries";
|
||||
roles[ApiServerCountryCodeRole] = "apiServerCountryCode";
|
||||
roles[ApiServiceProtocolRole] = "apiServiceProtocol";
|
||||
roles[ApiAvailableProtocolsRole] = "apiAvailableProtocols";
|
||||
|
||||
roles[IsAdVisibleRole] = "isAdVisible";
|
||||
roles[AdHeaderRole] = "adHeader";
|
||||
|
||||
@@ -47,6 +47,8 @@ public:
|
||||
IsCountrySelectionAvailableRole,
|
||||
ApiAvailableCountriesRole,
|
||||
ApiServerCountryCodeRole,
|
||||
ApiServiceProtocolRole,
|
||||
ApiAvailableProtocolsRole,
|
||||
IsAdVisibleRole,
|
||||
AdHeaderRole,
|
||||
AdDescriptionRole,
|
||||
|
||||
@@ -20,6 +20,41 @@ import "../Components"
|
||||
PageType {
|
||||
id: root
|
||||
|
||||
property var apiAvailableProtocols: []
|
||||
property string apiCurrentProtocol: ""
|
||||
|
||||
readonly property bool isApiProtocolSelectionVisible: ServersModel.isDefaultServerFromApi && root.apiAvailableProtocols.length > 0
|
||||
|
||||
function updateApiProtocolState() {
|
||||
if (ServersModel.isDefaultServerFromApi) {
|
||||
root.apiAvailableProtocols = ServersModel.getDefaultServerData("apiAvailableProtocols")
|
||||
root.apiCurrentProtocol = ServersModel.getDefaultServerData("apiServiceProtocol")
|
||||
} else {
|
||||
root.apiAvailableProtocols = []
|
||||
root.apiCurrentProtocol = ""
|
||||
}
|
||||
}
|
||||
|
||||
function protocolDisplayName(protocol) {
|
||||
switch (protocol) {
|
||||
case "awg": return "AmneziaWG"
|
||||
case "vless": return "VLESS"
|
||||
default: return protocol
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
root.updateApiProtocolState()
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: ServersModel
|
||||
|
||||
function onDefaultServerDefaultContainerChanged() {
|
||||
root.updateApiProtocolState()
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Qt.application
|
||||
|
||||
@@ -209,6 +244,10 @@ PageType {
|
||||
drawer.collapsedHeight = collapsed.implicitHeight
|
||||
}
|
||||
|
||||
onImplicitHeightChanged: {
|
||||
drawer.collapsedHeight = collapsed.implicitHeight
|
||||
}
|
||||
|
||||
DividerType {
|
||||
Layout.topMargin: 10
|
||||
Layout.fillWidth: false
|
||||
@@ -310,7 +349,7 @@ PageType {
|
||||
objectName: "rowLayoutLabel"
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
|
||||
Layout.topMargin: 8
|
||||
Layout.bottomMargin: drawer.isCollapsedStateActive ? 44 : ServersModel.isDefaultServerFromApi ? 61 : 16
|
||||
Layout.bottomMargin: root.isApiProtocolSelectionVisible ? 8 : (drawer.isCollapsedStateActive ? 44 : ServersModel.isDefaultServerFromApi ? 61 : 16)
|
||||
spacing: 0
|
||||
|
||||
BasicButtonType {
|
||||
@@ -364,6 +403,61 @@ PageType {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
objectName: "protocolRowLayout"
|
||||
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
|
||||
Layout.bottomMargin: drawer.isCollapsedStateActive ? 44 : 61
|
||||
spacing: 0
|
||||
|
||||
visible: root.isApiProtocolSelectionVisible
|
||||
|
||||
BasicButtonType {
|
||||
id: protocolButton
|
||||
objectName: "protocolButton"
|
||||
|
||||
enabled: root.apiAvailableProtocols.length > 1
|
||||
hoverEnabled: enabled
|
||||
|
||||
implicitHeight: 36
|
||||
|
||||
leftPadding: 16
|
||||
rightPadding: 16
|
||||
|
||||
defaultColor: AmneziaStyle.color.transparent
|
||||
hoveredColor: AmneziaStyle.color.translucentWhite
|
||||
pressedColor: AmneziaStyle.color.sheerWhite
|
||||
disabledColor: AmneziaStyle.color.transparent
|
||||
textColor: AmneziaStyle.color.mutedGray
|
||||
|
||||
buttonTextLabel.lineHeight: 16
|
||||
buttonTextLabel.font.pixelSize: 13
|
||||
buttonTextLabel.font.weight: 400
|
||||
|
||||
text: root.apiAvailableProtocols.length > 1
|
||||
? root.protocolDisplayName(root.apiCurrentProtocol)
|
||||
: root.protocolDisplayName(root.apiAvailableProtocols[0])
|
||||
leftImageSource: "qrc:/images/controls/arrow-left-right.svg"
|
||||
leftImageColor: AmneziaStyle.color.mutedGray
|
||||
|
||||
rightImageSource: enabled ? "qrc:/images/controls/chevron-down.svg" : ""
|
||||
|
||||
Keys.onEnterPressed: this.clicked()
|
||||
Keys.onReturnPressed: this.clicked()
|
||||
|
||||
onClicked: {
|
||||
if (ConnectionController.isConnectionInProgress) {
|
||||
PageController.showNotificationMessage(qsTr("Unable change protocol while trying to make an active connection"))
|
||||
return
|
||||
}
|
||||
if (ConnectionController.isConnected) {
|
||||
PageController.showNotificationMessage(qsTr("Cannot change protocol during active connection"))
|
||||
return
|
||||
}
|
||||
protocolSelectionDrawer.openTriggered()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
@@ -476,4 +570,119 @@ PageType {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DrawerType2 {
|
||||
id: protocolSelectionDrawer
|
||||
objectName: "protocolSelectionDrawer"
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
expandedStateContent: Item {
|
||||
id: protocolDrawerContainer
|
||||
|
||||
implicitHeight: root.height * 0.5
|
||||
|
||||
Component.onCompleted: {
|
||||
protocolSelectionDrawer.expandedHeight = protocolDrawerContainer.implicitHeight
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: protocolDrawerHeader
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: 16
|
||||
|
||||
BackButtonType {
|
||||
id: protocolDrawerBackButton
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
backButtonImage: "qrc:/images/controls/arrow-left.svg"
|
||||
backButtonFunction: function() { protocolSelectionDrawer.closeTriggered() }
|
||||
}
|
||||
|
||||
Header2Type {
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
Layout.rightMargin: 16
|
||||
|
||||
headerText: qsTr("VPN protocol")
|
||||
}
|
||||
}
|
||||
|
||||
ListViewType {
|
||||
id: protocolDrawerListView
|
||||
|
||||
anchors.top: protocolDrawerHeader.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.topMargin: 16
|
||||
|
||||
model: root.apiAvailableProtocols
|
||||
|
||||
ButtonGroup {
|
||||
id: protocolDrawerButtonGroup
|
||||
}
|
||||
|
||||
delegate: Item {
|
||||
implicitWidth: protocolDrawerListView.width
|
||||
implicitHeight: protocolDrawerDelegate.implicitHeight
|
||||
|
||||
ColumnLayout {
|
||||
id: protocolDrawerDelegate
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 16
|
||||
anchors.rightMargin: 16
|
||||
|
||||
VerticalRadioButton {
|
||||
id: protocolDrawerRadioButton
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
text: root.protocolDisplayName(modelData)
|
||||
|
||||
ButtonGroup.group: protocolDrawerButtonGroup
|
||||
|
||||
checkable: !ConnectionController.isConnected
|
||||
checked: modelData === root.apiCurrentProtocol
|
||||
|
||||
onClicked: {
|
||||
protocolSelectionDrawer.closeTriggered()
|
||||
|
||||
if (modelData === root.apiCurrentProtocol) {
|
||||
return
|
||||
}
|
||||
|
||||
if (ConnectionController.isConnected) {
|
||||
PageController.showNotificationMessage(qsTr("Cannot change protocol during active connection"))
|
||||
return
|
||||
}
|
||||
|
||||
PageController.showBusyIndicator(true)
|
||||
ServersModel.processedIndex = ServersModel.defaultIndex
|
||||
ApiConfigsController.setCurrentProtocol(modelData)
|
||||
if (!ApiConfigsController.updateServiceFromGateway(ServersModel.defaultIndex, "", "", true)) {
|
||||
ApiConfigsController.setCurrentProtocol(root.apiCurrentProtocol)
|
||||
}
|
||||
root.updateApiProtocolState()
|
||||
PageController.showBusyIndicator(false)
|
||||
}
|
||||
|
||||
Keys.onEnterPressed: protocolDrawerRadioButton.clicked()
|
||||
Keys.onReturnPressed: protocolDrawerRadioButton.clicked()
|
||||
}
|
||||
|
||||
DividerType {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,40 +251,9 @@ PageType {
|
||||
}
|
||||
|
||||
DividerType {
|
||||
visible: !root.isSubscriptionExpired && !root.isSubscriptionExpiringSoon
|
||||
&& root.isSubscriptionRenewalAvailable && !root.isInAppPurchase
|
||||
}
|
||||
|
||||
SwitcherType {
|
||||
id: switcher
|
||||
|
||||
readonly property bool isVlessProtocol: ApiConfigsController.isVlessProtocol()
|
||||
readonly property bool isProtocolSwitchBlocked: ServersModel.isDefaultServerCurrentlyProcessed() && ConnectionController.isConnected
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 24
|
||||
Layout.rightMargin: 16
|
||||
Layout.leftMargin: 16
|
||||
|
||||
visible: ApiAccountInfoModel.data("isProtocolSelectionSupported")
|
||||
enabled: !switcher.isProtocolSwitchBlocked
|
||||
|
||||
text: qsTr("Use VLESS protocol")
|
||||
checked: switcher.isVlessProtocol
|
||||
onToggled: function() {
|
||||
if (ServersModel.isDefaultServerCurrentlyProcessed() && ConnectionController.isConnected) {
|
||||
PageController.showNotificationMessage(qsTr("Cannot change protocol during active connection"))
|
||||
} else {
|
||||
PageController.showBusyIndicator(true)
|
||||
ApiConfigsController.setCurrentProtocol(switcher.isVlessProtocol ? "awg" : "vless")
|
||||
ApiConfigsController.updateServiceFromGateway(ServersModel.processedIndex, "", "", true)
|
||||
PageController.showBusyIndicator(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DividerType {
|
||||
visible: footer.isVisibleForAmneziaFree
|
||||
visible: (!root.isSubscriptionExpired && !root.isSubscriptionExpiringSoon
|
||||
&& root.isSubscriptionRenewalAvailable && !root.isInAppPurchase)
|
||||
|| footer.isVisibleForAmneziaFree
|
||||
}
|
||||
|
||||
WarningType {
|
||||
|
||||
180
common/crypto/cryptoUtils.cpp
Normal file
180
common/crypto/cryptoUtils.cpp
Normal file
@@ -0,0 +1,180 @@
|
||||
#include "cryptoUtils.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <QDebug>
|
||||
|
||||
#include <openssl/err.h>
|
||||
#include <openssl/pem.h>
|
||||
#include <openssl/rand.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr int kAesKeySize = 32; // AES-256
|
||||
constexpr int kAesIvSize = 16; // CBC block size
|
||||
constexpr int kAesBlockSize = 16;
|
||||
|
||||
QByteArray lastOpenSslError()
|
||||
{
|
||||
return QByteArray(ERR_error_string(ERR_get_error(), nullptr));
|
||||
}
|
||||
}
|
||||
|
||||
QByteArray CryptoUtils::generateRandomBytes(int size)
|
||||
{
|
||||
if (size <= 0) {
|
||||
qCritical() << "CryptoUtils::generateRandomBytes invalid size" << size;
|
||||
return {};
|
||||
}
|
||||
|
||||
QByteArray buffer(size, Qt::Uninitialized);
|
||||
if (RAND_bytes(reinterpret_cast<unsigned char *>(buffer.data()), size) != 1) {
|
||||
qCritical() << "CryptoUtils::generateRandomBytes RAND_bytes failed:" << lastOpenSslError();
|
||||
return {};
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
QByteArray CryptoUtils::encryptAes256Cbc(const QByteArray &data, const QByteArray &key, const QByteArray &iv)
|
||||
{
|
||||
if (key.size() != kAesKeySize || iv.size() < kAesIvSize) {
|
||||
qCritical() << "CryptoUtils::encryptAes256Cbc invalid key/iv size" << key.size() << iv.size();
|
||||
return {};
|
||||
}
|
||||
|
||||
std::unique_ptr<EVP_CIPHER_CTX, decltype(&EVP_CIPHER_CTX_free)> ctx { EVP_CIPHER_CTX_new(), EVP_CIPHER_CTX_free };
|
||||
if (!ctx) {
|
||||
qCritical() << "CryptoUtils::encryptAes256Cbc EVP_CIPHER_CTX_new failed:" << lastOpenSslError();
|
||||
return {};
|
||||
}
|
||||
|
||||
if (EVP_EncryptInit_ex(ctx.get(), EVP_aes_256_cbc(), nullptr, reinterpret_cast<const unsigned char *>(key.constData()),
|
||||
reinterpret_cast<const unsigned char *>(iv.constData()))
|
||||
!= 1) {
|
||||
qCritical() << "CryptoUtils::encryptAes256Cbc EVP_EncryptInit_ex failed:" << lastOpenSslError();
|
||||
return {};
|
||||
}
|
||||
|
||||
QByteArray cipherText(data.size() + kAesBlockSize, Qt::Uninitialized);
|
||||
int length = 0;
|
||||
int cipherTextLength = 0;
|
||||
|
||||
if (EVP_EncryptUpdate(ctx.get(), reinterpret_cast<unsigned char *>(cipherText.data()), &length,
|
||||
reinterpret_cast<const unsigned char *>(data.constData()), data.size())
|
||||
!= 1) {
|
||||
qCritical() << "CryptoUtils::encryptAes256Cbc EVP_EncryptUpdate failed:" << lastOpenSslError();
|
||||
return {};
|
||||
}
|
||||
cipherTextLength = length;
|
||||
|
||||
if (EVP_EncryptFinal_ex(ctx.get(), reinterpret_cast<unsigned char *>(cipherText.data()) + cipherTextLength, &length) != 1) {
|
||||
qCritical() << "CryptoUtils::encryptAes256Cbc EVP_EncryptFinal_ex failed:" << lastOpenSslError();
|
||||
return {};
|
||||
}
|
||||
cipherTextLength += length;
|
||||
|
||||
cipherText.resize(cipherTextLength);
|
||||
return cipherText;
|
||||
}
|
||||
|
||||
QByteArray CryptoUtils::decryptAes256Cbc(const QByteArray &data, const QByteArray &key, const QByteArray &iv)
|
||||
{
|
||||
if (key.size() != kAesKeySize || iv.size() < kAesIvSize) {
|
||||
qCritical() << "CryptoUtils::decryptAes256Cbc invalid key/iv size" << key.size() << iv.size();
|
||||
return {};
|
||||
}
|
||||
|
||||
std::unique_ptr<EVP_CIPHER_CTX, decltype(&EVP_CIPHER_CTX_free)> ctx { EVP_CIPHER_CTX_new(), EVP_CIPHER_CTX_free };
|
||||
if (!ctx) {
|
||||
qCritical() << "CryptoUtils::decryptAes256Cbc EVP_CIPHER_CTX_new failed:" << lastOpenSslError();
|
||||
return {};
|
||||
}
|
||||
|
||||
if (EVP_DecryptInit_ex(ctx.get(), EVP_aes_256_cbc(), nullptr, reinterpret_cast<const unsigned char *>(key.constData()),
|
||||
reinterpret_cast<const unsigned char *>(iv.constData()))
|
||||
!= 1) {
|
||||
qCritical() << "CryptoUtils::decryptAes256Cbc EVP_DecryptInit_ex failed:" << lastOpenSslError();
|
||||
return {};
|
||||
}
|
||||
|
||||
QByteArray plainText(data.size() + kAesBlockSize, Qt::Uninitialized);
|
||||
int length = 0;
|
||||
int plainTextLength = 0;
|
||||
|
||||
if (EVP_DecryptUpdate(ctx.get(), reinterpret_cast<unsigned char *>(plainText.data()), &length,
|
||||
reinterpret_cast<const unsigned char *>(data.constData()), data.size())
|
||||
!= 1) {
|
||||
qCritical() << "CryptoUtils::decryptAes256Cbc EVP_DecryptUpdate failed:" << lastOpenSslError();
|
||||
return {};
|
||||
}
|
||||
plainTextLength = length;
|
||||
|
||||
if (EVP_DecryptFinal_ex(ctx.get(), reinterpret_cast<unsigned char *>(plainText.data()) + plainTextLength, &length) != 1) {
|
||||
qCritical() << "CryptoUtils::decryptAes256Cbc EVP_DecryptFinal_ex failed:" << lastOpenSslError();
|
||||
return {};
|
||||
}
|
||||
plainTextLength += length;
|
||||
|
||||
plainText.resize(plainTextLength);
|
||||
return plainText;
|
||||
}
|
||||
|
||||
EVP_PKEY *CryptoUtils::loadPublicKeyFromPem(const QByteArray &pem)
|
||||
{
|
||||
std::unique_ptr<BIO, decltype(&BIO_free)> bio { BIO_new_mem_buf(pem.constData(), pem.size()), BIO_free };
|
||||
if (!bio) {
|
||||
qCritical() << "CryptoUtils::loadPublicKeyFromPem BIO_new_mem_buf failed:" << lastOpenSslError();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
EVP_PKEY *key = PEM_read_bio_PUBKEY(bio.get(), nullptr, nullptr, nullptr);
|
||||
if (!key) {
|
||||
qCritical() << "CryptoUtils::loadPublicKeyFromPem PEM_read_bio_PUBKEY failed:" << lastOpenSslError();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
QByteArray CryptoUtils::rsaEncrypt(const QByteArray &data, EVP_PKEY *publicKey, int padding)
|
||||
{
|
||||
if (!publicKey) {
|
||||
qCritical() << "CryptoUtils::rsaEncrypt public key is null";
|
||||
return {};
|
||||
}
|
||||
|
||||
std::unique_ptr<EVP_PKEY_CTX, decltype(&EVP_PKEY_CTX_free)> ctx { EVP_PKEY_CTX_new(publicKey, nullptr), EVP_PKEY_CTX_free };
|
||||
if (!ctx) {
|
||||
qCritical() << "CryptoUtils::rsaEncrypt EVP_PKEY_CTX_new failed:" << lastOpenSslError();
|
||||
return {};
|
||||
}
|
||||
|
||||
if (EVP_PKEY_encrypt_init(ctx.get()) != 1) {
|
||||
qCritical() << "CryptoUtils::rsaEncrypt EVP_PKEY_encrypt_init failed:" << lastOpenSslError();
|
||||
return {};
|
||||
}
|
||||
|
||||
if (EVP_PKEY_CTX_set_rsa_padding(ctx.get(), padding) != 1) {
|
||||
qCritical() << "CryptoUtils::rsaEncrypt EVP_PKEY_CTX_set_rsa_padding failed:" << lastOpenSslError();
|
||||
return {};
|
||||
}
|
||||
|
||||
const auto *plainData = reinterpret_cast<const unsigned char *>(data.constData());
|
||||
|
||||
std::size_t cipherTextLength = 0;
|
||||
if (EVP_PKEY_encrypt(ctx.get(), nullptr, &cipherTextLength, plainData, data.size()) != 1) {
|
||||
qCritical() << "CryptoUtils::rsaEncrypt couldn't determine buffer length:" << lastOpenSslError();
|
||||
return {};
|
||||
}
|
||||
|
||||
QByteArray cipherText(static_cast<int>(cipherTextLength), Qt::Uninitialized);
|
||||
if (EVP_PKEY_encrypt(ctx.get(), reinterpret_cast<unsigned char *>(cipherText.data()), &cipherTextLength, plainData, data.size())
|
||||
!= 1) {
|
||||
qCritical() << "CryptoUtils::rsaEncrypt EVP_PKEY_encrypt failed:" << lastOpenSslError();
|
||||
return {};
|
||||
}
|
||||
|
||||
cipherText.resize(static_cast<int>(cipherTextLength));
|
||||
return cipherText;
|
||||
}
|
||||
35
common/crypto/cryptoUtils.h
Normal file
35
common/crypto/cryptoUtils.h
Normal file
@@ -0,0 +1,35 @@
|
||||
#ifndef CRYPTOUTILS_H
|
||||
#define CRYPTOUTILS_H
|
||||
|
||||
#include <QByteArray>
|
||||
|
||||
#include <openssl/evp.h>
|
||||
|
||||
// Thin wrapper around OpenSSL EVP used for local settings encryption and for
|
||||
// the API-gateway request/response envelope. Replaces the unmaintained
|
||||
// QSimpleCrypto library. All functions report failures by returning an empty
|
||||
// QByteArray (or nullptr) and logging, without throwing.
|
||||
//
|
||||
// Wire compatibility note: encryptAes256Cbc/decryptAes256Cbc reproduce the exact
|
||||
// behaviour of QSimpleCrypto::QBlockCipher (AES-256-CBC, PKCS7 padding, first
|
||||
// 16 bytes of the IV buffer), so data written by previous app versions stays
|
||||
// readable and gateway requests keep the same format.
|
||||
namespace CryptoUtils
|
||||
{
|
||||
// Cryptographically secure random bytes of the requested size.
|
||||
QByteArray generateRandomBytes(int size);
|
||||
|
||||
// AES-256-CBC with PKCS7 padding. key must be 32 bytes, iv must be at least
|
||||
// 16 bytes (only the first 16 are used, matching the legacy behaviour).
|
||||
QByteArray encryptAes256Cbc(const QByteArray &data, const QByteArray &key, const QByteArray &iv);
|
||||
QByteArray decryptAes256Cbc(const QByteArray &data, const QByteArray &key, const QByteArray &iv);
|
||||
|
||||
// Loads an RSA public key from PEM bytes. Returns nullptr on failure.
|
||||
// The caller owns the result and must free it with EVP_PKEY_free().
|
||||
EVP_PKEY *loadPublicKeyFromPem(const QByteArray &pem);
|
||||
|
||||
// RSA encryption with the given OpenSSL padding (e.g. RSA_PKCS1_PADDING).
|
||||
QByteArray rsaEncrypt(const QByteArray &data, EVP_PKEY *publicKey, int padding);
|
||||
}
|
||||
|
||||
#endif // CRYPTOUTILS_H
|
||||
@@ -29,8 +29,6 @@ elseif(LINUX)
|
||||
set(AMNEZIA_XRAY_INCLUDE_DIR "${AMNEZIA_XRAY_ROOT_DIR}/linux/x86_64")
|
||||
endif()
|
||||
|
||||
set(QSIMPLECRYPTO_DIR ${CMAKE_CURRENT_LIST_DIR}/../../client/3rd/QSimpleCrypto/src)
|
||||
|
||||
|
||||
set(OPENSSL_ROOT_DIR "${CMAKE_CURRENT_LIST_DIR}/../../client/3rd-prebuilt/3rd-prebuilt/openssl/")
|
||||
set(OPENSSL_LIBRARIES_DIR "${OPENSSL_ROOT_DIR}/lib")
|
||||
@@ -65,11 +63,11 @@ set(OPENSSL_USE_STATIC_LIBS TRUE)
|
||||
include_directories(
|
||||
${AMNEZIA_XRAY_INCLUDE_DIR}
|
||||
${OPENSSL_INCLUDE_DIR}
|
||||
${QSIMPLECRYPTO_DIR}
|
||||
)
|
||||
|
||||
set(HEADERS
|
||||
${CMAKE_CURRENT_LIST_DIR}/../../client/utilities.h
|
||||
${CMAKE_CURRENT_LIST_DIR}/../../common/crypto/cryptoUtils.h
|
||||
${CMAKE_CURRENT_LIST_DIR}/../../client/secure_qsettings.h
|
||||
${CMAKE_CURRENT_LIST_DIR}/../../client/core/networkUtilities.h
|
||||
${CMAKE_CURRENT_LIST_DIR}/../../ipc/ipc.h
|
||||
@@ -82,16 +80,11 @@ set(HEADERS
|
||||
${CMAKE_CURRENT_LIST_DIR}/systemservice.h
|
||||
${CMAKE_CURRENT_LIST_DIR}/xray.h
|
||||
${CMAKE_CURRENT_BINARY_DIR}/version.h
|
||||
${QSIMPLECRYPTO_DIR}/include/QAead.h
|
||||
${QSIMPLECRYPTO_DIR}/include/QBlockCipher.h
|
||||
${QSIMPLECRYPTO_DIR}/include/QRsa.h
|
||||
${QSIMPLECRYPTO_DIR}/include/QSimpleCrypto_global.h
|
||||
${QSIMPLECRYPTO_DIR}/include/QX509.h
|
||||
${QSIMPLECRYPTO_DIR}/include/QX509Store.h
|
||||
)
|
||||
|
||||
set(SOURCES
|
||||
${CMAKE_CURRENT_LIST_DIR}/../../client/utilities.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/../../common/crypto/cryptoUtils.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/../../client/secure_qsettings.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/../../client/core/networkUtilities.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/../../ipc/ipcserver.cpp
|
||||
@@ -103,11 +96,6 @@ set(SOURCES
|
||||
${CMAKE_CURRENT_LIST_DIR}/killswitch.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/systemservice.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/xray.cpp
|
||||
${QSIMPLECRYPTO_DIR}/sources/QAead.cpp
|
||||
${QSIMPLECRYPTO_DIR}/sources/QBlockCipher.cpp
|
||||
${QSIMPLECRYPTO_DIR}/sources/QRsa.cpp
|
||||
${QSIMPLECRYPTO_DIR}/sources/QX509.cpp
|
||||
${QSIMPLECRYPTO_DIR}/sources/QX509Store.cpp
|
||||
)
|
||||
|
||||
# Mozilla headres
|
||||
@@ -346,6 +334,7 @@ include_directories(
|
||||
${CMAKE_CURRENT_LIST_DIR}/../../client
|
||||
${CMAKE_CURRENT_LIST_DIR}/../../ipc
|
||||
${CMAKE_CURRENT_LIST_DIR}/../../common/logger
|
||||
${CMAKE_CURRENT_LIST_DIR}/../../common/crypto
|
||||
${CMAKE_CURRENT_BINARY_DIR}
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user