Compare commits

...

15 Commits

Author SHA1 Message Date
yp
03c5a9f7e0 Add check version iOS/Android & Splesh screen 2026-07-17 16:38:49 +03:00
vkamn
72ee32af1b chore: bump xcode version 2026-07-11 02:18:16 +08:00
vkamn
4cbe3e7f17 feat: add connection warmup 2026-07-11 02:06:09 +08:00
vkamn
241ed18f96 feat: move protocol selection to home page 2026-07-09 22:33:45 +08:00
vkamn
aadd1e81d1 feat: add cryptoUtils 2026-07-09 13:31:30 +08:00
NickVs2015
129ae44edc fix: backup file and sigfall when app start (#2732)
* fix: backup loading

* fix: qt 6.11 sig fall
2026-06-15 12:56:58 +07:00
vkamn
16fc44f989 chore: bump version 2026-06-14 13:09:02 +07:00
vkamn
ef909d3605 chore: fix typo 2026-06-14 13:05:19 +07:00
Yaroslav Gurov
b9ca3315c6 fix: handle undefined values properly 2026-06-14 13:00:44 +07:00
vkamn
e9ed5b59a4 chore: bump version 2026-06-11 11:42:39 +07:00
vkamn
047dbb2677 Merge branch 'feat/proxy-storage-cache' of github-amnezia:amnezia-vpn/amnezia-client into feat/proxy-storage-cache 2026-06-09 23:02:46 +07:00
vkamn
e9efe32f9b chore: bump version 2026-06-09 23:01:49 +07:00
Yaroslav Gurov
2dd3531e78 chore: bump awg dependency 2026-06-09 10:51:58 +02:00
vkamn
129f79ca2c chore: bump version 2026-05-29 22:51:23 +08:00
vkamn
50769f231d feat: add proxy storage cache 2026-05-25 19:17:17 +08:00
45 changed files with 1330 additions and 245 deletions

View File

@@ -410,7 +410,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
@@ -547,7 +547,7 @@ jobs:
env:
ANDROID_BUILD_PLATFORM: android-36
QT_VERSION: 6.10.1
QT_VERSION: 6.11.1
QT_MODULES: 'qtremoteobjects qt5compat qtimageformats qtshadertools'
PROD_AGW_PUBLIC_KEY: ${{ secrets.PROD_AGW_PUBLIC_KEY }}
PROD_S3_ENDPOINT: ${{ secrets.PROD_S3_ENDPOINT }}

3
.gitmodules vendored
View File

@@ -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

View File

@@ -1,7 +1,7 @@
cmake_minimum_required(VERSION 3.25.0 FATAL_ERROR)
set(PROJECT AmneziaVPN)
set(AMNEZIAVPN_VERSION 4.8.15.4)
set(AMNEZIAVPN_VERSION 4.8.21.0)
project(${PROJECT} VERSION ${AMNEZIAVPN_VERSION}
DESCRIPTION "AmneziaVPN"
@@ -12,7 +12,7 @@ string(TIMESTAMP CURRENT_DATE "%Y-%m-%d")
set(RELEASE_DATE "${CURRENT_DATE}")
set(APP_MAJOR_VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}.${CMAKE_PROJECT_VERSION_PATCH})
set(APP_ANDROID_VERSION_CODE 2120)
set(APP_ANDROID_VERSION_CODE 2133)
if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
set(MZ_PLATFORM_NAME "linux")

View File

@@ -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}
)

View File

@@ -122,4 +122,5 @@ dependencies {
implementation(libs.google.mlkit)
implementation(libs.androidx.datastore)
implementation(libs.androidx.biometric)
implementation(libs.google.play.app.update)
}

View File

