Skip to content
Open
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
48 changes: 48 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,54 @@ 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).

## [2.4.0] - 2026-02-13

- Android SDK version: 18.0.2
- iOS SDK version: 6.13.0

### Capacitor

#### Added

- Added cache for freeRASP callbacks when listener is not registered with the app
- Added API for `automation` callback into `ThreatEventActions` (Android only)

#### Fixed

- Prevent multiple registration of the freeRASP listeners on the native side

#### Changed

- Updated compile and target SDK versions to 36 on Android

### Android

#### Added

- Added support for `KernelSU` to the existing root detection capabilities
- Added support for `HMA` to the existing root detection capabilities
- Added new malware detection capabilities
- Added `onAutomationDetected()` callback to `ThreatDetected` interface
- We are introducing a new capability, detecting whether the device is being automated using tools like Appium
- Added value restrictions to `externalId`
- Method `storeExternalId()` now returns `ExternalIdResult`, which indicates `Success` or `Error` when `externalId` violates restrictions

#### Fixed

- Fixed exception handling for the KeyStore `getEntry` operation
- Fixed issue in `ScreenProtector` concerning the `onScreenRecordingDetected` invocations
- Merged internal shared libraries into a single one, reducing the final APK size
- Fixed bug related to key storing in keystore type detection (hw-backed keystore check)
- Fixed manifest queries merge

#### Changed

- Removed unused library `tmlib`
- Refactoring of signature verification code
- Updated compile and target API to 36
- Improved root detection capabilities
- Detection of wireless ADB added to ADB detections

## [2.3.0] - 2025-12-15

- Android SDK version: 17.0.1
Expand Down
6 changes: 5 additions & 1 deletion CapacitorFreerasp.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ Pod::Spec.new do |s|
s.homepage = package['repository']['url']
s.author = package['author']
s.source = { :git => package['repository']['url'], :tag => s.version.to_s }
s.source_files = 'ios/Plugin/models/*.{swift,h,m,c,cc,mm,cpp}', 'ios/Plugin/utils/*.{swift,h,m,c,cc,mm,cpp}', 'ios/Plugin/*.{swift,h,m,c,cc,mm,cpp}', 'ios/Plugin/TalsecRuntime.xcframework'
s.source_files = 'ios/Plugin/models/*.{swift,h,m,c,cc,mm,cpp}',
'ios/Plugin/utils/*.{swift,h,m,c,cc,mm,cpp}',
'ios/Plugin/dispatchers/*.{swift,h,m,c,cc,mm,cpp}',
'ios/Plugin/*.{swift,h,m,c,cc,mm,cpp}',
'ios/Plugin/TalsecRuntime.xcframework'
s.ios.deployment_target = '13.0'
s.dependency 'Capacitor'
s.swift_version = '5.1'
Expand Down
6 changes: 3 additions & 3 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ apply plugin: 'kotlinx-serialization'

