Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
node_modules/
lib/
example/
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ android.iml
**/android/**/build
**/android/**/release
**/android/**/debug
!**/android/app/src/debug/AndroidManixest.xml

# Cocoapods
#
Expand Down
44 changes: 44 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,50 @@ 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.14.0] - 2024-03-05

- iOS SDK version: 6.8.0
- Android SDK version: 14.0.1

### React Native

#### Added

- `blockScreenCapture` method to block/unblock screen capture
- `isScreenCaptureBlocked` method to get the current screen capture blocking status
- New callbacks:
- `screenshot`: Detects when a screenshot is taken
- `screenRecording`: Detects when screen recording is active

#### Changed

- Raised Android compileSDK level to 35

#### Fixed

- Compatibility issues with RN New Architecture
- Added proguard rules for malware data serialization in release mode on Android

### Android

#### Added

- Passive and active screenshot/screen recording protection

#### Changed

- Improved root detection

#### Fixed

- Proguard rules to address warnings from okhttp dependency

### iOS

#### Added

- Passive Screenshot/Screen Recording detection

## [3.13.0] - 2024-12-20

- iOS SDK version: 6.6.3
Expand Down
2 changes: 1 addition & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,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:13.2.0"
implementation "com.aheaditec.talsec.security:TalsecSecurity-Community-ReactNative:14.0.1"
}

