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/**
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
#### Breaking

- `RaspExecutionStateCallback.onAllChecksFinished` renamed to `onFastChecksFinished` and `RaspExecutionState.allChecksFinished` renamed to `fastChecksFinished`. On Android, it is triggered when the fast checks are completed, while the postponed checks may still run; on iOS, when all checks are completed
- `AndroidConfig.signingCertHashes` renamed to `certificateHashes` and `AndroidConfig.supportedStores` renamed to `supportedAlternativeStores`, matching the Android SDK and the other freeRASP plugins
- `IOSConfig.bundleIds` renamed to `appBundleIds` and `IOSConfig.teamId` renamed to `appTeamId`, matching the iOS SDK and the other freeRASP plugins. The JSON keys used by `toJson`/`fromJson` change accordingly
- `ThreatCallback.onADBEnabled` renamed to `onAdbEnabled` and `ThreatCallback.onUnsecureWiFi` renamed to `onUnsecureWifi`; `Threat.deviceId` renamed to `deviceID` and `Threat.unsecureWiFi` renamed to `unsecureWifi`, matching the other freeRASP plugins
- All `Talsec` methods now throw only `TalsecException` or its subclasses: Android-only methods throw the new `UnsupportedPlatformException` (code `unsupported-platform`) instead of `UnimplementedError`, `blockScreenCapture`/`isScreenCaptureBlocked` no longer throw a generic `Exception`, `start` wraps platform errors in `TalsecException`, and `addToWhitelist`/`getAppIcon` throw `MalwareFailureException`
- `MalwareCallback` now receives a `List<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

- `ThreatCallback.onOngoingCall` callback with the new `CallState` enum (`started`, `ongoing`, `stopped`), driven by `Talsec.startOngoingCallDetection()` and `Talsec.stopOngoingCallDetection()` (Android only)
- `Talsec.blockScreenControl()` and `Talsec.isScreenControlBlocked()` to prevent accessibility services from reading or controlling the app screen (Android only)
- `Talsec.blockOverlayWindows()` and `Talsec.isOverlayWindowsBlocked()` to hide overlay windows of other apps (Android 12+ only, requires the `HIDE_OVERLAY_WINDOWS` permission)
- `RaspExecutionStateCallback.onCriticalChecksFinished` and `RaspExecutionStateCallback.onPostponedChecksFinished`, with matching `RaspExecutionState` values (Android only)
- `ExternalIdFailureException` and `MalwareFailureException` are now exported, so they can be caught by type

#### Changed

- Android package (namespace) of the plugin renamed from `com.aheaditec.freerasp` to `app.talsec.rasp.flutter`, following the Android SDK's `app.talsec.rasp`
- On Android, `Talsec.isScreenCaptureBlocked()` reports the protection state of the current activity
- On Android, freeRASP now starts in background mode (`TalsecMode.BACKGROUND`), so the initialization and synchronous checks no longer run on the main thread
- Malware and ongoing call detections are delivered over the same channel as the other threats, so `ThreatCallback` receives all detections in the order in which they occur. The plugin no longer uses Pigeon

### Android

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"
}
}
4 changes: 2 additions & 2 deletions android/src/main/kotlin/app/talsec/rasp/flutter/Utils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ internal object Utils {
val killOnBypass = json.optBoolean("killOnBypass")
val androidConfig = json.getJSONObject("androidConfig")
val packageName = androidConfig.getString("packageName")
val certificateHashes = androidConfig.extractArray<String>("signingCertHashes")
val alternativeStores = androidConfig.extractArray<String>("supportedStores")
val certificateHashes = androidConfig.extractArray<String>("certificateHashes")
val alternativeStores = androidConfig.extractArray<String>("supportedAlternativeStores")

val builder = TalsecConfig.Builder(packageName, certificateHashes)
.watcherMail(watcherMail)
Expand Down
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