Skip to content

Commit 69ae21f

Browse files
committed
feat(ts): add malware detection
1 parent 5800b96 commit 69ae21f

File tree

8 files changed

+275
-46
lines changed

8 files changed

+275
-46
lines changed

android/build.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ buildscript {
1111
classpath "com.android.tools.build:gradle:7.2.1"
1212
// noinspection DifferentKotlinGradleVersion
1313
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
14+
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
1415
}
1516
}
1617

@@ -20,6 +21,7 @@ def isNewArchitectureEnabled() {
2021

2122
apply plugin: "com.android.library"
2223
apply plugin: "kotlin-android"
24+
apply plugin: 'kotlinx-serialization'
2325

2426
if (isNewArchitectureEnabled()) {
2527
apply plugin: "com.facebook.react"
@@ -87,6 +89,7 @@ dependencies {
8789
//noinspection GradleDynamicVersion
8890
implementation "com.facebook.react:react-native:$react_native_version"
8991
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
92+
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1"
9093
implementation "com.aheaditec.talsec.security:TalsecSecurity-Community-ReactNative:11.1.3"
9194
}
9295

android/src/main/java/com/freeraspreactnative/FreeraspReactNativeModule.kt

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.freeraspreactnative
22

3+
import com.aheaditec.talsec_security.security.api.SuspiciousAppInfo
34
import com.aheaditec.talsec_security.security.api.Talsec
45
import com.aheaditec.talsec_security.security.api.TalsecConfig
56
import com.aheaditec.talsec_security.security.api.ThreatListener
@@ -12,8 +13,14 @@ import com.facebook.react.bridge.ReadableMap
1213
import com.facebook.react.bridge.UiThreadUtil.runOnUiThread
1314
import com.facebook.react.bridge.WritableArray
1415
import com.facebook.react.modules.core.DeviceEventManagerModule
15-
16-
class FreeraspReactNativeModule(val reactContext: ReactApplicationContext) :
16+
import com.freeraspreactnative.utils.getArraySafe
17+
import com.freeraspreactnative.utils.getBooleanSafe
18+
import com.freeraspreactnative.utils.getMapThrowing
19+
import com.freeraspreactnative.utils.getNestedArraySafe
20+
import com.freeraspreactnative.utils.getStringThrowing
21+
import com.freeraspreactnative.utils.toEncodedWritableArray
22+
23+
class FreeraspReactNativeModule(private val reactContext: ReactApplicationContext) :
1724
ReactContextBaseJavaModule(reactContext) {
1825

1926
private val listener = ThreatListener(FreeraspThreatHandler, FreeraspThreatHandler)
@@ -42,8 +49,7 @@ class FreeraspReactNativeModule(val reactContext: ReactApplicationContext) :
4249

4350
promise.resolve("freeRASP started")
4451

45-
}
46-
catch (e: Exception) {
52+
} catch (e: Exception) {
4753
promise.reject("TalsecInitializationError", e.message, e)
4854
}
4955
}
@@ -65,6 +71,7 @@ class FreeraspReactNativeModule(val reactContext: ReactApplicationContext) :
6571
val channelData: WritableArray = Arguments.createArray()
6672
channelData.pushString(THREAT_CHANNEL_NAME)
6773
channelData.pushString(THREAT_CHANNEL_KEY)
74+
channelData.pushString(MALWARE_CHANNEL_KEY)
6875
promise.resolve(channelData)
6976
}
7077

@@ -87,6 +94,15 @@ class FreeraspReactNativeModule(val reactContext: ReactApplicationContext) :
8794
// Remove upstream listeners, stop unnecessary background tasks
8895
}
8996

97+
/**
98+
* Method to add apps to Malware whitelist, so they don't get flagged as malware
99+
*/
100+
@ReactMethod
101+
fun addToWhitelist(packageName: String, promise: Promise) {
102+
Talsec.addToWhitelist(reactContext, packageName)
103+
promise.resolve(true)
104+
}
105+
90106
private fun buildTalsecConfig(config: ReadableMap): TalsecConfig {
91107
val androidConfig = config.getMapThrowing("androidConfig")
92108
val packageName = androidConfig.getStringThrowing("packageName")
@@ -97,6 +113,14 @@ class FreeraspReactNativeModule(val reactContext: ReactApplicationContext) :
97113
.supportedAlternativeStores(androidConfig.getArraySafe("supportedAlternativeStores"))
98114
.prod(config.getBooleanSafe("isProd"))
99115

116+
if (androidConfig.hasKey("malware")) {
117+
val malwareConfig = androidConfig.getMapThrowing("malware")
118+
talsecBuilder.whitelistedInstallationSources(malwareConfig.getArraySafe("whitelistedInstallationSources"))
119+
talsecBuilder.blocklistedHashes(malwareConfig.getArraySafe("blocklistedHashes"))
120+
talsecBuilder.blocklistedPermissions(malwareConfig.getNestedArraySafe("blocklistedPermissions"))
121+
talsecBuilder.blocklistedPackageNames(malwareConfig.getArraySafe("blocklistedPackageNames"))
122+
}
123+
100124
return talsecBuilder.build()
101125
}
102126

