From 6e7d18a63ac927298e6363c0b116b62d7720ab8e Mon Sep 17 00:00:00 2001 From: Tomas Psota Date: Tue, 10 Nov 2026 15:20:11 +0100 Subject: [PATCH 01/11] release: freeRASP 8.4.0 (Android SDK 20.0.1) Bump the Android SDK to 20.0.1 (app.talsec.rasp:TalsecRasp-Community-Flutter) and expose its new features: - ongoing call detection: ThreatCallback.onOngoingCall with CallState, Talsec.startOngoingCallDetection/stopOngoingCallDetection - screen control blocking: blockScreenControl/isScreenControlBlocked - overlay windows hiding: blockOverlayWindows/isOverlayWindowsBlocked - onCriticalChecksFinished/onPostponedChecksFinished execution states onAllChecksFinished is now driven by the SDK's onFastChecksFinished, which fires where the removed onAllChecksFinished used to. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 58 +++++++- README.md | 2 + android/build.gradle | 2 +- .../com/aheaditec/freerasp/Extensions.kt | 14 ++ .../freerasp/RaspExecutionStateEvent.kt | 4 + .../freerasp/dispatchers/ThreatDispatcher.kt | 24 ++++ .../freerasp/generated/TalsecPigeonApi.kt | 49 ++++++- .../freerasp/handlers/MethodCallHandler.kt | 130 +++++++++++++++++- .../freerasp/handlers/PluginThreatHandler.kt | 17 ++- .../android/app/src/main/AndroidManifest.xml | 3 + example/lib/main.dart | 32 +++++ example/lib/threat_notifier.dart | 15 +- example/lib/threat_state.dart | 11 ++ example/lib/widgets/protection_toggle.dart | 57 ++++++++ example/lib/widgets/widgets.dart | 1 + lib/freerasp.dart | 2 +- .../rasp_execution_state_callback.dart | 20 ++- lib/src/callbacks/threat_callback.dart | 16 +++ lib/src/enums/rasp_execution_state.dart | 21 ++- lib/src/generated/talsec_pigeon_api.g.dart | 55 +++++++- lib/src/talsec.dart | 116 ++++++++++++++++ lib/src/typedefs.dart | 3 + pigeons/talsec_pigeon_api.dart | 15 +- pubspec.yaml | 2 +- test/src/talsec_test.dart | 82 +++++++++++ test/src/threat_callback_test.dart | 14 ++ 26 files changed, 747 insertions(+), 18 deletions(-) create mode 100644 example/lib/widgets/protection_toggle.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index b33bfdb..776d42b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,63 @@ 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). -## [Unreleased] +## [8.4.0] - 2026-11-10 + +- Android SDK version: 20.0.1 +- iOS SDK version: 8.0.1 + +### Flutter + +#### Added + +- `ThreatCallback.onOngoingCall` callback with the new `CallState` enum (`started`, `ongoing`, `stopped`), driven by `Talsec.startOngoingCallDetection()` and `Talsec.stopOngoingCallDetection()` (Android only) +- `Talsec.blockScreenControl()` and `Talsec.isScreenControlBlocked()` to prevent accessibility services from reading or controlling the app screen (Android only) +- `Talsec.blockOverlayWindows()` and `Talsec.isOverlayWindowsBlocked()` to hide overlay windows of other apps (Android 12+ only, requires the `HIDE_OVERLAY_WINDOWS` permission) +- `RaspExecutionStateCallback.onCriticalChecksFinished` and `RaspExecutionStateCallback.onPostponedChecksFinished`, with matching `RaspExecutionState` values (Android only) + +#### Changed + +- On Android, `onAllChecksFinished` is now triggered when the fast checks are completed, where it was triggered before; the postponed checks may still run afterwards +- On Android, `Talsec.isScreenCaptureBlocked()` reports the protection state of the current activity + +### Android + +#### Breaking Changes + +- ❗️Breaking: `ThreatListener.RaspExecutionState.onAllChecksFinished()` was removed and replaced by two callbacks: + - `onFastChecksFinished()` is triggered after the fast standard checks are completed (where `onAllChecksFinished()` used to be triggered) + - `onPostponedChecksFinished()` is triggered after the postponed/deep checks are completed +- ❗️Breaking: Modified method `Talsec.isScreenCaptureBlocked(activity: Activity): Boolean` which now takes activity as parameter, and returns if the given activity view is protected against screen capture +- ❗️Breaking: Renamed Maven coordinates from `com.aheaditec.talsec.security:TalsecSecurity-{flavor}` + +#### Added + +- Added `isScreenControlBlocked(activity: Activity): Boolean` to check if the screen control protection is set for given activity +- Added `blockScreenControl(activity: Activity, enable: Boolean)` to prevent accessibility services from reading or controlling an activity's screen +- Added `blockOverlayWindows(activity: Activity, enable: Boolean): Boolean` to hide overlay windows of other apps over the given activity (Android 12+, requires `HIDE_OVERLAY_WINDOWS` permission) +- Added `isOverlayWindowsBlocked(activity: Activity): Boolean` to check if overlay windows are hidden for given activity +- Added a new `Ongoing Call` detection engine (VoIP, Cellular) + - started/paused per activity with `startOngoingCallDetection` / `stopOngoingCallDetection` + - new callback `onOngoingCall(CallState)` in `ThreatListener.ThreatDetected` + - new `CallState` enum with 3 call states (`STARTED`) +- Added callback `onCriticalChecksFinished()` in `ThreatListener.RaspExecutionState`, which is triggered after critical checks are completed + +#### Fixed + +- Improved hook detection to prevent collisions with running VPN services and to better work with the device resources (threads/RAM) +- Fixed a release crash during root detection when a checked package is not installed +- Fixed a crash in `ScreenProtector.unregisterScreenCallbacks` on Android 15+ when several activities registered screen callbacks +- Fixed intermittent build failures of apps using the SDK (`NullPointerException: Cannot read field "d"`, shown by Gradle as `[CIRCULAR REFERENCE]`) caused by invalid bytecode in some SDK builds +- Fixed `killOnBypass` closing the app when the same threat was reported twice in quick succession + +#### Changed + +- Improved Bootloader detection +- Extended emulator detection with additional device-property and ABI checks +- Improved logging structure and data enrichment for better Portal insights/reports +- Refactoring of the time spoofing detection which improves the performance of the freeRASP check execution +- Improved the performance of the App integrity check +- Updated CA bundle ### iOS diff --git a/README.md b/README.md index 53dd665..a461611 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,8 @@ Key features are the detection and prevention of * Unlocked or compromised bootloader * Hooking framework (e.g., Frida, LSPosed, Shadow) * UI automation and driven-input attacks (e.g., Appium) +* Ongoing cellular and VoIP calls during sensitive flows (Android) +* Screen control by accessibility services and overlay windows of other apps (Android) * Untrusted installation method * App Data Migration protection diff --git a/android/build.gradle b/android/build.gradle index dea2813..2c48362 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -3,7 +3,7 @@ version '1.0-SNAPSHOT' buildscript { ext.kotlin_version = '2.1.0' - ext.talsec_version = '19.3.1' + ext.talsec_version = '20.0.1' repositories { google() mavenCentral() diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/Extensions.kt b/android/src/main/kotlin/com/aheaditec/freerasp/Extensions.kt index 0529694..7ede6ee 100644 --- a/android/src/main/kotlin/com/aheaditec/freerasp/Extensions.kt +++ b/android/src/main/kotlin/com/aheaditec/freerasp/Extensions.kt @@ -12,6 +12,8 @@ import app.talsec.rasp.security.api.SuspiciousAppInfo import io.flutter.plugin.common.MethodChannel import org.json.JSONArray import org.json.JSONObject +import app.talsec.rasp.security.api.ThreatListener.CallState +import com.aheaditec.freerasp.generated.CallState as FlutterCallState import com.aheaditec.freerasp.generated.PackageInfo as FlutterPackageInfo import com.aheaditec.freerasp.generated.SuspiciousAppInfo as FlutterSuspiciousAppInfo @@ -117,3 +119,15 @@ internal fun JSONObject.toSuspiciousAppDetectionConfig(): SuspiciousAppDetection reasonMode, ) } + +/** + * Converts a [CallState] instance to a [com.aheaditec.freerasp.generated.CallState] instance + * used by Pigeon package for Flutter. + */ +internal fun CallState.toPigeon(): FlutterCallState { + return when (this) { + CallState.STARTED -> FlutterCallState.STARTED + CallState.ONGOING -> FlutterCallState.ONGOING + CallState.STOPPED -> FlutterCallState.STOPPED + } +} diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/RaspExecutionStateEvent.kt b/android/src/main/kotlin/com/aheaditec/freerasp/RaspExecutionStateEvent.kt index 998e75c..21659c4 100644 --- a/android/src/main/kotlin/com/aheaditec/freerasp/RaspExecutionStateEvent.kt +++ b/android/src/main/kotlin/com/aheaditec/freerasp/RaspExecutionStateEvent.kt @@ -2,4 +2,8 @@ package com.aheaditec.freerasp internal sealed class RaspExecutionStateEvent(val value: Int) { object AllChecksFinished : RaspExecutionStateEvent(187429) + + object CriticalChecksFinished : RaspExecutionStateEvent(614702) + + object PostponedChecksFinished : RaspExecutionStateEvent(952318) } diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/dispatchers/ThreatDispatcher.kt b/android/src/main/kotlin/com/aheaditec/freerasp/dispatchers/ThreatDispatcher.kt index 9afd875..1db9e06 100644 --- a/android/src/main/kotlin/com/aheaditec/freerasp/dispatchers/ThreatDispatcher.kt +++ b/android/src/main/kotlin/com/aheaditec/freerasp/dispatchers/ThreatDispatcher.kt @@ -3,11 +3,13 @@ package com.aheaditec.freerasp.dispatchers import com.aheaditec.freerasp.Threat import com.aheaditec.freerasp.handlers.MethodCallHandler import app.talsec.rasp.security.api.SuspiciousAppInfo +import app.talsec.rasp.security.api.ThreatListener.CallState import io.flutter.plugin.common.EventChannel.EventSink internal object ThreatDispatcher { private val threatCache = mutableSetOf() private val malwareCache = mutableListOf() + private val callStateCache = mutableListOf() private var isAppInForeground = false var eventSink: EventSink? = null @@ -25,6 +27,7 @@ internal object ThreatDispatcher { if (value != null) { isAppInForeground = true flushMalwareCache() + flushCallStateCache() } } @@ -35,6 +38,7 @@ internal object ThreatDispatcher { } if (methodSink != null) { flushMalwareCache() + flushCallStateCache() } } @@ -62,6 +66,17 @@ internal object ThreatDispatcher { } } + fun dispatchOngoingCall(state: CallState) { + if (isAppInForeground && methodSink != null) { + methodSink?.onOngoingCall(state) + } else { + // Keep the order: a call that started and stopped in background is reported as both. + synchronized(callStateCache) { + callStateCache.add(state) + } + } + } + private fun flushThreatCache() { val threats = synchronized(threatCache) { val snapshot = threatCache.toSet() @@ -81,4 +96,13 @@ internal object ThreatDispatcher { methodSink?.onMalwareDetected(malware) } } + + private fun flushCallStateCache() { + val states = synchronized(callStateCache) { + val snapshot = callStateCache.toList() + callStateCache.clear() + snapshot + } + states.forEach { methodSink?.onOngoingCall(it) } + } } diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/generated/TalsecPigeonApi.kt b/android/src/main/kotlin/com/aheaditec/freerasp/generated/TalsecPigeonApi.kt index 97e8a59..f81bd94 100644 --- a/android/src/main/kotlin/com/aheaditec/freerasp/generated/TalsecPigeonApi.kt +++ b/android/src/main/kotlin/com/aheaditec/freerasp/generated/TalsecPigeonApi.kt @@ -29,6 +29,22 @@ class FlutterError ( val details: Any? = null ) : Throwable() +/** State of an ongoing call reported by the ongoing call detection. */ +enum class CallState(val raw: Int) { + /** A call started while the detection was running. */ + STARTED(0), + /** A call was already in progress when the detection started. */ + ONGOING(1), + /** A previously reported call has ended. */ + STOPPED(2); + + companion object { + fun ofRaw(raw: Int): CallState? { + return values().firstOrNull { it.raw == raw } + } + } +} + /** Generated class from Pigeon that represents data sent in messages. */ data class PackageInfo ( val packageName: String, @@ -70,7 +86,6 @@ data class SuspiciousAppInfo ( fun fromList(pigeonVar_list: List): SuspiciousAppInfo { val packageInfo = pigeonVar_list[0] as PackageInfo val reasons = pigeonVar_list[1] as List - @Suppress("UNCHECKED_CAST") val permissions = pigeonVar_list[2] as List? return SuspiciousAppInfo(packageInfo, reasons, permissions) } @@ -87,11 +102,16 @@ private open class TalsecPigeonApiPigeonCodec : StandardMessageCodec() { override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { return when (type) { 129.toByte() -> { + return (readValue(buffer) as Long?)?.let { + CallState.ofRaw(it.toInt()) + } + } + 130.toByte() -> { return (readValue(buffer) as? List)?.let { PackageInfo.fromList(it) } } - 130.toByte() -> { + 131.toByte() -> { return (readValue(buffer) as? List)?.let { SuspiciousAppInfo.fromList(it) } @@ -101,12 +121,16 @@ private open class TalsecPigeonApiPigeonCodec : StandardMessageCodec() { } override fun writeValue(stream: ByteArrayOutputStream, value: Any?) { when (value) { - is PackageInfo -> { + is CallState -> { stream.write(129) + writeValue(stream, value.raw) + } + is PackageInfo -> { + stream.write(130) writeValue(stream, value.toList()) } is SuspiciousAppInfo -> { - stream.write(130) + stream.write(131) writeValue(stream, value.toList()) } else -> super.writeValue(stream, value) @@ -139,4 +163,21 @@ class TalsecPigeonApi(private val binaryMessenger: BinaryMessenger, private val } } } + fun onOngoingCallDetected(stateArg: CallState, callback: (Result) -> Unit) +{ + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + val channelName = "dev.flutter.pigeon.freerasp.TalsecPigeonApi.onOngoingCallDetected$separatedMessageChannelSuffix" + val channel = BasicMessageChannel(binaryMessenger, channelName, codec) + channel.send(listOf(stateArg)) { + if (it is List<*>) { + if (it.size > 1) { + callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) + } else { + callback(Result.success(Unit)) + } + } else { + callback(Result.failure(createConnectionError(channelName))) + } + } + } } diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/handlers/MethodCallHandler.kt b/android/src/main/kotlin/com/aheaditec/freerasp/handlers/MethodCallHandler.kt index 9c98c11..1f2d6a3 100644 --- a/android/src/main/kotlin/com/aheaditec/freerasp/handlers/MethodCallHandler.kt +++ b/android/src/main/kotlin/com/aheaditec/freerasp/handlers/MethodCallHandler.kt @@ -1,7 +1,9 @@ package com.aheaditec.freerasp.handlers +import android.annotation.SuppressLint import android.app.Activity import android.content.Context +import android.os.Build import android.os.Handler import android.os.HandlerThread import android.os.Looper @@ -15,6 +17,7 @@ import com.aheaditec.freerasp.runResultCatching import com.aheaditec.freerasp.toPigeon import app.talsec.rasp.security.api.SuspiciousAppInfo import app.talsec.rasp.security.api.Talsec +import app.talsec.rasp.security.api.ThreatListener.CallState import io.flutter.Log import io.flutter.plugin.common.BinaryMessenger import io.flutter.plugin.common.MethodCall @@ -32,7 +35,22 @@ internal class MethodCallHandler : MethodCallHandler, LifecycleEventObserver { private val backgroundHandler = Handler(backgroundHandlerThread.looper) private val mainHandler = Handler(Looper.getMainLooper()) + private var isOngoingCallDetectionEnabled = false + + /** + * The current activity. Ongoing call detection is registered per activity, so it is moved + * along when the activity is replaced (e.g. on configuration changes) and stopped when the + * activity is detached. + */ internal var activity: Activity? = null + set(value) { + if (field === value) return + if (isOngoingCallDetectionEnabled) { + field?.let { Talsec.stopOngoingCallDetection(it) } + value?.let { Talsec.startOngoingCallDetection(it) } + } + field = value + } companion object { private const val CHANNEL_NAME: String = "talsec.app/freerasp/methods" @@ -52,10 +70,21 @@ internal class MethodCallHandler : MethodCallHandler, LifecycleEventObserver { } } } + + override fun onOngoingCall(state: CallState) { + talsecPigeon?.onOngoingCallDetected(state.toPigeon()) { result -> + result.getOrElse { + Log.e("MethodCallHandlerSink", "Result ended with failure") + throw it + } + } + } } internal interface MethodSink { fun onMalwareDetected(packageInfo: List) + + fun onOngoingCall(state: CallState) } /** @@ -122,6 +151,12 @@ internal class MethodCallHandler : MethodCallHandler, LifecycleEventObserver { "getAppIcon" -> getAppIcon(call, result) "blockScreenCapture" -> blockScreenCapture(call, result) "isScreenCaptureBlocked" -> isScreenCaptureBlocked(result) + "blockScreenControl" -> blockScreenControl(call, result) + "isScreenControlBlocked" -> isScreenControlBlocked(result) + "blockOverlayWindows" -> blockOverlayWindows(call, result) + "isOverlayWindowsBlocked" -> isOverlayWindowsBlocked(result) + "startOngoingCallDetection" -> startOngoingCallDetection(result) + "stopOngoingCallDetection" -> stopOngoingCallDetection(result) "storeExternalId" -> storeExternalId(call, result) "removeExternalId" -> removeExternalId(result) else -> result.notImplemented() @@ -203,10 +238,103 @@ internal class MethodCallHandler : MethodCallHandler, LifecycleEventObserver { */ private fun isScreenCaptureBlocked(result: MethodChannel.Result) { runResultCatching(result) { - result.success(Talsec.isScreenCaptureBlocked()) + result.success(Talsec.isScreenCaptureBlocked(requireActivity("check screen capture state"))) + } + } + + /** + * Blocks or unblocks accessibility services from reading or controlling the screen. + * + * @param call The method call containing the enable flag. + * @param result The result handler of the method call. + */ + private fun blockScreenControl(call: MethodCall, result: MethodChannel.Result) { + runResultCatching(result) { + val enable = call.argument("enable") ?: throw NullPointerException("Enable flag cannot be null.") + Talsec.blockScreenControl(requireActivity("block screen control"), enable) + result.success(null) + } + } + + /** + * Checks if screen control is blocked. + * + * @param result The result handler of the method call. + */ + private fun isScreenControlBlocked(result: MethodChannel.Result) { + runResultCatching(result) { + result.success(Talsec.isScreenControlBlocked(requireActivity("check screen control state"))) } } + /** + * Hides or shows overlay windows of other apps over the activity. Requires Android 12+ and the + * HIDE_OVERLAY_WINDOWS permission, otherwise `false` is returned. + * + * @param call The method call containing the enable flag. + * @param result The result handler of the method call. + */ + // Missing permission is suppressed because the decision to use the overlay API is made + // by developer, and not enforced by the library. + @SuppressLint("MissingPermission") + private fun blockOverlayWindows(call: MethodCall, result: MethodChannel.Result) { + runResultCatching(result) { + val enable = call.argument("enable") ?: throw NullPointerException("Enable flag cannot be null.") + val activity = requireActivity("block overlay windows") + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { + result.success(false) + return@runResultCatching + } + result.success(Talsec.blockOverlayWindows(activity, enable)) + } + } + + /** + * Checks if overlay windows of other apps are hidden over the activity. + * + * @param result The result handler of the method call. + */ + private fun isOverlayWindowsBlocked(result: MethodChannel.Result) { + runResultCatching(result) { + result.success(Talsec.isOverlayWindowsBlocked(requireActivity("check overlay windows state"))) + } + } + + /** + * Starts the ongoing call detection for the current activity. + * + * @param result The result handler of the method call. + */ + private fun startOngoingCallDetection(result: MethodChannel.Result) { + runResultCatching(result) { + val activity = requireActivity("start ongoing call detection") + if (!isOngoingCallDetectionEnabled) { + Talsec.startOngoingCallDetection(activity) + isOngoingCallDetectionEnabled = true + } + result.success(null) + } + } + + /** + * Stops the ongoing call detection for the current activity. + * + * @param result The result handler of the method call. + */ + private fun stopOngoingCallDetection(result: MethodChannel.Result) { + runResultCatching(result) { + if (isOngoingCallDetectionEnabled) { + activity?.let { Talsec.stopOngoingCallDetection(it) } + isOngoingCallDetectionEnabled = false + } + result.success(null) + } + } + + private fun requireActivity(action: String): Activity { + return activity ?: throw IllegalStateException("Unable to $action - activity is null") + } + /** * Stores an external ID. * diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/handlers/PluginThreatHandler.kt b/android/src/main/kotlin/com/aheaditec/freerasp/handlers/PluginThreatHandler.kt index 8b00478..0d9f9d5 100644 --- a/android/src/main/kotlin/com/aheaditec/freerasp/handlers/PluginThreatHandler.kt +++ b/android/src/main/kotlin/com/aheaditec/freerasp/handlers/PluginThreatHandler.kt @@ -7,6 +7,7 @@ import com.aheaditec.freerasp.dispatchers.ExecutionStateDispatcher import com.aheaditec.freerasp.dispatchers.ThreatDispatcher import app.talsec.rasp.security.api.SuspiciousAppInfo import app.talsec.rasp.security.api.ThreatListener +import app.talsec.rasp.security.api.ThreatListener.CallState import app.talsec.rasp.security.api.ThreatListener.DeviceState import app.talsec.rasp.security.api.ThreatListener.RaspExecutionState import app.talsec.rasp.security.api.ThreatListener.ThreatDetected @@ -86,6 +87,10 @@ internal object PluginThreatHandler { override fun onBootloader() { ThreatDispatcher.dispatchThreat(Threat.Bootloader) } + + override fun onOngoingCall(state: CallState) { + ThreatDispatcher.dispatchOngoingCall(state) + } } private val deviceState = object : DeviceState() { @@ -111,9 +116,19 @@ internal object PluginThreatHandler { } private val raspExecutionState = object : RaspExecutionState() { - override fun onAllChecksFinished() { + override fun onCriticalChecksFinished() { + ExecutionStateDispatcher.dispatch(RaspExecutionStateEvent.CriticalChecksFinished) + } + + // Fast checks finish where the removed onAllChecksFinished() used to be triggered, so + // they keep driving the cross-platform allChecksFinished state. + override fun onFastChecksFinished() { ExecutionStateDispatcher.dispatch(RaspExecutionStateEvent.AllChecksFinished) } + + override fun onPostponedChecksFinished() { + ExecutionStateDispatcher.dispatch(RaspExecutionStateEvent.PostponedChecksFinished) + } } private val internalListener = ThreatListener(threatDetected, deviceState, raspExecutionState) diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index f700a23..2a935ab 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -5,6 +5,9 @@ + + + diff --git a/example/lib/main.dart b/example/lib/main.dart index 2e80cf9..dc03c40 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,5 +1,7 @@ // 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'; @@ -60,6 +62,11 @@ Future _initializeTalsec() async { ); await Talsec.instance.start(config); + + if (Platform.isAndroid) { + // Report cellular and VoIP calls through ThreatCallback.onOngoingCall + await Talsec.instance.startOngoingCallDetection(); + } } /// The root widget of the application @@ -205,10 +212,35 @@ class _HomePageState extends ConsumerState { ], ), ), + if (Platform.isAndroid) ...[ + ProtectionToggle( + title: 'Screen Control', + isBlocked: Talsec.instance.isScreenControlBlocked, + block: Talsec.instance.blockScreenControl, + ), + ProtectionToggle( + title: 'Overlay Windows', + isBlocked: Talsec.instance.isOverlayWindowsBlocked, + block: ({required enabled}) => + Talsec.instance.blockOverlayWindows(enabled: enabled), + ), + ], ListTile( title: const Text('Check Rounds Completed'), trailing: SafetyIcon(isDetected: !threatState.allChecksPassed), ), + if (Platform.isAndroid) ...[ + ListTile( + title: const Text('Postponed Checks Completed'), + trailing: SafetyIcon( + isDetected: !threatState.postponedChecksPassed, + ), + ), + ListTile( + title: const Text('Ongoing Call'), + trailing: Text(threatState.callState?.name ?? 'none'), + ), + ], Expanded( child: ThreatListView(threats: threatState.detectedThreats), ), diff --git a/example/lib/threat_notifier.dart b/example/lib/threat_notifier.dart index 5cebb66..76a3528 100644 --- a/example/lib/threat_notifier.dart +++ b/example/lib/threat_notifier.dart @@ -36,10 +36,13 @@ class ThreatNotifier extends AutoDisposeNotifier { onLocationSpoofing: () => _updateThreat(Threat.locationSpoofing), onAutomation: () => _updateThreat(Threat.automation), onBootloader: () => _updateThreat(Threat.bootloader), + onOngoingCall: _updateCallState, ); - final raspExecutionStateCallback = - RaspExecutionStateCallback(onAllChecksFinished: _updateChecksStatus); + final raspExecutionStateCallback = RaspExecutionStateCallback( + onAllChecksFinished: _updateChecksStatus, + onPostponedChecksFinished: _updatePostponedChecksStatus, + ); Talsec.instance.attachListener(threatCallback); Talsec.instance.attachExecutionStateListener(raspExecutionStateCallback); @@ -56,4 +59,12 @@ class ThreatNotifier extends AutoDisposeNotifier { void _updateChecksStatus() { state = state.copyWith(allChecksPassed: true); } + + void _updatePostponedChecksStatus() { + state = state.copyWith(postponedChecksPassed: true); + } + + void _updateCallState(CallState callState) { + state = state.copyWith(callState: callState); + } } diff --git a/example/lib/threat_state.dart b/example/lib/threat_state.dart index 85efba0..d427a8a 100644 --- a/example/lib/threat_state.dart +++ b/example/lib/threat_state.dart @@ -7,28 +7,39 @@ class ThreatState { detectedThreats: {}, detectedMalware: [], allChecksPassed: false, + postponedChecksPassed: false, + callState: null, ); const ThreatState._({ required this.detectedThreats, required this.detectedMalware, required this.allChecksPassed, + required this.postponedChecksPassed, + required this.callState, }); final Set detectedThreats; final List detectedMalware; final bool allChecksPassed; + final bool postponedChecksPassed; + final CallState? callState; ThreatState copyWith({ Set? detectedThreats, List? detectedMalware, bool? allChecksPassed, + bool? postponedChecksPassed, + CallState? callState, }) { return ThreatState._( detectedThreats: detectedThreats ?? this.detectedThreats, detectedMalware: detectedMalware?.nonNulls.toList() ?? this.detectedMalware, allChecksPassed: allChecksPassed ?? this.allChecksPassed, + postponedChecksPassed: + postponedChecksPassed ?? this.postponedChecksPassed, + callState: callState ?? this.callState, ); } } diff --git a/example/lib/widgets/protection_toggle.dart b/example/lib/widgets/protection_toggle.dart new file mode 100644 index 0000000..ea9d223 --- /dev/null +++ b/example/lib/widgets/protection_toggle.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; + +/// A row with a button that toggles an Android screen protection. +class ProtectionToggle extends StatefulWidget { + /// Creates a [ProtectionToggle]. + const ProtectionToggle({ + required this.title, + required this.isBlocked, + required this.block, + super.key, + }); + + /// Name of the protection. + final String title; + + /// Reads whether the protection is active. + final Future Function() isBlocked; + + /// Turns the protection on or off. + final Future Function({required bool enabled}) block; + + @override + State createState() => _ProtectionToggleState(); +} + +class _ProtectionToggleState extends State { + bool _isBlocked = false; + + Future _toggle() async { + await widget.block(enabled: !_isBlocked); + final isBlocked = await widget.isBlocked(); + if (mounted) setState(() => _isBlocked = isBlocked); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(widget.title, style: Theme.of(context).textTheme.titleMedium), + ElevatedButton( + onPressed: _toggle, + style: ElevatedButton.styleFrom( + backgroundColor: _isBlocked ? Colors.green : Colors.red, + ), + child: Text( + _isBlocked ? 'Protected' : 'Unprotected', + style: const TextStyle(color: Colors.white), + ), + ), + ], + ), + ); + } +} diff --git a/example/lib/widgets/widgets.dart b/example/lib/widgets/widgets.dart index 2342ae2..78092a2 100644 --- a/example/lib/widgets/widgets.dart +++ b/example/lib/widgets/widgets.dart @@ -1,3 +1,4 @@ export 'malware_bottom_sheet.dart'; +export 'protection_toggle.dart'; export 'safety_icon.dart'; export 'threat_listview.dart'; diff --git a/lib/freerasp.dart b/lib/freerasp.dart index f504da3..010db3d 100644 --- a/lib/freerasp.dart +++ b/lib/freerasp.dart @@ -3,7 +3,7 @@ export 'src/callbacks/threat_callback.dart'; export 'src/enums/enums.dart'; export 'src/errors/errors.dart'; export 'src/generated/talsec_pigeon_api.g.dart' - show PackageInfo, SuspiciousAppInfo; + show CallState, PackageInfo, SuspiciousAppInfo; export 'src/models/models.dart'; export 'src/talsec.dart'; export 'src/typedefs.dart'; diff --git a/lib/src/callbacks/rasp_execution_state_callback.dart b/lib/src/callbacks/rasp_execution_state_callback.dart index 6bb24e3..4a11986 100644 --- a/lib/src/callbacks/rasp_execution_state_callback.dart +++ b/lib/src/callbacks/rasp_execution_state_callback.dart @@ -21,8 +21,26 @@ class RaspExecutionStateCallback { /// checks have been completed by the native security engine. RaspExecutionStateCallback({ this.onAllChecksFinished, + this.onCriticalChecksFinished, + this.onPostponedChecksFinished, }); - /// Callback invoked when all security checks are completed. + /// Callback invoked when all standard security checks are completed. + /// + /// On Android, this is invoked after the fast checks are completed, while + /// the postponed checks still run. Use [onPostponedChecksFinished] to know + /// when the initial evaluation has fully ended. final VoidCallback? onAllChecksFinished; + + /// Callback invoked when the critical security checks are completed, + /// before the non-critical checks. + /// + /// Android only + final VoidCallback? onCriticalChecksFinished; + + /// Callback invoked when the postponed security checks are completed, + /// which ends the initial evaluation. + /// + /// Android only + final VoidCallback? onPostponedChecksFinished; } diff --git a/lib/src/callbacks/threat_callback.dart b/lib/src/callbacks/threat_callback.dart index 9e48f91..af10e85 100644 --- a/lib/src/callbacks/threat_callback.dart +++ b/lib/src/callbacks/threat_callback.dart @@ -44,6 +44,7 @@ class ThreatCallback extends TalsecPigeonApi { this.onLocationSpoofing, this.onAutomation, this.onBootloader, + this.onOngoingCall, }); /// This method is called when a threat related dynamic hooking (e.g. Frida) @@ -131,4 +132,19 @@ class ThreatCallback extends TalsecPigeonApi { /// /// Android only final MalwareCallback? onMalware; + + @override + void onOngoingCallDetected(CallState state) { + onOngoingCall?.call(state); + } + + /// This method is called when an ongoing call (cellular or VoIP) starts, + /// is already in progress, or stops while ongoing call detection is + /// running. + /// + /// Detection has to be started with + /// `Talsec.instance.startOngoingCallDetection()`. + /// + /// Android only + final OngoingCallCallback? onOngoingCall; } diff --git a/lib/src/enums/rasp_execution_state.dart b/lib/src/enums/rasp_execution_state.dart index 2bb8d5f..d8ffdb6 100644 --- a/lib/src/enums/rasp_execution_state.dart +++ b/lib/src/enums/rasp_execution_state.dart @@ -1,8 +1,23 @@ /// Represents the state of the RASP (Runtime Application Self-Protection) /// execution. enum RaspExecutionState { - /// All security checks have been completed. + /// All standard security checks have been completed. + /// + /// On Android, this is triggered after the fast checks are completed, while + /// the postponed checks still run. allChecksFinished, + + /// The critical security checks have been completed, before the + /// non-critical checks. + /// + /// Android only + criticalChecksFinished, + + /// The postponed security checks have been completed, which ends the + /// initial evaluation. + /// + /// Android only + postponedChecksFinished, } /// Extensions for [RaspExecutionState]. @@ -12,6 +27,10 @@ extension RaspExecutionStateX on RaspExecutionState { switch (value) { case 187429: return RaspExecutionState.allChecksFinished; + case 614702: + return RaspExecutionState.criticalChecksFinished; + case 952318: + return RaspExecutionState.postponedChecksFinished; default: throw ArgumentError('Unknown execution state code: $value'); } diff --git a/lib/src/generated/talsec_pigeon_api.g.dart b/lib/src/generated/talsec_pigeon_api.g.dart index ef275d9..7e5215a 100644 --- a/lib/src/generated/talsec_pigeon_api.g.dart +++ b/lib/src/generated/talsec_pigeon_api.g.dart @@ -19,6 +19,18 @@ List wrapResponse( return [error.code, error.message, error.details]; } +/// State of an ongoing call reported by the ongoing call detection. +enum CallState { + /// A call started while the detection was running. + started, + + /// A call was already in progress when the detection started. + ongoing, + + /// A previously reported call has ended. + stopped, +} + class PackageInfo { PackageInfo({ required this.packageName, @@ -98,11 +110,14 @@ class _PigeonCodec extends StandardMessageCodec { if (value is int) { buffer.putUint8(4); buffer.putInt64(value); - } else if (value is PackageInfo) { + } else if (value is CallState) { buffer.putUint8(129); + writeValue(buffer, value.index); + } else if (value is PackageInfo) { + buffer.putUint8(130); writeValue(buffer, value.encode()); } else if (value is SuspiciousAppInfo) { - buffer.putUint8(130); + buffer.putUint8(131); writeValue(buffer, value.encode()); } else { super.writeValue(buffer, value); @@ -113,8 +128,11 @@ class _PigeonCodec extends StandardMessageCodec { Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { case 129: - return PackageInfo.decode(readValue(buffer)!); + final int? value = readValue(buffer) as int?; + return value == null ? null : CallState.values[value]; case 130: + return PackageInfo.decode(readValue(buffer)!); + case 131: return SuspiciousAppInfo.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); @@ -127,6 +145,8 @@ abstract class TalsecPigeonApi { void onMalwareDetected(List packageInfo); + void onOngoingCallDetected(CallState state); + static void setUp( TalsecPigeonApi? api, { BinaryMessenger? binaryMessenger, @@ -164,5 +184,34 @@ abstract class TalsecPigeonApi { }); } } + { + final BasicMessageChannel< + Object?> pigeonVar_channel = BasicMessageChannel< + Object?>( + 'dev.flutter.pigeon.freerasp.TalsecPigeonApi.onOngoingCallDetected$messageChannelSuffix', + pigeonChannelCodec, + binaryMessenger: binaryMessenger); + if (api == null) { + pigeonVar_channel.setMessageHandler(null); + } else { + pigeonVar_channel.setMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.freerasp.TalsecPigeonApi.onOngoingCallDetected was null.'); + final List args = (message as List?)!; + final CallState? arg_state = (args[0] as CallState?); + assert(arg_state != null, + 'Argument for dev.flutter.pigeon.freerasp.TalsecPigeonApi.onOngoingCallDetected was null, expected non-null CallState.'); + try { + api.onOngoingCallDetected(arg_state!); + return wrapResponse(empty: true); + } on PlatformException catch (e) { + return wrapResponse(error: e); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString())); + } + }); + } + } } } diff --git a/lib/src/talsec.dart b/lib/src/talsec.dart index 532e705..e9d965b 100644 --- a/lib/src/talsec.dart +++ b/lib/src/talsec.dart @@ -209,6 +209,118 @@ class Talsec { } } + /// Prevents accessibility services from reading or controlling the current + /// application screen. + /// + /// Accessibility services are commonly abused by malware to read screen + /// content and perform actions on behalf of the user. + /// + /// Android only + Future blockScreenControl({required bool enabled}) async { + _checkAndroid(); + try { + await methodChannel + .invokeMethod('blockScreenControl', {'enable': enabled}); + } on PlatformException catch (e) { + throw TalsecException.fromPlatformException(e); + } + } + + /// Checks status of screen control protection. + /// + /// For more details about screen control protection, see the + /// [blockScreenControl] method. + /// + /// Returns `true` if screen control protection is active, `false` + /// otherwise. + /// + /// Android only + Future isScreenControlBlocked() { + _checkAndroid(); + return _invokeBoolMethod('isScreenControlBlocked'); + } + + /// Hides overlay windows of other applications over the current application + /// screen. + /// + /// Requires Android 12 (API 31) or newer and the + /// `android.permission.HIDE_OVERLAY_WINDOWS` permission in the app's + /// `AndroidManifest.xml`. + /// + /// Returns `true` if the request was applied, `false` otherwise (e.g. on an + /// older Android version or when the permission is missing). + /// + /// Android only + Future blockOverlayWindows({required bool enabled}) { + _checkAndroid(); + return _invokeBoolMethod('blockOverlayWindows', {'enable': enabled}); + } + + /// Checks whether overlay windows of other applications are hidden over the + /// current application screen. + /// + /// For more details, see the [blockOverlayWindows] method. + /// + /// Android only + Future isOverlayWindowsBlocked() { + _checkAndroid(); + return _invokeBoolMethod('isOverlayWindowsBlocked'); + } + + /// Starts the ongoing call detection (cellular and VoIP calls) for the + /// current application screen. + /// + /// Call state changes are reported through [ThreatCallback.onOngoingCall]. + /// If a call is already in progress when the detection starts, it is + /// reported as [CallState.ongoing]. + /// + /// Android only + Future startOngoingCallDetection() async { + _checkAndroid(); + try { + await methodChannel.invokeMethod('startOngoingCallDetection'); + } on PlatformException catch (e) { + throw TalsecException.fromPlatformException(e); + } + } + + /// Stops the ongoing call detection started by [startOngoingCallDetection]. + /// + /// Android only + Future stopOngoingCallDetection() async { + _checkAndroid(); + try { + await methodChannel.invokeMethod('stopOngoingCallDetection'); + } on PlatformException catch (e) { + throw TalsecException.fromPlatformException(e); + } + } + + Future _invokeBoolMethod( + String method, [ + Map? arguments, + ]) async { + try { + final result = await methodChannel.invokeMethod(method, arguments); + + if (result is! bool) { + throw TalsecException(message: '$method returned no result.'); + } + + return result; + } on PlatformException catch (e) { + throw TalsecException.fromPlatformException(e); + } + } + + void _checkAndroid() { + if (!Platform.isAndroid) { + throw UnimplementedError( + 'Platform is not supported: $defaultTargetPlatform', + ); + } + } + /// Sends given [data] to the backend. Each call overwrites data stored in /// the backend. /// @@ -344,6 +456,10 @@ class Talsec { switch (event) { case RaspExecutionState.allChecksFinished: callback.onAllChecksFinished?.call(); + case RaspExecutionState.criticalChecksFinished: + callback.onCriticalChecksFinished?.call(); + case RaspExecutionState.postponedChecksFinished: + callback.onPostponedChecksFinished?.call(); } }); } diff --git a/lib/src/typedefs.dart b/lib/src/typedefs.dart index 0814189..d658d9b 100644 --- a/lib/src/typedefs.dart +++ b/lib/src/typedefs.dart @@ -5,3 +5,6 @@ typedef VoidCallback = void Function(); /// Typedef for malware callback typedef MalwareCallback = void Function(List packageInfo); + +/// Typedef for ongoing call callback +typedef OngoingCallCallback = void Function(CallState state); diff --git a/pigeons/talsec_pigeon_api.dart b/pigeons/talsec_pigeon_api.dart index 83bb736..8e16292 100644 --- a/pigeons/talsec_pigeon_api.dart +++ b/pigeons/talsec_pigeon_api.dart @@ -37,9 +37,22 @@ class SuspiciousAppInfo { final List? permissions; } +/// State of an ongoing call reported by the ongoing call detection. +enum CallState { + /// A call started while the detection was running. + started, + + /// A call was already in progress when the detection started. + ongoing, + + /// A previously reported call has ended. + stopped, +} + @FlutterApi() // Migrate whole Talsec API to pigeon -// ignore: one_member_abstracts abstract class TalsecPigeonApi { void onMalwareDetected(List packageInfo); + + void onOngoingCallDetected(CallState state); } diff --git a/pubspec.yaml b/pubspec.yaml index 335510e..a18be5b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: freerasp description: Flutter library for improving app security and threat monitoring on Android and iOS mobile devices. Learn more about provided features on the freeRASP's homepage first. -version: 8.3.0 +version: 8.4.0 homepage: https://www.talsec.app/freerasp-in-app-protection-security-talsec repository: https://github.com/talsec/Free-RASP-Flutter diff --git a/test/src/talsec_test.dart b/test/src/talsec_test.dart index 6a3ef53..2bd6541 100644 --- a/test/src/talsec_test.dart +++ b/test/src/talsec_test.dart @@ -301,5 +301,87 @@ void main() { // Assert await expectLater(completer.future, completes); }); + + test('Should receive Android-only execution states', () { + // Arrange + final eventChannel = MockEventChannel( + eventChannel: Talsec.instance.executionStateChannel, + data: [614702, 187429, 952318], + ); + final talsec = Talsec.private( + FakeMethodChannel(), + FakeEventChannel(), + eventChannel.eventChannel, + ); + + // Act + final stream = talsec.onRaspExecutionState; + + // Assert + expectLater( + stream, + emitsInOrder([ + RaspExecutionState.criticalChecksFinished, + RaspExecutionState.allChecksFinished, + RaspExecutionState.postponedChecksFinished, + emitsDone, + ]), + ); + }); + + test('attachExecutionStateListener should invoke every callback', () async { + // Arrange + final eventChannel = MockEventChannel( + eventChannel: Talsec.instance.executionStateChannel, + data: [614702, 187429, 952318], + ); + final talsec = Talsec.private( + FakeMethodChannel(), + FakeEventChannel(), + eventChannel.eventChannel, + ); + + final log = []; + final completer = Completer(); + final callback = RaspExecutionStateCallback( + onCriticalChecksFinished: () => log.add('critical'), + onAllChecksFinished: () => log.add('all'), + onPostponedChecksFinished: () { + log.add('postponed'); + completer.complete(); + }, + ); + + // Act + await talsec.attachExecutionStateListener(callback); + + // Assert + await expectLater(completer.future, completes); + expect(log, ['critical', 'all', 'postponed']); + }); + }); + + group('Android-only APIs', () { + // Tests run on the host, which is not Android. + final talsec = Talsec.private( + FakeMethodChannel(), + FakeEventChannel(), + FakeEventChannel(), + ); + + final calls = Function()>{ + 'blockScreenControl': () => talsec.blockScreenControl(enabled: true), + 'isScreenControlBlocked': talsec.isScreenControlBlocked, + 'blockOverlayWindows': () => talsec.blockOverlayWindows(enabled: true), + 'isOverlayWindowsBlocked': talsec.isOverlayWindowsBlocked, + 'startOngoingCallDetection': talsec.startOngoingCallDetection, + 'stopOngoingCallDetection': talsec.stopOngoingCallDetection, + }; + + for (final entry in calls.entries) { + test('${entry.key} should throw outside Android', () { + expect(entry.value, throwsA(isA())); + }); + } }); } diff --git a/test/src/threat_callback_test.dart b/test/src/threat_callback_test.dart index 862a94d..4de0c10 100644 --- a/test/src/threat_callback_test.dart +++ b/test/src/threat_callback_test.dart @@ -34,6 +34,20 @@ void main() { expect(SpyThreatListener.log[Threat.unofficialStore], 0); expect(SpyThreatListener.log[Threat.obfuscationIssues], 0); }); + + test('Should forward ongoing call state', () { + // Arrange + final states = []; + final callback = ThreatCallback(onOngoingCall: states.add); + + // Act + callback + ..onOngoingCallDetected(CallState.started) + ..onOngoingCallDetected(CallState.stopped); + + // Assert + expect(states, [CallState.started, CallState.stopped]); + }); }); } From cbd4d5ee4bc56136c021067e883d6ae760ee227d Mon Sep 17 00:00:00 2001 From: Tomas Psota Date: Wed, 11 Nov 2026 09:38:03 +0100 Subject: [PATCH 02/11] docs: reword the Maven coordinates changelog entry Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 776d42b..5971d29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,7 +32,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `onFastChecksFinished()` is triggered after the fast standard checks are completed (where `onAllChecksFinished()` used to be triggered) - `onPostponedChecksFinished()` is triggered after the postponed/deep checks are completed - ❗️Breaking: Modified method `Talsec.isScreenCaptureBlocked(activity: Activity): Boolean` which now takes activity as parameter, and returns if the given activity view is protected against screen capture -- ❗️Breaking: Renamed Maven coordinates from `com.aheaditec.talsec.security:TalsecSecurity-{flavor}` +- ❗️Breaking: Deprecated the freeRASP Maven coordinates (`com.aheaditec.talsec.security:TalsecSecurity-{flavor}`) #### Added From 552bc7bfada74839e49c92a29bef9c14f2b73d26 Mon Sep 17 00:00:00 2001 From: Tomas Psota Date: Wed, 11 Nov 2026 10:03:29 +0100 Subject: [PATCH 03/11] feat!: rename allChecksFinished to fastChecksFinished The execution state that fires when the fast (standard) checks complete is renamed on all platforms to match the Android SDK's onFastChecksFinished. On iOS it is driven by the SDK's onAllChecksFinished. This is a breaking change, so the package major version is bumped. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 7 +++++-- .../freerasp/RaspExecutionStateEvent.kt | 2 +- .../freerasp/handlers/PluginThreatHandler.kt | 4 +--- example/lib/threat_notifier.dart | 2 +- .../Sources/Models/RaspExecutionStates.swift | 2 +- ios/freerasp/Sources/TalsecHandlers.swift | 3 ++- .../rasp_execution_state_callback.dart | 20 +++++++++---------- lib/src/enums/rasp_execution_state.dart | 10 +++++----- lib/src/talsec.dart | 4 ++-- pubspec.yaml | 2 +- test/src/talsec_test.dart | 8 ++++---- 11 files changed, 33 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5971d29..dd0754f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,13 +5,17 @@ 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). -## [8.4.0] - 2026-11-10 +## [9.0.0] - 2026-11-11 - Android SDK version: 20.0.1 - iOS SDK version: 8.0.1 ### Flutter +#### Breaking + +- `RaspExecutionStateCallback.onAllChecksFinished` renamed to `onFastChecksFinished` and `RaspExecutionState.allChecksFinished` renamed to `fastChecksFinished`. On Android, it is triggered when the fast checks are completed, while the postponed checks may still run; on iOS, when all checks are completed + #### Added - `ThreatCallback.onOngoingCall` callback with the new `CallState` enum (`started`, `ongoing`, `stopped`), driven by `Talsec.startOngoingCallDetection()` and `Talsec.stopOngoingCallDetection()` (Android only) @@ -21,7 +25,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 #### Changed -- On Android, `onAllChecksFinished` is now triggered when the fast checks are completed, where it was triggered before; the postponed checks may still run afterwards - On Android, `Talsec.isScreenCaptureBlocked()` reports the protection state of the current activity ### Android diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/RaspExecutionStateEvent.kt b/android/src/main/kotlin/com/aheaditec/freerasp/RaspExecutionStateEvent.kt index 21659c4..04bed32 100644 --- a/android/src/main/kotlin/com/aheaditec/freerasp/RaspExecutionStateEvent.kt +++ b/android/src/main/kotlin/com/aheaditec/freerasp/RaspExecutionStateEvent.kt @@ -1,7 +1,7 @@ package com.aheaditec.freerasp internal sealed class RaspExecutionStateEvent(val value: Int) { - object AllChecksFinished : RaspExecutionStateEvent(187429) + object FastChecksFinished : RaspExecutionStateEvent(187429) object CriticalChecksFinished : RaspExecutionStateEvent(614702) diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/handlers/PluginThreatHandler.kt b/android/src/main/kotlin/com/aheaditec/freerasp/handlers/PluginThreatHandler.kt index 0d9f9d5..b5d2ce8 100644 --- a/android/src/main/kotlin/com/aheaditec/freerasp/handlers/PluginThreatHandler.kt +++ b/android/src/main/kotlin/com/aheaditec/freerasp/handlers/PluginThreatHandler.kt @@ -120,10 +120,8 @@ internal object PluginThreatHandler { ExecutionStateDispatcher.dispatch(RaspExecutionStateEvent.CriticalChecksFinished) } - // Fast checks finish where the removed onAllChecksFinished() used to be triggered, so - // they keep driving the cross-platform allChecksFinished state. override fun onFastChecksFinished() { - ExecutionStateDispatcher.dispatch(RaspExecutionStateEvent.AllChecksFinished) + ExecutionStateDispatcher.dispatch(RaspExecutionStateEvent.FastChecksFinished) } override fun onPostponedChecksFinished() { diff --git a/example/lib/threat_notifier.dart b/example/lib/threat_notifier.dart index 76a3528..29baec0 100644 --- a/example/lib/threat_notifier.dart +++ b/example/lib/threat_notifier.dart @@ -40,7 +40,7 @@ class ThreatNotifier extends AutoDisposeNotifier { ); final raspExecutionStateCallback = RaspExecutionStateCallback( - onAllChecksFinished: _updateChecksStatus, + onFastChecksFinished: _updateChecksStatus, onPostponedChecksFinished: _updatePostponedChecksStatus, ); diff --git a/ios/freerasp/Sources/Models/RaspExecutionStates.swift b/ios/freerasp/Sources/Models/RaspExecutionStates.swift index 4ea3577..4d42af3 100644 --- a/ios/freerasp/Sources/Models/RaspExecutionStates.swift +++ b/ios/freerasp/Sources/Models/RaspExecutionStates.swift @@ -1,3 +1,3 @@ enum RaspExecutionStates: Int { - case allChecksFinished = 187429 + case fastChecksFinished = 187429 } diff --git a/ios/freerasp/Sources/TalsecHandlers.swift b/ios/freerasp/Sources/TalsecHandlers.swift index 51e2a68..d1ca42b 100644 --- a/ios/freerasp/Sources/TalsecHandlers.swift +++ b/ios/freerasp/Sources/TalsecHandlers.swift @@ -26,8 +26,9 @@ extension SecurityThreatCenter: SecurityThreatHandler, TalsecRuntime.RaspExecuti ThreatDispatcher.shared.dispatch(threat: securityThreat) } + // The iOS SDK reports its standard checks as a single "all checks" event, which maps to fastChecksFinished. public func onAllChecksFinished() { - ExecutionStateDispatcher.shared.dispatch(event: .allChecksFinished) + ExecutionStateDispatcher.shared.dispatch(event: .fastChecksFinished) } } diff --git a/lib/src/callbacks/rasp_execution_state_callback.dart b/lib/src/callbacks/rasp_execution_state_callback.dart index 4a11986..b5d8060 100644 --- a/lib/src/callbacks/rasp_execution_state_callback.dart +++ b/lib/src/callbacks/rasp_execution_state_callback.dart @@ -6,8 +6,8 @@ import 'package:freerasp/src/typedefs.dart'; /// Example usage: /// ```dart /// final callback = RaspExecutionStateCallback( -/// onAllChecksFinished: () { -/// print('All security checks have been completed'); +/// onFastChecksFinished: () { +/// print('Fast security checks have been completed'); /// // Update UI or perform additional actions /// }, /// ); @@ -17,20 +17,20 @@ import 'package:freerasp/src/typedefs.dart'; class RaspExecutionStateCallback { /// Creates a new [RaspExecutionStateCallback] instance. /// - /// The [onAllChecksFinished] callback will be invoked when all security - /// checks have been completed by the native security engine. + /// The [onFastChecksFinished] callback will be invoked when the fast + /// security checks have been completed by the native security engine. RaspExecutionStateCallback({ - this.onAllChecksFinished, + this.onFastChecksFinished, this.onCriticalChecksFinished, this.onPostponedChecksFinished, }); - /// Callback invoked when all standard security checks are completed. + /// Callback invoked when the fast (standard) security checks are completed. /// - /// On Android, this is invoked after the fast checks are completed, while - /// the postponed checks still run. Use [onPostponedChecksFinished] to know - /// when the initial evaluation has fully ended. - final VoidCallback? onAllChecksFinished; + /// On Android, the postponed checks may still run afterwards; use + /// [onPostponedChecksFinished] to know when the initial evaluation has fully + /// ended. On iOS, this is invoked when all security checks are completed. + final VoidCallback? onFastChecksFinished; /// Callback invoked when the critical security checks are completed, /// before the non-critical checks. diff --git a/lib/src/enums/rasp_execution_state.dart b/lib/src/enums/rasp_execution_state.dart index d8ffdb6..67b78b9 100644 --- a/lib/src/enums/rasp_execution_state.dart +++ b/lib/src/enums/rasp_execution_state.dart @@ -1,11 +1,11 @@ /// Represents the state of the RASP (Runtime Application Self-Protection) /// execution. enum RaspExecutionState { - /// All standard security checks have been completed. + /// The fast (standard) security checks have been completed. /// - /// On Android, this is triggered after the fast checks are completed, while - /// the postponed checks still run. - allChecksFinished, + /// On Android, the postponed checks may still run afterwards. On iOS, this + /// is triggered when all security checks have been completed. + fastChecksFinished, /// The critical security checks have been completed, before the /// non-critical checks. @@ -26,7 +26,7 @@ extension RaspExecutionStateX on RaspExecutionState { static RaspExecutionState fromInt(int value) { switch (value) { case 187429: - return RaspExecutionState.allChecksFinished; + return RaspExecutionState.fastChecksFinished; case 614702: return RaspExecutionState.criticalChecksFinished; case 952318: diff --git a/lib/src/talsec.dart b/lib/src/talsec.dart index e9d965b..bb37117 100644 --- a/lib/src/talsec.dart +++ b/lib/src/talsec.dart @@ -454,8 +454,8 @@ class Talsec { await detachExecutionStateListener(); _executionStateSubscription ??= onRaspExecutionState.listen((event) { switch (event) { - case RaspExecutionState.allChecksFinished: - callback.onAllChecksFinished?.call(); + case RaspExecutionState.fastChecksFinished: + callback.onFastChecksFinished?.call(); case RaspExecutionState.criticalChecksFinished: callback.onCriticalChecksFinished?.call(); case RaspExecutionState.postponedChecksFinished: diff --git a/pubspec.yaml b/pubspec.yaml index a18be5b..5dbbbe3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: freerasp description: Flutter library for improving app security and threat monitoring on Android and iOS mobile devices. Learn more about provided features on the freeRASP's homepage first. -version: 8.4.0 +version: 9.0.0 homepage: https://www.talsec.app/freerasp-in-app-protection-security-talsec repository: https://github.com/talsec/Free-RASP-Flutter diff --git a/test/src/talsec_test.dart b/test/src/talsec_test.dart index 2bd6541..b791692 100644 --- a/test/src/talsec_test.dart +++ b/test/src/talsec_test.dart @@ -272,7 +272,7 @@ void main() { expectLater( stream, emitsInOrder([ - RaspExecutionState.allChecksFinished, + RaspExecutionState.fastChecksFinished, emitsDone, ]), ); @@ -292,7 +292,7 @@ void main() { final completer = Completer(); final callback = RaspExecutionStateCallback( - onAllChecksFinished: completer.complete, + onFastChecksFinished: completer.complete, ); // Act @@ -322,7 +322,7 @@ void main() { stream, emitsInOrder([ RaspExecutionState.criticalChecksFinished, - RaspExecutionState.allChecksFinished, + RaspExecutionState.fastChecksFinished, RaspExecutionState.postponedChecksFinished, emitsDone, ]), @@ -345,7 +345,7 @@ void main() { final completer = Completer(); final callback = RaspExecutionStateCallback( onCriticalChecksFinished: () => log.add('critical'), - onAllChecksFinished: () => log.add('all'), + onFastChecksFinished: () => log.add('all'), onPostponedChecksFinished: () { log.add('postponed'); completer.complete(); From 1564bb792002e0854bdc4599e33d43f1ea7f42dc Mon Sep 17 00:00:00 2001 From: Tomas Psota Date: Wed, 11 Nov 2026 10:13:55 +0100 Subject: [PATCH 04/11] docs: drop Breaking markers from Android SDK changes absorbed by the plugin The plugin hides these native API changes from its users, so they move from the Android Breaking Changes section to Changed. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd0754f..3dfc02d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,14 +29,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Android -#### Breaking Changes - -- ❗️Breaking: `ThreatListener.RaspExecutionState.onAllChecksFinished()` was removed and replaced by two callbacks: - - `onFastChecksFinished()` is triggered after the fast standard checks are completed (where `onAllChecksFinished()` used to be triggered) - - `onPostponedChecksFinished()` is triggered after the postponed/deep checks are completed -- ❗️Breaking: Modified method `Talsec.isScreenCaptureBlocked(activity: Activity): Boolean` which now takes activity as parameter, and returns if the given activity view is protected against screen capture -- ❗️Breaking: Deprecated the freeRASP Maven coordinates (`com.aheaditec.talsec.security:TalsecSecurity-{flavor}`) - #### Added - Added `isScreenControlBlocked(activity: Activity): Boolean` to check if the screen control protection is set for given activity @@ -59,6 +51,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 #### Changed +- `ThreatListener.RaspExecutionState.onAllChecksFinished()` was removed and replaced by two callbacks: + - `onFastChecksFinished()` is triggered after the fast standard checks are completed (where `onAllChecksFinished()` used to be triggered) + - `onPostponedChecksFinished()` is triggered after the postponed/deep checks are completed +- Modified method `Talsec.isScreenCaptureBlocked(activity: Activity): Boolean` which now takes activity as parameter, and returns if the given activity view is protected against screen capture +- Deprecated the freeRASP Maven coordinates (`com.aheaditec.talsec.security:TalsecSecurity-{flavor}`) - Improved Bootloader detection - Extended emulator detection with additional device-property and ABI checks - Improved logging structure and data enrichment for better Portal insights/reports From 945787d319567a3f24d043452cd87469b39636f7 Mon Sep 17 00:00:00 2001 From: Tomas Psota Date: Wed, 11 Nov 2026 10:15:55 +0100 Subject: [PATCH 05/11] docs: drop Android changelog entries already listed in the plugin section Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3dfc02d..f7f9079 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,18 +29,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Android -#### Added - -- Added `isScreenControlBlocked(activity: Activity): Boolean` to check if the screen control protection is set for given activity -- Added `blockScreenControl(activity: Activity, enable: Boolean)` to prevent accessibility services from reading or controlling an activity's screen -- Added `blockOverlayWindows(activity: Activity, enable: Boolean): Boolean` to hide overlay windows of other apps over the given activity (Android 12+, requires `HIDE_OVERLAY_WINDOWS` permission) -- Added `isOverlayWindowsBlocked(activity: Activity): Boolean` to check if overlay windows are hidden for given activity -- Added a new `Ongoing Call` detection engine (VoIP, Cellular) - - started/paused per activity with `startOngoingCallDetection` / `stopOngoingCallDetection` - - new callback `onOngoingCall(CallState)` in `ThreatListener.ThreatDetected` - - new `CallState` enum with 3 call states (`STARTED`) -- Added callback `onCriticalChecksFinished()` in `ThreatListener.RaspExecutionState`, which is triggered after critical checks are completed - #### Fixed - Improved hook detection to prevent collisions with running VPN services and to better work with the device resources (threads/RAM) @@ -51,10 +39,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 #### Changed -- `ThreatListener.RaspExecutionState.onAllChecksFinished()` was removed and replaced by two callbacks: - - `onFastChecksFinished()` is triggered after the fast standard checks are completed (where `onAllChecksFinished()` used to be triggered) - - `onPostponedChecksFinished()` is triggered after the postponed/deep checks are completed -- Modified method `Talsec.isScreenCaptureBlocked(activity: Activity): Boolean` which now takes activity as parameter, and returns if the given activity view is protected against screen capture - Deprecated the freeRASP Maven coordinates (`com.aheaditec.talsec.security:TalsecSecurity-{flavor}`) - Improved Bootloader detection - Extended emulator detection with additional device-property and ABI checks From 1187397c2bca65d5321c34897118ef1a1388c1a8 Mon Sep 17 00:00:00 2001 From: Tomas Psota <72520867+tompsota@users.noreply.github.com> Date: Wed, 11 Nov 2026 12:10:50 +0100 Subject: [PATCH 06/11] refactor(android): move plugin to the app.talsec.rasp.flutter namespace (#242) * refactor(android): move plugin to the app.talsec.rasp namespace Align the Android package of the plugin with the Android SDK's app.talsec.rasp, adding the platform as the last segment. Co-Authored-By: Claude Opus 5.5 * refactor(android): update package names to app.talsec.rasp Completes the move of the plugin sources: package declarations, imports, Gradle namespace and build configuration. Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Claude Opus 5.5 --- CHANGELOG.md | 1 + android/build.gradle | 4 ++-- android/src/main/AndroidManifest.xml | 2 +- .../talsec/rasp/flutter}/Extensions.kt | 18 +++++++++--------- .../talsec/rasp/flutter}/FreeraspPlugin.kt | 10 +++++----- .../rasp/flutter}/RaspExecutionStateEvent.kt | 2 +- .../talsec/rasp/flutter}/ScreenProtector.kt | 2 +- .../talsec/rasp/flutter}/Threat.kt | 2 +- .../talsec/rasp/flutter}/Utils.kt | 2 +- .../dispatchers/ExecutionStateDispatcher.kt | 4 ++-- .../flutter}/dispatchers/ThreatDispatcher.kt | 6 +++--- .../rasp/flutter}/generated/TalsecPigeonApi.kt | 2 +- .../handlers/ExecutionStateStreamHandler.kt | 2 +- .../flutter}/handlers/MethodCallHandler.kt | 12 ++++++------ .../flutter}/handlers/PluginThreatHandler.kt | 10 +++++----- .../rasp/flutter}/handlers/StreamHandler.kt | 2 +- .../flutter}/handlers/TalsecThreatHandler.kt | 10 +++++----- pigeons/talsec_pigeon_api.dart | 4 ++-- pubspec.yaml | 2 +- 19 files changed, 49 insertions(+), 48 deletions(-) rename android/src/main/kotlin/{com/aheaditec/freerasp => app/talsec/rasp/flutter}/Extensions.kt (86%) rename android/src/main/kotlin/{com/aheaditec/freerasp => app/talsec/rasp/flutter}/FreeraspPlugin.kt (92%) rename android/src/main/kotlin/{com/aheaditec/freerasp => app/talsec/rasp/flutter}/RaspExecutionStateEvent.kt (89%) rename android/src/main/kotlin/{com/aheaditec/freerasp => app/talsec/rasp/flutter}/ScreenProtector.kt (99%) rename android/src/main/kotlin/{com/aheaditec/freerasp => app/talsec/rasp/flutter}/Threat.kt (97%) rename android/src/main/kotlin/{com/aheaditec/freerasp => app/talsec/rasp/flutter}/Utils.kt (99%) rename android/src/main/kotlin/{com/aheaditec/freerasp => app/talsec/rasp/flutter}/dispatchers/ExecutionStateDispatcher.kt (93%) rename android/src/main/kotlin/{com/aheaditec/freerasp => app/talsec/rasp/flutter}/dispatchers/ThreatDispatcher.kt (95%) rename android/src/main/kotlin/{com/aheaditec/freerasp => app/talsec/rasp/flutter}/generated/TalsecPigeonApi.kt (99%) rename android/src/main/kotlin/{com/aheaditec/freerasp => app/talsec/rasp/flutter}/handlers/ExecutionStateStreamHandler.kt (96%) rename android/src/main/kotlin/{com/aheaditec/freerasp => app/talsec/rasp/flutter}/handlers/MethodCallHandler.kt (98%) rename android/src/main/kotlin/{com/aheaditec/freerasp => app/talsec/rasp/flutter}/handlers/PluginThreatHandler.kt (94%) rename android/src/main/kotlin/{com/aheaditec/freerasp => app/talsec/rasp/flutter}/handlers/StreamHandler.kt (98%) rename android/src/main/kotlin/{com/aheaditec/freerasp => app/talsec/rasp/flutter}/handlers/TalsecThreatHandler.kt (94%) diff --git a/CHANGELOG.md b/CHANGELOG.md index f7f9079..87d01f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 #### Changed +- Android package (namespace) of the plugin renamed from `com.aheaditec.freerasp` to `app.talsec.rasp.flutter`, following the Android SDK's `app.talsec.rasp` - On Android, `Talsec.isScreenCaptureBlocked()` reports the protection state of the current activity ### Android diff --git a/android/build.gradle b/android/build.gradle index 2c48362..7352feb 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,4 +1,4 @@ -group 'com.aheaditec.freerasp' +group 'app.talsec.rasp.flutter' version '1.0-SNAPSHOT' buildscript { @@ -32,7 +32,7 @@ apply plugin: 'app.talsec.plugin' android { if (project.android.hasProperty("namespace")) { - namespace("com.aheaditec.freerasp") + namespace("app.talsec.rasp.flutter") } compileSdk 36 diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index b299bf1..bc71b75 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -1,3 +1,3 @@ + package="app.talsec.rasp.flutter"> diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/Extensions.kt b/android/src/main/kotlin/app/talsec/rasp/flutter/Extensions.kt similarity index 86% rename from android/src/main/kotlin/com/aheaditec/freerasp/Extensions.kt rename to android/src/main/kotlin/app/talsec/rasp/flutter/Extensions.kt index 7ede6ee..2d87996 100644 --- a/android/src/main/kotlin/com/aheaditec/freerasp/Extensions.kt +++ b/android/src/main/kotlin/app/talsec/rasp/flutter/Extensions.kt @@ -1,4 +1,4 @@ -package com.aheaditec.freerasp +package app.talsec.rasp.flutter import android.content.Context import android.content.pm.PackageInfo @@ -13,9 +13,9 @@ import io.flutter.plugin.common.MethodChannel import org.json.JSONArray import org.json.JSONObject import app.talsec.rasp.security.api.ThreatListener.CallState -import com.aheaditec.freerasp.generated.CallState as FlutterCallState -import com.aheaditec.freerasp.generated.PackageInfo as FlutterPackageInfo -import com.aheaditec.freerasp.generated.SuspiciousAppInfo as FlutterSuspiciousAppInfo +import app.talsec.rasp.flutter.generated.CallState as FlutterCallState +import app.talsec.rasp.flutter.generated.PackageInfo as FlutterPackageInfo +import app.talsec.rasp.flutter.generated.SuspiciousAppInfo as FlutterSuspiciousAppInfo /** * Executes the provided block of code and catches any exceptions thrown by it, returning the @@ -34,10 +34,10 @@ internal inline fun runResultCatching(result: MethodChannel.Result, block: () -> } /** - * Converts a [SuspiciousAppInfo] instance to a [com.aheaditec.freerasp.generated.SuspiciousAppInfo] + * Converts a [SuspiciousAppInfo] instance to a [app.talsec.rasp.flutter.generated.SuspiciousAppInfo] * instance used by Pigeon package for Flutter. * - * @return A new [com.aheaditec.freerasp.generated.SuspiciousAppInfo] object with information from + * @return A new [app.talsec.rasp.flutter.generated.SuspiciousAppInfo] object with information from * this [SuspiciousAppInfo]. */ internal fun SuspiciousAppInfo.toPigeon(context: Context): FlutterSuspiciousAppInfo { @@ -45,10 +45,10 @@ internal fun SuspiciousAppInfo.toPigeon(context: Context): FlutterSuspiciousAppI } /** - * Converts a [PackageInfo] instance to a [com.aheaditec.freerasp.generated.PackageInfo] instance + * Converts a [PackageInfo] instance to a [app.talsec.rasp.flutter.generated.PackageInfo] instance * used by Pigeon package for Flutter. * - * @return A new [com.aheaditec.freerasp.generated.PackageInfo] object with information from + * @return A new [app.talsec.rasp.flutter.generated.PackageInfo] object with information from * this [PackageInfo]. */ private fun PackageInfo.toPigeon(context: Context): FlutterPackageInfo { @@ -121,7 +121,7 @@ internal fun JSONObject.toSuspiciousAppDetectionConfig(): SuspiciousAppDetection } /** - * Converts a [CallState] instance to a [com.aheaditec.freerasp.generated.CallState] instance + * Converts a [CallState] instance to a [app.talsec.rasp.flutter.generated.CallState] instance * used by Pigeon package for Flutter. */ internal fun CallState.toPigeon(): FlutterCallState { diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/FreeraspPlugin.kt b/android/src/main/kotlin/app/talsec/rasp/flutter/FreeraspPlugin.kt similarity index 92% rename from android/src/main/kotlin/com/aheaditec/freerasp/FreeraspPlugin.kt rename to android/src/main/kotlin/app/talsec/rasp/flutter/FreeraspPlugin.kt index cbbf80f..d0f473a 100644 --- a/android/src/main/kotlin/com/aheaditec/freerasp/FreeraspPlugin.kt +++ b/android/src/main/kotlin/app/talsec/rasp/flutter/FreeraspPlugin.kt @@ -1,4 +1,4 @@ -package com.aheaditec.freerasp +package app.talsec.rasp.flutter import android.app.Activity import android.content.Context @@ -6,10 +6,10 @@ import android.os.Build import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleOwner -import com.aheaditec.freerasp.handlers.ExecutionStateStreamHandler -import com.aheaditec.freerasp.handlers.MethodCallHandler -import com.aheaditec.freerasp.handlers.StreamHandler -import com.aheaditec.freerasp.handlers.TalsecThreatHandler +import app.talsec.rasp.flutter.handlers.ExecutionStateStreamHandler +import app.talsec.rasp.flutter.handlers.MethodCallHandler +import app.talsec.rasp.flutter.handlers.StreamHandler +import app.talsec.rasp.flutter.handlers.TalsecThreatHandler import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.activity.ActivityAware import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/RaspExecutionStateEvent.kt b/android/src/main/kotlin/app/talsec/rasp/flutter/RaspExecutionStateEvent.kt similarity index 89% rename from android/src/main/kotlin/com/aheaditec/freerasp/RaspExecutionStateEvent.kt rename to android/src/main/kotlin/app/talsec/rasp/flutter/RaspExecutionStateEvent.kt index 04bed32..15ab836 100644 --- a/android/src/main/kotlin/com/aheaditec/freerasp/RaspExecutionStateEvent.kt +++ b/android/src/main/kotlin/app/talsec/rasp/flutter/RaspExecutionStateEvent.kt @@ -1,4 +1,4 @@ -package com.aheaditec.freerasp +package app.talsec.rasp.flutter internal sealed class RaspExecutionStateEvent(val value: Int) { object FastChecksFinished : RaspExecutionStateEvent(187429) diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/ScreenProtector.kt b/android/src/main/kotlin/app/talsec/rasp/flutter/ScreenProtector.kt similarity index 99% rename from android/src/main/kotlin/com/aheaditec/freerasp/ScreenProtector.kt rename to android/src/main/kotlin/app/talsec/rasp/flutter/ScreenProtector.kt index d63ddb6..8dfb672 100644 --- a/android/src/main/kotlin/com/aheaditec/freerasp/ScreenProtector.kt +++ b/android/src/main/kotlin/app/talsec/rasp/flutter/ScreenProtector.kt @@ -1,4 +1,4 @@ -package com.aheaditec.freerasp +package app.talsec.rasp.flutter import android.annotation.SuppressLint import android.app.Activity diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/Threat.kt b/android/src/main/kotlin/app/talsec/rasp/flutter/Threat.kt similarity index 97% rename from android/src/main/kotlin/com/aheaditec/freerasp/Threat.kt rename to android/src/main/kotlin/app/talsec/rasp/flutter/Threat.kt index 6e733a4..a118369 100644 --- a/android/src/main/kotlin/com/aheaditec/freerasp/Threat.kt +++ b/android/src/main/kotlin/app/talsec/rasp/flutter/Threat.kt @@ -1,4 +1,4 @@ -package com.aheaditec.freerasp +package app.talsec.rasp.flutter /** * Sealed class to represent the error codes. diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/Utils.kt b/android/src/main/kotlin/app/talsec/rasp/flutter/Utils.kt similarity index 99% rename from android/src/main/kotlin/com/aheaditec/freerasp/Utils.kt rename to android/src/main/kotlin/app/talsec/rasp/flutter/Utils.kt index bf0ebba..a897820 100644 --- a/android/src/main/kotlin/com/aheaditec/freerasp/Utils.kt +++ b/android/src/main/kotlin/app/talsec/rasp/flutter/Utils.kt @@ -1,4 +1,4 @@ -package com.aheaditec.freerasp +package app.talsec.rasp.flutter import android.content.Context import android.graphics.Bitmap diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/dispatchers/ExecutionStateDispatcher.kt b/android/src/main/kotlin/app/talsec/rasp/flutter/dispatchers/ExecutionStateDispatcher.kt similarity index 93% rename from android/src/main/kotlin/com/aheaditec/freerasp/dispatchers/ExecutionStateDispatcher.kt rename to android/src/main/kotlin/app/talsec/rasp/flutter/dispatchers/ExecutionStateDispatcher.kt index 3449855..245c81a 100644 --- a/android/src/main/kotlin/com/aheaditec/freerasp/dispatchers/ExecutionStateDispatcher.kt +++ b/android/src/main/kotlin/app/talsec/rasp/flutter/dispatchers/ExecutionStateDispatcher.kt @@ -1,6 +1,6 @@ -package com.aheaditec.freerasp.dispatchers +package app.talsec.rasp.flutter.dispatchers -import com.aheaditec.freerasp.RaspExecutionStateEvent +import app.talsec.rasp.flutter.RaspExecutionStateEvent import io.flutter.plugin.common.EventChannel.EventSink internal object ExecutionStateDispatcher { diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/dispatchers/ThreatDispatcher.kt b/android/src/main/kotlin/app/talsec/rasp/flutter/dispatchers/ThreatDispatcher.kt similarity index 95% rename from android/src/main/kotlin/com/aheaditec/freerasp/dispatchers/ThreatDispatcher.kt rename to android/src/main/kotlin/app/talsec/rasp/flutter/dispatchers/ThreatDispatcher.kt index 1db9e06..f2ec65c 100644 --- a/android/src/main/kotlin/com/aheaditec/freerasp/dispatchers/ThreatDispatcher.kt +++ b/android/src/main/kotlin/app/talsec/rasp/flutter/dispatchers/ThreatDispatcher.kt @@ -1,7 +1,7 @@ -package com.aheaditec.freerasp.dispatchers +package app.talsec.rasp.flutter.dispatchers -import com.aheaditec.freerasp.Threat -import com.aheaditec.freerasp.handlers.MethodCallHandler +import app.talsec.rasp.flutter.Threat +import app.talsec.rasp.flutter.handlers.MethodCallHandler import app.talsec.rasp.security.api.SuspiciousAppInfo import app.talsec.rasp.security.api.ThreatListener.CallState import io.flutter.plugin.common.EventChannel.EventSink diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/generated/TalsecPigeonApi.kt b/android/src/main/kotlin/app/talsec/rasp/flutter/generated/TalsecPigeonApi.kt similarity index 99% rename from android/src/main/kotlin/com/aheaditec/freerasp/generated/TalsecPigeonApi.kt rename to android/src/main/kotlin/app/talsec/rasp/flutter/generated/TalsecPigeonApi.kt index f81bd94..59636ea 100644 --- a/android/src/main/kotlin/com/aheaditec/freerasp/generated/TalsecPigeonApi.kt +++ b/android/src/main/kotlin/app/talsec/rasp/flutter/generated/TalsecPigeonApi.kt @@ -2,7 +2,7 @@ // See also: https://pub.dev/packages/pigeon @file:Suppress("UNCHECKED_CAST", "ArrayInDataClass") -package com.aheaditec.freerasp.generated +package app.talsec.rasp.flutter.generated import android.util.Log import io.flutter.plugin.common.BasicMessageChannel diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/handlers/ExecutionStateStreamHandler.kt b/android/src/main/kotlin/app/talsec/rasp/flutter/handlers/ExecutionStateStreamHandler.kt similarity index 96% rename from android/src/main/kotlin/com/aheaditec/freerasp/handlers/ExecutionStateStreamHandler.kt rename to android/src/main/kotlin/app/talsec/rasp/flutter/handlers/ExecutionStateStreamHandler.kt index 215f7fd..fee7dfc 100644 --- a/android/src/main/kotlin/com/aheaditec/freerasp/handlers/ExecutionStateStreamHandler.kt +++ b/android/src/main/kotlin/app/talsec/rasp/flutter/handlers/ExecutionStateStreamHandler.kt @@ -1,4 +1,4 @@ -package com.aheaditec.freerasp.handlers +package app.talsec.rasp.flutter.handlers import io.flutter.Log import io.flutter.plugin.common.BinaryMessenger diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/handlers/MethodCallHandler.kt b/android/src/main/kotlin/app/talsec/rasp/flutter/handlers/MethodCallHandler.kt similarity index 98% rename from android/src/main/kotlin/com/aheaditec/freerasp/handlers/MethodCallHandler.kt rename to android/src/main/kotlin/app/talsec/rasp/flutter/handlers/MethodCallHandler.kt index 1f2d6a3..d734d7b 100644 --- a/android/src/main/kotlin/com/aheaditec/freerasp/handlers/MethodCallHandler.kt +++ b/android/src/main/kotlin/app/talsec/rasp/flutter/handlers/MethodCallHandler.kt @@ -1,4 +1,4 @@ -package com.aheaditec.freerasp.handlers +package app.talsec.rasp.flutter.handlers import android.annotation.SuppressLint import android.app.Activity @@ -10,11 +10,11 @@ import android.os.Looper import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleOwner -import com.aheaditec.freerasp.Utils -import com.aheaditec.freerasp.generated.TalsecPigeonApi -import com.aheaditec.freerasp.resolve -import com.aheaditec.freerasp.runResultCatching -import com.aheaditec.freerasp.toPigeon +import app.talsec.rasp.flutter.Utils +import app.talsec.rasp.flutter.generated.TalsecPigeonApi +import app.talsec.rasp.flutter.resolve +import app.talsec.rasp.flutter.runResultCatching +import app.talsec.rasp.flutter.toPigeon import app.talsec.rasp.security.api.SuspiciousAppInfo import app.talsec.rasp.security.api.Talsec import app.talsec.rasp.security.api.ThreatListener.CallState diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/handlers/PluginThreatHandler.kt b/android/src/main/kotlin/app/talsec/rasp/flutter/handlers/PluginThreatHandler.kt similarity index 94% rename from android/src/main/kotlin/com/aheaditec/freerasp/handlers/PluginThreatHandler.kt rename to android/src/main/kotlin/app/talsec/rasp/flutter/handlers/PluginThreatHandler.kt index b5d2ce8..46e4cd8 100644 --- a/android/src/main/kotlin/com/aheaditec/freerasp/handlers/PluginThreatHandler.kt +++ b/android/src/main/kotlin/app/talsec/rasp/flutter/handlers/PluginThreatHandler.kt @@ -1,10 +1,10 @@ -package com.aheaditec.freerasp.handlers +package app.talsec.rasp.flutter.handlers import android.content.Context -import com.aheaditec.freerasp.RaspExecutionStateEvent -import com.aheaditec.freerasp.Threat -import com.aheaditec.freerasp.dispatchers.ExecutionStateDispatcher -import com.aheaditec.freerasp.dispatchers.ThreatDispatcher +import app.talsec.rasp.flutter.RaspExecutionStateEvent +import app.talsec.rasp.flutter.Threat +import app.talsec.rasp.flutter.dispatchers.ExecutionStateDispatcher +import app.talsec.rasp.flutter.dispatchers.ThreatDispatcher import app.talsec.rasp.security.api.SuspiciousAppInfo import app.talsec.rasp.security.api.ThreatListener import app.talsec.rasp.security.api.ThreatListener.CallState diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/handlers/StreamHandler.kt b/android/src/main/kotlin/app/talsec/rasp/flutter/handlers/StreamHandler.kt similarity index 98% rename from android/src/main/kotlin/com/aheaditec/freerasp/handlers/StreamHandler.kt rename to android/src/main/kotlin/app/talsec/rasp/flutter/handlers/StreamHandler.kt index 0156c1c..7b9e1f7 100644 --- a/android/src/main/kotlin/com/aheaditec/freerasp/handlers/StreamHandler.kt +++ b/android/src/main/kotlin/app/talsec/rasp/flutter/handlers/StreamHandler.kt @@ -1,4 +1,4 @@ -package com.aheaditec.freerasp.handlers +package app.talsec.rasp.flutter.handlers import io.flutter.Log import io.flutter.plugin.common.BinaryMessenger diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/handlers/TalsecThreatHandler.kt b/android/src/main/kotlin/app/talsec/rasp/flutter/handlers/TalsecThreatHandler.kt similarity index 94% rename from android/src/main/kotlin/com/aheaditec/freerasp/handlers/TalsecThreatHandler.kt rename to android/src/main/kotlin/app/talsec/rasp/flutter/handlers/TalsecThreatHandler.kt index 93a3958..bd057fb 100644 --- a/android/src/main/kotlin/com/aheaditec/freerasp/handlers/TalsecThreatHandler.kt +++ b/android/src/main/kotlin/app/talsec/rasp/flutter/handlers/TalsecThreatHandler.kt @@ -1,11 +1,11 @@ -package com.aheaditec.freerasp.handlers +package app.talsec.rasp.flutter.handlers import android.content.Context import android.os.Build -import com.aheaditec.freerasp.RaspExecutionStateEvent -import com.aheaditec.freerasp.ScreenProtector -import com.aheaditec.freerasp.dispatchers.ExecutionStateDispatcher -import com.aheaditec.freerasp.dispatchers.ThreatDispatcher +import app.talsec.rasp.flutter.RaspExecutionStateEvent +import app.talsec.rasp.flutter.ScreenProtector +import app.talsec.rasp.flutter.dispatchers.ExecutionStateDispatcher +import app.talsec.rasp.flutter.dispatchers.ThreatDispatcher import app.talsec.rasp.security.api.Talsec import app.talsec.rasp.security.api.TalsecConfig import io.flutter.plugin.common.EventChannel.EventSink diff --git a/pigeons/talsec_pigeon_api.dart b/pigeons/talsec_pigeon_api.dart index 8e16292..29d2026 100644 --- a/pigeons/talsec_pigeon_api.dart +++ b/pigeons/talsec_pigeon_api.dart @@ -4,9 +4,9 @@ import 'package:pigeon/pigeon.dart'; PigeonOptions( dartOut: 'lib/src/generated/talsec_pigeon_api.g.dart', kotlinOut: - 'android/src/main/kotlin/com/aheaditec/freerasp/generated/TalsecPigeonApi.kt', + 'android/src/main/kotlin/app/talsec/rasp/flutter/generated/TalsecPigeonApi.kt', input: 'pigeons/talsec_pigeon_api.dart', - kotlinOptions: KotlinOptions(package: 'com.aheaditec.freerasp.generated'), + kotlinOptions: KotlinOptions(package: 'app.talsec.rasp.flutter.generated'), ), ) class PackageInfo { diff --git a/pubspec.yaml b/pubspec.yaml index 5dbbbe3..aa109ea 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -36,7 +36,7 @@ flutter: plugin: platforms: android: - package: com.aheaditec.freerasp + package: app.talsec.rasp.flutter pluginClass: FreeraspPlugin ios: pluginClass: SwiftFreeraspPlugin From 5c4e4daa38bc28d0a4573e648c86095e93e31bd4 Mon Sep 17 00:00:00 2001 From: Tomas Psota Date: Wed, 11 Nov 2026 12:26:00 +0100 Subject: [PATCH 07/11] example: show ongoing call alongside the other threats Co-Authored-By: Claude Opus 5.5 --- example/lib/main.dart | 10 +++--- example/lib/widgets/threat_listview.dart | 45 ++++++++++++++++++++---- 2 files changed, 43 insertions(+), 12 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index dc03c40..e00a469 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -236,13 +236,13 @@ class _HomePageState extends ConsumerState { isDetected: !threatState.postponedChecksPassed, ), ), - ListTile( - title: const Text('Ongoing Call'), - trailing: Text(threatState.callState?.name ?? 'none'), - ), ], Expanded( - child: ThreatListView(threats: threatState.detectedThreats), + child: ThreatListView( + threats: threatState.detectedThreats, + showOngoingCall: Platform.isAndroid, + callState: threatState.callState, + ), ), ], ), diff --git a/example/lib/widgets/threat_listview.dart b/example/lib/widgets/threat_listview.dart index 2342436..1db3973 100644 --- a/example/lib/widgets/threat_listview.dart +++ b/example/lib/widgets/threat_listview.dart @@ -8,28 +8,59 @@ class ThreatListView extends StatelessWidget { /// Represents a list of detected threats const ThreatListView({ required this.threats, + this.showOngoingCall = false, + this.callState, super.key, }); /// Set of detected threats final Set threats; + /// Whether to show the ongoing call row (Android only) + final bool showOngoingCall; + + /// Last reported state of an ongoing call + final CallState? callState; + @override Widget build(BuildContext context) { + // Ongoing call carries a state instead of being a Threat value, so it is + // appended to the list as an extra row. + final itemCount = Threat.values.length + (showOngoingCall ? 1 : 0); + return ListView.separated( padding: const EdgeInsets.all(8), - itemCount: Threat.values.length, + itemCount: itemCount, itemBuilder: (context, index) { - final currentThreat = Threat.values[index]; - final isDetected = threats.contains(currentThreat); + if (index == Threat.values.length) { + final isDetected = + callState == CallState.started || callState == CallState.ongoing; + return _ThreatTile(name: 'OngoingCall', isDetected: isDetected); + } - return ListTile( - title: Text(currentThreat.name.capitalize()), - subtitle: Text(isDetected ? 'Danger' : 'Safe'), - trailing: SafetyIcon(isDetected: isDetected), + final currentThreat = Threat.values[index]; + return _ThreatTile( + name: currentThreat.name.capitalize(), + isDetected: threats.contains(currentThreat), ); }, separatorBuilder: (_, __) => const Divider(height: 1), ); } } + +class _ThreatTile extends StatelessWidget { + const _ThreatTile({required this.name, required this.isDetected}); + + final String name; + final bool isDetected; + + @override + Widget build(BuildContext context) { + return ListTile( + title: Text(name), + subtitle: Text(isDetected ? 'Danger' : 'Safe'), + trailing: SafetyIcon(isDetected: isDetected), + ); + } +} From 98707a95cd87ec0ea52715097f2dd4bcd5d94159 Mon Sep 17 00:00:00 2001 From: Tomas Psota Date: Wed, 11 Nov 2026 12:49:17 +0100 Subject: [PATCH 08/11] example: show the ongoing call state (Not detected, Started, Ongoing, Stopped) Co-Authored-By: Claude Opus 5.5 --- example/lib/widgets/threat_listview.dart | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/example/lib/widgets/threat_listview.dart b/example/lib/widgets/threat_listview.dart index 1db3973..06c7ece 100644 --- a/example/lib/widgets/threat_listview.dart +++ b/example/lib/widgets/threat_listview.dart @@ -35,7 +35,16 @@ class ThreatListView extends StatelessWidget { if (index == Threat.values.length) { final isDetected = callState == CallState.started || callState == CallState.ongoing; - return _ThreatTile(name: 'OngoingCall', isDetected: isDetected); + return _ThreatTile( + name: 'OngoingCall', + isDetected: isDetected, + subtitle: switch (callState) { + null => 'Not detected', + CallState.started => 'Started', + CallState.ongoing => 'Ongoing', + CallState.stopped => 'Stopped', + }, + ); } final currentThreat = Threat.values[index]; @@ -50,16 +59,21 @@ class ThreatListView extends StatelessWidget { } class _ThreatTile extends StatelessWidget { - const _ThreatTile({required this.name, required this.isDetected}); + const _ThreatTile({ + required this.name, + required this.isDetected, + this.subtitle, + }); final String name; final bool isDetected; + final String? subtitle; @override Widget build(BuildContext context) { return ListTile( title: Text(name), - subtitle: Text(isDetected ? 'Danger' : 'Safe'), + subtitle: Text(subtitle ?? (isDetected ? 'Danger' : 'Safe')), trailing: SafetyIcon(isDetected: isDetected), ); } From a41134b5e5ac182b4cfc71f0074983da97401b65 Mon Sep 17 00:00:00 2001 From: Tomas Psota Date: Wed, 11 Nov 2026 13:05:27 +0100 Subject: [PATCH 09/11] feat(android): start Talsec in background mode Talsec.start now uses TalsecMode.BACKGROUND, as the React Native plugin already does, so the SDK initializes off the calling thread. Co-Authored-By: Claude Opus 5.5 --- .../app/talsec/rasp/flutter/handlers/TalsecThreatHandler.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/android/src/main/kotlin/app/talsec/rasp/flutter/handlers/TalsecThreatHandler.kt b/android/src/main/kotlin/app/talsec/rasp/flutter/handlers/TalsecThreatHandler.kt index bd057fb..d33b1f9 100644 --- a/android/src/main/kotlin/app/talsec/rasp/flutter/handlers/TalsecThreatHandler.kt +++ b/android/src/main/kotlin/app/talsec/rasp/flutter/handlers/TalsecThreatHandler.kt @@ -8,6 +8,7 @@ import app.talsec.rasp.flutter.dispatchers.ExecutionStateDispatcher import app.talsec.rasp.flutter.dispatchers.ThreatDispatcher import app.talsec.rasp.security.api.Talsec import app.talsec.rasp.security.api.TalsecConfig +import app.talsec.rasp.security.api.TalsecMode import io.flutter.plugin.common.EventChannel.EventSink /** @@ -24,7 +25,7 @@ internal object TalsecThreatHandler { */ internal fun start(context: Context, config: TalsecConfig) { attachListener(context) - Talsec.start(context, config) + Talsec.start(context, config, TalsecMode.BACKGROUND) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { ScreenProtector.enable() } From 0898a8c35e2a92881d54f8a518d600e17ab4d699 Mon Sep 17 00:00:00 2001 From: Tomas Psota Date: Thu, 12 Nov 2026 08:41:50 +0100 Subject: [PATCH 10/11] docs(changelog): note that freeRASP starts in background mode on Android Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 87d01f7..37aba4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Android package (namespace) of the plugin renamed from `com.aheaditec.freerasp` to `app.talsec.rasp.flutter`, following the Android SDK's `app.talsec.rasp` - On Android, `Talsec.isScreenCaptureBlocked()` reports the protection state of the current activity +- On Android, freeRASP now starts in background mode (`TalsecMode.BACKGROUND`), so the initialization and synchronous checks no longer run on the main thread ### Android From 4ab77489a6c6a79187ff45112ed284cc52a98153 Mon Sep 17 00:00:00 2001 From: Tomas Psota <72520867+tompsota@users.noreply.github.com> Date: Fri, 13 Nov 2026 23:05:02 +0100 Subject: [PATCH 11/11] feat!: unify the public API with the other freeRASP plugins (#243) * feat!: unify the public API with the other freeRASP plugins - config fields follow the native SDKs: AndroidConfig.certificateHashes, supportedAlternativeStores; IOSConfig.appBundleIds, appTeamId - threat names match the other plugins: onAdbEnabled, onUnsecureWifi, Threat.deviceID, Threat.unsecureWifi - every method throws only TalsecException or a subclass; new UnsupportedPlatformException replaces UnimplementedError, and the malware/external-id exceptions are now exported Co-Authored-By: Claude Opus 5.5 * test(ios): use the renamed config keys in RunnerTests Co-Authored-By: Claude Opus 5.5 * refactor!: hide Talsec internals, tighten malware callback, unify unknown-code handling - Talsec.private and the channel fields are private; tests use an internal createTalsecForTesting factory that freerasp.dart hides - MalwareCallback takes List suspiciousApps - ThreatX.fromInt throws ArgumentError; Talsec terminates the app on an unknown threat or execution state code, like the JS plugins Co-Authored-By: Claude Opus 5.5 * fix: use @visibleForTesting for the test factory @internal is not exported by flutter/foundation in Flutter 3.24, the minimum supported version used by CI. Co-Authored-By: Claude Opus 5.5 * refactor!: replace Pigeon with the threat event channel (#244) * refactor!: replace Pigeon with the threat event channel Malware and ongoing call detections are sent over the existing threat EventChannel as {type, ...} maps next to the plain threat codes, so one subscription delivers every detection in order. PackageInfo, SuspiciousAppInfo and CallState become hand-written Dart types with the same names; ThreatCallback no longer extends the generated TalsecPigeonApi. The pigeon dev dependency and generated code are removed. Co-Authored-By: Claude Opus 5.5 * fix: import @internal and @immutable from package:meta flutter/foundation does not export @internal in Flutter 3.24, the minimum supported version used by CI. Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Claude Opus 5.5 --------- Co-authored-by: Claude Opus 5.5 --- .pubignore | 1 - CHANGELOG.md | 11 + .../app/talsec/rasp/flutter/Extensions.kt | 48 ++- .../kotlin/app/talsec/rasp/flutter/Utils.kt | 4 +- .../flutter/dispatchers/ThreatDispatcher.kt | 61 ++-- .../rasp/flutter/generated/TalsecPigeonApi.kt | 183 ----------- .../flutter/handlers/MethodCallHandler.kt | 42 --- .../flutter/handlers/PluginThreatHandler.kt | 8 +- .../flutter/handlers/TalsecThreatHandler.kt | 8 - example/ios/RunnerTests/RunnerTests.swift | 2 +- example/lib/main.dart | 8 +- example/lib/threat_notifier.dart | 10 +- .../Sources/FlutterTalsecConfig.swift | 6 +- lib/freerasp.dart | 4 +- lib/src/callbacks/threat_callback.dart | 21 +- lib/src/enums/call_state.dart | 13 + lib/src/enums/enums.dart | 1 + lib/src/enums/threat.dart | 25 +- lib/src/errors/errors.dart | 3 + lib/src/errors/malware_failure_exception.dart | 6 + .../unsupported_platform_exception.dart | 13 + lib/src/generated/talsec_pigeon_api.g.dart | 217 ------------- lib/src/models/android_config.dart | 8 +- lib/src/models/android_config.g.dart | 15 +- lib/src/models/ios_config.dart | 8 +- lib/src/models/ios_config.g.dart | 11 +- lib/src/models/models.dart | 2 + lib/src/models/package_info.dart | 62 ++++ lib/src/models/suspicious_app_info.dart | 63 ++++ lib/src/talsec.dart | 298 +++++++++++------- lib/src/typedefs.dart | 7 +- lib/src/utils/config_verifier.dart | 16 +- pigeons/build.sh | 22 -- pigeons/talsec_pigeon_api.dart | 58 ---- pubspec.yaml | 1 - test/src/enums/threat_test.dart | 12 +- test/src/models/android_config_test.dart | 42 +-- test/src/models/ios_config_test.dart | 28 +- test/src/models/talsec_config_test.dart | 6 +- test/src/talsec_test.dart | 51 +-- test/src/threat_callback_test.dart | 14 - test/src/threat_events_test.dart | 103 ++++++ test/src/utils/config_verifier_test.dart | 38 +-- test/test_utils/mock_event_channel.dart | 2 +- test/test_utils/spy_threat_callback.dart | 12 +- 45 files changed, 689 insertions(+), 885 deletions(-) delete mode 100644 android/src/main/kotlin/app/talsec/rasp/flutter/generated/TalsecPigeonApi.kt create mode 100644 lib/src/enums/call_state.dart create mode 100644 lib/src/errors/unsupported_platform_exception.dart delete mode 100644 lib/src/generated/talsec_pigeon_api.g.dart create mode 100644 lib/src/models/package_info.dart create mode 100644 lib/src/models/suspicious_app_info.dart delete mode 100755 pigeons/build.sh delete mode 100644 pigeons/talsec_pigeon_api.dart create mode 100644 test/src/threat_events_test.dart diff --git a/.pubignore b/.pubignore index f6ab228..925a7f6 100644 --- a/.pubignore +++ b/.pubignore @@ -7,6 +7,5 @@ example/* example/lib/* !example/lib/main.dart test/ -pigeons/ !**/TalsecRuntime.xcframework/ !**/TalsecRuntime.xcframework/** diff --git a/CHANGELOG.md b/CHANGELOG.md index 37aba4b..63e25ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 #### Breaking - `RaspExecutionStateCallback.onAllChecksFinished` renamed to `onFastChecksFinished` and `RaspExecutionState.allChecksFinished` renamed to `fastChecksFinished`. On Android, it is triggered when the fast checks are completed, while the postponed checks may still run; on iOS, when all checks are completed +- `AndroidConfig.signingCertHashes` renamed to `certificateHashes` and `AndroidConfig.supportedStores` renamed to `supportedAlternativeStores`, matching the Android SDK and the other freeRASP plugins +- `IOSConfig.bundleIds` renamed to `appBundleIds` and `IOSConfig.teamId` renamed to `appTeamId`, matching the iOS SDK and the other freeRASP plugins. The JSON keys used by `toJson`/`fromJson` change accordingly +- `ThreatCallback.onADBEnabled` renamed to `onAdbEnabled` and `ThreatCallback.onUnsecureWiFi` renamed to `onUnsecureWifi`; `Threat.deviceId` renamed to `deviceID` and `Threat.unsecureWiFi` renamed to `unsecureWifi`, matching the other freeRASP plugins +- All `Talsec` methods now throw only `TalsecException` or its subclasses: Android-only methods throw the new `UnsupportedPlatformException` (code `unsupported-platform`) instead of `UnimplementedError`, `blockScreenCapture`/`isScreenCaptureBlocked` no longer throw a generic `Exception`, `start` wraps platform errors in `TalsecException`, and `addToWhitelist`/`getAppIcon` throw `MalwareFailureException` +- `MalwareCallback` now receives a `List` without nullable items, and its parameter is renamed from `packageInfo` to `suspiciousApps` +- The `Talsec.private` constructor and the `methodChannel`, `eventChannel` and `executionStateChannel` fields are no longer public; use `Talsec.instance` +- `ThreatX.fromInt` throws an `ArgumentError` for an unknown code instead of terminating the app. The app is still terminated when the native side sends an unknown threat code, and now also for an unknown execution state code, like in the other freeRASP plugins +- `ThreatCallback` no longer extends the generated `TalsecPigeonApi`; its `onMalwareDetected` and `onOngoingCallDetected` methods are removed, use the `onMalware` and `onOngoingCall` callbacks instead +- `PackageInfo` and `SuspiciousAppInfo` are now immutable classes with value equality; their fields are final and the `encode`/`decode` methods are removed #### Added @@ -22,12 +31,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `Talsec.blockScreenControl()` and `Talsec.isScreenControlBlocked()` to prevent accessibility services from reading or controlling the app screen (Android only) - `Talsec.blockOverlayWindows()` and `Talsec.isOverlayWindowsBlocked()` to hide overlay windows of other apps (Android 12+ only, requires the `HIDE_OVERLAY_WINDOWS` permission) - `RaspExecutionStateCallback.onCriticalChecksFinished` and `RaspExecutionStateCallback.onPostponedChecksFinished`, with matching `RaspExecutionState` values (Android only) +- `ExternalIdFailureException` and `MalwareFailureException` are now exported, so they can be caught by type #### Changed - Android package (namespace) of the plugin renamed from `com.aheaditec.freerasp` to `app.talsec.rasp.flutter`, following the Android SDK's `app.talsec.rasp` - On Android, `Talsec.isScreenCaptureBlocked()` reports the protection state of the current activity - On Android, freeRASP now starts in background mode (`TalsecMode.BACKGROUND`), so the initialization and synchronous checks no longer run on the main thread +- Malware and ongoing call detections are delivered over the same channel as the other threats, so `ThreatCallback` receives all detections in the order in which they occur. The plugin no longer uses Pigeon ### Android diff --git a/android/src/main/kotlin/app/talsec/rasp/flutter/Extensions.kt b/android/src/main/kotlin/app/talsec/rasp/flutter/Extensions.kt index 2d87996..efb6332 100644 --- a/android/src/main/kotlin/app/talsec/rasp/flutter/Extensions.kt +++ b/android/src/main/kotlin/app/talsec/rasp/flutter/Extensions.kt @@ -13,9 +13,6 @@ import io.flutter.plugin.common.MethodChannel import org.json.JSONArray import org.json.JSONObject import app.talsec.rasp.security.api.ThreatListener.CallState -import app.talsec.rasp.flutter.generated.CallState as FlutterCallState -import app.talsec.rasp.flutter.generated.PackageInfo as FlutterPackageInfo -import app.talsec.rasp.flutter.generated.SuspiciousAppInfo as FlutterSuspiciousAppInfo /** * Executes the provided block of code and catches any exceptions thrown by it, returning the @@ -34,31 +31,27 @@ internal inline fun runResultCatching(result: MethodChannel.Result, block: () -> } /** - * Converts a [SuspiciousAppInfo] instance to a [app.talsec.rasp.flutter.generated.SuspiciousAppInfo] - * instance used by Pigeon package for Flutter. - * - * @return A new [app.talsec.rasp.flutter.generated.SuspiciousAppInfo] object with information from - * this [SuspiciousAppInfo]. + * Converts a [SuspiciousAppInfo] to the map that the Dart `SuspiciousAppInfo.fromMap` reads. */ -internal fun SuspiciousAppInfo.toPigeon(context: Context): FlutterSuspiciousAppInfo { - return FlutterSuspiciousAppInfo(this.packageInfo.toPigeon(context), this.reasons.toList(), this.permissions?.toList()) +internal fun SuspiciousAppInfo.toFlutterMap(context: Context): Map { + return mapOf( + "packageInfo" to packageInfo.toFlutterMap(context), + "reasons" to reasons.toList(), + "permissions" to permissions?.toList(), + ) } /** - * Converts a [PackageInfo] instance to a [app.talsec.rasp.flutter.generated.PackageInfo] instance - * used by Pigeon package for Flutter. - * - * @return A new [app.talsec.rasp.flutter.generated.PackageInfo] object with information from - * this [PackageInfo]. + * Converts a [PackageInfo] to the map that the Dart `PackageInfo.fromMap` reads. */ -private fun PackageInfo.toPigeon(context: Context): FlutterPackageInfo { - return FlutterPackageInfo( - packageName = packageName, - appName = applicationInfo?.let { - context.packageManager.getApplicationLabel(it) as String +private fun PackageInfo.toFlutterMap(context: Context): Map { + return mapOf( + "packageName" to packageName, + "appName" to applicationInfo?.let { + context.packageManager.getApplicationLabel(it).toString() }, - version = getVersionString(), - installerStore = Utils.getInstallerPackageName(context, packageName), + "version" to getVersionString(), + "installerStore" to Utils.getInstallerPackageName(context, packageName), ) } @@ -121,13 +114,12 @@ internal fun JSONObject.toSuspiciousAppDetectionConfig(): SuspiciousAppDetection } /** - * Converts a [CallState] instance to a [app.talsec.rasp.flutter.generated.CallState] instance - * used by Pigeon package for Flutter. + * Converts a [CallState] to the name of the Dart `CallState` value. */ -internal fun CallState.toPigeon(): FlutterCallState { +internal fun CallState.toFlutterName(): String { return when (this) { - CallState.STARTED -> FlutterCallState.STARTED - CallState.ONGOING -> FlutterCallState.ONGOING - CallState.STOPPED -> FlutterCallState.STOPPED + CallState.STARTED -> "started" + CallState.ONGOING -> "ongoing" + CallState.STOPPED -> "stopped" } } diff --git a/android/src/main/kotlin/app/talsec/rasp/flutter/Utils.kt b/android/src/main/kotlin/app/talsec/rasp/flutter/Utils.kt index a897820..8060721 100644 --- a/android/src/main/kotlin/app/talsec/rasp/flutter/Utils.kt +++ b/android/src/main/kotlin/app/talsec/rasp/flutter/Utils.kt @@ -26,8 +26,8 @@ internal object Utils { val killOnBypass = json.optBoolean("killOnBypass") val androidConfig = json.getJSONObject("androidConfig") val packageName = androidConfig.getString("packageName") - val certificateHashes = androidConfig.extractArray("signingCertHashes") - val alternativeStores = androidConfig.extractArray("supportedStores") + val certificateHashes = androidConfig.extractArray("certificateHashes") + val alternativeStores = androidConfig.extractArray("supportedAlternativeStores") val builder = TalsecConfig.Builder(packageName, certificateHashes) .watcherMail(watcherMail) diff --git a/android/src/main/kotlin/app/talsec/rasp/flutter/dispatchers/ThreatDispatcher.kt b/android/src/main/kotlin/app/talsec/rasp/flutter/dispatchers/ThreatDispatcher.kt index f2ec65c..94231ec 100644 --- a/android/src/main/kotlin/app/talsec/rasp/flutter/dispatchers/ThreatDispatcher.kt +++ b/android/src/main/kotlin/app/talsec/rasp/flutter/dispatchers/ThreatDispatcher.kt @@ -1,14 +1,20 @@ package app.talsec.rasp.flutter.dispatchers import app.talsec.rasp.flutter.Threat -import app.talsec.rasp.flutter.handlers.MethodCallHandler -import app.talsec.rasp.security.api.SuspiciousAppInfo +import app.talsec.rasp.flutter.toFlutterName import app.talsec.rasp.security.api.ThreatListener.CallState import io.flutter.plugin.common.EventChannel.EventSink +/** + * Sends detections to Flutter over the threat [EventSink], in the order in which they occur. + * + * A plain threat is sent as its [Int] code. A detection with a payload is sent as a map with a + * `type` key: `malware` (with `suspiciousApps`) or `ongoingCall` (with `state`). Detections that + * occur while the app is in background or no listener is attached are cached and sent later. + */ internal object ThreatDispatcher { private val threatCache = mutableSetOf() - private val malwareCache = mutableListOf() + private val malwareCache = mutableListOf>() private val callStateCache = mutableListOf() private var isAppInForeground = false @@ -17,28 +23,14 @@ internal object ThreatDispatcher { field = value if (value != null) { isAppInForeground = true - flushThreatCache() - } - } - - var methodSink: MethodCallHandler.MethodSink? = null - set(value) { - field = value - if (value != null) { - isAppInForeground = true - flushMalwareCache() - flushCallStateCache() + flushCache() } } fun onResume() { isAppInForeground = true if (eventSink != null) { - flushThreatCache() - } - if (methodSink != null) { - flushMalwareCache() - flushCallStateCache() + flushCache() } } @@ -56,9 +48,12 @@ internal object ThreatDispatcher { } } - fun dispatchMalware(apps: List) { - if (isAppInForeground && methodSink != null) { - methodSink?.onMalwareDetected(apps) + /** + * @param apps The suspicious apps, already converted with `toFlutterMap`. + */ + fun dispatchMalware(apps: List>) { + if (isAppInForeground && eventSink != null) { + eventSink?.success(malwareEvent(apps)) } else { synchronized(malwareCache) { malwareCache.addAll(apps) @@ -67,8 +62,8 @@ internal object ThreatDispatcher { } fun dispatchOngoingCall(state: CallState) { - if (isAppInForeground && methodSink != null) { - methodSink?.onOngoingCall(state) + if (isAppInForeground && eventSink != null) { + eventSink?.success(ongoingCallEvent(state)) } else { // Keep the order: a call that started and stopped in background is reported as both. synchronized(callStateCache) { @@ -77,6 +72,12 @@ internal object ThreatDispatcher { } } + private fun flushCache() { + flushThreatCache() + flushMalwareCache() + flushCallStateCache() + } + private fun flushThreatCache() { val threats = synchronized(threatCache) { val snapshot = threatCache.toSet() @@ -88,12 +89,12 @@ internal object ThreatDispatcher { private fun flushMalwareCache() { val malware = synchronized(malwareCache) { - val snapshot = malwareCache.toMutableList() + val snapshot = malwareCache.toList() malwareCache.clear() snapshot } if (malware.isNotEmpty()) { - methodSink?.onMalwareDetected(malware) + eventSink?.success(malwareEvent(malware)) } } @@ -103,6 +104,12 @@ internal object ThreatDispatcher { callStateCache.clear() snapshot } - states.forEach { methodSink?.onOngoingCall(it) } + states.forEach { eventSink?.success(ongoingCallEvent(it)) } } + + private fun malwareEvent(apps: List>): Map = + mapOf("type" to "malware", "suspiciousApps" to apps) + + private fun ongoingCallEvent(state: CallState): Map = + mapOf("type" to "ongoingCall", "state" to state.toFlutterName()) } diff --git a/android/src/main/kotlin/app/talsec/rasp/flutter/generated/TalsecPigeonApi.kt b/android/src/main/kotlin/app/talsec/rasp/flutter/generated/TalsecPigeonApi.kt deleted file mode 100644 index 59636ea..0000000 --- a/android/src/main/kotlin/app/talsec/rasp/flutter/generated/TalsecPigeonApi.kt +++ /dev/null @@ -1,183 +0,0 @@ -// Autogenerated from Pigeon (v22.7.4), do not edit directly. -// See also: https://pub.dev/packages/pigeon -@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass") - -package app.talsec.rasp.flutter.generated - -import android.util.Log -import io.flutter.plugin.common.BasicMessageChannel -import io.flutter.plugin.common.BinaryMessenger -import io.flutter.plugin.common.EventChannel -import io.flutter.plugin.common.MessageCodec -import io.flutter.plugin.common.StandardMethodCodec -import io.flutter.plugin.common.StandardMessageCodec -import java.io.ByteArrayOutputStream -import java.nio.ByteBuffer - -private fun createConnectionError(channelName: String): FlutterError { - return FlutterError("channel-error", "Unable to establish connection on channel: '$channelName'.", "")} - -/** - * Error class for passing custom error details to Flutter via a thrown PlatformException. - * @property code The error code. - * @property message The error message. - * @property details The error details. Must be a datatype supported by the api codec. - */ -class FlutterError ( - val code: String, - override val message: String? = null, - val details: Any? = null -) : Throwable() - -/** State of an ongoing call reported by the ongoing call detection. */ -enum class CallState(val raw: Int) { - /** A call started while the detection was running. */ - STARTED(0), - /** A call was already in progress when the detection started. */ - ONGOING(1), - /** A previously reported call has ended. */ - STOPPED(2); - - companion object { - fun ofRaw(raw: Int): CallState? { - return values().firstOrNull { it.raw == raw } - } - } -} - -/** Generated class from Pigeon that represents data sent in messages. */ -data class PackageInfo ( - val packageName: String, - val appIcon: String? = null, - val appName: String? = null, - val version: String? = null, - val installerStore: String? = null -) - { - companion object { - fun fromList(pigeonVar_list: List): PackageInfo { - val packageName = pigeonVar_list[0] as String - val appIcon = pigeonVar_list[1] as String? - val appName = pigeonVar_list[2] as String? - val version = pigeonVar_list[3] as String? - val installerStore = pigeonVar_list[4] as String? - return PackageInfo(packageName, appIcon, appName, version, installerStore) - } - } - fun toList(): List { - return listOf( - packageName, - appIcon, - appName, - version, - installerStore, - ) - } -} - -/** Generated class from Pigeon that represents data sent in messages. */ -data class SuspiciousAppInfo ( - val packageInfo: PackageInfo, - val reasons: List, - val permissions: List? = null -) - { - companion object { - fun fromList(pigeonVar_list: List): SuspiciousAppInfo { - val packageInfo = pigeonVar_list[0] as PackageInfo - val reasons = pigeonVar_list[1] as List - val permissions = pigeonVar_list[2] as List? - return SuspiciousAppInfo(packageInfo, reasons, permissions) - } - } - fun toList(): List { - return listOf( - packageInfo, - reasons, - permissions, - ) - } -} -private open class TalsecPigeonApiPigeonCodec : StandardMessageCodec() { - override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { - return when (type) { - 129.toByte() -> { - return (readValue(buffer) as Long?)?.let { - CallState.ofRaw(it.toInt()) - } - } - 130.toByte() -> { - return (readValue(buffer) as? List)?.let { - PackageInfo.fromList(it) - } - } - 131.toByte() -> { - return (readValue(buffer) as? List)?.let { - SuspiciousAppInfo.fromList(it) - } - } - else -> super.readValueOfType(type, buffer) - } - } - override fun writeValue(stream: ByteArrayOutputStream, value: Any?) { - when (value) { - is CallState -> { - stream.write(129) - writeValue(stream, value.raw) - } - is PackageInfo -> { - stream.write(130) - writeValue(stream, value.toList()) - } - is SuspiciousAppInfo -> { - stream.write(131) - writeValue(stream, value.toList()) - } - else -> super.writeValue(stream, value) - } - } -} - -/** Generated class from Pigeon that represents Flutter messages that can be called from Kotlin. */ -class TalsecPigeonApi(private val binaryMessenger: BinaryMessenger, private val messageChannelSuffix: String = "") { - companion object { - /** The codec used by TalsecPigeonApi. */ - val codec: MessageCodec by lazy { - TalsecPigeonApiPigeonCodec() - } - } - fun onMalwareDetected(packageInfoArg: List, callback: (Result) -> Unit) -{ - val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" - val channelName = "dev.flutter.pigeon.freerasp.TalsecPigeonApi.onMalwareDetected$separatedMessageChannelSuffix" - val channel = BasicMessageChannel(binaryMessenger, channelName, codec) - channel.send(listOf(packageInfoArg)) { - if (it is List<*>) { - if (it.size > 1) { - callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) - } else { - callback(Result.success(Unit)) - } - } else { - callback(Result.failure(createConnectionError(channelName))) - } - } - } - fun onOngoingCallDetected(stateArg: CallState, callback: (Result) -> Unit) -{ - val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" - val channelName = "dev.flutter.pigeon.freerasp.TalsecPigeonApi.onOngoingCallDetected$separatedMessageChannelSuffix" - val channel = BasicMessageChannel(binaryMessenger, channelName, codec) - channel.send(listOf(stateArg)) { - if (it is List<*>) { - if (it.size > 1) { - callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) - } else { - callback(Result.success(Unit)) - } - } else { - callback(Result.failure(createConnectionError(channelName))) - } - } - } -} diff --git a/android/src/main/kotlin/app/talsec/rasp/flutter/handlers/MethodCallHandler.kt b/android/src/main/kotlin/app/talsec/rasp/flutter/handlers/MethodCallHandler.kt index d734d7b..0bae62a 100644 --- a/android/src/main/kotlin/app/talsec/rasp/flutter/handlers/MethodCallHandler.kt +++ b/android/src/main/kotlin/app/talsec/rasp/flutter/handlers/MethodCallHandler.kt @@ -11,13 +11,9 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleOwner import app.talsec.rasp.flutter.Utils -import app.talsec.rasp.flutter.generated.TalsecPigeonApi import app.talsec.rasp.flutter.resolve import app.talsec.rasp.flutter.runResultCatching -import app.talsec.rasp.flutter.toPigeon -import app.talsec.rasp.security.api.SuspiciousAppInfo import app.talsec.rasp.security.api.Talsec -import app.talsec.rasp.security.api.ThreatListener.CallState import io.flutter.Log import io.flutter.plugin.common.BinaryMessenger import io.flutter.plugin.common.MethodCall @@ -30,7 +26,6 @@ import io.flutter.plugin.common.MethodChannel.MethodCallHandler internal class MethodCallHandler : MethodCallHandler, LifecycleEventObserver { private var context: Context? = null private var methodChannel: MethodChannel? = null - private var talsecPigeon: TalsecPigeonApi? = null private val backgroundHandlerThread = HandlerThread("BackgroundThread").apply { start() } private val backgroundHandler = Handler(backgroundHandlerThread.looper) private val mainHandler = Handler(Looper.getMainLooper()) @@ -56,37 +51,6 @@ internal class MethodCallHandler : MethodCallHandler, LifecycleEventObserver { private const val CHANNEL_NAME: String = "talsec.app/freerasp/methods" } - private val sink = object : MethodSink { - override fun onMalwareDetected(packageInfo: List) { - context?.let { context -> - val pigeonPackageInfo = packageInfo.map { it.toPigeon(context) } - talsecPigeon?.onMalwareDetected(pigeonPackageInfo) { result -> - // Parse the result (which is Unit so we can ignore it) or throw an exception - // Exceptions are translated to Flutter errors automatically - result.getOrElse { - Log.e("MethodCallHandlerSink", "Result ended with failure") - throw it - } - } - } - } - - override fun onOngoingCall(state: CallState) { - talsecPigeon?.onOngoingCallDetected(state.toPigeon()) { result -> - result.getOrElse { - Log.e("MethodCallHandlerSink", "Result ended with failure") - throw it - } - } - } - } - - internal interface MethodSink { - fun onMalwareDetected(packageInfo: List) - - fun onOngoingCall(state: CallState) - } - /** * Creates a new [MethodChannel] with the specified [BinaryMessenger] instance. Sets this class * as the [MethodCallHandler]. @@ -106,9 +70,6 @@ internal class MethodCallHandler : MethodCallHandler, LifecycleEventObserver { } this.context = context - this.talsecPigeon = TalsecPigeonApi(messenger) - - TalsecThreatHandler.attachMethodSink(sink) } /** @@ -119,9 +80,6 @@ internal class MethodCallHandler : MethodCallHandler, LifecycleEventObserver { methodChannel = null this.context = null - this.talsecPigeon = null - - TalsecThreatHandler.detachMethodSink() } override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { diff --git a/android/src/main/kotlin/app/talsec/rasp/flutter/handlers/PluginThreatHandler.kt b/android/src/main/kotlin/app/talsec/rasp/flutter/handlers/PluginThreatHandler.kt index 46e4cd8..cda31c7 100644 --- a/android/src/main/kotlin/app/talsec/rasp/flutter/handlers/PluginThreatHandler.kt +++ b/android/src/main/kotlin/app/talsec/rasp/flutter/handlers/PluginThreatHandler.kt @@ -5,6 +5,7 @@ import app.talsec.rasp.flutter.RaspExecutionStateEvent import app.talsec.rasp.flutter.Threat import app.talsec.rasp.flutter.dispatchers.ExecutionStateDispatcher import app.talsec.rasp.flutter.dispatchers.ThreatDispatcher +import app.talsec.rasp.flutter.toFlutterMap import app.talsec.rasp.security.api.SuspiciousAppInfo import app.talsec.rasp.security.api.ThreatListener import app.talsec.rasp.security.api.ThreatListener.CallState @@ -19,6 +20,9 @@ import app.talsec.rasp.security.api.ThreatListener.ThreatDetected */ internal object PluginThreatHandler { + /** Application context, used to resolve the details of apps reported as malware. */ + private var appContext: Context? = null + private val threatDetected = object : ThreatDetected() { override fun onPrivilegedAccess() { ThreatDispatcher.dispatchThreat(Threat.PrivilegedAccess) @@ -53,7 +57,8 @@ internal object PluginThreatHandler { } override fun onMalware(suspiciousApps: List) { - ThreatDispatcher.dispatchMalware(suspiciousApps) + val context = appContext ?: return + ThreatDispatcher.dispatchMalware(suspiciousApps.map { it.toFlutterMap(context) }) } override fun onScreenshot() { @@ -132,6 +137,7 @@ internal object PluginThreatHandler { private val internalListener = ThreatListener(threatDetected, deviceState, raspExecutionState) internal fun registerListener(context: Context) { + appContext = context.applicationContext internalListener.registerListener(context) } diff --git a/android/src/main/kotlin/app/talsec/rasp/flutter/handlers/TalsecThreatHandler.kt b/android/src/main/kotlin/app/talsec/rasp/flutter/handlers/TalsecThreatHandler.kt index d33b1f9..fd20acd 100644 --- a/android/src/main/kotlin/app/talsec/rasp/flutter/handlers/TalsecThreatHandler.kt +++ b/android/src/main/kotlin/app/talsec/rasp/flutter/handlers/TalsecThreatHandler.kt @@ -133,14 +133,6 @@ internal object TalsecThreatHandler { internal fun detachExecutionStateSink() { ExecutionStateDispatcher.eventSink = null } - - internal fun attachMethodSink(sink: MethodCallHandler.MethodSink) { - ThreatDispatcher.methodSink = sink - } - - internal fun detachMethodSink() { - ThreatDispatcher.methodSink = null - } } diff --git a/example/ios/RunnerTests/RunnerTests.swift b/example/ios/RunnerTests/RunnerTests.swift index 78a684d..adf6d5f 100644 --- a/example/ios/RunnerTests/RunnerTests.swift +++ b/example/ios/RunnerTests/RunnerTests.swift @@ -14,7 +14,7 @@ class RunnerTests: XCTestCase { private let validConfig = """ {"watcherMail":"security@example.com","isProd":false,\ - "iosConfig":{"bundleIds":["app.talsec.freeraspExample"],"teamId":"ABCDE12345"}} + "iosConfig":{"appBundleIds":["app.talsec.freeraspExample"],"appTeamId":"ABCDE12345"}} """ override func setUp() { diff --git a/example/lib/main.dart b/example/lib/main.dart index e00a469..88ca173 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -38,8 +38,8 @@ Future _initializeTalsec() async { final config = TalsecConfig( androidConfig: AndroidConfig( packageName: 'com.aheaditec.freeraspExample', - signingCertHashes: ['AKoRuyLMM91E7lX/Zqp3u4jMmd0A7hH/Iqozu0TMVd0='], - supportedStores: ['com.sec.android.app.samsungapps'], + certificateHashes: ['AKoRuyLMM91E7lX/Zqp3u4jMmd0A7hH/Iqozu0TMVd0='], + supportedAlternativeStores: ['com.sec.android.app.samsungapps'], suspiciousAppDetectionConfig: const SuspiciousAppDetectionConfig( packageNames: ['com.aheaditec.freeraspExample'], requestedPermissions: [ @@ -53,8 +53,8 @@ Future _initializeTalsec() async { ), ), iosConfig: IOSConfig( - bundleIds: ['com.aheaditec.freeraspExample'], - teamId: 'M8AK35...', + appBundleIds: ['com.aheaditec.freeraspExample'], + appTeamId: 'M8AK35...', ), watcherMail: 'your_mail@example.com', isProd: true, diff --git a/example/lib/threat_notifier.dart b/example/lib/threat_notifier.dart index 29baec0..acbdaa4 100644 --- a/example/lib/threat_notifier.dart +++ b/example/lib/threat_notifier.dart @@ -16,7 +16,7 @@ class ThreatNotifier extends AutoDisposeNotifier { onHooks: () => _updateThreat(Threat.hooks), onDebug: () => _updateThreat(Threat.debug), onPasscode: () => _updateThreat(Threat.passcode), - onDeviceID: () => _updateThreat(Threat.deviceId), + onDeviceID: () => _updateThreat(Threat.deviceID), onSimulator: () => _updateThreat(Threat.simulator), onAppIntegrity: () => _updateThreat(Threat.appIntegrity), onObfuscationIssues: () => _updateThreat(Threat.obfuscationIssues), @@ -27,11 +27,11 @@ class ThreatNotifier extends AutoDisposeNotifier { _updateThreat(Threat.secureHardwareNotAvailable), onSystemVPN: () => _updateThreat(Threat.systemVPN), onDevMode: () => _updateThreat(Threat.devMode), - onADBEnabled: () => _updateThreat(Threat.adbEnabled), + onAdbEnabled: () => _updateThreat(Threat.adbEnabled), onScreenshot: () => _updateThreat(Threat.screenshot), onScreenRecording: () => _updateThreat(Threat.screenRecording), onMultiInstance: () => _updateThreat(Threat.multiInstance), - onUnsecureWiFi: () => _updateThreat(Threat.unsecureWiFi), + onUnsecureWifi: () => _updateThreat(Threat.unsecureWifi), onTimeSpoofing: () => _updateThreat(Threat.timeSpoofing), onLocationSpoofing: () => _updateThreat(Threat.locationSpoofing), onAutomation: () => _updateThreat(Threat.automation), @@ -52,8 +52,8 @@ class ThreatNotifier extends AutoDisposeNotifier { state = state.copyWith(detectedThreats: {...state.detectedThreats, threat}); } - void _updateMalware(List malware) { - state = state.copyWith(detectedMalware: malware.nonNulls.toList()); + void _updateMalware(List malware) { + state = state.copyWith(detectedMalware: malware); } void _updateChecksStatus() { diff --git a/ios/freerasp/Sources/FlutterTalsecConfig.swift b/ios/freerasp/Sources/FlutterTalsecConfig.swift index 6d261b0..20e525e 100644 --- a/ios/freerasp/Sources/FlutterTalsecConfig.swift +++ b/ios/freerasp/Sources/FlutterTalsecConfig.swift @@ -7,11 +7,11 @@ struct FlutterTalsecConfig : Decodable { let isProd: Bool func toNativeConfig() -> TalsecConfig { - return TalsecConfig(appBundleIds: iosConfig.bundleIds, appTeamId: iosConfig.teamId, watcherMailAddress: watcherMail, isProd: isProd) + return TalsecConfig(appBundleIds: iosConfig.appBundleIds, appTeamId: iosConfig.appTeamId, watcherMailAddress: watcherMail, isProd: isProd) } } struct IOSConfig : Decodable { - let bundleIds: [String] - let teamId: String + let appBundleIds: [String] + let appTeamId: String } diff --git a/lib/freerasp.dart b/lib/freerasp.dart index 010db3d..cd681e3 100644 --- a/lib/freerasp.dart +++ b/lib/freerasp.dart @@ -2,9 +2,7 @@ export 'src/callbacks/rasp_execution_state_callback.dart'; export 'src/callbacks/threat_callback.dart'; export 'src/enums/enums.dart'; export 'src/errors/errors.dart'; -export 'src/generated/talsec_pigeon_api.g.dart' - show CallState, PackageInfo, SuspiciousAppInfo; export 'src/models/models.dart'; -export 'src/talsec.dart'; +export 'src/talsec.dart' hide createTalsecForTesting; export 'src/typedefs.dart'; export 'src/utils/utils.dart' hide ConfigVerifier; diff --git a/lib/src/callbacks/threat_callback.dart b/lib/src/callbacks/threat_callback.dart index af10e85..e612b0e 100644 --- a/lib/src/callbacks/threat_callback.dart +++ b/lib/src/callbacks/threat_callback.dart @@ -1,4 +1,3 @@ -import 'package:freerasp/src/generated/talsec_pigeon_api.g.dart'; import 'package:freerasp/src/typedefs.dart'; /// A class which represents a set of callbacks that are used to notify the @@ -18,7 +17,7 @@ import 'package:freerasp/src/typedefs.dart'; /// // Attaching callback to Talsec /// Talsec.instance.attachListener(callback); /// ``` -class ThreatCallback extends TalsecPigeonApi { +class ThreatCallback { /// Constructs a [ThreatCallback] instance. ThreatCallback({ this.onHooks, @@ -34,12 +33,12 @@ class ThreatCallback extends TalsecPigeonApi { this.onSecureHardwareNotAvailable, this.onSystemVPN, this.onDevMode, - this.onADBEnabled, + this.onAdbEnabled, this.onMalware, this.onScreenshot, this.onScreenRecording, this.onMultiInstance, - this.onUnsecureWiFi, + this.onUnsecureWifi, this.onTimeSpoofing, this.onLocationSpoofing, this.onAutomation, @@ -94,7 +93,7 @@ class ThreatCallback extends TalsecPigeonApi { final VoidCallback? onDevMode; /// This method is called when the device has active ADB (Android Debug) - final VoidCallback? onADBEnabled; + final VoidCallback? onAdbEnabled; /// This method is called when screenshot detected final VoidCallback? onScreenshot; @@ -106,7 +105,7 @@ class ThreatCallback extends TalsecPigeonApi { final VoidCallback? onMultiInstance; /// This method is called when unsecure WiFi is detected - final VoidCallback? onUnsecureWiFi; + final VoidCallback? onUnsecureWifi; /// This method is called when time manipulation is detected final VoidCallback? onTimeSpoofing; @@ -123,21 +122,11 @@ class ThreatCallback extends TalsecPigeonApi { /// Android only final VoidCallback? onBootloader; - @override - void onMalwareDetected(List packageInfo) { - onMalware?.call(packageInfo); - } - /// This method is called when malware is detected. /// /// Android only final MalwareCallback? onMalware; - @override - void onOngoingCallDetected(CallState state) { - onOngoingCall?.call(state); - } - /// This method is called when an ongoing call (cellular or VoIP) starts, /// is already in progress, or stops while ongoing call detection is /// running. diff --git a/lib/src/enums/call_state.dart b/lib/src/enums/call_state.dart new file mode 100644 index 0000000..6264fd4 --- /dev/null +++ b/lib/src/enums/call_state.dart @@ -0,0 +1,13 @@ +/// State of an ongoing call reported by the ongoing call detection. +/// +/// Android only +enum CallState { + /// A call started while the detection was running. + started, + + /// A call was already in progress when the detection started. + ongoing, + + /// A previously reported call has ended. + stopped, +} diff --git a/lib/src/enums/enums.dart b/lib/src/enums/enums.dart index ec737bf..f2b5d15 100644 --- a/lib/src/enums/enums.dart +++ b/lib/src/enums/enums.dart @@ -1,2 +1,3 @@ +export 'call_state.dart'; export 'rasp_execution_state.dart'; export 'threat.dart'; diff --git a/lib/src/enums/threat.dart b/lib/src/enums/threat.dart index 316a199..b7f5e21 100644 --- a/lib/src/enums/threat.dart +++ b/lib/src/enums/threat.dart @@ -1,5 +1,3 @@ -import 'dart:io'; - /// Possible threats which can occur in Flutter app. enum Threat { /// The application is being hooked (e.g. Frida), indicating that the app is @@ -17,7 +15,7 @@ enum Threat { /// The application was reinstalled. /// /// iOS only. - deviceId, + deviceID, /// The application is running on a simulator, potentially indicating that /// it is being analyzed or reverse engineered. @@ -77,7 +75,7 @@ enum Threat { /// or weak security protocol. /// /// Android only - unsecureWiFi, + unsecureWifi, /// This method is called when the device time is manipulated timeSpoofing, @@ -102,8 +100,8 @@ enum Threat { extension ThreatX on Threat { /// Converts a [int] code to an instance of the [Threat] enum. /// - /// Kills an app if the given int [code] does not match any of the predefined - /// values for the [Threat] enum. + /// Throws an [ArgumentError] if the given int [code] does not match any of + /// the predefined values for the [Threat] enum. /// /// Example usage: /// @@ -111,12 +109,11 @@ extension ThreatX on Threat { /// final threat = ThreatX.fromInt(1268968002); // Threat.debug /// ``` /// - /// Shared error codes: + /// Threat codes: /// * 1268968002 - debug /// * 209533833 - hooks - /// * 55943254 - overlay /// * 1293399086 - passcode - /// * 1514211414 - deviceId + /// * 1514211414 - deviceID /// * 477190884 - simulator /// * 1115787534 - appIntegrity /// * 1001443554 - obfuscationIssues @@ -130,6 +127,9 @@ extension ThreatX on Threat { /// * 705651459 - screenshot /// * 64690214 - screenRecording /// * 859307284 - multiInstance + /// * 363588890 - unsecureWifi + /// * 189105221 - timeSpoofing + /// * 653273273 - locationSpoofing /// * 298453120 - automation /// * 1651076936 - bootloader static Threat fromInt(int code) { @@ -141,7 +141,7 @@ extension ThreatX on Threat { case 1293399086: return Threat.passcode; case 1514211414: - return Threat.deviceId; + return Threat.deviceID; case 477190884: return Threat.simulator; case 1115787534: @@ -169,7 +169,7 @@ extension ThreatX on Threat { case 859307284: return Threat.multiInstance; case 363588890: - return Threat.unsecureWiFi; + return Threat.unsecureWifi; case 189105221: return Threat.timeSpoofing; case 653273273: @@ -179,8 +179,7 @@ extension ThreatX on Threat { case 1651076936: return Threat.bootloader; default: - // Unknown data came from native code. This shouldn't normally happen. - exit(127); + throw ArgumentError('Unknown threat code: $code'); } } } diff --git a/lib/src/errors/errors.dart b/lib/src/errors/errors.dart index 1d47b6b..31f6a55 100644 --- a/lib/src/errors/errors.dart +++ b/lib/src/errors/errors.dart @@ -1,2 +1,5 @@ export 'configuration_exception.dart'; +export 'external_id_failure_exception.dart'; +export 'malware_failure_exception.dart'; export 'talsec_exception.dart'; +export 'unsupported_platform_exception.dart'; diff --git a/lib/src/errors/malware_failure_exception.dart b/lib/src/errors/malware_failure_exception.dart index f6cc147..a4a63f7 100644 --- a/lib/src/errors/malware_failure_exception.dart +++ b/lib/src/errors/malware_failure_exception.dart @@ -1,3 +1,4 @@ +import 'package:flutter/services.dart'; import 'package:freerasp/freerasp.dart'; /// An exception that is thrown when there is a failure in malware data parsing. @@ -9,4 +10,9 @@ class MalwareFailureException extends TalsecException { /// Constructs an instance of [MalwareFailureException]. const MalwareFailureException({super.message, super.stackTrace}) : super(code: 'malware-failure'); + + /// Constructs an instance of [MalwareFailureException] from a + /// [PlatformException]. + MalwareFailureException.fromPlatformException(PlatformException exception) + : this(message: exception.message, stackTrace: exception.stacktrace); } diff --git a/lib/src/errors/unsupported_platform_exception.dart b/lib/src/errors/unsupported_platform_exception.dart new file mode 100644 index 0000000..c4cf120 --- /dev/null +++ b/lib/src/errors/unsupported_platform_exception.dart @@ -0,0 +1,13 @@ +import 'package:freerasp/freerasp.dart'; + +/// An exception that is thrown when a method is not supported on the current +/// platform (e.g. an Android-only method called on iOS). +/// +/// This exception extends the [TalsecException] class and includes an error +/// code of 'unsupported-platform'. It can include an optional [message] and +/// [stackTrace] parameter. +class UnsupportedPlatformException extends TalsecException { + /// Constructs an instance of [UnsupportedPlatformException]. + const UnsupportedPlatformException({super.message, super.stackTrace}) + : super(code: 'unsupported-platform'); +} diff --git a/lib/src/generated/talsec_pigeon_api.g.dart b/lib/src/generated/talsec_pigeon_api.g.dart deleted file mode 100644 index 7e5215a..0000000 --- a/lib/src/generated/talsec_pigeon_api.g.dart +++ /dev/null @@ -1,217 +0,0 @@ -// Autogenerated from Pigeon (v22.7.4), do not edit directly. -// See also: https://pub.dev/packages/pigeon -// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers - -import 'dart:async'; -import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List; - -import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer; -import 'package:flutter/services.dart'; - -List wrapResponse( - {Object? result, PlatformException? error, bool empty = false}) { - if (empty) { - return []; - } - if (error == null) { - return [result]; - } - return [error.code, error.message, error.details]; -} - -/// State of an ongoing call reported by the ongoing call detection. -enum CallState { - /// A call started while the detection was running. - started, - - /// A call was already in progress when the detection started. - ongoing, - - /// A previously reported call has ended. - stopped, -} - -class PackageInfo { - PackageInfo({ - required this.packageName, - this.appIcon, - this.appName, - this.version, - this.installerStore, - }); - - String packageName; - - String? appIcon; - - String? appName; - - String? version; - - String? installerStore; - - Object encode() { - return [ - packageName, - appIcon, - appName, - version, - installerStore, - ]; - } - - static PackageInfo decode(Object result) { - result as List; - return PackageInfo( - packageName: result[0]! as String, - appIcon: result[1] as String?, - appName: result[2] as String?, - version: result[3] as String?, - installerStore: result[4] as String?, - ); - } -} - -class SuspiciousAppInfo { - SuspiciousAppInfo({ - required this.packageInfo, - required this.reasons, - this.permissions, - }); - - PackageInfo packageInfo; - - List reasons; - - List? permissions; - - Object encode() { - return [ - packageInfo, - reasons, - permissions, - ]; - } - - static SuspiciousAppInfo decode(Object result) { - result as List; - return SuspiciousAppInfo( - packageInfo: result[0]! as PackageInfo, - reasons: (result[1] as List?)!.cast(), - permissions: (result[2] as List?)?.cast(), - ); - } -} - -class _PigeonCodec extends StandardMessageCodec { - const _PigeonCodec(); - @override - void writeValue(WriteBuffer buffer, Object? value) { - if (value is int) { - buffer.putUint8(4); - buffer.putInt64(value); - } else if (value is CallState) { - buffer.putUint8(129); - writeValue(buffer, value.index); - } else if (value is PackageInfo) { - buffer.putUint8(130); - writeValue(buffer, value.encode()); - } else if (value is SuspiciousAppInfo) { - buffer.putUint8(131); - writeValue(buffer, value.encode()); - } else { - super.writeValue(buffer, value); - } - } - - @override - Object? readValueOfType(int type, ReadBuffer buffer) { - switch (type) { - case 129: - final int? value = readValue(buffer) as int?; - return value == null ? null : CallState.values[value]; - case 130: - return PackageInfo.decode(readValue(buffer)!); - case 131: - return SuspiciousAppInfo.decode(readValue(buffer)!); - default: - return super.readValueOfType(type, buffer); - } - } -} - -abstract class TalsecPigeonApi { - static const MessageCodec pigeonChannelCodec = _PigeonCodec(); - - void onMalwareDetected(List packageInfo); - - void onOngoingCallDetected(CallState state); - - static void setUp( - TalsecPigeonApi? api, { - BinaryMessenger? binaryMessenger, - String messageChannelSuffix = '', - }) { - messageChannelSuffix = - messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; - { - final BasicMessageChannel< - Object?> pigeonVar_channel = BasicMessageChannel< - Object?>( - 'dev.flutter.pigeon.freerasp.TalsecPigeonApi.onMalwareDetected$messageChannelSuffix', - pigeonChannelCodec, - binaryMessenger: binaryMessenger); - if (api == null) { - pigeonVar_channel.setMessageHandler(null); - } else { - pigeonVar_channel.setMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.freerasp.TalsecPigeonApi.onMalwareDetected was null.'); - final List args = (message as List?)!; - final List? arg_packageInfo = - (args[0] as List?)?.cast(); - assert(arg_packageInfo != null, - 'Argument for dev.flutter.pigeon.freerasp.TalsecPigeonApi.onMalwareDetected was null, expected non-null List.'); - try { - api.onMalwareDetected(arg_packageInfo!); - return wrapResponse(empty: true); - } on PlatformException catch (e) { - return wrapResponse(error: e); - } catch (e) { - return wrapResponse( - error: PlatformException(code: 'error', message: e.toString())); - } - }); - } - } - { - final BasicMessageChannel< - Object?> pigeonVar_channel = BasicMessageChannel< - Object?>( - 'dev.flutter.pigeon.freerasp.TalsecPigeonApi.onOngoingCallDetected$messageChannelSuffix', - pigeonChannelCodec, - binaryMessenger: binaryMessenger); - if (api == null) { - pigeonVar_channel.setMessageHandler(null); - } else { - pigeonVar_channel.setMessageHandler((Object? message) async { - assert(message != null, - 'Argument for dev.flutter.pigeon.freerasp.TalsecPigeonApi.onOngoingCallDetected was null.'); - final List args = (message as List?)!; - final CallState? arg_state = (args[0] as CallState?); - assert(arg_state != null, - 'Argument for dev.flutter.pigeon.freerasp.TalsecPigeonApi.onOngoingCallDetected was null, expected non-null CallState.'); - try { - api.onOngoingCallDetected(arg_state!); - return wrapResponse(empty: true); - } on PlatformException catch (e) { - return wrapResponse(error: e); - } catch (e) { - return wrapResponse( - error: PlatformException(code: 'error', message: e.toString())); - } - }); - } - } - } -} diff --git a/lib/src/models/android_config.dart b/lib/src/models/android_config.dart index 5543259..3ec4d87 100644 --- a/lib/src/models/android_config.dart +++ b/lib/src/models/android_config.dart @@ -10,8 +10,8 @@ class AndroidConfig { /// Android configuration fields AndroidConfig({ required this.packageName, - required this.signingCertHashes, - this.supportedStores = const [], + required this.certificateHashes, + this.supportedAlternativeStores = const [], this.suspiciousAppDetectionConfig, }) { ConfigVerifier.verifyAndroid(this); @@ -28,10 +28,10 @@ class AndroidConfig { final String packageName; /// List of expected signing hashes. - final List signingCertHashes; + final List certificateHashes; /// List of supported sources where application can be installed from. - final List supportedStores; + final List supportedAlternativeStores; /// Suspicious app detection configuration for Android. final SuspiciousAppDetectionConfig? suspiciousAppDetectionConfig; diff --git a/lib/src/models/android_config.g.dart b/lib/src/models/android_config.g.dart index 43cb86f..2550b85 100644 --- a/lib/src/models/android_config.g.dart +++ b/lib/src/models/android_config.g.dart @@ -9,13 +9,14 @@ part of 'android_config.dart'; AndroidConfig _$AndroidConfigFromJson(Map json) => AndroidConfig( packageName: json['packageName'] as String, - signingCertHashes: (json['signingCertHashes'] as List) + certificateHashes: (json['certificateHashes'] as List) .map((e) => e as String) .toList(), - supportedStores: (json['supportedStores'] as List?) - ?.map((e) => e as String) - .toList() ?? - const [], + supportedAlternativeStores: + (json['supportedAlternativeStores'] as List?) + ?.map((e) => e as String) + .toList() ?? + const [], suspiciousAppDetectionConfig: json['suspiciousAppDetectionConfig'] == null ? null : SuspiciousAppDetectionConfig.fromJson( @@ -25,8 +26,8 @@ AndroidConfig _$AndroidConfigFromJson(Map json) => Map _$AndroidConfigToJson(AndroidConfig instance) { final val = { 'packageName': instance.packageName, - 'signingCertHashes': instance.signingCertHashes, - 'supportedStores': instance.supportedStores, + 'certificateHashes': instance.certificateHashes, + 'supportedAlternativeStores': instance.supportedAlternativeStores, }; void writeNotNull(String key, dynamic value) { diff --git a/lib/src/models/ios_config.dart b/lib/src/models/ios_config.dart index f71a7c6..0dffdde 100644 --- a/lib/src/models/ios_config.dart +++ b/lib/src/models/ios_config.dart @@ -8,8 +8,8 @@ part 'ios_config.g.dart'; class IOSConfig { /// iOS configuration fields IOSConfig({ - required this.bundleIds, - required this.teamId, + required this.appBundleIds, + required this.appTeamId, }) { ConfigVerifier.verifyIOS(this); } @@ -22,8 +22,8 @@ class IOSConfig { Map toJson() => _$IOSConfigToJson(this); /// Bundle ID of the app. - final List bundleIds; + final List appBundleIds; /// Team ID of the app. - final String teamId; + final String appTeamId; } diff --git a/lib/src/models/ios_config.g.dart b/lib/src/models/ios_config.g.dart index 11a9f13..ce5081b 100644 --- a/lib/src/models/ios_config.g.dart +++ b/lib/src/models/ios_config.g.dart @@ -7,12 +7,13 @@ part of 'ios_config.dart'; // ************************************************************************** IOSConfig _$IOSConfigFromJson(Map json) => IOSConfig( - bundleIds: - (json['bundleIds'] as List).map((e) => e as String).toList(), - teamId: json['teamId'] as String, + appBundleIds: (json['appBundleIds'] as List) + .map((e) => e as String) + .toList(), + appTeamId: json['appTeamId'] as String, ); Map _$IOSConfigToJson(IOSConfig instance) => { - 'bundleIds': instance.bundleIds, - 'teamId': instance.teamId, + 'appBundleIds': instance.appBundleIds, + 'appTeamId': instance.appTeamId, }; diff --git a/lib/src/models/models.dart b/lib/src/models/models.dart index 2db18eb..1ff1a87 100644 --- a/lib/src/models/models.dart +++ b/lib/src/models/models.dart @@ -1,4 +1,6 @@ export 'android_config.dart'; export 'ios_config.dart'; +export 'package_info.dart'; export 'suspicious_app_detection_config.dart'; +export 'suspicious_app_info.dart'; export 'talsec_config.dart'; diff --git a/lib/src/models/package_info.dart b/lib/src/models/package_info.dart new file mode 100644 index 0000000..6916892 --- /dev/null +++ b/lib/src/models/package_info.dart @@ -0,0 +1,62 @@ +import 'package:meta/meta.dart'; + +/// Information about an installed application. +@immutable +class PackageInfo { + /// Creates a [PackageInfo]. + const PackageInfo({ + required this.packageName, + this.appIcon, + this.appName, + this.version, + this.installerStore, + }); + + /// Creates a [PackageInfo] from the map sent by the native platform. + /// + /// Throws if the map does not have the expected shape. + @internal + factory PackageInfo.fromMap(Map map) { + return PackageInfo( + packageName: map['packageName']! as String, + appIcon: map['appIcon'] as String?, + appName: map['appName'] as String?, + version: map['version'] as String?, + installerStore: map['installerStore'] as String?, + ); + } + + /// Package name of the application, e.g. `com.example.app`. + final String packageName; + + /// Application icon as a Base64-encoded PNG. + /// + /// Not sent with malware detections; use `Talsec.instance.getAppIcon()`. + final String? appIcon; + + /// User-visible name of the application. + final String? appName; + + /// Version code of the application. + final String? version; + + /// Package name of the store that installed the application. + final String? installerStore; + + @override + bool operator ==(Object other) => + other is PackageInfo && + other.packageName == packageName && + other.appIcon == appIcon && + other.appName == appName && + other.version == version && + other.installerStore == installerStore; + + @override + int get hashCode => + Object.hash(packageName, appIcon, appName, version, installerStore); + + @override + String toString() => 'PackageInfo(packageName: $packageName, ' + 'appName: $appName, version: $version, installerStore: $installerStore)'; +} diff --git a/lib/src/models/suspicious_app_info.dart b/lib/src/models/suspicious_app_info.dart new file mode 100644 index 0000000..972d196 --- /dev/null +++ b/lib/src/models/suspicious_app_info.dart @@ -0,0 +1,63 @@ +import 'package:freerasp/src/models/package_info.dart'; +import 'package:meta/meta.dart'; + +/// An application reported by the malware detection. +@immutable +class SuspiciousAppInfo { + /// Creates a [SuspiciousAppInfo]. + const SuspiciousAppInfo({ + required this.packageInfo, + required this.reasons, + this.permissions, + }); + + /// Creates a [SuspiciousAppInfo] from the map sent by the native platform. + /// + /// Throws if the map does not have the expected shape. + @internal + factory SuspiciousAppInfo.fromMap(Map map) { + return SuspiciousAppInfo( + packageInfo: + PackageInfo.fromMap(map['packageInfo']! as Map), + reasons: (map['reasons']! as List).cast().toList(), + permissions: + (map['permissions'] as List?)?.cast().toList(), + ); + } + + /// The application that was reported. + final PackageInfo packageInfo; + + /// Why the application was reported, e.g. `blacklist` or `permission`. + final List reasons; + + /// Permissions of the application that caused the report, if any. + final List? permissions; + + @override + bool operator ==(Object other) => + other is SuspiciousAppInfo && + other.packageInfo == packageInfo && + _listEquals(other.reasons, reasons) && + _listEquals(other.permissions, permissions); + + @override + int get hashCode => Object.hash( + packageInfo, + Object.hashAll(reasons), + permissions == null ? null : Object.hashAll(permissions!), + ); + + @override + String toString() => 'SuspiciousAppInfo(packageInfo: $packageInfo, ' + 'reasons: $reasons, permissions: $permissions)'; +} + +bool _listEquals(List? a, List? b) { + if (identical(a, b)) return true; + if (a == null || b == null || a.length != b.length) return false; + for (var i = 0; i < a.length; i++) { + if (a[i] != b[i]) return false; + } + return true; +} diff --git a/lib/src/talsec.dart b/lib/src/talsec.dart index bb37117..2fbe3d7 100644 --- a/lib/src/talsec.dart +++ b/lib/src/talsec.dart @@ -5,9 +5,18 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:freerasp/freerasp.dart'; -import 'package:freerasp/src/errors/external_id_failure_exception.dart'; -import 'package:freerasp/src/errors/malware_failure_exception.dart'; -import 'package:freerasp/src/generated/talsec_pigeon_api.g.dart' as pigeon; + +/// Creates a [Talsec] instance that communicates over the given channels. +/// +/// For tests of this package only; it is not exported from +/// `package:freerasp/freerasp.dart`. +@visibleForTesting +Talsec createTalsecForTesting( + MethodChannel methodChannel, + EventChannel eventChannel, + EventChannel executionStateChannel, +) => + Talsec._(methodChannel, eventChannel, executionStateChannel); /// A class which maintains all security related operations. /// @@ -29,59 +38,55 @@ import 'package:freerasp/src/generated/talsec_pigeon_api.g.dart' as pigeon; /// singleton is initialized. However, subsequent calls to the instance getter /// will return the same singleton instance without any additional delay. class Talsec { - /// Private constructor for internal and testing purposes. - @visibleForTesting - Talsec.private( - this.methodChannel, - this.eventChannel, - this.executionStateChannel, + Talsec._( + this._methodChannel, + this._eventChannel, + this._executionStateChannel, ); /// Named channel used to communicate with platform plugins. /// /// When threat is detected on platform side, [int] is sent which is then /// transformed into [Threat] using [ThreatX.fromInt]. - static const EventChannel _eventChannel = + static const EventChannel _defaultEventChannel = EventChannel('talsec.app/freerasp/events'); /// Named channel used to communicate with platform plugins. /// /// Transforms data and function calls such as [start]. - static const MethodChannel _methodChannel = + static const MethodChannel _defaultMethodChannel = MethodChannel('talsec.app/freerasp/methods'); /// Named channel used to communicate with platform plugins. /// /// Stream of execution state events. - static const EventChannel _executionStateChannel = + static const EventChannel _defaultExecutionStateChannel = EventChannel('talsec.app/freerasp/execution_state'); /// Private [Talsec] variable which holds current instance of class. - static final _instance = Talsec.private( - _methodChannel, - _eventChannel, - _executionStateChannel, + static final _instance = Talsec._( + _defaultMethodChannel, + _defaultEventChannel, + _defaultExecutionStateChannel, ); /// Initialize Talsec lazily/obtain current instance of Talsec. static Talsec get instance => _instance; /// [MethodChannel] used to interact with native platform. - @visibleForTesting - late final MethodChannel methodChannel; + final MethodChannel _methodChannel; /// [EventChannel] used to receive Threats from the native platform. - @visibleForTesting - late final EventChannel eventChannel; + final EventChannel _eventChannel; /// [EventChannel] used to receive execution state events from the native /// platform. - @visibleForTesting - late final EventChannel executionStateChannel; + final EventChannel _executionStateChannel; - StreamSubscription? _streamSubscription; + StreamSubscription? _streamSubscription; StreamSubscription? _executionStateSubscription; + Stream? _events; Stream? _onThreatDetected; Stream? _onRaspExecutionState; @@ -113,15 +118,19 @@ class Talsec { return _onThreatDetected!; } - _onThreatDetected = eventChannel - .receiveBroadcastStream() + _onThreatDetected = _threatEvents + .where((event) => event is int) .cast() - .map(ThreatX.fromInt) - .handleError(_handleStreamError); + .map((code) => _decodeOrAbort(code, ThreatX.fromInt)); return _onThreatDetected!; } + /// Broadcast stream of all events of the threat channel: an [int] threat + /// code, or a map with the payload of a malware or ongoing call detection. + Stream get _threatEvents => _events ??= + _eventChannel.receiveBroadcastStream().handleError(_handleStreamError); + /// Returns a broadcast stream. When RASP execution state changes /// [onRaspExecutionState] receives what state it is. Stream get onRaspExecutionState { @@ -129,22 +138,29 @@ class Talsec { return _onRaspExecutionState!; } - _onRaspExecutionState = executionStateChannel + _onRaspExecutionState = _executionStateChannel .receiveBroadcastStream() .cast() - .map(RaspExecutionStateX.fromInt) + .map((code) => _decodeOrAbort(code, RaspExecutionStateX.fromInt)) .handleError(_handleStreamError); return _onRaspExecutionState!; } /// Starts freeRASP with configuration provided in [config]. - Future start(TalsecConfig config) { + /// + /// Throws a [ConfigurationException] when the config for the current + /// platform is missing, or a [TalsecException] when the start fails. + Future start(TalsecConfig config) async { _checkConfig(config); - return methodChannel.invokeMethod( - 'start', - {'config': jsonEncode(config.toJson())}, - ); + try { + await _methodChannel.invokeMethod( + 'start', + {'config': jsonEncode(config.toJson())}, + ); + } on PlatformException catch (e) { + throw TalsecException.fromPlatformException(e); + } } /// Adds [packageName] to the whitelist. @@ -157,16 +173,15 @@ class Talsec { /// whitelist, you need to remove application data or reinstall the /// application. Future addToWhitelist(String packageName) async { - if (!Platform.isAndroid) { - throw UnimplementedError( - 'Platform is not supported: $defaultTargetPlatform}', + _checkAndroid(); + try { + await _methodChannel.invokeMethod( + 'addToWhitelist', + {'packageName': packageName}, ); + } on PlatformException catch (e) { + throw MalwareFailureException.fromPlatformException(e); } - - return methodChannel.invokeMethod( - 'addToWhitelist', - {'packageName': packageName}, - ); } /// Prevents the screen from being captured. @@ -180,10 +195,10 @@ class Talsec { /// Future blockScreenCapture({required bool enabled}) async { try { - await methodChannel + await _methodChannel .invokeMethod('blockScreenCapture', {'enable': enabled}); } on PlatformException catch (e) { - throw Exception('Failed to call block screen capture: ${e.message}'); + throw TalsecException.fromPlatformException(e); } } @@ -197,7 +212,7 @@ class Talsec { Future isScreenCaptureBlocked() async { try { final result = - await methodChannel.invokeMethod('isScreenCaptureBlocked'); + await _methodChannel.invokeMethod('isScreenCaptureBlocked'); if (result is! bool) { throw const TalsecException(message: 'Screen capture state failed.'); @@ -205,7 +220,7 @@ class Talsec { return result; } on PlatformException catch (e) { - throw Exception('Failed to check screen capture state: ${e.message}'); + throw TalsecException.fromPlatformException(e); } } @@ -219,7 +234,7 @@ class Talsec { Future blockScreenControl({required bool enabled}) async { _checkAndroid(); try { - await methodChannel + await _methodChannel .invokeMethod('blockScreenControl', {'enable': enabled}); } on PlatformException catch (e) { throw TalsecException.fromPlatformException(e); @@ -278,7 +293,7 @@ class Talsec { Future startOngoingCallDetection() async { _checkAndroid(); try { - await methodChannel.invokeMethod('startOngoingCallDetection'); + await _methodChannel.invokeMethod('startOngoingCallDetection'); } on PlatformException catch (e) { throw TalsecException.fromPlatformException(e); } @@ -290,7 +305,7 @@ class Talsec { Future stopOngoingCallDetection() async { _checkAndroid(); try { - await methodChannel.invokeMethod('stopOngoingCallDetection'); + await _methodChannel.invokeMethod('stopOngoingCallDetection'); } on PlatformException catch (e) { throw TalsecException.fromPlatformException(e); } @@ -301,7 +316,7 @@ class Talsec { Map? arguments, ]) async { try { - final result = await methodChannel.invokeMethod(method, arguments); + final result = await _methodChannel.invokeMethod(method, arguments); if (result is! bool) { throw TalsecException(message: '$method returned no result.'); @@ -315,8 +330,8 @@ class Talsec { void _checkAndroid() { if (!Platform.isAndroid) { - throw UnimplementedError( - 'Platform is not supported: $defaultTargetPlatform', + throw UnsupportedPlatformException( + message: 'Platform is not supported: $defaultTargetPlatform', ); } } @@ -327,7 +342,8 @@ class Talsec { /// Throws a [ExternalIdFailureException] when storing failed. Future storeExternalId(String data) async { try { - await methodChannel.invokeMethod('storeExternalId', {'data': data}); + await _methodChannel + .invokeMethod('storeExternalId', {'data': data}); } on PlatformException catch (e) { throw ExternalIdFailureException.fromPlatformException(e); } @@ -338,7 +354,7 @@ class Talsec { /// Throws a [ExternalIdFailureException] when removing failed. Future removeExternalId() async { try { - await methodChannel.invokeMethod('removeExternalId'); + await _methodChannel.invokeMethod('removeExternalId'); } on PlatformException catch (e) { throw ExternalIdFailureException.fromPlatformException(e); } @@ -362,7 +378,9 @@ class Talsec { case TargetPlatform.linux: case TargetPlatform.macOS: case TargetPlatform.windows: - throw UnimplementedError('Platform is not supported'); + throw UnsupportedPlatformException( + message: 'Platform is not supported: $defaultTargetPlatform', + ); } } @@ -370,63 +388,105 @@ class Talsec { /// already attached, current one will be detached and replaced with /// [callback]. /// - /// When invoked, functions starts listening to [onThreatDetected] and turns - /// stream events into function callbacks of [ThreatCallback]. + /// When invoked, functions starts listening to the threat events and turns + /// them into function callbacks of [ThreatCallback], in the order in which + /// they were detected. /// /// When threat is detected, respective callback of [ThreatCallback] is /// invoked. Future attachListener(ThreatCallback callback) async { - pigeon.TalsecPigeonApi.setUp(callback); - await detachListener(); - _streamSubscription ??= onThreatDetected.listen((event) { - switch (event) { - case Threat.hooks: - callback.onHooks?.call(); - case Threat.debug: - callback.onDebug?.call(); - case Threat.passcode: - callback.onPasscode?.call(); - case Threat.deviceId: - callback.onDeviceID?.call(); - case Threat.simulator: - callback.onSimulator?.call(); - case Threat.appIntegrity: - callback.onAppIntegrity?.call(); - case Threat.obfuscationIssues: - callback.onObfuscationIssues?.call(); - case Threat.deviceBinding: - callback.onDeviceBinding?.call(); - case Threat.unofficialStore: - callback.onUnofficialStore?.call(); - case Threat.privilegedAccess: - callback.onPrivilegedAccess?.call(); - case Threat.secureHardwareNotAvailable: - callback.onSecureHardwareNotAvailable?.call(); - case Threat.systemVPN: - callback.onSystemVPN?.call(); - case Threat.devMode: - callback.onDevMode?.call(); - case Threat.adbEnabled: - callback.onADBEnabled?.call(); - case Threat.screenshot: - callback.onScreenshot?.call(); - case Threat.screenRecording: - callback.onScreenRecording?.call(); - case Threat.multiInstance: - callback.onMultiInstance?.call(); - case Threat.unsecureWiFi: - callback.onUnsecureWiFi?.call(); - case Threat.timeSpoofing: - callback.onTimeSpoofing?.call(); - case Threat.locationSpoofing: - callback.onLocationSpoofing?.call(); - case Threat.automation: - callback.onAutomation?.call(); - case Threat.bootloader: - callback.onBootloader?.call(); + _streamSubscription ??= _threatEvents.listen( + (event) => _dispatchThreatEvent(callback, event), + ); + } + + void _dispatchThreatEvent(ThreatCallback callback, Object? event) { + switch (event) { + case final int code: + _dispatchThreat(callback, _decodeOrAbort(code, ThreatX.fromInt)); + case final Map payload: + _dispatchPayload(callback, payload); + default: + // Unknown data came from the native platform. + exit(127); + } + } + + /// Delivers a malware or ongoing call detection. + /// + /// An unexpected payload is handled like an unknown threat code. + void _dispatchPayload( + ThreatCallback callback, + Map payload, + ) { + final void Function() deliver; + try { + switch (payload['type']) { + case 'malware': + final apps = (payload['suspiciousApps']! as List) + .map((app) => SuspiciousAppInfo.fromMap(app! as Map)) + .toList(); + deliver = () => callback.onMalware?.call(apps); + case 'ongoingCall': + final state = CallState.values.byName(payload['state']! as String); + deliver = () => callback.onOngoingCall?.call(state); + default: + exit(127); } - }); + } on Object { + exit(127); + } + deliver(); + } + + void _dispatchThreat(ThreatCallback callback, Threat threat) { + switch (threat) { + case Threat.hooks: + callback.onHooks?.call(); + case Threat.debug: + callback.onDebug?.call(); + case Threat.passcode: + callback.onPasscode?.call(); + case Threat.deviceID: + callback.onDeviceID?.call(); + case Threat.simulator: + callback.onSimulator?.call(); + case Threat.appIntegrity: + callback.onAppIntegrity?.call(); + case Threat.obfuscationIssues: + callback.onObfuscationIssues?.call(); + case Threat.deviceBinding: + callback.onDeviceBinding?.call(); + case Threat.unofficialStore: + callback.onUnofficialStore?.call(); + case Threat.privilegedAccess: + callback.onPrivilegedAccess?.call(); + case Threat.secureHardwareNotAvailable: + callback.onSecureHardwareNotAvailable?.call(); + case Threat.systemVPN: + callback.onSystemVPN?.call(); + case Threat.devMode: + callback.onDevMode?.call(); + case Threat.adbEnabled: + callback.onAdbEnabled?.call(); + case Threat.screenshot: + callback.onScreenshot?.call(); + case Threat.screenRecording: + callback.onScreenRecording?.call(); + case Threat.multiInstance: + callback.onMultiInstance?.call(); + case Threat.unsecureWifi: + callback.onUnsecureWifi?.call(); + case Threat.timeSpoofing: + callback.onTimeSpoofing?.call(); + case Threat.locationSpoofing: + callback.onLocationSpoofing?.call(); + case Threat.automation: + callback.onAutomation?.call(); + case Threat.bootloader: + callback.onBootloader?.call(); + } } /// Removes instance of latest [ThreatCallback]. Also cancels @@ -438,6 +498,21 @@ class Talsec { _streamSubscription = null; } + /// Decodes a [code] received from the native platform. + /// + /// An unknown code means that the communication with the native platform was + /// tampered with, so the app is terminated, like in the other freeRASP + /// plugins. + static T _decodeOrAbort(int code, T Function(int) decode) { + try { + return decode(code); + // fromInt reports an unknown code with an ArgumentError. + // ignore: avoid_catching_errors + } on ArgumentError { + exit(127); + } + } + void _handleStreamError(Object error) { if (error is PlatformException) { throw TalsecException.fromPlatformException(error); @@ -475,22 +550,17 @@ class Talsec { /// Throws a [TalsecException] with error message description if /// the app with given package name couldn't be obtained. Future getAppIcon(String packageName) async { - if (!Platform.isAndroid) { - throw UnimplementedError( - 'Platform is not supported: $defaultTargetPlatform}', - ); - } - + _checkAndroid(); try { return await _getAppIcon(packageName); } on PlatformException catch (e) { - throw TalsecException.fromPlatformException(e); + throw MalwareFailureException.fromPlatformException(e); } } Future _getAppIcon(String packageName) async { final args = {'packageName': packageName}; - final result = await methodChannel.invokeMethod( + final result = await _methodChannel.invokeMethod( 'getAppIcon', args, ); diff --git a/lib/src/typedefs.dart b/lib/src/typedefs.dart index d658d9b..a73c2e7 100644 --- a/lib/src/typedefs.dart +++ b/lib/src/typedefs.dart @@ -1,10 +1,13 @@ -import 'package:freerasp/src/generated/talsec_pigeon_api.g.dart'; +import 'package:freerasp/src/enums/call_state.dart'; +import 'package:freerasp/src/models/suspicious_app_info.dart'; /// Typedef for void methods typedef VoidCallback = void Function(); /// Typedef for malware callback -typedef MalwareCallback = void Function(List packageInfo); +typedef MalwareCallback = void Function( + List suspiciousApps, +); /// Typedef for ongoing call callback typedef OngoingCallCallback = void Function(CallState state); diff --git a/lib/src/utils/config_verifier.dart b/lib/src/utils/config_verifier.dart index e06eede..d21f150 100644 --- a/lib/src/utils/config_verifier.dart +++ b/lib/src/utils/config_verifier.dart @@ -10,7 +10,7 @@ class ConfigVerifier { /// Verifies if provided [androidConfig] is valid. /// Verification is run only if current TargetPlatform is Android. /// Verifies [AndroidConfig.packageName] presence and validity of provided - /// [AndroidConfig.signingCertHashes]. + /// [AndroidConfig.certificateHashes]. /// /// Throws [ConfigurationException] if config is invalid. static void verifyAndroid(AndroidConfig androidConfig) { @@ -24,15 +24,15 @@ class ConfigVerifier { ); } - if (androidConfig.signingCertHashes.isEmpty) { + if (androidConfig.certificateHashes.isEmpty) { throw const ConfigurationException( - message: 'signingCertHashes cannot be empty', + message: 'certificateHashes cannot be empty', ); } - _verifyHashesFormat(androidConfig.signingCertHashes); + _verifyHashesFormat(androidConfig.certificateHashes); - _verifyHashesValidity(androidConfig.signingCertHashes); + _verifyHashesValidity(androidConfig.certificateHashes); } static void _verifyHashLength(String hash) { @@ -65,7 +65,7 @@ class ConfigVerifier { /// Verifies if provided [iOSConfig] is valid. /// Verification is run only if current TargetPlatform is iOS. - /// Verifies [IOSConfig.bundleIds] and [IOSConfig.teamId] presence. + /// Verifies [IOSConfig.appBundleIds] and [IOSConfig.appTeamId] presence. /// /// Throws [ConfigurationException] if config is invalid. static void verifyIOS(IOSConfig iOSConfig) { @@ -73,13 +73,13 @@ class ConfigVerifier { return; } - if (iOSConfig.bundleIds.isEmpty) { + if (iOSConfig.appBundleIds.isEmpty) { throw const ConfigurationException( message: 'bundle IDs cannot be empty', ); } - if (iOSConfig.teamId.isEmpty) { + if (iOSConfig.appTeamId.isEmpty) { throw const ConfigurationException( message: 'team ID cannot be empty', ); diff --git a/pigeons/build.sh b/pigeons/build.sh deleted file mode 100755 index 328c92f..0000000 --- a/pigeons/build.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/zsh - -# Exit on error -set -euo pipefail - -# Variables -INPUT_FILES=("talsec_pigeon_api.dart" "rasp_execution_state.dart") - -# Check current directory -if [ "$(basename "$PWD")" != "pigeons" ]; then - echo "⚠️ Not in the /pigeons directory." - echo "⚠️ Current directory: $PWD" - echo "Run this script from the /pigeons directory" - exit 1 -fi - -# Generate Pigeon code -cd .. -for file in "${INPUT_FILES[@]}"; do - echo "🚀 Generating Pigeon code for $file..." - dart run pigeon --input "./pigeons/$file" -done diff --git a/pigeons/talsec_pigeon_api.dart b/pigeons/talsec_pigeon_api.dart deleted file mode 100644 index 29d2026..0000000 --- a/pigeons/talsec_pigeon_api.dart +++ /dev/null @@ -1,58 +0,0 @@ -import 'package:pigeon/pigeon.dart'; - -@ConfigurePigeon( - PigeonOptions( - dartOut: 'lib/src/generated/talsec_pigeon_api.g.dart', - kotlinOut: - 'android/src/main/kotlin/app/talsec/rasp/flutter/generated/TalsecPigeonApi.kt', - input: 'pigeons/talsec_pigeon_api.dart', - kotlinOptions: KotlinOptions(package: 'app.talsec.rasp.flutter.generated'), - ), -) -class PackageInfo { - const PackageInfo({ - required this.packageName, - this.appIcon, - this.version, - this.appName, - this.installerStore, - }); - - final String packageName; - final String? appIcon; - final String? appName; - final String? version; - final String? installerStore; -} - -class SuspiciousAppInfo { - const SuspiciousAppInfo({ - required this.packageInfo, - required this.reasons, - this.permissions, - }); - - final PackageInfo packageInfo; - final List reasons; - final List? permissions; -} - -/// State of an ongoing call reported by the ongoing call detection. -enum CallState { - /// A call started while the detection was running. - started, - - /// A call was already in progress when the detection started. - ongoing, - - /// A previously reported call has ended. - stopped, -} - -@FlutterApi() -// Migrate whole Talsec API to pigeon -abstract class TalsecPigeonApi { - void onMalwareDetected(List packageInfo); - - void onOngoingCallDetected(CallState state); -} diff --git a/pubspec.yaml b/pubspec.yaml index aa109ea..fdc092a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -29,7 +29,6 @@ dev_dependencies: sdk: flutter json_serializable: ^6.8.0 mocktail: ^1.0.4 - pigeon: ^22.7.2 very_good_analysis: ^7.0.0 flutter: diff --git a/test/src/enums/threat_test.dart b/test/src/enums/threat_test.dart index 1479f91..a15a933 100644 --- a/test/src/enums/threat_test.dart +++ b/test/src/enums/threat_test.dart @@ -14,7 +14,7 @@ void main() { expect(threatValues[0], Threat.hooks); expect(threatValues[1], Threat.debug); expect(threatValues[2], Threat.passcode); - expect(threatValues[3], Threat.deviceId); + expect(threatValues[3], Threat.deviceID); expect(threatValues[4], Threat.simulator); expect(threatValues[5], Threat.appIntegrity); expect(threatValues[6], Threat.obfuscationIssues); @@ -28,7 +28,7 @@ void main() { expect(threatValues[14], Threat.screenshot); expect(threatValues[15], Threat.screenRecording); expect(threatValues[16], Threat.multiInstance); - expect(threatValues[17], Threat.unsecureWiFi); + expect(threatValues[17], Threat.unsecureWifi); expect(threatValues[18], Threat.timeSpoofing); expect(threatValues[19], Threat.locationSpoofing); expect(threatValues[20], Threat.automation); @@ -42,7 +42,7 @@ void main() { expect(ThreatX.fromInt(1268968002), Threat.debug); expect(ThreatX.fromInt(209533833), Threat.hooks); expect(ThreatX.fromInt(1293399086), Threat.passcode); - expect(ThreatX.fromInt(1514211414), Threat.deviceId); + expect(ThreatX.fromInt(1514211414), Threat.deviceID); expect(ThreatX.fromInt(477190884), Threat.simulator); expect(ThreatX.fromInt(1115787534), Threat.appIntegrity); expect(ThreatX.fromInt(1001443554), Threat.obfuscationIssues); @@ -56,11 +56,15 @@ void main() { expect(ThreatX.fromInt(705651459), Threat.screenshot); expect(ThreatX.fromInt(64690214), Threat.screenRecording); expect(ThreatX.fromInt(859307284), Threat.multiInstance); - expect(ThreatX.fromInt(363588890), Threat.unsecureWiFi); + expect(ThreatX.fromInt(363588890), Threat.unsecureWifi); expect(ThreatX.fromInt(189105221), Threat.timeSpoofing); expect(ThreatX.fromInt(653273273), Threat.locationSpoofing); expect(ThreatX.fromInt(298453120), Threat.automation); expect(ThreatX.fromInt(1651076936), Threat.bootloader); }, ); + + test('ThreatX.fromInt should throw ArgumentError for unknown code', () { + expect(() => ThreatX.fromInt(55943254), throwsArgumentError); + }); } diff --git a/test/src/models/android_config_test.dart b/test/src/models/android_config_test.dart index 20cee1b..cfdf234 100644 --- a/test/src/models/android_config_test.dart +++ b/test/src/models/android_config_test.dart @@ -12,8 +12,8 @@ void main() { androidConfig = AndroidConfig( packageName: 'com.example.app', - signingCertHashes: ['AKoRuyLMM91E7lX/Zqp3u4jMmd0A7hH/Iqozu0TMVd0='], - supportedStores: ['adb'], + certificateHashes: ['AKoRuyLMM91E7lX/Zqp3u4jMmd0A7hH/Iqozu0TMVd0='], + supportedAlternativeStores: ['adb'], ); }); @@ -21,25 +21,25 @@ void main() { expect(androidConfig, isA()); expect(androidConfig.packageName, 'com.example.app'); expect( - androidConfig.signingCertHashes, + androidConfig.certificateHashes, ['AKoRuyLMM91E7lX/Zqp3u4jMmd0A7hH/Iqozu0TMVd0='], ); - expect(androidConfig.supportedStores, ['adb']); + expect(androidConfig.supportedAlternativeStores, ['adb']); }); test('Should create AndroidConfig instance with empty stores', () { final androidConfig = AndroidConfig( packageName: 'com.example.app', - signingCertHashes: ['AKoRuyLMM91E7lX/Zqp3u4jMmd0A7hH/Iqozu0TMVd0='], + certificateHashes: ['AKoRuyLMM91E7lX/Zqp3u4jMmd0A7hH/Iqozu0TMVd0='], ); expect(androidConfig, isA()); expect(androidConfig.packageName, 'com.example.app'); expect( - androidConfig.signingCertHashes, + androidConfig.certificateHashes, ['AKoRuyLMM91E7lX/Zqp3u4jMmd0A7hH/Iqozu0TMVd0='], ); - expect(androidConfig.supportedStores, []); + expect(androidConfig.supportedAlternativeStores, []); }); test('Should convert AndroidConfig to JSON', () { @@ -48,17 +48,17 @@ void main() { expect(json, isA>()); expect(json['packageName'], 'com.example.app'); expect( - json['signingCertHashes'], + json['certificateHashes'], ['AKoRuyLMM91E7lX/Zqp3u4jMmd0A7hH/Iqozu0TMVd0='], ); - expect(json['supportedStores'], ['adb']); + expect(json['supportedAlternativeStores'], ['adb']); }); test('Should create AndroidConfig instance from JSON', () { final json = { 'packageName': 'com.example.app', - 'signingCertHashes': ['AKoRuyLMM91E7lX/Zqp3u4jMmd0A7hH/Iqozu0TMVd0='], - 'supportedStores': ['adb'], + 'certificateHashes': ['AKoRuyLMM91E7lX/Zqp3u4jMmd0A7hH/Iqozu0TMVd0='], + 'supportedAlternativeStores': ['adb'], }; final androidConfigFromJson = AndroidConfig.fromJson(json); @@ -66,17 +66,17 @@ void main() { expect(androidConfigFromJson, isA()); expect(androidConfigFromJson.packageName, 'com.example.app'); expect( - androidConfigFromJson.signingCertHashes, + androidConfigFromJson.certificateHashes, ['AKoRuyLMM91E7lX/Zqp3u4jMmd0A7hH/Iqozu0TMVd0='], ); - expect(androidConfigFromJson.supportedStores, ['adb']); + expect(androidConfigFromJson.supportedAlternativeStores, ['adb']); }); test('Should create AndroidConfig instance with empty stores from JSON', () { final json = { 'packageName': 'com.example.app', - 'signingCertHashes': ['AKoRuyLMM91E7lX/Zqp3u4jMmd0A7hH/Iqozu0TMVd0='], + 'certificateHashes': ['AKoRuyLMM91E7lX/Zqp3u4jMmd0A7hH/Iqozu0TMVd0='], }; final androidConfigFromJson = AndroidConfig.fromJson(json); @@ -84,10 +84,10 @@ void main() { expect(androidConfigFromJson, isA()); expect(androidConfigFromJson.packageName, 'com.example.app'); expect( - androidConfigFromJson.signingCertHashes, + androidConfigFromJson.certificateHashes, ['AKoRuyLMM91E7lX/Zqp3u4jMmd0A7hH/Iqozu0TMVd0='], ); - expect(androidConfigFromJson.supportedStores, []); + expect(androidConfigFromJson.supportedAlternativeStores, []); }); test('Should verify AndroidConfig using ConfigVerifier', () { @@ -101,7 +101,7 @@ void main() { expect( () => AndroidConfig( packageName: '', - signingCertHashes: ['AKoRuyLMM91E7lX/Zqp3u4jMmd0A7hH/Iqozu0TMVd0='], + certificateHashes: ['AKoRuyLMM91E7lX/Zqp3u4jMmd0A7hH/Iqozu0TMVd0='], ), throwsA(isA()), ); @@ -111,7 +111,7 @@ void main() { expect( () => AndroidConfig( packageName: 'com.example.app', - signingCertHashes: [], + certificateHashes: [], ), throwsA(isA()), ); @@ -121,7 +121,7 @@ void main() { expect( () => AndroidConfig( packageName: 'com.example.app', - signingCertHashes: ['invalid'], + certificateHashes: ['invalid'], ), throwsA(isA()), ); @@ -130,7 +130,7 @@ void main() { test('Should serialize suspiciousAppDetectionConfig to JSON', () { final config = AndroidConfig( packageName: 'com.example.app', - signingCertHashes: ['AKoRuyLMM91E7lX/Zqp3u4jMmd0A7hH/Iqozu0TMVd0='], + certificateHashes: ['AKoRuyLMM91E7lX/Zqp3u4jMmd0A7hH/Iqozu0TMVd0='], suspiciousAppDetectionConfig: const SuspiciousAppDetectionConfig( packageNames: ['com.malware.app'], scanScope: ScanScope(scopeType: ScopeType.all), @@ -159,7 +159,7 @@ void main() { test('Should deserialize suspiciousAppDetectionConfig from JSON', () { final json = { 'packageName': 'com.example.app', - 'signingCertHashes': ['AKoRuyLMM91E7lX/Zqp3u4jMmd0A7hH/Iqozu0TMVd0='], + 'certificateHashes': ['AKoRuyLMM91E7lX/Zqp3u4jMmd0A7hH/Iqozu0TMVd0='], 'suspiciousAppDetectionConfig': { 'packageNames': ['com.malware.app'], 'scanScope': {'scopeType': 'SIDELOADED_AND_OEM'}, diff --git a/test/src/models/ios_config_test.dart b/test/src/models/ios_config_test.dart index 62c5ced..5d67f17 100644 --- a/test/src/models/ios_config_test.dart +++ b/test/src/models/ios_config_test.dart @@ -11,55 +11,55 @@ void main() { debugDefaultTargetPlatformOverride = TargetPlatform.iOS; iosConfig = IOSConfig( - bundleIds: ['com.example.app1', 'com.example.app2'], - teamId: 'team123', + appBundleIds: ['com.example.app1', 'com.example.app2'], + appTeamId: 'team123', ); }); test('Should create IOSConfig instance', () { expect(iosConfig, isA()); - expect(iosConfig.bundleIds, ['com.example.app1', 'com.example.app2']); - expect(iosConfig.teamId, 'team123'); + expect(iosConfig.appBundleIds, ['com.example.app1', 'com.example.app2']); + expect(iosConfig.appTeamId, 'team123'); }); test('Should convert IOSConfig to JSON', () { final json = iosConfig.toJson(); expect(json, isA>()); - expect(json['bundleIds'], ['com.example.app1', 'com.example.app2']); - expect(json['teamId'], 'team123'); + expect(json['appBundleIds'], ['com.example.app1', 'com.example.app2']); + expect(json['appTeamId'], 'team123'); }); test('Should create IOSConfig instance from JSON', () { final json = { - 'bundleIds': ['com.example.app1', 'com.example.app2'], - 'teamId': 'team123', + 'appBundleIds': ['com.example.app1', 'com.example.app2'], + 'appTeamId': 'team123', }; final iosConfigFromJson = IOSConfig.fromJson(json); expect(iosConfigFromJson, isA()); expect( - iosConfigFromJson.bundleIds, + iosConfigFromJson.appBundleIds, ['com.example.app1', 'com.example.app2'], ); - expect(iosConfigFromJson.teamId, 'team123'); + expect(iosConfigFromJson.appTeamId, 'team123'); }); test('Should verify IOSConfig using ConfigVerifier', () { expect(() => ConfigVerifier.verifyIOS(iosConfig), returnsNormally); }); - test('Should throw ConfigurationException when bundleIds is empty', () { + test('Should throw ConfigurationException when appBundleIds is empty', () { expect( - () => IOSConfig(bundleIds: [], teamId: 'team123'), + () => IOSConfig(appBundleIds: [], appTeamId: 'team123'), throwsA(isA()), ); }); - test('Should throw ConfigurationException when teamId is empty', () { + test('Should throw ConfigurationException when appTeamId is empty', () { expect( - () => IOSConfig(bundleIds: ['com.example.app1'], teamId: ''), + () => IOSConfig(appBundleIds: ['com.example.app1'], appTeamId: ''), throwsA(isA()), ); }); diff --git a/test/src/models/talsec_config_test.dart b/test/src/models/talsec_config_test.dart index f9848b4..9018e8e 100644 --- a/test/src/models/talsec_config_test.dart +++ b/test/src/models/talsec_config_test.dart @@ -14,12 +14,12 @@ void main() { final androidConfig = AndroidConfig( packageName: packageName, - signingCertHashes: signingHashes, + certificateHashes: signingHashes, ); final iosConfig = IOSConfig( - bundleIds: appBundleIds, - teamId: appTeamId, + appBundleIds: appBundleIds, + appTeamId: appTeamId, ); test('Provide configuration for each platform', () { diff --git a/test/src/talsec_test.dart b/test/src/talsec_test.dart index b791692..0b0dd54 100644 --- a/test/src/talsec_test.dart +++ b/test/src/talsec_test.dart @@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:freerasp/freerasp.dart'; +import 'package:freerasp/src/talsec.dart' show createTalsecForTesting; import '../test_utils/test_utils.dart'; @@ -19,12 +20,12 @@ void main() { const mockWatcherMail = 'your_mail@example.com'; final mockAndroidConfig = AndroidConfig( packageName: 'com.aheaditec.freeraspExample', - signingCertHashes: ['AKoRuyLMM91E7lX/Zqp3u4jMmd0A7hH/Iqozu0TMVd0='], - supportedStores: ['com.sec.android.app.samsungapps'], + certificateHashes: ['AKoRuyLMM91E7lX/Zqp3u4jMmd0A7hH/Iqozu0TMVd0='], + supportedAlternativeStores: ['com.sec.android.app.samsungapps'], ); final mockIOSConfig = IOSConfig( - bundleIds: ['com.aheaditec.freeraspExample'], - teamId: 'M8AK35...', + appBundleIds: ['com.aheaditec.freeraspExample'], + appTeamId: 'M8AK35...', ); tearDown(log.clear); @@ -43,7 +44,7 @@ void main() { watcherMail: mockWatcherMail, ); // Act - final talsec = Talsec.private( + final talsec = createTalsecForTesting( methodChannel.methodChannel, FakeEventChannel(), FakeEventChannel(), @@ -70,7 +71,7 @@ void main() { watcherMail: mockWatcherMail, ); // Act - final talsec = Talsec.private( + final talsec = createTalsecForTesting( methodChannel.methodChannel, FakeEventChannel(), FakeEventChannel(), @@ -93,7 +94,7 @@ void main() { debugDefaultTargetPlatformOverride = TargetPlatform.android; // Act - final talsec = Talsec.private( + final talsec = createTalsecForTesting( methodChannel.methodChannel, FakeEventChannel(), FakeEventChannel(), @@ -122,7 +123,7 @@ void main() { ); // Act - final talsec = Talsec.private( + final talsec = createTalsecForTesting( methodChannel.methodChannel, FakeEventChannel(), FakeEventChannel(), @@ -157,13 +158,13 @@ void main() { test('Should receive stream of Threats', () { // Arrange final eventChannel = MockEventChannel( - eventChannel: Talsec.instance.eventChannel, + eventChannel: const EventChannel('talsec.app/freerasp/events'), data: [ 44506749, 1115787534, ], ); - final talsec = Talsec.private( + final talsec = createTalsecForTesting( FakeMethodChannel(), eventChannel.eventChannel, FakeEventChannel(), @@ -185,11 +186,11 @@ void main() { test('Should transform every $PlatformException into $TalsecException', () { final eventChannel = MockEventChannel( - eventChannel: Talsec.instance.eventChannel, + eventChannel: const EventChannel('talsec.app/freerasp/events'), data: [], exceptions: [PlatformException(code: 'dummy-code')], ); - final talsec = Talsec.private( + final talsec = createTalsecForTesting( FakeMethodChannel(), eventChannel.eventChannel, FakeEventChannel(), @@ -216,14 +217,14 @@ void main() { test('Should survive thrown exception when listening', () { final eventChannel = MockEventChannel( - eventChannel: Talsec.instance.eventChannel, + eventChannel: const EventChannel('talsec.app/freerasp/events'), data: [ 44506749, 1115787534, ], exceptions: [PlatformException(code: 'dummy-code')], ); - final talsec = Talsec.private( + final talsec = createTalsecForTesting( FakeMethodChannel(), eventChannel.eventChannel, FakeEventChannel(), @@ -256,10 +257,10 @@ void main() { test('Should receive stream of RaspExecutionState', () { // Arrange final eventChannel = MockEventChannel( - eventChannel: Talsec.instance.executionStateChannel, + eventChannel: const EventChannel('talsec.app/freerasp/execution_state'), data: [187429], ); - final talsec = Talsec.private( + final talsec = createTalsecForTesting( FakeMethodChannel(), FakeEventChannel(), eventChannel.eventChannel, @@ -281,10 +282,10 @@ void main() { test('attachExecutionStateListener should invoke callback', () async { // Arrange final eventChannel = MockEventChannel( - eventChannel: Talsec.instance.executionStateChannel, + eventChannel: const EventChannel('talsec.app/freerasp/execution_state'), data: [187429], ); - final talsec = Talsec.private( + final talsec = createTalsecForTesting( FakeMethodChannel(), FakeEventChannel(), eventChannel.eventChannel, @@ -305,10 +306,10 @@ void main() { test('Should receive Android-only execution states', () { // Arrange final eventChannel = MockEventChannel( - eventChannel: Talsec.instance.executionStateChannel, + eventChannel: const EventChannel('talsec.app/freerasp/execution_state'), data: [614702, 187429, 952318], ); - final talsec = Talsec.private( + final talsec = createTalsecForTesting( FakeMethodChannel(), FakeEventChannel(), eventChannel.eventChannel, @@ -332,10 +333,10 @@ void main() { test('attachExecutionStateListener should invoke every callback', () async { // Arrange final eventChannel = MockEventChannel( - eventChannel: Talsec.instance.executionStateChannel, + eventChannel: const EventChannel('talsec.app/freerasp/execution_state'), data: [614702, 187429, 952318], ); - final talsec = Talsec.private( + final talsec = createTalsecForTesting( FakeMethodChannel(), FakeEventChannel(), eventChannel.eventChannel, @@ -363,7 +364,7 @@ void main() { group('Android-only APIs', () { // Tests run on the host, which is not Android. - final talsec = Talsec.private( + final talsec = createTalsecForTesting( FakeMethodChannel(), FakeEventChannel(), FakeEventChannel(), @@ -376,11 +377,13 @@ void main() { 'isOverlayWindowsBlocked': talsec.isOverlayWindowsBlocked, 'startOngoingCallDetection': talsec.startOngoingCallDetection, 'stopOngoingCallDetection': talsec.stopOngoingCallDetection, + 'addToWhitelist': () => talsec.addToWhitelist('com.example.app'), + 'getAppIcon': () => talsec.getAppIcon('com.example.app'), }; for (final entry in calls.entries) { test('${entry.key} should throw outside Android', () { - expect(entry.value, throwsA(isA())); + expect(entry.value, throwsA(isA())); }); } }); diff --git a/test/src/threat_callback_test.dart b/test/src/threat_callback_test.dart index 4de0c10..862a94d 100644 --- a/test/src/threat_callback_test.dart +++ b/test/src/threat_callback_test.dart @@ -34,20 +34,6 @@ void main() { expect(SpyThreatListener.log[Threat.unofficialStore], 0); expect(SpyThreatListener.log[Threat.obfuscationIssues], 0); }); - - test('Should forward ongoing call state', () { - // Arrange - final states = []; - final callback = ThreatCallback(onOngoingCall: states.add); - - // Act - callback - ..onOngoingCallDetected(CallState.started) - ..onOngoingCallDetected(CallState.stopped); - - // Assert - expect(states, [CallState.started, CallState.stopped]); - }); }); } diff --git a/test/src/threat_events_test.dart b/test/src/threat_events_test.dart new file mode 100644 index 0000000..fd137a0 --- /dev/null +++ b/test/src/threat_events_test.dart @@ -0,0 +1,103 @@ +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:freerasp/freerasp.dart'; +import 'package:freerasp/src/talsec.dart' show createTalsecForTesting; + +import '../test_utils/test_utils.dart'; + +const _events = EventChannel('talsec.app/freerasp/events'); + +const _malwareApp = { + 'packageInfo': { + 'packageName': 'com.example.malware', + 'appName': 'Malware', + 'version': '42', + 'installerStore': null, + }, + 'reasons': ['blacklist'], + 'permissions': null, +}; + +Talsec _talsecWith(List data) { + final channel = MockEventChannel(eventChannel: _events, data: data); + return createTalsecForTesting( + FakeMethodChannel(), + channel.eventChannel, + FakeEventChannel(), + ); +} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + test('attachListener delivers threats and payloads in order', () async { + // Arrange + final log = []; + final talsec = _talsecWith([ + 44506749, + { + 'type': 'malware', + 'suspiciousApps': [_malwareApp], + }, + {'type': 'ongoingCall', 'state': 'started'}, + {'type': 'ongoingCall', 'state': 'stopped'}, + ]); + final callback = ThreatCallback( + onPrivilegedAccess: () => log.add(Threat.privilegedAccess), + onMalware: log.add, + onOngoingCall: log.add, + ); + + // Act + await talsec.attachListener(callback); + await pumpEventQueue(); + + // Assert + expect(log, [ + Threat.privilegedAccess, + [ + const SuspiciousAppInfo( + packageInfo: PackageInfo( + packageName: 'com.example.malware', + appName: 'Malware', + version: '42', + ), + reasons: ['blacklist'], + ), + ], + CallState.started, + CallState.stopped, + ]); + await talsec.detachListener(); + }); + + test('onThreatDetected skips payload events', () { + final talsec = _talsecWith([ + {'type': 'ongoingCall', 'state': 'ongoing'}, + 1115787534, + ]); + + expectLater( + talsec.onThreatDetected, + emitsInOrder([Threat.appIntegrity, emitsDone]), + ); + }); + + test('SuspiciousAppInfo.fromMap reads permissions', () { + final app = SuspiciousAppInfo.fromMap(const { + 'packageInfo': {'packageName': 'com.example.malware'}, + 'reasons': ['permission'], + 'permissions': ['android.permission.READ_SMS'], + }); + + expect(app.permissions, ['android.permission.READ_SMS']); + expect(app.packageInfo.packageName, 'com.example.malware'); + }); + + test('SuspiciousAppInfo.fromMap throws on a malformed map', () { + expect( + () => SuspiciousAppInfo.fromMap(const {'reasons': []}), + throwsA(anything), + ); + }); +} diff --git a/test/src/utils/config_verifier_test.dart b/test/src/utils/config_verifier_test.dart index fb84b34..9741e25 100644 --- a/test/src/utils/config_verifier_test.dart +++ b/test/src/utils/config_verifier_test.dart @@ -11,7 +11,7 @@ void main() { const validHash = 'AKoRuyLMM91E7lX/Zqp3u4jMmd0A7hH/Iqozu0TMVd0='; final config = AndroidConfig( packageName: 'packageName', - signingCertHashes: [validHash], + certificateHashes: [validHash], ); // Act & Assert @@ -26,7 +26,7 @@ void main() { expect( () => AndroidConfig( packageName: 'packageName', - signingCertHashes: [invalidHash], + certificateHashes: [invalidHash], ), throwsA(isA()), ); @@ -43,7 +43,7 @@ void main() { expect( () => AndroidConfig( packageName: 'packageName', - signingCertHashes: [validHash, invalidHash], + certificateHashes: [validHash, invalidHash], ), throwsA(isA()), ); @@ -53,7 +53,7 @@ void main() { expect( () => AndroidConfig( packageName: 'packageName', - signingCertHashes: [], + certificateHashes: [], ), throwsA(isA()), ); @@ -67,7 +67,7 @@ void main() { expect( () => AndroidConfig( packageName: '', - signingCertHashes: [validHash], + certificateHashes: [validHash], ), throwsA(isA()), ); @@ -76,16 +76,16 @@ void main() { test('Should encode TalsecConfig to String', () { // Arrange const expectedString = - '{"androidConfig":{"packageName":"com.aheaditec.freeraspExample","signingCertHashes":["AKoRuyLMM91E7lX/Zqp3u4jMmd0A7hH/Iqozu0TMVd0="],"supportedStores":["com.sec.android.app.samsungapps"]},"iosConfig":{"bundleIds":["com.aheaditec.freeraspExample"],"teamId":"M8AK35..."},"watcherMail":"test_mail@example.com","isProd":false}'; + '{"androidConfig":{"packageName":"com.aheaditec.freeraspExample","certificateHashes":["AKoRuyLMM91E7lX/Zqp3u4jMmd0A7hH/Iqozu0TMVd0="],"supportedAlternativeStores":["com.sec.android.app.samsungapps"]},"iosConfig":{"appBundleIds":["com.aheaditec.freeraspExample"],"appTeamId":"M8AK35..."},"watcherMail":"test_mail@example.com","isProd":false}'; final config = TalsecConfig( androidConfig: AndroidConfig( packageName: 'com.aheaditec.freeraspExample', - signingCertHashes: ['AKoRuyLMM91E7lX/Zqp3u4jMmd0A7hH/Iqozu0TMVd0='], - supportedStores: ['com.sec.android.app.samsungapps'], + certificateHashes: ['AKoRuyLMM91E7lX/Zqp3u4jMmd0A7hH/Iqozu0TMVd0='], + supportedAlternativeStores: ['com.sec.android.app.samsungapps'], ), iosConfig: IOSConfig( - bundleIds: ['com.aheaditec.freeraspExample'], - teamId: 'M8AK35...', + appBundleIds: ['com.aheaditec.freeraspExample'], + appTeamId: 'M8AK35...', ), watcherMail: 'test_mail@example.com', isProd: false, @@ -103,19 +103,19 @@ void main() { final expectedConfig = TalsecConfig( androidConfig: AndroidConfig( packageName: 'com.aheaditec.freeraspExample', - signingCertHashes: ['AKoRuyLMM91E7lX/Zqp3u4jMmd0A7hH/Iqozu0TMVd0='], - supportedStores: ['com.sec.android.app.samsungapps'], + certificateHashes: ['AKoRuyLMM91E7lX/Zqp3u4jMmd0A7hH/Iqozu0TMVd0='], + supportedAlternativeStores: ['com.sec.android.app.samsungapps'], ), iosConfig: IOSConfig( - bundleIds: ['com.aheaditec.freeraspExample'], - teamId: 'M8AK35...', + appBundleIds: ['com.aheaditec.freeraspExample'], + appTeamId: 'M8AK35...', ), watcherMail: 'test_mail@example.com', isProd: false, ); const config = - '{"androidConfig":{"packageName":"com.aheaditec.freeraspExample","signingCertHashes":["AKoRuyLMM91E7lX/Zqp3u4jMmd0A7hH/Iqozu0TMVd0="],"supportedStores":["com.sec.android.app.samsungapps"]},"iosConfig":{"bundleIds":["com.aheaditec.freeraspExample"],"teamId":"M8AK35..."},"watcherMail":"test_mail@example.com","isProd":false}'; + '{"androidConfig":{"packageName":"com.aheaditec.freeraspExample","certificateHashes":["AKoRuyLMM91E7lX/Zqp3u4jMmd0A7hH/Iqozu0TMVd0="],"supportedAlternativeStores":["com.sec.android.app.samsungapps"]},"iosConfig":{"appBundleIds":["com.aheaditec.freeraspExample"],"appTeamId":"M8AK35..."},"watcherMail":"test_mail@example.com","isProd":false}'; // Act final actualConfig = @@ -131,12 +131,12 @@ void main() { equals(expectedConfig.isProd), ); expect( - actualConfig.androidConfig?.signingCertHashes, - equals(expectedConfig.androidConfig?.signingCertHashes), + actualConfig.androidConfig?.certificateHashes, + equals(expectedConfig.androidConfig?.certificateHashes), ); expect( - actualConfig.iosConfig?.bundleIds, - equals(expectedConfig.iosConfig?.bundleIds), + actualConfig.iosConfig?.appBundleIds, + equals(expectedConfig.iosConfig?.appBundleIds), ); }); }); diff --git a/test/test_utils/mock_event_channel.dart b/test/test_utils/mock_event_channel.dart index 4aa6e7c..d7204c3 100644 --- a/test/test_utils/mock_event_channel.dart +++ b/test/test_utils/mock_event_channel.dart @@ -12,7 +12,7 @@ class MockEventChannel { } final EventChannel eventChannel; - final List data; + final List data; final List exceptions; MockStreamHandler _handler() { diff --git a/test/test_utils/spy_threat_callback.dart b/test/test_utils/spy_threat_callback.dart index 0e88c82..fddc545 100644 --- a/test/test_utils/spy_threat_callback.dart +++ b/test/test_utils/spy_threat_callback.dart @@ -11,7 +11,7 @@ class SpyThreatListener { onAppIntegrity: () => _log(Threat.appIntegrity), onDebug: () => _log(Threat.debug), onDeviceBinding: () => _log(Threat.deviceBinding), - onDeviceID: () => _log(Threat.deviceId), + onDeviceID: () => _log(Threat.deviceID), onHooks: () => _log(Threat.hooks), onPasscode: () => _log(Threat.passcode), onPrivilegedAccess: () => _log(Threat.privilegedAccess), @@ -20,7 +20,7 @@ class SpyThreatListener { onUnofficialStore: () => _log(Threat.unofficialStore), onSystemVPN: () => _log(Threat.systemVPN), onDevMode: () => _log(Threat.devMode), - onADBEnabled: () => _log(Threat.adbEnabled), + onAdbEnabled: () => _log(Threat.adbEnabled), onScreenshot: () => _log(Threat.screenshot), onScreenRecording: () => _log(Threat.screenRecording), onObfuscationIssues: () => _log(Threat.obfuscationIssues), @@ -53,7 +53,7 @@ class SpyThreatListener { callback.onUnofficialStore?.call(); case Threat.passcode: callback.onPasscode?.call(); - case Threat.deviceId: + case Threat.deviceID: callback.onDeviceID?.call(); case Threat.secureHardwareNotAvailable: callback.onSecureHardwareNotAvailable?.call(); @@ -62,15 +62,15 @@ class SpyThreatListener { case Threat.devMode: callback.onDevMode?.call(); case Threat.adbEnabled: - callback.onADBEnabled?.call(); + callback.onAdbEnabled?.call(); case Threat.screenshot: callback.onScreenshot?.call(); case Threat.screenRecording: callback.onScreenRecording?.call(); case Threat.multiInstance: callback.onMultiInstance?.call(); - case Threat.unsecureWiFi: - callback.onUnsecureWiFi?.call(); + case Threat.unsecureWifi: + callback.onUnsecureWifi?.call(); case Threat.timeSpoofing: callback.onTimeSpoofing?.call(); case Threat.locationSpoofing: