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
49 changes: 49 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,55 @@ 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.0.0] - 2026-11-11

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

### Breaking

- `allChecksFinished` RASP execution state action 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
- `TalsecIosConfig.appBundleId` (string) replaced by `appBundleIds` (string[]), matching the iOS SDK
- Methods that resolved with a status string or a constant now resolve to `void`: `talsecStart`, `addToWhitelist`, `blockScreenCapture`, `blockScreenControl`, `startOngoingCallDetection`, `stopOngoingCallDetection`, `storeExternalId`, `removeExternalId`
- All methods now reject with `TalsecError` (`code`: `talsec-failure`, `unsupported-platform`, `configuration-exception`, `external-id-failure`, `malware-failure`) instead of native errors or plain strings
- `onInvalidCallback` and `removeListenerForEvent` are no longer exported; use `abortApp` to terminate the app

### React Native

#### Added

- `useFreeRasp` returns `{ started, error }`; `error` is the `TalsecError` that prevented the listeners from being registered or freeRASP from starting, which was previously only logged to the console
- `ongoingCall` threat action with the new `CallState` type (`'STARTED' | 'ONGOING' | 'STOPPED'`), driven by `startOngoingCallDetection()` and `stopOngoingCallDetection()` (Android only)
- `blockScreenControl()` and `isScreenControlBlocked()` to prevent accessibility services from reading or controlling the app screen (Android only)
- `blockOverlayWindows()` and `isOverlayWindowsBlocked()` to hide overlay windows of other apps (Android 12+ only, requires the `HIDE_OVERLAY_WINDOWS` permission)
- `criticalChecksFinished` and `postponedChecksFinished` RASP execution state actions (Android only)

#### Changed

- Android package (namespace) of the plugin renamed from `com.freeraspreactnative` to `app.talsec.rasp.reactnative`, following the Android SDK's `app.talsec.rasp` (update the import if you register `FreeraspReactNativePackage` manually)
- On Android, `isScreenCaptureBlocked()` reports the protection state of the current activity
- Threat and RASP execution state actions are typed as returning `void` instead of `any`

### 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

## [5.3.0] - 2026-11-09

- Android SDK version: 19.3.1
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,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
Expand Up @@ -57,7 +57,7 @@ android {
compileSdk getMaxVersion("compileSdk")

talsec {
sdkVersion = "19.3.1"
sdkVersion = "20.0.1"
platform = "ReactNative"
}

Expand Down Expand Up @@ -87,7 +87,7 @@ android {
}

if (project.android.hasProperty("namespace")) {
namespace("com.freeraspreactnative")
namespace("app.talsec.rasp.reactnative")
}
}

Expand Down Expand Up @@ -119,6 +119,6 @@ if (isNewArchitectureEnabled()) {
react {
jsRootDir = file("../src/")
libraryName = "FreeraspReactNative"
codegenJavaPackageName = "com.freeraspreactnative"
codegenJavaPackageName = "app.talsec.rasp.reactnative"
}
}
6 changes: 3 additions & 3 deletions android/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
-dontwarn java.lang.invoke.StringConcatFactory
-keep class com.freeraspreactnative.FreeraspReactNativePackage { public *; }
-keep class app.talsec.rasp.reactnative.FreeraspReactNativePackage { public *; }

-if @kotlinx.serialization.Serializable class **
-keep class <1> {
*;
}

-keep class com.freeraspreactnative.models.RNSuspiciousAppInfo$Companion
-keep class com.freeraspreactnative.models.RNPackageInfo$Companion
-keep class app.talsec.rasp.reactnative.models.RNSuspiciousAppInfo$Companion
-keep class app.talsec.rasp.reactnative.models.RNPackageInfo$Companion

-repackageclasses 'ts'
2 changes: 1 addition & 1 deletion android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.freeraspreactnative">
package="app.talsec.rasp.reactnative">

</manifest>
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.freeraspreactnative
package app.talsec.rasp.reactnative