@@ -12,6 +12,7 @@ androidx-datastore = "1.1.1"
kotlinx-coroutines = "1.8.1"
kotlinx-serialization = "1.6.3"
google-mlkit = "17.3.0"
google-play-app-update = "2.1.0"
[libraries]
androidx-core = { module = "androidx.core:core-ktx", version.ref = "androidx-core" }
@@ -28,6 +29,7 @@ androidx-datastore = { module = "androidx.datastore:datastore-preferences", vers
kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx-coroutines" }
kotlinx-serialization-protobuf = { module = "org.jetbrains.kotlinx:kotlinx-serialization-protobuf", version.ref = "kotlinx-serialization" }
google-mlkit = { module = "com.google.mlkit:barcode-scanning", version.ref = "google-mlkit" }
google-play-app-update = { module = "com.google.android.play:app-update-ktx", version.ref = "google-play-app-update" }
[bundles]
androidx-camera = [

View File

@@ -47,6 +47,12 @@ import kotlin.LazyThreadSafetyMode.NONE
import kotlin.coroutines.CoroutineContext
import kotlin.text.RegexOption.IGNORE_CASE
import AppListProvider
import com.google.android.play.core.appupdate.AppUpdateInfo
import com.google.android.play.core.appupdate.AppUpdateManager
import com.google.android.play.core.appupdate.AppUpdateManagerFactory
import com.google.android.play.core.appupdate.AppUpdateOptions
import com.google.android.play.core.install.model.AppUpdateType
import com.google.android.play.core.install.model.UpdateAvailability
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -73,6 +79,7 @@ private const val CHECK_VPN_PERMISSION_ACTION_CODE = 1
private const val CREATE_FILE_ACTION_CODE = 2
private const val OPEN_FILE_ACTION_CODE = 3
private const val CHECK_NOTIFICATION_PERMISSION_ACTION_CODE = 4
private const val UPDATE_APP_ACTION_CODE = 5
private const val PREFS_NOTIFICATION_PERMISSION_ASKED = "NOTIFICATION_PERMISSION_ASKED"
private const val OPEN_FILE_AFTER_RESUME_DELAY_MS = 400L
@@ -99,6 +106,9 @@ class AmneziaActivity : QtActivity() {
private var pendingOpenFileUri: String? = null
private var openFileDeliveryScheduled = false
private var appUpdateManager: AppUpdateManager? = null
private var pendingUpdateInfo: AppUpdateInfo? = null
private val vpnServiceEventHandler: Handler by lazy(NONE) {
object : Handler(Looper.getMainLooper()) {
override fun handleMessage(msg: Message) {
@@ -376,6 +386,18 @@ class AmneziaActivity : QtActivity() {
QtAndroidController.onActivityResumed()
}
appUpdateManager?.appUpdateInfo?.addOnSuccessListener { info ->
if (info.updateAvailability() == UpdateAvailability.DEVELOPER_TRIGGERED_UPDATE_IN_PROGRESS) {
try {
appUpdateManager?.startUpdateFlowForResult(
info, this, AppUpdateOptions.newBuilder(AppUpdateType.IMMEDIATE).build(), UPDATE_APP_ACTION_CODE
)
} catch (e: Exception) {
Log.w(TAG, "resume update flow failed: ${e.message}")
}
}
}
if (pendingOpenFileUri != null && !openFileDeliveryScheduled) {
val uri = pendingOpenFileUri!!
openFileDeliveryScheduled = true
@@ -489,9 +511,48 @@ class AmneziaActivity : QtActivity() {
super.onDestroy()
}
fun checkPlayUpdate() {
runOnUiThread {
val manager = appUpdateManager
?: AppUpdateManagerFactory.create(applicationContext).also { appUpdateManager = it }
manager.appUpdateInfo
.addOnSuccessListener { info ->
val available = info.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE &&
info.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE)
pendingUpdateInfo = if (available) info else null
Log.d(TAG, "Play update available: $available")
QtAndroidController.onPlayUpdateAvailability(available)
}
.addOnFailureListener { e ->
Log.w(TAG, "checkPlayUpdate failed: ${e.message}")
QtAndroidController.onPlayUpdateAvailability(false)
}
}
}
fun startPlayUpdateFlow() {
runOnUiThread {
val manager = appUpdateManager ?: return@runOnUiThread
val info = pendingUpdateInfo ?: return@runOnUiThread
try {
manager.startUpdateFlowForResult(
info, this, AppUpdateOptions.newBuilder(AppUpdateType.IMMEDIATE).build(), UPDATE_APP_ACTION_CODE
)
} catch (e: Exception) {
Log.w(TAG, "startPlayUpdateFlow failed: ${e.message}")
}
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
Log.d(TAG, "Process activity result, code: ${actionCodeToString(requestCode)}, " +
"resultCode: $resultCode, data: $data")
if (requestCode == UPDATE_APP_ACTION_CODE) {
if (resultCode != RESULT_OK) {
QtAndroidController.onPlayUpdateAvailability(true)
}
return
}
actionResultHandlers[requestCode]?.let { handler ->
when (resultCode) {
RESULT_OK -> handler.onSuccess(data)
@@ -792,6 +853,16 @@ class AmneziaActivity : QtActivity() {
else -> type = "*/*"
}
}
// Force system document picker to avoid third-party file managers
// that may lack storage permissions (common on Android TV devices)
val systemPickerPackage = listOf("com.google.android.documentsui", "com.android.documentsui")
.firstOrNull { pkg ->
try { packageManager.getPackageInfo(pkg, 0); true }
catch (_: PackageManager.NameNotFoundException) { false }
}
if (systemPickerPackage != null) {
`package` = systemPickerPackage
}
}
} else {
Intent(this@AmneziaActivity, TvFilePicker::class.java)
@@ -1064,13 +1135,11 @@ class AmneziaActivity : QtActivity() {
@Suppress("unused")
fun sendTouch(x: Float, y: Float) {
Log.v(TAG, "Send touch: $x, $y")
blockingCall {
findQtWindow(window.decorView)?.let {
Log.v(TAG, "Send touch to $it")
it.dispatchTouchEvent(createEvent(x, y, SystemClock.uptimeMillis(), MotionEvent.ACTION_DOWN))
it.dispatchTouchEvent(createEvent(x, y, SystemClock.uptimeMillis(), MotionEvent.ACTION_UP))
}
}
}
private fun findQtWindow(view: View): View? {
@@ -1182,6 +1251,7 @@ class AmneziaActivity : QtActivity() {
CREATE_FILE_ACTION_CODE -> "CREATE_FILE"
OPEN_FILE_ACTION_CODE -> "OPEN_FILE"
CHECK_NOTIFICATION_PERMISSION_ACTION_CODE -> "CHECK_NOTIFICATION_PERMISSION"
UPDATE_APP_ACTION_CODE -> "UPDATE_APP"
else -> actionCode.toString()
}
}

View File

@@ -34,4 +34,6 @@ object QtAndroidController {
external fun onActivityPaused()
external fun onActivityResumed()
external fun onPlayUpdateAvailability(available: Boolean)
}

View File

@@ -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

View File

@@ -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
)

View File

@@ -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

View File

@@ -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");

View File

@@ -24,6 +24,8 @@ CoreController::CoreController(const QSharedPointer<VpnConnection> &vpnConnectio
initAndroidController();
initAppleController();
m_appUpdateController->start();
initNotificationHandler();
m_translator.reset(new QTranslator());
@@ -165,6 +167,9 @@ void CoreController::initControllers()
m_apiNewsController.reset(new ApiNewsController(m_newsModel, m_settings, m_serversModel, this));
m_engine->rootContext()->setContextProperty("ApiNewsController", m_apiNewsController.get());
m_appUpdateController.reset(new AppUpdateController(this));
m_engine->rootContext()->setContextProperty("AppUpdate", m_appUpdateController.get());
}
void CoreController::initAndroidController()

View File

@@ -23,6 +23,7 @@
#include "ui/controllers/settingsController.h"
#include "ui/controllers/sitesController.h"
#include "ui/controllers/systemController.h"
#include "ui/controllers/appUpdateController.h"
#include "ui/models/allowed_dns_model.h"
#include "ui/models/containers_model.h"
@@ -123,6 +124,8 @@ private:
QScopedPointer<ApiConfigsController> m_apiConfigsController;
QScopedPointer<ApiNewsController> m_apiNewsController;
QScopedPointer<AppUpdateController> m_appUpdateController;
QSharedPointer<ContainersModel> m_containersModel;
QSharedPointer<ContainersModel> m_defaultServerContainersModel;
QSharedPointer<ServersModel> m_serversModel;

View File

@@ -12,13 +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 "utilities.h"
#include "settings.h"
#ifdef AMNEZIA_DESKTOP
#include "core/ipcclient.h"
@@ -51,15 +51,75 @@ namespace
constexpr QLatin1String unprocessableSubscriptionMessage("Failed to retrieve subscription information. Is it activated?");
constexpr int proxyStorageRequestTimeoutMsecs = 3000;
QStringList shuffledProxyUrls(const QStringList &proxyUrls)
{
QStringList shuffled = proxyUrls;
std::random_device randomDevice;
std::mt19937 generator(randomDevice());
std::shuffle(shuffled.begin(), shuffled.end(), generator);
return shuffled;
}
QString getProxyUrlsCacheKey(const QString &serviceType, const QString &userCountryCode)
{
return QStringLiteral("service_%1_country_%2").arg(serviceType, userCountryCode);
}
bool decryptProxyUrlsPayload(const QByteArray &encryptedPayload, bool isDevEnvironment, QByteArray &decryptedPayload)
{
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);
decryptedPayload = CryptoUtils::decryptAes256Cbc(ba, decKey, iv);
if (decryptedPayload.isEmpty()) {
return false;
}
} else {
decryptedPayload = encryptedPayload;
}
return true;
}
QStringList readCachedProxyUrls(const QByteArray &cachedProxyUrlsEncrypted, bool isDevEnvironment)
{
if (cachedProxyUrlsEncrypted.isEmpty()) {
return {};
}
QByteArray cachedProxyUrlsDecrypted;
if (!decryptProxyUrlsPayload(cachedProxyUrlsEncrypted, isDevEnvironment, cachedProxyUrlsDecrypted)) {
qCritical() << "error decrypting cached proxy urls payload";
return {};
}
QJsonArray endpointsArray = QJsonDocument::fromJson(cachedProxyUrlsDecrypted).array();
QStringList endpoints;
endpoints.reserve(endpointsArray.size());
for (const QJsonValue &endpoint : endpointsArray) {
endpoints.push_back(endpoint.toString());
}
return endpoints;
}
}
GatewayController::GatewayController(const QString &gatewayEndpoint, const bool isDevEnvironment, const int requestTimeoutMsecs,
const bool isStrictKillSwitchEnabled, QObject *parent)
const bool isStrictKillSwitchEnabled, const std::shared_ptr<Settings> &settings,
QObject *parent)
: QObject(parent),
m_gatewayEndpoint(gatewayEndpoint),
m_isDevEnvironment(isDevEnvironment),
m_requestTimeoutMsecs(requestTimeoutMsecs),
m_isStrictKillSwitchEnabled(isStrictKillSwitchEnabled)
m_isStrictKillSwitchEnabled(isStrictKillSwitchEnabled),
m_settings(settings)
{
}
@@ -93,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;
@@ -148,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;
}
@@ -271,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();
@@ -310,8 +358,9 @@ QFuture<QPair<ErrorCode, QByteArray>> GatewayController::postAsync(const QString
QStringList proxyStorageUrls;
appendStorageUrls(primaryBaseUrls, proxyStorageUrls);
appendStorageUrls(fallbackBaseUrls, proxyStorageUrls);
const QString proxyUrlsCacheKey = getProxyUrlsCacheKey(serviceType, userCountryCode);
getProxyUrlsAsync(proxyStorageUrls, 0, [this, encRequestData, endpoint, processResponse](const QStringList &proxyUrls) {
getProxyUrlsAsync(proxyStorageUrls, 0, proxyUrlsCacheKey, [this, encRequestData, endpoint, processResponse](const QStringList &proxyUrls) {
getProxyUrlAsync(proxyUrls, 0, [this, encRequestData, endpoint, processResponse](const QString &proxyUrl) {
bypassProxyAsync(endpoint, proxyUrl, encRequestData,
[processResponse, this](const QByteArray &decryptedBody, bool isDecryptionSuccessful,
@@ -357,8 +406,6 @@ QStringList GatewayController::getProxyUrls(const QString &serviceType, const QS
std::shuffle(primaryBaseUrls.begin(), primaryBaseUrls.end(), generator);
std::shuffle(fallbackBaseUrls.begin(), fallbackBaseUrls.end(), generator);
QByteArray key = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
auto appendStorageUrls = [&serviceType, &userCountryCode](const QStringList &baseUrls, QStringList &target) {
if (!serviceType.isEmpty()) {
for (const auto &baseUrl : baseUrls) {
@@ -374,10 +421,12 @@ QStringList GatewayController::getProxyUrls(const QString &serviceType, const QS
QStringList proxyStorageUrls;
appendStorageUrls(primaryBaseUrls, proxyStorageUrls);
appendStorageUrls(fallbackBaseUrls, proxyStorageUrls);
const QString proxyUrlsCacheKey = getProxyUrlsCacheKey(serviceType, userCountryCode);
const QByteArray cachedProxyUrlsEncrypted = m_settings->readGatewayProxyUrls(proxyUrlsCacheKey);
if (proxyStorageUrls.empty()) {
qDebug() << "empty storage endpoint list";
return {};
return readCachedProxyUrls(cachedProxyUrlsEncrypted, m_isDevEnvironment);
}
for (const auto &proxyStorageUrl : proxyStorageUrls) {
@@ -392,26 +441,8 @@ QStringList GatewayController::getProxyUrls(const QString &serviceType, const QS
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();
if (!decryptProxyUrlsPayload(encryptedResponseBody, m_isDevEnvironment, responseBody)) {
qCritical() << "error loading private key from environment variables or decrypting payload" << encryptedResponseBody;
continue;
}
@@ -422,6 +453,8 @@ QStringList GatewayController::getProxyUrls(const QString &serviceType, const QS
for (const auto &endpoint : endpointsArray) {
endpoints.push_back(endpoint.toString());
}
m_settings->writeGatewayProxyUrls(proxyUrlsCacheKey, encryptedResponseBody);
return endpoints;
} else {
auto replyError = reply->error();
@@ -433,7 +466,7 @@ QStringList GatewayController::getProxyUrls(const QString &serviceType, const QS
reply->deleteLater();
}
}
return {};
return readCachedProxyUrls(cachedProxyUrlsEncrypted, m_isDevEnvironment);
}
bool GatewayController::shouldBypassProxy(const QNetworkReply::NetworkError &replyError, const QByteArray &decryptedResponseBody,
@@ -571,10 +604,12 @@ void GatewayController::bypassProxy(const QString &endpoint, const QString &serv
}
void GatewayController::getProxyUrlsAsync(const QStringList proxyStorageUrls, const int currentProxyStorageIndex,
std::function<void(const QStringList &)> onComplete)
const QString &proxyUrlsCacheKey, std::function<void(const QStringList &)> onComplete)
{
const QByteArray cachedProxyUrlsEncrypted = m_settings->readGatewayProxyUrls(proxyUrlsCacheKey);
if (currentProxyStorageIndex >= proxyStorageUrls.size()) {
onComplete({});
onComplete(shuffledProxyUrls(readCachedProxyUrls(cachedProxyUrlsEncrypted, m_isDevEnvironment)));
return;
}
@@ -587,33 +622,17 @@ void GatewayController::getProxyUrlsAsync(const QStringList proxyStorageUrls, co
// connect(reply, &QNetworkReply::sslErrors, this, [state](const QList<QSslError> &e) { *(state->sslErrors) = e; });
connect(reply, &QNetworkReply::finished, this, [this, proxyStorageUrls, currentProxyStorageIndex, onComplete, reply]() {
connect(reply, &QNetworkReply::finished, this,
[this, proxyStorageUrls, currentProxyStorageIndex, proxyUrlsCacheKey, onComplete, reply]() {
if (reply->error() == QNetworkReply::NoError) {
QByteArray encrypted = reply->readAll();
reply->deleteLater();
QByteArray responseBody;
try {
QByteArray key = m_isDevEnvironment ? DEV_AGW_PUBLIC_KEY : PROD_AGW_PUBLIC_KEY;
if (!m_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(encrypted);
QSimpleCrypto::QBlockCipher cipher;
responseBody = cipher.decryptAesBlockCipher(ba, decKey, iv);
} else {
responseBody = encrypted;
}
} catch (...) {
Utils::logException();
if (!decryptProxyUrlsPayload(encrypted, m_isDevEnvironment, responseBody)) {
qCritical() << "error decrypting payload";
QMetaObject::invokeMethod(
this, [=]() { getProxyUrlsAsync(proxyStorageUrls, currentProxyStorageIndex + 1, onComplete); }, Qt::QueuedConnection);
this, [=]() { getProxyUrlsAsync(proxyStorageUrls, currentProxyStorageIndex + 1, proxyUrlsCacheKey, onComplete); }, Qt::QueuedConnection);
return;
}
@@ -621,13 +640,9 @@ void GatewayController::getProxyUrlsAsync(const QStringList proxyStorageUrls, co
QStringList endpoints;
for (const QJsonValue &endpoint : endpointsArray)
endpoints.push_back(endpoint.toString());
m_settings->writeGatewayProxyUrls(proxyUrlsCacheKey, encrypted);
QStringList shuffled = endpoints;
std::random_device randomDevice;
std::mt19937 generator(randomDevice());
std::shuffle(shuffled.begin(), shuffled.end(), generator);
onComplete(shuffled);
onComplete(shuffledProxyUrls(endpoints));
return;
}
@@ -636,7 +651,7 @@ void GatewayController::getProxyUrlsAsync(const QStringList proxyStorageUrls, co
qDebug() << "go to the next storage endpoint";
reply->deleteLater();
QMetaObject::invokeMethod(
this, [=]() { getProxyUrlsAsync(proxyStorageUrls, currentProxyStorageIndex + 1, onComplete); }, Qt::QueuedConnection);
this, [=]() { getProxyUrlsAsync(proxyStorageUrls, currentProxyStorageIndex + 1, proxyUrlsCacheKey, onComplete); }, Qt::QueuedConnection);
});
}

View File

@@ -7,6 +7,9 @@
#include <QPair>
#include <QPromise>
#include <QSharedPointer>
#include <QString>
#include <QStringList>
#include <memory>
#include "core/defs.h"
@@ -14,13 +17,16 @@
#include "platforms/ios/ios_controller.h"
#endif
class Settings;
class GatewayController : public QObject
{
Q_OBJECT
public:
explicit GatewayController(const QString &gatewayEndpoint, const bool isDevEnvironment, const int requestTimeoutMsecs,
const bool isStrictKillSwitchEnabled, QObject *parent = nullptr);
const bool isStrictKillSwitchEnabled, const std::shared_ptr<Settings> &settings,
QObject *parent = nullptr);
amnezia::ErrorCode post(const QString &endpoint, const QJsonObject apiPayload, QByteArray &responseBody);
QFuture<QPair<amnezia::ErrorCode, QByteArray>> postAsync(const QString &endpoint, const QJsonObject apiPayload);
@@ -53,7 +59,7 @@ private:
std::function<bool(QNetworkReply *reply, const QList<QSslError> &sslErrors)> replyProcessingFunction);
void getProxyUrlsAsync(const QStringList proxyStorageUrls, const int currentProxyStorageIndex,
std::function<void(const QStringList &)> onComplete);
const QString &proxyUrlsCacheKey, std::function<void(const QStringList &)> onComplete);
void getProxyUrlAsync(const QStringList proxyUrls, const int currentProxyIndex, std::function<void(const QString &)> onComplete);
void bypassProxyAsync(
const QString &endpoint, const QString &proxyUrl, EncryptedRequestData encRequestData,
@@ -63,6 +69,7 @@ private:
QString m_gatewayEndpoint;
bool m_isDevEnvironment = false;
bool m_isStrictKillSwitchEnabled = false;
std::shared_ptr<Settings> m_settings;
inline static QString m_proxyUrl;
};

View File

@@ -0,0 +1,193 @@
#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());
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;
}
}

View File

@@ -0,0 +1,18 @@
#ifndef PAYLOADSENDER_H
#define PAYLOADSENDER_H
#include <QByteArray>
#include <QJsonArray>
#include <QString>
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

View File

@@ -390,55 +390,55 @@ bool Daemon::parseConfig(const QJsonObject& obj, InterfaceConfig& config) {
config.m_killSwitchEnabled = QVariant(obj.value("killSwitchOption").toString()).toBool();
if (!obj.value("Jc").isNull()) {
config.m_junkPacketCount = obj.value("Jc").toString();
if (const auto jc = obj.value("Jc"); !jc.isUndefined()) {
config.m_junkPacketCount = jc.toString();
}
if (!obj.value("Jmin").isNull()) {
config.m_junkPacketMinSize = obj.value("Jmin").toString();
if (const auto jmin = obj.value("Jmin"); !jmin.isUndefined()) {
config.m_junkPacketMinSize = jmin.toString();
}
if (!obj.value("Jmax").isNull()) {
config.m_junkPacketMaxSize = obj.value("Jmax").toString();
if (const auto jmax = obj.value("Jmax"); !jmax.isUndefined()) {
config.m_junkPacketMaxSize = jmax.toString();
}
if (!obj.value("S1").isNull()) {
config.m_initPacketJunkSize = obj.value("S1").toString();
if (const auto s1 = obj.value("S1"); !s1.isUndefined()) {
config.m_initPacketJunkSize = s1.toString();
}
if (!obj.value("S2").isNull()) {
config.m_responsePacketJunkSize = obj.value("S2").toString();
if (const auto s2 = obj.value("S2"); !s2.isUndefined()) {
config.m_responsePacketJunkSize = s2.toString();
}
if (!obj.value("S3").isNull()) {
config.m_cookieReplyPacketJunkSize = obj.value("S3").toString();
if (const auto s3 = obj.value("S3"); !s3.isUndefined()) {
config.m_cookieReplyPacketJunkSize = s3.toString();
}
if (!obj.value("S4").isNull()) {
config.m_transportPacketJunkSize = obj.value("S4").toString();
if (const auto s4 = obj.value("S4"); !s4.isUndefined()) {
config.m_transportPacketJunkSize = s4.toString();
}
if (!obj.value("H1").isNull()) {
config.m_initPacketMagicHeader = obj.value("H1").toString();
if (const auto h1 = obj.value("H1"); !h1.isUndefined()) {
config.m_initPacketMagicHeader = h1.toString();
}
if (!obj.value("H2").isNull()) {
config.m_responsePacketMagicHeader = obj.value("H2").toString();
if (const auto h2 = obj.value("H2"); !h2.isUndefined()) {
config.m_responsePacketMagicHeader = h2.toString();
}
if (!obj.value("H3").isNull()) {
config.m_underloadPacketMagicHeader = obj.value("H3").toString();
if (const auto h3 = obj.value("H3"); !h3.isUndefined()) {
config.m_underloadPacketMagicHeader = h3.toString();
}
if (!obj.value("H4").isNull()) {
config.m_transportPacketMagicHeader = obj.value("H4").toString();
if (const auto h4 = obj.value("H4"); !h4.isUndefined()) {
config.m_transportPacketMagicHeader = h4.toString();
}
if (!obj.value("I1").isNull()) {
config.m_specialJunk["I1"] = obj.value("I1").toString();
if (const auto i1 = obj.value("I1"); !i1.isUndefined()) {
config.m_specialJunk["I1"] = i1.toString();
}
if (!obj.value("I2").isNull()) {
config.m_specialJunk["I2"] = obj.value("I2").toString();
if (const auto i2 = obj.value("I2"); !i2.isUndefined()) {
config.m_specialJunk["I2"] = i2.toString();
}
if (!obj.value("I3").isNull()) {
config.m_specialJunk["I3"] = obj.value("I3").toString();
if (const auto i3 = obj.value("I3"); !i3.isUndefined()) {
config.m_specialJunk["I3"] = i3.toString();
}
if (!obj.value("I4").isNull()) {
config.m_specialJunk["I4"] = obj.value("I4").toString();
if (const auto i4 = obj.value("I4"); !i4.isUndefined()) {
config.m_specialJunk["I4"] = i4.toString();
}
if (!obj.value("I5").isNull()) {
config.m_specialJunk["I5"] = obj.value("I5").toString();
if (const auto i5 = obj.value("I5"); !i5.isUndefined()) {
config.m_specialJunk["I5"] = i5.toString();
}
return true;

View 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

View File

@@ -103,7 +103,8 @@ bool AndroidController::initialize()
{"onImeInsetsChanged", "(I)V", reinterpret_cast<void *>(onImeInsetsChanged)},
{"onSystemBarsInsetsChanged", "(II)V", reinterpret_cast<void *>(onSystemBarsInsetsChanged)},
{"onActivityPaused", "()V", reinterpret_cast<void *>(onActivityPaused)},
{"onActivityResumed", "()V", reinterpret_cast<void *>(onActivityResumed)}
{"onActivityResumed", "()V", reinterpret_cast<void *>(onActivityResumed)},
{"onPlayUpdateAvailability", "(Z)V", reinterpret_cast<void *>(onPlayUpdateAvailability)}
};
QJniEnvironment env;
@@ -154,6 +155,16 @@ void AndroidController::resetLastServer(int serverIndex)
callActivityMethod("resetLastServer", "(I)V", serverIndex);
}
void AndroidController::checkPlayUpdate()
{
callActivityMethod("checkPlayUpdate", "()V");
}
void AndroidController::startPlayUpdateFlow()
{
callActivityMethod("startPlayUpdateFlow", "()V");
}
void AndroidController::saveFile(const QString &fileName, const QString &data)
{
callActivityMethod("saveFile", "(Ljava/lang/String;Ljava/lang/String;)V",
@@ -533,6 +544,14 @@ void AndroidController::onAuthResult(JNIEnv *env, jobject thiz, jboolean result)
emit AndroidController::instance()->authenticationResult(result);
}
void AndroidController::onPlayUpdateAvailability(JNIEnv *env, jobject thiz, jboolean available)
{
Q_UNUSED(env);
Q_UNUSED(thiz);
emit AndroidController::instance()->playUpdateAvailability(available);
}
// static
bool AndroidController::decodeQrCode(JNIEnv *env, jobject thiz, jstring data)
{

View File

@@ -56,6 +56,9 @@ public:
bool requestAuthentication();
void sendTouch(float x, float y);
void checkPlayUpdate();
void startPlayUpdateFlow();
static bool initLogging();
static void messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &message);
@@ -77,6 +80,7 @@ signals:
void systemBarsInsetsChanged(int navBarHeightDp, int statusBarHeightDp);
void activityPaused();
void activityResumed();
void playUpdateAvailability(bool available);
private:
bool isWaitingStatus = true;
@@ -109,6 +113,7 @@ private:
static void onSystemBarsInsetsChanged(JNIEnv *env, jobject thiz, jint navBarHeightDp, jint statusBarHeightDp);
static void onActivityPaused(JNIEnv *env, jobject thiz);
static void onActivityResumed(JNIEnv *env, jobject thiz);
static void onPlayUpdateAvailability(JNIEnv *env, jobject thiz, jboolean available);
template <typename Ret, typename ...Args>
static auto callActivityMethod(const char *methodName, const char *signature, Args &&...args);

View File

@@ -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";

View File

@@ -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>

View 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";
}

View File

@@ -15,6 +15,7 @@ namespace
const char cloudFlareNs2[] = "1.0.0.1";
constexpr char gatewayEndpoint[] = "http://gw.amnezia.org:80/";
constexpr char proxyUrlsKey[] = "Conf/proxyUrls/";
}
Settings::Settings(QObject *parent) : QObject(parent), m_settings(ORGANIZATION_NAME, APPLICATION_NAME, this)
@@ -526,6 +527,24 @@ void Settings::toggleDevGatewayEnv(bool enabled)
m_settings.setValue("Conf/devGatewayEnv", enabled);
}
QByteArray Settings::readGatewayProxyUrls(const QString &cacheKey) const
{
if (cacheKey.isEmpty()) {
return {};
}
return m_settings.value(QString(proxyUrlsKey) + cacheKey).toByteArray();
}
void Settings::writeGatewayProxyUrls(const QString &cacheKey, const QByteArray &proxyUrlsEncrypted)
{
if (cacheKey.isEmpty()) {
return;
}
m_settings.setValue(QString(proxyUrlsKey) + cacheKey, proxyUrlsEncrypted);
}
bool Settings::isHomeAdLabelVisible()
{
return m_settings.value("Conf/homeAdLabelVisible", true).toBool();

View File

@@ -4,6 +4,7 @@
#include <QObject>
#include <QSettings>
#include <QString>
#include <QByteArray>
#include <QJsonArray>
#include <QJsonDocument>
@@ -234,6 +235,8 @@ public:
QString getGatewayEndpoint(bool isTestPurchase = false);
bool isDevGatewayEnv(bool isTestPurchase = false);
void toggleDevGatewayEnv(bool enabled);
QByteArray readGatewayProxyUrls(const QString &cacheKey) const;
void writeGatewayProxyUrls(const QString &cacheKey, const QByteArray &proxyUrlsEncrypted);
bool isHomeAdLabelVisible();
void disableHomeAdLabel();

View File

@@ -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(),
@@ -1027,7 +1045,7 @@ bool ApiConfigsController::updateServiceFromTelegram(const int serverIndex)
#endif
GatewayController gatewayController(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(), apiDefs::requestTimeoutMsecs,
m_settings->isStrictKillSwitchEnabled());
m_settings->isStrictKillSwitchEnabled(), m_settings);
auto serverConfig = m_serversModel->getServerConfig(serverIndex);
auto installationUuid = m_settings->getInstallationUuid(true);
@@ -1273,6 +1291,6 @@ ErrorCode ApiConfigsController::executeRequest(const QString &endpoint, const QJ
bool isTestPurchase)
{
GatewayController gatewayController(m_settings->getGatewayEndpoint(isTestPurchase), m_settings->isDevGatewayEnv(isTestPurchase),
apiDefs::requestTimeoutMsecs, m_settings->isStrictKillSwitchEnabled());
apiDefs::requestTimeoutMsecs, m_settings->isStrictKillSwitchEnabled(), m_settings);
return gatewayController.post(endpoint, apiPayload, responseBody);
}

View File

@@ -32,7 +32,8 @@ void ApiNewsController::fetchNews(bool showError)
}
auto gatewayController = QSharedPointer<GatewayController>::create(m_settings->getGatewayEndpoint(), m_settings->isDevGatewayEnv(),
apiDefs::requestTimeoutMsecs, m_settings->isStrictKillSwitchEnabled());
apiDefs::requestTimeoutMsecs,
m_settings->isStrictKillSwitchEnabled(), m_settings);
QJsonObject payload;
payload.insert("locale", m_settings->getAppLanguage().name().split("_").first());

View File

@@ -71,7 +71,7 @@ bool ApiSettingsController::getAccountInfo(bool reload)
bool isTestPurchase = apiConfig.value(apiDefs::key::isTestPurchase).toBool(false);
GatewayController gatewayController(m_settings->getGatewayEndpoint(isTestPurchase), m_settings->isDevGatewayEnv(isTestPurchase),
requestTimeoutMsecs, m_settings->isStrictKillSwitchEnabled());
requestTimeoutMsecs, m_settings->isStrictKillSwitchEnabled(), m_settings);
QJsonObject apiPayload;
apiPayload[configKey::userCountryCode] = apiConfig.value(configKey::userCountryCode).toString();
@@ -110,7 +110,7 @@ void ApiSettingsController::getRenewalLink()
auto gatewayController = QSharedPointer<GatewayController>::create(m_settings->getGatewayEndpoint(isTestPurchase),
m_settings->isDevGatewayEnv(isTestPurchase),
requestTimeoutMsecs,
m_settings->isStrictKillSwitchEnabled());
m_settings->isStrictKillSwitchEnabled(), m_settings);
QJsonObject apiPayload;
apiPayload[configKey::userCountryCode] = apiConfig.value(configKey::userCountryCode).toString();

View File

@@ -0,0 +1,179 @@
#include "appUpdateController.h"
#include <QCoreApplication>
#include <QDebug>
#include "version.h"
#if defined(Q_OS_IOS) || defined(Q_OS_ANDROID)
#include <QDesktopServices>
#endif
#if defined(Q_OS_IOS) || (defined(Q_OS_ANDROID) && defined(QT_DEBUG))
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QLocale>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QRegularExpression>
#include <QVersionNumber>
#endif
#if defined(Q_OS_ANDROID)
#include "platforms/android/android_controller.h"
#endif
namespace
{
#if defined(Q_OS_IOS)
constexpr auto kIosBundleId = "org.amnezia.AmneziaVPN";
constexpr auto kIosStoreUrlFallback = "itms-apps://itunes.apple.com/app/id1600529900";
#endif
#if defined(Q_OS_ANDROID)
constexpr auto kAndroidPackage = "org.amnezia.vpn";
constexpr auto kAndroidStoreUrl = "https://play.google.com/store/apps/details?id=org.amnezia.vpn";
#endif
} // namespace
AppUpdateController::AppUpdateController(QObject *parent) : QObject(parent)
{
#if defined(Q_OS_ANDROID) && !defined(QT_DEBUG)
connect(AndroidController::instance(), &AndroidController::playUpdateAvailability, this,
[this](bool available) { setState(available ? UpdateRequired : UpToDate); },
Qt::QueuedConnection);
#endif
}
QString AppUpdateController::currentVersion() const
{
return QString(APP_VERSION);
}
void AppUpdateController::setState(State state)
{
if (m_state == state) {
return;
}
m_state = state;
emit stateChanged();
}
void AppUpdateController::start()
{
setState(Checking);
#if defined(Q_OS_IOS)
startHttpCheck(versionSourceUrl());
#elif defined(Q_OS_ANDROID)
#if defined(QT_DEBUG)
startHttpCheck(versionSourceUrl());
#else
AndroidController::instance()->checkPlayUpdate();
#endif
#else
setState(UpToDate);
#endif
}
void AppUpdateController::openStore()
{
#if defined(Q_OS_IOS)
if (!m_storeUrl.isEmpty()) {
QDesktopServices::openUrl(QUrl(m_storeUrl));
}
#elif defined(Q_OS_ANDROID)
#if defined(QT_DEBUG)
QDesktopServices::openUrl(QUrl(m_storeUrl.isEmpty() ? QString::fromLatin1(kAndroidStoreUrl) : m_storeUrl));
#else
AndroidController::instance()->startPlayUpdateFlow();
#endif
#endif
}
void AppUpdateController::quit()
{
qApp->quit();
}
#if defined(Q_OS_IOS) || (defined(Q_OS_ANDROID) && defined(QT_DEBUG))
QUrl AppUpdateController::versionSourceUrl() const
{
#if defined(Q_OS_IOS)
const QString country = QLocale::system().name().section('_', 1, 1).toLower();
QString url = QStringLiteral("https://itunes.apple.com/lookup?bundleId=%1").arg(kIosBundleId);
if (!country.isEmpty()) {
url += QStringLiteral("&country=%1").arg(country);
}
return QUrl(url);
#else
return QUrl(QStringLiteral("https://play.google.com/store/apps/details?id=%1&hl=en&gl=US").arg(kAndroidPackage));
#endif
}
bool AppUpdateController::parseVersion(const QByteArray &body, QString &version, QString &storeUrl)
{
#if defined(Q_OS_IOS)
const auto results = QJsonDocument::fromJson(body).object().value("results").toArray();
if (results.isEmpty()) {
return false;
}
const auto first = results.first().toObject();
version = first.value("version").toString();
storeUrl = first.value("trackViewUrl").toString();
if (storeUrl.isEmpty()) {
storeUrl = QString::fromLatin1(kIosStoreUrlFallback);
}
return !version.isEmpty();
#else
const QString html = QString::fromUtf8(body);
static const QRegularExpression re(QStringLiteral("\\[\\[\\[\"(\\d+\\.\\d+(?:\\.\\d+){0,2})\"\\]\\]"));
const auto match = re.match(html);
if (match.hasMatch()) {
version = match.captured(1);
}
storeUrl = QString::fromLatin1(kAndroidStoreUrl);
return !version.isEmpty();
#endif
}
void AppUpdateController::applyStoreVersion(const QString &version, const QString &storeUrl)
{
m_storeVersion = version;
m_storeUrl = storeUrl;
const auto current = QVersionNumber::fromString(currentVersion()).normalized();
const auto store = QVersionNumber::fromString(version).normalized();
qInfo() << "[AppUpdate] current:" << current.toString() << "store:" << store.toString();
setState(store > current ? UpdateRequired : UpToDate);
}
void AppUpdateController::startHttpCheck(const QUrl &url)
{
QNetworkRequest request(url);
request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork);
request.setHeader(QNetworkRequest::UserAgentHeader, QByteArrayLiteral("AmneziaVPN"));
QNetworkReply *reply = m_nam.get(request);
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
reply->deleteLater();
if (reply->error() != QNetworkReply::NoError) {
qWarning() << "[AppUpdate] network error:" << reply->errorString();
setState(NoInternet);
return;
}
QString version;
QString storeUrl;
if (!parseVersion(reply->readAll(), version, storeUrl) || version.isEmpty()) {
qWarning() << "[AppUpdate] could not determine store version";
setState(NoInternet);
return;
}
applyStoreVersion(version, storeUrl);
});
}
#endif

View File

@@ -0,0 +1,61 @@
#ifndef APPUPDATECONTROLLER_H
#define APPUPDATECONTROLLER_H
#include <QNetworkAccessManager>
#include <QObject>
#include <QUrl>
class AppUpdateController : public QObject
{
Q_OBJECT
Q_PROPERTY(bool blocking READ blocking NOTIFY stateChanged)
Q_PROPERTY(bool checking READ checking NOTIFY stateChanged)
Q_PROPERTY(bool noInternet READ noInternet NOTIFY stateChanged)
Q_PROPERTY(bool updateRequired READ updateRequired NOTIFY stateChanged)
Q_PROPERTY(QString currentVersion READ currentVersion CONSTANT)
Q_PROPERTY(QString storeVersion READ storeVersion NOTIFY stateChanged)
public:
explicit AppUpdateController(QObject *parent = nullptr);
bool blocking() const { return m_state != UpToDate; }
bool checking() const { return m_state == Checking; }
bool noInternet() const { return m_state == NoInternet; }
bool updateRequired() const { return m_state == UpdateRequired; }
QString currentVersion() const;
QString storeVersion() const { return m_storeVersion; }
public slots:
void start();
void recheck() { start(); }
void openStore();
void quit();
signals:
void stateChanged();
private:
enum State {
Checking,
NoInternet,
UpdateRequired,
UpToDate
};
void setState(State state);
#if defined(Q_OS_IOS) || (defined(Q_OS_ANDROID) && defined(QT_DEBUG))
void startHttpCheck(const QUrl &url);
void applyStoreVersion(const QString &version, const QString &storeUrl);
QUrl versionSourceUrl() const;
bool parseVersion(const QByteArray &body, QString &version, QString &storeUrl);
#endif
QNetworkAccessManager m_nam;
State m_state { Checking };
QString m_storeVersion;
QString m_storeUrl;
};
#endif // APPUPDATECONTROLLER_H

View File

@@ -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);
}

View File

@@ -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";

View File

@@ -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;

View File

@@ -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";

View File

@@ -47,6 +47,8 @@ public:
IsCountrySelectionAvailableRole,
ApiAvailableCountriesRole,
ApiServerCountryCodeRole,
ApiServiceProtocolRole,
ApiAvailableProtocolsRole,
IsAdVisibleRole,
AdHeaderRole,
AdDescriptionRole,

View File

@@ -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
}
}
}
}
}
}
}

View File

@@ -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 {

View File

@@ -9,6 +9,7 @@ import Style 1.0
import "Config"
import "Controls2"
import "Controls2/TextTypes"
import "Components"
import "Pages2"
@@ -328,6 +329,81 @@ Window {
}
}
Loader {
id: forceUpdateOverlay
anchors.fill: parent
z: 1000
active: AppUpdate.blocking
sourceComponent: Rectangle {
color: AmneziaStyle.color.midnightBlack
MouseArea {
anchors.fill: parent
hoverEnabled: true
}
ColumnLayout {
anchors.centerIn: parent
width: Math.min(root.width - 64, 400)
spacing: 16
BusyIndicator {
Layout.alignment: Qt.AlignHCenter
running: AppUpdate.checking
visible: AppUpdate.checking
}
Header1TextType {
Layout.fillWidth: true
horizontalAlignment: Text.AlignHCenter
visible: !AppUpdate.checking
text: AppUpdate.noInternet ? qsTr("No internet connection") : qsTr("Update required")
}
ParagraphTextType {
Layout.fillWidth: true
horizontalAlignment: Text.AlignHCenter
visible: !AppUpdate.checking
text: AppUpdate.noInternet
? qsTr("Internet access is required to start AmneziaVPN. Check your connection and try again.")
: qsTr("A new version of AmneziaVPN is available. Please update to the latest version to continue.")
}
BasicButtonType {
Layout.fillWidth: true
Layout.topMargin: 16
visible: !AppUpdate.checking
text: AppUpdate.noInternet ? qsTr("Retry") : qsTr("Update")
clickedFunc: function() {
if (AppUpdate.noInternet) {
AppUpdate.recheck()
} else {
AppUpdate.openStore()
}
}
}
BasicButtonType {
Layout.fillWidth: true
visible: !AppUpdate.checking
text: qsTr("Exit")
defaultColor: AmneziaStyle.color.transparent
hoveredColor: AmneziaStyle.color.translucentWhite
pressedColor: AmneziaStyle.color.sheerWhite
disabledColor: AmneziaStyle.color.mutedGray
textColor: AmneziaStyle.color.paleGray
borderWidth: 1
clickedFunc: function() {
AppUpdate.quit()
}
}
}
}
}
function showQuestionDrawer(headerText, descriptionText, yesButtonText, noButtonText, yesButtonFunction, noButtonFunction) {
questionDrawer.headerText = headerText
questionDrawer.descriptionText = descriptionText

View 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;
}

View 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

View File

@@ -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}
)