Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
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
58 changes: 58 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
module.exports = {
parser: "@typescript-eslint/parser",
parserOptions: {
project: "./tsconfig.json"
},
plugins: [
"@typescript-eslint",
"import"
],
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"prettier",
"plugin:import/typescript"
],
rules: {
"no-fallthrough": "off",
"no-constant-condition": "off",
"@typescript-eslint/no-this-alias": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/explicit-module-boundary-types": [
"error",
{
"allowArgumentsExplicitlyTypedAsAny": true
}
],
"@typescript-eslint/array-type": "error",
"@typescript-eslint/consistent-type-assertions": "error",
"@typescript-eslint/consistent-type-imports": "error",
"@typescript-eslint/prefer-for-of": "error",
"@typescript-eslint/prefer-optional-chain": "error",
"import/first": "error",
"import/order": [
"error",
{
"alphabetize": {
"order": "asc",
"caseInsensitive": false
},
"groups": [
[
"builtin",
"external"
],
"parent",
[
"sibling",
"index"
]
],
"newlines-between": "always"
}
],
"import/newline-after-import": "error",
"import/no-duplicates": "error",
"import/no-mutable-exports": "error"
}
};
4 changes: 2 additions & 2 deletions .github/actions/setup/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ runs:
using: composite
steps:
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: '20.x'
node-version: 24.x
node-version-file: .nvmrc

- name: Upgrade npm for trusted publishing
Expand Down
57 changes: 57 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,63 @@ 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.3.0] - 2025-12-15

- Android SDK version: 17.0.1
- iOS SDK version: 6.13.0

### Capacitor

#### Added

