Skip to content

Commit 91a0927

Browse files
feat: screen capture detection (#29)
* Add Screen Capture detection and prevention * run prettier for formating ts files * add ios support * feat: register screen recording after Talsec.start * Update android/build.gradle Co-authored-by: Tomas Psota <72520867+tompsota@users.noreply.github.com> * Update android/build.gradle Co-authored-by: Tomas Psota <72520867+tompsota@users.noreply.github.com> * Update android/build.gradle Co-authored-by: Tomas Psota <72520867+tompsota@users.noreply.github.com> * fix PR Comments * feat: apply minifyEnabled in the plugin * fix PR Comments * chore: add dev team to ios example * release: freeRASP 1.10.0 * feat(Android): add registered flag to screen protector * Update CHANGELOG.md --------- Co-authored-by: Tomas Psota <to.psota@gmail.com> Co-authored-by: Tomas Psota <72520867+tompsota@users.noreply.github.com>
1 parent 0aa0d89 commit 91a0927

File tree

73 files changed

+1802
-839
lines changed

Some content is hidden

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

73 files changed

+1802
-839
lines changed

CHANGELOG.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,46 @@ 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+
## [1.10.0] - 2025-03-05
9+
10+
- iOS SDK version: 6.8.0
11+
- Android SDK version: 14.0.1
12+
13+
### Capacitor
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+
- Set minifyEnabled in plugin to `true` implicitly on Android
27+
28+
### Android
29+
30+
#### Added
31+
32+
- Passive and active screenshot/screen recording protection
33+
34+
#### Changed
35+
36+
- Improved root detection
37+
38+
#### Fixed
39+
40+
- Proguard rules to address warnings from okhttp dependency
41+
42+
### iOS
43+
44+
#### Added
45+
46+
- Passive Screenshot/Screen Recording detection
47+
848
## [1.9.0] - 2024-12-29
949

1050
- iOS SDK version: 6.6.3

android/build.gradle

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,19 +27,17 @@ apply plugin: 'kotlinx-serialization'
2727

2828
android {
2929
namespace "com.aheaditec.freerasp"
30-
compileSdkVersion project.hasProperty('compileSdkVersion') ? rootProject.ext.compileSdkVersion : 33
30+
compileSdk 35
3131
defaultConfig {
3232
minSdkVersion 23
33-
targetSdkVersion project.hasProperty('targetSdkVersion') ? rootProject.ext.targetSdkVersion : 33
33+
targetSdkVersion 35
3434
versionCode 1
3535
versionName "1.0"
3636
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
37-
// now, the rule skips all classes of Capacitor plugin for freeRASP
38-
consumerProguardFiles 'consumer-rules.pro'
3937
}
4038
buildTypes {
4139
release {
42-
minifyEnabled false
40+
minifyEnabled true
4341
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
4442
}
4543
}
@@ -78,5 +76,5 @@ dependencies {
7876
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
7977
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
8078

81-
implementation 'com.aheaditec.talsec.security:TalsecSecurity-Community-Capacitor:13.2.0'
79+
implementation 'com.aheaditec.talsec.security:TalsecSecurity-Community-Capacitor:14.0.1'
8280
}

android/consumer-rules.pro

Lines changed: 0 additions & 1 deletion
This file was deleted.

android/proguard-rules.pro

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,5 @@
1919
# If you keep the line number information, uncomment this to
2020
# hide the original source file name.
2121
#-renamesourcefileattribute SourceFile
22+
23+
-dontwarn java.lang.invoke.StringConcatFactory

android/src/main/java/com/aheaditec/freerasp/FreeraspPlugin.kt

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.aheaditec.freerasp
22

3+
import android.os.Build
34
import android.os.Handler
45
import android.os.HandlerThread
56
import android.os.Looper
@@ -17,7 +18,6 @@ import com.getcapacitor.PluginCall
1718
import com.getcapacitor.PluginMethod
1819
import com.getcapacitor.annotation.CapacitorPlugin
1920
import org.json.JSONArray
20-
import java.lang.Exception
2121

2222
@CapacitorPlugin(name = "Freerasp")
2323
class FreeraspPlugin : Plugin() {
@@ -38,8 +38,16 @@ class FreeraspPlugin : Plugin() {
3838
listener.registerListener(context)
3939
bridge.activity.runOnUiThread {
4040
Talsec.start(context, talsecConfig)
41+
mainHandler.post {
42+
talsecStarted = true
43+
// This code must be called only AFTER Talsec.start
44+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
45+
ScreenProtector.register(activity)
46+
}
47+
call.resolve(JSObject().put("started", true))
48+
}
4149
}
42-
call.resolve(JSObject().put("started", true))
50+
4351
} catch (e: Exception) {
4452
call.reject(
4553
"Error during Talsec Native plugin initialization - ${e.message}",
@@ -49,6 +57,13 @@ class FreeraspPlugin : Plugin() {
4957
}
5058
}
5159

60+
override fun handleOnStart() {
61+
super.handleOnStart()
62+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
63+
ScreenProtector.register(activity)
64+
}
65+
}
66+
5267
override fun handleOnPause() {
5368
super.handleOnPause()
5469
if (activity.isFinishing) {
@@ -65,6 +80,13 @@ class FreeraspPlugin : Plugin() {
6580
}
6681
}
6782

83+
override fun handleOnStop() {
84+
super.handleOnStop()
85+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
86+
ScreenProtector.unregister(activity)
87+
}
88+
}
89+
6890
override fun handleOnDestroy() {
6991
super.handleOnDestroy()
7092
backgroundHandlerThread.quitSafely()
@@ -142,6 +164,49 @@ class FreeraspPlugin : Plugin() {
142164
}
143165
}
144166

167+
/**
168+
* Method to set screen capture state
169+
* @param enable Pass `true` to block screen capture, `false` to enable it
170+
*/
171+
@PluginMethod
172+
fun blockScreenCapture(call: PluginCall) {
173+
val enable = call.getBoolean("enable") ?: run {
174+
call.reject(
175+
"Enable argument is missing or not a boolean.", "MissingArgumentError"
176+
)
177+
return
178+
}
179+
180+
activity?.runOnUiThread {
181+
try {
182+
Talsec.blockScreenCapture(context, enable)
183+
call.resolve(JSObject().put("result", true))
184+
} catch (e: Exception) {
185+
call.reject(
186+
"Error while setting screen capture: ${e.message}", "BlockScreenCaptureError"
187+
)
188+
}
189+
} ?: run {
190+
call.reject("Cannot block screen capture, activity is null.", "BlockScreenCaptureError")
191+
}
192+
}
193+
194+
/**
195+
* Method to check if screen capturing is currently blocked
196+
*/
197+
@PluginMethod
198+
fun isScreenCaptureBlocked(call: PluginCall) {
199+
try {
200+
val isBlocked = Talsec.isScreenCaptureBlocked()
201+
call.resolve(JSObject().put("result", isBlocked))
202+
} catch (e: Exception) {
203+
call.reject(
204+
"Error while checking if screen capture is blocked: ${e.message}",
205+
"IsScreenCaptureBlockedError"
206+
)
207+
}
208+
}
209+
145210
internal fun notifyListeners(threat: Threat) {
146211
notifyListeners(THREAT_CHANNEL_NAME, JSObject().put(THREAT_CHANNEL_KEY, threat.value), true)
147212
}
@@ -179,6 +244,7 @@ class FreeraspPlugin : Plugin() {
179244
return talsecBuilder.build()
180245
}
181246

247+
182248
companion object {
183249
private val THREAT_CHANNEL_NAME = (10000..999999999).random()
184250
.toString() // name of the channel over which threat callbacks are sent
@@ -189,5 +255,7 @@ class FreeraspPlugin : Plugin() {
189255
private val backgroundHandlerThread = HandlerThread("BackgroundThread").apply { start() }
190256
private val backgroundHandler = Handler(backgroundHandlerThread.looper)
191257
private val mainHandler = Handler(Looper.getMainLooper())
258+
259+
internal var talsecStarted = false
192260
}
193261
}
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
package com.aheaditec.freerasp
2+
3+
import android.annotation.SuppressLint
4+
import android.app.Activity
5+
import android.app.Activity.ScreenCaptureCallback
6+
import android.content.Context
7+
import android.content.pm.PackageManager
8+
import android.os.Build
9+
import android.util.Log
10+
import android.view.WindowManager.SCREEN_RECORDING_STATE_VISIBLE
11+
import androidx.annotation.RequiresApi
12+
import androidx.core.content.ContextCompat
13+
import com.aheaditec.talsec_security.security.api.Talsec
14+
import java.util.function.Consumer
15+
16+
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
17+
internal object ScreenProtector {
18+
private const val TAG = "TalsecScreenProtector"
19+
private const val SCREEN_CAPTURE_PERMISSION = "android.permission.DETECT_SCREEN_CAPTURE"
20+
private const val SCREEN_RECORDING_PERMISSION = "android.permission.DETECT_SCREEN_RECORDING"
21+
private var registered = false
22+
private val screenCaptureCallback = ScreenCaptureCallback { Talsec.onScreenshotDetected() }
23+
private val screenRecordCallback: Consumer<Int> = Consumer<Int> { state ->
24+
if (state == SCREEN_RECORDING_STATE_VISIBLE) {
25+
Talsec.onScreenRecordingDetected()
26+
}
27+
}
28+
29+
/**
30+
* Registers screenshot and screen recording detector with the given activity
31+
*
32+
* **IMPORTANT**: android.permission.DETECT_SCREEN_CAPTURE and
33+
* android.permission.DETECT_SCREEN_RECORDING must be
34+
* granted for the app in the AndroidManifest.xml
35+
*/
36+
internal fun register(activity: Activity) {
37+
if (!FreeraspPlugin.talsecStarted || registered) {
38+
return
39+
}
40+
41+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
42+
registerScreenCapture(activity)
43+
}
44+
45+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
46+
registerScreenRecording(activity)
47+
}
48+
registered = true
49+
}
50+
51+
/**
52+
* Register Talsec Screen Capture (screenshot) Detector for given activity instance.
53+
* The MainActivity of the app is registered by the plugin itself, other
54+
* activities bust be registered manually as described in the integration guide.
55+
*
56+
* Missing permission is suppressed because the decision to use the screen
57+
* capture API is made by developer, and not enforced by the library.
58+
*
59+
* **IMPORTANT**: android.permission.DETECT_SCREEN_CAPTURE (API 34+) must be
60+
* granted for the app in the AndroidManifest.xml
61+
*/
62+
@SuppressLint("MissingPermission")
63+
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
64+
private fun registerScreenCapture(activity: Activity) {
65+
val context = activity.applicationContext
66+
if (!hasPermission(context, SCREEN_CAPTURE_PERMISSION)) {
67+
reportMissingPermission("screenshot", SCREEN_CAPTURE_PERMISSION)
68+
return
69+
}
70+
71+
activity.registerScreenCaptureCallback(context.mainExecutor, screenCaptureCallback)
72+
}
73+
74+
/**
75+
* Register Talsec Screen Recording Detector for given activity instance.
76+
* The MainActivity of the app is registered by the plugin itself, other
77+
* activities bust be registered manually as described in the integration guide.
78+
*
79+
* Missing permission is suppressed because the decision to use the screen
80+
* capture API is made by developer, and not enforced by the library.
81+
*
82+
* **IMPORTANT**: android.permission.DETECT_SCREEN_RECORDING (API 35+) must be
83+
* granted for the app in the AndroidManifest.xml
84+
*/
85+
@SuppressLint("MissingPermission")
86+
@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
87+
private fun registerScreenRecording(activity: Activity) {
88+
val context = activity.applicationContext
89+
if (!hasPermission(context, SCREEN_RECORDING_PERMISSION)) {
90+
reportMissingPermission("screen record", SCREEN_RECORDING_PERMISSION)
91+
return
92+
}
93+
94+
val initialState = activity.windowManager.addScreenRecordingCallback(
95+
context.mainExecutor, screenRecordCallback
96+
)
97+
screenRecordCallback.accept(initialState)
98+
99+
}
100+
101+
/**
102+
* Unregisters screenshot and screen recording detector with the given activity
103+
*
104+
* **IMPORTANT**: android.permission.DETECT_SCREEN_CAPTURE and
105+
* android.permission.DETECT_SCREEN_RECORDING must be
106+
* granted for the app in the AndroidManifest.xml
107+
*/
108+
@SuppressLint("MissingPermission")
109+
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
110+
internal fun unregister(activity: Activity) {
111+
if (!FreeraspPlugin.talsecStarted || !registered) {
112+
return
113+
}
114+
115+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
116+
unregisterScreenCapture(activity)
117+
}
118+
119+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
120+
unregisterScreenRecording(activity)
121+
}
122+
registered = false
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/aheaditec/freerasp/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(): JSONArray {
@@ -44,7 +46,9 @@ internal sealed class Threat(val value: Int) {
4446
ObfuscationIssues.value,
4547
DevMode.value,
4648
Malware.value,
47-
ADBEnabled.value
49+
ADBEnabled.value,
50+
Screenshot.value,
51+
ScreenRecording.value
4852
))
4953
)
5054
}

0 commit comments

Comments
 (0)