diff --git a/CHANGELOG.md b/CHANGELOG.md index a0fae39..59defb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,56 @@ 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). -## [4.3.2] - 2025-01-03 +## [4.4.0] - 2026-02-06 + +- Android SDK version: 18.0.1 +- iOS SDK version: 6.13.0 + +### React Native + +#### 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 +- Higher compileSdk from [rootProject, plugin] is now used in build.gradle 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 + +## [4.3.2] - 2026-01-03 - Android SDK version: 17.0.1 - iOS SDK version: 6.13.0 diff --git a/android/build.gradle b/android/build.gradle index 599f0f2..43633a6 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -44,8 +44,14 @@ def getDefault(name) { return project.properties["FreeraspReactNative_" + name] } +def getMaxVersion(name) { + def rootVersion = rootProject.ext.has(name) ? rootProject.ext.get(name).toInteger() : 0 + def pluginVersion = getIntegerDefault(name) + return Math.max(rootVersion, pluginVersion) +} + android { - compileSdkVersion getIntegerDefault("compileSdkVersion") + compileSdk getMaxVersion("compileSdk") defaultConfig { minSdkVersion getIntegerDefault("minSdkVersion") @@ -99,7 +105,7 @@ dependencies { implementation "com.facebook.react:react-native:$react_native_version" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1" - implementation "com.aheaditec.talsec.security:TalsecSecurity-Community-ReactNative:17.0.1" + implementation "com.aheaditec.talsec.security:TalsecSecurity-Community-ReactNative:18.0.1" } if (isNewArchitectureEnabled()) { diff --git a/android/gradle.properties b/android/gradle.properties index 05fca26..7a36075 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,6 +1,6 @@ FreeraspReactNative_kotlinVersion=2.1.0 FreeraspReactNative_minSdkVersion=23 -FreeraspReactNative_targetSdkVersion=35 -FreeraspReactNative_compileSdkVersion=35 +FreeraspReactNative_targetSdkVersion=36 +FreeraspReactNative_compileSdk=36 FreeraspReactNative_ndkversion=21.4.7075529 FreeraspReactNative_reactNativeVersion=+ diff --git a/android/src/main/java/com/freeraspreactnative/FreeraspReactNativeModule.kt b/android/src/main/java/com/freeraspreactnative/FreeraspReactNativeModule.kt index 746a16b..fac2db4 100644 --- a/android/src/main/java/com/freeraspreactnative/FreeraspReactNativeModule.kt +++ b/android/src/main/java/com/freeraspreactnative/FreeraspReactNativeModule.kt @@ -22,6 +22,8 @@ import com.facebook.react.modules.core.DeviceEventManagerModule import com.freeraspreactnative.events.BaseRaspEvent import com.freeraspreactnative.events.RaspExecutionStateEvent import com.freeraspreactnative.events.ThreatEvent +import com.freeraspreactnative.interfaces.WrapperExecutionStateListener +import com.freeraspreactnative.interfaces.WrapperThreatListener import com.freeraspreactnative.utils.Utils import com.freeraspreactnative.utils.getArraySafe import com.freeraspreactnative.utils.getBooleanSafe @@ -33,7 +35,12 @@ import com.freeraspreactnative.utils.toEncodedWritableArray class FreeraspReactNativeModule(private val reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) { - private val listener = ThreatListener(FreeraspThreatHandler, FreeraspThreatHandler, FreeraspThreatHandler) + private val listener = ThreatListener( + WrapperThreatHandler.threatDetected, + WrapperThreatHandler.deviceState, + WrapperThreatHandler.raspExecutionState + ) + private val lifecycleListener = object : LifecycleEventListener { override fun onHostResume() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { @@ -69,7 +76,6 @@ class FreeraspReactNativeModule(private val reactContext: ReactApplicationContex try { val config = buildTalsecConfig(options) - FreeraspThreatHandler.listener = ThreatListener listener.registerListener(reactContext) runOnUiThread { Talsec.start(reactContext, config, TalsecMode.BACKGROUND) @@ -147,13 +153,30 @@ class FreeraspReactNativeModule(private val reactContext: ReactApplicationContex } @ReactMethod - fun addListener(@Suppress("UNUSED_PARAMETER") eventName: String) { - // Set up any upstream listeners or background tasks as necessary + fun addListener(eventName: String) { + if (eventName == ThreatEvent.CHANNEL_NAME) { + WrapperThreatHandler.threatDispatcher.listener = WrapperListener + } + if (eventName == RaspExecutionStateEvent.CHANNEL_NAME) { + WrapperThreatHandler.executionStateDispatcher.listener = WrapperListener + } } @ReactMethod fun removeListeners(@Suppress("UNUSED_PARAMETER") count: Int) { - // Remove upstream listeners, stop unnecessary background tasks + // built-in RN method, however it does not suit us as it just tells + // number of un-registered listeners, so we use `removeListenerForEvent` + } + + @ReactMethod + fun removeListenerForEvent(eventName: String, promise: Promise) { + if (eventName == ThreatEvent.CHANNEL_NAME) { + WrapperThreatHandler.threatDispatcher.listener = null + } + if (eventName == RaspExecutionStateEvent.CHANNEL_NAME) { + WrapperThreatHandler.executionStateDispatcher.listener = null + } + promise.resolve("Listener unregistered") } /** @@ -218,17 +241,17 @@ class FreeraspReactNativeModule(private val reactContext: ReactApplicationContex @ReactMethod fun storeExternalId( - externalId: String, promise: Promise + externalId: String, promise: Promise ) { - try { - Talsec.storeExternalId(reactContext, externalId) - promise.resolve("OK - Store external ID") - } catch (e: Exception) { - promise.reject( - "NativePluginError", - "Error during storeExternalId operation in Talsec Native Plugin" - ) - } + try { + Talsec.storeExternalId(reactContext, externalId) + promise.resolve("OK - Store external ID") + } catch (e: Exception) { + promise.reject( + "NativePluginError", + "Error during storeExternalId operation in Talsec Native Plugin" + ) + } } private fun buildTalsecConfig(config: ReadableMap): TalsecConfig { @@ -294,7 +317,7 @@ class FreeraspReactNativeModule(private val reactContext: ReactApplicationContex } } - internal object ThreatListener : FreeraspThreatHandler.TalsecReactNative { + internal object WrapperListener : WrapperThreatListener, WrapperExecutionStateListener { override fun threatDetected(threatEventType: ThreatEvent) { notifyEvent(threatEventType) } diff --git a/android/src/main/java/com/freeraspreactnative/FreeraspThreatHandler.kt b/android/src/main/java/com/freeraspreactnative/FreeraspThreatHandler.kt deleted file mode 100644 index 3554800..0000000 --- a/android/src/main/java/com/freeraspreactnative/FreeraspThreatHandler.kt +++ /dev/null @@ -1,103 +0,0 @@ -package com.freeraspreactnative - -import com.aheaditec.talsec_security.security.api.SuspiciousAppInfo -import com.aheaditec.talsec_security.security.api.ThreatListener -import com.freeraspreactnative.events.RaspExecutionStateEvent -import com.freeraspreactnative.events.ThreatEvent - -internal object FreeraspThreatHandler : ThreatListener.ThreatDetected, ThreatListener.DeviceState, ThreatListener.RaspExecutionState() { - - internal var listener: TalsecReactNative? = null - - override fun onRootDetected() { - listener?.threatDetected(ThreatEvent.PrivilegedAccess) - } - - override fun onDebuggerDetected() { - listener?.threatDetected(ThreatEvent.Debug) - } - - override fun onEmulatorDetected() { - listener?.threatDetected(ThreatEvent.Simulator) - } - - override fun onTamperDetected() { - listener?.threatDetected(ThreatEvent.AppIntegrity) - } - - override fun onUntrustedInstallationSourceDetected() { - listener?.threatDetected(ThreatEvent.UnofficialStore) - } - - override fun onHookDetected() { - listener?.threatDetected(ThreatEvent.Hooks) - } - - override fun onDeviceBindingDetected() { - listener?.threatDetected(ThreatEvent.DeviceBinding) - } - - override fun onObfuscationIssuesDetected() { - listener?.threatDetected(ThreatEvent.ObfuscationIssues) - } - - override fun onMalwareDetected(suspiciousAppInfos: MutableList) { - listener?.malwareDetected(suspiciousAppInfos ?: mutableListOf()) - } - - override fun onUnlockedDeviceDetected() { - listener?.threatDetected(ThreatEvent.Passcode) - } - - override fun onHardwareBackedKeystoreNotAvailableDetected() { - listener?.threatDetected(ThreatEvent.SecureHardwareNotAvailable) - } - - override fun onDeveloperModeDetected() { - listener?.threatDetected(ThreatEvent.DevMode) - } - - override fun onADBEnabledDetected() { - listener?.threatDetected(ThreatEvent.ADBEnabled) - } - - override fun onSystemVPNDetected() { - listener?.threatDetected(ThreatEvent.SystemVPN) - } - - override fun onScreenshotDetected() { - listener?.threatDetected(ThreatEvent.Screenshot) - } - - override fun onScreenRecordingDetected() { - listener?.threatDetected(ThreatEvent.ScreenRecording) - } - - override fun onMultiInstanceDetected() { - listener?.threatDetected(ThreatEvent.MultiInstance) - } - - override fun onUnsecureWifiDetected() { - listener?.threatDetected(ThreatEvent.UnsecureWifi) - } - - override fun onTimeSpoofingDetected() { - listener?.threatDetected(ThreatEvent.TimeSpoofing) - } - - override fun onLocationSpoofingDetected() { - listener?.threatDetected(ThreatEvent.LocationSpoofing) - } - - override fun onAllChecksFinished() { - listener?.raspExecutionStateChanged(RaspExecutionStateEvent.AllChecksFinished) - } - - internal interface TalsecReactNative { - fun threatDetected(threatEventType: ThreatEvent) - - fun malwareDetected(suspiciousApps: MutableList) - - fun raspExecutionStateChanged(event: RaspExecutionStateEvent) - } -} diff --git a/android/src/main/java/com/freeraspreactnative/WrapperThreatHandler.kt b/android/src/main/java/com/freeraspreactnative/WrapperThreatHandler.kt new file mode 100644 index 0000000..00e519e --- /dev/null +++ b/android/src/main/java/com/freeraspreactnative/WrapperThreatHandler.kt @@ -0,0 +1,110 @@ +package com.freeraspreactnative + +import com.aheaditec.talsec_security.security.api.SuspiciousAppInfo +import com.aheaditec.talsec_security.security.api.ThreatListener +import com.freeraspreactnative.dispatchers.ExecutionStateDispatcher +import com.freeraspreactnative.dispatchers.ThreatDispatcher +import com.freeraspreactnative.events.RaspExecutionStateEvent +import com.freeraspreactnative.events.ThreatEvent + +internal object WrapperThreatHandler { + + internal val threatDispatcher = ThreatDispatcher() + internal val executionStateDispatcher = ExecutionStateDispatcher() + + internal 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) { + 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) + } + } + + internal 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) + } + } + + internal val raspExecutionState = object : ThreatListener.RaspExecutionState() { + override fun onAllChecksFinished() { + executionStateDispatcher.dispatch(RaspExecutionStateEvent.AllChecksFinished) + } + } +} diff --git a/android/src/main/java/com/freeraspreactnative/dispatchers/ExecutionStateDispatcher.kt b/android/src/main/java/com/freeraspreactnative/dispatchers/ExecutionStateDispatcher.kt new file mode 100644 index 0000000..d6e0de9 --- /dev/null +++ b/android/src/main/java/com/freeraspreactnative/dispatchers/ExecutionStateDispatcher.kt @@ -0,0 +1,36 @@ +package com.freeraspreactnative.dispatchers + +import com.freeraspreactnative.events.RaspExecutionStateEvent +import com.freeraspreactnative.interfaces.WrapperExecutionStateListener + +internal class ExecutionStateDispatcher { + private val cache = mutableSetOf() + + var listener: WrapperExecutionStateListener? = null + set(value) { + field = value + if (value != null) { + flushCache(value) + } + } + + fun dispatch(event: RaspExecutionStateEvent) { + val currentListener = listener + if (currentListener != null) { + currentListener.raspExecutionStateChanged(event) + } else { + synchronized(cache) { + val checkedListener = listener + checkedListener?.raspExecutionStateChanged(event) ?: cache.add(event) + } + } + } + + private fun flushCache(registeredListener: WrapperExecutionStateListener) { + synchronized(cache) { + cache.forEach { registeredListener.raspExecutionStateChanged(it) } + cache.clear() + } + } +} + diff --git a/android/src/main/java/com/freeraspreactnative/dispatchers/ThreatDispatcher.kt b/android/src/main/java/com/freeraspreactnative/dispatchers/ThreatDispatcher.kt new file mode 100644 index 0000000..9c89741 --- /dev/null +++ b/android/src/main/java/com/freeraspreactnative/dispatchers/ThreatDispatcher.kt @@ -0,0 +1,56 @@ +package com.freeraspreactnative.dispatchers + +import com.aheaditec.talsec_security.security.api.SuspiciousAppInfo +import com.freeraspreactnative.events.ThreatEvent +import com.freeraspreactnative.interfaces.WrapperThreatListener + +internal class ThreatDispatcher { + private val threatCache = mutableSetOf() + private val malwareCache = mutableSetOf() + + var listener: WrapperThreatListener? = null + set(value) { + field = value + if (value != null) { + flushCache(value) + } + } + + fun dispatchThreat(event: ThreatEvent) { + val currentListener = listener + if (currentListener != null) { + currentListener.threatDetected(event) + } else { + synchronized(threatCache) { + val checkedListener = listener + checkedListener?.threatDetected(event) ?: threatCache.add(event) + } + } + } + + fun dispatchMalware(apps: MutableList) { + val currentListener = listener + if (currentListener != null) { + currentListener.malwareDetected(apps) + } else { + synchronized(malwareCache) { + val checkedListener = listener + checkedListener?.malwareDetected(apps) ?: malwareCache.addAll(apps) + } + } + } + + private fun flushCache(registeredListener: WrapperThreatListener) { + synchronized(threatCache) { + threatCache.forEach { registeredListener.threatDetected(it) } + threatCache.clear() + } + synchronized(malwareCache) { + if (malwareCache.isNotEmpty()) { + registeredListener.malwareDetected(malwareCache.toMutableList()) + malwareCache.clear() + } + } + } +} + diff --git a/android/src/main/java/com/freeraspreactnative/events/ThreatEvent.kt b/android/src/main/java/com/freeraspreactnative/events/ThreatEvent.kt index c73de57..6f5f6ab 100644 --- a/android/src/main/java/com/freeraspreactnative/events/ThreatEvent.kt +++ b/android/src/main/java/com/freeraspreactnative/events/ThreatEvent.kt @@ -34,6 +34,7 @@ internal sealed class ThreatEvent(override val value: Int) : BaseRaspEvent { data object TimeSpoofing : ThreatEvent(RandomGenerator.next()) data object LocationSpoofing : ThreatEvent(RandomGenerator.next()) data object UnsecureWifi : ThreatEvent(RandomGenerator.next()) + data object Automation : ThreatEvent(RandomGenerator.next()) companion object { internal val CHANNEL_NAME = RandomGenerator.next().toString() @@ -61,7 +62,8 @@ internal sealed class ThreatEvent(override val value: Int) : BaseRaspEvent { MultiInstance, TimeSpoofing, LocationSpoofing, - UnsecureWifi + UnsecureWifi, + Automation ).map { it.value }) } } diff --git a/android/src/main/java/com/freeraspreactnative/interfaces/WrapperExecutionStateListener.kt b/android/src/main/java/com/freeraspreactnative/interfaces/WrapperExecutionStateListener.kt new file mode 100644 index 0000000..214324e --- /dev/null +++ b/android/src/main/java/com/freeraspreactnative/interfaces/WrapperExecutionStateListener.kt @@ -0,0 +1,7 @@ +package com.freeraspreactnative.interfaces + +import com.freeraspreactnative.events.RaspExecutionStateEvent + +internal interface WrapperExecutionStateListener { + fun raspExecutionStateChanged(event: RaspExecutionStateEvent) +} diff --git a/android/src/main/java/com/freeraspreactnative/interfaces/WrapperThreatListener.kt b/android/src/main/java/com/freeraspreactnative/interfaces/WrapperThreatListener.kt new file mode 100644 index 0000000..e6caaea --- /dev/null +++ b/android/src/main/java/com/freeraspreactnative/interfaces/WrapperThreatListener.kt @@ -0,0 +1,9 @@ +package com.freeraspreactnative.interfaces + +import com.aheaditec.talsec_security.security.api.SuspiciousAppInfo +import com.freeraspreactnative.events.ThreatEvent + +internal interface WrapperThreatListener { + fun threatDetected(threatEventType: ThreatEvent) + fun malwareDetected(suspiciousApps: MutableList) +} diff --git a/example/src/App.tsx b/example/src/App.tsx index 5b89c72..6f07242 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -251,6 +251,16 @@ const App = () => { ) ); }, + // Android only + automation: () => { + setAppChecks((currentState) => + currentState.map((threat) => + threat.name === 'Automation' + ? { ...threat, status: 'nok' } + : threat + ) + ); + }, }; const raspExecutionStateActions = { diff --git a/example/src/checks.ts b/example/src/checks.ts index ac1582b..d96ae02 100644 --- a/example/src/checks.ts +++ b/example/src/checks.ts @@ -24,4 +24,5 @@ export const androidChecks = [ { name: 'Time Spoofing', status: 'ok' }, { name: 'Location Spoofing', status: 'ok' }, { name: 'Unsecure Wifi', status: 'ok' }, + { name: 'Automation', status: 'ok' }, ]; diff --git a/example/tsconfig.json b/example/tsconfig.json index 2699cac..23473b4 100644 --- a/example/tsconfig.json +++ b/example/tsconfig.json @@ -1,6 +1,5 @@ { "compilerOptions": { - "baseUrl": "./", "paths": { "freerasp-react-native": ["../src/index"] }, @@ -12,7 +11,7 @@ "jsx": "react", "lib": ["esnext"], "module": "esnext", - "moduleResolution": "node", + "moduleResolution": "bundler", "noFallthroughCasesInSwitch": true, "noImplicitReturns": true, "noImplicitUseStrict": false, diff --git a/package.json b/package.json index acfff88..3997d39 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "freerasp-react-native", - "version": "4.3.2", + "version": "4.4.0", "description": "React Native plugin for improving app security and threat monitoring on Android and iOS mobile devices.", "main": "lib/commonjs/index", "module": "lib/module/index", diff --git a/src/api/listeners/raspExecutionState.ts b/src/api/listeners/raspExecutionState.ts index bbbb5df..c39ea18 100644 --- a/src/api/listeners/raspExecutionState.ts +++ b/src/api/listeners/raspExecutionState.ts @@ -1,6 +1,6 @@ import { NativeEventEmitter, type EmitterSubscription } from 'react-native'; import { FreeraspReactNative } from '../nativeModules'; -import { onInvalidCallback } from '../methods/native'; +import { onInvalidCallback, removeListenerForEvent } from '../methods/native'; import type { NativeEvent, RaspExecutionStateEventActions, @@ -12,19 +12,41 @@ import { import { RaspExecutionState } from '../../models/raspExecutionState'; const eventEmitter = new NativeEventEmitter(FreeraspReactNative); -let eventsListener: EmitterSubscription | undefined; +let eventsListener: EmitterSubscription | null = null; +let executionStateChannel: string | null = null; +let executionStateKey: string | null = null; + +let isMappingPrepared = false; +let isInitializing = false; export const setRaspExecutionStateListener = async ( config: RaspExecutionStateEventActions ) => { - const [channel, key] = await getRaspExecutionStateChannelData(); - await prepareRaspExecutionStateMapping(); + if (isInitializing) { + return; + } + + isInitializing = true; + + await removeRaspExecutionStateEventListener(); + + if (!executionStateChannel || !executionStateKey) { + [executionStateChannel, executionStateKey] = + await getRaspExecutionStateChannelData(); + } + + if (!isMappingPrepared) { + await prepareRaspExecutionStateMapping(); + isMappingPrepared = true; + } const listener = async (event: NativeEvent) => { - if (event[key] === undefined) { + if (!executionStateKey) { onInvalidCallback(); + return; } - switch (event[key]) { + + switch (event[executionStateKey]) { case RaspExecutionState.AllChecksFinished.value: config.allChecksFinished?.(); break; @@ -33,10 +55,16 @@ export const setRaspExecutionStateListener = async ( break; } }; - eventsListener = eventEmitter.addListener(channel, listener); + eventsListener = eventEmitter.addListener(executionStateChannel, listener); + isInitializing = false; }; -export const removeRaspExecutionStateEventListener = (): void => { - eventsListener?.remove(); - eventsListener = undefined; -}; +export const removeRaspExecutionStateEventListener = + async (): Promise => { + if (!eventsListener || !executionStateChannel) { + return; + } + await removeListenerForEvent(executionStateChannel); + eventsListener?.remove(); + eventsListener = null; + }; diff --git a/src/api/listeners/threat.ts b/src/api/listeners/threat.ts index c3fa9a1..cec54ee 100644 --- a/src/api/listeners/threat.ts +++ b/src/api/listeners/threat.ts @@ -1,6 +1,6 @@ import { NativeEventEmitter, type EmitterSubscription } from 'react-native'; import { FreeraspReactNative } from '../nativeModules'; -import { onInvalidCallback } from '../methods/native'; +import { onInvalidCallback, removeListenerForEvent } from '../methods/native'; import { Threat } from '../../models/threat'; import type { NativeEvent, ThreatEventActions } from '../../types/types'; import { parseMalwareData } from '../../utils/malware'; @@ -10,17 +10,40 @@ import { } from '../../channels/threat'; const eventEmitter = new NativeEventEmitter(FreeraspReactNative); -let eventsListener: EmitterSubscription | undefined; +let eventsListener: EmitterSubscription | null = null; + +let threatChannel: string | null = null; +let threatKey: string | null = null; +let threatMalwareKey: string | null = null; + +let isMappingPrepared = false; +let isInitializing = false; export const setThreatListeners = async (config: ThreatEventActions) => { - const [channel, key, malwareKey] = await getThreatChannelData(); - await prepareThreatMapping(); + if (isInitializing) { + return; + } + + isInitializing = true; + + await removeThreatListener(); + + if (!threatChannel || !threatKey || !threatMalwareKey) { + [threatChannel, threatKey, threatMalwareKey] = await getThreatChannelData(); + } + + if (!isMappingPrepared) { + await prepareThreatMapping(); + isMappingPrepared = true; + } const listener = async (event: NativeEvent) => { - if (event[key] === undefined) { + if (!threatKey || !threatMalwareKey) { onInvalidCallback(); + return; } - switch (event[key]) { + + switch (event[threatKey]) { case Threat.PrivilegedAccess.value: config.privilegedAccess?.(); break; @@ -61,7 +84,7 @@ export const setThreatListeners = async (config: ThreatEventActions) => { config.systemVPN?.(); break; case Threat.Malware.value: - const malwareData = event[malwareKey]; + const malwareData = event[threatMalwareKey]; config.malware?.( await parseMalwareData(Array.isArray(malwareData) ? malwareData : []) ); @@ -87,15 +110,23 @@ export const setThreatListeners = async (config: ThreatEventActions) => { case Threat.UnsecureWifi.value: config.unsecureWifi?.(); break; + case Threat.Automation.value: + config.automation?.(); + break; default: onInvalidCallback(); break; } }; - eventsListener = eventEmitter.addListener(channel, listener); + eventsListener = eventEmitter.addListener(threatChannel, listener); + isInitializing = false; }; -export const removeThreatListener = (): void => { +export const removeThreatListener = async (): Promise => { + if (!eventsListener || !threatChannel) { + return; + } + await removeListenerForEvent(threatChannel); eventsListener?.remove(); - eventsListener = undefined; + eventsListener = null; }; diff --git a/src/api/methods/native.ts b/src/api/methods/native.ts index 8496dc0..7db5fd1 100644 --- a/src/api/methods/native.ts +++ b/src/api/methods/native.ts @@ -37,3 +37,7 @@ export const getAppIcon = (packageName: string): Promise => { export const onInvalidCallback = (): void => { FreeraspReactNative.onInvalidCallback(); }; + +export const removeListenerForEvent = (channel: string): Promise => { + return FreeraspReactNative.removeListenerForEvent(channel); +}; diff --git a/src/api/methods/reactNative.ts b/src/api/methods/reactNative.ts index 17452e2..4379446 100644 --- a/src/api/methods/reactNative.ts +++ b/src/api/methods/reactNative.ts @@ -1,4 +1,4 @@ -import { talsecStart } from 'freerasp-react-native'; +import { talsecStart } from './native'; import { useEffect } from 'react'; import { setRaspExecutionStateListener, @@ -12,6 +12,8 @@ import type { } from '../../types/types'; import { onInvalidCallback } from './native'; +let isRaspStarted = false; + export const useFreeRasp = ( config: TalsecConfig, actions: ThreatEventActions, @@ -22,21 +24,28 @@ export const useFreeRasp = ( await setThreatListeners(actions); raspExecutionStateActions && (await setRaspExecutionStateListener(raspExecutionStateActions)); + + if (isRaspStarted) { + return; + } + try { - let response = await talsecStart(config); + const response = await talsecStart(config); if (response !== 'freeRASP started') { onInvalidCallback(); } - console.log(response); + isRaspStarted = true; } catch (e: any) { console.error(`${e.code}: ${e.message}`); } - - return () => { - removeThreatListener(); - removeRaspExecutionStateEventListener(); - }; })(); + + return () => { + (async () => { + await removeThreatListener(); + await removeRaspExecutionStateEventListener(); + })(); + }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); }; diff --git a/src/models/threat.ts b/src/models/threat.ts index d93ec33..0ebb768 100644 --- a/src/models/threat.ts +++ b/src/models/threat.ts @@ -24,6 +24,7 @@ export class Threat { static TimeSpoofing = new Threat(0); static LocationSpoofing = new Threat(0); static UnsecureWifi = new Threat(0); + static Automation = new Threat(0); constructor(value: number) { this.value = value; @@ -52,6 +53,7 @@ export class Threat { this.TimeSpoofing, this.LocationSpoofing, this.UnsecureWifi, + this.Automation, ] : [ this.AppIntegrity, diff --git a/src/types/types.ts b/src/types/types.ts index a289120..96f4d56 100644 --- a/src/types/types.ts +++ b/src/types/types.ts @@ -61,6 +61,7 @@ export type ThreatEventActions = { timeSpoofing?: () => any; locationSpoofing?: () => any; unsecureWifi?: () => any; + automation?: () => any; }; export type NativeEvent = { [key: string]: number | string[] | undefined }; diff --git a/tsconfig.json b/tsconfig.json index abf1cee..a4940c8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,7 +11,7 @@ "jsx": "react", "lib": ["esnext"], "module": "esnext", - "moduleResolution": "node", + "moduleResolution": "bundler", "noFallthroughCasesInSwitch": true, "noImplicitReturns": true, "noImplicitUseStrict": false,