Skip to content

Commit 0a2abfd

Browse files
committed
Screen Capture protection
1 parent b17a59d commit 0a2abfd

File tree

13 files changed

+451
-88
lines changed

13 files changed

+451
-88
lines changed

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/src/main/java/com/freeraspreactnative/FreeraspReactNativeModule.kt

Lines changed: 56 additions & 8 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
@@ -24,18 +25,23 @@ import com.freeraspreactnative.utils.getMapThrowing
2425
import com.freeraspreactnative.utils.getNestedArraySafe
2526
import com.freeraspreactnative.utils.getStringThrowing
2627
import com.freeraspreactnative.utils.toEncodedWritableArray
28+
import com.freeraspreactnative.ScreenProtector
2729

2830
class FreeraspReactNativeModule(private val reactContext: ReactApplicationContext) :
2931
ReactContextBaseJavaModule(reactContext) {
3032

3133
private val listener = ThreatListener(FreeraspThreatHandler, FreeraspThreatHandler)
3234
private val lifecycleListener = object : LifecycleEventListener {
3335
override fun onHostResume() {
34-
// do nothing
36+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
37+
currentActivity?.let { ScreenProtector.register(it) }
38+
}
3539
}
3640

3741
override fun onHostPause() {
38-
// do nothing
42+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
43+
currentActivity?.let { ScreenProtector.unregister(it) }
44+
}
3945
}
4046

