Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/screen capture detection #129

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
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
2 changes: 1 addition & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ android {
namespace("com.aheaditec.freerasp")
}

compileSdkVersion 33
compileSdkVersion 34

compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
Expand Down
4 changes: 3 additions & 1 deletion android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.aheaditec.freerasp">
package="com.aheaditec.freerasp">

<uses-permission android:name="android.permission.DETECT_SCREEN_CAPTURE" />
</manifest>
6 changes: 6 additions & 0 deletions android/src/main/kotlin/com/aheaditec/freerasp/CaptureType.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.aheaditec.freerasp

internal sealed class CaptureType(val value: Int) {
object Unknown : CaptureType(1115787534)

}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.aheaditec.freerasp

import android.app.Activity
import android.content.Context
import android.view.WindowManager
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.LifecycleOwner
Expand All @@ -19,6 +21,7 @@ class FreeraspPlugin : FlutterPlugin, ActivityAware, LifecycleEventObserver {
private var methodCallHandler: MethodCallHandler = MethodCallHandler()
private var context: Context? = null
private var lifecycle: Lifecycle? = null
private var activity: Activity? = null

override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
val messenger = flutterPluginBinding.binaryMessenger
Expand All @@ -38,6 +41,8 @@ class FreeraspPlugin : FlutterPlugin, ActivityAware, LifecycleEventObserver {
lifecycle = FlutterLifecycleAdapter.getActivityLifecycle(binding).also {
it.addObserver(this)
}

TalsecThreatHandler.guardActivity(binding.getActivity() as Activity?)
}

override fun onDetachedFromActivity() {
Expand Down
2 changes: 2 additions & 0 deletions android/src/main/kotlin/com/aheaditec/freerasp/Threat.kt
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,6 @@ internal sealed class Threat(val value: Int) {
object SystemVPN : Threat(659382561)

object DevMode : Threat(45291047)

object ScreenCapture : Threat(669512294)
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.aheaditec.freerasp.handlers

import android.content.Context
import com.aheaditec.freerasp.CaptureType
import com.aheaditec.freerasp.runResultCatching
import com.aheaditec.freerasp.utils.Utils
import io.flutter.Log
Expand Down Expand Up @@ -38,6 +39,7 @@ internal class MethodCallHandler : MethodCallHandler {
it.setMethodCallHandler(this)
}

TalsecThreatHandler.attachMethod(sink)
this.context = context
}

Expand All @@ -47,6 +49,7 @@ internal class MethodCallHandler : MethodCallHandler {
fun destroyMethodChannel() {
methodChannel?.setMethodCallHandler(null)
methodChannel = null
TalsecThreatHandler.detachMethod()
this.context = null
}

Expand All @@ -72,11 +75,24 @@ internal class MethodCallHandler : MethodCallHandler {
private fun start(call: MethodCall, result: MethodChannel.Result) {
runResultCatching(result) {
val config = call.argument<String>("config")
val talsecConfig = Utils.toTalsecConfigThrowing(config)
val extendedTalsecConfig = Utils.toExtendedTalsecConfigThrowing(config)
context?.let {
TalsecThreatHandler.start(it, talsecConfig)
TalsecThreatHandler.start(it, extendedTalsecConfig)
} ?: throw IllegalStateException("Unable to run Talsec - context is null")
result.success(null)
}
}
}

private val sink = object : MethodSink {
override fun onScreenCaptureDetected(captureType: CaptureType) {
methodChannel?.invokeMethod(
"onScreenCaptureDetected",
mapOf(Pair("type", captureType.value))
)
}
}

internal interface MethodSink {
fun onScreenCaptureDetected(captureType: CaptureType)
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package com.aheaditec.freerasp.handlers

import android.app.Activity
import android.content.Context
import android.os.Build
import android.view.WindowManager
import com.aheaditec.freerasp.CaptureType
import com.aheaditec.freerasp.Threat
import com.aheaditec.talsec_security.security.api.Talsec
import com.aheaditec.talsec_security.security.api.TalsecConfig
Expand All @@ -12,16 +16,23 @@ import io.flutter.plugin.common.EventChannel.EventSink
*/
internal object TalsecThreatHandler {
private var eventSink: EventSink? = null
private var methodSink: MethodCallHandler.MethodSink? = null
private var isListening = false
private var enableScreenCaptureGuard = false

/**
* Initializes [Talsec] and starts the [PluginThreatHandler] listener with this object.
*
* @param context The Android application context.
*/
internal fun start(context: Context, config: TalsecConfig) {
internal fun start(context: Context, extendedConfig: Pair<TalsecConfig, Boolean>) {
attachListener(context)
Talsec.start(context, config)

if (extendedConfig.first.isProd && extendedConfig.second) {
enableScreenCaptureGuard = true
}

Talsec.start(context, extendedConfig.first)
}

/**
Expand Down Expand Up @@ -116,6 +127,10 @@ internal object TalsecThreatHandler {
flushThreatCache(eventSink)
}

internal fun attachMethod(methodSink: MethodCallHandler.MethodSink) {
this.methodSink = methodSink
}

/**
* Called when a listener unsubscribes from the event channel.
*/
Expand All @@ -124,6 +139,22 @@ internal object TalsecThreatHandler {
PluginThreatHandler.listener = null
}

internal fun detachMethod() {
methodSink = null
}

internal fun guardActivity(activity: Activity?) {
if (enableScreenCaptureGuard) {
activity?.window?.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
}

if (Build.VERSION.SDK_INT >= 34) {
Activity.ScreenCaptureCallback {
methodSink?.onScreenCaptureDetected(CaptureType.Unknown)
}
}
}

/**
* Sends any cached detected threats to the listener.
*
Expand Down
21 changes: 14 additions & 7 deletions android/src/main/kotlin/com/aheaditec/freerasp/utils/Utils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import org.json.JSONObject

internal class Utils {
companion object {
fun toTalsecConfigThrowing(configJson: String?): TalsecConfig {
fun toExtendedTalsecConfigThrowing(configJson: String?): Pair<TalsecConfig, Boolean> {
if (configJson == null) {
throw JSONException("Configuration is null")
}
Expand All @@ -31,12 +31,19 @@ internal class Utils {
isProd = json.getBoolean("isProd")
}

return TalsecConfig(
packageName,
certificateHashes.toTypedArray(),
watcherMail,
alternativeStores.toTypedArray(),
isProd
var enableScreenCaptureGuard = false
if (json.has("enableScreenCaptureGuard")) {
enableScreenCaptureGuard = json.getBoolean("enableScreenCaptureGuard")
}

return Pair(
TalsecConfig(
packageName,
certificateHashes.toTypedArray(),
watcherMail,
alternativeStores.toTypedArray(),
isProd
), enableScreenCaptureGuard
)
}
}
Expand Down
3 changes: 3 additions & 0 deletions lib/src/models/android_config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class AndroidConfig {
required this.packageName,
required this.signingCertHashes,
this.supportedStores = const <String>[],
this.enableScreenCaptureGuard = false,
}) {
ConfigVerifier.verifyAndroid(this);
}
Expand All @@ -30,4 +31,6 @@ class AndroidConfig {

/// List of supported sources where application can be installed from.
final List<String> supportedStores;

final bool enableScreenCaptureGuard;
}
3 changes: 3 additions & 0 deletions lib/src/models/android_config.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions lib/src/models/talsec_config.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions lib/src/talsec.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'dart:async';
import 'dart:convert';
import 'dart:ffi';

import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
Expand Down Expand Up @@ -180,6 +181,14 @@ class Talsec {
break;
}
});

methodChannel.setMethodCallHandler((call) {
if (call.method == "onScreenCaptureDetected") {
final value = call.arguments as int;

callback.onScreenCaptureDetected?.call(CaptureType.fromInt(value));
}
});
}

/// Removes instance of latest [ThreatCallback]. Also cancels
Expand Down
21 changes: 21 additions & 0 deletions lib/src/threat_callback.dart
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ class ThreatCallback {
this.onSecureHardwareNotAvailable,
this.onSystemVPN,
this.onDevMode,
this.onScreenCaptureDetected
});

/// This method is called when a threat related dynamic hooking (e.g. Frida)
Expand Down Expand Up @@ -80,4 +81,24 @@ class ThreatCallback {

/// This method is called whe the device has Developer mode enabled
final VoidCallback? onDevMode;

void Function(CaptureType type)? onScreenCaptureDetected;
}

enum CaptureType {
unknown(1115787534),
screenshot(0),
recording(0),
mirroring(0);

final int value;

const CaptureType(this.value);

static CaptureType fromInt(int value) {
switch (value) {
case 1115787534: return CaptureType.unknown;
default: return CaptureType.unknown;
}
}
}
Loading