android {
namespace "com.aheaditec.freerasp"
compileSdk 35
compileSdk Math.max(36, project.hasProperty('rootProject.ext.compileSdk') ? rootProject.ext.compileSdk as int : 36)
defaultConfig {
minSdkVersion 23
targetSdkVersion 35
targetSdkVersion 36
versionCode 1
versionName "1.0"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
Expand Down Expand Up @@ -76,5 +76,5 @@ dependencies {
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"

implementation 'com.aheaditec.talsec.security:TalsecSecurity-Community-Capacitor:17.0.1'
implementation 'com.aheaditec.talsec.security:TalsecSecurity-Community-Capacitor:18.0.2'
}
2 changes: 1 addition & 1 deletion android/gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip
networkTimeout=10000
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
100 changes: 76 additions & 24 deletions android/src/main/java/com/aheaditec/freerasp/FreeraspPlugin.kt
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the plugin is destroyed but the application process remains alive, the singleton will continue to hold the old Context, causing a memory leak. Therefore, the handleOnDestroy method should be overridden to release the dispatcher listeners.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

onDestroy() is not guaranteed to be called in the Android activity lifecycle, so this is not the best place for such actions. However, yes I will add this

Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.aheaditec.freerasp

import android.content.Context
import android.os.Build
import android.os.Handler
import android.os.HandlerThread
Expand All @@ -11,22 +12,23 @@ import com.aheaditec.freerasp.utils.toEncodedJSArray
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 com.aheaditec.talsec_security.security.api.ThreatListener
import com.aheaditec.freerasp.events.BaseRaspEvent
import com.aheaditec.freerasp.events.RaspExecutionStateEvent
import com.aheaditec.freerasp.events.ThreatEvent
import com.aheaditec.freerasp.interfaces.PluginExecutionStateListener
import com.aheaditec.freerasp.interfaces.PluginThreatListener
import com.getcapacitor.JSObject
import com.getcapacitor.Plugin
import com.getcapacitor.PluginCall
import com.getcapacitor.PluginMethod
import com.getcapacitor.annotation.CapacitorPlugin
import org.json.JSONArray

typealias CapacitorCallback = (String, JSObject) -> Unit

@CapacitorPlugin(name = "Freerasp")
class FreeraspPlugin : Plugin() {

private val threatHandler = TalsecThreatHandler(this)
private val listener = ThreatListener(threatHandler, threatHandler, threatHandler)
private var registered = true

@PluginMethod
Expand All @@ -38,7 +40,15 @@ class FreeraspPlugin : Plugin() {
}
try {
val talsecConfig = buildTalsecConfigThrowing(config)
listener.registerListener(context)

val pluginCallback: CapacitorCallback = { eventName, data ->
notifyListeners(eventName, data, true)
}

PluginThreatHandler.threatDispatcher.listener = PluginListener(context, pluginCallback)
PluginThreatHandler.executionStateDispatcher.listener = PluginListener(context, pluginCallback)
PluginThreatHandler.registerListener(context)

bridge.activity.runOnUiThread {
Talsec.start(context, talsecConfig)
mainHandler.post {
Expand Down Expand Up @@ -70,16 +80,18 @@ class FreeraspPlugin : Plugin() {
override fun handleOnPause() {
super.handleOnPause()
if (activity.isFinishing) {
listener.unregisterListener(context)
PluginThreatHandler.unregisterListener(context)
registered = false
PluginThreatHandler.threatDispatcher.listener = null
PluginThreatHandler.executionStateDispatcher.listener = null
}
}

override fun handleOnResume() {
super.handleOnResume()
if (!registered) {
registered = true
listener.registerListener(context)
PluginThreatHandler.registerListener(context)
}
}

Expand Down Expand Up @@ -129,15 +141,15 @@ class FreeraspPlugin : Plugin() {
* Method to setup the execution state message passing between native and Capacitor
* @return list of [CHANNEL_NAME, CHANNEL_KEY]
*/
@PluginMethod
fun getRaspExecutionStateChannelData(call: PluginCall) {
@PluginMethod
fun getRaspExecutionStateChannelData(call: PluginCall) {
val channelData = JSONArray(
(listOf(
RaspExecutionStateEvent.CHANNEL_NAME, RaspExecutionStateEvent.CHANNEL_KEY
))
)
call.resolve(JSObject().put("ids", channelData))
}
}

/**
* We never send an invalid callback over our channel.
Expand Down Expand Up @@ -253,21 +265,17 @@ class FreeraspPlugin : Plugin() {
}
}

internal fun notifyListeners(event: BaseRaspEvent) {
notifyListeners(event.channelName, JSObject().put(event.channelKey, event.value), true)
}

internal fun notifyMalware(suspiciousApps: MutableList<SuspiciousAppInfo>) {
// Perform the malware encoding on a background thread
backgroundHandler.post {

val encodedSuspiciousApps = suspiciousApps.toEncodedJSArray(context)
mainHandler.post {
val params = JSObject()
.put(ThreatEvent.CHANNEL_KEY, ThreatEvent.Malware.value)
.put(ThreatEvent.MALWARE_CHANNEL_KEY, encodedSuspiciousApps)
notifyListeners(ThreatEvent.CHANNEL_NAME, params, true)
}
@PluginMethod
fun removeExternalId(call: PluginCall) {
try {
Talsec.removeExternalId(context)
call.resolve(JSObject().put("result", true))
} catch (e: Exception) {
call.reject(
"Error during removeExternalId operation in freeRASP Native Plugin",
"NativePluginError"
)
return
}
}

Expand Down Expand Up @@ -298,5 +306,49 @@ class FreeraspPlugin : Plugin() {
private val mainHandler = Handler(Looper.getMainLooper())

internal var talsecStarted = false

internal fun notifyEvent(
event: BaseRaspEvent,
notifyListenersCallback: CapacitorCallback
) {
val params = JSObject().put(event.channelKey, event.value)
notifyListenersCallback(event.channelName, params)
}

internal fun notifyMalware(
suspiciousApps: MutableList<SuspiciousAppInfo>,
context: Context,
notifyListenersCallback: CapacitorCallback
) {
// Perform the malware encoding on a background thread
backgroundHandler.post {

val encodedSuspiciousApps = suspiciousApps.toEncodedJSArray(context)
mainHandler.post {
val params = JSObject()
.put(ThreatEvent.CHANNEL_KEY, ThreatEvent.Malware.value)
.put(ThreatEvent.MALWARE_CHANNEL_KEY, encodedSuspiciousApps)
notifyListenersCallback.invoke(ThreatEvent.CHANNEL_NAME, params)
}

}
}
}

internal class PluginListener(
private val context: Context,
private val pluginCallback: CapacitorCallback
) : PluginThreatListener, PluginExecutionStateListener {
override fun threatDetected(threatEventType: ThreatEvent) {
notifyEvent(threatEventType, pluginCallback)
}

override fun malwareDetected(suspiciousApps: MutableList<SuspiciousAppInfo>) {
notifyMalware(suspiciousApps, context, pluginCallback)
}

override fun raspExecutionStateChanged(event: RaspExecutionStateEvent) {
notifyEvent(event, pluginCallback)
}
}
}
121 changes: 121 additions & 0 deletions android/src/main/java/com/aheaditec/freerasp/PluginThreatHandler.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package com.aheaditec.freerasp

import android.content.Context
import com.aheaditec.talsec_security.security.api.SuspiciousAppInfo
import com.aheaditec.talsec_security.security.api.ThreatListener
import com.aheaditec.freerasp.dispatchers.ExecutionStateDispatcher
import com.aheaditec.freerasp.dispatchers.ThreatDispatcher
import com.aheaditec.freerasp.events.RaspExecutionStateEvent
import com.aheaditec.freerasp.events.ThreatEvent

internal object PluginThreatHandler {

internal val threatDispatcher = ThreatDispatcher()
internal val executionStateDispatcher = ExecutionStateDispatcher()

private val threatDetected = object : ThreatListener.ThreatDetected() {

override fun onRootDetected() {
threatDispatcher.dispatchThreat(ThreatEvent.PrivilegedAccess)
}

override fun onDebuggerDetected() {
threatDispatcher.dispatchThreat(ThreatEvent.Debug)
}

override fun onEmulatorDetected() {
threatDispatcher.dispatchThreat(ThreatEvent.Simulator)
}

override fun onTamperDetected() {
threatDispatcher.dispatchThreat(ThreatEvent.AppIntegrity)
}

override fun onUntrustedInstallationSourceDetected() {
threatDispatcher.dispatchThreat(ThreatEvent.UnofficialStore)
}

override fun onHookDetected() {
threatDispatcher.dispatchThreat(ThreatEvent.Hooks)
}

override fun onDeviceBindingDetected() {
threatDispatcher.dispatchThreat(ThreatEvent.DeviceBinding)
}

override fun onObfuscationIssuesDetected() {
threatDispatcher.dispatchThreat(ThreatEvent.ObfuscationIssues)
}

override fun onMalwareDetected(suspiciousAppInfos: MutableList<SuspiciousAppInfo>) {
threatDispatcher.dispatchMalware(suspiciousAppInfos ?: mutableListOf())
}

override fun onScreenshotDetected() {
threatDispatcher.dispatchThreat(ThreatEvent.Screenshot)
}

override fun onScreenRecordingDetected() {
threatDispatcher.dispatchThreat(ThreatEvent.ScreenRecording)
}

override fun onMultiInstanceDetected() {
threatDispatcher.dispatchThreat(ThreatEvent.MultiInstance)
}

override fun onUnsecureWifiDetected() {
threatDispatcher.dispatchThreat(ThreatEvent.UnsecureWifi)
}

override fun onTimeSpoofingDetected() {
threatDispatcher.dispatchThreat(ThreatEvent.TimeSpoofing)
}

override fun onLocationSpoofingDetected() {
threatDispatcher.dispatchThreat(ThreatEvent.LocationSpoofing)
}

override fun onAutomationDetected() {
threatDispatcher.dispatchThreat(ThreatEvent.Automation)
}
}

private val deviceState = object : ThreatListener.DeviceState() {

override fun onUnlockedDeviceDetected() {
threatDispatcher.dispatchThreat(ThreatEvent.Passcode)
}

override fun onHardwareBackedKeystoreNotAvailableDetected() {
threatDispatcher.dispatchThreat(ThreatEvent.SecureHardwareNotAvailable)
}

override fun onDeveloperModeDetected() {
threatDispatcher.dispatchThreat(ThreatEvent.DevMode)
}

override fun onADBEnabledDetected() {
threatDispatcher.dispatchThreat(ThreatEvent.ADBEnabled)
}

override fun onSystemVPNDetected() {
threatDispatcher.dispatchThreat(ThreatEvent.SystemVPN)
}
}

private val raspExecutionState = object : ThreatListener.RaspExecutionState() {
override fun onAllChecksFinished() {
executionStateDispatcher.dispatch(RaspExecutionStateEvent.AllChecksFinished)
}
}

private val internalListener = ThreatListener(threatDetected, deviceState, raspExecutionState)

internal fun registerListener(context: Context) {
internalListener.registerListener(context)
}

internal fun unregisterListener(context: Context) {
internalListener.unregisterListener(context)
}
}
Loading
Loading