Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,43 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [7.1.0] - 2025-05-19

- iOS SDK version: 6.11.0
- Android SDK version: 15.1.0

### Flutter

#### Added

- Added interface for screenshot / screen recording blocking on iOS
- Added interface for external ID storage

### Android

#### Added

- Added externalId to put an integrator-specified custom identifier into the logs.
- Added eventId to the logs, which is unique per each log. It allows traceability of the same log across various systems.

#### Changed

- New root detection checks added

### iOS

#### Added

- Added externalId to put an integrator-specified custom identifier into the logs.
- Added eventId to the logs, which is unique per each log. It allows traceability of the same log across various systems.
- Screen capture protection obscuring app content in screenshots and screen recordings preventing unauthorized content capture. Refer to the freeRASP integration documentation.

#### Fixed

- Issue with the screen recording detection.
- Issue that prevented Xcode tests from running correctly.
- Issue that caused compilation errors due to unknown references.

## [7.0.0] - 2024-03-26

- iOS SDK version: 6.9.0
Expand Down
2 changes: 1 addition & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ version '1.0-SNAPSHOT'

buildscript {
ext.kotlin_version = '2.1.0'
ext.talsec_version = '15.0.0'
ext.talsec_version = '15.1.0'
repositories {
google()
mavenCentral()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,11 @@ internal class ScreenProtector : DefaultLifecycleObserver {
}

private fun register(activity: Activity) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
if (Build.VERSION.SDK_INT >= 34) {
registerScreenCapture(activity)
}

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
if (Build.VERSION.SDK_INT >= 35) {
registerScreenRecording(activity)
}
}
Expand All @@ -66,14 +66,14 @@ internal class ScreenProtector : DefaultLifecycleObserver {
private fun unregister(currentActivity: Activity) {
val context = currentActivity.applicationContext

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && hasPermission(
if (Build.VERSION.SDK_INT >= 34 && hasPermission(
context, SCREEN_CAPTURE_PERMISSION
)
) {
currentActivity.unregisterScreenCaptureCallback(screenCaptureCallback)
}

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

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ internal class MethodCallHandler : MethodCallHandler, LifecycleEventObserver {
"getAppIcon" -> getAppIcon(call, result)
"blockScreenCapture" -> blockScreenCapture(call, result)
"isScreenCaptureBlocked" -> isScreenCaptureBlocked(result)
"storeExternalId" -> storeExternalId(call, result)
else -> result.notImplemented()
}
}
Expand Down Expand Up @@ -199,4 +200,18 @@ internal class MethodCallHandler : MethodCallHandler, LifecycleEventObserver {
result.success(Talsec.isScreenCaptureBlocked())
}
}

/**
* Stores an external ID.
*
* @param call The method call containing the external ID.
* @param result The result handler of the method call.
*/
private fun storeExternalId(call: MethodCall, result: MethodChannel.Result) {
runResultCatching(result) {
val data = call.argument<String>("data")
Talsec.storeExternalId(context, data)
result.success(null)
}
}
}
2 changes: 1 addition & 1 deletion example/android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ if (flutterVersionName == null) {

android {
namespace 'com.aheaditec.freerasp_example'

compileSdkVersion 35
ndkVersion = "27.1.12297006"

compileOptions {
sourceCompatibility JavaVersion.VERSION_17
Expand Down
4 changes: 2 additions & 2 deletions example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ EXTERNAL SOURCES:

SPEC CHECKSUMS:
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
freerasp: d77275f774facb901f52e9608e5bd34768728363
freerasp: bb827d80b926abcfb8f4ca4ff4557c2fe4a5ae21

PODFILE CHECKSUM: c4c93c5f6502fe2754f48404d3594bf779584011

COCOAPODS: 1.16.2
COCOAPODS: 1.15.2
3 changes: 3 additions & 0 deletions example/ios/Runner.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,7 @@
DEVELOPMENT_TEAM = PBDDS45LQS;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
Expand Down Expand Up @@ -493,6 +494,7 @@
DEVELOPMENT_TEAM = PBDDS45LQS;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
Expand All @@ -516,6 +518,7 @@
DEVELOPMENT_TEAM = PBDDS45LQS;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
Expand Down
40 changes: 25 additions & 15 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
// ignore_for_file: public_member_api_docs, avoid_redundant_argument_values

import 'dart:io';

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:freerasp/freerasp.dart';
Expand Down Expand Up @@ -56,6 +54,11 @@ Future<void> _initializeTalsec() async {
await Talsec.instance.start(config);
}

/// Example of how to use [Talsec.storeExternalId].
Future<void> testStoreExternalId(String data) async {
await Talsec.instance.storeExternalId(data);
}

/// The root widget of the application
class App extends StatelessWidget {
const App({super.key});
Expand Down Expand Up @@ -97,20 +100,27 @@ class HomePage extends ConsumerWidget {
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
if (Platform.isAndroid)
ListTile(
title: const Text('Change Screen Capture'),
leading: SafetyIcon(
isDetected:
!(ref.watch(screenCaptureProvider).value ?? true),
),
trailing: IconButton(
icon: const Icon(Icons.refresh),
onPressed: () {
ref.read(screenCaptureProvider.notifier).toggle();
},
),
ListTile(
title: const Text('Store External ID'),
trailing: IconButton(
icon: const Icon(Icons.refresh),
onPressed: () {
testStoreExternalId('testData');
},
),
),
ListTile(
title: const Text('Change Screen Capture'),
leading: SafetyIcon(
isDetected: !(ref.watch(screenCaptureProvider).value ?? true),
),
trailing: IconButton(
icon: const Icon(Icons.refresh),
onPressed: () {
ref.read(screenCaptureProvider.notifier).toggle();
},
),
),
Expanded(
child: ThreatListView(threats: threatState.detectedThreats),
),
Expand Down
85 changes: 76 additions & 9 deletions ios/Classes/SwiftFreeraspPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,20 @@ public class SwiftFreeraspPlugin: NSObject, FlutterPlugin, FlutterStreamHandler
/// - call: The `FlutterMethodCall` object representing the method call.
/// - result: The `FlutterResult` object to be returned to the caller.
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
guard let args = call.arguments as? Dictionary<String, String>
else {
result(FlutterError(code: "talsec-failure", message: "Unexpected arguments", details: nil))
return
}
let args = call.arguments as? Dictionary<String, Any> ?? [:]

switch call.method {
case "start":
start(args: args, result: result)
start(configJson: args["config"] as? String, result: result)
return
case "blockScreenCapture":
blockScreenCapture(enable: args["enable"] as? Bool, result: result)
return
case "isScreenCaptureBlocked":
isScreenCaptureBlocked(result: result)
return
case "storeExternalId":
storeExternalId(data: args["data"] as? String, result: result)
return
default:
result(FlutterMethodNotImplemented)
Expand All @@ -50,9 +55,8 @@ public class SwiftFreeraspPlugin: NSObject, FlutterPlugin, FlutterStreamHandler
/// - Parameters:
/// - args: The arguments received from Flutter which contains configuration
/// - result: The `FlutterResult` object to be returned to the caller.
private func start(args: Dictionary<String, String>, result: @escaping FlutterResult) {
guard let json = args["config"],
let data = json.data(using: .utf8),
private func start(configJson: String?, result: @escaping FlutterResult) {
guard let data = configJson?.data(using: .utf8),
let flutterConfig = try? JSONDecoder().decode(FlutterTalsecConfig.self, from: data)
else {
result(FlutterError(code: "configuration-exception", message: "Unable to decode configuration", details: nil))
Expand All @@ -65,6 +69,69 @@ public class SwiftFreeraspPlugin: NSObject, FlutterPlugin, FlutterStreamHandler
result(nil)
}

/// Blocks screen capture for the current UIWindow.
///
/// - Parameters:
/// - enable: Whether screen capture should be enabled / disabled.
/// - result: The `FlutterResult` object to be returned to the caller.
private func blockScreenCapture(enable: Bool?, result: @escaping FlutterResult){
guard let enableSafe = enable else {
result(FlutterError(code: "block-screen-capture-failure", message: "Couldn't process data.", details: nil))
return
}

getProtectedWindow { window in
if let window = window {
Talsec.blockScreenCapture(enable: enableSafe, window: window)
result(nil)
} else {
result(FlutterError(code: "block-screen-capture-failure", message: "No windows found to block screen capture", details: nil))
}
}
}

/// Determines whether screen capture is blocked for the current UIWindow.
///
/// - Parameters:
/// - nonce: The nonce to be used in the cryptogram calculation.
/// - result: The `FlutterResult` object to be returned to the caller.
private func isScreenCaptureBlocked(result: @escaping FlutterResult){
getProtectedWindow { window in
if let window = window {
let isBlocked = Talsec.isScreenCaptureBlocked(in: window)
result(isBlocked)
} else {
result(FlutterError(code: "is-screen-capture-blocked-failure", message: "Error while checking if screen capture is blocked", details: nil))
}
}
}

private func getProtectedWindow(completion: @escaping (UIWindow?) -> Void) {
DispatchQueue.main.async {
if #available(iOS 13.0, *) {
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
if let window = windowScene.windows.first {
completion(window)
} else {
completion(nil)
}
} else {
completion(nil)
}
}
}
}

/// Stores the external ID in user defaults.
///
/// - Parameters:
/// - data: The data to be stored.
/// - result: The `FlutterResult` object to be returned to the caller.
private func storeExternalId(data: String?, result: @escaping FlutterResult){
UserDefaults.standard.set(data, forKey: "app.talsec.externalid")
result(nil)
}

/// Attaches a FlutterEventSink to the EventProcessor and processes any detectedThreats in the queue.
///
/// - Parameters:
Expand Down
10 changes: 5 additions & 5 deletions ios/TalsecRuntime.xcframework/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -8,32 +8,32 @@
<key>BinaryPath</key>
<string>TalsecRuntime.framework/TalsecRuntime</string>
<key>LibraryIdentifier</key>
<string>ios-arm64</string>
<string>ios-arm64_x86_64-simulator</string>
<key>LibraryPath</key>
<string>TalsecRuntime.framework</string>
<key>SupportedArchitectures</key>
<array>
<string>arm64</string>
<string>x86_64</string>
</array>
<key>SupportedPlatform</key>
<string>ios</string>
<key>SupportedPlatformVariant</key>
<string>simulator</string>
</dict>
<dict>
<key>BinaryPath</key>
<string>TalsecRuntime.framework/TalsecRuntime</string>
<key>LibraryIdentifier</key>
<string>ios-arm64_x86_64-simulator</string>
<string>ios-arm64</string>
<key>LibraryPath</key>
<string>TalsecRuntime.framework</string>
<key>SupportedArchitectures</key>
<array>
<string>arm64</string>
<string>x86_64</string>
</array>
<key>SupportedPlatform</key>
<string>ios</string>
<key>SupportedPlatformVariant</key>
<string>simulator</string>
</dict>
</array>
<key>CFBundlePackageType</key>
Expand Down
Binary file modified ios/TalsecRuntime.xcframework/_CodeSignature/CodeDirectory
Binary file not shown.
Binary file modified ios/TalsecRuntime.xcframework/_CodeSignature/CodeRequirements-1
Binary file not shown.
Loading