import android.annotation.SuppressLint
import android.app.Activity
import android.os.Build
import android.os.Handler
import android.os.HandlerThread
Expand All @@ -9,6 +11,7 @@ import app.talsec.rasp.security.api.SuspiciousAppInfo
import app.talsec.rasp.security.api.Talsec
import app.talsec.rasp.security.api.TalsecConfig
import app.talsec.rasp.security.api.TalsecMode
import app.talsec.rasp.security.api.ThreatListener.CallState
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.LifecycleEventListener
import com.facebook.react.bridge.Promise
Expand All @@ -20,20 +23,21 @@ import com.facebook.react.bridge.UiThreadUtil.runOnUiThread
import com.facebook.react.bridge.WritableArray
import com.facebook.react.bridge.WritableMap
import com.facebook.react.modules.core.DeviceEventManagerModule
import com.freeraspreactnative.dispatchers.ExecutionStateDispatcher
import com.freeraspreactnative.dispatchers.ThreatDispatcher
import com.freeraspreactnative.events.RaspExecutionStateEvent
import com.freeraspreactnative.events.ThreatEvent
import com.freeraspreactnative.interfaces.PluginExecutionStateListener
import com.freeraspreactnative.interfaces.PluginThreatListener
import com.freeraspreactnative.utils.Utils
import com.freeraspreactnative.utils.getArraySafe
import com.freeraspreactnative.utils.getBooleanSafe
import com.freeraspreactnative.utils.getMapThrowing
import com.freeraspreactnative.utils.getNestedArraySafe
import com.freeraspreactnative.utils.getStringThrowing
import com.freeraspreactnative.utils.toEncodedWritableArray
import com.freeraspreactnative.utils.toSuspiciousAppDetectionConfig
import app.talsec.rasp.reactnative.dispatchers.ExecutionStateDispatcher
import app.talsec.rasp.reactnative.dispatchers.ThreatDispatcher
import app.talsec.rasp.reactnative.events.RaspExecutionStateEvent
import app.talsec.rasp.reactnative.events.ThreatEvent
import app.talsec.rasp.reactnative.interfaces.PluginExecutionStateListener
import app.talsec.rasp.reactnative.interfaces.PluginThreatListener
import app.talsec.rasp.reactnative.utils.Utils
import app.talsec.rasp.reactnative.utils.getArraySafe
import app.talsec.rasp.reactnative.utils.getBooleanSafe
import app.talsec.rasp.reactnative.utils.getMapThrowing
import app.talsec.rasp.reactnative.utils.getNestedArraySafe
import app.talsec.rasp.reactnative.utils.getStringThrowing
import app.talsec.rasp.reactnative.utils.toEncodedWritableArray
import app.talsec.rasp.reactnative.utils.toSuspiciousAppDetectionConfig
import java.lang.ref.WeakReference

