Skip to content

Commit 18cca72

Browse files
tompsotayardexx
andauthored
feat: add ios screenshot blocking, externalID support (freeRASP 7.1.0) (#166)
* chore: fix Android NDK version in example * feat: add iOS screenshot blocking, externalID support * chore: remove unused condition * chore: remove unused import * fix: version enums causing compilation issues * fix: version enums causing compilation issues --------- Co-authored-by: Jaroslav Novotný <62177414+yardexx@users.noreply.github.com>
1 parent a6ce4a9 commit 18cca72

44 files changed

Lines changed: 1329 additions & 813 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,43 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [7.1.0] - 2025-05-19
9+
10+
- iOS SDK version: 6.11.0
11+
- Android SDK version: 15.1.0
12+
13+
### Flutter
14+
15+
#### Added
16+
17+
- Added interface for screenshot / screen recording blocking on iOS
18+
- Added interface for external ID storage
19+
20+
### Android
21+
22+
#### Added
23+
24+
- Added externalId to put an integrator-specified custom identifier into the logs.
25+
- Added eventId to the logs, which is unique per each log. It allows traceability of the same log across various systems.
26+
27+
#### Changed
28+
29+
- New root detection checks added
30+
31+
### iOS
32+
33+
#### Added
34+
35+
- Added externalId to put an integrator-specified custom identifier into the logs.
36+
- Added eventId to the logs, which is unique per each log. It allows traceability of the same log across various systems.
37+
- Screen capture protection obscuring app content in screenshots and screen recordings preventing unauthorized content capture. Refer to the freeRASP integration documentation.
38+
39+
#### Fixed
40+
41+
- Issue with the screen recording detection.
42+
- Issue that prevented Xcode tests from running correctly.
43+
- Issue that caused compilation errors due to unknown references.
44+
845
## [7.0.0] - 2024-03-26
946

1047
- iOS SDK version: 6.9.0

android/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ version '1.0-SNAPSHOT'
33

44
buildscript {
55
ext.kotlin_version = '2.1.0'
6-
ext.talsec_version = '15.0.0'
6+
ext.talsec_version = '15.1.0'
77
repositories {
88
google()
99
mavenCentral()

android/src/main/kotlin/com/aheaditec/freerasp/ScreenProtector.kt

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,11 @@ internal class ScreenProtector : DefaultLifecycleObserver {
5151
}
5252

5353
private fun register(activity: Activity) {
54-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
54+
if (Build.VERSION.SDK_INT >= 34) {
5555
registerScreenCapture(activity)
5656
}
5757

58-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
58+
if (Build.VERSION.SDK_INT >= 35) {
5959
registerScreenRecording(activity)
6060
}
6161
}
@@ -66,14 +66,14 @@ internal class ScreenProtector : DefaultLifecycleObserver {
6666
private fun unregister(currentActivity: Activity) {
6767
val context = currentActivity.applicationContext
6868

69-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && hasPermission(
69+
if (Build.VERSION.SDK_INT >= 34 && hasPermission(
7070
context, SCREEN_CAPTURE_PERMISSION
7171
)
7272
) {
7373
currentActivity.unregisterScreenCaptureCallback(screenCaptureCallback)
7474
}
7575

76-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM && hasPermission(
76+
if (Build.VERSION.SDK_INT >= 35 && hasPermission(
7777
context, SCREEN_RECORDING_PERMISSION
7878
)
7979
) {
@@ -84,7 +84,7 @@ internal class ScreenProtector : DefaultLifecycleObserver {
8484
// Missing permission is suppressed because the decision to use the screen capture API is made
8585
// by developer, and not enforced by the library.
8686
@SuppressLint("MissingPermission")
87-
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
87+
@RequiresApi(34)
8888
private fun registerScreenCapture(currentActivity: Activity) {
8989
val context = currentActivity.applicationContext
9090

@@ -99,7 +99,7 @@ internal class ScreenProtector : DefaultLifecycleObserver {
9999
// Missing permission is suppressed because the decision to use the screen capture API is made
100100
// by developer, and not enforced by the library.
101101
@SuppressLint("MissingPermission")
102-
@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
102+
@RequiresApi(35)
103103
private fun registerScreenRecording(currentActivity: Activity) {
104104
val context = currentActivity.applicationContext
105105

android/src/main/kotlin/com/aheaditec/freerasp/handlers/MethodCallHandler.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ internal class MethodCallHandler : MethodCallHandler, LifecycleEventObserver {
121121
"getAppIcon" -> getAppIcon(call, result)
122122
"blockScreenCapture" -> blockScreenCapture(call, result)
123123
"isScreenCaptureBlocked" -> isScreenCaptureBlocked(result)
124+
"storeExternalId" -> storeExternalId(call, result)
124125
else -> result.notImplemented()
125126
}
126127
}
@@ -199,4 +200,18 @@ internal class MethodCallHandler : MethodCallHandler, LifecycleEventObserver {
199200
result.success(Talsec.isScreenCaptureBlocked())
200201
}
201202
}
203+
204+
/**
205+
* Stores an external ID.
206+
*
207+
* @param call The method call containing the external ID.
208+
* @param result The result handler of the method call.
209+
*/
210+
private fun storeExternalId(call: MethodCall, result: MethodChannel.Result) {
211+
runResultCatching(result) {
212+
val data = call.argument<String>("data")
213+
Talsec.storeExternalId(context, data)
214+
result.success(null)
215+
}
216+
}
202217
}

example/android/app/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ if (flutterVersionName == null) {
2424

2525
android {
2626
namespace 'com.aheaditec.freerasp_example'
27-
2827
compileSdkVersion 35
28+
ndkVersion = "27.1.12297006"
2929

3030
compileOptions {
3131
sourceCompatibility JavaVersion.VERSION_17

example/ios/Podfile.lock

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ EXTERNAL SOURCES:
1515

1616
SPEC CHECKSUMS:
1717
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
18-
freerasp: d77275f774facb901f52e9608e5bd34768728363
18+
freerasp: bb827d80b926abcfb8f4ca4ff4557c2fe4a5ae21
1919

2020
PODFILE CHECKSUM: c4c93c5f6502fe2754f48404d3594bf779584011
2121

22-
COCOAPODS: 1.16.2
22+
COCOAPODS: 1.15.2

example/ios/Runner.xcodeproj/project.pbxproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,7 @@
362362
DEVELOPMENT_TEAM = PBDDS45LQS;
363363
ENABLE_BITCODE = NO;
364364
INFOPLIST_FILE = Runner/Info.plist;
365+
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
365366
LD_RUNPATH_SEARCH_PATHS = (
366367
"$(inherited)",
367368
"@executable_path/Frameworks",
@@ -493,6 +494,7 @@
493494
DEVELOPMENT_TEAM = PBDDS45LQS;
494495
ENABLE_BITCODE = NO;
495496
INFOPLIST_FILE = Runner/Info.plist;
497+
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
496498
LD_RUNPATH_SEARCH_PATHS = (
497499
"$(inherited)",
498500
"@executable_path/Frameworks",
@@ -516,6 +518,7 @@
516518
DEVELOPMENT_TEAM = PBDDS45LQS;
517519
ENABLE_BITCODE = NO;
518520
INFOPLIST_FILE = Runner/Info.plist;
521+
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
519522
LD_RUNPATH_SEARCH_PATHS = (
520523
"$(inherited)",
521524
"@executable_path/Frameworks",

example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
ignoresPersistentStateOnLaunch = "NO"
4949
debugDocumentVersioning = "YES"
5050
debugServiceExtension = "internal"
51+
enableGPUValidationMode = "1"
5152
allowLocationSimulation = "YES">
5253
<BuildableProductRunnable
5354
runnableDebuggingMode = "0">

example/lib/main.dart

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
// ignore_for_file: public_member_api_docs, avoid_redundant_argument_values
22

3-
import 'dart:io';
4-
53
import 'package:flutter/material.dart';
64
import 'package:flutter_riverpod/flutter_riverpod.dart';
75
import 'package:freerasp/freerasp.dart';
@@ -56,6 +54,11 @@ Future<void> _initializeTalsec() async {
5654
await Talsec.instance.start(config);
5755
}
5856

57+
/// Example of how to use [Talsec.storeExternalId].
58+
Future<void> testStoreExternalId(String data) async {
59+
await Talsec.instance.storeExternalId(data);
60+
}
61+
5962
/// The root widget of the application
6063
class App extends StatelessWidget {
6164
const App({super.key});
@@ -97,20 +100,27 @@ class HomePage extends ConsumerWidget {
97100
style: Theme.of(context).textTheme.titleMedium,
98101
),
99102
const SizedBox(height: 8),
100-
if (Platform.isAndroid)
101-
ListTile(
102-
title: const Text('Change Screen Capture'),
103-
leading: SafetyIcon(
104-
isDetected:
105-
!(ref.watch(screenCaptureProvider).value ?? true),
106-
),
107-
trailing: IconButton(
108-
icon: const Icon(Icons.refresh),
109-
onPressed: () {
110-
ref.read(screenCaptureProvider.notifier).toggle();
111-
},
112-
),
103+
ListTile(
104+
title: const Text('Store External ID'),
105+
trailing: IconButton(
106+
icon: const Icon(Icons.refresh),
107+
onPressed: () {
108+
testStoreExternalId('testData');
109+
},
113110
),
111+
),
112+
ListTile(
113+
title: const Text('Change Screen Capture'),
114+
leading: SafetyIcon(
115+
isDetected: !(ref.watch(screenCaptureProvider).value ?? true),
116+
),
117+
trailing: IconButton(
118+
icon: const Icon(Icons.refresh),
119+
onPressed: () {
120+
ref.read(screenCaptureProvider.notifier).toggle();
121+
},
122+
),
123+
),
114124
Expanded(
115125
child: ThreatListView(threats: threatState.detectedThreats),
116126
),

ios/Classes/SwiftFreeraspPlugin.swift

Lines changed: 76 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,20 @@ public class SwiftFreeraspPlugin: NSObject, FlutterPlugin, FlutterStreamHandler
3030
/// - call: The `FlutterMethodCall` object representing the method call.
3131
/// - result: The `FlutterResult` object to be returned to the caller.
3232
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
33-
guard let args = call.arguments as? Dictionary<String, String>
34-
else {
35-
result(FlutterError(code: "talsec-failure", message: "Unexpected arguments", details: nil))
36-
return
37-
}
33+
let args = call.arguments as? Dictionary<String, Any> ?? [:]
3834

3935
switch call.method {
4036
case "start":
41-
start(args: args, result: result)
37+
start(configJson: args["config"] as? String, result: result)
38+
return
39+
case "blockScreenCapture":
40+
blockScreenCapture(enable: args["enable"] as? Bool, result: result)
41+
return
42+
case "isScreenCaptureBlocked":
43+
isScreenCaptureBlocked(result: result)
44+
return
45+
case "storeExternalId":
46+
storeExternalId(data: args["data"] as? String, result: result)
4247
return
4348
default:
4449
result(FlutterMethodNotImplemented)
@@ -50,9 +55,8 @@ public class SwiftFreeraspPlugin: NSObject, FlutterPlugin, FlutterStreamHandler
5055
/// - Parameters:
5156
/// - args: The arguments received from Flutter which contains configuration
5257
/// - result: The `FlutterResult` object to be returned to the caller.
53-
private func start(args: Dictionary<String, String>, result: @escaping FlutterResult) {
54-
guard let json = args["config"],
55-
let data = json.data(using: .utf8),
58+
private func start(configJson: String?, result: @escaping FlutterResult) {
59+
guard let data = configJson?.data(using: .utf8),
5660
let flutterConfig = try? JSONDecoder().decode(FlutterTalsecConfig.self, from: data)
5761
else {
5862
result(FlutterError(code: "configuration-exception", message: "Unable to decode configuration", details: nil))
@@ -65,6 +69,69 @@ public class SwiftFreeraspPlugin: NSObject, FlutterPlugin, FlutterStreamHandler
6569
result(nil)
6670
}
6771

72+
/// Blocks screen capture for the current UIWindow.
73+
///
74+
/// - Parameters:
75+
/// - enable: Whether screen capture should be enabled / disabled.
76+
/// - result: The `FlutterResult` object to be returned to the caller.
77+
private func blockScreenCapture(enable: Bool?, result: @escaping FlutterResult){
78+
guard let enableSafe = enable else {
79+
result(FlutterError(code: "block-screen-capture-failure", message: "Couldn't process data.", details: nil))
80+
return
81+
}
82+
83+
getProtectedWindow { window in
84+
if let window = window {
85+
Talsec.blockScreenCapture(enable: enableSafe, window: window)
86+
result(nil)
87+
} else {
88+
result(FlutterError(code: "block-screen-capture-failure", message: "No windows found to block screen capture", details: nil))
89+
}
90+
}
91+
}
92+
93+
/// Determines whether screen capture is blocked for the current UIWindow.
94+
///
95+
/// - Parameters:
96+
/// - nonce: The nonce to be used in the cryptogram calculation.
97+
/// - result: The `FlutterResult` object to be returned to the caller.
98+
private func isScreenCaptureBlocked(result: @escaping FlutterResult){
99+
getProtectedWindow { window in
100+
if let window = window {
101+
let isBlocked = Talsec.isScreenCaptureBlocked(in: window)
102+
result(isBlocked)
103+
} else {
104+
result(FlutterError(code: "is-screen-capture-blocked-failure", message: "Error while checking if screen capture is blocked", details: nil))
105+
}
106+
}
107+
}
108+
109+
private func getProtectedWindow(completion: @escaping (UIWindow?) -> Void) {
110+
DispatchQueue.main.async {
111+
if #available(iOS 13.0, *) {
112+
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
113+
if let window = windowScene.windows.first {
114+
completion(window)
115+
} else {
116+
completion(nil)
117+
}
118+
} else {
119+
completion(nil)
120+
}
121+
}
122+
}
123+
}
124+
125+
/// Stores the external ID in user defaults.
126+
///
127+
/// - Parameters:
128+
/// - data: The data to be stored.
129+
/// - result: The `FlutterResult` object to be returned to the caller.
130+
private func storeExternalId(data: String?, result: @escaping FlutterResult){
131+
UserDefaults.standard.set(data, forKey: "app.talsec.externalid")
132+
result(nil)
133+
}
134+
68135
/// Attaches a FlutterEventSink to the EventProcessor and processes any detectedThreats in the queue.
69136
///
70137
/// - Parameters:

0 commit comments

Comments
 (0)