From 840c388ab998ccc2a3e0b9e12b9b52d33568ce51 Mon Sep 17 00:00:00 2001 From: isamnezia <156459471+isamnezia@users.noreply.github.com> Date: Wed, 6 Mar 2024 04:18:19 +0300 Subject: [PATCH] Add in-app screenshot preventing (#606) In-app screenshot preventing fixes --- client/amnezia_application.cpp | 10 +++ .../src/org/amnezia/vpn/AmneziaActivity.kt | 10 +++ client/cmake/ios.cmake | 1 + .../platforms/android/android_controller.cpp | 5 ++ client/platforms/android/android_controller.h | 1 + client/platforms/ios/QtAppDelegate.mm | 25 ------ client/platforms/ios/ScreenProtection.swift | 87 +++++++++++++++++++ client/settings.h | 2 + client/ui/controllers/settingsController.cpp | 29 ------- client/ui/qml/Pages2/PageHome.qml | 1 - 10 files changed, 116 insertions(+), 55 deletions(-) create mode 100644 client/platforms/ios/ScreenProtection.swift diff --git a/client/amnezia_application.cpp b/client/amnezia_application.cpp index 1ac179fd9..7ddc88780 100644 --- a/client/amnezia_application.cpp +++ b/client/amnezia_application.cpp @@ -24,6 +24,7 @@ #if defined(Q_OS_IOS) #include "platforms/ios/ios_controller.h" + #include #endif #if defined(Q_OS_ANDROID) || defined(Q_OS_IOS) @@ -98,6 +99,10 @@ void AmneziaApplication::init() connect(m_settings.get(), &Settings::saveLogsChanged, AndroidController::instance(), &AndroidController::setSaveLogs); + AndroidController::instance()->setScreenshotsEnabled(m_settings->isScreenshotsEnabled()); + connect(m_settings.get(), &Settings::screenshotsEnabledChanged, + AndroidController::instance(), &AndroidController::setScreenshotsEnabled); + connect(m_settings.get(), &Settings::serverRemoved, AndroidController::instance(), &AndroidController::resetLastServer); @@ -134,6 +139,11 @@ void AmneziaApplication::init() m_pageController->goToPageSettingsBackup(); m_settingsController->importBackupFromOutside(filePath); }); + + AmneziaVPN::toggleScreenshots(m_settings->isScreenshotsEnabled()); + connect(m_settings.get(), &Settings::screenshotsEnabledChanged, [](bool enabled) { + AmneziaVPN::toggleScreenshots(enabled); + }); #endif m_notificationHandler.reset(NotificationHandler::create(nullptr)); diff --git a/client/android/src/org/amnezia/vpn/AmneziaActivity.kt b/client/android/src/org/amnezia/vpn/AmneziaActivity.kt index f01e8df60..ff25ab053 100644 --- a/client/android/src/org/amnezia/vpn/AmneziaActivity.kt +++ b/client/android/src/org/amnezia/vpn/AmneziaActivity.kt @@ -14,6 +14,7 @@ import android.os.IBinder import android.os.Looper import android.os.Message import android.os.Messenger +import android.view.WindowManager.LayoutParams import android.webkit.MimeTypeMap import android.widget.Toast import androidx.annotation.MainThread @@ -453,4 +454,13 @@ class AmneziaActivity : QtActivity() { Log.v(TAG, "Clear logs") Log.clearLogs() } + + @Suppress("unused") + fun setScreenshotsEnabled(enabled: Boolean) { + Log.v(TAG, "Set screenshots enabled: $enabled") + mainScope.launch { + val flag = if (enabled) 0 else LayoutParams.FLAG_SECURE + window.setFlags(flag, LayoutParams.FLAG_SECURE) + } + } } diff --git a/client/cmake/ios.cmake b/client/cmake/ios.cmake index 824c1cafe..ce6c8f94e 100644 --- a/client/cmake/ios.cmake +++ b/client/cmake/ios.cmake @@ -107,6 +107,7 @@ target_sources(${PROJECT} PRIVATE ${CLIENT_ROOT_DIR}/platforms/ios/LogController.swift ${CLIENT_ROOT_DIR}/platforms/ios/Log.swift ${CLIENT_ROOT_DIR}/platforms/ios/LogRecord.swift + ${CLIENT_ROOT_DIR}/platforms/ios/ScreenProtection.swift ) target_sources(${PROJECT} PRIVATE diff --git a/client/platforms/android/android_controller.cpp b/client/platforms/android/android_controller.cpp index b789f0e07..e18fd8640 100644 --- a/client/platforms/android/android_controller.cpp +++ b/client/platforms/android/android_controller.cpp @@ -204,6 +204,11 @@ void AndroidController::clearLogs() callActivityMethod("clearLogs", "()V"); } +void AndroidController::setScreenshotsEnabled(bool enabled) +{ + callActivityMethod("setScreenshotsEnabled", "(Z)V", enabled); +} + // Moving log processing to the Android side jclass AndroidController::log; jmethodID AndroidController::logDebug; diff --git a/client/platforms/android/android_controller.h b/client/platforms/android/android_controller.h index 3491d837e..6c104f3c6 100644 --- a/client/platforms/android/android_controller.h +++ b/client/platforms/android/android_controller.h @@ -39,6 +39,7 @@ public: void setSaveLogs(bool enabled); void exportLogsFile(const QString &fileName); void clearLogs(); + void setScreenshotsEnabled(bool enabled); static bool initLogging(); static void messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &message); diff --git a/client/platforms/ios/QtAppDelegate.mm b/client/platforms/ios/QtAppDelegate.mm index 70e544005..bd7ad6b10 100644 --- a/client/platforms/ios/QtAppDelegate.mm +++ b/client/platforms/ios/QtAppDelegate.mm @@ -3,7 +3,6 @@ #include -UIView *_screen; @implementation QIOSApplicationDelegate (AmneziaVPNDelegate) @@ -15,19 +14,6 @@ UIView *_screen; return YES; } -- (void)applicationWillResignActive:(UIApplication *)application -{ - // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. - // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game. - _screen = [UIScreen.mainScreen snapshotViewAfterScreenUpdates: false]; - UIBlurEffect *blurEffect = [UIBlurEffect effectWithStyle: UIBlurEffectStyleDark]; - UIVisualEffectView *blurBackground = [[UIVisualEffectView alloc] initWithEffect: blurEffect]; - [_screen addSubview: blurBackground]; - blurBackground.frame = _screen.frame; - UIWindow *_window = UIApplication.sharedApplication.keyWindow; - [_window addSubview: _screen]; -} - - (void)applicationDidEnterBackground:(UIApplication *)application { // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. @@ -41,17 +27,6 @@ UIView *_screen; NSLog(@"In the foreground"); } -- (void)applicationDidBecomeActive:(UIApplication *)application -{ - // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. - [_screen removeFromSuperview]; -} - -- (void)applicationWillTerminate:(UIApplication *)application -{ - // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. -} - -(void)application:(UIApplication *)application performFetchWithCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler { // We will add content here soon. NSLog(@"In the completionHandler"); diff --git a/client/platforms/ios/ScreenProtection.swift b/client/platforms/ios/ScreenProtection.swift new file mode 100644 index 000000000..1355dc13d --- /dev/null +++ b/client/platforms/ios/ScreenProtection.swift @@ -0,0 +1,87 @@ +import UIKit + +public func toggleScreenshots(_ isEnabled: Bool) { + let window = UIApplication.shared.keyWindows.first! + + if isEnabled { + ScreenProtection.shared.disable(for: window.rootViewController!.view) + } else { + ScreenProtection.shared.enable(for: window.rootViewController!.view) + } +} + +extension UIApplication { + var keyWindows: [UIWindow] { + connectedScenes + .compactMap { + if #available(iOS 15.0, *) { + ($0 as? UIWindowScene)?.keyWindow + } else { + ($0 as? UIWindowScene)?.windows.first { $0.isKeyWindow } + } + } + } +} + +class ScreenProtection { + public static let shared = ScreenProtection() + + var pairs = [ProtectionPair]() + + private var blurView: UIVisualEffectView? + private var recordingObservation: NSKeyValueObservation? + + public func enable(for view: UIView) { + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + view.subviews.forEach { + self.pairs.append(ProtectionPair(from: $0)) + } + } + } + + public func disable(for view: UIView) { + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + self.pairs.forEach { + $0.removeProtection() + } + + self.pairs.removeAll() + } + } +} + +struct ProtectionPair { + let textField: UITextField + let layer: CALayer + + init(from view: UIView) { + let secureTextField = UITextField() + secureTextField.backgroundColor = .clear + secureTextField.translatesAutoresizingMaskIntoConstraints = false + secureTextField.isSecureTextEntry = true + + view.insertSubview(secureTextField, at: 0) + secureTextField.isUserInteractionEnabled = false + + view.layer.superlayer?.addSublayer(secureTextField.layer) + secureTextField.layer.sublayers?.last?.addSublayer(view.layer) + + secureTextField.topAnchor.constraint(equalTo: view.topAnchor, constant: 0).isActive = true + secureTextField.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0).isActive = true + secureTextField.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0).isActive = true + secureTextField.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0).isActive = true + + self.init(textField: secureTextField, layer: view.layer) + } + + init(textField: UITextField, layer: CALayer) { + self.textField = textField + self.layer = layer + } + + func removeProtection() { + textField.superview?.superview?.layer.addSublayer(layer) + textField.layer.removeFromSuperlayer() + textField.removeFromSuperview() + } +} diff --git a/client/settings.h b/client/settings.h index 613d567bf..b11747e10 100644 --- a/client/settings.h +++ b/client/settings.h @@ -185,12 +185,14 @@ public: void setScreenshotsEnabled(bool enabled) { setValue("Conf/screenshotsEnabled", enabled); + emit screenshotsEnabledChanged(enabled); } void clearSettings(); signals: void saveLogsChanged(bool enabled); + void screenshotsEnabledChanged(bool enabled); void serverRemoved(int serverIndex); void settingsCleared(); diff --git a/client/ui/controllers/settingsController.cpp b/client/ui/controllers/settingsController.cpp index 4aa645338..658bbb198 100644 --- a/client/ui/controllers/settingsController.cpp +++ b/client/ui/controllers/settingsController.cpp @@ -7,9 +7,7 @@ #include "ui/qautostart.h" #include "version.h" #ifdef Q_OS_ANDROID - #include "platforms/android/android_utils.h" #include "platforms/android/android_controller.h" - #include #endif #ifdef Q_OS_IOS @@ -29,20 +27,6 @@ SettingsController::SettingsController(const QSharedPointer &serve m_settings(settings) { m_appVersion = QString("%1 (%2, %3)").arg(QString(APP_VERSION), __DATE__, GIT_COMMIT_HASH); - -#ifdef Q_OS_ANDROID - if (!m_settings->isScreenshotsEnabled()) { - // Set security screen for Android app - AndroidUtils::runOnAndroidThreadSync([]() { - QJniObject activity = AndroidUtils::getActivity(); - QJniObject window = activity.callObjectMethod("getWindow", "()Landroid/view/Window;"); - if (window.isValid()) { - const int FLAG_SECURE = 8192; - window.callMethod("addFlags", "(I)V", FLAG_SECURE); - } - }); - } -#endif } void SettingsController::toggleAmneziaDns(bool enable) @@ -204,19 +188,6 @@ bool SettingsController::isScreenshotsEnabled() void SettingsController::toggleScreenshotsEnabled(bool enable) { m_settings->setScreenshotsEnabled(enable); -#ifdef Q_OS_ANDROID - std::string command = enable ? "clearFlags" : "addFlags"; - - // Set security screen for Android app - AndroidUtils::runOnAndroidThreadSync([&command]() { - QJniObject activity = AndroidUtils::getActivity(); - QJniObject window = activity.callObjectMethod("getWindow", "()Landroid/view/Window;"); - if (window.isValid()) { - const int FLAG_SECURE = 8192; - window.callMethod(command.c_str(), "(I)V", FLAG_SECURE); - } - }); -#endif } bool SettingsController::isCameraPresent() diff --git a/client/ui/qml/Pages2/PageHome.qml b/client/ui/qml/Pages2/PageHome.qml index 06ce907ad..335d59a45 100644 --- a/client/ui/qml/Pages2/PageHome.qml +++ b/client/ui/qml/Pages2/PageHome.qml @@ -160,7 +160,6 @@ PageType { Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter text: ServersModel.defaultServerDescriptionCollapsed } - } expandedContent: Item { id: serverMenuContainer