class FreeraspReactNativeModule(private val reactContext: ReactApplicationContext) :
ReactContextBaseJavaModule(reactContext) {
Expand All @@ -54,6 +58,7 @@ class FreeraspReactNativeModule(private val reactContext: ReactApplicationContex
reactContext.currentActivity?.let { ScreenProtector.unregister(it) }
}
if (reactContext.currentActivity?.isFinishing == true) {
stopOngoingCallDetectionInternal()
ThreatDispatcher.unregisterListener()
ExecutionStateDispatcher.unregisterListener()
PluginThreatHandler.unregisterSDKListener(reactContext)
Expand Down Expand Up @@ -134,14 +139,15 @@ class FreeraspReactNativeModule(private val reactContext: ReactApplicationContex

/**
* Method to setup the threat message passing between native and React Native
* @return list of [THREAT_CHANNEL_NAME, THREAT_CHANNEL_KEY]
* @return list of [THREAT_CHANNEL_NAME, THREAT_CHANNEL_KEY, MALWARE_CHANNEL_KEY, CALL_STATE_CHANNEL_KEY]
*/
@ReactMethod
fun getThreatChannelData(promise: Promise) {
val channelData: WritableArray = Arguments.createArray()
channelData.pushString(ThreatEvent.CHANNEL_NAME)
channelData.pushString(ThreatEvent.CHANNEL_KEY)
channelData.pushString(ThreatEvent.MALWARE_CHANNEL_KEY)
channelData.pushString(ThreatEvent.CALL_STATE_CHANNEL_KEY)
promise.resolve(channelData)
}

Expand Down Expand Up @@ -245,14 +251,158 @@ class FreeraspReactNativeModule(private val reactContext: ReactApplicationContex
*/
@ReactMethod
fun isScreenCaptureBlocked(promise: Promise) {
val activity = reactContext.currentActivity ?: run {
promise.reject(
"NativePluginError", "Cannot check screen capture state, activity is null."
)
return
}

try {
val isBlocked = Talsec.isScreenCaptureBlocked()
val isBlocked = Talsec.isScreenCaptureBlocked(activity)
promise.resolve(isBlocked)
} catch (e: Exception) {
promise.reject("NativePluginError", "Error in isScreenCaptureBlocked: ${e.message}")
}
}

/**
* Method Block/Unblock accessibility services from reading or controlling the screen
* @param enable boolean for whether want to block or unblock the screen control
*/
@ReactMethod
fun blockScreenControl(enable: Boolean, promise: Promise) {
val activity = reactContext.currentActivity ?: run {
promise.reject(
"NativePluginError", "Cannot block screen control, activity is null."
)
return
}

runOnUiThread {
try {
Talsec.blockScreenControl(activity, enable)
promise.resolve("Screen control is now ${if (enable) "Blocked" else "Enabled"}.")
} catch (e: Exception) {
promise.reject("NativePluginError", "Error in blockScreenControl: ${e.message}")
}
}
}

/**
* Method Returns whether screen control is blocked or not
* @return boolean for is screen control blocked or not
*/
@ReactMethod
fun isScreenControlBlocked(promise: Promise) {
val activity = reactContext.currentActivity ?: run {
promise.reject(
"NativePluginError", "Cannot check screen control state, activity is null."
)
return
}

try {
promise.resolve(Talsec.isScreenControlBlocked(activity))
} catch (e: Exception) {
promise.reject("NativePluginError", "Error in isScreenControlBlocked: ${e.message}")
}
}

/**
* Method Hide/Show overlay windows of other apps over the activity.
* Requires Android 12+ and the HIDE_OVERLAY_WINDOWS permission.
* @param enable boolean for whether want to hide or show overlay windows
* @return boolean whether the request was applied
*/
// Missing permission is suppressed because the decision to use the overlay API is made
// by developer, and not enforced by the library.
@SuppressLint("MissingPermission")
@ReactMethod
fun blockOverlayWindows(enable: Boolean, promise: Promise) {
val activity = reactContext.currentActivity ?: run {
promise.reject(
"NativePluginError", "Cannot block overlay windows, activity is null."
)
return
}

if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
promise.resolve(false)
return
}

runOnUiThread {
try {
promise.resolve(Talsec.blockOverlayWindows(activity, enable))
} catch (e: Exception) {
promise.reject("NativePluginError", "Error in blockOverlayWindows: ${e.message}")
}
}
}

/**
* Method Returns whether overlay windows of other apps are hidden or not
* @return boolean for are overlay windows hidden or not
*/
@ReactMethod
fun isOverlayWindowsBlocked(promise: Promise) {
val activity = reactContext.currentActivity ?: run {
promise.reject(
"NativePluginError", "Cannot check overlay windows state, activity is null."
)
return
}

try {
promise.resolve(Talsec.isOverlayWindowsBlocked(activity))
} catch (e: Exception) {
promise.reject("NativePluginError", "Error in isOverlayWindowsBlocked: ${e.message}")
}
}

/**
* Method starts the ongoing call (cellular and VoIP) detection for the current activity
*/
@ReactMethod
fun startOngoingCallDetection(promise: Promise) {
val activity = reactContext.currentActivity ?: run {
promise.reject(
"NativePluginError", "Cannot start ongoing call detection, activity is null."
)
return
}

try {
if (ongoingCallActivity?.get() !== activity) {
stopOngoingCallDetectionInternal()
Talsec.startOngoingCallDetection(activity)
ongoingCallActivity = WeakReference(activity)
}
promise.resolve("Ongoing call detection started.")
} catch (e: Exception) {
promise.reject("NativePluginError", "Error in startOngoingCallDetection: ${e.message}")
}
}

/**
* Method stops the ongoing call detection
*/
@ReactMethod
fun stopOngoingCallDetection(promise: Promise) {
try {
stopOngoingCallDetectionInternal()
promise.resolve("Ongoing call detection stopped.")
} catch (e: Exception) {
promise.reject("NativePluginError", "Error in stopOngoingCallDetection: ${e.message}")
}
}

private fun stopOngoingCallDetectionInternal() {
ongoingCallActivity?.get()?.let { Talsec.stopOngoingCallDetection(it) }
ongoingCallActivity = null
}

@ReactMethod
fun storeExternalId(
externalId: String, promise: Promise
Expand Down Expand Up @@ -319,6 +469,9 @@ class FreeraspReactNativeModule(private val reactContext: ReactApplicationContex
private val mainHandler = Handler(Looper.getMainLooper())

internal var talsecStarted = false

// Ongoing call detection is registered per activity
private var ongoingCallActivity: WeakReference<Activity>? = null
}

internal class PluginListener(private val reactContext: ReactApplicationContext) :
Expand All @@ -344,6 +497,13 @@ class FreeraspReactNativeModule(private val reactContext: ReactApplicationContex
}
}

override fun ongoingCallDetected(state: CallState) {
val params = Arguments.createMap()
params.putInt(ThreatEvent.CHANNEL_KEY, ThreatEvent.OngoingCall.value)
params.putString(ThreatEvent.CALL_STATE_CHANNEL_KEY, state.name)
notifyEvent(ThreatEvent.CHANNEL_NAME, params)
}

override fun raspExecutionStateChanged(event: RaspExecutionStateEvent) {
val params = Arguments.createMap()
params.putInt(event.channelKey, event.value)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.freeraspreactnative
package app.talsec.rasp.reactnative

import com.facebook.react.ReactPackage
import com.facebook.react.bridge.NativeModule
Expand Down
Loading
Loading