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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .pubignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,5 @@ example/*
example/lib/*
!example/lib/main.dart
test/
pigeons/
!**/TalsecRuntime.xcframework/
!**/TalsecRuntime.xcframework/**
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `MalwareCallback` now receives a `List<SuspiciousAppInfo>` without nullable items, and its parameter is renamed from `packageInfo` to `suspiciousApps`
- The `Talsec.private` constructor and the `methodChannel`, `eventChannel` and `executionStateChannel` fields are no longer public; use `Talsec.instance`
- `ThreatX.fromInt` throws an `ArgumentError` for an unknown code instead of terminating the app. The app is still terminated when the native side sends an unknown threat code, and now also for an unknown execution state code, like in the other freeRASP plugins
- `ThreatCallback` no longer extends the generated `TalsecPigeonApi`; its `onMalwareDetected` and `onOngoingCallDetected` methods are removed, use the `onMalware` and `onOngoingCall` callbacks instead
- `PackageInfo` and `SuspiciousAppInfo` are now immutable classes with value equality; their fields are final and the `encode`/`decode` methods are removed

#### Added

Expand All @@ -36,6 +38,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Android package (namespace) of the plugin renamed from `com.aheaditec.freerasp` to `app.talsec.rasp.flutter`, following the Android SDK's `app.talsec.rasp`
- On Android, `Talsec.isScreenCaptureBlocked()` reports the protection state of the current activity
- On Android, freeRASP now starts in background mode (`TalsecMode.BACKGROUND`), so the initialization and synchronous checks no longer run on the main thread
- Malware and ongoing call detections are delivered over the same channel as the other threats, so `ThreatCallback` receives all detections in the order in which they occur. The plugin no longer uses Pigeon

### Android

Expand Down
48 changes: 20 additions & 28 deletions android/src/main/kotlin/app/talsec/rasp/flutter/Extensions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,6 @@ import io.flutter.plugin.common.MethodChannel
import org.json.JSONArray
import org.json.JSONObject
import app.talsec.rasp.security.api.ThreatListener.CallState
import app.talsec.rasp.flutter.generated.CallState as FlutterCallState
import app.talsec.rasp.flutter.generated.PackageInfo as FlutterPackageInfo
import app.talsec.rasp.flutter.generated.SuspiciousAppInfo as FlutterSuspiciousAppInfo

/**
* Executes the provided block of code and catches any exceptions thrown by it, returning the
Expand All @@ -34,31 +31,27 @@ internal inline fun runResultCatching(result: MethodChannel.Result, block: () ->
}

/**
* Converts a [SuspiciousAppInfo] instance to a [app.talsec.rasp.flutter.generated.SuspiciousAppInfo]
* instance used by Pigeon package for Flutter.
*
* @return A new [app.talsec.rasp.flutter.generated.SuspiciousAppInfo] object with information from
* this [SuspiciousAppInfo].
* Converts a [SuspiciousAppInfo] to the map that the Dart `SuspiciousAppInfo.fromMap` reads.
*/
internal fun SuspiciousAppInfo.toPigeon(context: Context): FlutterSuspiciousAppInfo {
return FlutterSuspiciousAppInfo(this.packageInfo.toPigeon(context), this.reasons.toList(), this.permissions?.toList())
internal fun SuspiciousAppInfo.toFlutterMap(context: Context): Map<String, Any?> {
return mapOf(
"packageInfo" to packageInfo.toFlutterMap(context),
"reasons" to reasons.toList(),
"permissions" to permissions?.toList(),
)
}

/**
* Converts a [PackageInfo] instance to a [app.talsec.rasp.flutter.generated.PackageInfo] instance
* used by Pigeon package for Flutter.
*
* @return A new [app.talsec.rasp.flutter.generated.PackageInfo] object with information from
* this [PackageInfo].
* Converts a [PackageInfo] to the map that the Dart `PackageInfo.fromMap` reads.
*/
private fun PackageInfo.toPigeon(context: Context): FlutterPackageInfo {
return FlutterPackageInfo(
packageName = packageName,
appName = applicationInfo?.let {
context.packageManager.getApplicationLabel(it) as String
private fun PackageInfo.toFlutterMap(context: Context): Map<String, Any?> {
return mapOf(
"packageName" to packageName,
"appName" to applicationInfo?.let {
context.packageManager.getApplicationLabel(it).toString()
},
version = getVersionString(),
installerStore = Utils.getInstallerPackageName(context, packageName),
"version" to getVersionString(),
"installerStore" to Utils.getInstallerPackageName(context, packageName),
)
}

Expand Down Expand Up @@ -121,13 +114,12 @@ internal fun JSONObject.toSuspiciousAppDetectionConfig(): SuspiciousAppDetection
}

/**
* Converts a [CallState] instance to a [app.talsec.rasp.flutter.generated.CallState] instance
* used by Pigeon package for Flutter.
* Converts a [CallState] to the name of the Dart `CallState` value.
*/
internal fun CallState.toPigeon(): FlutterCallState {
internal fun CallState.toFlutterName(): String {
return when (this) {
CallState.STARTED -> FlutterCallState.STARTED
CallState.ONGOING -> FlutterCallState.ONGOING
CallState.STOPPED -> FlutterCallState.STOPPED
CallState.STARTED -> "started"
CallState.ONGOING -> "ongoing"
CallState.STOPPED -> "stopped"
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
package app.talsec.rasp.flutter.dispatchers

import app.talsec.rasp.flutter.Threat
import app.talsec.rasp.flutter.handlers.MethodCallHandler
import app.talsec.rasp.security.api.SuspiciousAppInfo
import app.talsec.rasp.flutter.toFlutterName
import app.talsec.rasp.security.api.ThreatListener.CallState
import io.flutter.plugin.common.EventChannel.EventSink

/**
* Sends detections to Flutter over the threat [EventSink], in the order in which they occur.
*
* A plain threat is sent as its [Int] code. A detection with a payload is sent as a map with a
* `type` key: `malware` (with `suspiciousApps`) or `ongoingCall` (with `state`). Detections that
* occur while the app is in background or no listener is attached are cached and sent later.
*/
internal object ThreatDispatcher {
private val threatCache = mutableSetOf<Threat>()
private val malwareCache = mutableListOf<SuspiciousAppInfo>()
private val malwareCache = mutableListOf<Map<String, Any?>>()
private val callStateCache = mutableListOf<CallState>()
private var isAppInForeground = false

Expand All @@ -17,28 +23,14 @@ internal object ThreatDispatcher {
field = value
if (value != null) {
isAppInForeground = true
flushThreatCache()
}
}

var methodSink: MethodCallHandler.MethodSink? = null
set(value) {
field = value
if (value != null) {
isAppInForeground = true
flushMalwareCache()
flushCallStateCache()
flushCache()
}
}

fun onResume() {
isAppInForeground = true
if (eventSink != null) {
flushThreatCache()
}
if (methodSink != null) {
flushMalwareCache()
flushCallStateCache()
flushCache()
}
}

Expand All @@ -56,9 +48,12 @@ internal object ThreatDispatcher {
}
}

fun dispatchMalware(apps: List<SuspiciousAppInfo>) {
if (isAppInForeground && methodSink != null) {
methodSink?.onMalwareDetected(apps)
/**
* @param apps The suspicious apps, already converted with `toFlutterMap`.
*/
fun dispatchMalware(apps: List<Map<String, Any?>>) {
if (isAppInForeground && eventSink != null) {
eventSink?.success(malwareEvent(apps))
} else {
synchronized(malwareCache) {
malwareCache.addAll(apps)
Expand All @@ -67,8 +62,8 @@ internal object ThreatDispatcher {
}

fun dispatchOngoingCall(state: CallState) {
if (isAppInForeground && methodSink != null) {
methodSink?.onOngoingCall(state)
if (isAppInForeground && eventSink != null) {
eventSink?.success(ongoingCallEvent(state))
} else {
// Keep the order: a call that started and stopped in background is reported as both.
synchronized(callStateCache) {
Expand All @@ -77,6 +72,12 @@ internal object ThreatDispatcher {
}
}

private fun flushCache() {
flushThreatCache()
flushMalwareCache()
flushCallStateCache()
}

private fun flushThreatCache() {
val threats = synchronized(threatCache) {
val snapshot = threatCache.toSet()
Expand All @@ -88,12 +89,12 @@ internal object ThreatDispatcher {

private fun flushMalwareCache() {
val malware = synchronized(malwareCache) {
val snapshot = malwareCache.toMutableList()
val snapshot = malwareCache.toList()
malwareCache.clear()
snapshot
}
if (malware.isNotEmpty()) {
methodSink?.onMalwareDetected(malware)
eventSink?.success(malwareEvent(malware))
}
}

Expand All @@ -103,6 +104,12 @@ internal object ThreatDispatcher {
callStateCache.clear()
snapshot
}
states.forEach { methodSink?.onOngoingCall(it) }
states.forEach { eventSink?.success(ongoingCallEvent(it)) }
}

private fun malwareEvent(apps: List<Map<String, Any?>>): Map<String, Any?> =
mapOf("type" to "malware", "suspiciousApps" to apps)

private fun ongoingCallEvent(state: CallState): Map<String, Any?> =
mapOf("type" to "ongoingCall", "state" to state.toFlutterName())
}
Loading
Loading