package com.freeraspreactnative import android.os.Build import android.os.Handler import android.os.HandlerThread import android.os.Looper import com.aheaditec.talsec_security.security.api.ExternalIdResult 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.TalsecMode import com.facebook.react.bridge.Arguments import com.facebook.react.bridge.LifecycleEventListener import com.facebook.react.bridge.Promise import com.facebook.react.bridge.ReactApplicationContext import com.facebook.react.bridge.ReactContextBaseJavaModule import com.facebook.react.bridge.ReactMethod import com.facebook.react.bridge.ReadableMap 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 class FreeraspReactNativeModule(private val reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) { private val lifecycleListener = object : LifecycleEventListener { override fun onHostResume() { ThreatDispatcher.onResume() ExecutionStateDispatcher.onResume() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { reactContext.currentActivity?.let { ScreenProtector.register(it) } } } override fun onHostPause() { ThreatDispatcher.onPause() ExecutionStateDispatcher.onPause() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { reactContext.currentActivity?.let { ScreenProtector.unregister(it) } } if (reactContext.currentActivity?.isFinishing == true) { ThreatDispatcher.unregisterListener() ExecutionStateDispatcher.unregisterListener() PluginThreatHandler.unregisterSDKListener(reactContext) } } override fun onHostDestroy() { backgroundHandlerThread.quitSafely() } } override fun getName(): String { return NAME } override fun initialize() { reactContext.addLifecycleEventListener(lifecycleListener) initializeEventKeys() super.initialize() } @ReactMethod fun talsecStart( options: ReadableMap, promise: Promise ) { if (talsecStarted) { promise.resolve("freeRASP started") return } try { val config = buildTalsecConfig(options) PluginThreatHandler.registerSDKListener(reactContext) runOnUiThread { Talsec.start(reactContext, config, TalsecMode.BACKGROUND) mainHandler.post { talsecStarted = true // This code must be called only AFTER Talsec.start reactContext.currentActivity?.let { activity -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { ScreenProtector.register(activity) } } promise.resolve("freeRASP started") } } } catch (e: Exception) { promise.reject("TalsecInitializationError", e.message, e) } } // Trigger lazy initialization of the freeRASP events private fun initializeEventKeys() { ThreatEvent.ALL_EVENTS RaspExecutionStateEvent.ALL_EVENTS } /** * Method to get the random identifiers of callbacks */ @ReactMethod fun getThreatIdentifiers(promise: Promise) { promise.resolve(ThreatEvent.ALL_EVENTS) } /** * Method to get the random identifiers of callbacks */ @ReactMethod fun getRaspExecutionStateIdentifiers(promise: Promise) { promise.resolve(RaspExecutionStateEvent.ALL_EVENTS) } /** * Method to setup the threat message passing between native and React Native * @return list of [THREAT_CHANNEL_NAME, THREAT_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) promise.resolve(channelData) } /** * Method to setup the execution state message passing between native and React Native * @return list of [THREAT_CHANNEL_NAME, THREAT_CHANNEL_KEY] */ @ReactMethod fun getRaspExecutionStateChannelData(promise: Promise) { val channelData: WritableArray = Arguments.createArray() channelData.pushString(RaspExecutionStateEvent.CHANNEL_NAME) channelData.pushString(RaspExecutionStateEvent.CHANNEL_KEY) promise.resolve(channelData) } /** * We never send an invalid callback over our channel. * Therefore, if this happens, we want to kill the app. */ @ReactMethod fun onInvalidCallback() { android.os.Process.killProcess(android.os.Process.myPid()) } @ReactMethod fun addListener(eventName: String) { if (eventName == ThreatEvent.CHANNEL_NAME) { ThreatDispatcher.registerListener(PluginListener(reactContext)) } if (eventName == RaspExecutionStateEvent.CHANNEL_NAME) { ExecutionStateDispatcher.registerListener(PluginListener(reactContext)) } } @ReactMethod fun removeListeners(@Suppress("UNUSED_PARAMETER") count: Int) { // 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) { ThreatDispatcher.unregisterListener() } if (eventName == RaspExecutionStateEvent.CHANNEL_NAME) { ExecutionStateDispatcher.unregisterListener() } promise.resolve("Listener unregistered") } /** * Method to add apps to Malware whitelist, so they don't get flagged as malware */ @ReactMethod fun addToWhitelist(packageName: String, promise: Promise) { Talsec.addToWhitelist(reactContext, packageName) promise.resolve(true) } /** * Method retrieves app icon for the given parameter * @param packageName package name of the app we want to retrieve icon for * @return PNG with app icon encoded as a base64 string */ @ReactMethod fun getAppIcon(packageName: String, promise: Promise) { // Perform the app icon encoding on a background thread backgroundHandler.post { val encodedData = Utils.getAppIconAsBase64String(reactContext, packageName) mainHandler.post { promise.resolve(encodedData) } } } /** * Method Block/Unblock screen capture * @param enable boolean for whether want to block or unblock the screen capture */ @ReactMethod fun blockScreenCapture(enable: Boolean, promise: Promise) { val activity = reactContext.currentActivity ?: run { promise.reject( "NativePluginError", "Cannot block screen capture, activity is null." ) return } runOnUiThread { try { Talsec.blockScreenCapture(activity, enable) promise.resolve("Screen capture is now ${if (enable) "Blocked" else "Enabled"}.") } catch (e: Exception) { promise.reject("NativePluginError", "Error in blockScreenCapture: ${e.message}") } } } /** * Method Returns whether screen capture is blocked or not * @return boolean for is screem capture blocked or not */ @ReactMethod fun isScreenCaptureBlocked(promise: Promise) { try { val isBlocked = Talsec.isScreenCaptureBlocked() promise.resolve(isBlocked) } catch (e: Exception) { promise.reject("NativePluginError", "Error in isScreenCaptureBlocked: ${e.message}") } } @ReactMethod fun storeExternalId( externalId: String, promise: Promise ) { try { when (val result = Talsec.storeExternalId(reactContext, externalId)) { is ExternalIdResult.Error -> { promise.reject( "ExternalIdError", "Setting up External ID failed - ${result.errorMsg}" ) } is ExternalIdResult.Success -> { promise.resolve("OK - Store external ID") return } } } catch (e: Exception) { promise.reject( "NativePluginError", "Error during storeExternalId operation in Talsec Native Plugin" ) } } @ReactMethod fun removeExternalId(promise: Promise) { try { Talsec.removeExternalId(reactContext) promise.resolve("OK - External ID removed") } catch (e: Exception) { promise.reject( "NativePluginError", "Error during storeExternalId operation in Talsec Native Plugin" ) } } private fun buildTalsecConfig(config: ReadableMap): TalsecConfig { val androidConfig = config.getMapThrowing("androidConfig") val packageName = androidConfig.getStringThrowing("packageName") val certificateHashes = androidConfig.getArraySafe("certificateHashes") val talsecBuilder = TalsecConfig.Builder(packageName, certificateHashes) .watcherMail(config.getString("watcherMail")) .prod(config.getBooleanSafe("isProd")) .killOnBypass(config.getBooleanSafe("killOnBypass", false)) .supportedAlternativeStores(androidConfig.getArraySafe("supportedAlternativeStores")) if (androidConfig.hasKey("suspiciousAppDetectionConfig")) { val suspiciousAppConfig = androidConfig.getMapThrowing("suspiciousAppDetectionConfig") talsecBuilder.suspiciousAppDetection(suspiciousAppConfig.toSuspiciousAppDetectionConfig()) } return talsecBuilder.build() } companion object { const val NAME = "FreeraspReactNative" private val backgroundHandlerThread = HandlerThread("BackgroundThread").apply { start() } private val backgroundHandler = Handler(backgroundHandlerThread.looper) private val mainHandler = Handler(Looper.getMainLooper()) internal var talsecStarted = false } internal class PluginListener(private val reactContext: ReactApplicationContext) : PluginThreatListener, PluginExecutionStateListener { override fun threatDetected(threatEventType: ThreatEvent) { val params = Arguments.createMap() params.putInt(threatEventType.channelKey, threatEventType.value) notifyEvent(ThreatEvent.CHANNEL_NAME, params) } override fun malwareDetected(suspiciousApps: MutableList) { backgroundHandler.post { val encodedSuspiciousApps = suspiciousApps.toEncodedWritableArray(reactContext) mainHandler.post { val params = Arguments.createMap() params.putInt(ThreatEvent.CHANNEL_KEY, ThreatEvent.Malware.value) params.putArray( ThreatEvent.MALWARE_CHANNEL_KEY, encodedSuspiciousApps ) notifyEvent(ThreatEvent.CHANNEL_NAME, params) } } } override fun raspExecutionStateChanged(event: RaspExecutionStateEvent) { val params = Arguments.createMap() params.putInt(event.channelKey, event.value) notifyEvent(event.channelName, params) } private fun notifyEvent(eventName: String, params: WritableMap) { reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) .emit(eventName, params) } } }