#include "gatewayController.h" #include #include #include #include #include #include #include #include #include "QBlockCipher.h" #include "QRsa.h" #include "amnezia_application.h" #include "core/api/apiUtils.h" #include "core/networkUtilities.h" #include "utilities.h" #ifdef AMNEZIA_DESKTOP #include "core/ipcclient.h" #endif namespace { namespace configKey { constexpr char aesKey[] = "aes_key"; constexpr char aesIv[] = "aes_iv"; constexpr char aesSalt[] = "aes_salt"; constexpr char apiPayload[] = "api_payload"; constexpr char keyPayload[] = "key_payload"; } constexpr QLatin1String errorResponsePattern1("No active configuration found for"); constexpr QLatin1String errorResponsePattern2("No non-revoked public key found for"); constexpr QLatin1String errorResponsePattern3("Account not found."); constexpr QLatin1String updateRequestResponsePattern("client version update is required"); } GatewayController::GatewayController(const QString &gatewayEndpoint, const bool isDevEnvironment, const int requestTimeoutMsecs, const bool isStrictKillSwitchEnabled, QObject *parent) : QObject(parent), m_gatewayEndpoint(gatewayEndpoint), m_isDevEnvironment(isDevEnvironment), m_requestTimeoutMsecs(requestTimeoutMsecs), m_isStrictKillSwitchEnabled(isStrictKillSwitchEnabled) { } GatewayController::EncryptedRequestData GatewayController::prepareRequest(const QString &endpoint, const QJsonObject &apiPayload) { EncryptedRequestData encRequestData; encRequestData.errorCode = ErrorCode::NoError; #ifdef Q_OS_IOS IosController::Instance()->requestInetAccess(); QThread::msleep(10); #endif encRequestData.request.setTransferTimeout(m_requestTimeoutMsecs); encRequestData.request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); encRequestData.request.setRawHeader(QString("X-Client-Request-ID").toUtf8(), QUuid::createUuid().toString(QUuid::WithoutBraces).toUtf8()); encRequestData.request.setUrl(endpoint.arg(m_proxyUrl.isEmpty() ? m_gatewayEndpoint : m_proxyUrl)); // bypass killSwitch exceptions for API-gateway #ifdef AMNEZIA_DESKTOP if (m_isStrictKillSwitchEnabled) { QString host = QUrl(encRequestData.request.url()).host(); QString ip = NetworkUtilities::getIPAddress(host); if (!ip.isEmpty()) { IpcClient::Interface()->addKillSwitchAllowedRange(QStringList { ip }); } } #endif QSimpleCrypto::QBlockCipher blockCipher; encRequestData.key = blockCipher.generatePrivateSalt(32); encRequestData.iv = blockCipher.generatePrivateSalt(32); encRequestData.salt = blockCipher.generatePrivateSalt(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; 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; } encryptedKeyPayload = rsa.encrypt(QJsonDocument(keyPayload).toJson(), publicKey, RSA_PKCS1_PADDING); EVP_PKEY_free(publicKey); encryptedApiPayload = blockCipher.encryptAesBlockCipher(QJsonDocument(apiPayload).toJson(), encRequestData.key, encRequestData.iv, "", encRequestData.salt); } catch (...) { Utils::logException(); qCritical() << "error when encrypting the request body"; encRequestData.errorCode = ErrorCode::ApiConfigDecryptionError; return encRequestData; } QJsonObject requestBody; requestBody[configKey::keyPayload] = QString(encryptedKeyPayload.toBase64()); requestBody[configKey::apiPayload] = QString(encryptedApiPayload.toBase64()); encRequestData.requestBody = QJsonDocument(requestBody).toJson(); return encRequestData; } ErrorCode GatewayController::post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody) { EncryptedRequestData encRequestData = prepareRequest(endpoint, apiPayload); if (encRequestData.errorCode != ErrorCode::NoError) { return encRequestData.errorCode; } QNetworkReply *reply = amnApp->networkManager()->post(encRequestData.request, encRequestData.requestBody); QEventLoop wait; connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit); QList sslErrors; connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList &errors) { sslErrors = errors; }); wait.exec(); QByteArray encryptedResponseBody = reply->readAll(); QString replyErrorString = reply->errorString(); auto replyError = reply->error(); int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); reply->deleteLater(); if (sslErrors.isEmpty() && shouldBypassProxy(replyError, encryptedResponseBody, true, encRequestData.key, encRequestData.iv, encRequestData.salt)) { auto requestFunction = [&encRequestData, &encryptedResponseBody](const QString &url) { encRequestData.request.setUrl(url); return amnApp->networkManager()->post(encRequestData.request, encRequestData.requestBody); }; auto replyProcessingFunction = [&encryptedResponseBody, &replyErrorString, &replyError, &httpStatusCode, &sslErrors, &encRequestData, this](QNetworkReply *reply, const QList &nestedSslErrors) { encryptedResponseBody = reply->readAll(); replyErrorString = reply->errorString(); replyError = reply->error(); httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); if (!sslErrors.isEmpty() || shouldBypassProxy(replyError, encryptedResponseBody, true, encRequestData.key, encRequestData.iv, encRequestData.salt)) { sslErrors = nestedSslErrors; return false; } return true; }; auto serviceType = apiPayload.value(apiDefs::key::serviceType).toString(""); auto userCountryCode = apiPayload.value(apiDefs::key::userCountryCode).toString(""); bypassProxy(endpoint, serviceType, userCountryCode, requestFunction, replyProcessingFunction); } auto errorCode = apiUtils::checkNetworkReplyErrors(sslErrors, replyErrorString, replyError, httpStatusCode, encryptedResponseBody); if (errorCode) { return errorCode; } try { QSimpleCrypto::QBlockCipher blockCipher; responseBody = blockCipher.decryptAesBlockCipher(encryptedResponseBody, encRequestData.key, encRequestData.iv, "", encRequestData.salt); return ErrorCode::NoError; } catch (...) { // todo change error handling in QSimpleCrypto? Utils::logException(); qCritical() << "error when decrypting the request body"; return ErrorCode::ApiConfigDecryptionError; } } QFuture> GatewayController::postAsync(const QString &endpoint, const QJsonObject apiPayload) { auto promise = QSharedPointer>>::create(); promise->start(); EncryptedRequestData encRequestData = prepareRequest(endpoint, apiPayload); if (encRequestData.errorCode != ErrorCode::NoError) { promise->addResult(qMakePair(encRequestData.errorCode, QByteArray())); promise->finish(); return promise->future(); } QNetworkReply *reply = amnApp->networkManager()->post(encRequestData.request, encRequestData.requestBody); auto sslErrors = QSharedPointer>::create(); connect(reply, &QNetworkReply::sslErrors, [sslErrors](const QList &errors) { *sslErrors = errors; }); connect(reply, &QNetworkReply::finished, reply, [=]() { QByteArray encryptedResponseBody = reply->readAll(); QString replyErrorString = reply->errorString(); auto replyError = reply->error(); int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); reply->deleteLater(); auto errorCode = apiUtils::checkNetworkReplyErrors(*sslErrors, replyErrorString, replyError, httpStatusCode, encryptedResponseBody); if (errorCode) { promise->addResult(qMakePair(errorCode, QByteArray())); promise->finish(); return; } QSimpleCrypto::QBlockCipher blockCipher; try { QByteArray responseBody = blockCipher.decryptAesBlockCipher(encryptedResponseBody, encRequestData.key, encRequestData.iv, "", encRequestData.salt); promise->addResult(qMakePair(ErrorCode::NoError, responseBody)); promise->finish(); } catch (...) { Utils::logException(); qCritical() << "error when decrypting the request body"; promise->addResult(qMakePair(ErrorCode::ApiConfigDecryptionError, QByteArray())); promise->finish(); } }); return promise->future(); } QStringList GatewayController::getProxyUrls(const QString &serviceType, const QString &userCountryCode) { QNetworkRequest request; request.setTransferTimeout(m_requestTimeoutMsecs); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); QEventLoop wait; QList sslErrors; QNetworkReply *reply; QStringList baseUrls; if (m_isDevEnvironment) { baseUrls = QString(DEV_S3_ENDPOINT).split(", "); } else { baseUrls = QString(PROD_S3_ENDPOINT).split(", "); } QByteArray key = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY; QStringList proxyStorageUrls; if (!serviceType.isEmpty()) { for (const auto &baseUrl : baseUrls) { QByteArray path = ("endpoints-" + serviceType + "-" + userCountryCode).toUtf8(); proxyStorageUrls.push_back(baseUrl + path.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals) + ".json"); } } for (const auto &baseUrl : baseUrls) { proxyStorageUrls.push_back(baseUrl + "endpoints.json"); } for (const auto &proxyStorageUrl : proxyStorageUrls) { request.setUrl(proxyStorageUrl); reply = amnApp->networkManager()->get(request); connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit); connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList &errors) { sslErrors = errors; }); wait.exec(); if (reply->error() == QNetworkReply::NetworkError::NoError) { auto encryptedResponseBody = reply->readAll(); reply->deleteLater(); EVP_PKEY *privateKey = nullptr; QByteArray responseBody; try { if (!m_isDevEnvironment) { QCryptographicHash hash(QCryptographicHash::Sha512); hash.addData(key); QByteArray hashResult = hash.result().toHex(); QByteArray key = QByteArray::fromHex(hashResult.left(64)); QByteArray iv = QByteArray::fromHex(hashResult.mid(64, 32)); QByteArray ba = QByteArray::fromBase64(encryptedResponseBody); QSimpleCrypto::QBlockCipher blockCipher; responseBody = blockCipher.decryptAesBlockCipher(ba, key, iv); } else { responseBody = encryptedResponseBody; } } catch (...) { Utils::logException(); qCritical() << "error loading private key from environment variables or decrypting payload" << encryptedResponseBody; continue; } auto endpointsArray = QJsonDocument::fromJson(responseBody).array(); QStringList endpoints; for (const auto &endpoint : endpointsArray) { endpoints.push_back(endpoint.toString()); } return endpoints; } else { auto replyError = reply->error(); int httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); qDebug() << replyError; qDebug() << httpStatusCode; qDebug() << "go to the next storage endpoint"; reply->deleteLater(); } } return {}; } bool GatewayController::shouldBypassProxy(const QNetworkReply::NetworkError &replyError, const QByteArray &responseBody, bool checkEncryption, const QByteArray &key, const QByteArray &iv, const QByteArray &salt) { if (replyError == QNetworkReply::NetworkError::OperationCanceledError || replyError == QNetworkReply::NetworkError::TimeoutError) { qDebug() << "timeout occurred"; qDebug() << replyError; return true; } else if (responseBody.contains("html")) { qDebug() << "the response contains an html tag"; return true; } else if (replyError == QNetworkReply::NetworkError::ContentNotFoundError) { if (responseBody.contains(errorResponsePattern1) || responseBody.contains(errorResponsePattern2) || responseBody.contains(errorResponsePattern3)) { return false; } else { qDebug() << replyError; return true; } } else if (replyError == QNetworkReply::NetworkError::OperationNotImplementedError) { if (responseBody.contains(updateRequestResponsePattern)) { return false; } else { qDebug() << replyError; return true; } } else if (replyError != QNetworkReply::NetworkError::NoError) { qDebug() << replyError; return true; } else if (checkEncryption) { try { QSimpleCrypto::QBlockCipher blockCipher; static_cast(blockCipher.decryptAesBlockCipher(responseBody, key, iv, "", salt)); } catch (...) { qDebug() << "failed to decrypt the data"; return true; } } return false; } void GatewayController::bypassProxy(const QString &endpoint, const QString &serviceType, const QString &userCountryCode, std::function requestFunction, std::function &sslErrors)> replyProcessingFunction) { QStringList proxyUrls = getProxyUrls(serviceType, userCountryCode); std::random_device randomDevice; std::mt19937 generator(randomDevice()); std::shuffle(proxyUrls.begin(), proxyUrls.end(), generator); QByteArray responseBody; auto bypassFunction = [this](const QString &endpoint, const QString &proxyUrl, std::function requestFunction, std::function &sslErrors)> replyProcessingFunction) { QEventLoop wait; QList sslErrors; qDebug() << "go to the next proxy endpoint"; QNetworkReply *reply = requestFunction(endpoint.arg(proxyUrl)); QObject::connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit); connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList &errors) { sslErrors = errors; }); wait.exec(); auto result = replyProcessingFunction(reply, sslErrors); reply->deleteLater(); return result; }; if (m_proxyUrl.isEmpty()) { QNetworkRequest request; request.setTransferTimeout(1000); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); QEventLoop wait; QList sslErrors; QNetworkReply *reply; for (const QString &proxyUrl : proxyUrls) { request.setUrl(proxyUrl + "lmbd-health"); reply = amnApp->networkManager()->get(request); connect(reply, &QNetworkReply::finished, &wait, &QEventLoop::quit); connect(reply, &QNetworkReply::sslErrors, [this, &sslErrors](const QList &errors) { sslErrors = errors; }); wait.exec(); if (reply->error() == QNetworkReply::NetworkError::NoError) { reply->deleteLater(); m_proxyUrl = proxyUrl; if (!m_proxyUrl.isEmpty()) { break; } } else { reply->deleteLater(); } } } if (!m_proxyUrl.isEmpty()) { if (bypassFunction(endpoint, m_proxyUrl, requestFunction, replyProcessingFunction)) { return; } } for (const QString &proxyUrl : proxyUrls) { if (bypassFunction(endpoint, proxyUrl, requestFunction, replyProcessingFunction)) { m_proxyUrl = proxyUrl; break; } } }