@@ -106,6 +130,8 @@ class FreeraspReactNativeModule(val reactContext: ReactApplicationContext) :
106130
.toString() // name of the channel over which threat callbacks are sent
107131
val THREAT_CHANNEL_KEY = (10000..999999999).random()
108132
.toString() // key of the argument map under which threats are expected
133+
val MALWARE_CHANNEL_KEY = (10000..999999999).random()
134+
.toString() // key of the argument map under which malware data is expected
109135
private lateinit var appReactContext: ReactApplicationContext
110136
private fun notifyListeners(threat: Threat) {
111137
val params = Arguments.createMap()
@@ -114,11 +140,30 @@ class FreeraspReactNativeModule(val reactContext: ReactApplicationContext) :
114140
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
115141
.emit(THREAT_CHANNEL_NAME, params)
116142
}
143+
144+
/**
145+
* Sends malware detected event to React Native
146+
*/
147+
private fun notifyMalware(suspiciousApps: MutableList<SuspiciousAppInfo>) {
148+
val params = Arguments.createMap()
149+
params.putInt(THREAT_CHANNEL_KEY, Threat.Malware.value)
150+
params.putArray(
151+
MALWARE_CHANNEL_KEY, suspiciousApps.toEncodedWritableArray(appReactContext)
152+
)
153+
154+
appReactContext
155+
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
156+
.emit(THREAT_CHANNEL_NAME, params)
157+
}
117158
}
118159

119160
internal object ThreatListener : FreeraspThreatHandler.TalsecReactNative {
120161
override fun threatDetected(threatType: Threat) {
121162
notifyListeners(threatType)
122163
}
164+
165+
override fun malwareDetected(suspiciousApps: MutableList<SuspiciousAppInfo>) {
166+
notifyMalware(suspiciousApps)
167+
}
123168
}
124169
}

android/src/main/java/com/freeraspreactnative/FreeraspThreatHandler.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@ internal object FreeraspThreatHandler : ThreatListener.ThreatDetected, ThreatLis
3939
listener?.threatDetected(Threat.ObfuscationIssues)
4040
}
4141

42-
override fun onMalwareDetected(p0: MutableList<SuspiciousAppInfo>?) {}
42+
override fun onMalwareDetected(suspiciousAppInfos: MutableList<SuspiciousAppInfo>?) {
43+
listener?.malwareDetected(suspiciousAppInfos ?: mutableListOf())
44+
}
4345

4446
override fun onUnlockedDeviceDetected() {
4547
listener?.threatDetected(Threat.Passcode)
@@ -59,5 +61,7 @@ internal object FreeraspThreatHandler : ThreatListener.ThreatDetected, ThreatLis
5961

6062
internal interface TalsecReactNative {
6163
fun threatDetected(threatType: Threat)
64+
65+
fun malwareDetected(suspiciousApps: MutableList<SuspiciousAppInfo>)
6266
}
6367
}

