diff --git a/CHANGELOG.md b/CHANGELOG.md index 12d0897..c0e10e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,24 @@ 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). +## [3.12.0] - 2024-12-06 + +- iOS SDK version: 6.6.3 +- Android SDK version: 13.0.0 + +### React Native + +#### Changed + +- App icons for detected malware are not fetched automatically anymore, which reduces computation required to retrieve malware data. From now on, app icons have to be retrieved using the `getAppIcon` method +- Parsing of malware data is now async + +### Android + +#### Changed + +- Malware data is now parsed on background thread to improve responsiveness + ## [3.11.0] - 2024-11-19 ### React Native diff --git a/android/src/main/java/com/freeraspreactnative/FreeraspReactNativeModule.kt b/android/src/main/java/com/freeraspreactnative/FreeraspReactNativeModule.kt index e55b205..71bf610 100644 --- a/android/src/main/java/com/freeraspreactnative/FreeraspReactNativeModule.kt +++ b/android/src/main/java/com/freeraspreactnative/FreeraspReactNativeModule.kt @@ -1,10 +1,14 @@ package com.freeraspreactnative +import android.os.Handler +import android.os.HandlerThread +import android.os.Looper 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.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 @@ -13,6 +17,7 @@ import com.facebook.react.bridge.ReadableMap import com.facebook.react.bridge.UiThreadUtil.runOnUiThread import com.facebook.react.bridge.WritableArray import com.facebook.react.modules.core.DeviceEventManagerModule +import com.freeraspreactnative.utils.Utils import com.freeraspreactnative.utils.getArraySafe import com.freeraspreactnative.utils.getBooleanSafe import com.freeraspreactnative.utils.getMapThrowing @@ -24,6 +29,19 @@ class FreeraspReactNativeModule(private val reactContext: ReactApplicationContex ReactContextBaseJavaModule(reactContext) { private val listener = ThreatListener(FreeraspThreatHandler, FreeraspThreatHandler) + private val lifecycleListener = object : LifecycleEventListener { + override fun onHostResume() { + // do nothing + } + + override fun onHostPause() { + // do nothing + } + + override fun onHostDestroy() { + backgroundHandlerThread.quitSafely() + } + } override fun getName(): String { return NAME @@ -31,6 +49,7 @@ class FreeraspReactNativeModule(private val reactContext: ReactApplicationContex init { appReactContext = reactContext + reactContext.addLifecycleEventListener(lifecycleListener) } @ReactMethod @@ -103,6 +122,20 @@ class FreeraspReactNativeModule(private val reactContext: ReactApplicationContex 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) } + } + } + private fun buildTalsecConfig(config: ReadableMap): TalsecConfig { val androidConfig = config.getMapThrowing("androidConfig") val packageName = androidConfig.getStringThrowing("packageName") @@ -126,13 +159,19 @@ class FreeraspReactNativeModule(private val reactContext: ReactApplicationContex companion object { const val NAME = "FreeraspReactNative" - val THREAT_CHANNEL_NAME = (10000..999999999).random() + private val THREAT_CHANNEL_NAME = (10000..999999999).random() .toString() // name of the channel over which threat callbacks are sent - val THREAT_CHANNEL_KEY = (10000..999999999).random() + private val THREAT_CHANNEL_KEY = (10000..999999999).random() .toString() // key of the argument map under which threats are expected - val MALWARE_CHANNEL_KEY = (10000..999999999).random() + 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()) + private lateinit var appReactContext: ReactApplicationContext + private fun notifyListeners(threat: Threat) { val params = Arguments.createMap() params.putInt(THREAT_CHANNEL_KEY, threat.value) @@ -145,15 +184,23 @@ class FreeraspReactNativeModule(private val reactContext: ReactApplicationContex * Sends malware detected event to React Native */ private fun notifyMalware(suspiciousApps: MutableList) { - val params = Arguments.createMap() - params.putInt(THREAT_CHANNEL_KEY, Threat.Malware.value) - params.putArray( - MALWARE_CHANNEL_KEY, suspiciousApps.toEncodedWritableArray(appReactContext) - ) - - appReactContext - .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) - .emit(THREAT_CHANNEL_NAME, params) + // Perform the malware encoding on a background thread + backgroundHandler.post { + + val encodedSuspiciousApps = suspiciousApps.toEncodedWritableArray(appReactContext) + + mainHandler.post { + val params = Arguments.createMap() + params.putInt(THREAT_CHANNEL_KEY, Threat.Malware.value) + params.putArray( + MALWARE_CHANNEL_KEY, encodedSuspiciousApps + ) + + appReactContext + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java) + .emit(THREAT_CHANNEL_NAME, params) + } + } } } diff --git a/android/src/main/java/com/freeraspreactnative/utils/Extensions.kt b/android/src/main/java/com/freeraspreactnative/utils/Extensions.kt index 85cb803..fec90eb 100644 --- a/android/src/main/java/com/freeraspreactnative/utils/Extensions.kt +++ b/android/src/main/java/com/freeraspreactnative/utils/Extensions.kt @@ -86,7 +86,7 @@ internal fun PackageInfo.toRNPackageInfo(context: ReactContext): RNPackageInfo { packageName = this.packageName, appName = Utils.getAppName(context, this.applicationInfo), version = this.versionName, - appIcon = Utils.getAppIconAsBase64String(context, this.packageName), + appIcon = null, // this requires heavier computations, so appIcon has to be retrieved separately installerStore = Utils.getInstallationSource(context, this.packageName) ) } diff --git a/android/src/main/java/com/freeraspreactnative/utils/Utils.kt b/android/src/main/java/com/freeraspreactnative/utils/Utils.kt index 0bcc513..e523b64 100644 --- a/android/src/main/java/com/freeraspreactnative/utils/Utils.kt +++ b/android/src/main/java/com/freeraspreactnative/utils/Utils.kt @@ -75,7 +75,10 @@ internal object Utils { context.packageManager.getInstallerPackageName(packageName) } } catch (e: Exception) { - Log.e("Talsec", "Could not retrieve app installation source for ${packageName}: ${e.message}") + Log.e( + "Talsec", + "Could not retrieve app installation source for ${packageName}: ${e.message}", + ) null } } diff --git a/example/src/MalwareItem.tsx b/example/src/MalwareItem.tsx index aa5fb04..24973eb 100644 --- a/example/src/MalwareItem.tsx +++ b/example/src/MalwareItem.tsx @@ -1,8 +1,12 @@ import { Button, HStack } from '@react-native-material/core'; -import { addToWhitelist, type SuspiciousAppInfo } from 'freerasp-react-native'; +import { + addToWhitelist, + getAppIcon, + type SuspiciousAppInfo, +} from 'freerasp-react-native'; import ArrowUp from '../assets/arrow-up.png'; import ArrowDown from '../assets/arrow-down.png'; -import React from 'react'; +import React, { useEffect } from 'react'; import { useState } from 'react'; import { TouchableOpacity, View, Text, Image, StyleSheet } from 'react-native'; import { Colors } from './styles'; @@ -10,6 +14,14 @@ import { Colors } from './styles'; export const MalwareItem: React.FC<{ app: SuspiciousAppInfo }> = ({ app }) => { const [expanded, setExpanded] = useState(false); + useEffect(() => { + (async () => { + // retrieve app icons for detected malware + const appIcon = await getAppIcon(app.packageInfo.packageName); + app.packageInfo.appIcon = appIcon; + })(); + }, [app.packageInfo]); + const appUninstall = async () => { alert('Implement yourself!'); }; @@ -94,19 +106,6 @@ export const MalwareItem: React.FC<{ app: SuspiciousAppInfo }> = ({ app }) => { }; const styles = StyleSheet.create({ - button: { - borderRadius: 20, - paddingHorizontal: 30, - paddingVertical: 10, - marginTop: 15, - elevation: 2, - }, - buttonOpen: { - backgroundColor: '#F194FF', - }, - buttonClose: { - backgroundColor: '#2196F3', - }, item: { backgroundColor: '#d4e4ff', borderRadius: 20, diff --git a/package.json b/package.json index c819046..a27b782 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "freerasp-react-native", - "version": "3.11.0", + "version": "3.12.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/index.tsx b/src/index.tsx index 74f0908..9332a2c 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -54,8 +54,16 @@ const prepareMapping = async (): Promise => { }; // parses base64-encoded malware data to SuspiciousAppInfo[] -const parseMalwareData = (data: string[]): SuspiciousAppInfo[] => { - return data.map((entry) => toSuspiciousAppInfo(entry)); +const parseMalwareData = async ( + data: string[] +): Promise => { + return new Promise((resolve, reject) => { + try { + resolve(data.map((entry) => toSuspiciousAppInfo(entry))); + } catch (error: any) { + reject(`Error while parsing app data: ${error}`); + } + }); }; const toSuspiciousAppInfo = (base64Value: string): SuspiciousAppInfo => { @@ -70,7 +78,7 @@ export const setThreatListeners = async ( const [channel, key, malwareKey] = await getThreatChannelData(); await prepareMapping(); - eventsListener = eventEmitter.addListener(channel, (event) => { + eventsListener = eventEmitter.addListener(channel, async (event) => { if (event[key] === undefined) { onInvalidCallback(); } @@ -115,7 +123,7 @@ export const setThreatListeners = async ( config.systemVPN?.(); break; case Threat.Malware.value: - config.malware?.(parseMalwareData(event[malwareKey])); + config.malware?.(await parseMalwareData(event[malwareKey])); break; case Threat.ADBEnabled.value: config.adbEnabled?.(); @@ -167,5 +175,14 @@ export const addToWhitelist = async (packageName: string): Promise => { return FreeraspReactNative.addToWhitelist(packageName); }; +export const getAppIcon = (packageName: string): Promise => { + if (Platform.OS === 'ios') { + return Promise.reject( + 'App icon retrieval for Malware detection not available on iOS' + ); + } + return FreeraspReactNative.getAppIcon(packageName); +}; + export * from './types'; export default FreeraspReactNative;