4147
override fun onHostDestroy() {
@@ -54,8 +60,7 @@ class FreeraspReactNativeModule(private val reactContext: ReactApplicationContex
5460

5561
@ReactMethod
5662
fun talsecStart(
57-
options: ReadableMap,
58-
promise: Promise
63+
options: ReadableMap, promise: Promise
5964
) {
6065

6166
try {
@@ -64,6 +69,12 @@ class FreeraspReactNativeModule(private val reactContext: ReactApplicationContex
6469
listener.registerListener(reactContext)
6570
runOnUiThread {
6671
Talsec.start(reactContext, config)
72+
// Ensure ScreenProtector is registered AFTER Talsec starts
73+
currentActivity?.let { activity ->
74+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
75+
ScreenProtector.register(activity)
76+
}
77+
}
6778
}
6879

6980
promise.resolve("freeRASP started")
@@ -136,6 +147,43 @@ class FreeraspReactNativeModule(private val reactContext: ReactApplicationContex
136147
}
137148
}
138149

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

221+
internal var talsecStarted = false
222+
173223
private lateinit var appReactContext: ReactApplicationContext
174224

175225
private fun notifyListeners(threat: Threat) {
176226
val params = Arguments.createMap()
177227
params.putInt(THREAT_CHANNEL_KEY, threat.value)
178-
appReactContext
179-
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
228+
appReactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
180229
.emit(THREAT_CHANNEL_NAME, params)
181230
}
182231

@@ -196,8 +245,7 @@ class FreeraspReactNativeModule(private val reactContext: ReactApplicationContex
196245
MALWARE_CHANNEL_KEY, encodedSuspiciousApps
197246
)
198247

199-
appReactContext
200-
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
248+
appReactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
201249
.emit(THREAT_CHANNEL_NAME, params)
202250
}
203251
}

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: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
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+
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) {
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+
}
51+
52+
/**
53+
* Register Talsec Screen Capture (screenshot) Detector for given activity instance.
54+
* The MainActivity of the app is registered by the plugin itself, other
55+
* activities bust be registered manually as described in the integration guide.
56+
*
57+
* Missing permission is suppressed because the decision to use the screen
58+
* capture API is made by developer, and not enforced by the library.
59+
*
60+
* **IMPORTANT**: android.permission.DETECT_SCREEN_CAPTURE (API 34+) must be
61+
* granted for the app in the AndroidManifest.xml
62+
*/
63+
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
64+
@SuppressLint("MissingPermission")
65+
private fun registerScreenCapture(activity: Activity) {
66+
val context = activity.applicationContext
67+
if (!hasPermission(context, SCREEN_CAPTURE_PERMISSION)) {
68+
reportMissingPermission("screenshot", SCREEN_CAPTURE_PERMISSION)
69+
return
70+
}
71+
72+
activity.registerScreenCaptureCallback(context.mainExecutor, screenCaptureCallback)
73+
}
74+
75+
/**
76+
* Register Talsec Screen Recording Detector for given activity instance.
77+
* The MainActivity of the app is registered by the plugin itself, other
78+
* activities bust be registered manually as described in the integration guide.
79+
*
80+
* Missing permission is suppressed because the decision to use the screen
81+
* capture API is made by developer, and not enforced by the library.
82+
*
83+
* **IMPORTANT**: android.permission.DETECT_SCREEN_RECORDING (API 35+) must be
84+
* granted for the app in the AndroidManifest.xml
85+
*/
86+
@SuppressLint("MissingPermission")
87+
@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
88+
private fun registerScreenRecording(activity: Activity) {
89+
val context = activity.applicationContext
90+
if (!hasPermission(context, SCREEN_RECORDING_PERMISSION)) {
91+
reportMissingPermission("screen record", SCREEN_RECORDING_PERMISSION)
92+
return
93+
}
94+
95+
val initialState = activity.windowManager.addScreenRecordingCallback(
96+
context.mainExecutor, screenRecordCallback
97+
)
98+
screenRecordCallback.accept(initialState)
99+
100+
}
101+
102+
/**
103+
* Unregisters screenshot and screen recording detector with the given activity
104+
*
105+
* **IMPORTANT**: android.permission.DETECT_SCREEN_CAPTURE and
106+
* android.permission.DETECT_SCREEN_RECORDING must be
107+
* granted for the app in the AndroidManifest.xml
108+
*/
109+
@SuppressLint("MissingPermission")
110+
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
111+
internal fun unregister(activity: Activity) {
112+
if (!FreeraspReactNativeModule.talsecStarted) {
113+
return
114+
}
115+
116+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
117+
unregisterScreenCapture(activity)
118+
}
119+
120+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
121+
unregisterScreenRecording(activity)
122+
}
123+
}
124+
125+
// Missing permission is suppressed because the decision to use the screen capture API is made
126+
// by developer, and not enforced by the library.
127+
@SuppressLint("MissingPermission")
128+
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
129+
private fun unregisterScreenCapture(activity: Activity) {
130+
val context = activity.applicationContext
131+
if (!hasPermission(context, SCREEN_CAPTURE_PERMISSION)) {
132+
return
133+
}
134+
activity.unregisterScreenCaptureCallback(screenCaptureCallback)
135+
}
136+
137+
// Missing permission is suppressed because the decision to use the screen capture API is made
138+
// by developer, and not enforced by the library.
139+
@SuppressLint("MissingPermission")
140+
@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
141+
private fun unregisterScreenRecording(activity: Activity) {
142+
val context = activity.applicationContext
143+
if (!hasPermission(context, SCREEN_RECORDING_PERMISSION)) {
144+
return
145+
}
146+
147+
activity.windowManager?.removeScreenRecordingCallback(screenRecordCallback)
148+
}
149+
150+
private fun hasPermission(context: Context, permission: String): Boolean {
151+
return ContextCompat.checkSelfPermission(
152+
context, permission
153+
) == PackageManager.PERMISSION_GRANTED
154+
}
155+
156+
private fun reportMissingPermission(protectionType: String, permission: String) {
157+
Log.e(
158+
TAG,
159+
"Failed to register $protectionType callback. Check if $permission permission is granted in AndroidManifest.xml"
160+
)
161+
}
162+
}

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ internal sealed class Threat(val value: Int) {
2525
object DevMode : Threat((10000..999999999).random())
2626
object Malware : Threat((10000..999999999).random())
2727
object ADBEnabled : Threat((10000..999999999).random())
28+
object Screenshot : Threat((10000..999999999).random())
29+
object ScreenRecording : Threat((10000..999999999).random())
2830

2931
companion object {
3032
internal fun getThreatValues(): WritableArray {
@@ -43,7 +45,9 @@ internal sealed class Threat(val value: Int) {
4345
ObfuscationIssues.value,
4446
DevMode.value,
4547
Malware.value,
46-
ADBEnabled.value
48+
ADBEnabled.value,
49+
Screenshot.value,
50+
ScreenRecording.value
4751
)
4852
)
4953
}

example/src/App.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,24 @@ const App = () => {
188188
)
189189
);
190190
},
191+
// Android only
192+
screenshot: () => {
193+
setAppChecks((currentState) =>
194+
currentState.map((threat) =>
195+
threat.name === 'Screenshot' ? { ...threat, status: 'nok' } : threat
196+
)
197+
);
198+
},
199+
// Android only
200+
screenRecording: () => {
201+
setAppChecks((currentState) =>
202+
currentState.map((threat) =>
203+
threat.name === 'Screen Recording'
204+
? { ...threat, status: 'nok' }
205+
: threat
206+
)
207+
);
208+
},
191209
};
192210

193211
const addItemsToMalwareWhitelist = async () => {

0 commit comments

Comments
 (0)