From 3542031f2ea9a0fd9eae20684c88e3678b644a5f Mon Sep 17 00:00:00 2001 From: Denis Dobanda Date: Fri, 31 Jan 2025 10:25:02 +0100 Subject: [PATCH 1/4] chore: ktlint Signed-off-by: Denis Dobanda --- .editorconfig | 20 ++++++ example/app/build.gradle.kts | 4 +- .../ExampleInstrumentedTest.kt | 13 ++-- .../MainActivity.kt | 3 +- .../ThreatStatusList.kt | 17 ++--- .../ThreatStatusRow.kt | 16 +++-- .../ui/theme/Color.kt | 2 +- .../ui/theme/Theme.kt | 35 +++++----- .../ui/theme/Type.kt | 8 +-- .../ExampleUnitTest.kt | 5 +- example/build.gradle.kts | 2 +- example/settings.gradle.kts | 1 - securitytoolkit/build.gradle.kts | 12 ++-- .../securitytoolkit/ThreatDetectionCenter.kt | 4 +- .../internal/EmulatorDetector.kt | 67 ++++++++++--------- .../internal/HooksDetection.kt | 22 +++--- .../securitytoolkit/internal/RootDetection.kt | 8 +-- 17 files changed, 133 insertions(+), 106 deletions(-) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..8d9b054 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,20 @@ +root = true + +[*] +insert_final_newline = true + +[{*.kt,*.kts}] +ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL +ktlint_code_style = android_studio +ktlint_function_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than = 3 +max_line_length = 80 + +ktlint_function_naming_ignore_when_annotated_with = Composable + +ij_kotlin_allow_trailing_comma = true +ij_kotlin_allow_trailing_comma_on_call_site = true + +# Disable wildcard imports entirely with the following three configurations +ij_kotlin_name_count_to_use_star_import = 2147483647 +ij_kotlin_name_count_to_use_star_import_for_members = 2147483647 +ij_kotlin_packages_to_use_import_on_demand = unset diff --git a/example/app/build.gradle.kts b/example/app/build.gradle.kts index 5447b6e..82421c1 100644 --- a/example/app/build.gradle.kts +++ b/example/app/build.gradle.kts @@ -25,7 +25,7 @@ android { isMinifyEnabled = false proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" + "proguard-rules.pro", ) // Don't do this, just just to test signingConfig = signingConfigs.getByName("debug") @@ -70,4 +70,4 @@ dependencies { androidTestImplementation(libs.androidx.ui.test.junit4) debugImplementation(libs.androidx.ui.tooling) debugImplementation(libs.androidx.ui.test.manifest) -} \ No newline at end of file +} diff --git a/example/app/src/androidTest/java/com/exxeta/mobilesecuritytoolkitexample/ExampleInstrumentedTest.kt b/example/app/src/androidTest/java/com/exxeta/mobilesecuritytoolkitexample/ExampleInstrumentedTest.kt index 2ad8a17..d736373 100644 --- a/example/app/src/androidTest/java/com/exxeta/mobilesecuritytoolkitexample/ExampleInstrumentedTest.kt +++ b/example/app/src/androidTest/java/com/exxeta/mobilesecuritytoolkitexample/ExampleInstrumentedTest.kt @@ -1,13 +1,11 @@ package com.exxeta.mobilesecuritytoolkitexample -import androidx.test.platform.app.InstrumentationRegistry import androidx.test.ext.junit.runners.AndroidJUnit4 - +import androidx.test.platform.app.InstrumentationRegistry +import org.junit.Assert.* import org.junit.Test import org.junit.runner.RunWith -import org.junit.Assert.* - /** * Instrumented test, which will execute on an Android device. * @@ -19,6 +17,9 @@ class ExampleInstrumentedTest { fun useAppContext() { // Context of the app under test. val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("com.exxeta.mobilesecuritytoolkitexample", appContext.packageName) + assertEquals( + "com.exxeta.mobilesecuritytoolkitexample", + appContext.packageName, + ) } -} \ No newline at end of file +} diff --git a/example/app/src/main/java/com/exxeta/mobilesecuritytoolkitexample/MainActivity.kt b/example/app/src/main/java/com/exxeta/mobilesecuritytoolkitexample/MainActivity.kt index 1c04f9e..85b2a46 100644 --- a/example/app/src/main/java/com/exxeta/mobilesecuritytoolkitexample/MainActivity.kt +++ b/example/app/src/main/java/com/exxeta/mobilesecuritytoolkitexample/MainActivity.kt @@ -4,7 +4,6 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge -import androidx.compose.ui.platform.LocalContext import com.exxeta.mobilesecuritytoolkitexample.ui.theme.MobileSecurityToolkitExampleTheme class MainActivity : ComponentActivity() { @@ -17,4 +16,4 @@ class MainActivity : ComponentActivity() { } } } -} \ No newline at end of file +} diff --git a/example/app/src/main/java/com/exxeta/mobilesecuritytoolkitexample/ThreatStatusList.kt b/example/app/src/main/java/com/exxeta/mobilesecuritytoolkitexample/ThreatStatusList.kt index b7c53e8..ca847c3 100644 --- a/example/app/src/main/java/com/exxeta/mobilesecuritytoolkitexample/ThreatStatusList.kt +++ b/example/app/src/main/java/com/exxeta/mobilesecuritytoolkitexample/ThreatStatusList.kt @@ -28,7 +28,6 @@ import com.exxeta.securitytoolkit.ThreatDetectionCenter @Composable fun ThreatStatusList() { - val context = LocalContext.current val detectionCenter = ThreatDetectionCenter(context) detectionCenter.threats @@ -46,7 +45,9 @@ fun ThreatStatusList() { ThreatStatus( "Root", "Is a way of acquiring privileged control over the operating system of a device. Tools such as Magisk or Shadow can hide the privileged access", - reportedThreats.contains(ThreatDetectionCenter.Threat.ROOT_PRIVILEGES), + reportedThreats.contains( + ThreatDetectionCenter.Threat.ROOT_PRIVILEGES, + ), ), ThreatStatus( "Hooks", @@ -62,7 +63,7 @@ fun ThreatStatusList() { LazyColumn( modifier = Modifier - .padding(16.dp) + .padding(16.dp), ) { item { Spacer(modifier = Modifier.height(48.dp)) @@ -71,7 +72,7 @@ fun ThreatStatusList() { contentDescription = "stethoscope_24px", modifier = Modifier .fillMaxWidth() - .size(80.dp) + .size(80.dp), ) Spacer(modifier = Modifier.height(24.dp)) Text( @@ -80,7 +81,7 @@ fun ThreatStatusList() { modifier = Modifier .fillMaxWidth() .padding(bottom = 8.dp), - textAlign = TextAlign.Center + textAlign = TextAlign.Center, ) Text( text = "Here is a list of the threats that could put you at risk", @@ -89,7 +90,7 @@ fun ThreatStatusList() { .fillMaxWidth() .padding(bottom = 16.dp), textAlign = TextAlign.Center, - color = Color.Gray + color = Color.Gray, ) } items(threats) { item -> @@ -98,7 +99,7 @@ fun ThreatStatusList() { .padding(8.dp) .fillMaxWidth(), shape = MaterialTheme.shapes.medium, - elevation = CardDefaults.cardElevation(1.dp) + elevation = CardDefaults.cardElevation(1.dp), ) { ThreatStatusRow(threatStatus = item) } @@ -112,4 +113,4 @@ private fun Preview() { MobileSecurityToolkitExampleTheme { ThreatStatusList() } -} \ No newline at end of file +} diff --git a/example/app/src/main/java/com/exxeta/mobilesecuritytoolkitexample/ThreatStatusRow.kt b/example/app/src/main/java/com/exxeta/mobilesecuritytoolkitexample/ThreatStatusRow.kt index fe847bd..ddd2024 100644 --- a/example/app/src/main/java/com/exxeta/mobilesecuritytoolkitexample/ThreatStatusRow.kt +++ b/example/app/src/main/java/com/exxeta/mobilesecuritytoolkitexample/ThreatStatusRow.kt @@ -46,7 +46,9 @@ fun ThreatStatusRow(threatStatus: ThreatStatus) { Box( modifier = Modifier .clip(RoundedCornerShape(CornerSize(size = 4.dp))) - .background(if (threatStatus.isDetected) redColor else greenColor) + .background( + if (threatStatus.isDetected) redColor else greenColor, + ), ) { Text( text = if (threatStatus.isDetected) "DETECTED" else "SAFE", @@ -54,14 +56,14 @@ fun ThreatStatusRow(threatStatus: ThreatStatus) { style = MaterialTheme.typography.bodySmall, modifier = Modifier .padding(vertical = 2.dp) - .padding(horizontal = 8.dp) + .padding(horizontal = 8.dp), ) } } Text( threatStatus.description, style = MaterialTheme.typography.titleMedium, - color = Color.Gray + color = Color.Gray, ) } } @@ -72,8 +74,10 @@ private fun Preview() { MobileSecurityToolkitExampleTheme { ThreatStatusRow( ThreatStatus( - "Jailbreak", "Description", false, - ) + "Jailbreak", + "Description", + false, + ), ) } -} \ No newline at end of file +} diff --git a/example/app/src/main/java/com/exxeta/mobilesecuritytoolkitexample/ui/theme/Color.kt b/example/app/src/main/java/com/exxeta/mobilesecuritytoolkitexample/ui/theme/Color.kt index 6a3458e..0a0ef84 100644 --- a/example/app/src/main/java/com/exxeta/mobilesecuritytoolkitexample/ui/theme/Color.kt +++ b/example/app/src/main/java/com/exxeta/mobilesecuritytoolkitexample/ui/theme/Color.kt @@ -8,4 +8,4 @@ val Pink80 = Color(0xFFEFB8C8) val Purple40 = Color(0xFF6650a4) val PurpleGrey40 = Color(0xFF625b71) -val Pink40 = Color(0xFF7D5260) \ No newline at end of file +val Pink40 = Color(0xFF7D5260) diff --git a/example/app/src/main/java/com/exxeta/mobilesecuritytoolkitexample/ui/theme/Theme.kt b/example/app/src/main/java/com/exxeta/mobilesecuritytoolkitexample/ui/theme/Theme.kt index b2645f5..f615d63 100644 --- a/example/app/src/main/java/com/exxeta/mobilesecuritytoolkitexample/ui/theme/Theme.kt +++ b/example/app/src/main/java/com/exxeta/mobilesecuritytoolkitexample/ui/theme/Theme.kt @@ -1,6 +1,5 @@ package com.exxeta.mobilesecuritytoolkitexample.ui.theme -import android.app.Activity import android.os.Build import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.MaterialTheme @@ -14,13 +13,13 @@ import androidx.compose.ui.platform.LocalContext private val DarkColorScheme = darkColorScheme( primary = Purple80, secondary = PurpleGrey80, - tertiary = Pink80 + tertiary = Pink80, ) private val LightColorScheme = lightColorScheme( primary = Purple40, secondary = PurpleGrey40, - tertiary = Pink40 + tertiary = Pink40, /* Other default colors to override background = Color(0xFFFFFBFE), @@ -30,7 +29,7 @@ private val LightColorScheme = lightColorScheme( onTertiary = Color.White, onBackground = Color(0xFF1C1B1F), onSurface = Color(0xFF1C1B1F), - */ + */ ) @Composable @@ -38,20 +37,26 @@ fun MobileSecurityToolkitExampleTheme( darkTheme: Boolean = isSystemInDarkTheme(), // Dynamic color is available on Android 12+ dynamicColor: Boolean = true, - content: @Composable () -> Unit + content: @Composable () -> Unit, ) { val colorScheme = when { - dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { - val context = LocalContext.current - if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) - } - darkTheme -> DarkColorScheme - else -> LightColorScheme + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) { + dynamicDarkColorScheme( + context, + ) + } else { + dynamicLightColorScheme(context) + } + } + darkTheme -> DarkColorScheme + else -> LightColorScheme } MaterialTheme( - colorScheme = colorScheme, - typography = Typography, - content = content + colorScheme = colorScheme, + typography = Typography, + content = content, ) -} \ No newline at end of file +} diff --git a/example/app/src/main/java/com/exxeta/mobilesecuritytoolkitexample/ui/theme/Type.kt b/example/app/src/main/java/com/exxeta/mobilesecuritytoolkitexample/ui/theme/Type.kt index d8cd65d..0652f9c 100644 --- a/example/app/src/main/java/com/exxeta/mobilesecuritytoolkitexample/ui/theme/Type.kt +++ b/example/app/src/main/java/com/exxeta/mobilesecuritytoolkitexample/ui/theme/Type.kt @@ -13,8 +13,8 @@ val Typography = Typography( fontWeight = FontWeight.Normal, fontSize = 16.sp, lineHeight = 24.sp, - letterSpacing = 0.5.sp - ) + letterSpacing = 0.5.sp, + ), /* Other default text styles to override titleLarge = TextStyle( fontFamily = FontFamily.Default, @@ -30,5 +30,5 @@ val Typography = Typography( lineHeight = 16.sp, letterSpacing = 0.5.sp ) - */ -) \ No newline at end of file + */ +) diff --git a/example/app/src/test/java/com/exxeta/mobilesecuritytoolkitexample/ExampleUnitTest.kt b/example/app/src/test/java/com/exxeta/mobilesecuritytoolkitexample/ExampleUnitTest.kt index 0c747c7..a217a34 100644 --- a/example/app/src/test/java/com/exxeta/mobilesecuritytoolkitexample/ExampleUnitTest.kt +++ b/example/app/src/test/java/com/exxeta/mobilesecuritytoolkitexample/ExampleUnitTest.kt @@ -1,8 +1,7 @@ package com.exxeta.mobilesecuritytoolkitexample -import org.junit.Test - import org.junit.Assert.* +import org.junit.Test /** * Example local unit test, which will execute on the development machine (host). @@ -14,4 +13,4 @@ class ExampleUnitTest { fun addition_isCorrect() { assertEquals(4, 2 + 2) } -} \ No newline at end of file +} diff --git a/example/build.gradle.kts b/example/build.gradle.kts index f74b04b..c1e23bc 100644 --- a/example/build.gradle.kts +++ b/example/build.gradle.kts @@ -2,4 +2,4 @@ plugins { alias(libs.plugins.android.application) apply false alias(libs.plugins.jetbrains.kotlin.android) apply false -} \ No newline at end of file +} diff --git a/example/settings.gradle.kts b/example/settings.gradle.kts index 3520977..58f8415 100644 --- a/example/settings.gradle.kts +++ b/example/settings.gradle.kts @@ -25,4 +25,3 @@ dependencyResolutionManagement { rootProject.name = "Mobile Security Toolkit Example" include(":app") - \ No newline at end of file diff --git a/securitytoolkit/build.gradle.kts b/securitytoolkit/build.gradle.kts index 5a45351..580ed31 100644 --- a/securitytoolkit/build.gradle.kts +++ b/securitytoolkit/build.gradle.kts @@ -30,8 +30,8 @@ android { "x86", "x86_64", "armeabi-v7a", - "arm64-v8a" - ) + "arm64-v8a", + ), ) } } @@ -47,7 +47,7 @@ android { proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" + "proguard-rules.pro", ) } } @@ -70,11 +70,9 @@ dependencies { fun getPropertyOrEmpty(propertyName: String): String = project.findProperty(propertyName)?.toString().orEmpty() - project.version = getPropertyOrEmpty("VERSION_NAME") project.group = getPropertyOrEmpty("GROUP") - fun isReleaseBuild(): Boolean = !getPropertyOrEmpty("VERSION_NAME").contains("SNAPSHOT") @@ -130,7 +128,9 @@ publishing { } } repositories { - maven(url = if (isReleaseBuild()) getReleaseRepositoryUrl() else getSnapshotRepositoryUrl()) { + maven( + url = if (isReleaseBuild()) getReleaseRepositoryUrl() else getSnapshotRepositoryUrl(), + ) { credentials { username = getRepositoryUsername() password = getRepositoryPassword() diff --git a/securitytoolkit/src/main/java/com/exxeta/securitytoolkit/ThreatDetectionCenter.kt b/securitytoolkit/src/main/java/com/exxeta/securitytoolkit/ThreatDetectionCenter.kt index f2c508e..961cbc3 100644 --- a/securitytoolkit/src/main/java/com/exxeta/securitytoolkit/ThreatDetectionCenter.kt +++ b/securitytoolkit/src/main/java/com/exxeta/securitytoolkit/ThreatDetectionCenter.kt @@ -69,6 +69,6 @@ class ThreatDetectionCenter(private val context: Context) { public enum class Threat { ROOT_PRIVILEGES, HOOKS, - SIMULATOR + SIMULATOR, } -} \ No newline at end of file +} diff --git a/securitytoolkit/src/main/java/com/exxeta/securitytoolkit/internal/EmulatorDetector.kt b/securitytoolkit/src/main/java/com/exxeta/securitytoolkit/internal/EmulatorDetector.kt index a613007..60dc380 100644 --- a/securitytoolkit/src/main/java/com/exxeta/securitytoolkit/internal/EmulatorDetector.kt +++ b/securitytoolkit/src/main/java/com/exxeta/securitytoolkit/internal/EmulatorDetector.kt @@ -14,31 +14,34 @@ internal object EmulatorDetector { * * @return true if emulator detected */ - fun threatDetected(): Boolean { - return hasSuspiciousBuildConfiguration() || hasSuspiciousFiles() || Debug.isDebuggerConnected() - } + fun threatDetected(): Boolean = hasSuspiciousBuildConfiguration() || + hasSuspiciousFiles() || + Debug.isDebuggerConnected() - private fun hasSuspiciousBuildConfiguration(): Boolean { - return (Build.MANUFACTURER.contains("Genymotion") - || Build.MODEL.contains("google_sdk") - || Build.MODEL.lowercase().contains("droid4x") - || Build.MODEL.contains("Emulator") - || Build.MODEL.contains("Android SDK built for x86") - || Build.HARDWARE.contains("goldfish") - || Build.HARDWARE.contains("ranchu") - || Build.HARDWARE.contains("vbox86") - || Build.FINGERPRINT.startsWith("generic") - || Build.PRODUCT.contains("sdk") - || Build.PRODUCT.contains("google_sdk") - || Build.PRODUCT.contains("sdk_x86") - || Build.PRODUCT.contains("vbox86p") - || Build.HARDWARE.lowercase().contains("nox") - || Build.PRODUCT.lowercase().contains("nox") - || Build.BOARD.lowercase().contains("nox") - || (Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith( - "generic" - ))) - } + private fun hasSuspiciousBuildConfiguration(): Boolean = ( + Build.MANUFACTURER.contains("Genymotion") || + Build.MODEL.contains("google_sdk") || + Build.MODEL.lowercase().contains("droid4x") || + Build.MODEL.contains("Emulator") || + Build.MODEL.contains("Android SDK built for x86") || + Build.HARDWARE.contains("goldfish") || + Build.HARDWARE.contains("ranchu") || + Build.HARDWARE.contains("vbox86") || + Build.FINGERPRINT.startsWith("generic") || + Build.PRODUCT.contains("sdk") || + Build.PRODUCT.contains("google_sdk") || + Build.PRODUCT.contains("sdk_x86") || + Build.PRODUCT.contains("vbox86p") || + Build.HARDWARE.lowercase().contains("nox") || + Build.PRODUCT.lowercase().contains("nox") || + Build.BOARD.lowercase().contains("nox") || + ( + Build.BRAND.startsWith("generic") && + Build.DEVICE.startsWith( + "generic", + ) + ) + ) private fun hasSuspiciousFiles(): Boolean { val NOX_FILES = arrayOf("fstab.nox", "init.nox.rc", "ueventd.nox.rc") @@ -54,13 +57,15 @@ internal object EmulatorDetector { "fstab.ttVM_x86", "fstab.vbox86", "init.vbox86.rc", - "ueventd.vbox86.rc" + "ueventd.vbox86.rc", ) - return (checkFiles(GENY_FILES) - || checkFiles(ANDY_FILES) - || checkFiles(NOX_FILES) - || checkFiles(X86_FILES) - || checkFiles(PIPES)) + return ( + checkFiles(GENY_FILES) || + checkFiles(ANDY_FILES) || + checkFiles(NOX_FILES) || + checkFiles(X86_FILES) || + checkFiles(PIPES) + ) } private fun checkFiles(targets: Array): Boolean { @@ -72,4 +77,4 @@ internal object EmulatorDetector { } return false } -} \ No newline at end of file +} diff --git a/securitytoolkit/src/main/java/com/exxeta/securitytoolkit/internal/HooksDetection.kt b/securitytoolkit/src/main/java/com/exxeta/securitytoolkit/internal/HooksDetection.kt index f4cb9c8..25a2738 100644 --- a/securitytoolkit/src/main/java/com/exxeta/securitytoolkit/internal/HooksDetection.kt +++ b/securitytoolkit/src/main/java/com/exxeta/securitytoolkit/internal/HooksDetection.kt @@ -15,22 +15,18 @@ internal object HooksDetection { * * @return true if hooks are detected */ - fun threatDetected(): Boolean { - return isFridaServerListening() || isFridaLoaded() - } + fun threatDetected(): Boolean = isFridaServerListening() || isFridaLoaded() /** * Will check if frida is listening */ - private fun isFridaServerListening(): Boolean { - return try { - val address = InetSocketAddress("127.0.0.1", 27042) - val socket = Socket() - socket.connect(address, 200) - true - } catch (e: Throwable) { - false - } + private fun isFridaServerListening(): Boolean = try { + val address = InetSocketAddress("127.0.0.1", 27042) + val socket = Socket() + socket.connect(address, 200) + true + } catch (e: Throwable) { + false } /** @@ -48,4 +44,4 @@ internal object HooksDetection { return false } } -} \ No newline at end of file +} diff --git a/securitytoolkit/src/main/java/com/exxeta/securitytoolkit/internal/RootDetection.kt b/securitytoolkit/src/main/java/com/exxeta/securitytoolkit/internal/RootDetection.kt index f669974..9958caf 100644 --- a/securitytoolkit/src/main/java/com/exxeta/securitytoolkit/internal/RootDetection.kt +++ b/securitytoolkit/src/main/java/com/exxeta/securitytoolkit/internal/RootDetection.kt @@ -16,9 +16,7 @@ internal object RootDetection { * @return true if root detected */ @Throws(RuntimeException::class) - fun threatDetected(context: Context): Boolean { - return isRooted(context) - } + fun threatDetected(context: Context): Boolean = isRooted(context) /** * Performs check for Root @@ -30,9 +28,9 @@ internal object RootDetection { @Throws(RuntimeException::class) private fun isRooted(context: Context): Boolean { try { - return RootBeer(context).isRooted() + return RootBeer(context).isRooted } catch (e: Throwable) { throw RuntimeException("Could not check for root: $e") } } -} \ No newline at end of file +} From 83566847cadd9cafd9cb7f64b56f51756aa07fc1 Mon Sep 17 00:00:00 2001 From: Denis Dobanda Date: Fri, 31 Jan 2025 11:06:57 +0100 Subject: [PATCH 2/4] feat: password protection check added Signed-off-by: Denis Dobanda --- example/app/build.gradle.kts | 9 +++----- .../ThreatStatusList.kt | 7 ++++++ example/gradle/libs.versions.toml | 2 +- example/settings.gradle.kts | 12 ++++++---- jitpack.yml | 2 +- securitytoolkit/build.gradle.kts | 4 ++-- .../securitytoolkit/ThreatDetectionCenter.kt | 19 +++++++++++++-- .../internal/DevicePasscodeDetection.kt | 23 +++++++++++++++++++ 8 files changed, 62 insertions(+), 16 deletions(-) create mode 100644 securitytoolkit/src/main/java/com/exxeta/securitytoolkit/internal/DevicePasscodeDetection.kt diff --git a/example/app/build.gradle.kts b/example/app/build.gradle.kts index 82421c1..391e360 100644 --- a/example/app/build.gradle.kts +++ b/example/app/build.gradle.kts @@ -27,16 +27,13 @@ android { getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro", ) - // Don't do this, just just to test + // Don't do this, just to test signingConfig = signingConfigs.getByName("debug") } } compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 - } - kotlinOptions { - jvmTarget = "1.8" + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 } buildFeatures { compose = true diff --git a/example/app/src/main/java/com/exxeta/mobilesecuritytoolkitexample/ThreatStatusList.kt b/example/app/src/main/java/com/exxeta/mobilesecuritytoolkitexample/ThreatStatusList.kt index ca847c3..e020bbf 100644 --- a/example/app/src/main/java/com/exxeta/mobilesecuritytoolkitexample/ThreatStatusList.kt +++ b/example/app/src/main/java/com/exxeta/mobilesecuritytoolkitexample/ThreatStatusList.kt @@ -59,6 +59,13 @@ fun ThreatStatusList() { "Running the application in an Emulator", reportedThreats.contains(ThreatDetectionCenter.Threat.SIMULATOR), ), + ThreatStatus( + "Passcode", + "Indicates if current device is unprotected with a passcode. Biometric protection requires a passcode to be set up", + reportedThreats.contains( + ThreatDetectionCenter.Threat.DEVICE_WITHOUT_PASSCODE, + ), + ), ) LazyColumn( diff --git a/example/gradle/libs.versions.toml b/example/gradle/libs.versions.toml index 2fb0ccd..42584fd 100644 --- a/example/gradle/libs.versions.toml +++ b/example/gradle/libs.versions.toml @@ -25,7 +25,7 @@ androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-toolin androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } androidx-material3 = { group = "androidx.compose.material3", name = "material3" } -android-security-toolkit = { module = "com.github.EXXETA:Android-Security-Toolkit", version.ref = "androidSecurityToolkit" } +android-security-toolkit = { module = "com.local.project:securitytoolkit", version.ref = "androidSecurityToolkit" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } diff --git a/example/settings.gradle.kts b/example/settings.gradle.kts index 58f8415..c89975c 100644 --- a/example/settings.gradle.kts +++ b/example/settings.gradle.kts @@ -16,12 +16,16 @@ dependencyResolutionManagement { repositories { google() mavenCentral() - maven { - name = "Jitpack" - url = uri("https://jitpack.io") - } } } rootProject.name = "Mobile Security Toolkit Example" include(":app") + +includeBuild("../") { + dependencySubstitution { + substitute( + module("com.local.project:securitytoolkit"), + ).using(project(":securitytoolkit")) + } +} diff --git a/jitpack.yml b/jitpack.yml index 1e41e00..727c9ab 100644 --- a/jitpack.yml +++ b/jitpack.yml @@ -1,2 +1,2 @@ jdk: - - openjdk17 \ No newline at end of file + - openjdk21 diff --git a/securitytoolkit/build.gradle.kts b/securitytoolkit/build.gradle.kts index 580ed31..78a68c8 100644 --- a/securitytoolkit/build.gradle.kts +++ b/securitytoolkit/build.gradle.kts @@ -37,8 +37,8 @@ android { } compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 } buildTypes { diff --git a/securitytoolkit/src/main/java/com/exxeta/securitytoolkit/ThreatDetectionCenter.kt b/securitytoolkit/src/main/java/com/exxeta/securitytoolkit/ThreatDetectionCenter.kt index 961cbc3..9eaa33a 100644 --- a/securitytoolkit/src/main/java/com/exxeta/securitytoolkit/ThreatDetectionCenter.kt +++ b/securitytoolkit/src/main/java/com/exxeta/securitytoolkit/ThreatDetectionCenter.kt @@ -1,6 +1,7 @@ package com.exxeta.securitytoolkit import android.content.Context +import com.exxeta.securitytoolkit.internal.DevicePasscodeDetection import com.exxeta.securitytoolkit.internal.EmulatorDetector import com.exxeta.securitytoolkit.internal.HooksDetection import com.exxeta.securitytoolkit.internal.RootDetection @@ -12,12 +13,15 @@ import kotlinx.coroutines.flow.flow * variables * - [areRootPrivilegesDetected]: to check for root * - [areHooksDetected]: to check for injection (hooking) tweaks - * - [isSimulatorDetected]: to check if the app is running in a simulated environment + * - [isSimulatorDetected]: to check if the app is running in a simulated + * environment + * - [isDeviceWithoutPasscodeDetected]: to check if device is protected with a + * passcode * * Better (safer) way of detection threats is to use the [threats] Flow. * Subscribe to it and collect values - threats that were detected * - * @property context - Application Context, required to check for root + * @property context - Application Context, required for multiple checks */ class ThreatDetectionCenter(private val context: Context) { @@ -42,6 +46,13 @@ class ThreatDetectionCenter(private val context: Context) { val isSimulatorDetected: Boolean get() = EmulatorDetector.threatDetected() + /** + * Performs check for Device Passcode presence + * Returns `false`, when device is **unprotected** + */ + val isDeviceWithoutPasscodeDetected: Boolean + get() = DevicePasscodeDetection.threatDetected(context) + /** * Defines a better way to detect threats. Will contain every threat that * is detected @@ -61,6 +72,9 @@ class ThreatDetectionCenter(private val context: Context) { if (EmulatorDetector.threatDetected()) { emit(Threat.SIMULATOR) } + if (DevicePasscodeDetection.threatDetected(context)) { + emit(Threat.DEVICE_WITHOUT_PASSCODE) + } } /** @@ -70,5 +84,6 @@ class ThreatDetectionCenter(private val context: Context) { ROOT_PRIVILEGES, HOOKS, SIMULATOR, + DEVICE_WITHOUT_PASSCODE, } } diff --git a/securitytoolkit/src/main/java/com/exxeta/securitytoolkit/internal/DevicePasscodeDetection.kt b/securitytoolkit/src/main/java/com/exxeta/securitytoolkit/internal/DevicePasscodeDetection.kt new file mode 100644 index 0000000..31c39b8 --- /dev/null +++ b/securitytoolkit/src/main/java/com/exxeta/securitytoolkit/internal/DevicePasscodeDetection.kt @@ -0,0 +1,23 @@ +package com.exxeta.securitytoolkit.internal + +import android.app.KeyguardManager +import android.content.Context + +/** + * A Detector object for device passcode presence + */ +internal object DevicePasscodeDetection { + + /** + * Exposes public API to detect device passcode + * + * @return true if device passcode is absent + */ + fun threatDetected(context: Context): Boolean { + val keyguardManager = + context.getSystemService( + Context.KEYGUARD_SERVICE, + ) as KeyguardManager + return !keyguardManager.isDeviceSecure + } +} From 90cb8172beb230a33fad224509b266a2df096ca6 Mon Sep 17 00:00:00 2001 From: Denis Dobanda Date: Fri, 31 Jan 2025 14:18:49 +0100 Subject: [PATCH 3/4] feat: password protection check added Signed-off-by: Denis Dobanda --- README.md | 4 +- .../ThreatStatusList.kt | 7 ++ .../securitytoolkit/ThreatDetectionCenter.kt | 20 ++++ .../internal/HardwareSecurityDetection.kt | 102 ++++++++++++++++++ 4 files changed, 131 insertions(+), 2 deletions(-) create mode 100644 securitytoolkit/src/main/java/com/exxeta/securitytoolkit/internal/HardwareSecurityDetection.kt diff --git a/README.md b/README.md index c491da4..71c6252 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,8 @@ Already implemented Features are: - [x] Jailbreak or Root Detection - [x] Hooks Detection - [x] Simulator Detection +- [x] Device Passcode Check +- [x] Hardware Security Check You can see them in action with the [Example App](./example) we've provided @@ -101,9 +103,7 @@ Next features to be implemented: - [ ] App Signature Check - [ ] Debugger Detection -- [ ] Device Passcode Check - [ ] Integrity Check -- [ ] Hardware Security Check ## Contributing diff --git a/example/app/src/main/java/com/exxeta/mobilesecuritytoolkitexample/ThreatStatusList.kt b/example/app/src/main/java/com/exxeta/mobilesecuritytoolkitexample/ThreatStatusList.kt index e020bbf..0b7d81f 100644 --- a/example/app/src/main/java/com/exxeta/mobilesecuritytoolkitexample/ThreatStatusList.kt +++ b/example/app/src/main/java/com/exxeta/mobilesecuritytoolkitexample/ThreatStatusList.kt @@ -66,6 +66,13 @@ fun ThreatStatusList() { ThreatDetectionCenter.Threat.DEVICE_WITHOUT_PASSCODE, ), ), + ThreatStatus( + "Hardware protection", + "Refers to hardware capabilities of current device, specific to hardware-backed cryptography operations. If not available, no additional hardware security layer can be used when working with keys, certificates and keychain.", + reportedThreats.contains( + ThreatDetectionCenter.Threat.HARDWARE_PROTECTION_UNAVAILABLE, + ), + ), ) LazyColumn( diff --git a/securitytoolkit/src/main/java/com/exxeta/securitytoolkit/ThreatDetectionCenter.kt b/securitytoolkit/src/main/java/com/exxeta/securitytoolkit/ThreatDetectionCenter.kt index 9eaa33a..e2d94a7 100644 --- a/securitytoolkit/src/main/java/com/exxeta/securitytoolkit/ThreatDetectionCenter.kt +++ b/securitytoolkit/src/main/java/com/exxeta/securitytoolkit/ThreatDetectionCenter.kt @@ -3,6 +3,7 @@ package com.exxeta.securitytoolkit import android.content.Context import com.exxeta.securitytoolkit.internal.DevicePasscodeDetection import com.exxeta.securitytoolkit.internal.EmulatorDetector +import com.exxeta.securitytoolkit.internal.HardwareSecurityDetection import com.exxeta.securitytoolkit.internal.HooksDetection import com.exxeta.securitytoolkit.internal.RootDetection import kotlinx.coroutines.flow.Flow @@ -17,6 +18,8 @@ import kotlinx.coroutines.flow.flow * environment * - [isDeviceWithoutPasscodeDetected]: to check if device is protected with a * passcode + * - [isHardwareProtectionUnavailable]: to check if device can use + * hardware-backed cryptography * * Better (safer) way of detection threats is to use the [threats] Flow. * Subscribe to it and collect values - threats that were detected @@ -53,6 +56,15 @@ class ThreatDetectionCenter(private val context: Context) { val isDeviceWithoutPasscodeDetected: Boolean get() = DevicePasscodeDetection.threatDetected(context) + /** + * Performs check for hardware backed encryption presence + * Returns `false`, when device does **not** support hardware encryption + * + * @throws [RuntimeException] if any operations with KeyStore have failed + */ + val isHardwareProtectionUnavailable: Boolean + get() = HardwareSecurityDetection.threatDetected(context) + /** * Defines a better way to detect threats. Will contain every threat that * is detected @@ -75,6 +87,13 @@ class ThreatDetectionCenter(private val context: Context) { if (DevicePasscodeDetection.threatDetected(context)) { emit(Threat.DEVICE_WITHOUT_PASSCODE) } + try { + if (HardwareSecurityDetection.threatDetected(context)) { + emit(Threat.HARDWARE_PROTECTION_UNAVAILABLE) + } + } catch (_: Throwable) { + // TODO + } } /** @@ -85,5 +104,6 @@ class ThreatDetectionCenter(private val context: Context) { HOOKS, SIMULATOR, DEVICE_WITHOUT_PASSCODE, + HARDWARE_PROTECTION_UNAVAILABLE, } } diff --git a/securitytoolkit/src/main/java/com/exxeta/securitytoolkit/internal/HardwareSecurityDetection.kt b/securitytoolkit/src/main/java/com/exxeta/securitytoolkit/internal/HardwareSecurityDetection.kt new file mode 100644 index 0000000..ebb06a3 --- /dev/null +++ b/securitytoolkit/src/main/java/com/exxeta/securitytoolkit/internal/HardwareSecurityDetection.kt @@ -0,0 +1,102 @@ +package com.exxeta.securitytoolkit.internal + +import android.app.admin.DevicePolicyManager +import android.content.Context +import android.os.Build +import android.security.keystore.KeyGenParameterSpec +import android.security.keystore.KeyInfo +import android.security.keystore.KeyProperties +import java.security.KeyStore +import javax.crypto.KeyGenerator +import javax.crypto.SecretKey +import javax.crypto.SecretKeyFactory + +/** + * A Detector object for device passcode presence + */ +internal object HardwareSecurityDetection { + private const val ANDROID_KEY_STORE_NAME = "AndroidKeyStore" + + /** + * Exposes public API to detect hardware security + * + * @return true if device is not backed by hardware encryption + * @throws [RuntimeException] if keystore operations failed + */ + fun threatDetected(context: Context): Boolean = + !isEncrypted(context) || !supportHardwareBackedCryptography() + + // https://source.android.com/docs/security/features/encryption + private fun isEncrypted(context: Context): Boolean { + val devicePolicyManager = context + .getSystemService( + Context.DEVICE_POLICY_SERVICE, + ) as DevicePolicyManager + + val status = devicePolicyManager.storageEncryptionStatus + return setOf( + DevicePolicyManager.ENCRYPTION_STATUS_ACTIVE, + DevicePolicyManager.ENCRYPTION_STATUS_ACTIVE_PER_USER, + ).contains(status) + } + + // https://developer.android.com/privacy-and-security/keystore + private fun supportHardwareBackedCryptography(): Boolean = try { + val keyStore = KeyStore.getInstance(ANDROID_KEY_STORE_NAME) + keyStore.load(null) + + val alias = getNewAlias(keyStore) + val result = createSecretKey(alias).let(::checkSecretKey) + keyStore.deleteEntry(alias) + + result + } catch (e: Throwable) { + throw RuntimeException("Could not check hardware encryption: $e") + } + + private fun getNewAlias(keyStore: KeyStore): String = + (100000..999999).random().toString().repeat(5).let { + if (keyStore.containsAlias(it)) { + getNewAlias(keyStore) + } else { + it + } + } + + private fun createSecretKey(keyAlias: String): SecretKey { + val keyGenerator = KeyGenerator.getInstance( + KeyProperties.KEY_ALGORITHM_AES, + ANDROID_KEY_STORE_NAME, + ) + val keySpec: KeyGenParameterSpec = KeyGenParameterSpec.Builder( + keyAlias, + KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT, + ) + .setKeySize(256) + .setBlockModes(KeyProperties.BLOCK_MODE_CBC) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7) + .setRandomizedEncryptionRequired(true) + .setUserAuthenticationRequired(true) + .setIsStrongBoxBacked(true) + .build() + keyGenerator.init(keySpec) + return keyGenerator.generateKey() + } + + private fun checkSecretKey(key: SecretKey): Boolean { + val keyInfo = SecretKeyFactory.getInstance(key.algorithm) + .getKeySpec(key, KeyInfo::class.java) as KeyInfo + + return checkKeyInfo(keyInfo) + } + + private fun checkKeyInfo(keyInfo: KeyInfo): Boolean = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + setOf( + KeyProperties.SECURITY_LEVEL_TRUSTED_ENVIRONMENT, + KeyProperties.SECURITY_LEVEL_STRONGBOX, + ).contains(keyInfo.securityLevel) + } else { + keyInfo.isInsideSecureHardware + } +} From aba11d20382bcfe18a366967c2a05b71198544af Mon Sep 17 00:00:00 2001 From: Denis Dobanda Date: Fri, 31 Jan 2025 14:42:48 +0100 Subject: [PATCH 4/4] feat: app signature missmatch detection added Signed-off-by: Denis Dobanda --- README.md | 2 +- .../ThreatStatusList.kt | 7 ++- .../securitytoolkit/ThreatDetectionCenter.kt | 19 +++++++ .../internal/AppSignatureDetection.kt | 56 +++++++++++++++++++ 4 files changed, 82 insertions(+), 2 deletions(-) create mode 100644 securitytoolkit/src/main/java/com/exxeta/securitytoolkit/internal/AppSignatureDetection.kt diff --git a/README.md b/README.md index 71c6252..e432869 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ Already implemented Features are: - [x] Simulator Detection - [x] Device Passcode Check - [x] Hardware Security Check +- [x] App Signature Check You can see them in action with the [Example App](./example) we've provided @@ -101,7 +102,6 @@ reportedThreats.contains(ThreatDetectionCenter.Threat.SIMULATOR) Next features to be implemented: -- [ ] App Signature Check - [ ] Debugger Detection - [ ] Integrity Check diff --git a/example/app/src/main/java/com/exxeta/mobilesecuritytoolkitexample/ThreatStatusList.kt b/example/app/src/main/java/com/exxeta/mobilesecuritytoolkitexample/ThreatStatusList.kt index 0b7d81f..52c3660 100644 --- a/example/app/src/main/java/com/exxeta/mobilesecuritytoolkitexample/ThreatStatusList.kt +++ b/example/app/src/main/java/com/exxeta/mobilesecuritytoolkitexample/ThreatStatusList.kt @@ -68,11 +68,16 @@ fun ThreatStatusList() { ), ThreatStatus( "Hardware protection", - "Refers to hardware capabilities of current device, specific to hardware-backed cryptography operations. If not available, no additional hardware security layer can be used when working with keys, certificates and keychain.", + "Refers to hardware capabilities of current device, specific to hardware-backed cryptography operations. If not available, no additional hardware security layer can be used when working with keys, certificates and keychain", reportedThreats.contains( ThreatDetectionCenter.Threat.HARDWARE_PROTECTION_UNAVAILABLE, ), ), + ThreatStatus( + "Signature missmatch", + "Expects app to be signed with a given certificate. For PlayStore should match the one provided by the store via Play Console", + detectionCenter.hasAppSignatureMissmatch("INVALID"), + ), ) LazyColumn( diff --git a/securitytoolkit/src/main/java/com/exxeta/securitytoolkit/ThreatDetectionCenter.kt b/securitytoolkit/src/main/java/com/exxeta/securitytoolkit/ThreatDetectionCenter.kt index e2d94a7..daba9a8 100644 --- a/securitytoolkit/src/main/java/com/exxeta/securitytoolkit/ThreatDetectionCenter.kt +++ b/securitytoolkit/src/main/java/com/exxeta/securitytoolkit/ThreatDetectionCenter.kt @@ -1,6 +1,7 @@ package com.exxeta.securitytoolkit import android.content.Context +import com.exxeta.securitytoolkit.internal.AppSignatureDetection import com.exxeta.securitytoolkit.internal.DevicePasscodeDetection import com.exxeta.securitytoolkit.internal.EmulatorDetector import com.exxeta.securitytoolkit.internal.HardwareSecurityDetection @@ -20,6 +21,8 @@ import kotlinx.coroutines.flow.flow * passcode * - [isHardwareProtectionUnavailable]: to check if device can use * hardware-backed cryptography + * - [hasAppSignatureMissmatch]: extract current app signature and match against + * expected one * * Better (safer) way of detection threats is to use the [threats] Flow. * Subscribe to it and collect values - threats that were detected @@ -65,6 +68,21 @@ class ThreatDetectionCenter(private val context: Context) { val isHardwareProtectionUnavailable: Boolean get() = HardwareSecurityDetection.threatDetected(context) + /** + * Performs check of app signature (signing certificate SHA-256 hash) + * Returns `false`, if expected and current signature does not match + * + * More: https://stackoverflow.com/questions/38558623/how-to-find-signature-of-apk-file#61807617 + * + * > When distributing via PlayStore, use the SHA-256 Hash from Play Console + * + * > Otherwise find App signature with apktool + * + * @throws [RuntimeException] if failed to extract current signature + */ + fun hasAppSignatureMissmatch(expectedSignature: String): Boolean = + AppSignatureDetection.threatDetected(context, expectedSignature) + /** * Defines a better way to detect threats. Will contain every threat that * is detected @@ -94,6 +112,7 @@ class ThreatDetectionCenter(private val context: Context) { } catch (_: Throwable) { // TODO } + // TODO: Add app signature check, after ThreatDetectionCenter refactoring } /** diff --git a/securitytoolkit/src/main/java/com/exxeta/securitytoolkit/internal/AppSignatureDetection.kt b/securitytoolkit/src/main/java/com/exxeta/securitytoolkit/internal/AppSignatureDetection.kt new file mode 100644 index 0000000..8477853 --- /dev/null +++ b/securitytoolkit/src/main/java/com/exxeta/securitytoolkit/internal/AppSignatureDetection.kt @@ -0,0 +1,56 @@ +package com.exxeta.securitytoolkit.internal + +import android.content.Context +import android.content.pm.PackageInfo +import android.content.pm.PackageManager +import java.security.MessageDigest + +/** + * A Detector object for app signature differences + */ +internal object AppSignatureDetection { + private const val SHA_256 = "SHA-256" + + /** + * Exposes public API to assert expected app signature + * + * @return true if app signature same as expected + * @throws [RuntimeException] if failed to extract current signature + */ + fun threatDetected(context: Context, expectedHash: String): Boolean = + getSigningCertHash(context) != expectedHash + + /** + * Will return the SHA-256 Hash of the signing certificate + */ + @Throws(RuntimeException::class) + private fun getSigningCertHash(context: Context): String? { + try { + val info: PackageInfo = context.packageManager.getPackageInfo( + context.packageName, + PackageManager.GET_SIGNING_CERTIFICATES, + ) + val signers = info.signingInfo?.apkContentsSigners + if (signers.isNullOrEmpty()) { + return null + } + val signature = signers.first() + + val sha = MessageDigest.getInstance(SHA_256) + sha.update(signature.toByteArray()) + val hash = sha.digest() + + return bytesToHex(hash) + } catch (e: Throwable) { + throw RuntimeException("Could not get signing cert hash: $e") + } + } + + private fun bytesToHex(bytes: ByteArray): String { + val builder = StringBuilder() + for (aByte in bytes) { + builder.append(String.format("%02x", aByte)) + } + return builder.toString() + } +}