Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 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
197 changes: 153 additions & 44 deletions CHANGELOG.md

Large diffs are not rendered by default.

16 changes: 8 additions & 8 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@ group 'com.aheaditec.freerasp'
version '1.0-SNAPSHOT'

buildscript {
ext.kotlin_version = '1.7.20'
ext.talsec_version = '13.2.0'
ext.kotlin_version = '1.7.22'
ext.talsec_version = '14.0.1'
repositories {
google()
mavenCentral()
}

dependencies {
classpath 'com.android.tools.build:gradle:7.4.2'
classpath 'com.android.tools.build:gradle:8.3.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
Expand All @@ -32,15 +32,15 @@ android {
namespace("com.aheaditec.freerasp")
}

compileSdkVersion 33
compileSdk 35

compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}

kotlinOptions {
jvmTarget = '1.8'
jvmTarget = 17
}

sourceSets {
Expand All @@ -58,7 +58,7 @@ android {
}

dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"

// Talsec SDK
implementation "com.aheaditec.talsec.security:TalsecSecurity-Community-Flutter:$talsec_version"
Expand Down
2 changes: 1 addition & 1 deletion android/gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-all.zip
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip
17 changes: 16 additions & 1 deletion android/src/main/kotlin/com/aheaditec/freerasp/FreeraspPlugin.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.aheaditec.freerasp

import android.content.Context
import android.os.Build
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.LifecycleOwner
Expand All @@ -12,11 +13,12 @@ import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
import io.flutter.embedding.engine.plugins.lifecycle.FlutterLifecycleAdapter


/** FreeraspPlugin */
class FreeraspPlugin : FlutterPlugin, ActivityAware, LifecycleEventObserver {
private var streamHandler: StreamHandler = StreamHandler()
private var methodCallHandler: MethodCallHandler = MethodCallHandler()
private var screenProtector: ScreenProtector? =
if (Build.VERSION.SDK_INT >= 34) ScreenProtector else null

private var context: Context? = null
private var lifecycle: Lifecycle? = null
Expand All @@ -39,19 +41,32 @@ class FreeraspPlugin : FlutterPlugin, ActivityAware, LifecycleEventObserver {
lifecycle = FlutterLifecycleAdapter.getActivityLifecycle(binding).also {
it.addObserver(this)
}
methodCallHandler.activity = binding.activity
screenProtector?.activity = binding.activity
screenProtector?.let { lifecycle?.addObserver(it) }
}

override fun onDetachedFromActivity() {
lifecycle?.removeObserver(this)
methodCallHandler.activity = null
screenProtector?.let { lifecycle?.removeObserver(it) }
screenProtector?.activity = null
}

override fun onDetachedFromActivityForConfigChanges() {
lifecycle?.removeObserver(this)
methodCallHandler.activity = null
screenProtector?.let { lifecycle?.removeObserver(it) }
screenProtector?.activity = null
}

override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
lifecycle = FlutterLifecycleAdapter.getActivityLifecycle(binding)
lifecycle?.addObserver(this)
methodCallHandler.activity = binding.activity
screenProtector?.activity = binding.activity
screenProtector?.let { lifecycle?.addObserver(it) }

}

override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
Expand Down
130 changes: 130 additions & 0 deletions android/src/main/kotlin/com/aheaditec/freerasp/ScreenProtector.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package com.aheaditec.freerasp

import android.annotation.SuppressLint
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 androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import com.aheaditec.talsec_security.security.api.Talsec
import java.util.function.Consumer

@SuppressLint("StaticFieldLeak")
internal object ScreenProtector : DefaultLifecycleObserver {
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"

internal var activity: Activity? = null
private var isEnabled = false

private val screenCaptureCallback = ScreenCaptureCallback { Talsec.onScreenshotDetected() }
private val screenRecordCallback: Consumer<Int> = Consumer<Int> { state ->
if (state == SCREEN_RECORDING_STATE_VISIBLE) {
Talsec.onScreenRecordingDetected()
Log.e("ScreenProtector", "Screen recording detected")
}
}

fun enable() {
if (isEnabled) return
isEnabled = true
activity?.let { register(it) }
}

override fun onStart(owner: LifecycleOwner) {
super.onStart(owner)

if (isEnabled)
activity?.let { register(it) }
}

override fun onStop(owner: LifecycleOwner) {
super.onStop(owner)

if (isEnabled)
activity?.let { unregister(it) }
}

private fun register(activity: Activity) {
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)
}
}

// 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")
private fun unregister(currentActivity: Activity) {
val context = currentActivity.applicationContext

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE &&
hasPermission(context, SCREEN_CAPTURE_PERMISSION)
) {
currentActivity.unregisterScreenCaptureCallback(screenCaptureCallback)
}

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM &&
hasPermission(context, SCREEN_RECORDING_PERMISSION)
) {
currentActivity.windowManager?.removeScreenRecordingCallback(screenRecordCallback)
}
}