- Added `killOnBypass` to `TalsecConfig` that configures if the app should be terminated when the threat callbacks are suppressed/hooked by an attacker (Android only) ([Issue 65](https://github.com/talsec/Free-RASP-Android/issues/65))
- Added API for `timeSpoofing` callback into `ThreatEventActions` (Android only)
- Added API for `unsecureWifi` callback into `ThreatEventActions` (Android only)
- Added API for `allChecksFinished` callback into new `RaspExecutionStateEventActions` object
- Added matched permissions to `SuspiciousAppInfo` object when malware detection reason is `suspiciousPermission`

#### Fixed

- Resolved potential collision in threat identifiers

### Android

#### Added

- Added `killOnBypass` method to the `TalsecConfig.Builder` that configures if the app should be terminated when the threat callbacks are suppressed/hooked by an attacker [Issue 65](https://github.com/talsec/Free-RASP-Android/issues/65)
- We are introducing a new capability, detecting whether the device time has been tampered with (`timeSpoofing`)
- We are introducing a new capability, detecting whether the location is being spoofed on the device (`locationSpoofing`)
- We are introducing a new capability, detection of unsecure WiFi (`unecureWifi`)
- Removed deprecated functionality `Pbkdf2Native` and both related native libraries (`libpbkdf2_native.so` and `libpolarssl.so`)
- Added new `RaspExecutionState` which contains `onAllChecksFinished()` method, which is triggered after all checks are completed.
- Added matched permissions to `SuspiciousAppInfo` object when malware detection reason is `suspiciousPermission`
- New option to start Talsec, `Talsec.start()` takes new parameter `TalsecMode` that determines the dispatcher thread of initialization and sync checks (uses background thread by default)
- Capability to check if another app has an option `REQUEST_INSTALL_PACKAGES` enabled in the system settings to malware detection

#### Fixed

- Root detection related bugs causing false positives
- ANR issue caused by `registerScreenCaptureCallback()` method on the main thread
- `NullPointerException` when checking key alias in Keystore on Android 7
- `JaCoCo` issue causing `MethodTooLargeException` during instrumentation
- `DeadApplicationException` when calling `Settings.Global.getInt` or `Settings.Secure.getInt` on invalid context
- `AndroidKeyStore` crashes causing `java.util.concurrent.TimeoutException` when calling `finalize()` method on `Cipher` (GC issues)
- Fixed issue with late initializers and `TalsecMode` coroutines scopes


#### Changed

- Deprecated Nexus repository removed (GCP artifact registry is the main supported distribution repository)
- Shortened the value of threat detection interval
- Refactoring of internal architecture of SDK that newly uses Coroutines to manage threading
- Update of internal dependencies and security libraries

### iOS

#### Changed

- Updated internal dependencies

## [2.2.2] - 2025-08-12

- iOS SDK version: 6.12.1
Expand Down
2 changes: 1 addition & 1 deletion CapacitorFreerasp.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ 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/*.{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/*.{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
2 changes: 1 addition & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
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:16.0.2'
implementation 'com.aheaditec.talsec.security:TalsecSecurity-Community-Capacitor:17.0.1'
}
57 changes: 39 additions & 18 deletions android/src/main/java/com/aheaditec/freerasp/FreeraspPlugin.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ 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.getcapacitor.JSObject
import com.getcapacitor.Plugin
import com.getcapacitor.PluginCall
Expand All @@ -23,7 +26,7 @@ import org.json.JSONArray
class FreeraspPlugin : Plugin() {

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

@PluginMethod
Expand Down Expand Up @@ -97,29 +100,51 @@ class FreeraspPlugin : Plugin() {
*/
@PluginMethod
fun getThreatIdentifiers(call: PluginCall) {
call.resolve(JSObject().put("ids", Threat.getThreatValues()))
call.resolve(JSObject().put("ids", ThreatEvent.ALL_EVENTS))
}

/**
* Method to setup the message passing between native and React Native
* @return list of [THREAT_CHANNEL_NAME, THREAT_CHANNEL_KEY]
* Method to get the random identifiers of callbacks
*/
@PluginMethod
fun getRaspExecutionStateIdentifiers(call: PluginCall) {
call.resolve(JSObject().put("ids", RaspExecutionStateEvent.ALL_EVENTS))
}

/**
* Method to setup the message passing between native and Capacitor
* @return list of [CHANNEL_NAME, CHANNEL_KEY, MALWARE_CHANNEL_KEY]
*/
@PluginMethod
fun getThreatChannelData(call: PluginCall) {
val channelData = JSONArray(
(listOf(
THREAT_CHANNEL_NAME, THREAT_CHANNEL_KEY, MALWARE_CHANNEL_KEY
ThreatEvent.CHANNEL_NAME, ThreatEvent.CHANNEL_KEY, ThreatEvent.MALWARE_CHANNEL_KEY
))
)
call.resolve(JSObject().put("ids", channelData))
}

/**
* Method to setup the execution state message passing between native and Capacitor
* @return list of [CHANNEL_NAME, CHANNEL_KEY]
*/
@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.
* Therefore, if this happens, we want to kill the app.
*/
@PluginMethod
fun onInvalidCallback() {
fun onInvalidCallback(call: PluginCall) {
android.os.Process.killProcess(android.os.Process.myPid())
}

Expand Down Expand Up @@ -179,7 +204,7 @@ class FreeraspPlugin : Plugin() {

activity?.runOnUiThread {
try {
Talsec.blockScreenCapture(context, enable)
Talsec.blockScreenCapture(activity, enable)
call.resolve(JSObject().put("result", true))
} catch (e: Exception) {
call.reject(
Expand Down Expand Up @@ -224,11 +249,12 @@ class FreeraspPlugin : Plugin() {
"Error during storeExternalId operation in freeRASP Native Plugin",
"NativePluginError"
)
return
}
}

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

internal fun notifyMalware(suspiciousApps: MutableList<SuspiciousAppInfo>) {
Expand All @@ -238,9 +264,9 @@ class FreeraspPlugin : Plugin() {
val encodedSuspiciousApps = suspiciousApps.toEncodedJSArray(context)
mainHandler.post {
val params = JSObject()
.put(THREAT_CHANNEL_KEY, Threat.Malware.value)
.put(MALWARE_CHANNEL_KEY, encodedSuspiciousApps)
notifyListeners(THREAT_CHANNEL_NAME, params, true)
.put(ThreatEvent.CHANNEL_KEY, ThreatEvent.Malware.value)
.put(ThreatEvent.MALWARE_CHANNEL_KEY, encodedSuspiciousApps)
notifyListeners(ThreatEvent.CHANNEL_NAME, params, true)
}
}
}
Expand All @@ -253,6 +279,7 @@ class FreeraspPlugin : Plugin() {
.watcherMail(configJson.getString("watcherMail"))
.supportedAlternativeStores(androidConfig.getArraySafe("supportedAlternativeStores"))
.prod(configJson.getBool("isProd") ?: true)
.killOnBypass(configJson.getBool("killOnBypass") ?: false)

if (androidConfig.has("malwareConfig")) {
val malwareConfig = androidConfig.getJSONObject("malwareConfig")
Expand All @@ -266,12 +293,6 @@ class FreeraspPlugin : Plugin() {


companion object {
private val THREAT_CHANNEL_NAME = (10000..999999999).random()
.toString() // name of the channel over which threat callbacks are sent
private val THREAT_CHANNEL_KEY = (10000..999999999).random()
.toString() // key of the argument map under which threats are expected
private val MALWARE_CHANNEL_KEY = (10000..999999999).random()
.toString() // key of the argument map under which malware data is expected
private val backgroundHandlerThread = HandlerThread("BackgroundThread").apply { start() }
private val backgroundHandler = Handler(backgroundHandlerThread.looper)
private val mainHandler = Handler(Looper.getMainLooper())
Expand Down
26 changes: 24 additions & 2 deletions android/src/main/java/com/aheaditec/freerasp/ScreenProtector.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,43 @@ import android.view.WindowManager.SCREEN_RECORDING_STATE_VISIBLE
import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat
import com.aheaditec.talsec_security.security.api.Talsec
import com.aheaditec.freerasp.events.ThreatEvent
import java.util.function.Consumer

@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
internal object ScreenProtector {
private const val TAG = "TalsecScreenProtector"
private const val SCREEN_CAPTURE_PERMISSION = "android.permission.DETECT_SCREEN_CAPTURE"
private const val SCREEN_RECORDING_PERMISSION = "android.permission.DETECT_SCREEN_RECORDING"

private var registered = false
private val screenCaptureCallback = ScreenCaptureCallback { Talsec.onScreenshotDetected() }
private val cachedThreats = mutableSetOf<ThreatEvent>()

private val screenCaptureCallback = ScreenCaptureCallback { handleThreat(ThreatEvent.Screenshot) }
private val screenRecordCallback: Consumer<Int> = Consumer<Int> { state ->
if (state == SCREEN_RECORDING_STATE_VISIBLE) {
Talsec.onScreenRecordingDetected()
handleThreat(ThreatEvent.ScreenRecording)
}
}

private fun handleThreat(threat: ThreatEvent) {
if(!FreeraspPlugin.talsecStarted) {
cachedThreats.add(threat)
return
}

when (threat) {
ThreatEvent.Screenshot -> Talsec.onScreenshotDetected()
ThreatEvent.ScreenRecording -> Talsec.onScreenRecordingDetected()
else -> throw IllegalArgumentException("Unexpected Threat type: $threat")
}
}

internal fun flushCache() {
cachedThreats.forEach { handleThreat(it) }
cachedThreats.clear()
}

/**
* Registers screenshot and screen recording detector with the given activity
*
Expand Down
58 changes: 0 additions & 58 deletions android/src/main/java/com/aheaditec/freerasp/Threat.kt

This file was deleted.

Loading
Loading