From df9e2a3688caf18af888279444fdfbbc506bec3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Novotn=C3=BD?= Date: Thu, 26 Sep 2024 09:42:00 +0200 Subject: [PATCH 01/66] feat: add data classes for malware (Dart) --- lib/src/models/package_info.dart | 24 +++++++++++++++++++++++ lib/src/models/package_info.g.dart | 24 +++++++++++++++++++++++ lib/src/models/suspicious_app_info.dart | 18 +++++++++++++++++ lib/src/models/suspicious_app_info.g.dart | 20 +++++++++++++++++++ 4 files changed, 86 insertions(+) create mode 100644 lib/src/models/package_info.dart create mode 100644 lib/src/models/package_info.g.dart create mode 100644 lib/src/models/suspicious_app_info.dart create mode 100644 lib/src/models/suspicious_app_info.g.dart diff --git a/lib/src/models/package_info.dart b/lib/src/models/package_info.dart new file mode 100644 index 0000000..665f717 --- /dev/null +++ b/lib/src/models/package_info.dart @@ -0,0 +1,24 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'package_info.g.dart'; + +@JsonSerializable() +class PackageInfo { + const PackageInfo({ + required this.packageName, + this.appIcon, + this.version, + this.appName, + this.installationSource, + }); + + final String packageName; + final String? appIcon; + final String? appName; + final String? version; + final String? installationSource; + + factory PackageInfo.fromJson(Map json) => + _$PackageInfoFromJson(json); +} + diff --git a/lib/src/models/package_info.g.dart b/lib/src/models/package_info.g.dart new file mode 100644 index 0000000..d6ed553 --- /dev/null +++ b/lib/src/models/package_info.g.dart @@ -0,0 +1,24 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'package_info.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +PackageInfo _$PackageInfoFromJson(Map json) => PackageInfo( + packageName: json['packageName'] as String, + appIcon: json['appIcon'] as String?, + version: json['version'] as String?, + appName: json['appName'] as String?, + installationSource: json['installationSource'] as String?, + ); + +Map _$PackageInfoToJson(PackageInfo instance) => + { + 'packageName': instance.packageName, + 'appIcon': instance.appIcon, + 'appName': instance.appName, + 'version': instance.version, + 'installationSource': instance.installationSource, + }; diff --git a/lib/src/models/suspicious_app_info.dart b/lib/src/models/suspicious_app_info.dart new file mode 100644 index 0000000..246854b --- /dev/null +++ b/lib/src/models/suspicious_app_info.dart @@ -0,0 +1,18 @@ +import 'package:freerasp/src/models/package_info.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'suspicious_app_info.g.dart'; + +@JsonSerializable() +class SuspiciousAppInfo { + const SuspiciousAppInfo({ + required this.packageInfo, + required this.reason, + }); + + final PackageInfo packageInfo; + final String reason; + + factory SuspiciousAppInfo.fromJson(Map json) => + _$SuspiciousAppInfoFromJson(json); +} diff --git a/lib/src/models/suspicious_app_info.g.dart b/lib/src/models/suspicious_app_info.g.dart new file mode 100644 index 0000000..250b832 --- /dev/null +++ b/lib/src/models/suspicious_app_info.g.dart @@ -0,0 +1,20 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'suspicious_app_info.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +SuspiciousAppInfo _$SuspiciousAppInfoFromJson(Map json) => + SuspiciousAppInfo( + packageInfo: + PackageInfo.fromJson(json['packageInfo'] as Map), + reason: json['reason'] as String, + ); + +Map _$SuspiciousAppInfoToJson(SuspiciousAppInfo instance) => + { + 'packageInfo': instance.packageInfo, + 'reason': instance.reason, + }; From 1ace1b85934cd95c32f2e3dc9d822512cd9c94b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Novotn=C3=BD?= Date: Thu, 26 Sep 2024 09:42:29 +0200 Subject: [PATCH 02/66] feat: add data classes for malware (Kotlin) --- .../aheaditec/freerasp/models/PackageInfo.kt | 33 +++++++++++++++++++ .../freerasp/models/SuspiciousAppInfo.kt | 21 ++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 android/src/main/kotlin/com/aheaditec/freerasp/models/PackageInfo.kt create mode 100644 android/src/main/kotlin/com/aheaditec/freerasp/models/SuspiciousAppInfo.kt diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/models/PackageInfo.kt b/android/src/main/kotlin/com/aheaditec/freerasp/models/PackageInfo.kt new file mode 100644 index 0000000..aab9a20 --- /dev/null +++ b/android/src/main/kotlin/com/aheaditec/freerasp/models/PackageInfo.kt @@ -0,0 +1,33 @@ +package com.aheaditec.freerasp.models + +import org.json.JSONObject + +data class PackageInfo( + val packageName: String, + val appIcon: String? = null, + val appName: String? = null, + val version: String? = null, + val installationSource: String? = null +) { + companion object { + fun fromTalsec(packageInfo: android.content.pm.PackageInfo): PackageInfo { + return PackageInfo( + packageInfo.packageName, + packageInfo.appIcon, + packageInfo.appName, + packageInfo.version, + packageInfo.installationSource + ) + } + } + + fun toJson(): String { + val json = JSONObject().put("packageName", packageName) + .putOpt("appIcon", appIcon) + .putOpt("appName", appName) + .putOpt("version", version) + .putOpt("installationSource", installationSource) + + return json.toString() + } +} \ No newline at end of file diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/models/SuspiciousAppInfo.kt b/android/src/main/kotlin/com/aheaditec/freerasp/models/SuspiciousAppInfo.kt new file mode 100644 index 0000000..bb93a8e --- /dev/null +++ b/android/src/main/kotlin/com/aheaditec/freerasp/models/SuspiciousAppInfo.kt @@ -0,0 +1,21 @@ +package com.aheaditec.freerasp.models + +import com.aheaditec.talsec_security.security.api.SuspiciousAppInfo as TalsecSuspiciousAppInfo + +class SuspiciousAppInfo( + val packageInfo: PackageInfo, + val reason: String +) { + companion object { + fun fromTalsec(suspiciousAppInfo: TalsecSuspiciousAppInfo): SuspiciousAppInfo { + return SuspiciousAppInfo( + PackageInfo.fromTalsec(suspiciousAppInfo.packageInfo), + suspiciousAppInfo.reason + ) + } + } + + fun toJson(): String { + return "{\"packageInfo\": ${packageInfo.toJson()}, \"reason\": \"$reason\"}" + } +} \ No newline at end of file From 8982a580e09cc7d41b2e8f91a5fcd461fc6c36c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Novotn=C3=BD?= Date: Thu, 26 Sep 2024 09:42:48 +0200 Subject: [PATCH 03/66] feat: update Android configuration --- lib/src/models/android_config.dart | 12 ++++++++++++ lib/src/models/android_config.g.dart | 23 +++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/lib/src/models/android_config.dart b/lib/src/models/android_config.dart index 5a8356e..e9662b2 100644 --- a/lib/src/models/android_config.dart +++ b/lib/src/models/android_config.dart @@ -11,6 +11,10 @@ class AndroidConfig { required this.packageName, required this.signingCertHashes, this.supportedStores = const [], + this.blocklistedPackageNames = const [], + this.blocklistedHashes = const [], + this.blocklistedPermissions = const >[[]], + this.whitelistedInstallationSources = const [], }) { ConfigVerifier.verifyAndroid(this); } @@ -30,4 +34,12 @@ class AndroidConfig { /// List of supported sources where application can be installed from. final List supportedStores; + + final List blocklistedPackageNames; + + final List blocklistedHashes; + + final List> blocklistedPermissions; + + final List whitelistedInstallationSources; } diff --git a/lib/src/models/android_config.g.dart b/lib/src/models/android_config.g.dart index 021f8a8..d93449d 100644 --- a/lib/src/models/android_config.g.dart +++ b/lib/src/models/android_config.g.dart @@ -16,6 +16,25 @@ AndroidConfig _$AndroidConfigFromJson(Map json) => ?.map((e) => e as String) .toList() ?? const [], + blocklistedPackageNames: + (json['blocklistedPackageNames'] as List?) + ?.map((e) => e as String) + .toList() ?? + const [], + blocklistedHashes: (json['blocklistedHashes'] as List?) + ?.map((e) => e as String) + .toList() ?? + const [], + blocklistedPermissions: (json['blocklistedPermissions'] as List?) + ?.map( + (e) => (e as List).map((e) => e as String).toList()) + .toList() ?? + const >[[]], + whitelistedInstallationSources: + (json['whitelistedInstallationSources'] as List?) + ?.map((e) => e as String) + .toList() ?? + const [], ); Map _$AndroidConfigToJson(AndroidConfig instance) => @@ -23,4 +42,8 @@ Map _$AndroidConfigToJson(AndroidConfig instance) => 'packageName': instance.packageName, 'signingCertHashes': instance.signingCertHashes, 'supportedStores': instance.supportedStores, + 'blocklistedPackageNames': instance.blocklistedPackageNames, + 'blocklistedHashes': instance.blocklistedHashes, + 'blocklistedPermissions': instance.blocklistedPermissions, + 'whitelistedInstallationSources': instance.whitelistedInstallationSources, }; From 5400029b32c088094e670772cc4131038e581d3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Novotn=C3=BD?= Date: Thu, 26 Sep 2024 09:43:02 +0200 Subject: [PATCH 04/66] feat: update Talsec configuration --- lib/src/models/talsec_config.g.dart | 3 +- lib/src/talsec.dart | 54 +++++++++++++++++++++-------- 2 files changed, 40 insertions(+), 17 deletions(-) diff --git a/lib/src/models/talsec_config.g.dart b/lib/src/models/talsec_config.g.dart index f901fc5..41aaa56 100644 --- a/lib/src/models/talsec_config.g.dart +++ b/lib/src/models/talsec_config.g.dart @@ -12,8 +12,7 @@ TalsecConfig _$TalsecConfigFromJson(Map json) => TalsecConfig( androidConfig: json['androidConfig'] == null ? null : AndroidConfig.fromJson( - json['androidConfig'] as Map, - ), + json['androidConfig'] as Map), iosConfig: json['iosConfig'] == null ? null : IOSConfig.fromJson(json['iosConfig'] as Map), diff --git a/lib/src/talsec.dart b/lib/src/talsec.dart index 25ad8cf..7fdc906 100644 --- a/lib/src/talsec.dart +++ b/lib/src/talsec.dart @@ -27,7 +27,17 @@ import 'package:freerasp/freerasp.dart'; class Talsec { /// Private constructor for internal and testing purposes. @visibleForTesting - Talsec.private(this.methodChannel, this.eventChannel); + Talsec.private(this.methodChannel, this.handlerChannel, this.eventChannel) { + handlerChannel.setMethodCallHandler(_methodHandler); + } + + Future _methodHandler(MethodCall call) async { + if (call.method != 'onMalwareDetected') { + return; + } + + print("data"); + } /// Named channel used to communicate with platform plugins. /// @@ -42,8 +52,12 @@ class Talsec { static const MethodChannel _methodChannel = MethodChannel('talsec.app/freerasp/methods'); + static const MethodChannel _handlerChannel = + MethodChannel('talsec.app/freerasp/invoke'); + /// Private [Talsec] variable which holds current instance of class. - static final _instance = Talsec.private(_methodChannel, _eventChannel); + static final _instance = + Talsec.private(_methodChannel, _handlerChannel, _eventChannel); /// Initialize Talsec lazily/obtain current instance of Talsec. static Talsec get instance => _instance; @@ -52,6 +66,10 @@ class Talsec { @visibleForTesting late final MethodChannel methodChannel; + /// [MethodChannel] used to invoke native platform. + @visibleForTesting + late final MethodChannel handlerChannel; + /// [EventChannel] used to receive Threats from the native platform. @visibleForTesting late final EventChannel eventChannel; @@ -60,6 +78,8 @@ class Talsec { Stream? _onThreatDetected; + List _suspiciousAppsCache = []; + /// Returns a broadcast stream. When security is compromised /// [onThreatDetected] receives what type of Threat caused it. /// @@ -97,6 +117,8 @@ class Talsec { return _onThreatDetected!; } + ThreatCallback? _callback; + /// Starts freeRASP with configuration provided in [config]. Future start(TalsecConfig config) { _checkConfig(config); @@ -137,46 +159,47 @@ class Talsec { /// invoked. void attachListener(ThreatCallback callback) { detachListener(); + _callback = callback; _streamSubscription ??= onThreatDetected.listen((event) { switch (event) { case Threat.hooks: - callback.onHooks?.call(); + _callback?.onHooks?.call(); break; case Threat.debug: - callback.onDebug?.call(); + _callback?.onDebug?.call(); break; case Threat.passcode: - callback.onPasscode?.call(); + _callback?.onPasscode?.call(); break; case Threat.deviceId: - callback.onDeviceID?.call(); + _callback?.onDeviceID?.call(); break; case Threat.simulator: - callback.onSimulator?.call(); + _callback?.onSimulator?.call(); break; case Threat.appIntegrity: - callback.onAppIntegrity?.call(); + _callback?.onAppIntegrity?.call(); break; case Threat.obfuscationIssues: - callback.onObfuscationIssues?.call(); + _callback?.onObfuscationIssues?.call(); break; case Threat.deviceBinding: - callback.onDeviceBinding?.call(); + _callback?.onDeviceBinding?.call(); break; case Threat.unofficialStore: - callback.onUnofficialStore?.call(); + _callback?.onUnofficialStore?.call(); break; case Threat.privilegedAccess: - callback.onPrivilegedAccess?.call(); + _callback?.onPrivilegedAccess?.call(); break; case Threat.secureHardwareNotAvailable: - callback.onSecureHardwareNotAvailable?.call(); + _callback?.onSecureHardwareNotAvailable?.call(); break; case Threat.systemVPN: - callback.onSystemVPN?.call(); + _callback?.onSystemVPN?.call(); break; case Threat.devMode: - callback.onDevMode?.call(); + _callback?.onDevMode?.call(); break; } }); @@ -189,6 +212,7 @@ class Talsec { void detachListener() { _streamSubscription?.cancel(); _streamSubscription = null; + _callback = null; } void _handleStreamError(Object error) { From 7c35e1986b4aea698a3cbc6ec89770de18120120 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Novotn=C3=BD?= Date: Thu, 26 Sep 2024 09:43:49 +0200 Subject: [PATCH 05/66] feat: add Flutter method invocation --- .../com/aheaditec/freerasp/FreeraspPlugin.kt | 5 ++ .../freerasp/handlers/MethodCallInvoker.kt | 72 +++++++++++++++++++ .../freerasp/handlers/PluginThreatHandler.kt | 13 ++++ .../freerasp/handlers/StreamHandler.kt | 6 +- .../freerasp/handlers/TalsecThreatHandler.kt | 28 ++++++-- lib/src/typedefs.dart | 4 ++ 6 files changed, 121 insertions(+), 7 deletions(-) create mode 100644 android/src/main/kotlin/com/aheaditec/freerasp/handlers/MethodCallInvoker.kt diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/FreeraspPlugin.kt b/android/src/main/kotlin/com/aheaditec/freerasp/FreeraspPlugin.kt index bda94d4..48ede80 100644 --- a/android/src/main/kotlin/com/aheaditec/freerasp/FreeraspPlugin.kt +++ b/android/src/main/kotlin/com/aheaditec/freerasp/FreeraspPlugin.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleOwner import com.aheaditec.freerasp.handlers.MethodCallHandler +import com.aheaditec.freerasp.handlers.MethodCallInvoker import com.aheaditec.freerasp.handlers.StreamHandler import com.aheaditec.freerasp.handlers.TalsecThreatHandler import io.flutter.embedding.engine.plugins.FlutterPlugin @@ -17,6 +18,8 @@ import io.flutter.embedding.engine.plugins.lifecycle.FlutterLifecycleAdapter class FreeraspPlugin : FlutterPlugin, ActivityAware, LifecycleEventObserver { private var streamHandler: StreamHandler = StreamHandler() private var methodCallHandler: MethodCallHandler = MethodCallHandler() + private var methodCallInvoker : MethodCallInvoker = MethodCallInvoker() + private var context: Context? = null private var lifecycle: Lifecycle? = null @@ -25,11 +28,13 @@ class FreeraspPlugin : FlutterPlugin, ActivityAware, LifecycleEventObserver { context = flutterPluginBinding.applicationContext methodCallHandler.createMethodChannel(messenger, flutterPluginBinding.applicationContext) + methodCallInvoker.createMethodChannel(messenger, flutterPluginBinding.applicationContext) streamHandler.createEventChannel(messenger) } override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { methodCallHandler.destroyMethodChannel() + methodCallInvoker.destroyMethodChannel() streamHandler.destroyEventChannel() TalsecThreatHandler.detachListener(binding.applicationContext) } diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/handlers/MethodCallInvoker.kt b/android/src/main/kotlin/com/aheaditec/freerasp/handlers/MethodCallInvoker.kt new file mode 100644 index 0000000..1f0f5f0 --- /dev/null +++ b/android/src/main/kotlin/com/aheaditec/freerasp/handlers/MethodCallInvoker.kt @@ -0,0 +1,72 @@ +package com.aheaditec.freerasp.handlers + +import android.content.Context +import com.aheaditec.talsec_security.security.api.SuspiciousAppInfo +import io.flutter.Log +import io.flutter.plugin.common.BinaryMessenger +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.MethodCallHandler + +/** + * A method handler that creates and manages an [MethodChannel] for freeRASP methods. + */ +internal class MethodCallInvoker: MethodCallHandler { + private var context: Context? = null + private var methodChannel: MethodChannel? = null + private val methodSink = object : MethodSink { + override fun onMalwareDetected(packageInfo: List) { + methodChannel?.invokeMethod("onMalwareDetected", mapOf("packageInfo" to packageInfo.map { })) + } + } + + companion object { + private const val CHANNEL_NAME: String = "talsec.app/freerasp/invoke" + } + + internal interface MethodSink { + fun onMalwareDetected(packageInfo: List) + } + + /** + * Creates a new [MethodChannel] with the specified [BinaryMessenger] instance. Sets this class + * as the [MethodCallHandler]. + * If an old [MethodChannel] already exists, it will be destroyed before creating a new one. + * + * @param messenger The binary messenger to use for creating the [MethodChannel]. + * @param context The Android [Context] associated with this channel. + */ + fun createMethodChannel(messenger: BinaryMessenger, context: Context) { + methodChannel?.let { + Log.i("MethodCallHandler", "Tried to create channel without disposing old one.") + destroyMethodChannel() + } + + methodChannel = MethodChannel(messenger, CHANNEL_NAME).also { + it.setMethodCallHandler(this) + } + + this.context = context + TalsecThreatHandler.attachMethodSink(methodSink) + } + + /** + * Destroys the `MethodChannel` and clears associated variables. + */ + fun destroyMethodChannel() { + methodChannel?.setMethodCallHandler(null) + methodChannel = null + this.context = null + TalsecThreatHandler.detachMethodSink() + } + + /** + * Handles method calls received through the [MethodChannel]. + * + * @param call The method call. + * @param result The result handler of the method call. + */ + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { + result.error("INVALID", "This channel does not handle calls from Flutter.", null) + } +} diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/handlers/PluginThreatHandler.kt b/android/src/main/kotlin/com/aheaditec/freerasp/handlers/PluginThreatHandler.kt index dd30f50..8ed309f 100644 --- a/android/src/main/kotlin/com/aheaditec/freerasp/handlers/PluginThreatHandler.kt +++ b/android/src/main/kotlin/com/aheaditec/freerasp/handlers/PluginThreatHandler.kt @@ -2,6 +2,7 @@ package com.aheaditec.freerasp.handlers import android.content.Context import com.aheaditec.freerasp.Threat +import com.aheaditec.talsec_security.security.api.SuspiciousAppInfo import com.aheaditec.talsec_security.security.api.ThreatListener import com.aheaditec.talsec_security.security.api.ThreatListener.DeviceState import com.aheaditec.talsec_security.security.api.ThreatListener.ThreatDetected @@ -14,6 +15,8 @@ import com.aheaditec.talsec_security.security.api.ThreatListener.ThreatDetected */ internal object PluginThreatHandler : ThreatDetected, DeviceState { internal val detectedThreats = mutableSetOf() + internal val detectedMalware = mutableListOf() + internal var listener: TalsecFlutter? = null private val internalListener = ThreatListener(this, this) @@ -73,11 +76,21 @@ internal object PluginThreatHandler : ThreatDetected, DeviceState { notify(Threat.DevMode) } + override fun onMalwareDetected(suspiciousApps: List) { + notify(suspiciousApps) + } + private fun notify(threat: Threat) { listener?.threatDetected(threat) ?: detectedThreats.add(threat) } + private fun notify(suspiciousApps: List) { + listener?.malwareDetected(suspiciousApps) ?: detectedMalware.addAll(suspiciousApps) + } + internal interface TalsecFlutter { fun threatDetected(threatType: Threat) + + fun malwareDetected(suspiciousApps: List) } } diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/handlers/StreamHandler.kt b/android/src/main/kotlin/com/aheaditec/freerasp/handlers/StreamHandler.kt index 6e55434..0156c1c 100644 --- a/android/src/main/kotlin/com/aheaditec/freerasp/handlers/StreamHandler.kt +++ b/android/src/main/kotlin/com/aheaditec/freerasp/handlers/StreamHandler.kt @@ -42,7 +42,7 @@ internal class StreamHandler : EventChannel.StreamHandler { // Don't forget to remove old sink // @see https://stackoverflow.com/questions/61934900/tried-to-send-a-platform-message-to-flutter-but-flutterjni-was-detached-from-n - TalsecThreatHandler.detachSink() + TalsecThreatHandler.detachEventSink() } /** @@ -54,7 +54,7 @@ internal class StreamHandler : EventChannel.StreamHandler { */ override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { events?.let { - TalsecThreatHandler.attachSink(it) + TalsecThreatHandler.attachEventSink(it) } } @@ -65,6 +65,6 @@ internal class StreamHandler : EventChannel.StreamHandler { * @param arguments The arguments passed by the subscriber. Not used in this implementation. */ override fun onCancel(arguments: Any?) { - TalsecThreatHandler.detachSink() + TalsecThreatHandler.detachEventSink() } } \ No newline at end of file diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/handlers/TalsecThreatHandler.kt b/android/src/main/kotlin/com/aheaditec/freerasp/handlers/TalsecThreatHandler.kt index 35b381a..cb195c9 100644 --- a/android/src/main/kotlin/com/aheaditec/freerasp/handlers/TalsecThreatHandler.kt +++ b/android/src/main/kotlin/com/aheaditec/freerasp/handlers/TalsecThreatHandler.kt @@ -2,6 +2,7 @@ package com.aheaditec.freerasp.handlers import android.content.Context import com.aheaditec.freerasp.Threat +import com.aheaditec.talsec_security.security.api.SuspiciousAppInfo import com.aheaditec.talsec_security.security.api.Talsec import com.aheaditec.talsec_security.security.api.TalsecConfig import io.flutter.plugin.common.EventChannel.EventSink @@ -12,6 +13,7 @@ import io.flutter.plugin.common.EventChannel.EventSink */ internal object TalsecThreatHandler { private var eventSink: EventSink? = null + private var methodSink: MethodCallInvoker.MethodSink? = null private var isListening = false /** @@ -78,7 +80,7 @@ internal object TalsecThreatHandler { * In contrast to [detachListener], this function does not unregister the listener. It only * suspends the listener, meaning all detected threats are cached and sent later. * - * In contrast to [detachSink], this function does not nullify the [eventSink]. It only suspends + * In contrast to [detachEventSink], this function does not nullify the [eventSink]. It only suspends * sending events to the event sink. This is useful when the application goes to background and * [EventSink] is not destroyed but also is not able to send events. */ @@ -92,7 +94,7 @@ internal object TalsecThreatHandler { * In contrast to [attachListener], this function does not register the listener. It only * resumes the listener, meaning all cached threats are sent to the [EventSink]. * - * In contrast to [attachSink], this function does not assign new [EventSink] to [eventSink]. + * In contrast to [attachEventSink], this function does not assign new [EventSink] to [eventSink]. * It only resumes sending events to the current [eventSink]. * This is useful when the application comes to foreground and [EventSink] is not destroyed but * also is not able to send events. @@ -110,7 +112,7 @@ internal object TalsecThreatHandler { * * @param eventSink The event sink of the new listener. */ - internal fun attachSink(eventSink: EventSink) { + internal fun attachEventSink(eventSink: EventSink) { this.eventSink = eventSink PluginThreatHandler.listener = ThreatListener flushThreatCache(eventSink) @@ -119,7 +121,7 @@ internal object TalsecThreatHandler { /** * Called when a listener unsubscribes from the event channel. */ - internal fun detachSink() { + internal fun detachEventSink() { eventSink = null PluginThreatHandler.listener = null } @@ -134,6 +136,19 @@ internal object TalsecThreatHandler { eventSink?.success(it.value) } PluginThreatHandler.detectedThreats.clear() + + PluginThreatHandler.detectedMalware.let { + methodSink?.onMalwareDetected(it) + } + PluginThreatHandler.detectedMalware.clear() + } + + internal fun attachMethodSink(methodSink: MethodCallInvoker.MethodSink) { + this.methodSink = methodSink + } + + internal fun detachMethodSink() { + methodSink = null } /** @@ -145,5 +160,10 @@ internal object TalsecThreatHandler { override fun threatDetected(threatType: Threat) { eventSink?.success(threatType.value) } + + override fun malwareDetected(suspiciousApps: List) { + methodSink?.onMalwareDetected(suspiciousApps) + } } } + diff --git a/lib/src/typedefs.dart b/lib/src/typedefs.dart index 9d3cbf9..085e5f1 100644 --- a/lib/src/typedefs.dart +++ b/lib/src/typedefs.dart @@ -1,2 +1,6 @@ +import '../freerasp.dart'; + /// Typedef for void methods typedef VoidCallback = void Function(); + +typedef MalwareCallback = void Function(List); From d65da877c53350a0f3f919beb621e8c720effd6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Novotn=C3=BD?= Date: Thu, 26 Sep 2024 09:44:06 +0200 Subject: [PATCH 06/66] feat: raise lib version --- android/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/build.gradle b/android/build.gradle index dbe2ff9..11253a8 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -56,5 +56,5 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" // Talsec SDK - implementation 'com.aheaditec.talsec.security:TalsecSecurity-Community-Flutter:9.6.0' + implementation 'com.aheaditec.talsec.security:TalsecSecurity-Community-Flutter:11.1.0' } From 8ef80c601603470aef32c1be59bd33bad1180f06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Novotn=C3=BD?= Date: Thu, 26 Sep 2024 09:44:15 +0200 Subject: [PATCH 07/66] feat: update example app --- example/lib/main.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/example/lib/main.dart b/example/lib/main.dart index 6a265dd..64be409 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -19,6 +19,7 @@ void main() async { packageName: 'com.aheaditec.freeraspExample', signingCertHashes: ['AKoRuyLMM91E7lX/Zqp3u4jMmd0A7hH/Iqozu0TMVd0='], supportedStores: ['com.sec.android.app.samsungapps'], + blocklistedPackageNames: ['com.aheaditec.freeraspExample'], ), /// For iOS From 25cfcc9bf242aee8155f6171d142e4fba1dbeca5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Novotn=C3=BD?= Date: Thu, 26 Sep 2024 09:44:32 +0200 Subject: [PATCH 08/66] feat: update threat callback --- lib/src/threat_callback.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/src/threat_callback.dart b/lib/src/threat_callback.dart index f91b553..c4732b5 100644 --- a/lib/src/threat_callback.dart +++ b/lib/src/threat_callback.dart @@ -33,6 +33,7 @@ class ThreatCallback { this.onSecureHardwareNotAvailable, this.onSystemVPN, this.onDevMode, + this.onMalware, }); /// This method is called when a threat related dynamic hooking (e.g. Frida) @@ -80,4 +81,7 @@ class ThreatCallback { /// This method is called whe the device has Developer mode enabled final VoidCallback? onDevMode; + + /// This method is called when malware is detected on the device + final MalwareCallback? onMalware; } From af634b2e24c65ed626bc18b6c4c12438a5c3419f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Novotn=C3=BD?= Date: Thu, 26 Sep 2024 09:44:41 +0200 Subject: [PATCH 09/66] feat: misc --- .../com/aheaditec/freerasp/utils/Utils.kt | 83 ++++++++++++++----- lib/src/models/models.dart | 2 + test/src/talsec_test.dart | 49 +++++++---- 3 files changed, 98 insertions(+), 36 deletions(-) diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/utils/Utils.kt b/android/src/main/kotlin/com/aheaditec/freerasp/utils/Utils.kt index e4678ff..a51e1b2 100644 --- a/android/src/main/kotlin/com/aheaditec/freerasp/utils/Utils.kt +++ b/android/src/main/kotlin/com/aheaditec/freerasp/utils/Utils.kt @@ -1,5 +1,6 @@ package com.aheaditec.freerasp.utils +import com.aheaditec.talsec_security.security.api.SuspiciousAppInfo import com.aheaditec.talsec_security.security.api.TalsecConfig import org.json.JSONException import org.json.JSONObject @@ -11,33 +12,71 @@ internal class Utils { throw JSONException("Configuration is null") } val json = JSONObject(configJson) - val androidConfig = json.getJSONObject("androidConfig") - val packageName = androidConfig.getString("packageName") - val certificateHashes = mutableListOf() - val hashes = androidConfig.getJSONArray("signingCertHashes") - for (i in 0 until hashes.length()) { - certificateHashes.add(hashes.getString(i)) - } + val watcherMail = json.getString("watcherMail") - val alternativeStores = mutableListOf() - if (androidConfig.has("supportedStores")) { - val stores = androidConfig.getJSONArray("supportedStores") - for (i in 0 until stores.length()) { - alternativeStores.add(stores.getString(i)) - } - } var isProd = true if (json.has("isProd")) { isProd = json.getBoolean("isProd") } + val androidConfig = json.getJSONObject("androidConfig") - return TalsecConfig( - packageName, - certificateHashes.toTypedArray(), - watcherMail, - alternativeStores.toTypedArray(), - isProd - ) + val packageName = androidConfig.getString("packageName") + val certificateHashes = androidConfig.extractArray("signingCertHashes") + val alternativeStores = androidConfig.extractArray("supportedStores") + val blocklistedPackageNames = + androidConfig.extractArray("blocklistedPackageNames") + val blocklistedHashes = androidConfig.extractArray("blocklistedHashes") + val whitelistedInstallationSources = + androidConfig.extractArray("whitelistedInstallationSources") + + val blocklistedPermissions = mutableListOf>() + if (androidConfig.has("blocklistedPermissions")) { + val permissions = androidConfig.getJSONArray("blocklistedPermissions") + for (i in 0 until permissions.length()) { + val permission = permissions.getJSONArray(i) + val permissionList = mutableListOf() + for (j in 0 until permission.length()) { + permissionList.add(permission.getString(j)) + } + blocklistedPermissions.add(permissionList.toTypedArray()) + } + } + + return TalsecConfig.Builder(packageName, certificateHashes) + .watcherMail(watcherMail) + .supportedAlternativeStores(alternativeStores) + .prod(isProd) + .blocklistedPackageNames(blocklistedPackageNames) + .blocklistedHashes(blocklistedHashes) + .blocklistedPermissions(blocklistedPermissions.toTypedArray()) + .whitelistedInstallationSources(whitelistedInstallationSources) + .build() + } + + fun fromTalsec(malwareInfo: List) { + val packageInfoList = mutableListOf() + for (info in malwareInfo) { + packageInfoList.add(info.toJson()) + } + } + } +} + +inline fun JSONObject.extractArray(key: String): Array { + val list = mutableListOf() + if (this.has(key)) { + val jsonArray = this.getJSONArray(key) + for (i in 0 until jsonArray.length()) { + val element = when (T::class) { + String::class -> jsonArray.getString(i) as T + Int::class -> jsonArray.getInt(i) as T + Double::class -> jsonArray.getDouble(i) as T + Boolean::class -> jsonArray.getBoolean(i) as T + Long::class -> jsonArray.getLong(i) as T + else -> throw IllegalArgumentException("Unsupported type") + } + list.add(element) } } -} \ No newline at end of file + return list.toTypedArray() +} diff --git a/lib/src/models/models.dart b/lib/src/models/models.dart index d18e85e..a24b299 100644 --- a/lib/src/models/models.dart +++ b/lib/src/models/models.dart @@ -1,3 +1,5 @@ export 'android_config.dart'; export 'ios_config.dart'; +export 'package_info.dart'; +export 'suspicious_app_info.dart'; export 'talsec_config.dart'; diff --git a/test/src/talsec_test.dart b/test/src/talsec_test.dart index 605b1ab..8e9fa8b 100644 --- a/test/src/talsec_test.dart +++ b/test/src/talsec_test.dart @@ -43,8 +43,11 @@ void main() { watcherMail: mockWatcherMail, ); // Act - final talsec = - Talsec.private(methodChannel.methodChannel, FakeEventChannel()); + final talsec = Talsec.private( + methodChannel.methodChannel, + methodChannel.methodChannel, + FakeEventChannel(), + ); // Assert expect( @@ -67,8 +70,11 @@ void main() { watcherMail: mockWatcherMail, ); // Act - final talsec = - Talsec.private(methodChannel.methodChannel, FakeEventChannel()); + final talsec = Talsec.private( + methodChannel.methodChannel, + methodChannel.methodChannel, + FakeEventChannel(), + ); // Assert expect( @@ -87,8 +93,11 @@ void main() { debugDefaultTargetPlatformOverride = TargetPlatform.android; // Act - final talsec = - Talsec.private(methodChannel.methodChannel, FakeEventChannel()); + final talsec = Talsec.private( + methodChannel.methodChannel, + methodChannel.methodChannel, + FakeEventChannel(), + ); // Assert expect( @@ -113,8 +122,11 @@ void main() { ); // Act - final talsec = - Talsec.private(methodChannel.methodChannel, FakeEventChannel()); + final talsec = Talsec.private( + methodChannel.methodChannel, + methodChannel.methodChannel, + FakeEventChannel(), + ); // Assert expect( @@ -151,8 +163,11 @@ void main() { 1115787534, ], ); - final talsec = - Talsec.private(FakeMethodChannel(), eventChannel.eventChannel); + final talsec = Talsec.private( + FakeMethodChannel(), + FakeMethodChannel(), + eventChannel.eventChannel, + ); // Act final stream = talsec.onThreatDetected; @@ -174,8 +189,11 @@ void main() { data: [], exceptions: [PlatformException(code: 'dummy-code')], ); - final talsec = - Talsec.private(FakeMethodChannel(), eventChannel.eventChannel); + final talsec = Talsec.private( + FakeMethodChannel(), + FakeMethodChannel(), + eventChannel.eventChannel, + ); // Act final stream = talsec.onThreatDetected; @@ -205,8 +223,11 @@ void main() { ], exceptions: [PlatformException(code: 'dummy-code')], ); - final talsec = - Talsec.private(FakeMethodChannel(), eventChannel.eventChannel); + final talsec = Talsec.private( + FakeMethodChannel(), + FakeMethodChannel(), + eventChannel.eventChannel, + ); // Act final stream = talsec.onThreatDetected; From fdb5c6555fd9f1e961c25fa26dabe7b22eba9720 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Novotn=C3=BD?= Date: Thu, 26 Sep 2024 09:42:00 +0200 Subject: [PATCH 10/66] feat: add data classes for malware (Dart) --- lib/src/models/package_info.dart | 24 +++++++++++++++++++++++ lib/src/models/package_info.g.dart | 24 +++++++++++++++++++++++ lib/src/models/suspicious_app_info.dart | 18 +++++++++++++++++ lib/src/models/suspicious_app_info.g.dart | 20 +++++++++++++++++++ 4 files changed, 86 insertions(+) create mode 100644 lib/src/models/package_info.dart create mode 100644 lib/src/models/package_info.g.dart create mode 100644 lib/src/models/suspicious_app_info.dart create mode 100644 lib/src/models/suspicious_app_info.g.dart diff --git a/lib/src/models/package_info.dart b/lib/src/models/package_info.dart new file mode 100644 index 0000000..665f717 --- /dev/null +++ b/lib/src/models/package_info.dart @@ -0,0 +1,24 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'package_info.g.dart'; + +@JsonSerializable() +class PackageInfo { + const PackageInfo({ + required this.packageName, + this.appIcon, + this.version, + this.appName, + this.installationSource, + }); + + final String packageName; + final String? appIcon; + final String? appName; + final String? version; + final String? installationSource; + + factory PackageInfo.fromJson(Map json) => + _$PackageInfoFromJson(json); +} + diff --git a/lib/src/models/package_info.g.dart b/lib/src/models/package_info.g.dart new file mode 100644 index 0000000..d6ed553 --- /dev/null +++ b/lib/src/models/package_info.g.dart @@ -0,0 +1,24 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'package_info.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +PackageInfo _$PackageInfoFromJson(Map json) => PackageInfo( + packageName: json['packageName'] as String, + appIcon: json['appIcon'] as String?, + version: json['version'] as String?, + appName: json['appName'] as String?, + installationSource: json['installationSource'] as String?, + ); + +Map _$PackageInfoToJson(PackageInfo instance) => + { + 'packageName': instance.packageName, + 'appIcon': instance.appIcon, + 'appName': instance.appName, + 'version': instance.version, + 'installationSource': instance.installationSource, + }; diff --git a/lib/src/models/suspicious_app_info.dart b/lib/src/models/suspicious_app_info.dart new file mode 100644 index 0000000..246854b --- /dev/null +++ b/lib/src/models/suspicious_app_info.dart @@ -0,0 +1,18 @@ +import 'package:freerasp/src/models/package_info.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'suspicious_app_info.g.dart'; + +@JsonSerializable() +class SuspiciousAppInfo { + const SuspiciousAppInfo({ + required this.packageInfo, + required this.reason, + }); + + final PackageInfo packageInfo; + final String reason; + + factory SuspiciousAppInfo.fromJson(Map json) => + _$SuspiciousAppInfoFromJson(json); +} diff --git a/lib/src/models/suspicious_app_info.g.dart b/lib/src/models/suspicious_app_info.g.dart new file mode 100644 index 0000000..250b832 --- /dev/null +++ b/lib/src/models/suspicious_app_info.g.dart @@ -0,0 +1,20 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'suspicious_app_info.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +SuspiciousAppInfo _$SuspiciousAppInfoFromJson(Map json) => + SuspiciousAppInfo( + packageInfo: + PackageInfo.fromJson(json['packageInfo'] as Map), + reason: json['reason'] as String, + ); + +Map _$SuspiciousAppInfoToJson(SuspiciousAppInfo instance) => + { + 'packageInfo': instance.packageInfo, + 'reason': instance.reason, + }; From 5396bf10cc9a472aefea4a2fa4141d5c533dc5eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Novotn=C3=BD?= Date: Thu, 26 Sep 2024 09:42:29 +0200 Subject: [PATCH 11/66] feat: add data classes for malware (Kotlin) --- .../aheaditec/freerasp/models/PackageInfo.kt | 33 +++++++++++++++++++ .../freerasp/models/SuspiciousAppInfo.kt | 21 ++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 android/src/main/kotlin/com/aheaditec/freerasp/models/PackageInfo.kt create mode 100644 android/src/main/kotlin/com/aheaditec/freerasp/models/SuspiciousAppInfo.kt diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/models/PackageInfo.kt b/android/src/main/kotlin/com/aheaditec/freerasp/models/PackageInfo.kt new file mode 100644 index 0000000..aab9a20 --- /dev/null +++ b/android/src/main/kotlin/com/aheaditec/freerasp/models/PackageInfo.kt @@ -0,0 +1,33 @@ +package com.aheaditec.freerasp.models + +import org.json.JSONObject + +data class PackageInfo( + val packageName: String, + val appIcon: String? = null, + val appName: String? = null, + val version: String? = null, + val installationSource: String? = null +) { + companion object { + fun fromTalsec(packageInfo: android.content.pm.PackageInfo): PackageInfo { + return PackageInfo( + packageInfo.packageName, + packageInfo.appIcon, + packageInfo.appName, + packageInfo.version, + packageInfo.installationSource + ) + } + } + + fun toJson(): String { + val json = JSONObject().put("packageName", packageName) + .putOpt("appIcon", appIcon) + .putOpt("appName", appName) + .putOpt("version", version) + .putOpt("installationSource", installationSource) + + return json.toString() + } +} \ No newline at end of file diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/models/SuspiciousAppInfo.kt b/android/src/main/kotlin/com/aheaditec/freerasp/models/SuspiciousAppInfo.kt new file mode 100644 index 0000000..bb93a8e --- /dev/null +++ b/android/src/main/kotlin/com/aheaditec/freerasp/models/SuspiciousAppInfo.kt @@ -0,0 +1,21 @@ +package com.aheaditec.freerasp.models + +import com.aheaditec.talsec_security.security.api.SuspiciousAppInfo as TalsecSuspiciousAppInfo + +class SuspiciousAppInfo( + val packageInfo: PackageInfo, + val reason: String +) { + companion object { + fun fromTalsec(suspiciousAppInfo: TalsecSuspiciousAppInfo): SuspiciousAppInfo { + return SuspiciousAppInfo( + PackageInfo.fromTalsec(suspiciousAppInfo.packageInfo), + suspiciousAppInfo.reason + ) + } + } + + fun toJson(): String { + return "{\"packageInfo\": ${packageInfo.toJson()}, \"reason\": \"$reason\"}" + } +} \ No newline at end of file From fc03c02591e39e2f4ddc7f34fb53feab1a7e3d36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Novotn=C3=BD?= Date: Thu, 26 Sep 2024 09:42:48 +0200 Subject: [PATCH 12/66] feat: update Android configuration --- lib/src/models/android_config.dart | 12 ++++++++++++ lib/src/models/android_config.g.dart | 23 +++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/lib/src/models/android_config.dart b/lib/src/models/android_config.dart index 5a8356e..e9662b2 100644 --- a/lib/src/models/android_config.dart +++ b/lib/src/models/android_config.dart @@ -11,6 +11,10 @@ class AndroidConfig { required this.packageName, required this.signingCertHashes, this.supportedStores = const [], + this.blocklistedPackageNames = const [], + this.blocklistedHashes = const [], + this.blocklistedPermissions = const >[[]], + this.whitelistedInstallationSources = const [], }) { ConfigVerifier.verifyAndroid(this); } @@ -30,4 +34,12 @@ class AndroidConfig { /// List of supported sources where application can be installed from. final List supportedStores; + + final List blocklistedPackageNames; + + final List blocklistedHashes; + + final List> blocklistedPermissions; + + final List whitelistedInstallationSources; } diff --git a/lib/src/models/android_config.g.dart b/lib/src/models/android_config.g.dart index 021f8a8..d93449d 100644 --- a/lib/src/models/android_config.g.dart +++ b/lib/src/models/android_config.g.dart @@ -16,6 +16,25 @@ AndroidConfig _$AndroidConfigFromJson(Map json) => ?.map((e) => e as String) .toList() ?? const [], + blocklistedPackageNames: + (json['blocklistedPackageNames'] as List?) + ?.map((e) => e as String) + .toList() ?? + const [], + blocklistedHashes: (json['blocklistedHashes'] as List?) + ?.map((e) => e as String) + .toList() ?? + const [], + blocklistedPermissions: (json['blocklistedPermissions'] as List?) + ?.map( + (e) => (e as List).map((e) => e as String).toList()) + .toList() ?? + const >[[]], + whitelistedInstallationSources: + (json['whitelistedInstallationSources'] as List?) + ?.map((e) => e as String) + .toList() ?? + const [], ); Map _$AndroidConfigToJson(AndroidConfig instance) => @@ -23,4 +42,8 @@ Map _$AndroidConfigToJson(AndroidConfig instance) => 'packageName': instance.packageName, 'signingCertHashes': instance.signingCertHashes, 'supportedStores': instance.supportedStores, + 'blocklistedPackageNames': instance.blocklistedPackageNames, + 'blocklistedHashes': instance.blocklistedHashes, + 'blocklistedPermissions': instance.blocklistedPermissions, + 'whitelistedInstallationSources': instance.whitelistedInstallationSources, }; From fada93209324c48c19b67d149f195a574972e133 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Novotn=C3=BD?= Date: Thu, 26 Sep 2024 09:43:02 +0200 Subject: [PATCH 13/66] feat: update Talsec configuration --- lib/src/models/talsec_config.g.dart | 3 +- lib/src/talsec.dart | 54 +++++++++++++++++++++-------- 2 files changed, 40 insertions(+), 17 deletions(-) diff --git a/lib/src/models/talsec_config.g.dart b/lib/src/models/talsec_config.g.dart index f901fc5..41aaa56 100644 --- a/lib/src/models/talsec_config.g.dart +++ b/lib/src/models/talsec_config.g.dart @@ -12,8 +12,7 @@ TalsecConfig _$TalsecConfigFromJson(Map json) => TalsecConfig( androidConfig: json['androidConfig'] == null ? null : AndroidConfig.fromJson( - json['androidConfig'] as Map, - ), + json['androidConfig'] as Map), iosConfig: json['iosConfig'] == null ? null : IOSConfig.fromJson(json['iosConfig'] as Map), diff --git a/lib/src/talsec.dart b/lib/src/talsec.dart index 25ad8cf..7fdc906 100644 --- a/lib/src/talsec.dart +++ b/lib/src/talsec.dart @@ -27,7 +27,17 @@ import 'package:freerasp/freerasp.dart'; class Talsec { /// Private constructor for internal and testing purposes. @visibleForTesting - Talsec.private(this.methodChannel, this.eventChannel); + Talsec.private(this.methodChannel, this.handlerChannel, this.eventChannel) { + handlerChannel.setMethodCallHandler(_methodHandler); + } + + Future _methodHandler(MethodCall call) async { + if (call.method != 'onMalwareDetected') { + return; + } + + print("data"); + } /// Named channel used to communicate with platform plugins. /// @@ -42,8 +52,12 @@ class Talsec { static const MethodChannel _methodChannel = MethodChannel('talsec.app/freerasp/methods'); + static const MethodChannel _handlerChannel = + MethodChannel('talsec.app/freerasp/invoke'); + /// Private [Talsec] variable which holds current instance of class. - static final _instance = Talsec.private(_methodChannel, _eventChannel); + static final _instance = + Talsec.private(_methodChannel, _handlerChannel, _eventChannel); /// Initialize Talsec lazily/obtain current instance of Talsec. static Talsec get instance => _instance; @@ -52,6 +66,10 @@ class Talsec { @visibleForTesting late final MethodChannel methodChannel; + /// [MethodChannel] used to invoke native platform. + @visibleForTesting + late final MethodChannel handlerChannel; + /// [EventChannel] used to receive Threats from the native platform. @visibleForTesting late final EventChannel eventChannel; @@ -60,6 +78,8 @@ class Talsec { Stream? _onThreatDetected; + List _suspiciousAppsCache = []; + /// Returns a broadcast stream. When security is compromised /// [onThreatDetected] receives what type of Threat caused it. /// @@ -97,6 +117,8 @@ class Talsec { return _onThreatDetected!; } + ThreatCallback? _callback; + /// Starts freeRASP with configuration provided in [config]. Future start(TalsecConfig config) { _checkConfig(config); @@ -137,46 +159,47 @@ class Talsec { /// invoked. void attachListener(ThreatCallback callback) { detachListener(); + _callback = callback; _streamSubscription ??= onThreatDetected.listen((event) { switch (event) { case Threat.hooks: - callback.onHooks?.call(); + _callback?.onHooks?.call(); break; case Threat.debug: - callback.onDebug?.call(); + _callback?.onDebug?.call(); break; case Threat.passcode: - callback.onPasscode?.call(); + _callback?.onPasscode?.call(); break; case Threat.deviceId: - callback.onDeviceID?.call(); + _callback?.onDeviceID?.call(); break; case Threat.simulator: - callback.onSimulator?.call(); + _callback?.onSimulator?.call(); break; case Threat.appIntegrity: - callback.onAppIntegrity?.call(); + _callback?.onAppIntegrity?.call(); break; case Threat.obfuscationIssues: - callback.onObfuscationIssues?.call(); + _callback?.onObfuscationIssues?.call(); break; case Threat.deviceBinding: - callback.onDeviceBinding?.call(); + _callback?.onDeviceBinding?.call(); break; case Threat.unofficialStore: - callback.onUnofficialStore?.call(); + _callback?.onUnofficialStore?.call(); break; case Threat.privilegedAccess: - callback.onPrivilegedAccess?.call(); + _callback?.onPrivilegedAccess?.call(); break; case Threat.secureHardwareNotAvailable: - callback.onSecureHardwareNotAvailable?.call(); + _callback?.onSecureHardwareNotAvailable?.call(); break; case Threat.systemVPN: - callback.onSystemVPN?.call(); + _callback?.onSystemVPN?.call(); break; case Threat.devMode: - callback.onDevMode?.call(); + _callback?.onDevMode?.call(); break; } }); @@ -189,6 +212,7 @@ class Talsec { void detachListener() { _streamSubscription?.cancel(); _streamSubscription = null; + _callback = null; } void _handleStreamError(Object error) { From 3158e81b5ac5bca59f7ea61be3e09c4ed6a96da7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Novotn=C3=BD?= Date: Thu, 26 Sep 2024 09:43:49 +0200 Subject: [PATCH 14/66] feat: add Flutter method invocation --- .../com/aheaditec/freerasp/FreeraspPlugin.kt | 5 ++ .../freerasp/handlers/MethodCallInvoker.kt | 72 +++++++++++++++++++ .../freerasp/handlers/PluginThreatHandler.kt | 14 +++- .../freerasp/handlers/StreamHandler.kt | 6 +- .../freerasp/handlers/TalsecThreatHandler.kt | 28 ++++++-- lib/src/typedefs.dart | 4 ++ 6 files changed, 119 insertions(+), 10 deletions(-) create mode 100644 android/src/main/kotlin/com/aheaditec/freerasp/handlers/MethodCallInvoker.kt diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/FreeraspPlugin.kt b/android/src/main/kotlin/com/aheaditec/freerasp/FreeraspPlugin.kt index bda94d4..48ede80 100644 --- a/android/src/main/kotlin/com/aheaditec/freerasp/FreeraspPlugin.kt +++ b/android/src/main/kotlin/com/aheaditec/freerasp/FreeraspPlugin.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleOwner import com.aheaditec.freerasp.handlers.MethodCallHandler +import com.aheaditec.freerasp.handlers.MethodCallInvoker import com.aheaditec.freerasp.handlers.StreamHandler import com.aheaditec.freerasp.handlers.TalsecThreatHandler import io.flutter.embedding.engine.plugins.FlutterPlugin @@ -17,6 +18,8 @@ import io.flutter.embedding.engine.plugins.lifecycle.FlutterLifecycleAdapter class FreeraspPlugin : FlutterPlugin, ActivityAware, LifecycleEventObserver { private var streamHandler: StreamHandler = StreamHandler() private var methodCallHandler: MethodCallHandler = MethodCallHandler() + private var methodCallInvoker : MethodCallInvoker = MethodCallInvoker() + private var context: Context? = null private var lifecycle: Lifecycle? = null @@ -25,11 +28,13 @@ class FreeraspPlugin : FlutterPlugin, ActivityAware, LifecycleEventObserver { context = flutterPluginBinding.applicationContext methodCallHandler.createMethodChannel(messenger, flutterPluginBinding.applicationContext) + methodCallInvoker.createMethodChannel(messenger, flutterPluginBinding.applicationContext) streamHandler.createEventChannel(messenger) } override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { methodCallHandler.destroyMethodChannel() + methodCallInvoker.destroyMethodChannel() streamHandler.destroyEventChannel() TalsecThreatHandler.detachListener(binding.applicationContext) } diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/handlers/MethodCallInvoker.kt b/android/src/main/kotlin/com/aheaditec/freerasp/handlers/MethodCallInvoker.kt new file mode 100644 index 0000000..1f0f5f0 --- /dev/null +++ b/android/src/main/kotlin/com/aheaditec/freerasp/handlers/MethodCallInvoker.kt @@ -0,0 +1,72 @@ +package com.aheaditec.freerasp.handlers + +import android.content.Context +import com.aheaditec.talsec_security.security.api.SuspiciousAppInfo +import io.flutter.Log +import io.flutter.plugin.common.BinaryMessenger +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.MethodCallHandler + +/** + * A method handler that creates and manages an [MethodChannel] for freeRASP methods. + */ +internal class MethodCallInvoker: MethodCallHandler { + private var context: Context? = null + private var methodChannel: MethodChannel? = null + private val methodSink = object : MethodSink { + override fun onMalwareDetected(packageInfo: List) { + methodChannel?.invokeMethod("onMalwareDetected", mapOf("packageInfo" to packageInfo.map { })) + } + } + + companion object { + private const val CHANNEL_NAME: String = "talsec.app/freerasp/invoke" + } + + internal interface MethodSink { + fun onMalwareDetected(packageInfo: List) + } + + /** + * Creates a new [MethodChannel] with the specified [BinaryMessenger] instance. Sets this class + * as the [MethodCallHandler]. + * If an old [MethodChannel] already exists, it will be destroyed before creating a new one. + * + * @param messenger The binary messenger to use for creating the [MethodChannel]. + * @param context The Android [Context] associated with this channel. + */ + fun createMethodChannel(messenger: BinaryMessenger, context: Context) { + methodChannel?.let { + Log.i("MethodCallHandler", "Tried to create channel without disposing old one.") + destroyMethodChannel() + } + + methodChannel = MethodChannel(messenger, CHANNEL_NAME).also { + it.setMethodCallHandler(this) + } + + this.context = context + TalsecThreatHandler.attachMethodSink(methodSink) + } + + /** + * Destroys the `MethodChannel` and clears associated variables. + */ + fun destroyMethodChannel() { + methodChannel?.setMethodCallHandler(null) + methodChannel = null + this.context = null + TalsecThreatHandler.detachMethodSink() + } + + /** + * Handles method calls received through the [MethodChannel]. + * + * @param call The method call. + * @param result The result handler of the method call. + */ + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { + result.error("INVALID", "This channel does not handle calls from Flutter.", null) + } +} diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/handlers/PluginThreatHandler.kt b/android/src/main/kotlin/com/aheaditec/freerasp/handlers/PluginThreatHandler.kt index b2bcc37..8ed309f 100644 --- a/android/src/main/kotlin/com/aheaditec/freerasp/handlers/PluginThreatHandler.kt +++ b/android/src/main/kotlin/com/aheaditec/freerasp/handlers/PluginThreatHandler.kt @@ -2,10 +2,10 @@ package com.aheaditec.freerasp.handlers import android.content.Context import com.aheaditec.freerasp.Threat +import com.aheaditec.talsec_security.security.api.SuspiciousAppInfo import com.aheaditec.talsec_security.security.api.ThreatListener import com.aheaditec.talsec_security.security.api.ThreatListener.DeviceState import com.aheaditec.talsec_security.security.api.ThreatListener.ThreatDetected -import com.aheaditec.talsec_security.security.api.SuspiciousAppInfo /** * A Singleton object that implements the [ThreatDetected] and [DeviceState] interfaces to handle @@ -15,6 +15,8 @@ import com.aheaditec.talsec_security.security.api.SuspiciousAppInfo */ internal object PluginThreatHandler : ThreatDetected, DeviceState { internal val detectedThreats = mutableSetOf() + internal val detectedMalware = mutableListOf() + internal var listener: TalsecFlutter? = null private val internalListener = ThreatListener(this, this) @@ -74,15 +76,21 @@ internal object PluginThreatHandler : ThreatDetected, DeviceState { notify(Threat.DevMode) } - override fun onMalwareDetected(appInfo: List) { - // Nothing to do yet. + override fun onMalwareDetected(suspiciousApps: List) { + notify(suspiciousApps) } private fun notify(threat: Threat) { listener?.threatDetected(threat) ?: detectedThreats.add(threat) } + private fun notify(suspiciousApps: List) { + listener?.malwareDetected(suspiciousApps) ?: detectedMalware.addAll(suspiciousApps) + } + internal interface TalsecFlutter { fun threatDetected(threatType: Threat) + + fun malwareDetected(suspiciousApps: List) } } diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/handlers/StreamHandler.kt b/android/src/main/kotlin/com/aheaditec/freerasp/handlers/StreamHandler.kt index 6e55434..0156c1c 100644 --- a/android/src/main/kotlin/com/aheaditec/freerasp/handlers/StreamHandler.kt +++ b/android/src/main/kotlin/com/aheaditec/freerasp/handlers/StreamHandler.kt @@ -42,7 +42,7 @@ internal class StreamHandler : EventChannel.StreamHandler { // Don't forget to remove old sink // @see https://stackoverflow.com/questions/61934900/tried-to-send-a-platform-message-to-flutter-but-flutterjni-was-detached-from-n - TalsecThreatHandler.detachSink() + TalsecThreatHandler.detachEventSink() } /** @@ -54,7 +54,7 @@ internal class StreamHandler : EventChannel.StreamHandler { */ override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { events?.let { - TalsecThreatHandler.attachSink(it) + TalsecThreatHandler.attachEventSink(it) } } @@ -65,6 +65,6 @@ internal class StreamHandler : EventChannel.StreamHandler { * @param arguments The arguments passed by the subscriber. Not used in this implementation. */ override fun onCancel(arguments: Any?) { - TalsecThreatHandler.detachSink() + TalsecThreatHandler.detachEventSink() } } \ No newline at end of file diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/handlers/TalsecThreatHandler.kt b/android/src/main/kotlin/com/aheaditec/freerasp/handlers/TalsecThreatHandler.kt index 35b381a..cb195c9 100644 --- a/android/src/main/kotlin/com/aheaditec/freerasp/handlers/TalsecThreatHandler.kt +++ b/android/src/main/kotlin/com/aheaditec/freerasp/handlers/TalsecThreatHandler.kt @@ -2,6 +2,7 @@ package com.aheaditec.freerasp.handlers import android.content.Context import com.aheaditec.freerasp.Threat +import com.aheaditec.talsec_security.security.api.SuspiciousAppInfo import com.aheaditec.talsec_security.security.api.Talsec import com.aheaditec.talsec_security.security.api.TalsecConfig import io.flutter.plugin.common.EventChannel.EventSink @@ -12,6 +13,7 @@ import io.flutter.plugin.common.EventChannel.EventSink */ internal object TalsecThreatHandler { private var eventSink: EventSink? = null + private var methodSink: MethodCallInvoker.MethodSink? = null private var isListening = false /** @@ -78,7 +80,7 @@ internal object TalsecThreatHandler { * In contrast to [detachListener], this function does not unregister the listener. It only * suspends the listener, meaning all detected threats are cached and sent later. * - * In contrast to [detachSink], this function does not nullify the [eventSink]. It only suspends + * In contrast to [detachEventSink], this function does not nullify the [eventSink]. It only suspends * sending events to the event sink. This is useful when the application goes to background and * [EventSink] is not destroyed but also is not able to send events. */ @@ -92,7 +94,7 @@ internal object TalsecThreatHandler { * In contrast to [attachListener], this function does not register the listener. It only * resumes the listener, meaning all cached threats are sent to the [EventSink]. * - * In contrast to [attachSink], this function does not assign new [EventSink] to [eventSink]. + * In contrast to [attachEventSink], this function does not assign new [EventSink] to [eventSink]. * It only resumes sending events to the current [eventSink]. * This is useful when the application comes to foreground and [EventSink] is not destroyed but * also is not able to send events. @@ -110,7 +112,7 @@ internal object TalsecThreatHandler { * * @param eventSink The event sink of the new listener. */ - internal fun attachSink(eventSink: EventSink) { + internal fun attachEventSink(eventSink: EventSink) { this.eventSink = eventSink PluginThreatHandler.listener = ThreatListener flushThreatCache(eventSink) @@ -119,7 +121,7 @@ internal object TalsecThreatHandler { /** * Called when a listener unsubscribes from the event channel. */ - internal fun detachSink() { + internal fun detachEventSink() { eventSink = null PluginThreatHandler.listener = null } @@ -134,6 +136,19 @@ internal object TalsecThreatHandler { eventSink?.success(it.value) } PluginThreatHandler.detectedThreats.clear() + + PluginThreatHandler.detectedMalware.let { + methodSink?.onMalwareDetected(it) + } + PluginThreatHandler.detectedMalware.clear() + } + + internal fun attachMethodSink(methodSink: MethodCallInvoker.MethodSink) { + this.methodSink = methodSink + } + + internal fun detachMethodSink() { + methodSink = null } /** @@ -145,5 +160,10 @@ internal object TalsecThreatHandler { override fun threatDetected(threatType: Threat) { eventSink?.success(threatType.value) } + + override fun malwareDetected(suspiciousApps: List) { + methodSink?.onMalwareDetected(suspiciousApps) + } } } + diff --git a/lib/src/typedefs.dart b/lib/src/typedefs.dart index 9d3cbf9..085e5f1 100644 --- a/lib/src/typedefs.dart +++ b/lib/src/typedefs.dart @@ -1,2 +1,6 @@ +import '../freerasp.dart'; + /// Typedef for void methods typedef VoidCallback = void Function(); + +typedef MalwareCallback = void Function(List); From 44a66b4a5ff6d37cb63f273ccf3f73d51ce94e5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Novotn=C3=BD?= Date: Thu, 26 Sep 2024 09:44:15 +0200 Subject: [PATCH 15/66] feat: update example app --- example/lib/main.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/example/lib/main.dart b/example/lib/main.dart index 6a265dd..64be409 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -19,6 +19,7 @@ void main() async { packageName: 'com.aheaditec.freeraspExample', signingCertHashes: ['AKoRuyLMM91E7lX/Zqp3u4jMmd0A7hH/Iqozu0TMVd0='], supportedStores: ['com.sec.android.app.samsungapps'], + blocklistedPackageNames: ['com.aheaditec.freeraspExample'], ), /// For iOS From 566599cad73e0b4b62ec539bb5cdf1b1df622f02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Novotn=C3=BD?= Date: Thu, 26 Sep 2024 09:44:32 +0200 Subject: [PATCH 16/66] feat: update threat callback --- lib/src/threat_callback.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/src/threat_callback.dart b/lib/src/threat_callback.dart index f91b553..c4732b5 100644 --- a/lib/src/threat_callback.dart +++ b/lib/src/threat_callback.dart @@ -33,6 +33,7 @@ class ThreatCallback { this.onSecureHardwareNotAvailable, this.onSystemVPN, this.onDevMode, + this.onMalware, }); /// This method is called when a threat related dynamic hooking (e.g. Frida) @@ -80,4 +81,7 @@ class ThreatCallback { /// This method is called whe the device has Developer mode enabled final VoidCallback? onDevMode; + + /// This method is called when malware is detected on the device + final MalwareCallback? onMalware; } From 7831d5782e8214b94707fa96cf1261456ba4a093 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Novotn=C3=BD?= Date: Thu, 26 Sep 2024 09:44:41 +0200 Subject: [PATCH 17/66] feat: misc --- .../com/aheaditec/freerasp/utils/Utils.kt | 75 ++++++++++++++----- lib/src/models/models.dart | 2 + test/src/talsec_test.dart | 49 ++++++++---- 3 files changed, 95 insertions(+), 31 deletions(-) diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/utils/Utils.kt b/android/src/main/kotlin/com/aheaditec/freerasp/utils/Utils.kt index 43efdea..a51e1b2 100644 --- a/android/src/main/kotlin/com/aheaditec/freerasp/utils/Utils.kt +++ b/android/src/main/kotlin/com/aheaditec/freerasp/utils/Utils.kt @@ -1,5 +1,6 @@ package com.aheaditec.freerasp.utils +import com.aheaditec.talsec_security.security.api.SuspiciousAppInfo import com.aheaditec.talsec_security.security.api.TalsecConfig import org.json.JSONException import org.json.JSONObject @@ -11,31 +12,71 @@ internal class Utils { throw JSONException("Configuration is null") } val json = JSONObject(configJson) - val androidConfig = json.getJSONObject("androidConfig") - val packageName = androidConfig.getString("packageName") - val certificateHashes = mutableListOf() - val hashes = androidConfig.getJSONArray("signingCertHashes") - for (i in 0 until hashes.length()) { - certificateHashes.add(hashes.getString(i)) - } + val watcherMail = json.getString("watcherMail") - val alternativeStores = mutableListOf() - if (androidConfig.has("supportedStores")) { - val stores = androidConfig.getJSONArray("supportedStores") - for (i in 0 until stores.length()) { - alternativeStores.add(stores.getString(i)) - } - } var isProd = true if (json.has("isProd")) { isProd = json.getBoolean("isProd") } + val androidConfig = json.getJSONObject("androidConfig") - return TalsecConfig.Builder(packageName, certificateHashes.toTypedArray()) + val packageName = androidConfig.getString("packageName") + val certificateHashes = androidConfig.extractArray("signingCertHashes") + val alternativeStores = androidConfig.extractArray("supportedStores") + val blocklistedPackageNames = + androidConfig.extractArray("blocklistedPackageNames") + val blocklistedHashes = androidConfig.extractArray("blocklistedHashes") + val whitelistedInstallationSources = + androidConfig.extractArray("whitelistedInstallationSources") + + val blocklistedPermissions = mutableListOf>() + if (androidConfig.has("blocklistedPermissions")) { + val permissions = androidConfig.getJSONArray("blocklistedPermissions") + for (i in 0 until permissions.length()) { + val permission = permissions.getJSONArray(i) + val permissionList = mutableListOf() + for (j in 0 until permission.length()) { + permissionList.add(permission.getString(j)) + } + blocklistedPermissions.add(permissionList.toTypedArray()) + } + } + + return TalsecConfig.Builder(packageName, certificateHashes) .watcherMail(watcherMail) - .supportedAlternativeStores(alternativeStores.toTypedArray()) + .supportedAlternativeStores(alternativeStores) .prod(isProd) + .blocklistedPackageNames(blocklistedPackageNames) + .blocklistedHashes(blocklistedHashes) + .blocklistedPermissions(blocklistedPermissions.toTypedArray()) + .whitelistedInstallationSources(whitelistedInstallationSources) .build() } + + fun fromTalsec(malwareInfo: List) { + val packageInfoList = mutableListOf() + for (info in malwareInfo) { + packageInfoList.add(info.toJson()) + } + } + } +} + +inline fun JSONObject.extractArray(key: String): Array { + val list = mutableListOf() + if (this.has(key)) { + val jsonArray = this.getJSONArray(key) + for (i in 0 until jsonArray.length()) { + val element = when (T::class) { + String::class -> jsonArray.getString(i) as T + Int::class -> jsonArray.getInt(i) as T + Double::class -> jsonArray.getDouble(i) as T + Boolean::class -> jsonArray.getBoolean(i) as T + Long::class -> jsonArray.getLong(i) as T + else -> throw IllegalArgumentException("Unsupported type") + } + list.add(element) + } } -} \ No newline at end of file + return list.toTypedArray() +} diff --git a/lib/src/models/models.dart b/lib/src/models/models.dart index d18e85e..a24b299 100644 --- a/lib/src/models/models.dart +++ b/lib/src/models/models.dart @@ -1,3 +1,5 @@ export 'android_config.dart'; export 'ios_config.dart'; +export 'package_info.dart'; +export 'suspicious_app_info.dart'; export 'talsec_config.dart'; diff --git a/test/src/talsec_test.dart b/test/src/talsec_test.dart index 605b1ab..8e9fa8b 100644 --- a/test/src/talsec_test.dart +++ b/test/src/talsec_test.dart @@ -43,8 +43,11 @@ void main() { watcherMail: mockWatcherMail, ); // Act - final talsec = - Talsec.private(methodChannel.methodChannel, FakeEventChannel()); + final talsec = Talsec.private( + methodChannel.methodChannel, + methodChannel.methodChannel, + FakeEventChannel(), + ); // Assert expect( @@ -67,8 +70,11 @@ void main() { watcherMail: mockWatcherMail, ); // Act - final talsec = - Talsec.private(methodChannel.methodChannel, FakeEventChannel()); + final talsec = Talsec.private( + methodChannel.methodChannel, + methodChannel.methodChannel, + FakeEventChannel(), + ); // Assert expect( @@ -87,8 +93,11 @@ void main() { debugDefaultTargetPlatformOverride = TargetPlatform.android; // Act - final talsec = - Talsec.private(methodChannel.methodChannel, FakeEventChannel()); + final talsec = Talsec.private( + methodChannel.methodChannel, + methodChannel.methodChannel, + FakeEventChannel(), + ); // Assert expect( @@ -113,8 +122,11 @@ void main() { ); // Act - final talsec = - Talsec.private(methodChannel.methodChannel, FakeEventChannel()); + final talsec = Talsec.private( + methodChannel.methodChannel, + methodChannel.methodChannel, + FakeEventChannel(), + ); // Assert expect( @@ -151,8 +163,11 @@ void main() { 1115787534, ], ); - final talsec = - Talsec.private(FakeMethodChannel(), eventChannel.eventChannel); + final talsec = Talsec.private( + FakeMethodChannel(), + FakeMethodChannel(), + eventChannel.eventChannel, + ); // Act final stream = talsec.onThreatDetected; @@ -174,8 +189,11 @@ void main() { data: [], exceptions: [PlatformException(code: 'dummy-code')], ); - final talsec = - Talsec.private(FakeMethodChannel(), eventChannel.eventChannel); + final talsec = Talsec.private( + FakeMethodChannel(), + FakeMethodChannel(), + eventChannel.eventChannel, + ); // Act final stream = talsec.onThreatDetected; @@ -205,8 +223,11 @@ void main() { ], exceptions: [PlatformException(code: 'dummy-code')], ); - final talsec = - Talsec.private(FakeMethodChannel(), eventChannel.eventChannel); + final talsec = Talsec.private( + FakeMethodChannel(), + FakeMethodChannel(), + eventChannel.eventChannel, + ); // Act final stream = talsec.onThreatDetected; From 587dbb6dfb3fe9cc683f6e97f75294dc2d1688c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Novotn=C3=BD?= Date: Mon, 30 Sep 2024 11:44:12 +0200 Subject: [PATCH 18/66] feat: so much code --- analysis_options.yaml | 4 + .../com/aheaditec/freerasp/Extensions.kt | 70 ++++++++ .../com/aheaditec/freerasp/FreeraspPlugin.kt | 4 - .../kotlin/com/aheaditec/freerasp/Utils.kt | 158 ++++++++++++++++++ .../freerasp/generated/TalsecPigeonApi.kt | 136 +++++++++++++++ .../freerasp/handlers/MethodCallHandler.kt | 33 +++- .../freerasp/handlers/MethodCallInvoker.kt | 72 -------- .../freerasp/handlers/TalsecThreatHandler.kt | 13 +- .../aheaditec/freerasp/models/PackageInfo.kt | 33 ---- .../freerasp/models/SuspiciousAppInfo.kt | 21 --- .../com/aheaditec/freerasp/utils/Utils.kt | 82 --------- example/lib/threat_notifier.dart | 2 + lib/src/generated/talsec_pigeon_api.g.dart | 153 +++++++++++++++++ lib/src/models/models.dart | 2 - lib/src/models/package_info.dart | 24 --- lib/src/models/package_info.g.dart | 24 --- lib/src/models/suspicious_app_info.dart | 18 -- lib/src/models/suspicious_app_info.g.dart | 20 --- lib/src/talsec.dart | 55 ++---- lib/src/threat_callback.dart | 12 +- lib/src/typedefs.dart | 5 +- pigeons/talsec_pigeon_api.dart | 44 +++++ pubspec.yaml | 1 + test/src/talsec_test.dart | 7 - 24 files changed, 638 insertions(+), 355 deletions(-) create mode 100644 android/src/main/kotlin/com/aheaditec/freerasp/Utils.kt create mode 100644 android/src/main/kotlin/com/aheaditec/freerasp/generated/TalsecPigeonApi.kt delete mode 100644 android/src/main/kotlin/com/aheaditec/freerasp/handlers/MethodCallInvoker.kt delete mode 100644 android/src/main/kotlin/com/aheaditec/freerasp/models/PackageInfo.kt delete mode 100644 android/src/main/kotlin/com/aheaditec/freerasp/models/SuspiciousAppInfo.kt delete mode 100644 android/src/main/kotlin/com/aheaditec/freerasp/utils/Utils.kt create mode 100644 lib/src/generated/talsec_pigeon_api.g.dart delete mode 100644 lib/src/models/package_info.dart delete mode 100644 lib/src/models/package_info.g.dart delete mode 100644 lib/src/models/suspicious_app_info.dart delete mode 100644 lib/src/models/suspicious_app_info.g.dart create mode 100644 pigeons/talsec_pigeon_api.dart diff --git a/analysis_options.yaml b/analysis_options.yaml index 273cb60..f74ab60 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1 +1,5 @@ include: package:very_good_analysis/analysis_options.3.1.0.yaml + +analyzer: + exclude: + - '**/*.g.dart' \ No newline at end of file diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/Extensions.kt b/android/src/main/kotlin/com/aheaditec/freerasp/Extensions.kt index ae3c415..0a86ebd 100644 --- a/android/src/main/kotlin/com/aheaditec/freerasp/Extensions.kt +++ b/android/src/main/kotlin/com/aheaditec/freerasp/Extensions.kt @@ -1,6 +1,12 @@ package com.aheaditec.freerasp +import android.content.Context +import android.content.pm.PackageInfo +import android.os.Build +import com.aheaditec.talsec_security.security.api.SuspiciousAppInfo import io.flutter.plugin.common.MethodChannel +import com.aheaditec.freerasp.generated.PackageInfo as FlutterPackageInfo +import com.aheaditec.freerasp.generated.SuspiciousAppInfo as FlutterSuspiciousAppInfo /** * Executes the provided block of code and catches any exceptions thrown by it, returning the @@ -17,3 +23,67 @@ internal inline fun runResultCatching(result: MethodChannel.Result, block: () -> result.error(err::class.java.name, err.message, null) } } + +/** + * Converts a [SuspiciousAppInfo] instance to a [com.aheaditec.freerasp.generated.SuspiciousAppInfo] + * instance used by Pigeon package for Flutter. + * + * @return A new [com.aheaditec.freerasp.generated.SuspiciousAppInfo] object with information from + * this [SuspiciousAppInfo]. + */ +internal fun SuspiciousAppInfo.toPigeon(context: Context): FlutterSuspiciousAppInfo { + return FlutterSuspiciousAppInfo(this.packageInfo.toPigeon(context), this.reason) +} + +/** + * Converts a [PackageInfo] instance to a [com.aheaditec.freerasp.generated.PackageInfo] instance + * used by Pigeon package for Flutter. + * + * @return A new [com.aheaditec.freerasp.generated.PackageInfo] object with information from + * this [PackageInfo]. + */ +private fun PackageInfo.toPigeon(context: Context): FlutterPackageInfo { + return FlutterPackageInfo( + packageName = packageName, + appName = applicationInfo?.name, + version = getVersionString(), + appIcon = Utils.parseIconBase64(context, packageName), + installationSource = Utils.getInstallerPackageName(context, packageName), + ) +} + +/** + * Retrieves the version string of the package. + * + * For devices running on Android P (API 28) and above, this method returns the `longVersionCode`. + * For older versions, it returns the `versionCode` (deprecated). + * + * @return A string representation of the version code. + */ +internal fun PackageInfo.getVersionString(): String { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + return longVersionCode.toString() + } + @Suppress("DEPRECATION") + return versionCode.toString() +} + +/** + * Returns the encapsulated value if this instance represents success or throws the encapsulated exception + * if it is a failure, executing the given action before throwing. + * + * This function is similar to `Result.getOrThrow()`, but with the added functionality of performing + * an action before throwing the exception. + * + * @param action The action to be executed if the result is a failure. This action should not throw an exception. + * @return The encapsulated value if the result is a success. + * @throws Throwable The encapsulated exception if the result is a failure. + * + * @see Result.getOrThrow + */ +inline fun Result.getOrElseThenThrow(action: () -> Unit): T { + return getOrElse { + action() + throw it + } +} \ No newline at end of file diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/FreeraspPlugin.kt b/android/src/main/kotlin/com/aheaditec/freerasp/FreeraspPlugin.kt index 48ede80..4188a4a 100644 --- a/android/src/main/kotlin/com/aheaditec/freerasp/FreeraspPlugin.kt +++ b/android/src/main/kotlin/com/aheaditec/freerasp/FreeraspPlugin.kt @@ -5,7 +5,6 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleOwner import com.aheaditec.freerasp.handlers.MethodCallHandler -import com.aheaditec.freerasp.handlers.MethodCallInvoker import com.aheaditec.freerasp.handlers.StreamHandler import com.aheaditec.freerasp.handlers.TalsecThreatHandler import io.flutter.embedding.engine.plugins.FlutterPlugin @@ -18,7 +17,6 @@ import io.flutter.embedding.engine.plugins.lifecycle.FlutterLifecycleAdapter class FreeraspPlugin : FlutterPlugin, ActivityAware, LifecycleEventObserver { private var streamHandler: StreamHandler = StreamHandler() private var methodCallHandler: MethodCallHandler = MethodCallHandler() - private var methodCallInvoker : MethodCallInvoker = MethodCallInvoker() private var context: Context? = null private var lifecycle: Lifecycle? = null @@ -28,13 +26,11 @@ class FreeraspPlugin : FlutterPlugin, ActivityAware, LifecycleEventObserver { context = flutterPluginBinding.applicationContext methodCallHandler.createMethodChannel(messenger, flutterPluginBinding.applicationContext) - methodCallInvoker.createMethodChannel(messenger, flutterPluginBinding.applicationContext) streamHandler.createEventChannel(messenger) } override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { methodCallHandler.destroyMethodChannel() - methodCallInvoker.destroyMethodChannel() streamHandler.destroyEventChannel() TalsecThreatHandler.detachListener(binding.applicationContext) } diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/Utils.kt b/android/src/main/kotlin/com/aheaditec/freerasp/Utils.kt new file mode 100644 index 0000000..37c97cc --- /dev/null +++ b/android/src/main/kotlin/com/aheaditec/freerasp/Utils.kt @@ -0,0 +1,158 @@ +package com.aheaditec.freerasp + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import android.os.Build +import android.util.Base64 +import com.aheaditec.talsec_security.security.api.TalsecConfig +import org.json.JSONException +import org.json.JSONObject +import java.io.ByteArrayOutputStream + +internal object Utils { + fun toTalsecConfigThrowing(configJson: String?): TalsecConfig { + if (configJson == null) { + throw JSONException("Configuration is null") + } + val json = JSONObject(configJson) + + val watcherMail = json.getString("watcherMail") + var isProd = true + if (json.has("isProd")) { + isProd = json.getBoolean("isProd") + } + val androidConfig = json.getJSONObject("androidConfig") + + val packageName = androidConfig.getString("packageName") + val certificateHashes = androidConfig.extractArray("signingCertHashes") + val alternativeStores = androidConfig.extractArray("supportedStores") + val blocklistedPackageNames = + androidConfig.extractArray("blocklistedPackageNames") + val blocklistedHashes = androidConfig.extractArray("blocklistedHashes") + val whitelistedInstallationSources = + androidConfig.extractArray("whitelistedInstallationSources") + + val blocklistedPermissions = mutableListOf>() + if (androidConfig.has("blocklistedPermissions")) { + val permissions = androidConfig.getJSONArray("blocklistedPermissions") + for (i in 0 until permissions.length()) { + val permission = permissions.getJSONArray(i) + val permissionList = mutableListOf() + for (j in 0 until permission.length()) { + permissionList.add(permission.getString(j)) + } + blocklistedPermissions.add(permissionList.toTypedArray()) + } + } + + return TalsecConfig.Builder(packageName, certificateHashes) + .watcherMail(watcherMail) + .supportedAlternativeStores(alternativeStores) + .prod(isProd) + .blocklistedPackageNames(blocklistedPackageNames) + .blocklistedHashes(blocklistedHashes) + .blocklistedPermissions(blocklistedPermissions.toTypedArray()) + .whitelistedInstallationSources(whitelistedInstallationSources) + .build() + } + + /** + * Retrieves the package name of the installer for a given app package. + * + * @param context The context of the application. + * @param packageName The package name of the app whose installer package name is to be retrieved. + * @return The package name of the installer if available, or `null` if not. + */ + fun getInstallerPackageName(context: Context, packageName: String): String? { + runCatching { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) + return context.packageManager.getInstallSourceInfo(packageName).installingPackageName + @Suppress("DEPRECATION") + return context.packageManager.getInstallerPackageName(packageName) + } + return null + } + + /** + * Converts the application icon of the specified package into a Base64 encoded string. + * + * @param context The context of the application. + * @param packageName The package name of the app whose icon is to be converted. + * @return A Base64 encoded string representing the app icon. + */ + fun parseIconBase64(context: Context, packageName: String): String? { + val result = runCatching { + val drawable = context.packageManager.getApplicationIcon(packageName) + val bitmap = drawable.toBitmap() + bitmap.toBase64() + } + + return result.getOrNull() + } + + /** + * Creates a Bitmap from a Drawable object. + * + * @param drawable The Drawable to be converted. + * @return A Bitmap representing the drawable. + */ + private fun createBitmapFromDrawable(drawable: Drawable): Bitmap { + val width = if (drawable.intrinsicWidth > 0) drawable.intrinsicWidth else 1 + val height = if (drawable.intrinsicHeight > 0) drawable.intrinsicHeight else 1 + val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) + val canvas = Canvas(bitmap) + + drawable.setBounds(0, 0, canvas.width, canvas.height) + drawable.draw(canvas) + + return bitmap + } + + /** + * Converts a Drawable into a Bitmap. + * + * @receiver The Drawable to be converted. + * @return A Bitmap representing the drawable. + */ + private fun Drawable.toBitmap(): Bitmap { + return when (this) { + is BitmapDrawable -> bitmap + else -> createBitmapFromDrawable(this) + } + } + + /** + * Converts a Bitmap into a Base64 encoded string. + * + * @receiver The Bitmap to be converted. + * @return A Base64 encoded string representing the bitmap. + */ + private fun Bitmap.toBase64(): String { + val byteArrayOutputStream = ByteArrayOutputStream() + compress(Bitmap.CompressFormat.PNG, 10, byteArrayOutputStream) + val byteArray = byteArrayOutputStream.toByteArray() + return Base64.encodeToString(byteArray, Base64.DEFAULT) + } +} + +inline fun JSONObject.extractArray(key: String): Array { + val list = mutableListOf() + if (this.has(key)) { + val jsonArray = this.getJSONArray(key) + for (i in 0 until jsonArray.length()) { + val element = when (T::class) { + String::class -> jsonArray.getString(i) as T + Int::class -> jsonArray.getInt(i) as T + Double::class -> jsonArray.getDouble(i) as T + Boolean::class -> jsonArray.getBoolean(i) as T + Long::class -> jsonArray.getLong(i) as T + else -> throw IllegalArgumentException("Unsupported type") + } + list.add(element) + } + } + return list.toTypedArray() +} diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/generated/TalsecPigeonApi.kt b/android/src/main/kotlin/com/aheaditec/freerasp/generated/TalsecPigeonApi.kt new file mode 100644 index 0000000..e75fd00 --- /dev/null +++ b/android/src/main/kotlin/com/aheaditec/freerasp/generated/TalsecPigeonApi.kt @@ -0,0 +1,136 @@ +// Autogenerated from Pigeon (v22.4.0), do not edit directly. +// See also: https://pub.dev/packages/pigeon +@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass") + +package com.aheaditec.freerasp.generated + +import android.util.Log +import io.flutter.plugin.common.BasicMessageChannel +import io.flutter.plugin.common.BinaryMessenger +import io.flutter.plugin.common.MessageCodec +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() + +/** 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 installationSource: 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 installationSource = pigeonVar_list[4] as String? + return PackageInfo(packageName, appIcon, appName, version, installationSource) + } + } + fun toList(): List { + return listOf( + packageName, + appIcon, + appName, + version, + installationSource, + ) + } +} + +/** Generated class from Pigeon that represents data sent in messages. */ +data class SuspiciousAppInfo ( + val packageInfo: PackageInfo, + val reason: String +) + { + companion object { + fun fromList(pigeonVar_list: List): SuspiciousAppInfo { + val packageInfo = pigeonVar_list[0] as PackageInfo + val reason = pigeonVar_list[1] as String + return SuspiciousAppInfo(packageInfo, reason) + } + } + fun toList(): List { + return listOf( + packageInfo, + reason, + ) + } +} +private open class TalsecPigeonApiPigeonCodec : StandardMessageCodec() { + override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { + return when (type) { + 129.toByte() -> { + return (readValue(buffer) as? List)?.let { + PackageInfo.fromList(it) + } + } + 130.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 PackageInfo -> { + stream.write(129) + writeValue(stream, value.toList()) + } + is SuspiciousAppInfo -> { + stream.write(130) + 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))) + } + } + } +} diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/handlers/MethodCallHandler.kt b/android/src/main/kotlin/com/aheaditec/freerasp/handlers/MethodCallHandler.kt index e6d47de..e8208ad 100644 --- a/android/src/main/kotlin/com/aheaditec/freerasp/handlers/MethodCallHandler.kt +++ b/android/src/main/kotlin/com/aheaditec/freerasp/handlers/MethodCallHandler.kt @@ -2,7 +2,11 @@ package com.aheaditec.freerasp.handlers import android.content.Context import com.aheaditec.freerasp.runResultCatching -import com.aheaditec.freerasp.utils.Utils +import com.aheaditec.freerasp.Utils +import com.aheaditec.freerasp.generated.TalsecPigeonApi +import com.aheaditec.freerasp.getOrElseThenThrow +import com.aheaditec.freerasp.toPigeon +import com.aheaditec.talsec_security.security.api.SuspiciousAppInfo import io.flutter.Log import io.flutter.plugin.common.BinaryMessenger import io.flutter.plugin.common.MethodCall @@ -15,11 +19,31 @@ import io.flutter.plugin.common.MethodChannel.MethodCallHandler internal class MethodCallHandler : MethodCallHandler { private var context: Context? = null private var methodChannel: MethodChannel? = null + private var pigeonApi: TalsecPigeonApi? = null companion object { 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) } + pigeonApi?.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.getOrElseThenThrow { + Log.e("MethodCallHandlerSink", "Result ended with failure") + } + } + } + } + } + + internal interface MethodSink { + fun onMalwareDetected(packageInfo: List) + } + /** * Creates a new [MethodChannel] with the specified [BinaryMessenger] instance. Sets this class * as the [MethodCallHandler]. @@ -39,6 +63,9 @@ internal class MethodCallHandler : MethodCallHandler { } this.context = context + this.pigeonApi = TalsecPigeonApi(messenger) + + TalsecThreatHandler.attachMethodSink(sink) } /** @@ -47,7 +74,11 @@ internal class MethodCallHandler : MethodCallHandler { fun destroyMethodChannel() { methodChannel?.setMethodCallHandler(null) methodChannel = null + this.context = null + this.pigeonApi = null + + TalsecThreatHandler.detachMethodSink() } /** diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/handlers/MethodCallInvoker.kt b/android/src/main/kotlin/com/aheaditec/freerasp/handlers/MethodCallInvoker.kt deleted file mode 100644 index 1f0f5f0..0000000 --- a/android/src/main/kotlin/com/aheaditec/freerasp/handlers/MethodCallInvoker.kt +++ /dev/null @@ -1,72 +0,0 @@ -package com.aheaditec.freerasp.handlers - -import android.content.Context -import com.aheaditec.talsec_security.security.api.SuspiciousAppInfo -import io.flutter.Log -import io.flutter.plugin.common.BinaryMessenger -import io.flutter.plugin.common.MethodCall -import io.flutter.plugin.common.MethodChannel -import io.flutter.plugin.common.MethodChannel.MethodCallHandler - -/** - * A method handler that creates and manages an [MethodChannel] for freeRASP methods. - */ -internal class MethodCallInvoker: MethodCallHandler { - private var context: Context? = null - private var methodChannel: MethodChannel? = null - private val methodSink = object : MethodSink { - override fun onMalwareDetected(packageInfo: List) { - methodChannel?.invokeMethod("onMalwareDetected", mapOf("packageInfo" to packageInfo.map { })) - } - } - - companion object { - private const val CHANNEL_NAME: String = "talsec.app/freerasp/invoke" - } - - internal interface MethodSink { - fun onMalwareDetected(packageInfo: List) - } - - /** - * Creates a new [MethodChannel] with the specified [BinaryMessenger] instance. Sets this class - * as the [MethodCallHandler]. - * If an old [MethodChannel] already exists, it will be destroyed before creating a new one. - * - * @param messenger The binary messenger to use for creating the [MethodChannel]. - * @param context The Android [Context] associated with this channel. - */ - fun createMethodChannel(messenger: BinaryMessenger, context: Context) { - methodChannel?.let { - Log.i("MethodCallHandler", "Tried to create channel without disposing old one.") - destroyMethodChannel() - } - - methodChannel = MethodChannel(messenger, CHANNEL_NAME).also { - it.setMethodCallHandler(this) - } - - this.context = context - TalsecThreatHandler.attachMethodSink(methodSink) - } - - /** - * Destroys the `MethodChannel` and clears associated variables. - */ - fun destroyMethodChannel() { - methodChannel?.setMethodCallHandler(null) - methodChannel = null - this.context = null - TalsecThreatHandler.detachMethodSink() - } - - /** - * Handles method calls received through the [MethodChannel]. - * - * @param call The method call. - * @param result The result handler of the method call. - */ - override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { - result.error("INVALID", "This channel does not handle calls from Flutter.", null) - } -} diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/handlers/TalsecThreatHandler.kt b/android/src/main/kotlin/com/aheaditec/freerasp/handlers/TalsecThreatHandler.kt index cb195c9..cfdf562 100644 --- a/android/src/main/kotlin/com/aheaditec/freerasp/handlers/TalsecThreatHandler.kt +++ b/android/src/main/kotlin/com/aheaditec/freerasp/handlers/TalsecThreatHandler.kt @@ -13,7 +13,7 @@ import io.flutter.plugin.common.EventChannel.EventSink */ internal object TalsecThreatHandler { private var eventSink: EventSink? = null - private var methodSink: MethodCallInvoker.MethodSink? = null + private var methodSink: MethodCallHandler.MethodSink? = null private var isListening = false /** @@ -135,16 +135,19 @@ internal object TalsecThreatHandler { PluginThreatHandler.detectedThreats.forEach { eventSink?.success(it.value) } - PluginThreatHandler.detectedThreats.clear() PluginThreatHandler.detectedMalware.let { - methodSink?.onMalwareDetected(it) + if (it.isNotEmpty()) { + methodSink?.onMalwareDetected(it) + } } + + PluginThreatHandler.detectedThreats.clear() PluginThreatHandler.detectedMalware.clear() } - internal fun attachMethodSink(methodSink: MethodCallInvoker.MethodSink) { - this.methodSink = methodSink + internal fun attachMethodSink(sink: MethodCallHandler.MethodSink) { + this.methodSink = sink } internal fun detachMethodSink() { diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/models/PackageInfo.kt b/android/src/main/kotlin/com/aheaditec/freerasp/models/PackageInfo.kt deleted file mode 100644 index aab9a20..0000000 --- a/android/src/main/kotlin/com/aheaditec/freerasp/models/PackageInfo.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.aheaditec.freerasp.models - -import org.json.JSONObject - -data class PackageInfo( - val packageName: String, - val appIcon: String? = null, - val appName: String? = null, - val version: String? = null, - val installationSource: String? = null -) { - companion object { - fun fromTalsec(packageInfo: android.content.pm.PackageInfo): PackageInfo { - return PackageInfo( - packageInfo.packageName, - packageInfo.appIcon, - packageInfo.appName, - packageInfo.version, - packageInfo.installationSource - ) - } - } - - fun toJson(): String { - val json = JSONObject().put("packageName", packageName) - .putOpt("appIcon", appIcon) - .putOpt("appName", appName) - .putOpt("version", version) - .putOpt("installationSource", installationSource) - - return json.toString() - } -} \ No newline at end of file diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/models/SuspiciousAppInfo.kt b/android/src/main/kotlin/com/aheaditec/freerasp/models/SuspiciousAppInfo.kt deleted file mode 100644 index bb93a8e..0000000 --- a/android/src/main/kotlin/com/aheaditec/freerasp/models/SuspiciousAppInfo.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.aheaditec.freerasp.models - -import com.aheaditec.talsec_security.security.api.SuspiciousAppInfo as TalsecSuspiciousAppInfo - -class SuspiciousAppInfo( - val packageInfo: PackageInfo, - val reason: String -) { - companion object { - fun fromTalsec(suspiciousAppInfo: TalsecSuspiciousAppInfo): SuspiciousAppInfo { - return SuspiciousAppInfo( - PackageInfo.fromTalsec(suspiciousAppInfo.packageInfo), - suspiciousAppInfo.reason - ) - } - } - - fun toJson(): String { - return "{\"packageInfo\": ${packageInfo.toJson()}, \"reason\": \"$reason\"}" - } -} \ No newline at end of file diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/utils/Utils.kt b/android/src/main/kotlin/com/aheaditec/freerasp/utils/Utils.kt deleted file mode 100644 index a51e1b2..0000000 --- a/android/src/main/kotlin/com/aheaditec/freerasp/utils/Utils.kt +++ /dev/null @@ -1,82 +0,0 @@ -package com.aheaditec.freerasp.utils - -import com.aheaditec.talsec_security.security.api.SuspiciousAppInfo -import com.aheaditec.talsec_security.security.api.TalsecConfig -import org.json.JSONException -import org.json.JSONObject - -internal class Utils { - companion object { - fun toTalsecConfigThrowing(configJson: String?): TalsecConfig { - if (configJson == null) { - throw JSONException("Configuration is null") - } - val json = JSONObject(configJson) - - val watcherMail = json.getString("watcherMail") - var isProd = true - if (json.has("isProd")) { - isProd = json.getBoolean("isProd") - } - val androidConfig = json.getJSONObject("androidConfig") - - val packageName = androidConfig.getString("packageName") - val certificateHashes = androidConfig.extractArray("signingCertHashes") - val alternativeStores = androidConfig.extractArray("supportedStores") - val blocklistedPackageNames = - androidConfig.extractArray("blocklistedPackageNames") - val blocklistedHashes = androidConfig.extractArray("blocklistedHashes") - val whitelistedInstallationSources = - androidConfig.extractArray("whitelistedInstallationSources") - - val blocklistedPermissions = mutableListOf>() - if (androidConfig.has("blocklistedPermissions")) { - val permissions = androidConfig.getJSONArray("blocklistedPermissions") - for (i in 0 until permissions.length()) { - val permission = permissions.getJSONArray(i) - val permissionList = mutableListOf() - for (j in 0 until permission.length()) { - permissionList.add(permission.getString(j)) - } - blocklistedPermissions.add(permissionList.toTypedArray()) - } - } - - return TalsecConfig.Builder(packageName, certificateHashes) - .watcherMail(watcherMail) - .supportedAlternativeStores(alternativeStores) - .prod(isProd) - .blocklistedPackageNames(blocklistedPackageNames) - .blocklistedHashes(blocklistedHashes) - .blocklistedPermissions(blocklistedPermissions.toTypedArray()) - .whitelistedInstallationSources(whitelistedInstallationSources) - .build() - } - - fun fromTalsec(malwareInfo: List) { - val packageInfoList = mutableListOf() - for (info in malwareInfo) { - packageInfoList.add(info.toJson()) - } - } - } -} - -inline fun JSONObject.extractArray(key: String): Array { - val list = mutableListOf() - if (this.has(key)) { - val jsonArray = this.getJSONArray(key) - for (i in 0 until jsonArray.length()) { - val element = when (T::class) { - String::class -> jsonArray.getString(i) as T - Int::class -> jsonArray.getInt(i) as T - Double::class -> jsonArray.getDouble(i) as T - Boolean::class -> jsonArray.getBoolean(i) as T - Long::class -> jsonArray.getLong(i) as T - else -> throw IllegalArgumentException("Unsupported type") - } - list.add(element) - } - } - return list.toTypedArray() -} diff --git a/example/lib/threat_notifier.dart b/example/lib/threat_notifier.dart index 2fe83fc..e68945b 100644 --- a/example/lib/threat_notifier.dart +++ b/example/lib/threat_notifier.dart @@ -1,3 +1,4 @@ +import 'dart:developer'; import 'dart:io'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -22,6 +23,7 @@ class ThreatNotifier extends StateNotifier> { onUnofficialStore: () => _updateThreat(Threat.unofficialStore), onSystemVPN: () => _updateThreat(Threat.systemVPN), onDevMode: () => _updateThreat(Threat.devMode), + onMalware: (threat) => log('Malware detected: $threat'), ); Talsec.instance.attachListener(callback); diff --git a/lib/src/generated/talsec_pigeon_api.g.dart b/lib/src/generated/talsec_pigeon_api.g.dart new file mode 100644 index 0000000..57d8406 --- /dev/null +++ b/lib/src/generated/talsec_pigeon_api.g.dart @@ -0,0 +1,153 @@ +// Autogenerated from Pigeon (v22.4.0), 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]; +} + +class PackageInfo { + PackageInfo({ + required this.packageName, + this.appIcon, + this.appName, + this.version, + this.installationSource, + }); + + String packageName; + + String? appIcon; + + String? appName; + + String? version; + + String? installationSource; + + Object encode() { + return [ + packageName, + appIcon, + appName, + version, + installationSource, + ]; + } + + 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?, + installationSource: result[4] as String?, + ); + } +} + +class SuspiciousAppInfo { + SuspiciousAppInfo({ + required this.packageInfo, + required this.reason, + }); + + PackageInfo packageInfo; + + String reason; + + Object encode() { + return [ + packageInfo, + reason, + ]; + } + + static SuspiciousAppInfo decode(Object result) { + result as List; + return SuspiciousAppInfo( + packageInfo: result[0]! as PackageInfo, + reason: result[1]! as String, + ); + } +} + + +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 PackageInfo) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); + } else if (value is SuspiciousAppInfo) { + buffer.putUint8(130); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 129: + return PackageInfo.decode(readValue(buffer)!); + case 130: + return SuspiciousAppInfo.decode(readValue(buffer)!); + default: + return super.readValueOfType(type, buffer); + } + } +} + +abstract class TalsecPigeonApi { + static const MessageCodec pigeonChannelCodec = _PigeonCodec(); + + void onMalwareDetected(List packageInfo); + + static void setUp(TalsecPigeonApi? api, {BinaryMessenger? binaryMessenger, String messageChannelSuffix = '',}) { + messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + { + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + '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())); + } + }); + } + } + } +} diff --git a/lib/src/models/models.dart b/lib/src/models/models.dart index a24b299..d18e85e 100644 --- a/lib/src/models/models.dart +++ b/lib/src/models/models.dart @@ -1,5 +1,3 @@ export 'android_config.dart'; export 'ios_config.dart'; -export 'package_info.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 deleted file mode 100644 index 665f717..0000000 --- a/lib/src/models/package_info.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:json_annotation/json_annotation.dart'; - -part 'package_info.g.dart'; - -@JsonSerializable() -class PackageInfo { - const PackageInfo({ - required this.packageName, - this.appIcon, - this.version, - this.appName, - this.installationSource, - }); - - final String packageName; - final String? appIcon; - final String? appName; - final String? version; - final String? installationSource; - - factory PackageInfo.fromJson(Map json) => - _$PackageInfoFromJson(json); -} - diff --git a/lib/src/models/package_info.g.dart b/lib/src/models/package_info.g.dart deleted file mode 100644 index d6ed553..0000000 --- a/lib/src/models/package_info.g.dart +++ /dev/null @@ -1,24 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'package_info.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -PackageInfo _$PackageInfoFromJson(Map json) => PackageInfo( - packageName: json['packageName'] as String, - appIcon: json['appIcon'] as String?, - version: json['version'] as String?, - appName: json['appName'] as String?, - installationSource: json['installationSource'] as String?, - ); - -Map _$PackageInfoToJson(PackageInfo instance) => - { - 'packageName': instance.packageName, - 'appIcon': instance.appIcon, - 'appName': instance.appName, - 'version': instance.version, - 'installationSource': instance.installationSource, - }; diff --git a/lib/src/models/suspicious_app_info.dart b/lib/src/models/suspicious_app_info.dart deleted file mode 100644 index 246854b..0000000 --- a/lib/src/models/suspicious_app_info.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:freerasp/src/models/package_info.dart'; -import 'package:json_annotation/json_annotation.dart'; - -part 'suspicious_app_info.g.dart'; - -@JsonSerializable() -class SuspiciousAppInfo { - const SuspiciousAppInfo({ - required this.packageInfo, - required this.reason, - }); - - final PackageInfo packageInfo; - final String reason; - - factory SuspiciousAppInfo.fromJson(Map json) => - _$SuspiciousAppInfoFromJson(json); -} diff --git a/lib/src/models/suspicious_app_info.g.dart b/lib/src/models/suspicious_app_info.g.dart deleted file mode 100644 index 250b832..0000000 --- a/lib/src/models/suspicious_app_info.g.dart +++ /dev/null @@ -1,20 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'suspicious_app_info.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -SuspiciousAppInfo _$SuspiciousAppInfoFromJson(Map json) => - SuspiciousAppInfo( - packageInfo: - PackageInfo.fromJson(json['packageInfo'] as Map), - reason: json['reason'] as String, - ); - -Map _$SuspiciousAppInfoToJson(SuspiciousAppInfo instance) => - { - 'packageInfo': instance.packageInfo, - 'reason': instance.reason, - }; diff --git a/lib/src/talsec.dart b/lib/src/talsec.dart index 7fdc906..56ae83c 100644 --- a/lib/src/talsec.dart +++ b/lib/src/talsec.dart @@ -4,6 +4,7 @@ import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:freerasp/freerasp.dart'; +import 'package:freerasp/src/generated/talsec_pigeon_api.g.dart'; /// A class which maintains all security related operations. /// @@ -27,17 +28,7 @@ import 'package:freerasp/freerasp.dart'; class Talsec { /// Private constructor for internal and testing purposes. @visibleForTesting - Talsec.private(this.methodChannel, this.handlerChannel, this.eventChannel) { - handlerChannel.setMethodCallHandler(_methodHandler); - } - - Future _methodHandler(MethodCall call) async { - if (call.method != 'onMalwareDetected') { - return; - } - - print("data"); - } + Talsec.private(this.methodChannel, this.eventChannel); /// Named channel used to communicate with platform plugins. /// @@ -52,12 +43,9 @@ class Talsec { static const MethodChannel _methodChannel = MethodChannel('talsec.app/freerasp/methods'); - static const MethodChannel _handlerChannel = - MethodChannel('talsec.app/freerasp/invoke'); - /// Private [Talsec] variable which holds current instance of class. static final _instance = - Talsec.private(_methodChannel, _handlerChannel, _eventChannel); + Talsec.private(_methodChannel, _eventChannel); /// Initialize Talsec lazily/obtain current instance of Talsec. static Talsec get instance => _instance; @@ -66,10 +54,6 @@ class Talsec { @visibleForTesting late final MethodChannel methodChannel; - /// [MethodChannel] used to invoke native platform. - @visibleForTesting - late final MethodChannel handlerChannel; - /// [EventChannel] used to receive Threats from the native platform. @visibleForTesting late final EventChannel eventChannel; @@ -78,8 +62,6 @@ class Talsec { Stream? _onThreatDetected; - List _suspiciousAppsCache = []; - /// Returns a broadcast stream. When security is compromised /// [onThreatDetected] receives what type of Threat caused it. /// @@ -117,8 +99,6 @@ class Talsec { return _onThreatDetected!; } - ThreatCallback? _callback; - /// Starts freeRASP with configuration provided in [config]. Future start(TalsecConfig config) { _checkConfig(config); @@ -158,48 +138,48 @@ class Talsec { /// When threat is detected, respective callback of [ThreatCallback] is /// invoked. void attachListener(ThreatCallback callback) { + TalsecPigeonApi.setUp(callback); detachListener(); - _callback = callback; _streamSubscription ??= onThreatDetected.listen((event) { switch (event) { case Threat.hooks: - _callback?.onHooks?.call(); + callback.onHooks?.call(); break; case Threat.debug: - _callback?.onDebug?.call(); + callback.onDebug?.call(); break; case Threat.passcode: - _callback?.onPasscode?.call(); + callback.onPasscode?.call(); break; case Threat.deviceId: - _callback?.onDeviceID?.call(); + callback.onDeviceID?.call(); break; case Threat.simulator: - _callback?.onSimulator?.call(); + callback.onSimulator?.call(); break; case Threat.appIntegrity: - _callback?.onAppIntegrity?.call(); + callback.onAppIntegrity?.call(); break; case Threat.obfuscationIssues: - _callback?.onObfuscationIssues?.call(); + callback.onObfuscationIssues?.call(); break; case Threat.deviceBinding: - _callback?.onDeviceBinding?.call(); + callback.onDeviceBinding?.call(); break; case Threat.unofficialStore: - _callback?.onUnofficialStore?.call(); + callback.onUnofficialStore?.call(); break; case Threat.privilegedAccess: - _callback?.onPrivilegedAccess?.call(); + callback.onPrivilegedAccess?.call(); break; case Threat.secureHardwareNotAvailable: - _callback?.onSecureHardwareNotAvailable?.call(); + callback.onSecureHardwareNotAvailable?.call(); break; case Threat.systemVPN: - _callback?.onSystemVPN?.call(); + callback.onSystemVPN?.call(); break; case Threat.devMode: - _callback?.onDevMode?.call(); + callback.onDevMode?.call(); break; } }); @@ -212,7 +192,6 @@ class Talsec { void detachListener() { _streamSubscription?.cancel(); _streamSubscription = null; - _callback = null; } void _handleStreamError(Object error) { diff --git a/lib/src/threat_callback.dart b/lib/src/threat_callback.dart index c4732b5..2417488 100644 --- a/lib/src/threat_callback.dart +++ b/lib/src/threat_callback.dart @@ -1,3 +1,4 @@ +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 @@ -17,7 +18,7 @@ import 'package:freerasp/src/typedefs.dart'; /// // Attaching callback to Talsec /// Talsec.instance.attachListener(callback); /// ``` -class ThreatCallback { +class ThreatCallback extends TalsecPigeonApi { /// Constructs a [ThreatCallback] instance. ThreatCallback({ this.onHooks, @@ -82,6 +83,13 @@ class ThreatCallback { /// This method is called whe the device has Developer mode enabled final VoidCallback? onDevMode; - /// This method is called when malware is detected on the device + @override + void onMalwareDetected(List packageInfo) { + onMalware?.call(packageInfo); + } + + /// This method is called when malware is detected. + /// + /// Android only final MalwareCallback? onMalware; } diff --git a/lib/src/typedefs.dart b/lib/src/typedefs.dart index 085e5f1..0814189 100644 --- a/lib/src/typedefs.dart +++ b/lib/src/typedefs.dart @@ -1,6 +1,7 @@ -import '../freerasp.dart'; +import 'package:freerasp/src/generated/talsec_pigeon_api.g.dart'; /// Typedef for void methods typedef VoidCallback = void Function(); -typedef MalwareCallback = void Function(List); +/// Typedef for malware callback +typedef MalwareCallback = void Function(List packageInfo); diff --git a/pigeons/talsec_pigeon_api.dart b/pigeons/talsec_pigeon_api.dart new file mode 100644 index 0000000..f11e5ca --- /dev/null +++ b/pigeons/talsec_pigeon_api.dart @@ -0,0 +1,44 @@ +import 'package:pigeon/pigeon.dart'; + +// ignore: flutter_style_todos +// TODO: Migrate whole Talsec API to pigeon +@ConfigurePigeon( + PigeonOptions( + dartOut: 'lib/src/generated/talsec_pigeon_api.g.dart', + kotlinOut: + 'android/src/main/kotlin/com/aheaditec/freerasp/generated/TalsecPigeonApi.kt', + input: 'pigeons/talsec_pigeon_api.dart', + kotlinOptions: KotlinOptions(package: 'com.aheaditec.talsec.generated'), + ), +) +class PackageInfo { + const PackageInfo({ + required this.packageName, + this.appIcon, + this.version, + this.appName, + this.installationSource, + }); + + final String packageName; + final String? appIcon; + final String? appName; + final String? version; + final String? installationSource; +} + +class SuspiciousAppInfo { + const SuspiciousAppInfo({ + required this.packageInfo, + required this.reason, + }); + + final PackageInfo packageInfo; + final String reason; +} + +@FlutterApi() +// ignore: one_member_abstracts +abstract class TalsecPigeonApi { + void onMalwareDetected(List packageInfo); +} diff --git a/pubspec.yaml b/pubspec.yaml index d89ae0d..5e73c16 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -29,6 +29,7 @@ dev_dependencies: sdk: flutter json_serializable: ^6.0.1 mocktail: ^0.2.0 + pigeon: ^22.4.0 very_good_analysis: ^3.0.0 flutter: diff --git a/test/src/talsec_test.dart b/test/src/talsec_test.dart index 8e9fa8b..7204b8e 100644 --- a/test/src/talsec_test.dart +++ b/test/src/talsec_test.dart @@ -44,7 +44,6 @@ void main() { ); // Act final talsec = Talsec.private( - methodChannel.methodChannel, methodChannel.methodChannel, FakeEventChannel(), ); @@ -71,7 +70,6 @@ void main() { ); // Act final talsec = Talsec.private( - methodChannel.methodChannel, methodChannel.methodChannel, FakeEventChannel(), ); @@ -94,7 +92,6 @@ void main() { // Act final talsec = Talsec.private( - methodChannel.methodChannel, methodChannel.methodChannel, FakeEventChannel(), ); @@ -123,7 +120,6 @@ void main() { // Act final talsec = Talsec.private( - methodChannel.methodChannel, methodChannel.methodChannel, FakeEventChannel(), ); @@ -164,7 +160,6 @@ void main() { ], ); final talsec = Talsec.private( - FakeMethodChannel(), FakeMethodChannel(), eventChannel.eventChannel, ); @@ -190,7 +185,6 @@ void main() { exceptions: [PlatformException(code: 'dummy-code')], ); final talsec = Talsec.private( - FakeMethodChannel(), FakeMethodChannel(), eventChannel.eventChannel, ); @@ -224,7 +218,6 @@ void main() { exceptions: [PlatformException(code: 'dummy-code')], ); final talsec = Talsec.private( - FakeMethodChannel(), FakeMethodChannel(), eventChannel.eventChannel, ); From d7d190caddea1a8cc69dca81e562be5954ef669f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Novotn=C3=BD?= Date: Tue, 1 Oct 2024 15:58:28 +0200 Subject: [PATCH 19/66] feat: add whitelist addition --- .../freerasp/handlers/MethodCallHandler.kt | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/handlers/MethodCallHandler.kt b/android/src/main/kotlin/com/aheaditec/freerasp/handlers/MethodCallHandler.kt index e8208ad..0b77cc4 100644 --- a/android/src/main/kotlin/com/aheaditec/freerasp/handlers/MethodCallHandler.kt +++ b/android/src/main/kotlin/com/aheaditec/freerasp/handlers/MethodCallHandler.kt @@ -7,6 +7,7 @@ import com.aheaditec.freerasp.generated.TalsecPigeonApi import com.aheaditec.freerasp.getOrElseThenThrow import com.aheaditec.freerasp.toPigeon import com.aheaditec.talsec_security.security.api.SuspiciousAppInfo +import com.aheaditec.talsec_security.security.api.Talsec import io.flutter.Log import io.flutter.plugin.common.BinaryMessenger import io.flutter.plugin.common.MethodCall @@ -90,6 +91,7 @@ internal class MethodCallHandler : MethodCallHandler { override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { when (call.method) { "start" -> start(call, result) + "addToWhitelist" -> addToWhitelist(call, result) else -> result.notImplemented() } } @@ -110,4 +112,16 @@ internal class MethodCallHandler : MethodCallHandler { result.success(null) } } + + private fun addToWhitelist(call: MethodCall, result: MethodChannel.Result) { + runResultCatching(result) { + val packageName = call.argument("packageName") + context?.let { + if (packageName != null) { + Talsec.addToWhitelist(it, packageName) + } + } ?: throw IllegalStateException("Unable to add package to whitelist - context is null") + result.success(null) + } + } } From 4e592f27730e2459934cdae281dd42609a49021a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Novotn=C3=BD?= Date: Tue, 1 Oct 2024 15:59:07 +0200 Subject: [PATCH 20/66] feat!: raise example sdk version --- example/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 571c350..be069ba 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -4,7 +4,7 @@ description: Demonstrates how to use the freerasp plugin. publish_to: 'none' environment: - sdk: ">=2.12.0 <4.0.0" + sdk: ">=3.0.0 <4.0.0" dependencies: flutter: From 49b82624ec77a574f4426f0d46f7c5a17b699d57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Novotn=C3=BD?= Date: Tue, 1 Oct 2024 15:59:23 +0200 Subject: [PATCH 21/66] docs: add documentation --- lib/src/models/android_config.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/src/models/android_config.dart b/lib/src/models/android_config.dart index e9662b2..31d456d 100644 --- a/lib/src/models/android_config.dart +++ b/lib/src/models/android_config.dart @@ -35,11 +35,15 @@ class AndroidConfig { /// List of supported sources where application can be installed from. final List supportedStores; + /// List of blocklisted applications with given package name. final List blocklistedPackageNames; + /// List of blocklisted applications with given hash. final List blocklistedHashes; + /// List of blocklisted applications with given permissions. final List> blocklistedPermissions; + /// List of whitelisted installation sources. final List whitelistedInstallationSources; } From b34c534b8c3c53fad928de1793c66043ff54197e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Novotn=C3=BD?= Date: Tue, 1 Oct 2024 15:59:35 +0200 Subject: [PATCH 22/66] docs: adjust exports --- lib/freerasp.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/freerasp.dart b/lib/freerasp.dart index 337b8bd..1862ca1 100644 --- a/lib/freerasp.dart +++ b/lib/freerasp.dart @@ -1,5 +1,6 @@ export 'src/enums/enums.dart'; export 'src/errors/errors.dart'; +export 'src/generated/talsec_pigeon_api.g.dart' show SuspiciousAppInfo, PackageInfo; export 'src/models/models.dart'; export 'src/talsec.dart'; export 'src/threat_callback.dart'; From 48ffe9ec45569b684b1945a2b4e634dda9f8e88f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Novotn=C3=BD?= Date: Tue, 1 Oct 2024 15:59:51 +0200 Subject: [PATCH 23/66] feat: update example --- example/lib/extensions.dart | 16 +++ example/lib/main.dart | 197 ++++++++++++++++++++----------- example/lib/safety_icon.dart | 2 +- example/lib/threat_notifier.dart | 53 ++++----- example/lib/threat_state.dart | 27 +++++ lib/src/talsec.dart | 19 ++- 6 files changed, 213 insertions(+), 101 deletions(-) create mode 100644 example/lib/extensions.dart create mode 100644 example/lib/threat_state.dart diff --git a/example/lib/extensions.dart b/example/lib/extensions.dart new file mode 100644 index 0000000..a77d6f4 --- /dev/null +++ b/example/lib/extensions.dart @@ -0,0 +1,16 @@ +/// Extensions for the `String` class. +extension StringX on String { + /// Converts the first character of the string to uppercase. + /// + /// If the string is empty, returns an empty string. + /// + /// If the string has only one character, returns the uppercase version of + /// the character. + /// + /// Otherwise, returns the string with the first character converted to + String toTitleCase() { + if (isEmpty) return ''; + if (length == 1) return toUpperCase(); + return this[0].toUpperCase() + substring(1); + } +} diff --git a/example/lib/main.dart b/example/lib/main.dart index 64be409..ccc0c10 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,113 +1,176 @@ +// ignore_for_file: public_member_api_docs, avoid_redundant_argument_values + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:freerasp/freerasp.dart'; +import 'package:freerasp_example/extensions.dart'; import 'package:freerasp_example/safety_icon.dart'; import 'package:freerasp_example/threat_notifier.dart'; +import 'package:freerasp_example/threat_state.dart'; /// Represents current state of the threats detectable by freeRASP -final threatProvider = StateNotifierProvider>( - (ref) => ThreatNotifier(), -); +final threatProvider = + NotifierProvider.autoDispose(() { + return ThreatNotifier(); +}); -void main() async { - /// Make sure that bindings are initialized before using Talsec. +Future main() async { WidgetsFlutterBinding.ensureInitialized(); + /// Initialize Talsec config + await _initializeTalsec(); + + runApp(const ProviderScope(child: App())); +} + +/// Initialize Talsec configuration for Android and iOS +Future _initializeTalsec() async { final config = TalsecConfig( - /// For Android androidConfig: AndroidConfig( packageName: 'com.aheaditec.freeraspExample', signingCertHashes: ['AKoRuyLMM91E7lX/Zqp3u4jMmd0A7hH/Iqozu0TMVd0='], supportedStores: ['com.sec.android.app.samsungapps'], blocklistedPackageNames: ['com.aheaditec.freeraspExample'], ), - - /// For iOS iosConfig: IOSConfig( bundleIds: ['com.aheaditec.freeraspExample'], teamId: 'M8AK35...', ), watcherMail: 'your_mail@example.com', - // ignore: avoid_redundant_argument_values - isProd: true, // use kReleaseMode for automatic switch + isProd: true, ); - /// freeRASP should be always initialized in the top-level widget await Talsec.instance.start(config); - - /// Another way to handle [Threat] is to use Stream. - /// ```dart - /// final subscription = Talsec.instance.onThreatDetected.listen((threat) { - /// log('Threat detected: $threat'); - /// }); - /// ``` - runApp(const ProviderScope(child: App())); } -/// The example app demonstrating usage of freeRASP +/// The root widget of the application class App extends StatelessWidget { - /// The root widget of the application. - const App({Key? key}) : super(key: key); + const App({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', - theme: ThemeData( - primarySwatch: Colors.blue, - ), - home: Scaffold( - appBar: AppBar( - title: const Text('freeRASP Demo'), - ), - body: const SafeArea( - child: Center( - child: HomePage(), - ), - ), - ), + theme: ThemeData(primarySwatch: Colors.blue), + home: const HomePage(), ); } } -/// Displays the main content of the application. +/// The home page that displays the threats and results class HomePage extends ConsumerWidget { - /// The constructor for the [HomePage] widget. - const HomePage({Key? key}) : super(key: key); + const HomePage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - final threatMap = ref.watch(threatProvider); - return Column( - children: [ - const SizedBox(height: 8), - Text( - 'Test results', - style: Theme.of(context).textTheme.titleMedium, - ), - Expanded( - child: ListView.separated( - padding: const EdgeInsets.all(8), - itemCount: threatMap.length, - itemBuilder: (context, index) { - /// Using Provider to get app state. - final currentThreat = threatMap.keys.elementAt(index); - final isDetected = threatMap[currentThreat]!; - return ListTile( - // ignore: sdk_version_since - title: Text(currentThreat.name), - subtitle: Text(isDetected ? 'Danger' : 'Safe'), - trailing: SafetyIcon(isDetected: isDetected), - ); - }, - separatorBuilder: (_, __) { - return const Divider( - height: 0, - ); - }, + final threatState = ref.watch(threatProvider); + + // Listen for changes in the threatProvider and show the malware modal + ref.listen(threatProvider, (prev, next) { + if (prev?.detectedMalware != next.detectedMalware) { + _showMalwareBottomSheet(context, next.detectedMalware); + } + }); + + return Scaffold( + appBar: AppBar(title: const Text('freeRASP Demo')), + body: SafeArea( + child: Padding( + padding: const EdgeInsets.all(8), + child: Column( + children: [ + Text( + 'Threat Status', + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 8), + Expanded( + child: _ThreatListView(threatState: threatState), + ), + ], ), ), - ], + ), + ); + } +} + +/// ListView displaying all detected threats +class _ThreatListView extends StatelessWidget { + const _ThreatListView({required this.threatState}); + + final ThreatState threatState; + + @override + Widget build(BuildContext context) { + return ListView.separated( + padding: const EdgeInsets.all(8), + itemCount: Threat.values.length, + itemBuilder: (context, index) { + final currentThreat = Threat.values[index]; + final isDetected = threatState.detectedThreats.contains(currentThreat); + + return ListTile( + title: Text(currentThreat.name.toTitleCase()), + subtitle: Text(isDetected ? 'Danger' : 'Safe'), + trailing: SafetyIcon(isDetected: isDetected), + ); + }, + separatorBuilder: (_, __) => const Divider(height: 1), + ); + } +} + +/// Bottom sheet widget that displays malware information +class MalwareBottomSheet extends StatelessWidget { + const MalwareBottomSheet({super.key, required this.suspiciousApps}); + + final List suspiciousApps; + + @override + Widget build(BuildContext context) { + final textTheme = Theme.of(context).textTheme; + + return Container( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Suspicious Apps', style: textTheme.titleMedium), + const SizedBox(height: 8), + ...suspiciousApps.map((malware) { + return ListTile( + title: Text(malware.packageInfo.packageName), + subtitle: Text('Reason: ${malware.reason}'), + leading: const Icon(Icons.warning, color: Colors.red), + ); + }), + const SizedBox(height: 16), + SizedBox( + width: double.infinity, + child: FilledButton( + onPressed: () => Navigator.pop(context), + child: const Text('Dismiss'), + ), + ), + ], + ), ); } } + +/// Extension method to show the malware bottom sheet +void _showMalwareBottomSheet( + BuildContext context, + List suspiciousApps, +) { + WidgetsBinding.instance.addPostFrameCallback((_) { + showModalBottomSheet( + context: context, + isDismissible: false, + enableDrag: false, + builder: (BuildContext context) => + MalwareBottomSheet(suspiciousApps: suspiciousApps), + ); + }); +} diff --git a/example/lib/safety_icon.dart b/example/lib/safety_icon.dart index 478dffc..2e00baf 100644 --- a/example/lib/safety_icon.dart +++ b/example/lib/safety_icon.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; /// Class responsible for changing threat icon and style in the example app class SafetyIcon extends StatelessWidget { /// Represents security state icon in the example app - const SafetyIcon({required this.isDetected, Key? key}) : super(key: key); + const SafetyIcon({required this.isDetected, super.key}); /// Determines whether given threat was detected final bool isDetected; diff --git a/example/lib/threat_notifier.dart b/example/lib/threat_notifier.dart index e68945b..8aa9663 100644 --- a/example/lib/threat_notifier.dart +++ b/example/lib/threat_notifier.dart @@ -1,51 +1,42 @@ -import 'dart:developer'; -import 'dart:io'; - import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:freerasp/freerasp.dart'; +import 'package:freerasp_example/threat_state.dart'; /// Class responsible for setting up listeners to detected threats -class ThreatNotifier extends StateNotifier> { - /// Sets up reactions to detected threats and starts the threat listener - ThreatNotifier() : super(_emptyState()) { - final callback = ThreatCallback( +class ThreatNotifier extends AutoDisposeNotifier { + @override + ThreatState build() { + _init(); + return ThreatState.initial(); + } + + void _init() { + final threatCallback = ThreatCallback( + onMalware: _updateMalware, + onHooks: () => _updateThreat(Threat.hooks), + onDebug: () => _updateThreat(Threat.debug), + onPasscode: () => _updateThreat(Threat.passcode), + onDeviceID: () => _updateThreat(Threat.deviceId), + onSimulator: () => _updateThreat(Threat.simulator), onAppIntegrity: () => _updateThreat(Threat.appIntegrity), onObfuscationIssues: () => _updateThreat(Threat.obfuscationIssues), - onDebug: () => _updateThreat(Threat.debug), onDeviceBinding: () => _updateThreat(Threat.deviceBinding), - onDeviceID: () => _updateThreat(Threat.deviceId), - onHooks: () => _updateThreat(Threat.hooks), - onPasscode: () => _updateThreat(Threat.passcode), + onUnofficialStore: () => _updateThreat(Threat.unofficialStore), onPrivilegedAccess: () => _updateThreat(Threat.privilegedAccess), onSecureHardwareNotAvailable: () => _updateThreat(Threat.secureHardwareNotAvailable), - onSimulator: () => _updateThreat(Threat.simulator), - onUnofficialStore: () => _updateThreat(Threat.unofficialStore), onSystemVPN: () => _updateThreat(Threat.systemVPN), onDevMode: () => _updateThreat(Threat.devMode), - onMalware: (threat) => log('Malware detected: $threat'), ); - Talsec.instance.attachListener(callback); + Talsec.instance.attachListener(threatCallback); } - static Map _emptyState() { - final threatMap = - Threat.values.asMap().map((key, value) => MapEntry(value, false)); - - if (Platform.isAndroid) { - threatMap.remove(Threat.deviceId); - } - - if (Platform.isIOS) { - threatMap.remove(Threat.devMode); - } - - return threatMap; + void _updateThreat(Threat threat) { + state = state.copyWith(detectedThreats: {...state.detectedThreats, threat}); } - void _updateThreat(Threat threat) { - final threatMap = {threat: true}; - state = {...state, ...threatMap}; + void _updateMalware(List malware) { + state = state.copyWith(detectedMalware: malware.nonNulls.toList()); } } diff --git a/example/lib/threat_state.dart b/example/lib/threat_state.dart new file mode 100644 index 0000000..837c2a3 --- /dev/null +++ b/example/lib/threat_state.dart @@ -0,0 +1,27 @@ +// ignore_for_file: public_member_api_docs + +import 'package:freerasp/freerasp.dart'; + +class ThreatState { + factory ThreatState.initial() => + const ThreatState._(detectedThreats: {}, detectedMalware: []); + + const ThreatState._({ + required this.detectedThreats, + required this.detectedMalware, + }); + + final Set detectedThreats; + final List detectedMalware; + + ThreatState copyWith({ + Set? detectedThreats, + List? detectedMalware, + }) { + return ThreatState._( + detectedThreats: detectedThreats ?? this.detectedThreats, + detectedMalware: + detectedMalware?.nonNulls.toList() ?? this.detectedMalware, + ); + } +} diff --git a/lib/src/talsec.dart b/lib/src/talsec.dart index 56ae83c..006f885 100644 --- a/lib/src/talsec.dart +++ b/lib/src/talsec.dart @@ -44,8 +44,7 @@ class Talsec { MethodChannel('talsec.app/freerasp/methods'); /// Private [Talsec] variable which holds current instance of class. - static final _instance = - Talsec.private(_methodChannel, _eventChannel); + static final _instance = Talsec.private(_methodChannel, _eventChannel); /// Initialize Talsec lazily/obtain current instance of Talsec. static Talsec get instance => _instance; @@ -108,6 +107,22 @@ class Talsec { ); } + /// Adds [packageName] to the whitelist. + /// + /// Once added, the package will be excluded from the list of blocklisted + /// packages and won't appear in the list of suspicious applications in + /// the future detections. + /// + /// **Adding package is one-way process** - to remove the package from the + /// whitelist, you need to remove application data or reinstall the + /// application. + Future addToWhitelist(String packageName) { + return methodChannel.invokeMethod( + 'addToWhitelist', + {'packageName': packageName}, + ); + } + void _checkConfig(TalsecConfig config) { // ignore: missing_enum_constant_in_switch switch (defaultTargetPlatform) { From 9fb2a5f59aafa4c49647ef0e11df4a4b648b39fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Novotn=C3=BD?= Date: Tue, 1 Oct 2024 16:00:49 +0200 Subject: [PATCH 24/66] style: formatting --- lib/freerasp.dart | 3 +- lib/src/generated/talsec_pigeon_api.g.dart | 38 ++++++++++++++-------- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/lib/freerasp.dart b/lib/freerasp.dart index 1862ca1..395cf81 100644 --- a/lib/freerasp.dart +++ b/lib/freerasp.dart @@ -1,6 +1,7 @@ export 'src/enums/enums.dart'; export 'src/errors/errors.dart'; -export 'src/generated/talsec_pigeon_api.g.dart' show SuspiciousAppInfo, PackageInfo; +export 'src/generated/talsec_pigeon_api.g.dart' + show SuspiciousAppInfo, PackageInfo; export 'src/models/models.dart'; export 'src/talsec.dart'; export 'src/threat_callback.dart'; diff --git a/lib/src/generated/talsec_pigeon_api.g.dart b/lib/src/generated/talsec_pigeon_api.g.dart index 57d8406..f596898 100644 --- a/lib/src/generated/talsec_pigeon_api.g.dart +++ b/lib/src/generated/talsec_pigeon_api.g.dart @@ -8,7 +8,8 @@ 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}) { +List wrapResponse( + {Object? result, PlatformException? error, bool empty = false}) { if (empty) { return []; } @@ -85,7 +86,6 @@ class SuspiciousAppInfo { } } - class _PigeonCodec extends StandardMessageCodec { const _PigeonCodec(); @override @@ -93,10 +93,10 @@ class _PigeonCodec extends StandardMessageCodec { if (value is int) { buffer.putUint8(4); buffer.putInt64(value); - } else if (value is PackageInfo) { + } else if (value is PackageInfo) { buffer.putUint8(129); writeValue(buffer, value.encode()); - } else if (value is SuspiciousAppInfo) { + } else if (value is SuspiciousAppInfo) { buffer.putUint8(130); writeValue(buffer, value.encode()); } else { @@ -107,9 +107,9 @@ class _PigeonCodec extends StandardMessageCodec { @override Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { - case 129: + case 129: return PackageInfo.decode(readValue(buffer)!); - case 130: + case 130: return SuspiciousAppInfo.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); @@ -122,20 +122,29 @@ abstract class TalsecPigeonApi { void onMalwareDetected(List packageInfo); - static void setUp(TalsecPigeonApi? api, {BinaryMessenger? binaryMessenger, String messageChannelSuffix = '',}) { - messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + static void setUp( + TalsecPigeonApi? api, { + BinaryMessenger? binaryMessenger, + String messageChannelSuffix = '', + }) { + messageChannelSuffix = + messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; { - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( - 'dev.flutter.pigeon.freerasp.TalsecPigeonApi.onMalwareDetected$messageChannelSuffix', pigeonChannelCodec, + 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.'); + '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(); + 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 { @@ -143,8 +152,9 @@ abstract class TalsecPigeonApi { return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); - } catch (e) { - return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString())); } }); } From 14f65507212a363ca869eeb55f78ede6f5caba06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Novotn=C3=BD?= Date: Tue, 1 Oct 2024 16:01:22 +0200 Subject: [PATCH 25/66] chore: raise android package version --- android/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/build.gradle b/android/build.gradle index b615983..0757cd1 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -3,7 +3,7 @@ version '1.0-SNAPSHOT' buildscript { ext.kotlin_version = '1.7.20' - ext.talsec_version = '11.1.0' + ext.talsec_version = '11.1.1' repositories { google() mavenCentral() From 0eaaec0f52b30bc59fdb06048b2769a123a6facb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Novotn=C3=BD?= Date: Thu, 26 Sep 2024 09:42:00 +0200 Subject: [PATCH 26/66] feat: add data classes for malware (Dart) --- lib/src/models/package_info.dart | 24 +++++++++++++++++++++++ lib/src/models/package_info.g.dart | 24 +++++++++++++++++++++++ lib/src/models/suspicious_app_info.dart | 18 +++++++++++++++++ lib/src/models/suspicious_app_info.g.dart | 20 +++++++++++++++++++ 4 files changed, 86 insertions(+) create mode 100644 lib/src/models/package_info.dart create mode 100644 lib/src/models/package_info.g.dart create mode 100644 lib/src/models/suspicious_app_info.dart create mode 100644 lib/src/models/suspicious_app_info.g.dart diff --git a/lib/src/models/package_info.dart b/lib/src/models/package_info.dart new file mode 100644 index 0000000..665f717 --- /dev/null +++ b/lib/src/models/package_info.dart @@ -0,0 +1,24 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'package_info.g.dart'; + +@JsonSerializable() +class PackageInfo { + const PackageInfo({ + required this.packageName, + this.appIcon, + this.version, + this.appName, + this.installationSource, + }); + + final String packageName; + final String? appIcon; + final String? appName; + final String? version; + final String? installationSource; + + factory PackageInfo.fromJson(Map json) => + _$PackageInfoFromJson(json); +} + diff --git a/lib/src/models/package_info.g.dart b/lib/src/models/package_info.g.dart new file mode 100644 index 0000000..d6ed553 --- /dev/null +++ b/lib/src/models/package_info.g.dart @@ -0,0 +1,24 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'package_info.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +PackageInfo _$PackageInfoFromJson(Map json) => PackageInfo( + packageName: json['packageName'] as String, + appIcon: json['appIcon'] as String?, + version: json['version'] as String?, + appName: json['appName'] as String?, + installationSource: json['installationSource'] as String?, + ); + +Map _$PackageInfoToJson(PackageInfo instance) => + { + 'packageName': instance.packageName, + 'appIcon': instance.appIcon, + 'appName': instance.appName, + 'version': instance.version, + 'installationSource': instance.installationSource, + }; diff --git a/lib/src/models/suspicious_app_info.dart b/lib/src/models/suspicious_app_info.dart new file mode 100644 index 0000000..246854b --- /dev/null +++ b/lib/src/models/suspicious_app_info.dart @@ -0,0 +1,18 @@ +import 'package:freerasp/src/models/package_info.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'suspicious_app_info.g.dart'; + +@JsonSerializable() +class SuspiciousAppInfo { + const SuspiciousAppInfo({ + required this.packageInfo, + required this.reason, + }); + + final PackageInfo packageInfo; + final String reason; + + factory SuspiciousAppInfo.fromJson(Map json) => + _$SuspiciousAppInfoFromJson(json); +} diff --git a/lib/src/models/suspicious_app_info.g.dart b/lib/src/models/suspicious_app_info.g.dart new file mode 100644 index 0000000..250b832 --- /dev/null +++ b/lib/src/models/suspicious_app_info.g.dart @@ -0,0 +1,20 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'suspicious_app_info.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +SuspiciousAppInfo _$SuspiciousAppInfoFromJson(Map json) => + SuspiciousAppInfo( + packageInfo: + PackageInfo.fromJson(json['packageInfo'] as Map), + reason: json['reason'] as String, + ); + +Map _$SuspiciousAppInfoToJson(SuspiciousAppInfo instance) => + { + 'packageInfo': instance.packageInfo, + 'reason': instance.reason, + }; From 78e4542ce6800f6be709300fd5fc9a570554ef94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Novotn=C3=BD?= Date: Thu, 26 Sep 2024 09:42:29 +0200 Subject: [PATCH 27/66] feat: add data classes for malware (Kotlin) --- .../aheaditec/freerasp/models/PackageInfo.kt | 33 +++++++++++++++++++ .../freerasp/models/SuspiciousAppInfo.kt | 21 ++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 android/src/main/kotlin/com/aheaditec/freerasp/models/PackageInfo.kt create mode 100644 android/src/main/kotlin/com/aheaditec/freerasp/models/SuspiciousAppInfo.kt diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/models/PackageInfo.kt b/android/src/main/kotlin/com/aheaditec/freerasp/models/PackageInfo.kt new file mode 100644 index 0000000..aab9a20 --- /dev/null +++ b/android/src/main/kotlin/com/aheaditec/freerasp/models/PackageInfo.kt @@ -0,0 +1,33 @@ +package com.aheaditec.freerasp.models + +import org.json.JSONObject + +data class PackageInfo( + val packageName: String, + val appIcon: String? = null, + val appName: String? = null, + val version: String? = null, + val installationSource: String? = null +) { + companion object { + fun fromTalsec(packageInfo: android.content.pm.PackageInfo): PackageInfo { + return PackageInfo( + packageInfo.packageName, + packageInfo.appIcon, + packageInfo.appName, + packageInfo.version, + packageInfo.installationSource + ) + } + } + + fun toJson(): String { + val json = JSONObject().put("packageName", packageName) + .putOpt("appIcon", appIcon) + .putOpt("appName", appName) + .putOpt("version", version) + .putOpt("installationSource", installationSource) + + return json.toString() + } +} \ No newline at end of file diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/models/SuspiciousAppInfo.kt b/android/src/main/kotlin/com/aheaditec/freerasp/models/SuspiciousAppInfo.kt new file mode 100644 index 0000000..bb93a8e --- /dev/null +++ b/android/src/main/kotlin/com/aheaditec/freerasp/models/SuspiciousAppInfo.kt @@ -0,0 +1,21 @@ +package com.aheaditec.freerasp.models + +import com.aheaditec.talsec_security.security.api.SuspiciousAppInfo as TalsecSuspiciousAppInfo + +class SuspiciousAppInfo( + val packageInfo: PackageInfo, + val reason: String +) { + companion object { + fun fromTalsec(suspiciousAppInfo: TalsecSuspiciousAppInfo): SuspiciousAppInfo { + return SuspiciousAppInfo( + PackageInfo.fromTalsec(suspiciousAppInfo.packageInfo), + suspiciousAppInfo.reason + ) + } + } + + fun toJson(): String { + return "{\"packageInfo\": ${packageInfo.toJson()}, \"reason\": \"$reason\"}" + } +} \ No newline at end of file From fade1fb48de300bb906d255743bcd4194be2b9e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Novotn=C3=BD?= Date: Thu, 26 Sep 2024 09:42:48 +0200 Subject: [PATCH 28/66] feat: update Android configuration --- lib/src/models/android_config.dart | 12 ++++++++++++ lib/src/models/android_config.g.dart | 23 +++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/lib/src/models/android_config.dart b/lib/src/models/android_config.dart index 5a8356e..e9662b2 100644 --- a/lib/src/models/android_config.dart +++ b/lib/src/models/android_config.dart @@ -11,6 +11,10 @@ class AndroidConfig { required this.packageName, required this.signingCertHashes, this.supportedStores = const [], + this.blocklistedPackageNames = const [], + this.blocklistedHashes = const [], + this.blocklistedPermissions = const >[[]], + this.whitelistedInstallationSources = const [], }) { ConfigVerifier.verifyAndroid(this); } @@ -30,4 +34,12 @@ class AndroidConfig { /// List of supported sources where application can be installed from. final List supportedStores; + + final List blocklistedPackageNames; + + final List blocklistedHashes; + + final List> blocklistedPermissions; + + final List whitelistedInstallationSources; } diff --git a/lib/src/models/android_config.g.dart b/lib/src/models/android_config.g.dart index 021f8a8..d93449d 100644 --- a/lib/src/models/android_config.g.dart +++ b/lib/src/models/android_config.g.dart @@ -16,6 +16,25 @@ AndroidConfig _$AndroidConfigFromJson(Map json) => ?.map((e) => e as String) .toList() ?? const [], + blocklistedPackageNames: + (json['blocklistedPackageNames'] as List?) + ?.map((e) => e as String) + .toList() ?? + const [], + blocklistedHashes: (json['blocklistedHashes'] as List?) + ?.map((e) => e as String) + .toList() ?? + const [], + blocklistedPermissions: (json['blocklistedPermissions'] as List?) + ?.map( + (e) => (e as List).map((e) => e as String).toList()) + .toList() ?? + const >[[]], + whitelistedInstallationSources: + (json['whitelistedInstallationSources'] as List?) + ?.map((e) => e as String) + .toList() ?? + const [], ); Map _$AndroidConfigToJson(AndroidConfig instance) => @@ -23,4 +42,8 @@ Map _$AndroidConfigToJson(AndroidConfig instance) => 'packageName': instance.packageName, 'signingCertHashes': instance.signingCertHashes, 'supportedStores': instance.supportedStores, + 'blocklistedPackageNames': instance.blocklistedPackageNames, + 'blocklistedHashes': instance.blocklistedHashes, + 'blocklistedPermissions': instance.blocklistedPermissions, + 'whitelistedInstallationSources': instance.whitelistedInstallationSources, }; From e4cbbf70db66e1701de034d18646dfc89e15ed81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Novotn=C3=BD?= Date: Thu, 26 Sep 2024 09:43:02 +0200 Subject: [PATCH 29/66] feat: update Talsec configuration --- lib/src/models/talsec_config.g.dart | 3 +- lib/src/talsec.dart | 54 +++++++++++++++++++++-------- 2 files changed, 40 insertions(+), 17 deletions(-) diff --git a/lib/src/models/talsec_config.g.dart b/lib/src/models/talsec_config.g.dart index f901fc5..41aaa56 100644 --- a/lib/src/models/talsec_config.g.dart +++ b/lib/src/models/talsec_config.g.dart @@ -12,8 +12,7 @@ TalsecConfig _$TalsecConfigFromJson(Map json) => TalsecConfig( androidConfig: json['androidConfig'] == null ? null : AndroidConfig.fromJson( - json['androidConfig'] as Map, - ), + json['androidConfig'] as Map), iosConfig: json['iosConfig'] == null ? null : IOSConfig.fromJson(json['iosConfig'] as Map), diff --git a/lib/src/talsec.dart b/lib/src/talsec.dart index 25ad8cf..7fdc906 100644 --- a/lib/src/talsec.dart +++ b/lib/src/talsec.dart @@ -27,7 +27,17 @@ import 'package:freerasp/freerasp.dart'; class Talsec { /// Private constructor for internal and testing purposes. @visibleForTesting - Talsec.private(this.methodChannel, this.eventChannel); + Talsec.private(this.methodChannel, this.handlerChannel, this.eventChannel) { + handlerChannel.setMethodCallHandler(_methodHandler); + } + + Future _methodHandler(MethodCall call) async { + if (call.method != 'onMalwareDetected') { + return; + } + + print("data"); + } /// Named channel used to communicate with platform plugins. /// @@ -42,8 +52,12 @@ class Talsec { static const MethodChannel _methodChannel = MethodChannel('talsec.app/freerasp/methods'); + static const MethodChannel _handlerChannel = + MethodChannel('talsec.app/freerasp/invoke'); + /// Private [Talsec] variable which holds current instance of class. - static final _instance = Talsec.private(_methodChannel, _eventChannel); + static final _instance = + Talsec.private(_methodChannel, _handlerChannel, _eventChannel); /// Initialize Talsec lazily/obtain current instance of Talsec. static Talsec get instance => _instance; @@ -52,6 +66,10 @@ class Talsec { @visibleForTesting late final MethodChannel methodChannel; + /// [MethodChannel] used to invoke native platform. + @visibleForTesting + late final MethodChannel handlerChannel; + /// [EventChannel] used to receive Threats from the native platform. @visibleForTesting late final EventChannel eventChannel; @@ -60,6 +78,8 @@ class Talsec { Stream? _onThreatDetected; + List _suspiciousAppsCache = []; + /// Returns a broadcast stream. When security is compromised /// [onThreatDetected] receives what type of Threat caused it. /// @@ -97,6 +117,8 @@ class Talsec { return _onThreatDetected!; } + ThreatCallback? _callback; + /// Starts freeRASP with configuration provided in [config]. Future start(TalsecConfig config) { _checkConfig(config); @@ -137,46 +159,47 @@ class Talsec { /// invoked. void attachListener(ThreatCallback callback) { detachListener(); + _callback = callback; _streamSubscription ??= onThreatDetected.listen((event) { switch (event) { case Threat.hooks: - callback.onHooks?.call(); + _callback?.onHooks?.call(); break; case Threat.debug: - callback.onDebug?.call(); + _callback?.onDebug?.call(); break; case Threat.passcode: - callback.onPasscode?.call(); + _callback?.onPasscode?.call(); break; case Threat.deviceId: - callback.onDeviceID?.call(); + _callback?.onDeviceID?.call(); break; case Threat.simulator: - callback.onSimulator?.call(); + _callback?.onSimulator?.call(); break; case Threat.appIntegrity: - callback.onAppIntegrity?.call(); + _callback?.onAppIntegrity?.call(); break; case Threat.obfuscationIssues: - callback.onObfuscationIssues?.call(); + _callback?.onObfuscationIssues?.call(); break; case Threat.deviceBinding: - callback.onDeviceBinding?.call(); + _callback?.onDeviceBinding?.call(); break; case Threat.unofficialStore: - callback.onUnofficialStore?.call(); + _callback?.onUnofficialStore?.call(); break; case Threat.privilegedAccess: - callback.onPrivilegedAccess?.call(); + _callback?.onPrivilegedAccess?.call(); break; case Threat.secureHardwareNotAvailable: - callback.onSecureHardwareNotAvailable?.call(); + _callback?.onSecureHardwareNotAvailable?.call(); break; case Threat.systemVPN: - callback.onSystemVPN?.call(); + _callback?.onSystemVPN?.call(); break; case Threat.devMode: - callback.onDevMode?.call(); + _callback?.onDevMode?.call(); break; } }); @@ -189,6 +212,7 @@ class Talsec { void detachListener() { _streamSubscription?.cancel(); _streamSubscription = null; + _callback = null; } void _handleStreamError(Object error) { From ae85a3e6d42b05982a7e83e278a5a85aa785706c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Novotn=C3=BD?= Date: Thu, 26 Sep 2024 09:43:49 +0200 Subject: [PATCH 30/66] feat: add Flutter method invocation --- .../com/aheaditec/freerasp/FreeraspPlugin.kt | 5 ++ .../freerasp/handlers/MethodCallInvoker.kt | 72 +++++++++++++++++++ .../freerasp/handlers/PluginThreatHandler.kt | 14 +++- .../freerasp/handlers/StreamHandler.kt | 6 +- .../freerasp/handlers/TalsecThreatHandler.kt | 28 ++++++-- lib/src/typedefs.dart | 4 ++ 6 files changed, 119 insertions(+), 10 deletions(-) create mode 100644 android/src/main/kotlin/com/aheaditec/freerasp/handlers/MethodCallInvoker.kt diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/FreeraspPlugin.kt b/android/src/main/kotlin/com/aheaditec/freerasp/FreeraspPlugin.kt index bda94d4..48ede80 100644 --- a/android/src/main/kotlin/com/aheaditec/freerasp/FreeraspPlugin.kt +++ b/android/src/main/kotlin/com/aheaditec/freerasp/FreeraspPlugin.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleOwner import com.aheaditec.freerasp.handlers.MethodCallHandler +import com.aheaditec.freerasp.handlers.MethodCallInvoker import com.aheaditec.freerasp.handlers.StreamHandler import com.aheaditec.freerasp.handlers.TalsecThreatHandler import io.flutter.embedding.engine.plugins.FlutterPlugin @@ -17,6 +18,8 @@ import io.flutter.embedding.engine.plugins.lifecycle.FlutterLifecycleAdapter class FreeraspPlugin : FlutterPlugin, ActivityAware, LifecycleEventObserver { private var streamHandler: StreamHandler = StreamHandler() private var methodCallHandler: MethodCallHandler = MethodCallHandler() + private var methodCallInvoker : MethodCallInvoker = MethodCallInvoker() + private var context: Context? = null private var lifecycle: Lifecycle? = null @@ -25,11 +28,13 @@ class FreeraspPlugin : FlutterPlugin, ActivityAware, LifecycleEventObserver { context = flutterPluginBinding.applicationContext methodCallHandler.createMethodChannel(messenger, flutterPluginBinding.applicationContext) + methodCallInvoker.createMethodChannel(messenger, flutterPluginBinding.applicationContext) streamHandler.createEventChannel(messenger) } override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { methodCallHandler.destroyMethodChannel() + methodCallInvoker.destroyMethodChannel() streamHandler.destroyEventChannel() TalsecThreatHandler.detachListener(binding.applicationContext) } diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/handlers/MethodCallInvoker.kt b/android/src/main/kotlin/com/aheaditec/freerasp/handlers/MethodCallInvoker.kt new file mode 100644 index 0000000..1f0f5f0 --- /dev/null +++ b/android/src/main/kotlin/com/aheaditec/freerasp/handlers/MethodCallInvoker.kt @@ -0,0 +1,72 @@ +package com.aheaditec.freerasp.handlers + +import android.content.Context +import com.aheaditec.talsec_security.security.api.SuspiciousAppInfo +import io.flutter.Log +import io.flutter.plugin.common.BinaryMessenger +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.MethodCallHandler + +/** + * A method handler that creates and manages an [MethodChannel] for freeRASP methods. + */ +internal class MethodCallInvoker: MethodCallHandler { + private var context: Context? = null + private var methodChannel: MethodChannel? = null + private val methodSink = object : MethodSink { + override fun onMalwareDetected(packageInfo: List) { + methodChannel?.invokeMethod("onMalwareDetected", mapOf("packageInfo" to packageInfo.map { })) + } + } + + companion object { + private const val CHANNEL_NAME: String = "talsec.app/freerasp/invoke" + } + + internal interface MethodSink { + fun onMalwareDetected(packageInfo: List) + } + + /** + * Creates a new [MethodChannel] with the specified [BinaryMessenger] instance. Sets this class + * as the [MethodCallHandler]. + * If an old [MethodChannel] already exists, it will be destroyed before creating a new one. + * + * @param messenger The binary messenger to use for creating the [MethodChannel]. + * @param context The Android [Context] associated with this channel. + */ + fun createMethodChannel(messenger: BinaryMessenger, context: Context) { + methodChannel?.let { + Log.i("MethodCallHandler", "Tried to create channel without disposing old one.") + destroyMethodChannel() + } + + methodChannel = MethodChannel(messenger, CHANNEL_NAME).also { + it.setMethodCallHandler(this) + } + + this.context = context + TalsecThreatHandler.attachMethodSink(methodSink) + } + + /** + * Destroys the `MethodChannel` and clears associated variables. + */ + fun destroyMethodChannel() { + methodChannel?.setMethodCallHandler(null) + methodChannel = null + this.context = null + TalsecThreatHandler.detachMethodSink() + } + + /** + * Handles method calls received through the [MethodChannel]. + * + * @param call The method call. + * @param result The result handler of the method call. + */ + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { + result.error("INVALID", "This channel does not handle calls from Flutter.", null) + } +} diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/handlers/PluginThreatHandler.kt b/android/src/main/kotlin/com/aheaditec/freerasp/handlers/PluginThreatHandler.kt index b2bcc37..8ed309f 100644 --- a/android/src/main/kotlin/com/aheaditec/freerasp/handlers/PluginThreatHandler.kt +++ b/android/src/main/kotlin/com/aheaditec/freerasp/handlers/PluginThreatHandler.kt @@ -2,10 +2,10 @@ package com.aheaditec.freerasp.handlers import android.content.Context import com.aheaditec.freerasp.Threat +import com.aheaditec.talsec_security.security.api.SuspiciousAppInfo import com.aheaditec.talsec_security.security.api.ThreatListener import com.aheaditec.talsec_security.security.api.ThreatListener.DeviceState import com.aheaditec.talsec_security.security.api.ThreatListener.ThreatDetected -import com.aheaditec.talsec_security.security.api.SuspiciousAppInfo /** * A Singleton object that implements the [ThreatDetected] and [DeviceState] interfaces to handle @@ -15,6 +15,8 @@ import com.aheaditec.talsec_security.security.api.SuspiciousAppInfo */ internal object PluginThreatHandler : ThreatDetected, DeviceState { internal val detectedThreats = mutableSetOf() + internal val detectedMalware = mutableListOf() + internal var listener: TalsecFlutter? = null private val internalListener = ThreatListener(this, this) @@ -74,15 +76,21 @@ internal object PluginThreatHandler : ThreatDetected, DeviceState { notify(Threat.DevMode) } - override fun onMalwareDetected(appInfo: List) { - // Nothing to do yet. + override fun onMalwareDetected(suspiciousApps: List) { + notify(suspiciousApps) } private fun notify(threat: Threat) { listener?.threatDetected(threat) ?: detectedThreats.add(threat) } + private fun notify(suspiciousApps: List) { + listener?.malwareDetected(suspiciousApps) ?: detectedMalware.addAll(suspiciousApps) + } + internal interface TalsecFlutter { fun threatDetected(threatType: Threat) + + fun malwareDetected(suspiciousApps: List) } } diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/handlers/StreamHandler.kt b/android/src/main/kotlin/com/aheaditec/freerasp/handlers/StreamHandler.kt index 6e55434..0156c1c 100644 --- a/android/src/main/kotlin/com/aheaditec/freerasp/handlers/StreamHandler.kt +++ b/android/src/main/kotlin/com/aheaditec/freerasp/handlers/StreamHandler.kt @@ -42,7 +42,7 @@ internal class StreamHandler : EventChannel.StreamHandler { // Don't forget to remove old sink // @see https://stackoverflow.com/questions/61934900/tried-to-send-a-platform-message-to-flutter-but-flutterjni-was-detached-from-n - TalsecThreatHandler.detachSink() + TalsecThreatHandler.detachEventSink() } /** @@ -54,7 +54,7 @@ internal class StreamHandler : EventChannel.StreamHandler { */ override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { events?.let { - TalsecThreatHandler.attachSink(it) + TalsecThreatHandler.attachEventSink(it) } } @@ -65,6 +65,6 @@ internal class StreamHandler : EventChannel.StreamHandler { * @param arguments The arguments passed by the subscriber. Not used in this implementation. */ override fun onCancel(arguments: Any?) { - TalsecThreatHandler.detachSink() + TalsecThreatHandler.detachEventSink() } } \ No newline at end of file diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/handlers/TalsecThreatHandler.kt b/android/src/main/kotlin/com/aheaditec/freerasp/handlers/TalsecThreatHandler.kt index 35b381a..cb195c9 100644 --- a/android/src/main/kotlin/com/aheaditec/freerasp/handlers/TalsecThreatHandler.kt +++ b/android/src/main/kotlin/com/aheaditec/freerasp/handlers/TalsecThreatHandler.kt @@ -2,6 +2,7 @@ package com.aheaditec.freerasp.handlers import android.content.Context import com.aheaditec.freerasp.Threat +import com.aheaditec.talsec_security.security.api.SuspiciousAppInfo import com.aheaditec.talsec_security.security.api.Talsec import com.aheaditec.talsec_security.security.api.TalsecConfig import io.flutter.plugin.common.EventChannel.EventSink @@ -12,6 +13,7 @@ import io.flutter.plugin.common.EventChannel.EventSink */ internal object TalsecThreatHandler { private var eventSink: EventSink? = null + private var methodSink: MethodCallInvoker.MethodSink? = null private var isListening = false /** @@ -78,7 +80,7 @@ internal object TalsecThreatHandler { * In contrast to [detachListener], this function does not unregister the listener. It only * suspends the listener, meaning all detected threats are cached and sent later. * - * In contrast to [detachSink], this function does not nullify the [eventSink]. It only suspends + * In contrast to [detachEventSink], this function does not nullify the [eventSink]. It only suspends * sending events to the event sink. This is useful when the application goes to background and * [EventSink] is not destroyed but also is not able to send events. */ @@ -92,7 +94,7 @@ internal object TalsecThreatHandler { * In contrast to [attachListener], this function does not register the listener. It only * resumes the listener, meaning all cached threats are sent to the [EventSink]. * - * In contrast to [attachSink], this function does not assign new [EventSink] to [eventSink]. + * In contrast to [attachEventSink], this function does not assign new [EventSink] to [eventSink]. * It only resumes sending events to the current [eventSink]. * This is useful when the application comes to foreground and [EventSink] is not destroyed but * also is not able to send events. @@ -110,7 +112,7 @@ internal object TalsecThreatHandler { * * @param eventSink The event sink of the new listener. */ - internal fun attachSink(eventSink: EventSink) { + internal fun attachEventSink(eventSink: EventSink) { this.eventSink = eventSink PluginThreatHandler.listener = ThreatListener flushThreatCache(eventSink) @@ -119,7 +121,7 @@ internal object TalsecThreatHandler { /** * Called when a listener unsubscribes from the event channel. */ - internal fun detachSink() { + internal fun detachEventSink() { eventSink = null PluginThreatHandler.listener = null } @@ -134,6 +136,19 @@ internal object TalsecThreatHandler { eventSink?.success(it.value) } PluginThreatHandler.detectedThreats.clear() + + PluginThreatHandler.detectedMalware.let { + methodSink?.onMalwareDetected(it) + } + PluginThreatHandler.detectedMalware.clear() + } + + internal fun attachMethodSink(methodSink: MethodCallInvoker.MethodSink) { + this.methodSink = methodSink + } + + internal fun detachMethodSink() { + methodSink = null } /** @@ -145,5 +160,10 @@ internal object TalsecThreatHandler { override fun threatDetected(threatType: Threat) { eventSink?.success(threatType.value) } + + override fun malwareDetected(suspiciousApps: List) { + methodSink?.onMalwareDetected(suspiciousApps) + } } } + diff --git a/lib/src/typedefs.dart b/lib/src/typedefs.dart index 9d3cbf9..085e5f1 100644 --- a/lib/src/typedefs.dart +++ b/lib/src/typedefs.dart @@ -1,2 +1,6 @@ +import '../freerasp.dart'; + /// Typedef for void methods typedef VoidCallback = void Function(); + +typedef MalwareCallback = void Function(List); From 466462a213d8fe0b5d9bd913bc2bfb6b49cff2e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Novotn=C3=BD?= Date: Thu, 26 Sep 2024 09:44:15 +0200 Subject: [PATCH 31/66] feat: update example app --- example/lib/main.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/example/lib/main.dart b/example/lib/main.dart index 6a265dd..64be409 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -19,6 +19,7 @@ void main() async { packageName: 'com.aheaditec.freeraspExample', signingCertHashes: ['AKoRuyLMM91E7lX/Zqp3u4jMmd0A7hH/Iqozu0TMVd0='], supportedStores: ['com.sec.android.app.samsungapps'], + blocklistedPackageNames: ['com.aheaditec.freeraspExample'], ), /// For iOS From 477aa3bbf36633a237c2eb9150ef1365b2f45fa1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Novotn=C3=BD?= Date: Thu, 26 Sep 2024 09:44:32 +0200 Subject: [PATCH 32/66] feat: update threat callback --- lib/src/threat_callback.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/src/threat_callback.dart b/lib/src/threat_callback.dart index f91b553..c4732b5 100644 --- a/lib/src/threat_callback.dart +++ b/lib/src/threat_callback.dart @@ -33,6 +33,7 @@ class ThreatCallback { this.onSecureHardwareNotAvailable, this.onSystemVPN, this.onDevMode, + this.onMalware, }); /// This method is called when a threat related dynamic hooking (e.g. Frida) @@ -80,4 +81,7 @@ class ThreatCallback { /// This method is called whe the device has Developer mode enabled final VoidCallback? onDevMode; + + /// This method is called when malware is detected on the device + final MalwareCallback? onMalware; } From d9cbb3c313b3bccaf7b3d32ac380e5c4425314ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Novotn=C3=BD?= Date: Thu, 26 Sep 2024 09:44:41 +0200 Subject: [PATCH 33/66] feat: misc --- .../com/aheaditec/freerasp/utils/Utils.kt | 75 ++++++++++++++----- lib/src/models/models.dart | 2 + test/src/talsec_test.dart | 49 ++++++++---- 3 files changed, 95 insertions(+), 31 deletions(-) diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/utils/Utils.kt b/android/src/main/kotlin/com/aheaditec/freerasp/utils/Utils.kt index 43efdea..a51e1b2 100644 --- a/android/src/main/kotlin/com/aheaditec/freerasp/utils/Utils.kt +++ b/android/src/main/kotlin/com/aheaditec/freerasp/utils/Utils.kt @@ -1,5 +1,6 @@ package com.aheaditec.freerasp.utils +import com.aheaditec.talsec_security.security.api.SuspiciousAppInfo import com.aheaditec.talsec_security.security.api.TalsecConfig import org.json.JSONException import org.json.JSONObject @@ -11,31 +12,71 @@ internal class Utils { throw JSONException("Configuration is null") } val json = JSONObject(configJson) - val androidConfig = json.getJSONObject("androidConfig") - val packageName = androidConfig.getString("packageName") - val certificateHashes = mutableListOf() - val hashes = androidConfig.getJSONArray("signingCertHashes") - for (i in 0 until hashes.length()) { - certificateHashes.add(hashes.getString(i)) - } + val watcherMail = json.getString("watcherMail") - val alternativeStores = mutableListOf() - if (androidConfig.has("supportedStores")) { - val stores = androidConfig.getJSONArray("supportedStores") - for (i in 0 until stores.length()) { - alternativeStores.add(stores.getString(i)) - } - } var isProd = true if (json.has("isProd")) { isProd = json.getBoolean("isProd") } + val androidConfig = json.getJSONObject("androidConfig") - return TalsecConfig.Builder(packageName, certificateHashes.toTypedArray()) + val packageName = androidConfig.getString("packageName") + val certificateHashes = androidConfig.extractArray("signingCertHashes") + val alternativeStores = androidConfig.extractArray("supportedStores") + val blocklistedPackageNames = + androidConfig.extractArray("blocklistedPackageNames") + val blocklistedHashes = androidConfig.extractArray("blocklistedHashes") + val whitelistedInstallationSources = + androidConfig.extractArray("whitelistedInstallationSources") + + val blocklistedPermissions = mutableListOf>() + if (androidConfig.has("blocklistedPermissions")) { + val permissions = androidConfig.getJSONArray("blocklistedPermissions") + for (i in 0 until permissions.length()) { + val permission = permissions.getJSONArray(i) + val permissionList = mutableListOf() + for (j in 0 until permission.length()) { + permissionList.add(permission.getString(j)) + } + blocklistedPermissions.add(permissionList.toTypedArray()) + } + } + + return TalsecConfig.Builder(packageName, certificateHashes) .watcherMail(watcherMail) - .supportedAlternativeStores(alternativeStores.toTypedArray()) + .supportedAlternativeStores(alternativeStores) .prod(isProd) + .blocklistedPackageNames(blocklistedPackageNames) + .blocklistedHashes(blocklistedHashes) + .blocklistedPermissions(blocklistedPermissions.toTypedArray()) + .whitelistedInstallationSources(whitelistedInstallationSources) .build() } + + fun fromTalsec(malwareInfo: List) { + val packageInfoList = mutableListOf() + for (info in malwareInfo) { + packageInfoList.add(info.toJson()) + } + } + } +} + +inline fun JSONObject.extractArray(key: String): Array { + val list = mutableListOf() + if (this.has(key)) { + val jsonArray = this.getJSONArray(key) + for (i in 0 until jsonArray.length()) { + val element = when (T::class) { + String::class -> jsonArray.getString(i) as T + Int::class -> jsonArray.getInt(i) as T + Double::class -> jsonArray.getDouble(i) as T + Boolean::class -> jsonArray.getBoolean(i) as T + Long::class -> jsonArray.getLong(i) as T + else -> throw IllegalArgumentException("Unsupported type") + } + list.add(element) + } } -} \ No newline at end of file + return list.toTypedArray() +} diff --git a/lib/src/models/models.dart b/lib/src/models/models.dart index d18e85e..a24b299 100644 --- a/lib/src/models/models.dart +++ b/lib/src/models/models.dart @@ -1,3 +1,5 @@ export 'android_config.dart'; export 'ios_config.dart'; +export 'package_info.dart'; +export 'suspicious_app_info.dart'; export 'talsec_config.dart'; diff --git a/test/src/talsec_test.dart b/test/src/talsec_test.dart index 605b1ab..8e9fa8b 100644 --- a/test/src/talsec_test.dart +++ b/test/src/talsec_test.dart @@ -43,8 +43,11 @@ void main() { watcherMail: mockWatcherMail, ); // Act - final talsec = - Talsec.private(methodChannel.methodChannel, FakeEventChannel()); + final talsec = Talsec.private( + methodChannel.methodChannel, + methodChannel.methodChannel, + FakeEventChannel(), + ); // Assert expect( @@ -67,8 +70,11 @@ void main() { watcherMail: mockWatcherMail, ); // Act - final talsec = - Talsec.private(methodChannel.methodChannel, FakeEventChannel()); + final talsec = Talsec.private( + methodChannel.methodChannel, + methodChannel.methodChannel, + FakeEventChannel(), + ); // Assert expect( @@ -87,8 +93,11 @@ void main() { debugDefaultTargetPlatformOverride = TargetPlatform.android; // Act - final talsec = - Talsec.private(methodChannel.methodChannel, FakeEventChannel()); + final talsec = Talsec.private( + methodChannel.methodChannel, + methodChannel.methodChannel, + FakeEventChannel(), + ); // Assert expect( @@ -113,8 +122,11 @@ void main() { ); // Act - final talsec = - Talsec.private(methodChannel.methodChannel, FakeEventChannel()); + final talsec = Talsec.private( + methodChannel.methodChannel, + methodChannel.methodChannel, + FakeEventChannel(), + ); // Assert expect( @@ -151,8 +163,11 @@ void main() { 1115787534, ], ); - final talsec = - Talsec.private(FakeMethodChannel(), eventChannel.eventChannel); + final talsec = Talsec.private( + FakeMethodChannel(), + FakeMethodChannel(), + eventChannel.eventChannel, + ); // Act final stream = talsec.onThreatDetected; @@ -174,8 +189,11 @@ void main() { data: [], exceptions: [PlatformException(code: 'dummy-code')], ); - final talsec = - Talsec.private(FakeMethodChannel(), eventChannel.eventChannel); + final talsec = Talsec.private( + FakeMethodChannel(), + FakeMethodChannel(), + eventChannel.eventChannel, + ); // Act final stream = talsec.onThreatDetected; @@ -205,8 +223,11 @@ void main() { ], exceptions: [PlatformException(code: 'dummy-code')], ); - final talsec = - Talsec.private(FakeMethodChannel(), eventChannel.eventChannel); + final talsec = Talsec.private( + FakeMethodChannel(), + FakeMethodChannel(), + eventChannel.eventChannel, + ); // Act final stream = talsec.onThreatDetected; From 49cb0c4b0f0276e9085ec4242c65c9d9e16b4845 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Novotn=C3=BD?= Date: Mon, 30 Sep 2024 11:44:12 +0200 Subject: [PATCH 34/66] feat: so much code --- analysis_options.yaml | 4 + .../com/aheaditec/freerasp/Extensions.kt | 70 ++++++++ .../com/aheaditec/freerasp/FreeraspPlugin.kt | 4 - .../kotlin/com/aheaditec/freerasp/Utils.kt | 158 ++++++++++++++++++ .../freerasp/generated/TalsecPigeonApi.kt | 136 +++++++++++++++ .../freerasp/handlers/MethodCallHandler.kt | 33 +++- .../freerasp/handlers/MethodCallInvoker.kt | 72 -------- .../freerasp/handlers/TalsecThreatHandler.kt | 13 +- .../aheaditec/freerasp/models/PackageInfo.kt | 33 ---- .../freerasp/models/SuspiciousAppInfo.kt | 21 --- .../com/aheaditec/freerasp/utils/Utils.kt | 82 --------- example/lib/threat_notifier.dart | 2 + lib/src/generated/talsec_pigeon_api.g.dart | 153 +++++++++++++++++ lib/src/models/models.dart | 2 - lib/src/models/package_info.dart | 24 --- lib/src/models/package_info.g.dart | 24 --- lib/src/models/suspicious_app_info.dart | 18 -- lib/src/models/suspicious_app_info.g.dart | 20 --- lib/src/talsec.dart | 55 ++---- lib/src/threat_callback.dart | 12 +- lib/src/typedefs.dart | 5 +- pigeons/talsec_pigeon_api.dart | 44 +++++ pubspec.yaml | 1 + test/src/talsec_test.dart | 7 - 24 files changed, 638 insertions(+), 355 deletions(-) create mode 100644 android/src/main/kotlin/com/aheaditec/freerasp/Utils.kt create mode 100644 android/src/main/kotlin/com/aheaditec/freerasp/generated/TalsecPigeonApi.kt delete mode 100644 android/src/main/kotlin/com/aheaditec/freerasp/handlers/MethodCallInvoker.kt delete mode 100644 android/src/main/kotlin/com/aheaditec/freerasp/models/PackageInfo.kt delete mode 100644 android/src/main/kotlin/com/aheaditec/freerasp/models/SuspiciousAppInfo.kt delete mode 100644 android/src/main/kotlin/com/aheaditec/freerasp/utils/Utils.kt create mode 100644 lib/src/generated/talsec_pigeon_api.g.dart delete mode 100644 lib/src/models/package_info.dart delete mode 100644 lib/src/models/package_info.g.dart delete mode 100644 lib/src/models/suspicious_app_info.dart delete mode 100644 lib/src/models/suspicious_app_info.g.dart create mode 100644 pigeons/talsec_pigeon_api.dart diff --git a/analysis_options.yaml b/analysis_options.yaml index 273cb60..f74ab60 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1 +1,5 @@ include: package:very_good_analysis/analysis_options.3.1.0.yaml + +analyzer: + exclude: + - '**/*.g.dart' \ No newline at end of file diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/Extensions.kt b/android/src/main/kotlin/com/aheaditec/freerasp/Extensions.kt index ae3c415..0a86ebd 100644 --- a/android/src/main/kotlin/com/aheaditec/freerasp/Extensions.kt +++ b/android/src/main/kotlin/com/aheaditec/freerasp/Extensions.kt @@ -1,6 +1,12 @@ package com.aheaditec.freerasp +import android.content.Context +import android.content.pm.PackageInfo +import android.os.Build +import com.aheaditec.talsec_security.security.api.SuspiciousAppInfo import io.flutter.plugin.common.MethodChannel +import com.aheaditec.freerasp.generated.PackageInfo as FlutterPackageInfo +import com.aheaditec.freerasp.generated.SuspiciousAppInfo as FlutterSuspiciousAppInfo /** * Executes the provided block of code and catches any exceptions thrown by it, returning the @@ -17,3 +23,67 @@ internal inline fun runResultCatching(result: MethodChannel.Result, block: () -> result.error(err::class.java.name, err.message, null) } } + +/** + * Converts a [SuspiciousAppInfo] instance to a [com.aheaditec.freerasp.generated.SuspiciousAppInfo] + * instance used by Pigeon package for Flutter. + * + * @return A new [com.aheaditec.freerasp.generated.SuspiciousAppInfo] object with information from + * this [SuspiciousAppInfo]. + */ +internal fun SuspiciousAppInfo.toPigeon(context: Context): FlutterSuspiciousAppInfo { + return FlutterSuspiciousAppInfo(this.packageInfo.toPigeon(context), this.reason) +} + +/** + * Converts a [PackageInfo] instance to a [com.aheaditec.freerasp.generated.PackageInfo] instance + * used by Pigeon package for Flutter. + * + * @return A new [com.aheaditec.freerasp.generated.PackageInfo] object with information from + * this [PackageInfo]. + */ +private fun PackageInfo.toPigeon(context: Context): FlutterPackageInfo { + return FlutterPackageInfo( + packageName = packageName, + appName = applicationInfo?.name, + version = getVersionString(), + appIcon = Utils.parseIconBase64(context, packageName), + installationSource = Utils.getInstallerPackageName(context, packageName), + ) +} + +/** + * Retrieves the version string of the package. + * + * For devices running on Android P (API 28) and above, this method returns the `longVersionCode`. + * For older versions, it returns the `versionCode` (deprecated). + * + * @return A string representation of the version code. + */ +internal fun PackageInfo.getVersionString(): String { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + return longVersionCode.toString() + } + @Suppress("DEPRECATION") + return versionCode.toString() +} + +/** + * Returns the encapsulated value if this instance represents success or throws the encapsulated exception + * if it is a failure, executing the given action before throwing. + * + * This function is similar to `Result.getOrThrow()`, but with the added functionality of performing + * an action before throwing the exception. + * + * @param action The action to be executed if the result is a failure. This action should not throw an exception. + * @return The encapsulated value if the result is a success. + * @throws Throwable The encapsulated exception if the result is a failure. + * + * @see Result.getOrThrow + */ +inline fun Result.getOrElseThenThrow(action: () -> Unit): T { + return getOrElse { + action() + throw it + } +} \ No newline at end of file diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/FreeraspPlugin.kt b/android/src/main/kotlin/com/aheaditec/freerasp/FreeraspPlugin.kt index 48ede80..4188a4a 100644 --- a/android/src/main/kotlin/com/aheaditec/freerasp/FreeraspPlugin.kt +++ b/android/src/main/kotlin/com/aheaditec/freerasp/FreeraspPlugin.kt @@ -5,7 +5,6 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleOwner import com.aheaditec.freerasp.handlers.MethodCallHandler -import com.aheaditec.freerasp.handlers.MethodCallInvoker import com.aheaditec.freerasp.handlers.StreamHandler import com.aheaditec.freerasp.handlers.TalsecThreatHandler import io.flutter.embedding.engine.plugins.FlutterPlugin @@ -18,7 +17,6 @@ import io.flutter.embedding.engine.plugins.lifecycle.FlutterLifecycleAdapter class FreeraspPlugin : FlutterPlugin, ActivityAware, LifecycleEventObserver { private var streamHandler: StreamHandler = StreamHandler() private var methodCallHandler: MethodCallHandler = MethodCallHandler() - private var methodCallInvoker : MethodCallInvoker = MethodCallInvoker() private var context: Context? = null private var lifecycle: Lifecycle? = null @@ -28,13 +26,11 @@ class FreeraspPlugin : FlutterPlugin, ActivityAware, LifecycleEventObserver { context = flutterPluginBinding.applicationContext methodCallHandler.createMethodChannel(messenger, flutterPluginBinding.applicationContext) - methodCallInvoker.createMethodChannel(messenger, flutterPluginBinding.applicationContext) streamHandler.createEventChannel(messenger) } override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { methodCallHandler.destroyMethodChannel() - methodCallInvoker.destroyMethodChannel() streamHandler.destroyEventChannel() TalsecThreatHandler.detachListener(binding.applicationContext) } diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/Utils.kt b/android/src/main/kotlin/com/aheaditec/freerasp/Utils.kt new file mode 100644 index 0000000..37c97cc --- /dev/null +++ b/android/src/main/kotlin/com/aheaditec/freerasp/Utils.kt @@ -0,0 +1,158 @@ +package com.aheaditec.freerasp + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import android.os.Build +import android.util.Base64 +import com.aheaditec.talsec_security.security.api.TalsecConfig +import org.json.JSONException +import org.json.JSONObject +import java.io.ByteArrayOutputStream + +internal object Utils { + fun toTalsecConfigThrowing(configJson: String?): TalsecConfig { + if (configJson == null) { + throw JSONException("Configuration is null") + } + val json = JSONObject(configJson) + + val watcherMail = json.getString("watcherMail") + var isProd = true + if (json.has("isProd")) { + isProd = json.getBoolean("isProd") + } + val androidConfig = json.getJSONObject("androidConfig") + + val packageName = androidConfig.getString("packageName") + val certificateHashes = androidConfig.extractArray("signingCertHashes") + val alternativeStores = androidConfig.extractArray("supportedStores") + val blocklistedPackageNames = + androidConfig.extractArray("blocklistedPackageNames") + val blocklistedHashes = androidConfig.extractArray("blocklistedHashes") + val whitelistedInstallationSources = + androidConfig.extractArray("whitelistedInstallationSources") + + val blocklistedPermissions = mutableListOf>() + if (androidConfig.has("blocklistedPermissions")) { + val permissions = androidConfig.getJSONArray("blocklistedPermissions") + for (i in 0 until permissions.length()) { + val permission = permissions.getJSONArray(i) + val permissionList = mutableListOf() + for (j in 0 until permission.length()) { + permissionList.add(permission.getString(j)) + } + blocklistedPermissions.add(permissionList.toTypedArray()) + } + } + + return TalsecConfig.Builder(packageName, certificateHashes) + .watcherMail(watcherMail) + .supportedAlternativeStores(alternativeStores) + .prod(isProd) + .blocklistedPackageNames(blocklistedPackageNames) + .blocklistedHashes(blocklistedHashes) + .blocklistedPermissions(blocklistedPermissions.toTypedArray()) + .whitelistedInstallationSources(whitelistedInstallationSources) + .build() + } + + /** + * Retrieves the package name of the installer for a given app package. + * + * @param context The context of the application. + * @param packageName The package name of the app whose installer package name is to be retrieved. + * @return The package name of the installer if available, or `null` if not. + */ + fun getInstallerPackageName(context: Context, packageName: String): String? { + runCatching { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) + return context.packageManager.getInstallSourceInfo(packageName).installingPackageName + @Suppress("DEPRECATION") + return context.packageManager.getInstallerPackageName(packageName) + } + return null + } + + /** + * Converts the application icon of the specified package into a Base64 encoded string. + * + * @param context The context of the application. + * @param packageName The package name of the app whose icon is to be converted. + * @return A Base64 encoded string representing the app icon. + */ + fun parseIconBase64(context: Context, packageName: String): String? { + val result = runCatching { + val drawable = context.packageManager.getApplicationIcon(packageName) + val bitmap = drawable.toBitmap() + bitmap.toBase64() + } + + return result.getOrNull() + } + + /** + * Creates a Bitmap from a Drawable object. + * + * @param drawable The Drawable to be converted. + * @return A Bitmap representing the drawable. + */ + private fun createBitmapFromDrawable(drawable: Drawable): Bitmap { + val width = if (drawable.intrinsicWidth > 0) drawable.intrinsicWidth else 1 + val height = if (drawable.intrinsicHeight > 0) drawable.intrinsicHeight else 1 + val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) + val canvas = Canvas(bitmap) + + drawable.setBounds(0, 0, canvas.width, canvas.height) + drawable.draw(canvas) + + return bitmap + } + + /** + * Converts a Drawable into a Bitmap. + * + * @receiver The Drawable to be converted. + * @return A Bitmap representing the drawable. + */ + private fun Drawable.toBitmap(): Bitmap { + return when (this) { + is BitmapDrawable -> bitmap + else -> createBitmapFromDrawable(this) + } + } + + /** + * Converts a Bitmap into a Base64 encoded string. + * + * @receiver The Bitmap to be converted. + * @return A Base64 encoded string representing the bitmap. + */ + private fun Bitmap.toBase64(): String { + val byteArrayOutputStream = ByteArrayOutputStream() + compress(Bitmap.CompressFormat.PNG, 10, byteArrayOutputStream) + val byteArray = byteArrayOutputStream.toByteArray() + return Base64.encodeToString(byteArray, Base64.DEFAULT) + } +} + +inline fun JSONObject.extractArray(key: String): Array { + val list = mutableListOf() + if (this.has(key)) { + val jsonArray = this.getJSONArray(key) + for (i in 0 until jsonArray.length()) { + val element = when (T::class) { + String::class -> jsonArray.getString(i) as T + Int::class -> jsonArray.getInt(i) as T + Double::class -> jsonArray.getDouble(i) as T + Boolean::class -> jsonArray.getBoolean(i) as T + Long::class -> jsonArray.getLong(i) as T + else -> throw IllegalArgumentException("Unsupported type") + } + list.add(element) + } + } + return list.toTypedArray() +} diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/generated/TalsecPigeonApi.kt b/android/src/main/kotlin/com/aheaditec/freerasp/generated/TalsecPigeonApi.kt new file mode 100644 index 0000000..e75fd00 --- /dev/null +++ b/android/src/main/kotlin/com/aheaditec/freerasp/generated/TalsecPigeonApi.kt @@ -0,0 +1,136 @@ +// Autogenerated from Pigeon (v22.4.0), do not edit directly. +// See also: https://pub.dev/packages/pigeon +@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass") + +package com.aheaditec.freerasp.generated + +import android.util.Log +import io.flutter.plugin.common.BasicMessageChannel +import io.flutter.plugin.common.BinaryMessenger +import io.flutter.plugin.common.MessageCodec +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() + +/** 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 installationSource: 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 installationSource = pigeonVar_list[4] as String? + return PackageInfo(packageName, appIcon, appName, version, installationSource) + } + } + fun toList(): List { + return listOf( + packageName, + appIcon, + appName, + version, + installationSource, + ) + } +} + +/** Generated class from Pigeon that represents data sent in messages. */ +data class SuspiciousAppInfo ( + val packageInfo: PackageInfo, + val reason: String +) + { + companion object { + fun fromList(pigeonVar_list: List): SuspiciousAppInfo { + val packageInfo = pigeonVar_list[0] as PackageInfo + val reason = pigeonVar_list[1] as String + return SuspiciousAppInfo(packageInfo, reason) + } + } + fun toList(): List { + return listOf( + packageInfo, + reason, + ) + } +} +private open class TalsecPigeonApiPigeonCodec : StandardMessageCodec() { + override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? { + return when (type) { + 129.toByte() -> { + return (readValue(buffer) as? List)?.let { + PackageInfo.fromList(it) + } + } + 130.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 PackageInfo -> { + stream.write(129) + writeValue(stream, value.toList()) + } + is SuspiciousAppInfo -> { + stream.write(130) + 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))) + } + } + } +} diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/handlers/MethodCallHandler.kt b/android/src/main/kotlin/com/aheaditec/freerasp/handlers/MethodCallHandler.kt index e6d47de..e8208ad 100644 --- a/android/src/main/kotlin/com/aheaditec/freerasp/handlers/MethodCallHandler.kt +++ b/android/src/main/kotlin/com/aheaditec/freerasp/handlers/MethodCallHandler.kt @@ -2,7 +2,11 @@ package com.aheaditec.freerasp.handlers import android.content.Context import com.aheaditec.freerasp.runResultCatching -import com.aheaditec.freerasp.utils.Utils +import com.aheaditec.freerasp.Utils +import com.aheaditec.freerasp.generated.TalsecPigeonApi +import com.aheaditec.freerasp.getOrElseThenThrow +import com.aheaditec.freerasp.toPigeon +import com.aheaditec.talsec_security.security.api.SuspiciousAppInfo import io.flutter.Log import io.flutter.plugin.common.BinaryMessenger import io.flutter.plugin.common.MethodCall @@ -15,11 +19,31 @@ import io.flutter.plugin.common.MethodChannel.MethodCallHandler internal class MethodCallHandler : MethodCallHandler { private var context: Context? = null private var methodChannel: MethodChannel? = null + private var pigeonApi: TalsecPigeonApi? = null companion object { 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) } + pigeonApi?.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.getOrElseThenThrow { + Log.e("MethodCallHandlerSink", "Result ended with failure") + } + } + } + } + } + + internal interface MethodSink { + fun onMalwareDetected(packageInfo: List) + } + /** * Creates a new [MethodChannel] with the specified [BinaryMessenger] instance. Sets this class * as the [MethodCallHandler]. @@ -39,6 +63,9 @@ internal class MethodCallHandler : MethodCallHandler { } this.context = context + this.pigeonApi = TalsecPigeonApi(messenger) + + TalsecThreatHandler.attachMethodSink(sink) } /** @@ -47,7 +74,11 @@ internal class MethodCallHandler : MethodCallHandler { fun destroyMethodChannel() { methodChannel?.setMethodCallHandler(null) methodChannel = null + this.context = null + this.pigeonApi = null + + TalsecThreatHandler.detachMethodSink() } /** diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/handlers/MethodCallInvoker.kt b/android/src/main/kotlin/com/aheaditec/freerasp/handlers/MethodCallInvoker.kt deleted file mode 100644 index 1f0f5f0..0000000 --- a/android/src/main/kotlin/com/aheaditec/freerasp/handlers/MethodCallInvoker.kt +++ /dev/null @@ -1,72 +0,0 @@ -package com.aheaditec.freerasp.handlers - -import android.content.Context -import com.aheaditec.talsec_security.security.api.SuspiciousAppInfo -import io.flutter.Log -import io.flutter.plugin.common.BinaryMessenger -import io.flutter.plugin.common.MethodCall -import io.flutter.plugin.common.MethodChannel -import io.flutter.plugin.common.MethodChannel.MethodCallHandler - -/** - * A method handler that creates and manages an [MethodChannel] for freeRASP methods. - */ -internal class MethodCallInvoker: MethodCallHandler { - private var context: Context? = null - private var methodChannel: MethodChannel? = null - private val methodSink = object : MethodSink { - override fun onMalwareDetected(packageInfo: List) { - methodChannel?.invokeMethod("onMalwareDetected", mapOf("packageInfo" to packageInfo.map { })) - } - } - - companion object { - private const val CHANNEL_NAME: String = "talsec.app/freerasp/invoke" - } - - internal interface MethodSink { - fun onMalwareDetected(packageInfo: List) - } - - /** - * Creates a new [MethodChannel] with the specified [BinaryMessenger] instance. Sets this class - * as the [MethodCallHandler]. - * If an old [MethodChannel] already exists, it will be destroyed before creating a new one. - * - * @param messenger The binary messenger to use for creating the [MethodChannel]. - * @param context The Android [Context] associated with this channel. - */ - fun createMethodChannel(messenger: BinaryMessenger, context: Context) { - methodChannel?.let { - Log.i("MethodCallHandler", "Tried to create channel without disposing old one.") - destroyMethodChannel() - } - - methodChannel = MethodChannel(messenger, CHANNEL_NAME).also { - it.setMethodCallHandler(this) - } - - this.context = context - TalsecThreatHandler.attachMethodSink(methodSink) - } - - /** - * Destroys the `MethodChannel` and clears associated variables. - */ - fun destroyMethodChannel() { - methodChannel?.setMethodCallHandler(null) - methodChannel = null - this.context = null - TalsecThreatHandler.detachMethodSink() - } - - /** - * Handles method calls received through the [MethodChannel]. - * - * @param call The method call. - * @param result The result handler of the method call. - */ - override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { - result.error("INVALID", "This channel does not handle calls from Flutter.", null) - } -} diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/handlers/TalsecThreatHandler.kt b/android/src/main/kotlin/com/aheaditec/freerasp/handlers/TalsecThreatHandler.kt index cb195c9..cfdf562 100644 --- a/android/src/main/kotlin/com/aheaditec/freerasp/handlers/TalsecThreatHandler.kt +++ b/android/src/main/kotlin/com/aheaditec/freerasp/handlers/TalsecThreatHandler.kt @@ -13,7 +13,7 @@ import io.flutter.plugin.common.EventChannel.EventSink */ internal object TalsecThreatHandler { private var eventSink: EventSink? = null - private var methodSink: MethodCallInvoker.MethodSink? = null + private var methodSink: MethodCallHandler.MethodSink? = null private var isListening = false /** @@ -135,16 +135,19 @@ internal object TalsecThreatHandler { PluginThreatHandler.detectedThreats.forEach { eventSink?.success(it.value) } - PluginThreatHandler.detectedThreats.clear() PluginThreatHandler.detectedMalware.let { - methodSink?.onMalwareDetected(it) + if (it.isNotEmpty()) { + methodSink?.onMalwareDetected(it) + } } + + PluginThreatHandler.detectedThreats.clear() PluginThreatHandler.detectedMalware.clear() } - internal fun attachMethodSink(methodSink: MethodCallInvoker.MethodSink) { - this.methodSink = methodSink + internal fun attachMethodSink(sink: MethodCallHandler.MethodSink) { + this.methodSink = sink } internal fun detachMethodSink() { diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/models/PackageInfo.kt b/android/src/main/kotlin/com/aheaditec/freerasp/models/PackageInfo.kt deleted file mode 100644 index aab9a20..0000000 --- a/android/src/main/kotlin/com/aheaditec/freerasp/models/PackageInfo.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.aheaditec.freerasp.models - -import org.json.JSONObject - -data class PackageInfo( - val packageName: String, - val appIcon: String? = null, - val appName: String? = null, - val version: String? = null, - val installationSource: String? = null -) { - companion object { - fun fromTalsec(packageInfo: android.content.pm.PackageInfo): PackageInfo { - return PackageInfo( - packageInfo.packageName, - packageInfo.appIcon, - packageInfo.appName, - packageInfo.version, - packageInfo.installationSource - ) - } - } - - fun toJson(): String { - val json = JSONObject().put("packageName", packageName) - .putOpt("appIcon", appIcon) - .putOpt("appName", appName) - .putOpt("version", version) - .putOpt("installationSource", installationSource) - - return json.toString() - } -} \ No newline at end of file diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/models/SuspiciousAppInfo.kt b/android/src/main/kotlin/com/aheaditec/freerasp/models/SuspiciousAppInfo.kt deleted file mode 100644 index bb93a8e..0000000 --- a/android/src/main/kotlin/com/aheaditec/freerasp/models/SuspiciousAppInfo.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.aheaditec.freerasp.models - -import com.aheaditec.talsec_security.security.api.SuspiciousAppInfo as TalsecSuspiciousAppInfo - -class SuspiciousAppInfo( - val packageInfo: PackageInfo, - val reason: String -) { - companion object { - fun fromTalsec(suspiciousAppInfo: TalsecSuspiciousAppInfo): SuspiciousAppInfo { - return SuspiciousAppInfo( - PackageInfo.fromTalsec(suspiciousAppInfo.packageInfo), - suspiciousAppInfo.reason - ) - } - } - - fun toJson(): String { - return "{\"packageInfo\": ${packageInfo.toJson()}, \"reason\": \"$reason\"}" - } -} \ No newline at end of file diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/utils/Utils.kt b/android/src/main/kotlin/com/aheaditec/freerasp/utils/Utils.kt deleted file mode 100644 index a51e1b2..0000000 --- a/android/src/main/kotlin/com/aheaditec/freerasp/utils/Utils.kt +++ /dev/null @@ -1,82 +0,0 @@ -package com.aheaditec.freerasp.utils - -import com.aheaditec.talsec_security.security.api.SuspiciousAppInfo -import com.aheaditec.talsec_security.security.api.TalsecConfig -import org.json.JSONException -import org.json.JSONObject - -internal class Utils { - companion object { - fun toTalsecConfigThrowing(configJson: String?): TalsecConfig { - if (configJson == null) { - throw JSONException("Configuration is null") - } - val json = JSONObject(configJson) - - val watcherMail = json.getString("watcherMail") - var isProd = true - if (json.has("isProd")) { - isProd = json.getBoolean("isProd") - } - val androidConfig = json.getJSONObject("androidConfig") - - val packageName = androidConfig.getString("packageName") - val certificateHashes = androidConfig.extractArray("signingCertHashes") - val alternativeStores = androidConfig.extractArray("supportedStores") - val blocklistedPackageNames = - androidConfig.extractArray("blocklistedPackageNames") - val blocklistedHashes = androidConfig.extractArray("blocklistedHashes") - val whitelistedInstallationSources = - androidConfig.extractArray("whitelistedInstallationSources") - - val blocklistedPermissions = mutableListOf>() - if (androidConfig.has("blocklistedPermissions")) { - val permissions = androidConfig.getJSONArray("blocklistedPermissions") - for (i in 0 until permissions.length()) { - val permission = permissions.getJSONArray(i) - val permissionList = mutableListOf() - for (j in 0 until permission.length()) { - permissionList.add(permission.getString(j)) - } - blocklistedPermissions.add(permissionList.toTypedArray()) - } - } - - return TalsecConfig.Builder(packageName, certificateHashes) - .watcherMail(watcherMail) - .supportedAlternativeStores(alternativeStores) - .prod(isProd) - .blocklistedPackageNames(blocklistedPackageNames) - .blocklistedHashes(blocklistedHashes) - .blocklistedPermissions(blocklistedPermissions.toTypedArray()) - .whitelistedInstallationSources(whitelistedInstallationSources) - .build() - } - - fun fromTalsec(malwareInfo: List) { - val packageInfoList = mutableListOf() - for (info in malwareInfo) { - packageInfoList.add(info.toJson()) - } - } - } -} - -inline fun JSONObject.extractArray(key: String): Array { - val list = mutableListOf() - if (this.has(key)) { - val jsonArray = this.getJSONArray(key) - for (i in 0 until jsonArray.length()) { - val element = when (T::class) { - String::class -> jsonArray.getString(i) as T - Int::class -> jsonArray.getInt(i) as T - Double::class -> jsonArray.getDouble(i) as T - Boolean::class -> jsonArray.getBoolean(i) as T - Long::class -> jsonArray.getLong(i) as T - else -> throw IllegalArgumentException("Unsupported type") - } - list.add(element) - } - } - return list.toTypedArray() -} diff --git a/example/lib/threat_notifier.dart b/example/lib/threat_notifier.dart index 2fe83fc..e68945b 100644 --- a/example/lib/threat_notifier.dart +++ b/example/lib/threat_notifier.dart @@ -1,3 +1,4 @@ +import 'dart:developer'; import 'dart:io'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -22,6 +23,7 @@ class ThreatNotifier extends StateNotifier> { onUnofficialStore: () => _updateThreat(Threat.unofficialStore), onSystemVPN: () => _updateThreat(Threat.systemVPN), onDevMode: () => _updateThreat(Threat.devMode), + onMalware: (threat) => log('Malware detected: $threat'), ); Talsec.instance.attachListener(callback); diff --git a/lib/src/generated/talsec_pigeon_api.g.dart b/lib/src/generated/talsec_pigeon_api.g.dart new file mode 100644 index 0000000..57d8406 --- /dev/null +++ b/lib/src/generated/talsec_pigeon_api.g.dart @@ -0,0 +1,153 @@ +// Autogenerated from Pigeon (v22.4.0), 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]; +} + +class PackageInfo { + PackageInfo({ + required this.packageName, + this.appIcon, + this.appName, + this.version, + this.installationSource, + }); + + String packageName; + + String? appIcon; + + String? appName; + + String? version; + + String? installationSource; + + Object encode() { + return [ + packageName, + appIcon, + appName, + version, + installationSource, + ]; + } + + 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?, + installationSource: result[4] as String?, + ); + } +} + +class SuspiciousAppInfo { + SuspiciousAppInfo({ + required this.packageInfo, + required this.reason, + }); + + PackageInfo packageInfo; + + String reason; + + Object encode() { + return [ + packageInfo, + reason, + ]; + } + + static SuspiciousAppInfo decode(Object result) { + result as List; + return SuspiciousAppInfo( + packageInfo: result[0]! as PackageInfo, + reason: result[1]! as String, + ); + } +} + + +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 PackageInfo) { + buffer.putUint8(129); + writeValue(buffer, value.encode()); + } else if (value is SuspiciousAppInfo) { + buffer.putUint8(130); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 129: + return PackageInfo.decode(readValue(buffer)!); + case 130: + return SuspiciousAppInfo.decode(readValue(buffer)!); + default: + return super.readValueOfType(type, buffer); + } + } +} + +abstract class TalsecPigeonApi { + static const MessageCodec pigeonChannelCodec = _PigeonCodec(); + + void onMalwareDetected(List packageInfo); + + static void setUp(TalsecPigeonApi? api, {BinaryMessenger? binaryMessenger, String messageChannelSuffix = '',}) { + messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + { + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + '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())); + } + }); + } + } + } +} diff --git a/lib/src/models/models.dart b/lib/src/models/models.dart index a24b299..d18e85e 100644 --- a/lib/src/models/models.dart +++ b/lib/src/models/models.dart @@ -1,5 +1,3 @@ export 'android_config.dart'; export 'ios_config.dart'; -export 'package_info.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 deleted file mode 100644 index 665f717..0000000 --- a/lib/src/models/package_info.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:json_annotation/json_annotation.dart'; - -part 'package_info.g.dart'; - -@JsonSerializable() -class PackageInfo { - const PackageInfo({ - required this.packageName, - this.appIcon, - this.version, - this.appName, - this.installationSource, - }); - - final String packageName; - final String? appIcon; - final String? appName; - final String? version; - final String? installationSource; - - factory PackageInfo.fromJson(Map json) => - _$PackageInfoFromJson(json); -} - diff --git a/lib/src/models/package_info.g.dart b/lib/src/models/package_info.g.dart deleted file mode 100644 index d6ed553..0000000 --- a/lib/src/models/package_info.g.dart +++ /dev/null @@ -1,24 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'package_info.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -PackageInfo _$PackageInfoFromJson(Map json) => PackageInfo( - packageName: json['packageName'] as String, - appIcon: json['appIcon'] as String?, - version: json['version'] as String?, - appName: json['appName'] as String?, - installationSource: json['installationSource'] as String?, - ); - -Map _$PackageInfoToJson(PackageInfo instance) => - { - 'packageName': instance.packageName, - 'appIcon': instance.appIcon, - 'appName': instance.appName, - 'version': instance.version, - 'installationSource': instance.installationSource, - }; diff --git a/lib/src/models/suspicious_app_info.dart b/lib/src/models/suspicious_app_info.dart deleted file mode 100644 index 246854b..0000000 --- a/lib/src/models/suspicious_app_info.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:freerasp/src/models/package_info.dart'; -import 'package:json_annotation/json_annotation.dart'; - -part 'suspicious_app_info.g.dart'; - -@JsonSerializable() -class SuspiciousAppInfo { - const SuspiciousAppInfo({ - required this.packageInfo, - required this.reason, - }); - - final PackageInfo packageInfo; - final String reason; - - factory SuspiciousAppInfo.fromJson(Map json) => - _$SuspiciousAppInfoFromJson(json); -} diff --git a/lib/src/models/suspicious_app_info.g.dart b/lib/src/models/suspicious_app_info.g.dart deleted file mode 100644 index 250b832..0000000 --- a/lib/src/models/suspicious_app_info.g.dart +++ /dev/null @@ -1,20 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'suspicious_app_info.dart'; - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -SuspiciousAppInfo _$SuspiciousAppInfoFromJson(Map json) => - SuspiciousAppInfo( - packageInfo: - PackageInfo.fromJson(json['packageInfo'] as Map), - reason: json['reason'] as String, - ); - -Map _$SuspiciousAppInfoToJson(SuspiciousAppInfo instance) => - { - 'packageInfo': instance.packageInfo, - 'reason': instance.reason, - }; diff --git a/lib/src/talsec.dart b/lib/src/talsec.dart index 7fdc906..56ae83c 100644 --- a/lib/src/talsec.dart +++ b/lib/src/talsec.dart @@ -4,6 +4,7 @@ import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:freerasp/freerasp.dart'; +import 'package:freerasp/src/generated/talsec_pigeon_api.g.dart'; /// A class which maintains all security related operations. /// @@ -27,17 +28,7 @@ import 'package:freerasp/freerasp.dart'; class Talsec { /// Private constructor for internal and testing purposes. @visibleForTesting - Talsec.private(this.methodChannel, this.handlerChannel, this.eventChannel) { - handlerChannel.setMethodCallHandler(_methodHandler); - } - - Future _methodHandler(MethodCall call) async { - if (call.method != 'onMalwareDetected') { - return; - } - - print("data"); - } + Talsec.private(this.methodChannel, this.eventChannel); /// Named channel used to communicate with platform plugins. /// @@ -52,12 +43,9 @@ class Talsec { static const MethodChannel _methodChannel = MethodChannel('talsec.app/freerasp/methods'); - static const MethodChannel _handlerChannel = - MethodChannel('talsec.app/freerasp/invoke'); - /// Private [Talsec] variable which holds current instance of class. static final _instance = - Talsec.private(_methodChannel, _handlerChannel, _eventChannel); + Talsec.private(_methodChannel, _eventChannel); /// Initialize Talsec lazily/obtain current instance of Talsec. static Talsec get instance => _instance; @@ -66,10 +54,6 @@ class Talsec { @visibleForTesting late final MethodChannel methodChannel; - /// [MethodChannel] used to invoke native platform. - @visibleForTesting - late final MethodChannel handlerChannel; - /// [EventChannel] used to receive Threats from the native platform. @visibleForTesting late final EventChannel eventChannel; @@ -78,8 +62,6 @@ class Talsec { Stream? _onThreatDetected; - List _suspiciousAppsCache = []; - /// Returns a broadcast stream. When security is compromised /// [onThreatDetected] receives what type of Threat caused it. /// @@ -117,8 +99,6 @@ class Talsec { return _onThreatDetected!; } - ThreatCallback? _callback; - /// Starts freeRASP with configuration provided in [config]. Future start(TalsecConfig config) { _checkConfig(config); @@ -158,48 +138,48 @@ class Talsec { /// When threat is detected, respective callback of [ThreatCallback] is /// invoked. void attachListener(ThreatCallback callback) { + TalsecPigeonApi.setUp(callback); detachListener(); - _callback = callback; _streamSubscription ??= onThreatDetected.listen((event) { switch (event) { case Threat.hooks: - _callback?.onHooks?.call(); + callback.onHooks?.call(); break; case Threat.debug: - _callback?.onDebug?.call(); + callback.onDebug?.call(); break; case Threat.passcode: - _callback?.onPasscode?.call(); + callback.onPasscode?.call(); break; case Threat.deviceId: - _callback?.onDeviceID?.call(); + callback.onDeviceID?.call(); break; case Threat.simulator: - _callback?.onSimulator?.call(); + callback.onSimulator?.call(); break; case Threat.appIntegrity: - _callback?.onAppIntegrity?.call(); + callback.onAppIntegrity?.call(); break; case Threat.obfuscationIssues: - _callback?.onObfuscationIssues?.call(); + callback.onObfuscationIssues?.call(); break; case Threat.deviceBinding: - _callback?.onDeviceBinding?.call(); + callback.onDeviceBinding?.call(); break; case Threat.unofficialStore: - _callback?.onUnofficialStore?.call(); + callback.onUnofficialStore?.call(); break; case Threat.privilegedAccess: - _callback?.onPrivilegedAccess?.call(); + callback.onPrivilegedAccess?.call(); break; case Threat.secureHardwareNotAvailable: - _callback?.onSecureHardwareNotAvailable?.call(); + callback.onSecureHardwareNotAvailable?.call(); break; case Threat.systemVPN: - _callback?.onSystemVPN?.call(); + callback.onSystemVPN?.call(); break; case Threat.devMode: - _callback?.onDevMode?.call(); + callback.onDevMode?.call(); break; } }); @@ -212,7 +192,6 @@ class Talsec { void detachListener() { _streamSubscription?.cancel(); _streamSubscription = null; - _callback = null; } void _handleStreamError(Object error) { diff --git a/lib/src/threat_callback.dart b/lib/src/threat_callback.dart index c4732b5..2417488 100644 --- a/lib/src/threat_callback.dart +++ b/lib/src/threat_callback.dart @@ -1,3 +1,4 @@ +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 @@ -17,7 +18,7 @@ import 'package:freerasp/src/typedefs.dart'; /// // Attaching callback to Talsec /// Talsec.instance.attachListener(callback); /// ``` -class ThreatCallback { +class ThreatCallback extends TalsecPigeonApi { /// Constructs a [ThreatCallback] instance. ThreatCallback({ this.onHooks, @@ -82,6 +83,13 @@ class ThreatCallback { /// This method is called whe the device has Developer mode enabled final VoidCallback? onDevMode; - /// This method is called when malware is detected on the device + @override + void onMalwareDetected(List packageInfo) { + onMalware?.call(packageInfo); + } + + /// This method is called when malware is detected. + /// + /// Android only final MalwareCallback? onMalware; } diff --git a/lib/src/typedefs.dart b/lib/src/typedefs.dart index 085e5f1..0814189 100644 --- a/lib/src/typedefs.dart +++ b/lib/src/typedefs.dart @@ -1,6 +1,7 @@ -import '../freerasp.dart'; +import 'package:freerasp/src/generated/talsec_pigeon_api.g.dart'; /// Typedef for void methods typedef VoidCallback = void Function(); -typedef MalwareCallback = void Function(List); +/// Typedef for malware callback +typedef MalwareCallback = void Function(List packageInfo); diff --git a/pigeons/talsec_pigeon_api.dart b/pigeons/talsec_pigeon_api.dart new file mode 100644 index 0000000..f11e5ca --- /dev/null +++ b/pigeons/talsec_pigeon_api.dart @@ -0,0 +1,44 @@ +import 'package:pigeon/pigeon.dart'; + +// ignore: flutter_style_todos +// TODO: Migrate whole Talsec API to pigeon +@ConfigurePigeon( + PigeonOptions( + dartOut: 'lib/src/generated/talsec_pigeon_api.g.dart', + kotlinOut: + 'android/src/main/kotlin/com/aheaditec/freerasp/generated/TalsecPigeonApi.kt', + input: 'pigeons/talsec_pigeon_api.dart', + kotlinOptions: KotlinOptions(package: 'com.aheaditec.talsec.generated'), + ), +) +class PackageInfo { + const PackageInfo({ + required this.packageName, + this.appIcon, + this.version, + this.appName, + this.installationSource, + }); + + final String packageName; + final String? appIcon; + final String? appName; + final String? version; + final String? installationSource; +} + +class SuspiciousAppInfo { + const SuspiciousAppInfo({ + required this.packageInfo, + required this.reason, + }); + + final PackageInfo packageInfo; + final String reason; +} + +@FlutterApi() +// ignore: one_member_abstracts +abstract class TalsecPigeonApi { + void onMalwareDetected(List packageInfo); +} diff --git a/pubspec.yaml b/pubspec.yaml index 9cd5aa8..b7c90e8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -29,6 +29,7 @@ dev_dependencies: sdk: flutter json_serializable: ^6.0.1 mocktail: ^0.2.0 + pigeon: ^22.4.0 very_good_analysis: ^3.0.0 flutter: diff --git a/test/src/talsec_test.dart b/test/src/talsec_test.dart index 8e9fa8b..7204b8e 100644 --- a/test/src/talsec_test.dart +++ b/test/src/talsec_test.dart @@ -44,7 +44,6 @@ void main() { ); // Act final talsec = Talsec.private( - methodChannel.methodChannel, methodChannel.methodChannel, FakeEventChannel(), ); @@ -71,7 +70,6 @@ void main() { ); // Act final talsec = Talsec.private( - methodChannel.methodChannel, methodChannel.methodChannel, FakeEventChannel(), ); @@ -94,7 +92,6 @@ void main() { // Act final talsec = Talsec.private( - methodChannel.methodChannel, methodChannel.methodChannel, FakeEventChannel(), ); @@ -123,7 +120,6 @@ void main() { // Act final talsec = Talsec.private( - methodChannel.methodChannel, methodChannel.methodChannel, FakeEventChannel(), ); @@ -164,7 +160,6 @@ void main() { ], ); final talsec = Talsec.private( - FakeMethodChannel(), FakeMethodChannel(), eventChannel.eventChannel, ); @@ -190,7 +185,6 @@ void main() { exceptions: [PlatformException(code: 'dummy-code')], ); final talsec = Talsec.private( - FakeMethodChannel(), FakeMethodChannel(), eventChannel.eventChannel, ); @@ -224,7 +218,6 @@ void main() { exceptions: [PlatformException(code: 'dummy-code')], ); final talsec = Talsec.private( - FakeMethodChannel(), FakeMethodChannel(), eventChannel.eventChannel, ); From 4f8f205763baa07ce2132df5a9b4ed29824beeca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Novotn=C3=BD?= Date: Tue, 1 Oct 2024 15:58:28 +0200 Subject: [PATCH 35/66] feat: add whitelist addition --- .../freerasp/handlers/MethodCallHandler.kt | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/handlers/MethodCallHandler.kt b/android/src/main/kotlin/com/aheaditec/freerasp/handlers/MethodCallHandler.kt index e8208ad..0b77cc4 100644 --- a/android/src/main/kotlin/com/aheaditec/freerasp/handlers/MethodCallHandler.kt +++ b/android/src/main/kotlin/com/aheaditec/freerasp/handlers/MethodCallHandler.kt @@ -7,6 +7,7 @@ import com.aheaditec.freerasp.generated.TalsecPigeonApi import com.aheaditec.freerasp.getOrElseThenThrow import com.aheaditec.freerasp.toPigeon import com.aheaditec.talsec_security.security.api.SuspiciousAppInfo +import com.aheaditec.talsec_security.security.api.Talsec import io.flutter.Log import io.flutter.plugin.common.BinaryMessenger import io.flutter.plugin.common.MethodCall @@ -90,6 +91,7 @@ internal class MethodCallHandler : MethodCallHandler { override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { when (call.method) { "start" -> start(call, result) + "addToWhitelist" -> addToWhitelist(call, result) else -> result.notImplemented() } } @@ -110,4 +112,16 @@ internal class MethodCallHandler : MethodCallHandler { result.success(null) } } + + private fun addToWhitelist(call: MethodCall, result: MethodChannel.Result) { + runResultCatching(result) { + val packageName = call.argument("packageName") + context?.let { + if (packageName != null) { + Talsec.addToWhitelist(it, packageName) + } + } ?: throw IllegalStateException("Unable to add package to whitelist - context is null") + result.success(null) + } + } } From 6443d84ab1a95bd021db00f6910313e1f137e09f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Novotn=C3=BD?= Date: Tue, 1 Oct 2024 15:59:07 +0200 Subject: [PATCH 36/66] feat!: raise example sdk version --- example/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 571c350..be069ba 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -4,7 +4,7 @@ description: Demonstrates how to use the freerasp plugin. publish_to: 'none' environment: - sdk: ">=2.12.0 <4.0.0" + sdk: ">=3.0.0 <4.0.0" dependencies: flutter: From 50afd82df40e5cf50724a7287e5b5a20ae8d384c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Novotn=C3=BD?= Date: Tue, 1 Oct 2024 15:59:23 +0200 Subject: [PATCH 37/66] docs: add documentation --- lib/src/models/android_config.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/src/models/android_config.dart b/lib/src/models/android_config.dart index e9662b2..31d456d 100644 --- a/lib/src/models/android_config.dart +++ b/lib/src/models/android_config.dart @@ -35,11 +35,15 @@ class AndroidConfig { /// List of supported sources where application can be installed from. final List supportedStores; + /// List of blocklisted applications with given package name. final List blocklistedPackageNames; + /// List of blocklisted applications with given hash. final List blocklistedHashes; + /// List of blocklisted applications with given permissions. final List> blocklistedPermissions; + /// List of whitelisted installation sources. final List whitelistedInstallationSources; } From 9bfa6dfa1c6219f2644e52274bd3cd54c746b282 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Novotn=C3=BD?= Date: Tue, 1 Oct 2024 15:59:35 +0200 Subject: [PATCH 38/66] docs: adjust exports --- lib/freerasp.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/freerasp.dart b/lib/freerasp.dart index 337b8bd..1862ca1 100644 --- a/lib/freerasp.dart +++ b/lib/freerasp.dart @@ -1,5 +1,6 @@ export 'src/enums/enums.dart'; export 'src/errors/errors.dart'; +export 'src/generated/talsec_pigeon_api.g.dart' show SuspiciousAppInfo, PackageInfo; export 'src/models/models.dart'; export 'src/talsec.dart'; export 'src/threat_callback.dart'; From b07331be9260c08df6106b41053b237f8d3b9ef0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Novotn=C3=BD?= Date: Tue, 1 Oct 2024 15:59:51 +0200 Subject: [PATCH 39/66] feat: update example --- example/lib/extensions.dart | 16 +++ example/lib/main.dart | 197 ++++++++++++++++++++----------- example/lib/safety_icon.dart | 2 +- example/lib/threat_notifier.dart | 53 ++++----- example/lib/threat_state.dart | 27 +++++ lib/src/talsec.dart | 19 ++- 6 files changed, 213 insertions(+), 101 deletions(-) create mode 100644 example/lib/extensions.dart create mode 100644 example/lib/threat_state.dart diff --git a/example/lib/extensions.dart b/example/lib/extensions.dart new file mode 100644 index 0000000..a77d6f4 --- /dev/null +++ b/example/lib/extensions.dart @@ -0,0 +1,16 @@ +/// Extensions for the `String` class. +extension StringX on String { + /// Converts the first character of the string to uppercase. + /// + /// If the string is empty, returns an empty string. + /// + /// If the string has only one character, returns the uppercase version of + /// the character. + /// + /// Otherwise, returns the string with the first character converted to + String toTitleCase() { + if (isEmpty) return ''; + if (length == 1) return toUpperCase(); + return this[0].toUpperCase() + substring(1); + } +} diff --git a/example/lib/main.dart b/example/lib/main.dart index 64be409..ccc0c10 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,113 +1,176 @@ +// ignore_for_file: public_member_api_docs, avoid_redundant_argument_values + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:freerasp/freerasp.dart'; +import 'package:freerasp_example/extensions.dart'; import 'package:freerasp_example/safety_icon.dart'; import 'package:freerasp_example/threat_notifier.dart'; +import 'package:freerasp_example/threat_state.dart'; /// Represents current state of the threats detectable by freeRASP -final threatProvider = StateNotifierProvider>( - (ref) => ThreatNotifier(), -); +final threatProvider = + NotifierProvider.autoDispose(() { + return ThreatNotifier(); +}); -void main() async { - /// Make sure that bindings are initialized before using Talsec. +Future main() async { WidgetsFlutterBinding.ensureInitialized(); + /// Initialize Talsec config + await _initializeTalsec(); + + runApp(const ProviderScope(child: App())); +} + +/// Initialize Talsec configuration for Android and iOS +Future _initializeTalsec() async { final config = TalsecConfig( - /// For Android androidConfig: AndroidConfig( packageName: 'com.aheaditec.freeraspExample', signingCertHashes: ['AKoRuyLMM91E7lX/Zqp3u4jMmd0A7hH/Iqozu0TMVd0='], supportedStores: ['com.sec.android.app.samsungapps'], blocklistedPackageNames: ['com.aheaditec.freeraspExample'], ), - - /// For iOS iosConfig: IOSConfig( bundleIds: ['com.aheaditec.freeraspExample'], teamId: 'M8AK35...', ), watcherMail: 'your_mail@example.com', - // ignore: avoid_redundant_argument_values - isProd: true, // use kReleaseMode for automatic switch + isProd: true, ); - /// freeRASP should be always initialized in the top-level widget await Talsec.instance.start(config); - - /// Another way to handle [Threat] is to use Stream. - /// ```dart - /// final subscription = Talsec.instance.onThreatDetected.listen((threat) { - /// log('Threat detected: $threat'); - /// }); - /// ``` - runApp(const ProviderScope(child: App())); } -/// The example app demonstrating usage of freeRASP +/// The root widget of the application class App extends StatelessWidget { - /// The root widget of the application. - const App({Key? key}) : super(key: key); + const App({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', - theme: ThemeData( - primarySwatch: Colors.blue, - ), - home: Scaffold( - appBar: AppBar( - title: const Text('freeRASP Demo'), - ), - body: const SafeArea( - child: Center( - child: HomePage(), - ), - ), - ), + theme: ThemeData(primarySwatch: Colors.blue), + home: const HomePage(), ); } } -/// Displays the main content of the application. +/// The home page that displays the threats and results class HomePage extends ConsumerWidget { - /// The constructor for the [HomePage] widget. - const HomePage({Key? key}) : super(key: key); + const HomePage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - final threatMap = ref.watch(threatProvider); - return Column( - children: [ - const SizedBox(height: 8), - Text( - 'Test results', - style: Theme.of(context).textTheme.titleMedium, - ), - Expanded( - child: ListView.separated( - padding: const EdgeInsets.all(8), - itemCount: threatMap.length, - itemBuilder: (context, index) { - /// Using Provider to get app state. - final currentThreat = threatMap.keys.elementAt(index); - final isDetected = threatMap[currentThreat]!; - return ListTile( - // ignore: sdk_version_since - title: Text(currentThreat.name), - subtitle: Text(isDetected ? 'Danger' : 'Safe'), - trailing: SafetyIcon(isDetected: isDetected), - ); - }, - separatorBuilder: (_, __) { - return const Divider( - height: 0, - ); - }, + final threatState = ref.watch(threatProvider); + + // Listen for changes in the threatProvider and show the malware modal + ref.listen(threatProvider, (prev, next) { + if (prev?.detectedMalware != next.detectedMalware) { + _showMalwareBottomSheet(context, next.detectedMalware); + } + }); + + return Scaffold( + appBar: AppBar(title: const Text('freeRASP Demo')), + body: SafeArea( + child: Padding( + padding: const EdgeInsets.all(8), + child: Column( + children: [ + Text( + 'Threat Status', + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 8), + Expanded( + child: _ThreatListView(threatState: threatState), + ), + ], ), ), - ], + ), + ); + } +} + +/// ListView displaying all detected threats +class _ThreatListView extends StatelessWidget { + const _ThreatListView({required this.threatState}); + + final ThreatState threatState; + + @override + Widget build(BuildContext context) { + return ListView.separated( + padding: const EdgeInsets.all(8), + itemCount: Threat.values.length, + itemBuilder: (context, index) { + final currentThreat = Threat.values[index]; + final isDetected = threatState.detectedThreats.contains(currentThreat); + + return ListTile( + title: Text(currentThreat.name.toTitleCase()), + subtitle: Text(isDetected ? 'Danger' : 'Safe'), + trailing: SafetyIcon(isDetected: isDetected), + ); + }, + separatorBuilder: (_, __) => const Divider(height: 1), + ); + } +} + +/// Bottom sheet widget that displays malware information +class MalwareBottomSheet extends StatelessWidget { + const MalwareBottomSheet({super.key, required this.suspiciousApps}); + + final List suspiciousApps; + + @override + Widget build(BuildContext context) { + final textTheme = Theme.of(context).textTheme; + + return Container( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Suspicious Apps', style: textTheme.titleMedium), + const SizedBox(height: 8), + ...suspiciousApps.map((malware) { + return ListTile( + title: Text(malware.packageInfo.packageName), + subtitle: Text('Reason: ${malware.reason}'), + leading: const Icon(Icons.warning, color: Colors.red), + ); + }), + const SizedBox(height: 16), + SizedBox( + width: double.infinity, + child: FilledButton( + onPressed: () => Navigator.pop(context), + child: const Text('Dismiss'), + ), + ), + ], + ), ); } } + +/// Extension method to show the malware bottom sheet +void _showMalwareBottomSheet( + BuildContext context, + List suspiciousApps, +) { + WidgetsBinding.instance.addPostFrameCallback((_) { + showModalBottomSheet( + context: context, + isDismissible: false, + enableDrag: false, + builder: (BuildContext context) => + MalwareBottomSheet(suspiciousApps: suspiciousApps), + ); + }); +} diff --git a/example/lib/safety_icon.dart b/example/lib/safety_icon.dart index 478dffc..2e00baf 100644 --- a/example/lib/safety_icon.dart +++ b/example/lib/safety_icon.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; /// Class responsible for changing threat icon and style in the example app class SafetyIcon extends StatelessWidget { /// Represents security state icon in the example app - const SafetyIcon({required this.isDetected, Key? key}) : super(key: key); + const SafetyIcon({required this.isDetected, super.key}); /// Determines whether given threat was detected final bool isDetected; diff --git a/example/lib/threat_notifier.dart b/example/lib/threat_notifier.dart index e68945b..8aa9663 100644 --- a/example/lib/threat_notifier.dart +++ b/example/lib/threat_notifier.dart @@ -1,51 +1,42 @@ -import 'dart:developer'; -import 'dart:io'; - import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:freerasp/freerasp.dart'; +import 'package:freerasp_example/threat_state.dart'; /// Class responsible for setting up listeners to detected threats -class ThreatNotifier extends StateNotifier> { - /// Sets up reactions to detected threats and starts the threat listener - ThreatNotifier() : super(_emptyState()) { - final callback = ThreatCallback( +class ThreatNotifier extends AutoDisposeNotifier { + @override + ThreatState build() { + _init(); + return ThreatState.initial(); + } + + void _init() { + final threatCallback = ThreatCallback( + onMalware: _updateMalware, + onHooks: () => _updateThreat(Threat.hooks), + onDebug: () => _updateThreat(Threat.debug), + onPasscode: () => _updateThreat(Threat.passcode), + onDeviceID: () => _updateThreat(Threat.deviceId), + onSimulator: () => _updateThreat(Threat.simulator), onAppIntegrity: () => _updateThreat(Threat.appIntegrity), onObfuscationIssues: () => _updateThreat(Threat.obfuscationIssues), - onDebug: () => _updateThreat(Threat.debug), onDeviceBinding: () => _updateThreat(Threat.deviceBinding), - onDeviceID: () => _updateThreat(Threat.deviceId), - onHooks: () => _updateThreat(Threat.hooks), - onPasscode: () => _updateThreat(Threat.passcode), + onUnofficialStore: () => _updateThreat(Threat.unofficialStore), onPrivilegedAccess: () => _updateThreat(Threat.privilegedAccess), onSecureHardwareNotAvailable: () => _updateThreat(Threat.secureHardwareNotAvailable), - onSimulator: () => _updateThreat(Threat.simulator), - onUnofficialStore: () => _updateThreat(Threat.unofficialStore), onSystemVPN: () => _updateThreat(Threat.systemVPN), onDevMode: () => _updateThreat(Threat.devMode), - onMalware: (threat) => log('Malware detected: $threat'), ); - Talsec.instance.attachListener(callback); + Talsec.instance.attachListener(threatCallback); } - static Map _emptyState() { - final threatMap = - Threat.values.asMap().map((key, value) => MapEntry(value, false)); - - if (Platform.isAndroid) { - threatMap.remove(Threat.deviceId); - } - - if (Platform.isIOS) { - threatMap.remove(Threat.devMode); - } - - return threatMap; + void _updateThreat(Threat threat) { + state = state.copyWith(detectedThreats: {...state.detectedThreats, threat}); } - void _updateThreat(Threat threat) { - final threatMap = {threat: true}; - state = {...state, ...threatMap}; + void _updateMalware(List malware) { + state = state.copyWith(detectedMalware: malware.nonNulls.toList()); } } diff --git a/example/lib/threat_state.dart b/example/lib/threat_state.dart new file mode 100644 index 0000000..837c2a3 --- /dev/null +++ b/example/lib/threat_state.dart @@ -0,0 +1,27 @@ +// ignore_for_file: public_member_api_docs + +import 'package:freerasp/freerasp.dart'; + +class ThreatState { + factory ThreatState.initial() => + const ThreatState._(detectedThreats: {}, detectedMalware: []); + + const ThreatState._({ + required this.detectedThreats, + required this.detectedMalware, + }); + + final Set detectedThreats; + final List detectedMalware; + + ThreatState copyWith({ + Set? detectedThreats, + List? detectedMalware, + }) { + return ThreatState._( + detectedThreats: detectedThreats ?? this.detectedThreats, + detectedMalware: + detectedMalware?.nonNulls.toList() ?? this.detectedMalware, + ); + } +} diff --git a/lib/src/talsec.dart b/lib/src/talsec.dart index 56ae83c..006f885 100644 --- a/lib/src/talsec.dart +++ b/lib/src/talsec.dart @@ -44,8 +44,7 @@ class Talsec { MethodChannel('talsec.app/freerasp/methods'); /// Private [Talsec] variable which holds current instance of class. - static final _instance = - Talsec.private(_methodChannel, _eventChannel); + static final _instance = Talsec.private(_methodChannel, _eventChannel); /// Initialize Talsec lazily/obtain current instance of Talsec. static Talsec get instance => _instance; @@ -108,6 +107,22 @@ class Talsec { ); } + /// Adds [packageName] to the whitelist. + /// + /// Once added, the package will be excluded from the list of blocklisted + /// packages and won't appear in the list of suspicious applications in + /// the future detections. + /// + /// **Adding package is one-way process** - to remove the package from the + /// whitelist, you need to remove application data or reinstall the + /// application. + Future addToWhitelist(String packageName) { + return methodChannel.invokeMethod( + 'addToWhitelist', + {'packageName': packageName}, + ); + } + void _checkConfig(TalsecConfig config) { // ignore: missing_enum_constant_in_switch switch (defaultTargetPlatform) { From 4137e8bf11ee6e7649d37a0e9a6eb015d1f676e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Novotn=C3=BD?= Date: Tue, 1 Oct 2024 16:00:49 +0200 Subject: [PATCH 40/66] style: formatting --- lib/freerasp.dart | 3 +- lib/src/generated/talsec_pigeon_api.g.dart | 38 ++++++++++++++-------- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/lib/freerasp.dart b/lib/freerasp.dart index 1862ca1..395cf81 100644 --- a/lib/freerasp.dart +++ b/lib/freerasp.dart @@ -1,6 +1,7 @@ export 'src/enums/enums.dart'; export 'src/errors/errors.dart'; -export 'src/generated/talsec_pigeon_api.g.dart' show SuspiciousAppInfo, PackageInfo; +export 'src/generated/talsec_pigeon_api.g.dart' + show SuspiciousAppInfo, PackageInfo; export 'src/models/models.dart'; export 'src/talsec.dart'; export 'src/threat_callback.dart'; diff --git a/lib/src/generated/talsec_pigeon_api.g.dart b/lib/src/generated/talsec_pigeon_api.g.dart index 57d8406..f596898 100644 --- a/lib/src/generated/talsec_pigeon_api.g.dart +++ b/lib/src/generated/talsec_pigeon_api.g.dart @@ -8,7 +8,8 @@ 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}) { +List wrapResponse( + {Object? result, PlatformException? error, bool empty = false}) { if (empty) { return []; } @@ -85,7 +86,6 @@ class SuspiciousAppInfo { } } - class _PigeonCodec extends StandardMessageCodec { const _PigeonCodec(); @override @@ -93,10 +93,10 @@ class _PigeonCodec extends StandardMessageCodec { if (value is int) { buffer.putUint8(4); buffer.putInt64(value); - } else if (value is PackageInfo) { + } else if (value is PackageInfo) { buffer.putUint8(129); writeValue(buffer, value.encode()); - } else if (value is SuspiciousAppInfo) { + } else if (value is SuspiciousAppInfo) { buffer.putUint8(130); writeValue(buffer, value.encode()); } else { @@ -107,9 +107,9 @@ class _PigeonCodec extends StandardMessageCodec { @override Object? readValueOfType(int type, ReadBuffer buffer) { switch (type) { - case 129: + case 129: return PackageInfo.decode(readValue(buffer)!); - case 130: + case 130: return SuspiciousAppInfo.decode(readValue(buffer)!); default: return super.readValueOfType(type, buffer); @@ -122,20 +122,29 @@ abstract class TalsecPigeonApi { void onMalwareDetected(List packageInfo); - static void setUp(TalsecPigeonApi? api, {BinaryMessenger? binaryMessenger, String messageChannelSuffix = '',}) { - messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + static void setUp( + TalsecPigeonApi? api, { + BinaryMessenger? binaryMessenger, + String messageChannelSuffix = '', + }) { + messageChannelSuffix = + messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; { - final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( - 'dev.flutter.pigeon.freerasp.TalsecPigeonApi.onMalwareDetected$messageChannelSuffix', pigeonChannelCodec, + 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.'); + '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(); + 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 { @@ -143,8 +152,9 @@ abstract class TalsecPigeonApi { return wrapResponse(empty: true); } on PlatformException catch (e) { return wrapResponse(error: e); - } catch (e) { - return wrapResponse(error: PlatformException(code: 'error', message: e.toString())); + } catch (e) { + return wrapResponse( + error: PlatformException(code: 'error', message: e.toString())); } }); } From 766d42abcdbbd4f30bb77a8774d26320c0b0c5f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Novotn=C3=BD?= Date: Tue, 1 Oct 2024 16:05:16 +0200 Subject: [PATCH 41/66] chore: raise version + CHANGELOG --- CHANGELOG.md | 18 ++++++++++++++++++ pubspec.yaml | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e23bb09..45e1f92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [6.8.0] - 2024-10-01 +- Android SDK version: 11.1.1 +- iOS SDK version: 6.6.0 + +### Android + +#### Added +- Implement empty callbacks for malware detection + +### Flutter + +#### Added +- Application detection and restriction based on configuration + ## [6.7.1] - 2024-09-30 - Android SDK version: 11.1.1 - iOS SDK version: 6.6.0 @@ -14,6 +28,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 #### Fixed - False positives for hook detection +## [6.8.0] - 2024-10-01 + + + ## [6.7.0] - 2024-09-26 - Android SDK version: 11.1.0 diff --git a/pubspec.yaml b/pubspec.yaml index b7c90e8..3fa0377 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: freerasp description: Flutter library for improving app security and threat monitoring on Android and iOS mobile devices. Learn more about provided features on the freeRASP's homepage first. -version: 6.7.1 +version: 6.8.0 homepage: https://www.talsec.app/freerasp-in-app-protection-security-talsec repository: https://github.com/talsec/Free-RASP-Flutter From 7e1d78040cd2dcc4d0e08177e83cfd7cab2e6757 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Novotn=C3=BD?= Date: Tue, 1 Oct 2024 16:23:53 +0200 Subject: [PATCH 42/66] fix: typo --- CHANGELOG.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 45e1f92..717e993 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,10 +28,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 #### Fixed - False positives for hook detection -## [6.8.0] - 2024-10-01 - - - ## [6.7.0] - 2024-09-26 - Android SDK version: 11.1.0 From f44814df31bc142908e2dafbab486d34ccb4300d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Novotn=C3=BD?= Date: Wed, 2 Oct 2024 09:32:16 +0200 Subject: [PATCH 43/66] fix: failing tests --- test/src/utils/config_verifier_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/src/utils/config_verifier_test.dart b/test/src/utils/config_verifier_test.dart index fb84b34..3d68905 100644 --- a/test/src/utils/config_verifier_test.dart +++ b/test/src/utils/config_verifier_test.dart @@ -76,7 +76,7 @@ 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","signingCertHashes":["AKoRuyLMM91E7lX/Zqp3u4jMmd0A7hH/Iqozu0TMVd0="],"supportedStores":["com.sec.android.app.samsungapps"],"blocklistedPackageNames":[],"blocklistedHashes":[],"blocklistedPermissions":[[]],"whitelistedInstallationSources":[]},"iosConfig":{"bundleIds":["com.aheaditec.freeraspExample"],"teamId":"M8AK35..."},"watcherMail":"test_mail@example.com","isProd":false}'; final config = TalsecConfig( androidConfig: AndroidConfig( packageName: 'com.aheaditec.freeraspExample', From 98da53a0d4e6ffac5b5e0bd565c3b1a091b4563b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Novotn=C3=BD?= Date: Wed, 2 Oct 2024 10:52:36 +0200 Subject: [PATCH 44/66] chore: update example --- example/lib/main.dart | 69 +------------------ example/lib/widgets/malware_bottom_sheet.dart | 42 +++++++++++ example/lib/{ => widgets}/safety_icon.dart | 0 example/lib/widgets/threat_listview.dart | 32 +++++++++ example/lib/widgets/widgets.dart | 3 + lib/src/talsec.dart | 7 +- 6 files changed, 85 insertions(+), 68 deletions(-) create mode 100644 example/lib/widgets/malware_bottom_sheet.dart rename example/lib/{ => widgets}/safety_icon.dart (100%) create mode 100644 example/lib/widgets/threat_listview.dart create mode 100644 example/lib/widgets/widgets.dart diff --git a/example/lib/main.dart b/example/lib/main.dart index ccc0c10..573fc56 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -3,10 +3,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:freerasp/freerasp.dart'; -import 'package:freerasp_example/extensions.dart'; -import 'package:freerasp_example/safety_icon.dart'; import 'package:freerasp_example/threat_notifier.dart'; import 'package:freerasp_example/threat_state.dart'; +import 'package:freerasp_example/widgets/widgets.dart'; /// Represents current state of the threats detectable by freeRASP final threatProvider = @@ -85,7 +84,7 @@ class HomePage extends ConsumerWidget { ), const SizedBox(height: 8), Expanded( - child: _ThreatListView(threatState: threatState), + child: ThreatListView(threats: threatState.detectedThreats), ), ], ), @@ -95,70 +94,6 @@ class HomePage extends ConsumerWidget { } } -/// ListView displaying all detected threats -class _ThreatListView extends StatelessWidget { - const _ThreatListView({required this.threatState}); - - final ThreatState threatState; - - @override - Widget build(BuildContext context) { - return ListView.separated( - padding: const EdgeInsets.all(8), - itemCount: Threat.values.length, - itemBuilder: (context, index) { - final currentThreat = Threat.values[index]; - final isDetected = threatState.detectedThreats.contains(currentThreat); - - return ListTile( - title: Text(currentThreat.name.toTitleCase()), - subtitle: Text(isDetected ? 'Danger' : 'Safe'), - trailing: SafetyIcon(isDetected: isDetected), - ); - }, - separatorBuilder: (_, __) => const Divider(height: 1), - ); - } -} - -/// Bottom sheet widget that displays malware information -class MalwareBottomSheet extends StatelessWidget { - const MalwareBottomSheet({super.key, required this.suspiciousApps}); - - final List suspiciousApps; - - @override - Widget build(BuildContext context) { - final textTheme = Theme.of(context).textTheme; - - return Container( - padding: const EdgeInsets.all(16), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text('Suspicious Apps', style: textTheme.titleMedium), - const SizedBox(height: 8), - ...suspiciousApps.map((malware) { - return ListTile( - title: Text(malware.packageInfo.packageName), - subtitle: Text('Reason: ${malware.reason}'), - leading: const Icon(Icons.warning, color: Colors.red), - ); - }), - const SizedBox(height: 16), - SizedBox( - width: double.infinity, - child: FilledButton( - onPressed: () => Navigator.pop(context), - child: const Text('Dismiss'), - ), - ), - ], - ), - ); - } -} - /// Extension method to show the malware bottom sheet void _showMalwareBottomSheet( BuildContext context, diff --git a/example/lib/widgets/malware_bottom_sheet.dart b/example/lib/widgets/malware_bottom_sheet.dart new file mode 100644 index 0000000..98c299d --- /dev/null +++ b/example/lib/widgets/malware_bottom_sheet.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:freerasp/freerasp.dart'; + +/// Bottom sheet widget that displays malware information +class MalwareBottomSheet extends StatelessWidget { + /// Represents malware information in the example app + const MalwareBottomSheet({super.key, required this.suspiciousApps}); + + /// List of suspicious apps + final List suspiciousApps; + + @override + Widget build(BuildContext context) { + final textTheme = Theme.of(context).textTheme; + + return Container( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Suspicious Apps', style: textTheme.titleMedium), + const SizedBox(height: 8), + ...suspiciousApps.map((malware) { + return ListTile( + title: Text(malware.packageInfo.packageName), + subtitle: Text('Reason: ${malware.reason}'), + leading: const Icon(Icons.warning, color: Colors.red), + ); + }), + const SizedBox(height: 16), + SizedBox( + width: double.infinity, + child: FilledButton( + onPressed: () => Navigator.pop(context), + child: const Text('Dismiss'), + ), + ), + ], + ), + ); + } +} diff --git a/example/lib/safety_icon.dart b/example/lib/widgets/safety_icon.dart similarity index 100% rename from example/lib/safety_icon.dart rename to example/lib/widgets/safety_icon.dart diff --git a/example/lib/widgets/threat_listview.dart b/example/lib/widgets/threat_listview.dart new file mode 100644 index 0000000..5909b1d --- /dev/null +++ b/example/lib/widgets/threat_listview.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'package:freerasp/freerasp.dart'; +import 'package:freerasp_example/extensions.dart'; +import 'package:freerasp_example/widgets/widgets.dart'; + +/// ListView displaying all detected threats +class ThreatListView extends StatelessWidget { + /// Represents a list of detected threats + const ThreatListView({super.key, required this.threats}); + + /// Set of detected threats + final Set threats; + + @override + Widget build(BuildContext context) { + return ListView.separated( + padding: const EdgeInsets.all(8), + itemCount: Threat.values.length, + itemBuilder: (context, index) { + final currentThreat = Threat.values[index]; + final isDetected = threats.contains(currentThreat); + + return ListTile( + title: Text(currentThreat.name.toTitleCase()), + subtitle: Text(isDetected ? 'Danger' : 'Safe'), + trailing: SafetyIcon(isDetected: isDetected), + ); + }, + separatorBuilder: (_, __) => const Divider(height: 1), + ); + } +} diff --git a/example/lib/widgets/widgets.dart b/example/lib/widgets/widgets.dart new file mode 100644 index 0000000..2342ae2 --- /dev/null +++ b/example/lib/widgets/widgets.dart @@ -0,0 +1,3 @@ +export 'malware_bottom_sheet.dart'; +export 'safety_icon.dart'; +export 'threat_listview.dart'; diff --git a/lib/src/talsec.dart b/lib/src/talsec.dart index 006f885..a24c5ae 100644 --- a/lib/src/talsec.dart +++ b/lib/src/talsec.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; @@ -116,7 +117,11 @@ class Talsec { /// **Adding package is one-way process** - to remove the package from the /// whitelist, you need to remove application data or reinstall the /// application. - Future addToWhitelist(String packageName) { + Future addToWhitelist(String packageName) async { + if (!Platform.isAndroid) { + return; + } + return methodChannel.invokeMethod( 'addToWhitelist', {'packageName': packageName}, From 445ce0c65aa88979da261babb904e74e52308807 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Novotn=C3=BD?= Date: Wed, 2 Oct 2024 12:23:22 +0200 Subject: [PATCH 45/66] feat: add pigeon build script --- pigeons/build.sh | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100755 pigeons/build.sh diff --git a/pigeons/build.sh b/pigeons/build.sh new file mode 100755 index 0000000..b06271e --- /dev/null +++ b/pigeons/build.sh @@ -0,0 +1,20 @@ +#!/bin/zsh + +# Exit on error +set -e + +# Variables +INPUT_FILE="talsec_pigeon_api.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 .. +dart run pigeon \ + --input pigeons/$INPUT_FILE From d431054c74f9dbe8da61bcb72c32081c42da5446 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Novotn=C3=BD?= Date: Tue, 15 Oct 2024 09:01:37 +0200 Subject: [PATCH 46/66] fix: CHANGELOG reformat --- CHANGELOG.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 717e993..386baa4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,15 +9,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Android SDK version: 11.1.1 - iOS SDK version: 6.6.0 -### Android +### Flutter #### Added -- Implement empty callbacks for malware detection +- Application detection and restriction based on configuration -### Flutter +### Android #### Added -- Application detection and restriction based on configuration +- New feature: Malware detection ## [6.7.1] - 2024-09-30 - Android SDK version: 11.1.1 From ffc93dd4838564e0a93083c9a38ceff77e48be5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Novotn=C3=BD?= Date: Wed, 16 Oct 2024 09:03:22 +0200 Subject: [PATCH 47/66] style: version break --- analysis_options.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/analysis_options.yaml b/analysis_options.yaml index f74ab60..7dedb6c 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,4 +1,4 @@ -include: package:very_good_analysis/analysis_options.3.1.0.yaml +include: package:very_good_analysis/analysis_options.yaml analyzer: exclude: From f0d9594556724abdaa54b7709041747d3ef00414 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Novotn=C3=BD?= Date: Wed, 16 Oct 2024 09:03:48 +0200 Subject: [PATCH 48/66] feat: raise SDK version --- pubspec.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pubspec.yaml b/pubspec.yaml index 3fa0377..7abe080 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -5,8 +5,8 @@ homepage: https://www.talsec.app/freerasp-in-app-protection-security-talsec repository: https://github.com/talsec/Free-RASP-Flutter environment: - sdk: ">=2.18.0 <4.0.0" - flutter: ">=3.3.0" + sdk: ">=3.0.0 <4.0.0" + flutter: ">=3.10.0" topics: - app-shielding @@ -28,9 +28,9 @@ dev_dependencies: flutter_test: sdk: flutter json_serializable: ^6.0.1 - mocktail: ^0.2.0 + mocktail: ^1.0.4 pigeon: ^22.4.0 - very_good_analysis: ^3.0.0 + very_good_analysis: ^6.0.0 flutter: plugin: From dbd94fe46d4750e94e2af7921e5e64cbced0f951 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Novotn=C3=BD?= Date: Wed, 16 Oct 2024 09:04:20 +0200 Subject: [PATCH 49/66] style: resolve issues --- lib/freerasp.dart | 2 +- lib/src/talsec.dart | 20 +++++--------------- test/test_utils/spy_threat_callback.dart | 13 ------------- 3 files changed, 6 insertions(+), 29 deletions(-) diff --git a/lib/freerasp.dart b/lib/freerasp.dart index 395cf81..711bd6d 100644 --- a/lib/freerasp.dart +++ b/lib/freerasp.dart @@ -1,7 +1,7 @@ export 'src/enums/enums.dart'; export 'src/errors/errors.dart'; export 'src/generated/talsec_pigeon_api.g.dart' - show SuspiciousAppInfo, PackageInfo; + show PackageInfo, SuspiciousAppInfo; export 'src/models/models.dart'; export 'src/talsec.dart'; export 'src/threat_callback.dart'; diff --git a/lib/src/talsec.dart b/lib/src/talsec.dart index a24c5ae..c314a48 100644 --- a/lib/src/talsec.dart +++ b/lib/src/talsec.dart @@ -137,14 +137,17 @@ class Talsec { message: 'Android config is required for Android platform', ); } - break; case TargetPlatform.iOS: if (config.iosConfig == null) { throw const ConfigurationException( message: 'iOS config is required for iOS platform', ); } - break; + case TargetPlatform.fuchsia: + case TargetPlatform.linux: + case TargetPlatform.macOS: + case TargetPlatform.windows: + throw UnimplementedError('Platform is not supported'); } } @@ -164,43 +167,30 @@ class Talsec { switch (event) { case Threat.hooks: callback.onHooks?.call(); - break; case Threat.debug: callback.onDebug?.call(); - break; case Threat.passcode: callback.onPasscode?.call(); - break; case Threat.deviceId: callback.onDeviceID?.call(); - break; case Threat.simulator: callback.onSimulator?.call(); - break; case Threat.appIntegrity: callback.onAppIntegrity?.call(); - break; case Threat.obfuscationIssues: callback.onObfuscationIssues?.call(); - break; case Threat.deviceBinding: callback.onDeviceBinding?.call(); - break; case Threat.unofficialStore: callback.onUnofficialStore?.call(); - break; case Threat.privilegedAccess: callback.onPrivilegedAccess?.call(); - break; case Threat.secureHardwareNotAvailable: callback.onSecureHardwareNotAvailable?.call(); - break; case Threat.systemVPN: callback.onSystemVPN?.call(); - break; case Threat.devMode: callback.onDevMode?.call(); - break; } }); } diff --git a/test/test_utils/spy_threat_callback.dart b/test/test_utils/spy_threat_callback.dart index 38494ff..55ac8f4 100644 --- a/test/test_utils/spy_threat_callback.dart +++ b/test/test_utils/spy_threat_callback.dart @@ -30,43 +30,30 @@ class SpyThreatListener { switch (threat) { case Threat.appIntegrity: callback.onAppIntegrity?.call(); - break; case Threat.obfuscationIssues: callback.onObfuscationIssues?.call(); - break; case Threat.debug: callback.onDebug?.call(); - break; case Threat.deviceBinding: callback.onDeviceBinding?.call(); - break; case Threat.hooks: callback.onHooks?.call(); - break; case Threat.privilegedAccess: callback.onPrivilegedAccess?.call(); - break; case Threat.simulator: callback.onSimulator?.call(); - break; case Threat.unofficialStore: callback.onUnofficialStore?.call(); - break; case Threat.passcode: callback.onPasscode?.call(); - break; case Threat.deviceId: callback.onDeviceID?.call(); - break; case Threat.secureHardwareNotAvailable: callback.onSecureHardwareNotAvailable?.call(); - break; case Threat.systemVPN: callback.onSystemVPN?.call(); - break; case Threat.devMode: callback.onDevMode?.call(); - break; } } } From 9cdde4ca6d1461ecdbc4456c4e259a41b2d4fd0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Novotn=C3=BD?= Date: Wed, 16 Oct 2024 10:19:12 +0200 Subject: [PATCH 50/66] feat: malware sheet scrollable --- example/lib/main.dart | 5 +-- example/lib/widgets/malware_bottom_sheet.dart | 33 +++++++++++++++---- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 573fc56..b137531 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -104,8 +104,9 @@ void _showMalwareBottomSheet( context: context, isDismissible: false, enableDrag: false, - builder: (BuildContext context) => - MalwareBottomSheet(suspiciousApps: suspiciousApps), + builder: (BuildContext context) => MalwareBottomSheet( + suspiciousApps: suspiciousApps, + ), ); }); } diff --git a/example/lib/widgets/malware_bottom_sheet.dart b/example/lib/widgets/malware_bottom_sheet.dart index 98c299d..6edd74a 100644 --- a/example/lib/widgets/malware_bottom_sheet.dart +++ b/example/lib/widgets/malware_bottom_sheet.dart @@ -20,13 +20,14 @@ class MalwareBottomSheet extends StatelessWidget { children: [ Text('Suspicious Apps', style: textTheme.titleMedium), const SizedBox(height: 8), - ...suspiciousApps.map((malware) { - return ListTile( - title: Text(malware.packageInfo.packageName), - subtitle: Text('Reason: ${malware.reason}'), - leading: const Icon(Icons.warning, color: Colors.red), - ); - }), + SizedBox( + height: 240, + child: ListView.builder( + itemCount: suspiciousApps.length, + itemBuilder: (_, index) => + MalwareListTile(malware: suspiciousApps[index]), + ), + ), const SizedBox(height: 16), SizedBox( width: double.infinity, @@ -40,3 +41,21 @@ class MalwareBottomSheet extends StatelessWidget { ); } } + +class MalwareListTile extends StatelessWidget { + const MalwareListTile({ + super.key, + required this.malware, + }); + + final SuspiciousAppInfo malware; + + @override + Widget build(BuildContext context) { + return ListTile( + title: Text(malware.packageInfo.packageName), + subtitle: Text('Reason: ${malware.reason}'), + leading: const Icon(Icons.warning, color: Colors.red), + ); + } +} From e45da011c5e09ffd4cad398364de45a7601662f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Novotn=C3=BD?= Date: Fri, 18 Oct 2024 10:00:17 +0200 Subject: [PATCH 51/66] feat: update parsing --- .../com/aheaditec/freerasp/Extensions.kt | 2 +- .../kotlin/com/aheaditec/freerasp/Utils.kt | 100 +++++++++++------- 2 files changed, 60 insertions(+), 42 deletions(-) diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/Extensions.kt b/android/src/main/kotlin/com/aheaditec/freerasp/Extensions.kt index 0a86ebd..143c150 100644 --- a/android/src/main/kotlin/com/aheaditec/freerasp/Extensions.kt +++ b/android/src/main/kotlin/com/aheaditec/freerasp/Extensions.kt @@ -45,7 +45,7 @@ internal fun SuspiciousAppInfo.toPigeon(context: Context): FlutterSuspiciousAppI private fun PackageInfo.toPigeon(context: Context): FlutterPackageInfo { return FlutterPackageInfo( packageName = packageName, - appName = applicationInfo?.name, + appName = context.packageManager.getApplicationLabel(applicationInfo) as String, version = getVersionString(), appIcon = Utils.parseIconBase64(context, packageName), installationSource = Utils.getInstallerPackageName(context, packageName), diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/Utils.kt b/android/src/main/kotlin/com/aheaditec/freerasp/Utils.kt index 37c97cc..ef8587e 100644 --- a/android/src/main/kotlin/com/aheaditec/freerasp/Utils.kt +++ b/android/src/main/kotlin/com/aheaditec/freerasp/Utils.kt @@ -8,57 +8,62 @@ import android.graphics.drawable.Drawable import android.os.Build import android.util.Base64 import com.aheaditec.talsec_security.security.api.TalsecConfig +import org.json.JSONArray import org.json.JSONException import org.json.JSONObject import java.io.ByteArrayOutputStream internal object Utils { + @Suppress("ArrayInDataClass") + data class MalwareConfig( + val blocklistedPackageNames: Array, + val blocklistedHashes: Array, + val blocklistedPermissions: Array>, + val whitelistedInstallationSources: Array + ) + fun toTalsecConfigThrowing(configJson: String?): TalsecConfig { if (configJson == null) { throw JSONException("Configuration is null") } + val json = JSONObject(configJson) val watcherMail = json.getString("watcherMail") - var isProd = true - if (json.has("isProd")) { - isProd = json.getBoolean("isProd") - } + val isProd = json.getBoolean("isProd") val androidConfig = json.getJSONObject("androidConfig") - val packageName = androidConfig.getString("packageName") val certificateHashes = androidConfig.extractArray("signingCertHashes") val alternativeStores = androidConfig.extractArray("supportedStores") - val blocklistedPackageNames = - androidConfig.extractArray("blocklistedPackageNames") - val blocklistedHashes = androidConfig.extractArray("blocklistedHashes") - val whitelistedInstallationSources = - androidConfig.extractArray("whitelistedInstallationSources") - - val blocklistedPermissions = mutableListOf>() - if (androidConfig.has("blocklistedPermissions")) { - val permissions = androidConfig.getJSONArray("blocklistedPermissions") - for (i in 0 until permissions.length()) { - val permission = permissions.getJSONArray(i) - val permissionList = mutableListOf() - for (j in 0 until permission.length()) { - permissionList.add(permission.getString(j)) - } - blocklistedPermissions.add(permissionList.toTypedArray()) - } - } + val malwareConfig = parseMalwareConfig(androidConfig) return TalsecConfig.Builder(packageName, certificateHashes) .watcherMail(watcherMail) .supportedAlternativeStores(alternativeStores) .prod(isProd) - .blocklistedPackageNames(blocklistedPackageNames) - .blocklistedHashes(blocklistedHashes) - .blocklistedPermissions(blocklistedPermissions.toTypedArray()) - .whitelistedInstallationSources(whitelistedInstallationSources) + .blocklistedPackageNames(malwareConfig.blocklistedPackageNames) + .blocklistedHashes(malwareConfig.blocklistedHashes) + .blocklistedPermissions(malwareConfig.blocklistedPermissions) + .whitelistedInstallationSources(malwareConfig.whitelistedInstallationSources) .build() } + private fun parseMalwareConfig(androidConfig: JSONObject): MalwareConfig { + if (!androidConfig.has("malwareConfig")) { + return MalwareConfig(emptyArray(), emptyArray(), emptyArray(), emptyArray()) + } + + val malwareConfig = androidConfig.getJSONObject("malwareConfig") + + return MalwareConfig( + malwareConfig.extractArray("blocklistedPackageNames"), + malwareConfig.extractArray("blocklistedHashes"), + malwareConfig.extractArray>("blocklistedPermissions"), + malwareConfig.extractArray("whitelistedInstallationSources") + ) + } + + /** * Retrieves the package name of the installer for a given app package. * @@ -134,25 +139,38 @@ internal object Utils { val byteArrayOutputStream = ByteArrayOutputStream() compress(Bitmap.CompressFormat.PNG, 10, byteArrayOutputStream) val byteArray = byteArrayOutputStream.toByteArray() - return Base64.encodeToString(byteArray, Base64.DEFAULT) + return Base64.encodeToString(byteArray, Base64.NO_WRAP) } } -inline fun JSONObject.extractArray(key: String): Array { +private inline fun JSONObject.extractArray(key: String): Array { + return this.optJSONArray(key)?.let { processArray(it) } ?: emptyArray() +} + +private inline fun processArray(jsonArray: JSONArray): Array { val list = mutableListOf() - if (this.has(key)) { - val jsonArray = this.getJSONArray(key) - for (i in 0 until jsonArray.length()) { - val element = when (T::class) { - String::class -> jsonArray.getString(i) as T - Int::class -> jsonArray.getInt(i) as T - Double::class -> jsonArray.getDouble(i) as T - Boolean::class -> jsonArray.getBoolean(i) as T - Long::class -> jsonArray.getLong(i) as T - else -> throw IllegalArgumentException("Unsupported type") + + for (i in 0 until jsonArray.length()) { + val element: T = when (T::class) { + String::class -> jsonArray.getString(i) as T + Int::class -> jsonArray.getInt(i) as T + Double::class -> jsonArray.getDouble(i) as T + Boolean::class -> jsonArray.getBoolean(i) as T + Long::class -> jsonArray.getLong(i) as T + Array::class -> { + // Not universal or ideal solution, but should work for our use case + val nestedArray = jsonArray.getJSONArray(i) + val nestedList = mutableListOf() + for (j in 0 until nestedArray.length()) { + nestedList.add(nestedArray.getString(j)) + } + nestedList.toTypedArray() as T } - list.add(element) + + else -> throw JSONException("Unsupported type") } + list.add(element) } + return list.toTypedArray() -} +} \ No newline at end of file From 7fee0a9ab26f7a4379ab95caccb11efb92386447 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Novotn=C3=BD?= Date: Fri, 18 Oct 2024 10:00:30 +0200 Subject: [PATCH 52/66] feat: update models --- lib/src/models/android_config.dart | 21 ++++----------- lib/src/models/android_config.g.dart | 30 +++++----------------- lib/src/models/malware_config.dart | 34 +++++++++++++++++++++++++ lib/src/models/malware_config.g.dart | 38 ++++++++++++++++++++++++++++ lib/src/models/models.dart | 1 + 5 files changed, 84 insertions(+), 40 deletions(-) create mode 100644 lib/src/models/malware_config.dart create mode 100644 lib/src/models/malware_config.g.dart diff --git a/lib/src/models/android_config.dart b/lib/src/models/android_config.dart index 31d456d..7aba49c 100644 --- a/lib/src/models/android_config.dart +++ b/lib/src/models/android_config.dart @@ -1,3 +1,4 @@ +import 'package:freerasp/freerasp.dart'; import 'package:freerasp/src/utils/utils.dart'; import 'package:json_annotation/json_annotation.dart'; @@ -10,11 +11,8 @@ class AndroidConfig { AndroidConfig({ required this.packageName, required this.signingCertHashes, - this.supportedStores = const [], - this.blocklistedPackageNames = const [], - this.blocklistedHashes = const [], - this.blocklistedPermissions = const >[[]], - this.whitelistedInstallationSources = const [], + this.supportedStores = const [], + this.malwareConfig, }) { ConfigVerifier.verifyAndroid(this); } @@ -35,15 +33,6 @@ class AndroidConfig { /// List of supported sources where application can be installed from. final List supportedStores; - /// List of blocklisted applications with given package name. - final List blocklistedPackageNames; - - /// List of blocklisted applications with given hash. - final List blocklistedHashes; - - /// List of blocklisted applications with given permissions. - final List> blocklistedPermissions; - - /// List of whitelisted installation sources. - final List whitelistedInstallationSources; + /// Malware configuration for Android. + final MalwareConfig? malwareConfig; } diff --git a/lib/src/models/android_config.g.dart b/lib/src/models/android_config.g.dart index d93449d..cc8ebed 100644 --- a/lib/src/models/android_config.g.dart +++ b/lib/src/models/android_config.g.dart @@ -15,26 +15,11 @@ AndroidConfig _$AndroidConfigFromJson(Map json) => supportedStores: (json['supportedStores'] as List?) ?.map((e) => e as String) .toList() ?? - const [], - blocklistedPackageNames: - (json['blocklistedPackageNames'] as List?) - ?.map((e) => e as String) - .toList() ?? - const [], - blocklistedHashes: (json['blocklistedHashes'] as List?) - ?.map((e) => e as String) - .toList() ?? - const [], - blocklistedPermissions: (json['blocklistedPermissions'] as List?) - ?.map( - (e) => (e as List).map((e) => e as String).toList()) - .toList() ?? - const >[[]], - whitelistedInstallationSources: - (json['whitelistedInstallationSources'] as List?) - ?.map((e) => e as String) - .toList() ?? - const [], + const [], + malwareConfig: json['malwareConfig'] == null + ? null + : MalwareConfig.fromJson( + json['malwareConfig'] as Map), ); Map _$AndroidConfigToJson(AndroidConfig instance) => @@ -42,8 +27,5 @@ Map _$AndroidConfigToJson(AndroidConfig instance) => 'packageName': instance.packageName, 'signingCertHashes': instance.signingCertHashes, 'supportedStores': instance.supportedStores, - 'blocklistedPackageNames': instance.blocklistedPackageNames, - 'blocklistedHashes': instance.blocklistedHashes, - 'blocklistedPermissions': instance.blocklistedPermissions, - 'whitelistedInstallationSources': instance.whitelistedInstallationSources, + 'malwareConfig': instance.malwareConfig, }; diff --git a/lib/src/models/malware_config.dart b/lib/src/models/malware_config.dart new file mode 100644 index 0000000..5ad7671 --- /dev/null +++ b/lib/src/models/malware_config.dart @@ -0,0 +1,34 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'malware_config.g.dart'; + +/// Configuration for malware detection. +@JsonSerializable() +class MalwareConfig { + /// Creates a new instance of [MalwareConfig]. + MalwareConfig({ + this.blocklistedPackageNames = const [], + this.blocklistedHashes = const [], + this.blocklistedPermissions = const [], + this.whitelistedInstallationSources = const [], + }); + + /// Converts config from json + factory MalwareConfig.fromJson(Map json) => + _$MalwareConfigFromJson(json); + + /// Converts config to json + Map toJson() => _$MalwareConfigToJson(this); + + /// List of blocklisted applications with given package name. + final List blocklistedPackageNames; + + /// List of blocklisted applications with given hash. + final List blocklistedHashes; + + /// List of blocklisted applications with given permissions. + final List> blocklistedPermissions; + + /// List of whitelisted installation sources. + final List whitelistedInstallationSources; +} diff --git a/lib/src/models/malware_config.g.dart b/lib/src/models/malware_config.g.dart new file mode 100644 index 0000000..85f4fc8 --- /dev/null +++ b/lib/src/models/malware_config.g.dart @@ -0,0 +1,38 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'malware_config.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +MalwareConfig _$MalwareConfigFromJson(Map json) => + MalwareConfig( + blocklistedPackageNames: + (json['blocklistedPackageNames'] as List?) + ?.map((e) => e as String) + .toList() ?? + const [], + blocklistedHashes: (json['blocklistedHashes'] as List?) + ?.map((e) => e as String) + .toList() ?? + const [], + blocklistedPermissions: (json['blocklistedPermissions'] as List?) + ?.map( + (e) => (e as List).map((e) => e as String).toList()) + .toList() ?? + const [], + whitelistedInstallationSources: + (json['whitelistedInstallationSources'] as List?) + ?.map((e) => e as String) + .toList() ?? + const [], + ); + +Map _$MalwareConfigToJson(MalwareConfig instance) => + { + 'blocklistedPackageNames': instance.blocklistedPackageNames, + 'blocklistedHashes': instance.blocklistedHashes, + 'blocklistedPermissions': instance.blocklistedPermissions, + 'whitelistedInstallationSources': instance.whitelistedInstallationSources, + }; diff --git a/lib/src/models/models.dart b/lib/src/models/models.dart index d18e85e..8b19213 100644 --- a/lib/src/models/models.dart +++ b/lib/src/models/models.dart @@ -1,3 +1,4 @@ export 'android_config.dart'; export 'ios_config.dart'; +export 'malware_config.dart'; export 'talsec_config.dart'; From ddb3496f26f0ad67992173fec3d6c5d98207f23d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Novotn=C3=BD?= Date: Fri, 18 Oct 2024 10:00:48 +0200 Subject: [PATCH 53/66] feat: update example app --- example/lib/main.dart | 8 +++++++- example/lib/widgets/malware_bottom_sheet.dart | 9 ++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index b137531..8c70d47 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -29,7 +29,13 @@ Future _initializeTalsec() async { packageName: 'com.aheaditec.freeraspExample', signingCertHashes: ['AKoRuyLMM91E7lX/Zqp3u4jMmd0A7hH/Iqozu0TMVd0='], supportedStores: ['com.sec.android.app.samsungapps'], - blocklistedPackageNames: ['com.aheaditec.freeraspExample'], + malwareConfig: MalwareConfig( + blocklistedPackageNames: ['com.aheaditec.freeraspExample'], + blocklistedPermissions: [ + ['android.permission.CAMERA'], + ['android.permission.READ_SMS', 'android.permission.READ_CONTACTS'] + ], + ), ), iosConfig: IOSConfig( bundleIds: ['com.aheaditec.freeraspExample'], diff --git a/example/lib/widgets/malware_bottom_sheet.dart b/example/lib/widgets/malware_bottom_sheet.dart index 6edd74a..499efc2 100644 --- a/example/lib/widgets/malware_bottom_sheet.dart +++ b/example/lib/widgets/malware_bottom_sheet.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:flutter/material.dart'; import 'package:freerasp/freerasp.dart'; @@ -42,12 +44,15 @@ class MalwareBottomSheet extends StatelessWidget { } } +/// List tile widget that displays malware information class MalwareListTile extends StatelessWidget { + /// Represents malware information in the example app const MalwareListTile({ super.key, required this.malware, }); + /// Malware information final SuspiciousAppInfo malware; @override @@ -55,7 +60,9 @@ class MalwareListTile extends StatelessWidget { return ListTile( title: Text(malware.packageInfo.packageName), subtitle: Text('Reason: ${malware.reason}'), - leading: const Icon(Icons.warning, color: Colors.red), + leading: malware.packageInfo.appIcon == null + ? const Icon(Icons.warning, color: Colors.red) + : Image.memory(base64.decode(malware.packageInfo.appIcon!)), ); } } From 84220bd4381ca7c859709759eb3c7ba136ce9231 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Novotn=C3=BD?= Date: Fri, 18 Oct 2024 10:02:53 +0200 Subject: [PATCH 54/66] style: fix missing comma --- example/lib/main.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 8c70d47..3630a5e 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -33,7 +33,7 @@ Future _initializeTalsec() async { blocklistedPackageNames: ['com.aheaditec.freeraspExample'], blocklistedPermissions: [ ['android.permission.CAMERA'], - ['android.permission.READ_SMS', 'android.permission.READ_CONTACTS'] + ['android.permission.READ_SMS', 'android.permission.READ_CONTACTS'], ], ), ), From 5cff44d0d55b36f5c8efea2dbad06a2420d9dcf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Novotn=C3=BD?= Date: Tue, 22 Oct 2024 09:00:08 +0200 Subject: [PATCH 55/66] fix: update versions to compatible ones --- example/analysis_options.yaml | 2 +- example/pubspec.yaml | 2 +- pubspec.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml index 273cb60..9df80aa 100644 --- a/example/analysis_options.yaml +++ b/example/analysis_options.yaml @@ -1 +1 @@ -include: package:very_good_analysis/analysis_options.3.1.0.yaml +include: package:very_good_analysis/analysis_options.yaml diff --git a/example/pubspec.yaml b/example/pubspec.yaml index be069ba..b3d5fb5 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -22,7 +22,7 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - very_good_analysis: ^3.1.0 + very_good_analysis: ^5.1.0 flutter: diff --git a/pubspec.yaml b/pubspec.yaml index 7abe080..0a71544 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -30,7 +30,7 @@ dev_dependencies: json_serializable: ^6.0.1 mocktail: ^1.0.4 pigeon: ^22.4.0 - very_good_analysis: ^6.0.0 + very_good_analysis: ^5.1.0 flutter: plugin: From cd8471c06c1fd5f2a6da3da1063dce6456b3efb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Novotn=C3=BD?= Date: Tue, 22 Oct 2024 14:04:25 +0200 Subject: [PATCH 56/66] fix: CHANGELOG.md --- CHANGELOG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e734f0..172994c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,18 +6,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [6.8.0] - 2024-10-01 -- Android SDK version: 11.1.1 +- Android SDK version: 11.1.3 - iOS SDK version: 6.6.0 ### Flutter #### Added -- Application detection and restriction based on configuration +- Configuration fields for Malware Detection ### Android #### Added -- New feature: Malware detection +- New feature: Malware Detection ## [6.7.2] - 2024-10-18 From 0685c4bd9c8b6247a83c80899b35d8a8f919ac36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Novotn=C3=BD?= Date: Tue, 22 Oct 2024 14:16:47 +0200 Subject: [PATCH 57/66] fix: extension name --- example/lib/extensions.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/lib/extensions.dart b/example/lib/extensions.dart index a77d6f4..817282b 100644 --- a/example/lib/extensions.dart +++ b/example/lib/extensions.dart @@ -8,7 +8,7 @@ extension StringX on String { /// the character. /// /// Otherwise, returns the string with the first character converted to - String toTitleCase() { + String capitalize() { if (isEmpty) return ''; if (length == 1) return toUpperCase(); return this[0].toUpperCase() + substring(1); From 19698417e0a2ae248b4cd2aa426f59008fe7ff5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Novotn=C3=BD?= Date: Tue, 22 Oct 2024 14:17:01 +0200 Subject: [PATCH 58/66] fix: remove function --- .../com/aheaditec/freerasp/Extensions.kt | 20 ------------------- .../freerasp/handlers/MethodCallHandler.kt | 4 ++-- 2 files changed, 2 insertions(+), 22 deletions(-) diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/Extensions.kt b/android/src/main/kotlin/com/aheaditec/freerasp/Extensions.kt index 143c150..d0e6a5b 100644 --- a/android/src/main/kotlin/com/aheaditec/freerasp/Extensions.kt +++ b/android/src/main/kotlin/com/aheaditec/freerasp/Extensions.kt @@ -67,23 +67,3 @@ internal fun PackageInfo.getVersionString(): String { @Suppress("DEPRECATION") return versionCode.toString() } - -/** - * Returns the encapsulated value if this instance represents success or throws the encapsulated exception - * if it is a failure, executing the given action before throwing. - * - * This function is similar to `Result.getOrThrow()`, but with the added functionality of performing - * an action before throwing the exception. - * - * @param action The action to be executed if the result is a failure. This action should not throw an exception. - * @return The encapsulated value if the result is a success. - * @throws Throwable The encapsulated exception if the result is a failure. - * - * @see Result.getOrThrow - */ -inline fun Result.getOrElseThenThrow(action: () -> Unit): T { - return getOrElse { - action() - throw it - } -} \ No newline at end of file diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/handlers/MethodCallHandler.kt b/android/src/main/kotlin/com/aheaditec/freerasp/handlers/MethodCallHandler.kt index 0b77cc4..3494dc2 100644 --- a/android/src/main/kotlin/com/aheaditec/freerasp/handlers/MethodCallHandler.kt +++ b/android/src/main/kotlin/com/aheaditec/freerasp/handlers/MethodCallHandler.kt @@ -4,7 +4,6 @@ import android.content.Context import com.aheaditec.freerasp.runResultCatching import com.aheaditec.freerasp.Utils import com.aheaditec.freerasp.generated.TalsecPigeonApi -import com.aheaditec.freerasp.getOrElseThenThrow import com.aheaditec.freerasp.toPigeon import com.aheaditec.talsec_security.security.api.SuspiciousAppInfo import com.aheaditec.talsec_security.security.api.Talsec @@ -33,8 +32,9 @@ internal class MethodCallHandler : MethodCallHandler { pigeonApi?.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.getOrElseThenThrow { + result.getOrElse { Log.e("MethodCallHandlerSink", "Result ended with failure") + throw it } } } From f724346e0ebbfba8f6ab776ffc6f544251326be9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Novotn=C3=BD?= Date: Tue, 22 Oct 2024 14:17:12 +0200 Subject: [PATCH 59/66] fix: rename extension name --- example/lib/widgets/threat_listview.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/lib/widgets/threat_listview.dart b/example/lib/widgets/threat_listview.dart index 5909b1d..c723d9a 100644 --- a/example/lib/widgets/threat_listview.dart +++ b/example/lib/widgets/threat_listview.dart @@ -21,7 +21,7 @@ class ThreatListView extends StatelessWidget { final isDetected = threats.contains(currentThreat); return ListTile( - title: Text(currentThreat.name.toTitleCase()), + title: Text(currentThreat.name.capitalize()), subtitle: Text(isDetected ? 'Danger' : 'Safe'), trailing: SafetyIcon(isDetected: isDetected), ); From 706eb2197d911f90ad7ea43afc71b9996c856d90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Novotn=C3=BD?= Date: Tue, 22 Oct 2024 14:17:20 +0200 Subject: [PATCH 60/66] fix: version number --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 0a71544..ed23260 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -29,7 +29,7 @@ dev_dependencies: sdk: flutter json_serializable: ^6.0.1 mocktail: ^1.0.4 - pigeon: ^22.4.0 + pigeon: ^17.0.0 very_good_analysis: ^5.1.0 flutter: From f5f19808c0719261b049b3a1252c29095518c4d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Novotn=C3=BD?= Date: Tue, 22 Oct 2024 14:18:20 +0200 Subject: [PATCH 61/66] fix: missing docs --- example/lib/extensions.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/example/lib/extensions.dart b/example/lib/extensions.dart index 817282b..0636a49 100644 --- a/example/lib/extensions.dart +++ b/example/lib/extensions.dart @@ -8,6 +8,7 @@ extension StringX on String { /// the character. /// /// Otherwise, returns the string with the first character converted to + /// uppercase. String capitalize() { if (isEmpty) return ''; if (length == 1) return toUpperCase(); From 0b677f362ac45901bd350a7f4d319039a058b703 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Novotn=C3=BD?= Date: Tue, 22 Oct 2024 14:31:33 +0200 Subject: [PATCH 62/66] fix: style --- example/lib/widgets/malware_bottom_sheet.dart | 7 +++++-- example/lib/widgets/threat_listview.dart | 5 ++++- lib/src/talsec.dart | 4 +++- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/example/lib/widgets/malware_bottom_sheet.dart b/example/lib/widgets/malware_bottom_sheet.dart index 499efc2..a5ecff8 100644 --- a/example/lib/widgets/malware_bottom_sheet.dart +++ b/example/lib/widgets/malware_bottom_sheet.dart @@ -6,7 +6,10 @@ import 'package:freerasp/freerasp.dart'; /// Bottom sheet widget that displays malware information class MalwareBottomSheet extends StatelessWidget { /// Represents malware information in the example app - const MalwareBottomSheet({super.key, required this.suspiciousApps}); + const MalwareBottomSheet({ + required this.suspiciousApps, + super.key, + }); /// List of suspicious apps final List suspiciousApps; @@ -48,8 +51,8 @@ class MalwareBottomSheet extends StatelessWidget { class MalwareListTile extends StatelessWidget { /// Represents malware information in the example app const MalwareListTile({ - super.key, required this.malware, + super.key, }); /// Malware information diff --git a/example/lib/widgets/threat_listview.dart b/example/lib/widgets/threat_listview.dart index c723d9a..2342436 100644 --- a/example/lib/widgets/threat_listview.dart +++ b/example/lib/widgets/threat_listview.dart @@ -6,7 +6,10 @@ import 'package:freerasp_example/widgets/widgets.dart'; /// ListView displaying all detected threats class ThreatListView extends StatelessWidget { /// Represents a list of detected threats - const ThreatListView({super.key, required this.threats}); + const ThreatListView({ + required this.threats, + super.key, + }); /// Set of detected threats final Set threats; diff --git a/lib/src/talsec.dart b/lib/src/talsec.dart index c314a48..83ec38d 100644 --- a/lib/src/talsec.dart +++ b/lib/src/talsec.dart @@ -119,7 +119,9 @@ class Talsec { /// application. Future addToWhitelist(String packageName) async { if (!Platform.isAndroid) { - return; + throw UnimplementedError( + 'Platform is not supported: $defaultTargetPlatform}', + ); } return methodChannel.invokeMethod( From 85497843464974a4e9df5aded3f8987877ac36ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Novotn=C3=BD?= Date: Tue, 22 Oct 2024 14:33:02 +0200 Subject: [PATCH 63/66] fix: unused import --- .../kotlin/com/aheaditec/freerasp/generated/TalsecPigeonApi.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/android/src/main/kotlin/com/aheaditec/freerasp/generated/TalsecPigeonApi.kt b/android/src/main/kotlin/com/aheaditec/freerasp/generated/TalsecPigeonApi.kt index e75fd00..41f6862 100644 --- a/android/src/main/kotlin/com/aheaditec/freerasp/generated/TalsecPigeonApi.kt +++ b/android/src/main/kotlin/com/aheaditec/freerasp/generated/TalsecPigeonApi.kt @@ -4,7 +4,6 @@ package com.aheaditec.freerasp.generated -import android.util.Log import io.flutter.plugin.common.BasicMessageChannel import io.flutter.plugin.common.BinaryMessenger import io.flutter.plugin.common.MessageCodec From fdfb19349b50fe33738f6095429f0f6a66640fd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Novotn=C3=BD?= Date: Wed, 23 Oct 2024 13:50:42 +0200 Subject: [PATCH 64/66] build: update dependencies --- pubspec.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pubspec.yaml b/pubspec.yaml index ed23260..0844619 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -20,14 +20,14 @@ dependencies: flutter: sdk: flutter flutter_plugin_android_lifecycle: ^2.0.11 - json_annotation: ^4.0.1 + json_annotation: ^4.9.0 meta: ^1.7.0 dev_dependencies: - build_runner: ^2.1.5 + build_runner: ^2.4.9 flutter_test: sdk: flutter - json_serializable: ^6.0.1 + json_serializable: ^6.8.0 mocktail: ^1.0.4 pigeon: ^17.0.0 very_good_analysis: ^5.1.0 From 05f60fcf59001fc8d1a9992a7e05cb9574b55280 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Novotn=C3=BD?= Date: Wed, 23 Oct 2024 13:50:53 +0200 Subject: [PATCH 65/66] chore: update tests --- test/src/utils/config_verifier_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/src/utils/config_verifier_test.dart b/test/src/utils/config_verifier_test.dart index 3d68905..fb84b34 100644 --- a/test/src/utils/config_verifier_test.dart +++ b/test/src/utils/config_verifier_test.dart @@ -76,7 +76,7 @@ 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"],"blocklistedPackageNames":[],"blocklistedHashes":[],"blocklistedPermissions":[[]],"whitelistedInstallationSources":[]},"iosConfig":{"bundleIds":["com.aheaditec.freeraspExample"],"teamId":"M8AK35..."},"watcherMail":"test_mail@example.com","isProd":false}'; + '{"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}'; final config = TalsecConfig( androidConfig: AndroidConfig( packageName: 'com.aheaditec.freeraspExample', From 2254ab2885974d43618004596f841af6ada4a430 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaroslav=20Novotn=C3=BD?= Date: Wed, 23 Oct 2024 13:51:20 +0200 Subject: [PATCH 66/66] feat: exclude null properties from json --- lib/src/models/android_config.dart | 2 +- lib/src/models/android_config.g.dart | 23 ++++++++++++++++------- lib/src/models/talsec_config.dart | 2 +- lib/src/models/talsec_config.g.dart | 22 +++++++++++++++------- 4 files changed, 33 insertions(+), 16 deletions(-) diff --git a/lib/src/models/android_config.dart b/lib/src/models/android_config.dart index 7aba49c..a613445 100644 --- a/lib/src/models/android_config.dart +++ b/lib/src/models/android_config.dart @@ -5,7 +5,7 @@ import 'package:json_annotation/json_annotation.dart'; part 'android_config.g.dart'; /// Class responsible for freeRASP Android configuration -@JsonSerializable() +@JsonSerializable(includeIfNull: false) class AndroidConfig { /// Android configuration fields AndroidConfig({ diff --git a/lib/src/models/android_config.g.dart b/lib/src/models/android_config.g.dart index cc8ebed..ed4dc50 100644 --- a/lib/src/models/android_config.g.dart +++ b/lib/src/models/android_config.g.dart @@ -22,10 +22,19 @@ AndroidConfig _$AndroidConfigFromJson(Map json) => json['malwareConfig'] as Map), ); -Map _$AndroidConfigToJson(AndroidConfig instance) => - { - 'packageName': instance.packageName, - 'signingCertHashes': instance.signingCertHashes, - 'supportedStores': instance.supportedStores, - 'malwareConfig': instance.malwareConfig, - }; +Map _$AndroidConfigToJson(AndroidConfig instance) { + final val = { + 'packageName': instance.packageName, + 'signingCertHashes': instance.signingCertHashes, + 'supportedStores': instance.supportedStores, + }; + + void writeNotNull(String key, dynamic value) { + if (value != null) { + val[key] = value; + } + } + + writeNotNull('malwareConfig', instance.malwareConfig); + return val; +} diff --git a/lib/src/models/talsec_config.dart b/lib/src/models/talsec_config.dart index ff1d2d0..c41560e 100644 --- a/lib/src/models/talsec_config.dart +++ b/lib/src/models/talsec_config.dart @@ -4,7 +4,7 @@ import 'package:json_annotation/json_annotation.dart'; part 'talsec_config.g.dart'; /// Class responsible for freeRASP configuration -@JsonSerializable(explicitToJson: true) +@JsonSerializable(explicitToJson: true, includeIfNull: false) class TalsecConfig { /// Configuration for [TalsecConfig]. TalsecConfig({ diff --git a/lib/src/models/talsec_config.g.dart b/lib/src/models/talsec_config.g.dart index 41aaa56..abbdfe3 100644 --- a/lib/src/models/talsec_config.g.dart +++ b/lib/src/models/talsec_config.g.dart @@ -18,10 +18,18 @@ TalsecConfig _$TalsecConfigFromJson(Map json) => TalsecConfig( : IOSConfig.fromJson(json['iosConfig'] as Map), ); -Map _$TalsecConfigToJson(TalsecConfig instance) => - { - 'androidConfig': instance.androidConfig?.toJson(), - 'iosConfig': instance.iosConfig?.toJson(), - 'watcherMail': instance.watcherMail, - 'isProd': instance.isProd, - }; +Map _$TalsecConfigToJson(TalsecConfig instance) { + final val = {}; + + void writeNotNull(String key, dynamic value) { + if (value != null) { + val[key] = value; + } + } + + writeNotNull('androidConfig', instance.androidConfig?.toJson()); + writeNotNull('iosConfig', instance.iosConfig?.toJson()); + val['watcherMail'] = instance.watcherMail; + val['isProd'] = instance.isProd; + return val; +}