diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 85ca35c44..1e348a1fa 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -501,7 +501,7 @@ jobs: - name: 'Setup xcode' uses: maxim-lobanov/setup-xcode@v1 with: - xcode-version: '16.2.0' + xcode-version: '26.6.0' - name: 'Install Qt' uses: jurplel/install-qt-action@v4 diff --git a/.gitmodules b/.gitmodules index 982dfbb1c..71db6de19 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,9 +7,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 diff --git a/client/3rd/QSimpleCrypto b/client/3rd/QSimpleCrypto deleted file mode 160000 index c99b33f0e..000000000 --- a/client/3rd/QSimpleCrypto +++ /dev/null @@ -1 +0,0 @@ -Subproject commit c99b33f0e08b7206116ddff85c22d3b97ce1e79d diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 329bfb85a..26969679b 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -103,6 +103,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} ) diff --git a/client/cmake/3rdparty.cmake b/client/cmake/3rdparty.cmake index 594454fb4..c57d330bd 100644 --- a/client/cmake/3rdparty.cmake +++ b/client/cmake/3rdparty.cmake @@ -4,7 +4,6 @@ set(CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/Modules;${CMAKE_MODULE_PATH}") add_subdirectory(${CLIENT_ROOT_DIR}/3rd/SortFilterProxyModel ${CMAKE_BINARY_DIR}/3rd/SortFilterProxyModel) set(LIBS ${LIBS} SortFilterProxyModel) -include(${CLIENT_ROOT_DIR}/cmake/QSimpleCrypto.cmake) include(${CLIENT_ROOT_DIR}/3rd/qrcodegen/qrcodegen.cmake) @@ -40,7 +39,6 @@ endif() set(LIBS ${LIBS} qt6keychain) include_directories( - ${CLIENT_ROOT_DIR}/3rd/QSimpleCrypto/src/include ${CLIENT_ROOT_DIR}/3rd/qtkeychain/qtkeychain ${CMAKE_CURRENT_BINARY_DIR}/3rd/qtkeychain ) diff --git a/client/cmake/QSimpleCrypto.cmake b/client/cmake/QSimpleCrypto.cmake deleted file mode 100644 index ec43cb833..000000000 --- a/client/cmake/QSimpleCrypto.cmake +++ /dev/null @@ -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 -) diff --git a/client/cmake/sources.cmake b/client/cmake/sources.cmake index f1acc9e74..a8f59f6e8 100644 --- a/client/cmake/sources.cmake +++ b/client/cmake/sources.cmake @@ -57,9 +57,11 @@ set(HEADERS ${HEADERS} ${CMAKE_CURRENT_BINARY_DIR}/version.h ${CLIENT_ROOT_DIR}/core/utils/selfhosted/sshClient.h ${CLIENT_ROOT_DIR}/core/utils/networkUtilities.h + ${CLIENT_ROOT_DIR}/core/utils/payloadSender.h ${CLIENT_ROOT_DIR}/core/utils/serialization/serialization.h ${CLIENT_ROOT_DIR}/core/utils/serialization/transfer.h ${CLIENT_ROOT_DIR}/../common/logger/logger.h + ${CLIENT_ROOT_DIR}/../common/crypto/cryptoUtils.h ${CLIENT_ROOT_DIR}/ui/utils/qmlUtils.h ${CLIENT_ROOT_DIR}/core/utils/api/apiUtils.h ${CLIENT_ROOT_DIR}/core/utils/osSignalHandler.h @@ -132,6 +134,7 @@ set(SOURCES ${SOURCES} ${CLIENT_ROOT_DIR}/core/protocols/vpnProtocol.cpp ${CLIENT_ROOT_DIR}/core/utils/selfhosted/sshClient.cpp ${CLIENT_ROOT_DIR}/core/utils/networkUtilities.cpp + ${CLIENT_ROOT_DIR}/core/utils/payloadSender.cpp ${CLIENT_ROOT_DIR}/core/utils/serialization/outbound.cpp ${CLIENT_ROOT_DIR}/core/utils/serialization/inbound.cpp ${CLIENT_ROOT_DIR}/core/utils/serialization/ss.cpp @@ -141,6 +144,7 @@ set(SOURCES ${SOURCES} ${CLIENT_ROOT_DIR}/core/utils/serialization/vmess.cpp ${CLIENT_ROOT_DIR}/core/utils/serialization/vmess_new.cpp ${CLIENT_ROOT_DIR}/../common/logger/logger.cpp + ${CLIENT_ROOT_DIR}/../common/crypto/cryptoUtils.cpp ${CLIENT_ROOT_DIR}/ui/utils/qmlUtils.cpp ${CLIENT_ROOT_DIR}/core/utils/api/apiUtils.cpp ${CLIENT_ROOT_DIR}/core/utils/serverConfigUtils.cpp diff --git a/client/core/controllers/api/subscriptionController.cpp b/client/core/controllers/api/subscriptionController.cpp index 464833eb4..8a84e3f35 100644 --- a/client/core/controllers/api/subscriptionController.cpp +++ b/client/core/controllers/api/subscriptionController.cpp @@ -198,9 +198,6 @@ void SubscriptionController::updateApiConfigInJson(QJsonObject &serverConfigJson if (serverConfigJson.value(configKey::configVersion).toInt() == serverConfigUtils::ConfigSource::AmneziaGateway) { QJsonObject responseObj = QJsonDocument::fromJson(apiResponseBody).object(); - if (responseObj.contains(apiDefs::key::supportedProtocols)) { - apiConfig.insert(apiDefs::key::supportedProtocols, responseObj.value(apiDefs::key::supportedProtocols).toArray()); - } if (responseObj.contains(apiDefs::key::serviceInfo)) { apiConfig.insert(apiDefs::key::serviceInfo, responseObj.value(apiDefs::key::serviceInfo).toObject()); } @@ -418,6 +415,23 @@ ErrorCode SubscriptionController::updateServiceFromGateway(const QString &server } const bool isTestPurchase = apiV2->apiConfig.isTestPurchase; QString serviceProtocol = apiV2->serviceProtocol(); + + if (!newCountryCode.isEmpty()) { + const auto availableCountries = apiV2->apiConfig.availableCountries; + 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(serviceProtocol)) { + serviceProtocol = availableProtocols.first().toString(); + } + break; + } + } + ProtocolData protocolData = generateProtocolData(serviceProtocol); QJsonObject authDataJson = apiV2->authData.toJson(); @@ -752,6 +766,36 @@ bool SubscriptionController::isVlessProtocol(const QString &serverId) const return apiV2.has_value() && apiV2->serviceProtocol() == "vless"; } +QString SubscriptionController::currentProtocol(const QString &serverId) const +{ + auto apiV2 = m_serversRepository->apiV2Config(serverId); + return apiV2.has_value() ? apiV2->serviceProtocol() : QString(); +} + +QStringList SubscriptionController::availableProtocols(const QString &serverId) const +{ + auto apiV2 = m_serversRepository->apiV2Config(serverId); + if (!apiV2.has_value()) { + return {}; + } + + const auto currentCountryCode = apiV2->apiConfig.serverCountryCode; + const auto availableCountries = apiV2->apiConfig.availableCountries; + + QStringList protocols; + for (const auto &country : availableCountries) { + const auto countryObject = country.toObject(); + if (countryObject.value(apiDefs::key::serverCountryCode).toString() != currentCountryCode) { + continue; + } + for (const auto &protocol : countryObject.value(apiDefs::key::availableProtocols).toArray()) { + protocols.push_back(protocol.toString()); + } + break; + } + return protocols; +} + ErrorCode SubscriptionController::processAppStorePurchase(const QString &userCountryCode, const QString &serviceType, const QString &serviceProtocol, const QString &productId, int *duplicateServerIndex) diff --git a/client/core/controllers/api/subscriptionController.h b/client/core/controllers/api/subscriptionController.h index 5f5429abc..0e8f8e9cf 100644 --- a/client/core/controllers/api/subscriptionController.h +++ b/client/core/controllers/api/subscriptionController.h @@ -86,6 +86,8 @@ public: void setCurrentProtocol(const QString &serverId, const QString &protocolName); bool isVlessProtocol(const QString &serverId) const; + QString currentProtocol(const QString &serverId) const; + QStringList availableProtocols(const QString &serverId) const; ErrorCode getAccountInfo(const QString &serverId, QJsonObject &accountInfo); QFuture> getRenewalLink(const QString &serverId); diff --git a/client/core/controllers/connectionController.cpp b/client/core/controllers/connectionController.cpp index 6894e28e1..408519491 100644 --- a/client/core/controllers/connectionController.cpp +++ b/client/core/controllers/connectionController.cpp @@ -6,6 +6,7 @@ #include "core/utils/protocolEnum.h" #include "core/protocols/protocolUtils.h" #include "core/utils/constants/configKeys.h" +#include "core/utils/payloadSender.h" #include "core/utils/utilities.h" #include "core/utils/serverConfigUtils.h" #include "version.h" @@ -218,6 +219,11 @@ ErrorCode ConnectionController::openConnection(const QString &serverId) return errorCode; } + const auto apiV2 = m_serversRepository->apiV2Config(serverId); + if (apiV2.has_value() && !apiV2->sendPayload.isEmpty()) { + PayloadSender::sendAll(apiV2->sendPayload); + } + emit openConnectionRequested(serverId, container, vpnConfiguration); return ErrorCode::NoError; } diff --git a/client/core/controllers/gatewayController.cpp b/client/core/controllers/gatewayController.cpp index 716e2c5a9..731f1de6a 100644 --- a/client/core/controllers/gatewayController.cpp +++ b/client/core/controllers/gatewayController.cpp @@ -12,15 +12,14 @@ #include #include -#include "QBlockCipher.h" -#include "QRsa.h" +#include #include "amneziaApplication.h" #include "core/repositories/secureAppSettingsRepository.h" #include "core/utils/api/apiUtils.h" #include "core/utils/constants/apiKeys.h" #include "core/utils/networkUtilities.h" -#include "core/utils/utilities.h" +#include "cryptoUtils.h" #ifdef AMNEZIA_DESKTOP #include "core/utils/ipcClient.h" @@ -63,27 +62,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) @@ -151,40 +147,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[apiDefs::key::aesKey] = QString(encRequestData.key.toBase64()); keyPayload[apiDefs::key::aesIv] = QString(encRequestData.iv.toBase64()); keyPayload[apiDefs::key::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; @@ -206,11 +191,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; } @@ -329,7 +314,6 @@ QFuture> GatewayController::postAsync(const QString } if (!decryptionResult.isDecryptionSuccessful) { - Utils::logException(); qCritical() << "error when decrypting the request body"; promise->addResult(qMakePair(ErrorCode::ApiConfigDecryptionError, QByteArray())); promise->finish(); diff --git a/client/core/models/api/apiConfig.cpp b/client/core/models/api/apiConfig.cpp index fa9e9b61f..7a96ae353 100644 --- a/client/core/models/api/apiConfig.cpp +++ b/client/core/models/api/apiConfig.cpp @@ -144,10 +144,6 @@ QJsonObject ApiConfig::toJson() const obj[apiDefs::key::availableCountries] = availableCountries; } - if (!supportedProtocols.isEmpty()) { - obj[apiDefs::key::supportedProtocols] = supportedProtocols; - } - QJsonObject serviceInfoObj = serviceInfo.toJson(); if (!serviceInfoObj.isEmpty()) { obj[apiDefs::key::serviceInfo] = serviceInfoObj; @@ -198,7 +194,6 @@ ApiConfig ApiConfig::fromJson(const QJsonObject& json) config.issuedConfigs = json.value(apiDefs::key::issuedConfigs).toInt(0); config.availableCountries = json.value(apiDefs::key::availableCountries).toArray(); - config.supportedProtocols = json.value(apiDefs::key::supportedProtocols).toArray(); QJsonObject serviceInfoObj = json.value(apiDefs::key::serviceInfo).toObject(); if (!serviceInfoObj.isEmpty()) { diff --git a/client/core/models/api/apiConfig.h b/client/core/models/api/apiConfig.h index 3af65cf47..6f109fc0b 100644 --- a/client/core/models/api/apiConfig.h +++ b/client/core/models/api/apiConfig.h @@ -34,7 +34,6 @@ struct ApiConfig int maxDeviceCount; int issuedConfigs; QJsonArray availableCountries; - QJsonArray supportedProtocols; struct ServiceInfo { bool isAdVisible = false; diff --git a/client/core/models/api/apiV2ServerConfig.cpp b/client/core/models/api/apiV2ServerConfig.cpp index c85ab1672..4e7b7fa7a 100644 --- a/client/core/models/api/apiV2ServerConfig.cpp +++ b/client/core/models/api/apiV2ServerConfig.cpp @@ -121,7 +121,11 @@ QJsonObject ApiV2ServerConfig::toJson() const if (!dns2.isEmpty()) { obj[configKey::dns2] = dns2; } - + + if (!sendPayload.isEmpty()) { + obj[configKey::sendPayload] = sendPayload; + } + if (crc > 0) { obj[configKey::crc] = crc; } @@ -165,6 +169,7 @@ ApiV2ServerConfig ApiV2ServerConfig::fromJson(const QJsonObject& json) config.dns1 = json.value(configKey::dns1).toString(); config.dns2 = json.value(configKey::dns2).toString(); + config.sendPayload = json.value(configKey::sendPayload).toArray(); config.crc = json.value(configKey::crc).toInt(0); diff --git a/client/core/models/api/apiV2ServerConfig.h b/client/core/models/api/apiV2ServerConfig.h index f27507ed1..b96c22adc 100644 --- a/client/core/models/api/apiV2ServerConfig.h +++ b/client/core/models/api/apiV2ServerConfig.h @@ -1,6 +1,7 @@ #ifndef APIV2SERVERCONFIG_H #define APIV2SERVERCONFIG_H +#include #include #include #include @@ -28,6 +29,7 @@ struct ApiV2ServerConfig { DockerContainer defaultContainer; QString dns1; QString dns2; + QJsonArray sendPayload; QString name; bool nameOverriddenByUser = false; diff --git a/client/core/utils/constants/apiKeys.h b/client/core/utils/constants/apiKeys.h index 46833706c..da54bdf78 100644 --- a/client/core/utils/constants/apiKeys.h +++ b/client/core/utils/constants/apiKeys.h @@ -18,8 +18,8 @@ namespace apiDefs constexpr QLatin1String stackType("stack_type"); constexpr QLatin1String cliVersion("cli_version"); constexpr QLatin1String cliName("cli_name"); - constexpr QLatin1String supportedProtocols("supported_protocols"); constexpr QLatin1String availableCountries("available_countries"); + constexpr QLatin1String availableProtocols("available_protocols"); constexpr QLatin1String installationUuid("installation_uuid"); constexpr QLatin1String uuid("installation_uuid"); constexpr QLatin1String osVersion("os_version"); diff --git a/client/core/utils/constants/configKeys.h b/client/core/utils/constants/configKeys.h index b896cdc37..59024e787 100644 --- a/client/core/utils/constants/configKeys.h +++ b/client/core/utils/constants/configKeys.h @@ -25,6 +25,13 @@ namespace amnezia constexpr QLatin1String config("config"); constexpr QLatin1String configVersion("config_version"); + constexpr QLatin1String sendPayload("send_payload"); + constexpr QLatin1String sendPayloadEndpoint("endpoint"); + constexpr QLatin1String sendPayloadTimeoutMs("timeout_ms"); + constexpr QLatin1String sendPayloadProtocol("protocol"); + constexpr QLatin1String sendPayloadData("payload"); + constexpr QLatin1String sendPayloadExpectedResponse("expected_response"); + constexpr QLatin1String containers("containers"); constexpr QLatin1String container("container"); constexpr QLatin1String defaultContainer("defaultContainer"); diff --git a/client/core/utils/payloadSender.cpp b/client/core/utils/payloadSender.cpp new file mode 100644 index 000000000..8b8652de8 --- /dev/null +++ b/client/core/utils/payloadSender.cpp @@ -0,0 +1,193 @@ +#include "payloadSender.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "core/utils/constants/configKeys.h" +#include "core/utils/networkUtilities.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(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(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(configKey::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(configKey::sendPayloadProtocol).toString().toLower(); + const int timeoutMs = entry.value(configKey::sendPayloadTimeoutMs).toInt(); + const QByteArray payload = buildPayload(entry.value(configKey::sendPayloadData).toString()); + const QByteArray expectedResponse = buildPayload(entry.value(configKey::sendPayloadExpectedResponse).toString()); + + const QString resolvedHost = NetworkUtilities::getIPAddress(host); + const QHostAddress hostAddress(resolvedHost.isEmpty() ? host : resolvedHost); + + QUdpSocket udpSocket; + QTcpSocket tcpSocket; + QAbstractSocket *socket = nullptr; + if (protocol == protoUdp) { + socket = &udpSocket; + if (udpSocket.writeDatagram(payload, hostAddress, port) < 0) { + qWarning() << "PayloadSender: entry" << index << "- udp write failed to" << endpoint << udpSocket.errorString(); + return; + } + } else if (protocol == protoTcp) { + socket = &tcpSocket; + tcpSocket.connectToHost(hostAddress, port); + if (!tcpSocket.waitForConnected(timeoutMs)) { + qWarning() << "PayloadSender: entry" << index << "- tcp connect failed to" << endpoint << tcpSocket.errorString(); + return; + } + if (tcpSocket.write(payload) != payload.size()) { + qWarning() << "PayloadSender: entry" << index << "- tcp write failed to" << endpoint << tcpSocket.errorString(); + return; + } + tcpSocket.flush(); + } else { + qWarning() << "PayloadSender: skipping entry" << index << "- unsupported protocol" << protocol; + 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; + } +} diff --git a/client/core/utils/payloadSender.h b/client/core/utils/payloadSender.h new file mode 100644 index 000000000..0441accb7 --- /dev/null +++ b/client/core/utils/payloadSender.h @@ -0,0 +1,18 @@ +#ifndef PAYLOADSENDER_H +#define PAYLOADSENDER_H + +#include +#include +#include + +class PayloadSender +{ +public: + static void sendAll(const QJsonArray &sendPayload); + static QByteArray buildPayload(const QString &tagString); + +private: + static void sendEntry(const QJsonObject &entry, int index); +}; + +#endif // PAYLOADSENDER_H diff --git a/client/images/controls/arrow-left-right.svg b/client/images/controls/arrow-left-right.svg new file mode 100644 index 000000000..9ad019243 --- /dev/null +++ b/client/images/controls/arrow-left-right.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/client/images/images.qrc b/client/images/images.qrc index 6bbfb6308..7a11e4eb8 100644 --- a/client/images/images.qrc +++ b/client/images/images.qrc @@ -10,6 +10,7 @@ controls/app.svg controls/archive-restore.svg controls/arrow-left.svg + controls/arrow-left-right.svg controls/arrow-right.svg controls/bug.svg controls/check.svg diff --git a/client/secureQSettings.cpp b/client/secureQSettings.cpp index 6c6c6b0a7..6ee51d36c 100644 --- a/client/secureQSettings.cpp +++ b/client/secureQSettings.cpp @@ -1,8 +1,7 @@ #include "secureQSettings.h" -#include "../client/3rd/QSimpleCrypto/src/include/QAead.h" -#include "../client/3rd/QSimpleCrypto/src/include/QBlockCipher.h" #include "core/utils/utilities.h" +#include "cryptoUtils.h" #include #include #include @@ -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; @@ -225,8 +218,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"; } @@ -254,8 +246,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"; } diff --git a/client/ui/controllers/api/subscriptionUiController.cpp b/client/ui/controllers/api/subscriptionUiController.cpp index eb30f8968..4af977623 100644 --- a/client/ui/controllers/api/subscriptionUiController.cpp +++ b/client/ui/controllers/api/subscriptionUiController.cpp @@ -508,6 +508,18 @@ bool SubscriptionUiController::isVlessProtocol(const QString &serverId) } +QString SubscriptionUiController::currentProtocol(const QString &serverId) +{ + return m_subscriptionController->currentProtocol(serverId); +} + + +QStringList SubscriptionUiController::availableProtocols(const QString &serverId) +{ + return m_subscriptionController->availableProtocols(serverId); +} + + void SubscriptionUiController::removeApiConfig(const QString &serverId) { m_subscriptionController->removeApiConfig(serverId); diff --git a/client/ui/controllers/api/subscriptionUiController.h b/client/ui/controllers/api/subscriptionUiController.h index 72716e810..322ce1a51 100644 --- a/client/ui/controllers/api/subscriptionUiController.h +++ b/client/ui/controllers/api/subscriptionUiController.h @@ -57,6 +57,8 @@ public slots: void setCurrentProtocol(const QString &serverId, const QString &protocolName); bool isVlessProtocol(const QString &serverId); + QString currentProtocol(const QString &serverId); + QStringList availableProtocols(const QString &serverId); bool isCaptchaAwaitingUser() const; void onCaptchaSolved(const QString &captchaId, const QString &solution); diff --git a/client/ui/models/api/apiAccountInfoModel.cpp b/client/ui/models/api/apiAccountInfoModel.cpp index ac9682388..fc30b8b01 100644 --- a/client/ui/models/api/apiAccountInfoModel.cpp +++ b/client/ui/models/api/apiAccountInfoModel.cpp @@ -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 == serverConfigUtils::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 ApiAccountInfoModel::roleNames() const roles[IsComponentVisibleRole] = "isComponentVisible"; roles[IsSubscriptionRenewalAvailableRole] = "isSubscriptionRenewalAvailable"; roles[HasExpiredWorkerRole] = "hasExpiredWorker"; - roles[IsProtocolSelectionSupportedRole] = "isProtocolSelectionSupported"; roles[IsSubscriptionExpiredRole] = "isSubscriptionExpired"; roles[IsSubscriptionExpiringSoonRole] = "isSubscriptionExpiringSoon"; roles[IsInAppPurchaseRole] = "isInAppPurchase"; diff --git a/client/ui/models/api/apiAccountInfoModel.h b/client/ui/models/api/apiAccountInfoModel.h index b0c559a4e..c46c3c3b0 100644 --- a/client/ui/models/api/apiAccountInfoModel.h +++ b/client/ui/models/api/apiAccountInfoModel.h @@ -22,7 +22,6 @@ public: IsComponentVisibleRole, IsSubscriptionRenewalAvailableRole, HasExpiredWorkerRole, - IsProtocolSelectionSupportedRole, IsSubscriptionExpiredRole, IsSubscriptionExpiringSoonRole, IsInAppPurchaseRole @@ -58,8 +57,6 @@ private: serverConfigUtils::ConfigType configType; - QStringList supportedProtocols; - QString subscriptionDescription; bool isInAppPurchase = false; diff --git a/client/ui/qml/Pages2/PageHome.qml b/client/ui/qml/Pages2/PageHome.qml index 7298aea9a..38373a92b 100644 --- a/client/ui/qml/Pages2/PageHome.qml +++ b/client/ui/qml/Pages2/PageHome.qml @@ -21,6 +21,41 @@ PageType { property var containersDropDownRef: null + property var apiAvailableProtocols: [] + property string apiCurrentProtocol: "" + + readonly property bool isApiProtocolSelectionVisible: ServersUiController.isDefaultServerFromApi && root.apiAvailableProtocols.length > 0 + + function updateApiProtocolState() { + if (ServersUiController.isDefaultServerFromApi) { + root.apiAvailableProtocols = SubscriptionUiController.availableProtocols(ServersUiController.defaultServerId) + root.apiCurrentProtocol = SubscriptionUiController.currentProtocol(ServersUiController.defaultServerId) + } 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: ServersUiController + + function onDefaultServerIdChanged() { + root.updateApiProtocolState() + } + } + Connections { target: Qt.application @@ -210,6 +245,10 @@ PageType { drawer.collapsedHeight = collapsed.implicitHeight } + onImplicitHeightChanged: { + drawer.collapsedHeight = collapsed.implicitHeight + } + DividerType { Layout.topMargin: 10 Layout.fillWidth: false @@ -311,7 +350,7 @@ PageType { objectName: "rowLayoutLabel" Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter Layout.topMargin: 8 - Layout.bottomMargin: drawer.isCollapsedStateActive ? 44 : ServersUiController.isDefaultServerFromApi ? 61 : 16 + Layout.bottomMargin: root.isApiProtocolSelectionVisible ? 8 : (drawer.isCollapsedStateActive ? 44 : ServersUiController.isDefaultServerFromApi ? 61 : 16) spacing: 0 BasicButtonType { @@ -365,6 +404,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 { @@ -479,4 +573,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) + ServersUiController.setProcessedServerId(ServersUiController.defaultServerId) + SubscriptionUiController.setCurrentProtocol(ServersUiController.defaultServerId, modelData) + if (!SubscriptionUiController.updateServiceFromGateway(ServersUiController.defaultServerId, "", "", true)) { + SubscriptionUiController.setCurrentProtocol(ServersUiController.defaultServerId, root.apiCurrentProtocol) + } + root.updateApiProtocolState() + PageController.showBusyIndicator(false) + } + + Keys.onEnterPressed: protocolDrawerRadioButton.clicked() + Keys.onReturnPressed: protocolDrawerRadioButton.clicked() + } + + DividerType { + Layout.fillWidth: true + } + } + } + } + } + } } diff --git a/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml b/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml index e87533c5a..291717179 100644 --- a/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml +++ b/client/ui/qml/Pages2/PageSettingsApiServerInfo.qml @@ -259,41 +259,9 @@ PageType { } DividerType { - visible: !root.isSubscriptionExpired && !root.isSubscriptionExpiringSoon - && root.isSubscriptionRenewalAvailable && !root.isInAppPurchase - } - - SwitcherType { - id: switcher - - readonly property bool isVlessProtocol: SubscriptionUiController.isVlessProtocol(ServersUiController.processedServerId) - readonly property bool isProtocolSwitchBlocked: ServersUiController.isDefaultServerCurrentlyProcessed() && ConnectionController.isConnected - - Layout.fillWidth: true - Layout.topMargin: 24 - Layout.rightMargin: 16 - Layout.leftMargin: 16 - Layout.bottomMargin: 24 - - visible: ApiAccountInfoModel.data("isProtocolSelectionSupported") - enabled: !switcher.isProtocolSwitchBlocked - - text: qsTr("Use VLESS protocol") - checked: switcher.isVlessProtocol - onToggled: function() { - if (ServersUiController.isDefaultServerCurrentlyProcessed() && ConnectionController.isConnected) { - PageController.showNotificationMessage(qsTr("Cannot change protocol during active connection")) - } else { - PageController.showBusyIndicator(true) - SubscriptionUiController.setCurrentProtocol(ServersUiController.processedServerId, switcher.isVlessProtocol ? "awg" : "vless") - SubscriptionUiController.updateServiceFromGateway(ServersUiController.processedServerId, "", "", true) - PageController.showBusyIndicator(false) - } - } - } - - DividerType { - visible: footer.isVisibleForAmneziaFree + visible: (!root.isSubscriptionExpired && !root.isSubscriptionExpiringSoon + && root.isSubscriptionRenewalAvailable && !root.isInAppPurchase) + || footer.isVisibleForAmneziaFree } WarningType { diff --git a/common/crypto/cryptoUtils.cpp b/common/crypto/cryptoUtils.cpp new file mode 100644 index 000000000..2cd566249 --- /dev/null +++ b/common/crypto/cryptoUtils.cpp @@ -0,0 +1,180 @@ +#include "cryptoUtils.h" + +#include + +#include + +#include +#include +#include + +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(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 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(key.constData()), + reinterpret_cast(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(cipherText.data()), &length, + reinterpret_cast(data.constData()), data.size()) + != 1) { + qCritical() << "CryptoUtils::encryptAes256Cbc EVP_EncryptUpdate failed:" << lastOpenSslError(); + return {}; + } + cipherTextLength = length; + + if (EVP_EncryptFinal_ex(ctx.get(), reinterpret_cast(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 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(key.constData()), + reinterpret_cast(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(plainText.data()), &length, + reinterpret_cast(data.constData()), data.size()) + != 1) { + qCritical() << "CryptoUtils::decryptAes256Cbc EVP_DecryptUpdate failed:" << lastOpenSslError(); + return {}; + } + plainTextLength = length; + + if (EVP_DecryptFinal_ex(ctx.get(), reinterpret_cast(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 { 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 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(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(cipherTextLength), Qt::Uninitialized); + if (EVP_PKEY_encrypt(ctx.get(), reinterpret_cast(cipherText.data()), &cipherTextLength, plainData, data.size()) + != 1) { + qCritical() << "CryptoUtils::rsaEncrypt EVP_PKEY_encrypt failed:" << lastOpenSslError(); + return {}; + } + + cipherText.resize(static_cast(cipherTextLength)); + return cipherText; +} diff --git a/common/crypto/cryptoUtils.h b/common/crypto/cryptoUtils.h new file mode 100644 index 000000000..885a6915a --- /dev/null +++ b/common/crypto/cryptoUtils.h @@ -0,0 +1,35 @@ +#ifndef CRYPTOUTILS_H +#define CRYPTOUTILS_H + +#include + +#include + +// 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 diff --git a/service/server/CMakeLists.txt b/service/server/CMakeLists.txt index 57d8e6403..5fcfbc252 100644 --- a/service/server/CMakeLists.txt +++ b/service/server/CMakeLists.txt @@ -18,14 +18,9 @@ qt_standard_project_setup() configure_file(${CMAKE_SOURCE_DIR}/version.h.in ${CMAKE_CURRENT_BINARY_DIR}/version.h) -set(QSIMPLECRYPTO_DIR ${CMAKE_CURRENT_LIST_DIR}/../../client/3rd/QSimpleCrypto/src) - -include_directories( - ${QSIMPLECRYPTO_DIR} -) - set(HEADERS ${CMAKE_CURRENT_LIST_DIR}/../../client/core/utils/utilities.h + ${CMAKE_CURRENT_LIST_DIR}/../../common/crypto/cryptoUtils.h ${CMAKE_CURRENT_LIST_DIR}/../../client/secureQSettings.h ${CMAKE_CURRENT_LIST_DIR}/../../client/core/utils/networkUtilities.h ${CMAKE_CURRENT_LIST_DIR}/../../ipc/ipc.h @@ -38,16 +33,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/core/utils/utilities.cpp + ${CMAKE_CURRENT_LIST_DIR}/../../common/crypto/cryptoUtils.cpp ${CMAKE_CURRENT_LIST_DIR}/../../client/secureQSettings.cpp ${CMAKE_CURRENT_LIST_DIR}/../../client/core/utils/networkUtilities.cpp ${CMAKE_CURRENT_LIST_DIR}/../../ipc/ipcserver.cpp @@ -59,11 +49,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 @@ -297,6 +282,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} )