android/src/main/java/com/freeraspreactnative/Threat.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ internal sealed class Threat(val value: Int) {
2323
object ObfuscationIssues : Threat((10000..999999999).random())
2424
object SystemVPN : Threat((10000..999999999).random())
2525
object DevMode : Threat((10000..999999999).random())
26+
object Malware : Threat((10000..999999999).random())
2627

2728
companion object {
2829
internal fun getThreatValues(): WritableArray {
@@ -39,7 +40,8 @@ internal sealed class Threat(val value: Int) {
3940
DeviceBinding.value,
4041
UnofficialStore.value,
4142
ObfuscationIssues.value,
42-
DevMode.value
43+
DevMode.value,
44+
Malware.value
4345
)
4446
)
4547
}

android/src/main/java/com/freeraspreactnative/Utils.kt

Lines changed: 0 additions & 40 deletions
This file was deleted.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package com.freeraspreactnative.models
2+
3+
import kotlinx.serialization.Serializable
4+
5+
6+
/**
7+
* Simplified, serializable wrapper for Talsec's SuspiciousAppInfo
8+
*/
9+
@Serializable
10+
data class RNSuspiciousAppInfo(
11+
val packageInfo: RNPackageInfo,
12+
val reason: String,
13+
)
14+
15+
/**
16+
* Simplified, serializable wrapper for Android's PackageInfo
17+
*/
18+
@Serializable
19+
data class RNPackageInfo(
20+
val packageName: String,
21+
val appName: String?,
22+
val version: String?,
23+
val appIcon: String?,
24+
val installerStore: String?
25+
)
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package com.freeraspreactnative.utils
2+
3+
import android.content.pm.PackageInfo
4+
import android.util.Base64
5+
import android.util.Log
6+
import com.aheaditec.talsec_security.security.api.SuspiciousAppInfo
7+
import com.facebook.react.bridge.Arguments
8+
import com.facebook.react.bridge.ReactContext
9+
import com.facebook.react.bridge.ReadableArray
10+
import com.facebook.react.bridge.ReadableMap
11+
import com.facebook.react.bridge.WritableArray
12+
import com.freeraspreactnative.exceptions.TalsecException
13+
import com.freeraspreactnative.models.RNPackageInfo
14+
import com.freeraspreactnative.models.RNSuspiciousAppInfo
15+
import kotlinx.serialization.encodeToString
16+
import kotlinx.serialization.json.Json
17+
18+
19+
internal fun ReadableMap.getMapThrowing(key: String): ReadableMap {
20+
return this.getMap(key) ?: throw TalsecException("Key missing in configuration: $key")
21+
}
22+
23+
internal fun ReadableMap.getStringThrowing(key: String): String {
24+
return this.getString(key) ?: throw TalsecException("Key missing in configuration: $key")
25+
}
26+
27+
internal fun ReadableMap.getBooleanSafe(key: String, defaultValue: Boolean = true): Boolean {
28+
if (this.hasKey(key)) {
29+
return this.getBoolean(key)
30+
}
31+
return defaultValue
32+
}
33+
34+
internal fun ReadableArray.toArray(): Array<String> {
35+
val output = mutableListOf<String>()
36+
for (i in 0 until this.size()) {
37+
// in RN versions < 0.63, getString is nullable
38+
@Suppress("UNNECESSARY_SAFE_CALL")
39+
this.getString(i)?.let {
40+
output.add(it)
41+
}
42+
}
43+
return output.toTypedArray()
44+
}
45+
46+
internal fun ReadableMap.getArraySafe(key: String): Array<String> {
47+
if (this.hasKey(key)) {
48+
val inputArray = this.getArray(key)!!
49+
return inputArray.toArray()
50+
}
51+
return arrayOf()
52+
}
53+
54+
internal fun ReadableMap.getNestedArraySafe(key: String): Array<Array<String>> {
55+
val outArray = mutableListOf<Array<String>>()
56+
if (this.hasKey(key)) {
57+
val inputArray = this.getArray(key)!!
58+
for (i in 0 until inputArray.size()) {
59+
outArray.add(inputArray.getArray(i).toArray())
60+
}
61+
}
62+
return outArray.toTypedArray()
63+
}
64+
65+
66+
/**
67+
* Converts the Talsec's SuspiciousAppInfo to React Native equivalent
68+
*/
69+
internal fun SuspiciousAppInfo.toRNSuspiciousAppInfo(context: ReactContext): RNSuspiciousAppInfo {
70+
return RNSuspiciousAppInfo(
71+
packageInfo = this.packageInfo.toRNPackageInfo(context),
72+
reason = this.reason,
73+
)
74+
}
75+
76+
/**
77+
* Converts the Android's PackageInfo to React Native equivalent
78+
*/
79+
internal fun PackageInfo.toRNPackageInfo(context: ReactContext): RNPackageInfo {
80+
return RNPackageInfo(
81+
packageName = this.packageName,
82+
appName = Utils.getAppName(context, this.applicationInfo),
83+
version = this.versionName,
84+
appIcon = Utils.getAppIconAsBase64String(context, this.packageName),
85+
installerStore = Utils.getInstallationSource(context, this.packageName)
86+
)
87+
}
88+
89+
/**
90+
* Convert the Talsec's SuspiciousAppInfo to base64-encoded json array,
91+
* which can be then sent to React Native
92+
*/
93+
internal fun MutableList<SuspiciousAppInfo>.toEncodedWritableArray(context: ReactContext): WritableArray {
94+
val output = Arguments.createArray()
95+
this.forEach { suspiciousAppInfo ->
96+
val rnSuspiciousAppInfo = suspiciousAppInfo.toRNSuspiciousAppInfo(context)
97+
try {
98+
val encodedAppInfo =
99+
Base64.encodeToString(
100+
Json.encodeToString(rnSuspiciousAppInfo).toByteArray(),
101+
Base64.DEFAULT
102+
)
103+
output.pushString(encodedAppInfo)
104+
} catch (e: Exception) {
105+
Log.e("Talsec", "Could not serialize suspicious app data: ${e.message}")
106+
}
107+
108+
}
109+
return output
110+
}
111+

0 commit comments

Comments
 (0)