if (isNewArchitectureEnabled()) {
Expand Down
4 changes: 2 additions & 2 deletions android/gradle.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
FreeraspReactNative_kotlinVersion=1.7.0
FreeraspReactNative_minSdkVersion=23
FreeraspReactNative_targetSdkVersion=31
FreeraspReactNative_compileSdkVersion=33
FreeraspReactNative_targetSdkVersion=35
FreeraspReactNative_compileSdkVersion=35
FreeraspReactNative_ndkversion=21.4.7075529
FreeraspReactNative_reactNativeVersion=+
8 changes: 8 additions & 0 deletions android/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -1,2 +1,10 @@
-dontwarn java.lang.invoke.StringConcatFactory
-keep class com.freeraspreactnative.FreeraspReactNativePackage { public *; }

-if @kotlinx.serialization.Serializable class **
-keep class <1> {
*;
}

-keep class com.freeraspreactnative.models.RNSuspiciousAppInfo$Companion
-keep class com.freeraspreactnative.models.RNPackageInfo$Companion
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.freeraspreactnative

import android.os.Build
import android.os.Handler
import android.os.HandlerThread
import android.os.Looper
Expand Down Expand Up @@ -31,11 +32,15 @@ class FreeraspReactNativeModule(private val reactContext: ReactApplicationContex
private val listener = ThreatListener(FreeraspThreatHandler, FreeraspThreatHandler)
private val lifecycleListener = object : LifecycleEventListener {
override fun onHostResume() {
// do nothing
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
currentActivity?.let { ScreenProtector.register(it) }
}
}

override fun onHostPause() {
// do nothing
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
currentActivity?.let { ScreenProtector.unregister(it) }
}
}

override fun onHostDestroy() {
Expand All @@ -54,8 +59,7 @@ class FreeraspReactNativeModule(private val reactContext: ReactApplicationContex

@ReactMethod
fun talsecStart(
options: ReadableMap,
promise: Promise
options: ReadableMap, promise: Promise
) {

try {
Expand All @@ -64,10 +68,18 @@ class FreeraspReactNativeModule(private val reactContext: ReactApplicationContex
listener.registerListener(reactContext)
runOnUiThread {
Talsec.start(reactContext, config)
mainHandler.post {
talsecStarted = true
// This code must be called only AFTER Talsec.start
currentActivity?.let { activity ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
ScreenProtector.register(activity)
}
}
promise.resolve("freeRASP started")
}
}

promise.resolve("freeRASP started")

} catch (e: Exception) {
promise.reject("TalsecInitializationError", e.message, e)
}
Expand Down Expand Up @@ -136,6 +148,43 @@ class FreeraspReactNativeModule(private val reactContext: ReactApplicationContex
}
}

/**
* 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 = 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}")
}
}

private fun buildTalsecConfig(config: ReadableMap): TalsecConfig {
val androidConfig = config.getMapThrowing("androidConfig")
val packageName = androidConfig.getStringThrowing("packageName")
Expand Down Expand Up @@ -172,11 +221,12 @@ class FreeraspReactNativeModule(private val reactContext: ReactApplicationContex

private lateinit var appReactContext: ReactApplicationContext

internal var talsecStarted = false

private fun notifyListeners(threat: Threat) {
val params = Arguments.createMap()
params.putInt(THREAT_CHANNEL_KEY, threat.value)
appReactContext
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
appReactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
.emit(THREAT_CHANNEL_NAME, params)
}

Expand All @@ -196,8 +246,7 @@ class FreeraspReactNativeModule(private val reactContext: ReactApplicationContex
MALWARE_CHANNEL_KEY, encodedSuspiciousApps
)

appReactContext
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
appReactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
.emit(THREAT_CHANNEL_NAME, params)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,14 @@ internal object FreeraspThreatHandler : ThreatListener.ThreatDetected, ThreatLis
listener?.threatDetected(Threat.SystemVPN)
}

override fun onScreenshotDetected() {
listener?.threatDetected(Threat.Screenshot)
}

override fun onScreenRecordingDetected() {
listener?.threatDetected(Threat.ScreenRecording)
}

internal interface TalsecReactNative {
fun threatDetected(threatType: Threat)

Expand Down
164 changes: 164 additions & 0 deletions android/src/main/java/com/freeraspreactnative/ScreenProtector.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
package com.freeraspreactnative

import android.annotation.SuppressLint
import android.annotation.TargetApi
import android.app.Activity
import android.app.Activity.ScreenCaptureCallback
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import android.util.Log
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 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 screenRecordCallback: Consumer<Int> = Consumer<Int> { state ->
if (state == SCREEN_RECORDING_STATE_VISIBLE) {
Talsec.onScreenRecordingDetected()
}
}

/**
* Registers screenshot and screen recording detector with the given activity
*
* **IMPORTANT**: android.permission.DETECT_SCREEN_CAPTURE and
* android.permission.DETECT_SCREEN_RECORDING must be
* granted for the app in the AndroidManifest.xml
*/
internal fun register(activity: Activity) {
if (!FreeraspReactNativeModule.talsecStarted || registered) {
return
}

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
registerScreenCapture(activity)
}

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
registerScreenRecording(activity)
}
registered = true
}

/**
* Register Talsec Screen Capture (screenshot) Detector for given activity instance.
* The MainActivity of the app is registered by the plugin itself, other
* activities bust be registered manually as described in the integration guide.
*
* Missing permission is suppressed because the decision to use the screen
* capture API is made by developer, and not enforced by the library.
*
* **IMPORTANT**: android.permission.DETECT_SCREEN_CAPTURE (API 34+) must be
* granted for the app in the AndroidManifest.xml
*/
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
@SuppressLint("MissingPermission")
private fun registerScreenCapture(activity: Activity) {
val context = activity.applicationContext
if (!hasPermission(context, SCREEN_CAPTURE_PERMISSION)) {
reportMissingPermission("screenshot", SCREEN_CAPTURE_PERMISSION)
return
}

activity.registerScreenCaptureCallback(context.mainExecutor, screenCaptureCallback)
}

/**
* Register Talsec Screen Recording Detector for given activity instance.
* The MainActivity of the app is registered by the plugin itself, other
* activities bust be registered manually as described in the integration guide.
*
* Missing permission is suppressed because the decision to use the screen
* capture API is made by developer, and not enforced by the library.
*
* **IMPORTANT**: android.permission.DETECT_SCREEN_RECORDING (API 35+) must be
* granted for the app in the AndroidManifest.xml
*/
@SuppressLint("MissingPermission")
@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
private fun registerScreenRecording(activity: Activity) {
val context = activity.applicationContext
if (!hasPermission(context, SCREEN_RECORDING_PERMISSION)) {
reportMissingPermission("screen record", SCREEN_RECORDING_PERMISSION)
return
}

val initialState = activity.windowManager.addScreenRecordingCallback(
context.mainExecutor, screenRecordCallback
)
screenRecordCallback.accept(initialState)

}

/**
* Unregisters screenshot and screen recording detector with the given activity
*
* **IMPORTANT**: android.permission.DETECT_SCREEN_CAPTURE and
* android.permission.DETECT_SCREEN_RECORDING must be
* granted for the app in the AndroidManifest.xml
*/
@SuppressLint("MissingPermission")
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
internal fun unregister(activity: Activity) {
if (!FreeraspReactNativeModule.talsecStarted || !registered) {
return
}

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
unregisterScreenCapture(activity)
}

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
unregisterScreenRecording(activity)
}
registered = false
}

// Missing permission is suppressed because the decision to use the screen capture API is made
// by developer, and not enforced by the library.
@SuppressLint("MissingPermission")
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
private fun unregisterScreenCapture(activity: Activity) {
val context = activity.applicationContext
if (!hasPermission(context, SCREEN_CAPTURE_PERMISSION)) {
return
}
activity.unregisterScreenCaptureCallback(screenCaptureCallback)
}

// Missing permission is suppressed because the decision to use the screen capture API is made
// by developer, and not enforced by the library.
@SuppressLint("MissingPermission")
@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
private fun unregisterScreenRecording(activity: Activity) {
val context = activity.applicationContext
if (!hasPermission(context, SCREEN_RECORDING_PERMISSION)) {
return
}

activity.windowManager?.removeScreenRecordingCallback(screenRecordCallback)
}

private fun hasPermission(context: Context, permission: String): Boolean {
return ContextCompat.checkSelfPermission(
context, permission
) == PackageManager.PERMISSION_GRANTED
}

private fun reportMissingPermission(protectionType: String, permission: String) {
Log.e(
TAG,
"Failed to register $protectionType callback. Check if $permission permission is granted in AndroidManifest.xml"
)
}
}
Loading
Loading