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/**
55 changes: 54 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,60 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
## [9.0.0] - 2026-11-11

- Android SDK version: 20.0.1
- iOS SDK version: 8.0.1

### Flutter

#### Breaking

- `RaspExecutionStateCallback.onAllChecksFinished` renamed to `onFastChecksFinished` and `RaspExecutionState.allChecksFinished` renamed to `fastChecksFinished`. On Android, it is triggered when the fast checks are completed, while the postponed checks may still run; on iOS, when all checks are completed
- `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

#### Fixed

- Improved hook detection to prevent collisions with running VPN services and to better work with the device resources (threads/RAM)
- Fixed a release crash during root detection when a checked package is not installed
- Fixed a crash in `ScreenProtector.unregisterScreenCallbacks` on Android 15+ when several activities registered screen callbacks
- Fixed intermittent build failures of apps using the SDK (`NullPointerException: Cannot read field "d"`, shown by Gradle as `[CIRCULAR REFERENCE]`) caused by invalid bytecode in some SDK builds
- Fixed `killOnBypass` closing the app when the same threat was reported twice in quick succession

#### Changed

- Deprecated the freeRASP Maven coordinates (`com.aheaditec.talsec.security:TalsecSecurity-{flavor}`)
- Improved Bootloader detection
- Extended emulator detection with additional device-property and ABI checks
- Improved logging structure and data enrichment for better Portal insights/reports
- Refactoring of the time spoofing detection which improves the performance of the freeRASP check execution
- Improved the performance of the App integrity check
- Updated CA bundle

### iOS

Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ Key features are the detection and prevention of
* Unlocked or compromised bootloader
* Hooking framework (e.g., Frida, LSPosed, Shadow)
* UI automation and driven-input attacks (e.g., Appium)
* Ongoing cellular and VoIP calls during sensitive flows (Android)
* Screen control by accessibility services and overlay windows of other apps (Android)
* Untrusted installation method
* App Data Migration protection

Expand Down
6 changes: 3 additions & 3 deletions android/build.gradle
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
group 'com.aheaditec.freerasp'
group 'app.talsec.rasp.flutter'
version '1.0-SNAPSHOT'

buildscript {
ext.kotlin_version = '2.1.0'
ext.talsec_version = '19.3.1'
ext.talsec_version = '20.0.1'
repositories {
google()
mavenCentral()
Expand Down Expand Up @@ -32,7 +32,7 @@ apply plugin: 'app.talsec.plugin'

android {
if (project.android.hasProperty("namespace")) {
namespace("com.aheaditec.freerasp")
namespace("app.talsec.rasp.flutter")
}

compileSdk 36
Expand Down
2 changes: 1 addition & 1 deletion android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.aheaditec.freerasp">
package="app.talsec.rasp.flutter">
</manifest>
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.aheaditec.freerasp
package app.talsec.rasp.flutter

import android.content.Context
import android.content.pm.PackageInfo
Expand All @@ -12,8 +12,7 @@ import app.talsec.rasp.security.api.SuspiciousAppInfo
import io.flutter.plugin.common.MethodChannel
import org.json.JSONArray
import org.json.JSONObject
import com.aheaditec.freerasp.generated.PackageInfo as FlutterPackageInfo
import com.aheaditec.freerasp.generated.SuspiciousAppInfo as FlutterSuspiciousAppInfo
import app.talsec.rasp.security.api.ThreatListener.CallState

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

/**
* 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].
* 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 [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].
* 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 @@ -117,3 +112,14 @@ internal fun JSONObject.toSuspiciousAppDetectionConfig(): SuspiciousAppDetection
reasonMode,
)
}

/**
* Converts a [CallState] to the name of the Dart `CallState` value.
*/
internal fun CallState.toFlutterName(): String {
return when (this) {
CallState.STARTED -> "started"
CallState.ONGOING -> "ongoing"
CallState.STOPPED -> "stopped"
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
package com.aheaditec.freerasp
package app.talsec.rasp.flutter

import android.app.Activity
import android.content.Context
import android.os.Build
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.LifecycleOwner
import com.aheaditec.freerasp.handlers.ExecutionStateStreamHandler
import com.aheaditec.freerasp.handlers.MethodCallHandler
import com.aheaditec.freerasp.handlers.StreamHandler
import com.aheaditec.freerasp.handlers.TalsecThreatHandler
import app.talsec.rasp.flutter.handlers.ExecutionStateStreamHandler
import app.talsec.rasp.flutter.handlers.MethodCallHandler
import app.talsec.rasp.flutter.handlers.StreamHandler
import app.talsec.rasp.flutter.handlers.TalsecThreatHandler
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package app.talsec.rasp.flutter

internal sealed class RaspExecutionStateEvent(val value: Int) {
object FastChecksFinished : RaspExecutionStateEvent(187429)

object CriticalChecksFinished : RaspExecutionStateEvent(614702)

object PostponedChecksFinished : RaspExecutionStateEvent(952318)
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.aheaditec.freerasp
package app.talsec.rasp.flutter

import android.annotation.SuppressLint
import android.app.Activity
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.aheaditec.freerasp
package app.talsec.rasp.flutter

/**
* Sealed class to represent the error codes.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.aheaditec.freerasp
package app.talsec.rasp.flutter

import android.content.Context
import android.graphics.Bitmap
Expand Down 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,6 +1,6 @@
package com.aheaditec.freerasp.dispatchers
package app.talsec.rasp.flutter.dispatchers

import com.aheaditec.freerasp.RaspExecutionStateEvent
import app.talsec.rasp.flutter.RaspExecutionStateEvent
import io.flutter.plugin.common.EventChannel.EventSink

internal object ExecutionStateDispatcher {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package app.talsec.rasp.flutter.dispatchers

import app.talsec.rasp.flutter.Threat
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<Map<String, Any?>>()
private val callStateCache = mutableListOf<CallState>()
private var isAppInForeground = false

var eventSink: EventSink? = null
set(value) {
field = value
if (value != null) {
isAppInForeground = true
flushCache()
}
}

fun onResume() {
isAppInForeground = true
if (eventSink != null) {
flushCache()
}
}

fun onPause() {
isAppInForeground = false
}

fun dispatchThreat(threat: Threat) {
if (isAppInForeground && eventSink != null) {
eventSink?.success(threat.value)
} else {
synchronized(threatCache) {
threatCache.add(threat)
}
}
}

/**
* @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)
}
}
}

fun dispatchOngoingCall(state: CallState) {
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) {
callStateCache.add(state)
}
}
}

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

private fun flushThreatCache() {
val threats = synchronized(threatCache) {
val snapshot = threatCache.toSet()
threatCache.clear()
snapshot
}
threats.forEach { eventSink?.success(it.value) }
}

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

private fun flushCallStateCache() {
val states = synchronized(callStateCache) {
val snapshot = callStateCache.toList()
callStateCache.clear()
snapshot
}
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())
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.aheaditec.freerasp.handlers
package app.talsec.rasp.flutter.handlers

import io.flutter.Log
import io.flutter.plugin.common.BinaryMessenger
Expand Down
Loading
Loading