Skip to content

Commit 01a55ca

Browse files
feat: Screen casting protection (#104)(#103)
* feat: update example app * chore: add build scripts * chore: remove duplicated gitignore rule * feat: Screen casting protection (#104) * Screen Capture protection * Screen Capture protection * Screen Capture protection * Screen Capture protection * Screen Capture protection * chore: update example app * feat: callback about start freerasp just after it is started * feat: use java 17 * feat: add check for screen protector being registered already * feat(Android): update proguard rules * chore(release): freeRASP 3.14.0 * chore: add permissions to example apps * rollback java version * chore: update demo app --------- Co-authored-by: Tomas Psota <[email protected]> * Update CHANGELOG.md --------- Co-authored-by: Ahmed Ayman Abd El-Moneim <[email protected]>
1 parent 603572e commit 01a55ca

File tree

118 files changed

+5252
-4602
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

118 files changed

+5252
-4602
lines changed

.eslintignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
node_modules/
22
lib/
3+
example/

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ android.iml
4242
**/android/**/build
4343
**/android/**/release
4444
**/android/**/debug
45+
!**/android/app/src/debug/AndroidManixest.xml
4546

4647
# Cocoapods
4748
#

CHANGELOG.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,50 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [3.14.0] - 2024-03-05
9+
10+
- iOS SDK version: 6.8.0
11+
- Android SDK version: 14.0.1
12+
13+
### React Native
14+
15+
#### Added
16+
17+
- `blockScreenCapture` method to block/unblock screen capture
18+
- `isScreenCaptureBlocked` method to get the current screen capture blocking status
19+
- New callbacks:
20+
- `screenshot`: Detects when a screenshot is taken
21+
- `screenRecording`: Detects when screen recording is active
22+
23+
#### Changed
24+
25+
- Raised Android compileSDK level to 35
26+
27+
#### Fixed
28+
29+
- Compatibility issues with RN New Architecture
30+
- Added proguard rules for malware data serialization in release mode on Android
31+
32+
### Android
33+
34+
#### Added
35+
36+
- Passive and active screenshot/screen recording protection
37+
38+
#### Changed
39+
40+
- Improved root detection
41+
42+
#### Fixed
43+
44+
- Proguard rules to address warnings from okhttp dependency
45+
46+
### iOS
47+
48+
#### Added
49+
50+
- Passive Screenshot/Screen Recording detection
51+
852
## [3.13.0] - 2024-12-20
953

1054
- iOS SDK version: 6.6.3

android/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ dependencies {
9090
implementation "com.facebook.react:react-native:$react_native_version"
9191
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
9292
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1"
93-
implementation "com.aheaditec.talsec.security:TalsecSecurity-Community-ReactNative:13.2.0"
93+
implementation "com.aheaditec.talsec.security:TalsecSecurity-Community-ReactNative:14.0.1"
9494
}
9595

9696
if (isNewArchitectureEnabled()) {

android/gradle.properties

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
FreeraspReactNative_kotlinVersion=1.7.0
22
FreeraspReactNative_minSdkVersion=23
3-
FreeraspReactNative_targetSdkVersion=31
4-
FreeraspReactNative_compileSdkVersion=33
3+
FreeraspReactNative_targetSdkVersion=35
4+
FreeraspReactNative_compileSdkVersion=35
55
FreeraspReactNative_ndkversion=21.4.7075529
66
FreeraspReactNative_reactNativeVersion=+

android/proguard-rules.pro

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,10 @@
11
-dontwarn java.lang.invoke.StringConcatFactory
22
-keep class com.freeraspreactnative.FreeraspReactNativePackage { public *; }
3+
4+
-if @kotlinx.serialization.Serializable class **
5+
-keep class <1> {
6+
*;
7+
}
8+
9+
-keep class com.freeraspreactnative.models.RNSuspiciousAppInfo$Companion
10+
-keep class com.freeraspreactnative.models.RNPackageInfo$Companion

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

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

3+
import android.os.Build
34
import android.os.Handler
45
import android.os.HandlerThread
56
import android.os.Looper
@@ -31,11 +32,15 @@ class FreeraspReactNativeModule(private val reactContext: ReactApplicationContex
3132
private val listener = ThreatListener(FreeraspThreatHandler, FreeraspThreatHandler)
3233
private val lifecycleListener = object : LifecycleEventListener {
3334
override fun onHostResume() {
34-
// do nothing
35+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
36+
currentActivity?.let { ScreenProtector.register(it) }
37+
}
3538
}
3639

3740
override fun onHostPause() {
38-
// do nothing
41+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
42+
currentActivity?.let { ScreenProtector.unregister(it) }
43+
}
3944
}
4045

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

5560
@ReactMethod
5661
fun talsecStart(
57-
options: ReadableMap,
58-
promise: Promise
62+
options: ReadableMap, promise: Promise
5963
) {
6064

6165
try {
@@ -64,10 +68,18 @@ class FreeraspReactNativeModule(private val reactContext: ReactApplicationContex
6468
listener.registerListener(reactContext)
6569
runOnUiThread {
6670
Talsec.start(reactContext, config)
71+
mainHandler.post {
72+
talsecStarted = true
73+
// This code must be called only AFTER Talsec.start
74+
currentActivity?.let { activity ->
75+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
76+
ScreenProtector.register(activity)
77+
}
78+
}
79+
promise.resolve("freeRASP started")
80+
}
6781
}
6882

69-
promise.resolve("freeRASP started")
70-
7183
} catch (e: Exception) {
7284
promise.reject("TalsecInitializationError", e.message, e)
7385
}
@@ -136,6 +148,43 @@ class FreeraspReactNativeModule(private val reactContext: ReactApplicationContex
136148
}
137149
}
138150

151+
/**
152+
* Method Block/Unblock screen capture
153+
* @param enable boolean for whether want to block or unblock the screen capture
154+
*/
155+
@ReactMethod
156+
fun blockScreenCapture(enable: Boolean, promise: Promise) {
157+
val activity = currentActivity ?: run {
158+
promise.reject(
159+
"NativePluginError", "Cannot block screen capture, activity is null."
160+
)
161+
return
162+
}
163+
164+
runOnUiThread {
165+
try {
166+
Talsec.blockScreenCapture(activity, enable)
167+
promise.resolve("Screen capture is now ${if (enable) "Blocked" else "Enabled"}.")
168+
} catch (e: Exception) {
169+
promise.reject("NativePluginError", "Error in blockScreenCapture: ${e.message}")
170+
}
171+
}
172+
}
173+
174+
/**
175+
* Method Returns whether screen capture is blocked or not
176+
* @return boolean for is screem capture blocked or not
177+
*/
178+
@ReactMethod
179+
fun isScreenCaptureBlocked(promise: Promise) {
180+
try {
181+
val isBlocked = Talsec.isScreenCaptureBlocked()
182+
promise.resolve(isBlocked)
183+
} catch (e: Exception) {
184+
promise.reject("NativePluginError", "Error in isScreenCaptureBlocked: ${e.message}")
185+
}
186+
}
187+
139188
private fun buildTalsecConfig(config: ReadableMap): TalsecConfig {
140189
val androidConfig = config.getMapThrowing("androidConfig")
141190
val packageName = androidConfig.getStringThrowing("packageName")
@@ -172,11 +221,12 @@ class FreeraspReactNativeModule(private val reactContext: ReactApplicationContex
172221

173222
private lateinit var appReactContext: ReactApplicationContext
174223

224+
internal var talsecStarted = false
225+
175226
private fun notifyListeners(threat: Threat) {
176227
val params = Arguments.createMap()
177228
params.putInt(THREAT_CHANNEL_KEY, threat.value)
178-
appReactContext
179-
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
229+
appReactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
180230
.emit(THREAT_CHANNEL_NAME, params)
181231
}
182232

@@ -196,8 +246,7 @@ class FreeraspReactNativeModule(private val reactContext: ReactApplicationContex
196246
MALWARE_CHANNEL_KEY, encodedSuspiciousApps
197247
)
198248

199-
appReactContext
200-
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
249+
appReactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
201250
.emit(THREAT_CHANNEL_NAME, params)
202251
}
203252
}

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,14 @@ internal object FreeraspThreatHandler : ThreatListener.ThreatDetected, ThreatLis
6363
listener?.threatDetected(Threat.SystemVPN)
6464
}
6565

66+
override fun onScreenshotDetected() {
67+
listener?.threatDetected(Threat.Screenshot)
68+
}
69+
70+
override fun onScreenRecordingDetected() {
71+
listener?.threatDetected(Threat.ScreenRecording)
72+
}
73+
6674
internal interface TalsecReactNative {
6775
fun threatDetected(threatType: Threat)
6876

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
package com.freeraspreactnative
2+
3+
import android.annotation.SuppressLint
4+
import android.annotation.TargetApi
5+
import android.app.Activity
6+
import android.app.Activity.ScreenCaptureCallback
7+
import android.content.Context
8+
import android.content.pm.PackageManager
9+
import android.os.Build
10+
import android.util.Log
11+
import android.view.WindowManager.SCREEN_RECORDING_STATE_VISIBLE
12+
import androidx.annotation.RequiresApi
13+
import androidx.core.content.ContextCompat
14+
15+
import com.aheaditec.talsec_security.security.api.Talsec
16+
import java.util.function.Consumer
17+
18+
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
19+
internal object ScreenProtector {
20+
private const val TAG = "TalsecScreenProtector"
21+
private const val SCREEN_CAPTURE_PERMISSION = "android.permission.DETECT_SCREEN_CAPTURE"
22+
private const val SCREEN_RECORDING_PERMISSION = "android.permission.DETECT_SCREEN_RECORDING"
23+
private var registered = false
24+
private val screenCaptureCallback = ScreenCaptureCallback { Talsec.onScreenshotDetected() }
25+
private val screenRecordCallback: Consumer<Int> = Consumer<Int> { state ->
26+
if (state == SCREEN_RECORDING_STATE_VISIBLE) {
27+
Talsec.onScreenRecordingDetected()
28+
}
29+
}
30+
31+
/**
32+
* Registers screenshot and screen recording detector with the given activity
33+
*
34+
* **IMPORTANT**: android.permission.DETECT_SCREEN_CAPTURE and
35+
* android.permission.DETECT_SCREEN_RECORDING must be
36+
* granted for the app in the AndroidManifest.xml
37+
*/
38+
internal fun register(activity: Activity) {
39+
if (!FreeraspReactNativeModule.talsecStarted || registered) {
40+
return
41+
}
42+
43+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
44+
registerScreenCapture(activity)
45+
}
46+
47+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
48+
registerScreenRecording(activity)
49+
}
50+
registered = true
51+
}
52+
53+
/**
54+
* Register Talsec Screen Capture (screenshot) Detector for given activity instance.
55+
* The MainActivity of the app is registered by the plugin itself, other
56+
* activities bust be registered manually as described in the integration guide.
57+
*
58+
* Missing permission is suppressed because the decision to use the screen
59+
* capture API is made by developer, and not enforced by the library.
60+
*
61+
* **IMPORTANT**: android.permission.DETECT_SCREEN_CAPTURE (API 34+) must be
62+
* granted for the app in the AndroidManifest.xml
63+
*/
64+
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
65+
@SuppressLint("MissingPermission")
66+
private fun registerScreenCapture(activity: Activity) {
67+
val context = activity.applicationContext
68+
if (!hasPermission(context, SCREEN_CAPTURE_PERMISSION)) {
69+
reportMissingPermission("screenshot", SCREEN_CAPTURE_PERMISSION)
70+
return
71+
}
72+
73+
activity.registerScreenCaptureCallback(context.mainExecutor, screenCaptureCallback)
74+
}
75+
76+
/**
77+
* Register Talsec Screen Recording Detector for given activity instance.
78+
* The MainActivity of the app is registered by the plugin itself, other
79+
* activities bust be registered manually as described in the integration guide.
80+
*
81+
* Missing permission is suppressed because the decision to use the screen
82+
* capture API is made by developer, and not enforced by the library.
83+
*
84+
* **IMPORTANT**: android.permission.DETECT_SCREEN_RECORDING (API 35+) must be
85+
* granted for the app in the AndroidManifest.xml
86+
*/
87+
@SuppressLint("MissingPermission")
88+
@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
89+
private fun registerScreenRecording(activity: Activity) {
90+
val context = activity.applicationContext
91+
if (!hasPermission(context, SCREEN_RECORDING_PERMISSION)) {
92+
reportMissingPermission("screen record", SCREEN_RECORDING_PERMISSION)
93+
return
94+
}
95+
96+
val initialState = activity.windowManager.addScreenRecordingCallback(
97+
context.mainExecutor, screenRecordCallback
98+
)
99+
screenRecordCallback.accept(initialState)
100+
101+
}
102+
103+
/**
104+
* Unregisters screenshot and screen recording detector with the given activity
105+
*
106+
* **IMPORTANT**: android.permission.DETECT_SCREEN_CAPTURE and
107+
* android.permission.DETECT_SCREEN_RECORDING must be
108+
* granted for the app in the AndroidManifest.xml
109+
*/
110+
@SuppressLint("MissingPermission")
111+
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
112+
internal fun unregister(activity: Activity) {
113+
if (!FreeraspReactNativeModule.talsecStarted || !registered) {
114+
return
115+
}
116+
117+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
118+
unregisterScreenCapture(activity)
119+
}
120+
121+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
122+
unregisterScreenRecording(activity)
123+
}
124+
registered = false
125+
}
126+
127+
// Missing permission is suppressed because the decision to use the screen capture API is made
128+
// by developer, and not enforced by the library.
129+
@SuppressLint("MissingPermission")
130+
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
131+
private fun unregisterScreenCapture(activity: Activity) {
132+
val context = activity.applicationContext
133+
if (!hasPermission(context, SCREEN_CAPTURE_PERMISSION)) {
134+
return
135+
}
136+
activity.unregisterScreenCaptureCallback(screenCaptureCallback)
137+
}
138+
139+
// Missing permission is suppressed because the decision to use the screen capture API is made
140+
// by developer, and not enforced by the library.
141+
@SuppressLint("MissingPermission")
142+
@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
143+
private fun unregisterScreenRecording(activity: Activity) {
144+
val context = activity.applicationContext
145+
if (!hasPermission(context, SCREEN_RECORDING_PERMISSION)) {
146+
return
147+
}
148+
149+
activity.windowManager?.removeScreenRecordingCallback(screenRecordCallback)
150+
}
151+
152+
private fun hasPermission(context: Context, permission: String): Boolean {
153+
return ContextCompat.checkSelfPermission(
154+
context, permission
155+
) == PackageManager.PERMISSION_GRANTED
156+
}
157+
158+
private fun reportMissingPermission(protectionType: String, permission: String) {
159+
Log.e(
160+
TAG,
161+
"Failed to register $protectionType callback. Check if $permission permission is granted in AndroidManifest.xml"
162+
)
163+
}
164+
}

0 commit comments

Comments
 (0)