// 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 registerScreenCapture(currentActivity: Activity) {
val context = currentActivity.applicationContext

if (!hasPermission(context, SCREEN_CAPTURE_PERMISSION)) {
reportMissingPermission("screenshot", SCREEN_CAPTURE_PERMISSION)
return
}

currentActivity.registerScreenCaptureCallback(context.mainExecutor, 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 registerScreenRecording(currentActivity: Activity) {
val context = currentActivity.applicationContext

if (!hasPermission(context, SCREEN_RECORDING_PERMISSION)) {
reportMissingPermission("screen record", SCREEN_RECORDING_PERMISSION)
return
}

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

}

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"
)
}
}
4 changes: 4 additions & 0 deletions android/src/main/kotlin/com/aheaditec/freerasp/Threat.kt
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,8 @@ internal sealed class Threat(val value: Int) {
object DevMode : Threat(45291047)

object ADBEnabled : Threat(379769839)

object Screenshot : Threat(705651459)

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

import android.app.Activity
import android.content.Context
import android.os.Handler
import android.os.HandlerThread
Expand Down Expand Up @@ -30,6 +31,8 @@ internal class MethodCallHandler : MethodCallHandler, LifecycleEventObserver {
private val backgroundHandler = Handler(backgroundHandlerThread.looper)
private val mainHandler = Handler(Looper.getMainLooper())

internal var activity: Activity? = null

companion object {
private const val CHANNEL_NAME: String = "talsec.app/freerasp/methods"
}
Expand Down Expand Up @@ -116,6 +119,8 @@ internal class MethodCallHandler : MethodCallHandler, LifecycleEventObserver {
"start" -> start(call, result)
"addToWhitelist" -> addToWhitelist(call, result)
"getAppIcon" -> getAppIcon(call, result)
"blockScreenCapture" -> blockScreenCapture(call, result)
"isScreenCaptureBlocked" -> isScreenCaptureBlocked(result)
else -> result.notImplemented()
}
}
Expand Down Expand Up @@ -169,4 +174,29 @@ internal class MethodCallHandler : MethodCallHandler, LifecycleEventObserver {

}
}

/**
* Blocks or unblocks screen capture. Sets the window flag to secure the screen.
*
* @param call The method call containing the enable flag.
* @param result The result handler of the method call.
*/
private fun blockScreenCapture(call: MethodCall, result: MethodChannel.Result) {
runResultCatching(result) {
val enable = call.argument<Boolean>("enable") ?: false
Talsec.blockScreenCapture(activity, enable)
result.success(null)
}
}

/**
* Checks if screen capture is blocked.
*
* @param result The result handler of the method call.
*/
private fun isScreenCaptureBlocked(result: MethodChannel.Result) {
runResultCatching(result) {
result.success(Talsec.isScreenCaptureBlocked())
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,14 @@ internal object PluginThreatHandler : ThreatDetected, DeviceState {
notify(Threat.ADBEnabled)
}

override fun onScreenshotDetected() {
notify(Threat.Screenshot)
}

override fun onScreenRecordingDetected() {
notify(Threat.ScreenRecording)
}

override fun onMalwareDetected(suspiciousApps: List<SuspiciousAppInfo>) {
notify(suspiciousApps)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.aheaditec.freerasp.handlers

import android.content.Context
import android.os.Build
import com.aheaditec.freerasp.ScreenProtector
import com.aheaditec.freerasp.Threat
import com.aheaditec.talsec_security.security.api.SuspiciousAppInfo
import com.aheaditec.talsec_security.security.api.Talsec
Expand All @@ -24,6 +26,8 @@ internal object TalsecThreatHandler {
internal fun start(context: Context, config: TalsecConfig) {
attachListener(context)
Talsec.start(context, config)
if (Build.VERSION.SDK_INT >= 34)
ScreenProtector.enable()
}

/**
Expand Down
12 changes: 9 additions & 3 deletions example/android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,17 @@ if (flutterVersionName == null) {
}

android {
namespace 'com.aheaditec.freerasp_example'

compileSdkVersion flutter.compileSdkVersion

compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}

kotlinOptions {
jvmTarget = '1.8'
jvmTarget = 17
}

sourceSets {
Expand All @@ -55,6 +57,10 @@ android {
signingConfig signingConfigs.debug
}
}

lintOptions {
disable 'InvalidPackage'
}
}

flutter {
Expand Down
5 changes: 5 additions & 0 deletions example/android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.aheaditec.freerasp_example">

<!-- These permissions are needed if want to detect screen capturing and recording. -->
<uses-permission android:name="android.permission.DETECT_SCREEN_CAPTURE" />
<uses-permission android:name="android.permission.DETECT_SCREEN_RECORDING" />

<application
android:label="freerasp_example"
android:icon="@mipmap/ic_launcher">
Expand Down
4 changes: 2 additions & 2 deletions example/android/gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#Fri Jun 23 08:50:38 CEST 2017
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip
zipStorePath=wrapper/dists
Loading