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: