@cap-kit/integrity 8.0.0-next.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CapKitIntegrity.podspec +17 -0
- package/LICENSE +21 -0
- package/Package.swift +26 -0
- package/README.md +1104 -0
- package/android/build.gradle +104 -0
- package/android/src/main/AndroidManifest.xml +21 -0
- package/android/src/main/java/io/capkit/integrity/IntegrityCheckOptions.kt +37 -0
- package/android/src/main/java/io/capkit/integrity/IntegrityConfig.kt +59 -0
- package/android/src/main/java/io/capkit/integrity/IntegrityError.kt +40 -0
- package/android/src/main/java/io/capkit/integrity/IntegrityImpl.kt +319 -0
- package/android/src/main/java/io/capkit/integrity/IntegrityPlugin.kt +475 -0
- package/android/src/main/java/io/capkit/integrity/IntegrityReportBuilder.kt +130 -0
- package/android/src/main/java/io/capkit/integrity/IntegritySignalBuilder.kt +72 -0
- package/android/src/main/java/io/capkit/integrity/emulator/IntegrityEmulatorChecks.kt +38 -0
- package/android/src/main/java/io/capkit/integrity/filesystem/IntegrityFilesystemChecks.kt +51 -0
- package/android/src/main/java/io/capkit/integrity/hook/IntegrityHookChecks.kt +61 -0
- package/android/src/main/java/io/capkit/integrity/remote/IntegrityRemoteAttestor.kt +49 -0
- package/android/src/main/java/io/capkit/integrity/root/IntegrityRootDetector.kt +136 -0
- package/android/src/main/java/io/capkit/integrity/runtime/IntegrityRuntimeChecks.kt +87 -0
- package/android/src/main/java/io/capkit/integrity/ui/IntegrityBlockActivity.kt +173 -0
- package/android/src/main/java/io/capkit/integrity/ui/IntegrityUISignals.kt +57 -0
- package/android/src/main/java/io/capkit/integrity/utils/IntegrityLogger.kt +85 -0
- package/android/src/main/java/io/capkit/integrity/utils/IntegrityUtils.kt +105 -0
- package/android/src/main/res/.gitkeep +0 -0
- package/android/src/main/res/values/styles.xml +5 -0
- package/dist/docs.json +598 -0
- package/dist/esm/definitions.d.ts +554 -0
- package/dist/esm/definitions.js +56 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/index.d.ts +15 -0
- package/dist/esm/index.js +16 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/web.d.ts +32 -0
- package/dist/esm/web.js +51 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs.js +130 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +133 -0
- package/dist/plugin.js.map +1 -0
- package/ios/Sources/IntegrityPlugin/IntegrityCheckOptions.swift +41 -0
- package/ios/Sources/IntegrityPlugin/IntegrityConfig.swift +135 -0
- package/ios/Sources/IntegrityPlugin/IntegrityEntitlementChecks.swift +58 -0
- package/ios/Sources/IntegrityPlugin/IntegrityError.swift +49 -0
- package/ios/Sources/IntegrityPlugin/IntegrityImpl.swift +397 -0
- package/ios/Sources/IntegrityPlugin/IntegrityPlugin.swift +345 -0
- package/ios/Sources/IntegrityPlugin/IntegrityReportBuilder.swift +184 -0
- package/ios/Sources/IntegrityPlugin/Utils/IntegrityLogger.swift +69 -0
- package/ios/Sources/IntegrityPlugin/Utils/IntegrityUtils.swift +144 -0
- package/ios/Sources/IntegrityPlugin/Version.swift +16 -0
- package/ios/Sources/IntegrityPlugin/filesystem/IntegrityFilesystemChecks.swift +86 -0
- package/ios/Sources/IntegrityPlugin/hook/IntegrityHookChecks.swift +85 -0
- package/ios/Sources/IntegrityPlugin/jailbreak/IntegrityJailbreakDetector.swift +74 -0
- package/ios/Sources/IntegrityPlugin/jailbreak/IntegrityJailbreakUrlSchemeDetector.swift +42 -0
- package/ios/Sources/IntegrityPlugin/remote/IntegrityRemoteAttestor.swift +40 -0
- package/ios/Sources/IntegrityPlugin/runtime/IntegrityRuntimeChecks.swift +63 -0
- package/ios/Sources/IntegrityPlugin/simulator/IntegritySimulatorChecks.swift +20 -0
- package/ios/Sources/IntegrityPlugin/ui/IntegrityBlockViewController.swift +143 -0
- package/ios/Tests/IntegrityPluginTests/IntegrityPluginTests.swift +10 -0
- package/package.json +106 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
package io.capkit.integrity.filesystem
|
|
2
|
+
|
|
3
|
+
import io.capkit.integrity.IntegrityCheckOptions
|
|
4
|
+
import io.capkit.integrity.IntegritySignalBuilder
|
|
5
|
+
import java.io.File
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Performs filesystem integrity checks related to sandbox escape
|
|
9
|
+
* and suspicious directory permissions.
|
|
10
|
+
*/
|
|
11
|
+
object IntegrityFilesystemChecks {
|
|
12
|
+
/**
|
|
13
|
+
* Attempts to detect sandbox escape by checking write access
|
|
14
|
+
* to protected system locations without performing mutations.
|
|
15
|
+
*
|
|
16
|
+
* NOTE:
|
|
17
|
+
* - This check is best-effort and heuristic-based
|
|
18
|
+
* - No filesystem writes are performed (read-only probes)
|
|
19
|
+
*/
|
|
20
|
+
fun checkSandboxEscape(options: IntegrityCheckOptions): Map<String, Any>? {
|
|
21
|
+
val protectedPaths =
|
|
22
|
+
listOf(
|
|
23
|
+
"/system",
|
|
24
|
+
"/system/bin",
|
|
25
|
+
"/system/xbin",
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
return try {
|
|
29
|
+
for (path in protectedPaths) {
|
|
30
|
+
val file = File(path)
|
|
31
|
+
if (file.exists() && file.canWrite()) {
|
|
32
|
+
return IntegritySignalBuilder.build(
|
|
33
|
+
id = "android_sandbox_escaped",
|
|
34
|
+
category = "tamper",
|
|
35
|
+
confidence = "high",
|
|
36
|
+
description = "Write access detected on protected system directory",
|
|
37
|
+
metadata = mapOf("path" to path),
|
|
38
|
+
options = options,
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
null
|
|
43
|
+
} catch (_: SecurityException) {
|
|
44
|
+
// Access denied → expected in a secure environment
|
|
45
|
+
null
|
|
46
|
+
} catch (_: Exception) {
|
|
47
|
+
// Expected failure in a secure environment
|
|
48
|
+
null
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
package io.capkit.integrity.hook
|
|
2
|
+
|
|
3
|
+
import io.capkit.integrity.IntegrityError
|
|
4
|
+
import java.io.File
|
|
5
|
+
import java.net.InetSocketAddress
|
|
6
|
+
import java.net.Socket
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Detects instrumentation frameworks (like Frida) via memory inspection
|
|
10
|
+
* and local network port scanning.
|
|
11
|
+
*/
|
|
12
|
+
object IntegrityHookChecks {
|
|
13
|
+
/**
|
|
14
|
+
* Frida detection via process memory map inspection.
|
|
15
|
+
* Looks for known library artifacts in /proc/self/maps.
|
|
16
|
+
*/
|
|
17
|
+
fun checkFridaMemory(): Boolean {
|
|
18
|
+
return try {
|
|
19
|
+
val mapsFile = File("/proc/self/maps")
|
|
20
|
+
if (mapsFile.exists()) {
|
|
21
|
+
mapsFile.useLines { lines ->
|
|
22
|
+
lines.any { it.contains("frida", ignoreCase = true) || it.contains("gadget", ignoreCase = true) }
|
|
23
|
+
}
|
|
24
|
+
} else {
|
|
25
|
+
false
|
|
26
|
+
}
|
|
27
|
+
} catch (e: SecurityException) {
|
|
28
|
+
// Swallowed to allow other checks to complete
|
|
29
|
+
false
|
|
30
|
+
} catch (_: Exception) {
|
|
31
|
+
false
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Detects known Frida server ports on localhost.
|
|
37
|
+
*
|
|
38
|
+
* @throws IntegrityError.Unavailable If socket access is restricted.
|
|
39
|
+
*/
|
|
40
|
+
fun checkFridaPorts(): Boolean {
|
|
41
|
+
val ports = listOf(27042, 27043)
|
|
42
|
+
val timeoutMs = 1000
|
|
43
|
+
|
|
44
|
+
return ports.any { port ->
|
|
45
|
+
try {
|
|
46
|
+
Socket().use { socket ->
|
|
47
|
+
// Use a timeout to prevent long-running blocking calls
|
|
48
|
+
socket.connect(InetSocketAddress("127.0.0.1", port), timeoutMs)
|
|
49
|
+
true
|
|
50
|
+
}
|
|
51
|
+
} catch (e: SecurityException) {
|
|
52
|
+
throw IntegrityError.Unavailable(
|
|
53
|
+
"Socket access denied while checking Frida ports.",
|
|
54
|
+
)
|
|
55
|
+
} catch (_: Exception) {
|
|
56
|
+
// Connection failed or timed out, which is expected in clean environments
|
|
57
|
+
false
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
package io.capkit.integrity.remote
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import io.capkit.integrity.IntegrityCheckOptions
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Handles remote attestation signals (Play Integrity API).
|
|
8
|
+
*
|
|
9
|
+
* IMPORTANT:
|
|
10
|
+
* - Play Integrity is NOT implemented yet.
|
|
11
|
+
* - Unavailability is reported explicitly.
|
|
12
|
+
* - The emitted signal is observational only and LOW confidence.
|
|
13
|
+
*/
|
|
14
|
+
object IntegrityRemoteAttestor {
|
|
15
|
+
/**
|
|
16
|
+
* Returns a LOW confidence signal indicating that Play Integrity
|
|
17
|
+
* attestation is not implemented or not available.
|
|
18
|
+
*
|
|
19
|
+
* The signal is emitted only when strict mode is requested.
|
|
20
|
+
*/
|
|
21
|
+
fun getPlayIntegritySignal(
|
|
22
|
+
context: Context,
|
|
23
|
+
options: IntegrityCheckOptions,
|
|
24
|
+
): Map<String, Any>? {
|
|
25
|
+
if (options.level != "strict") {
|
|
26
|
+
return null
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
val signal =
|
|
30
|
+
mutableMapOf<String, Any>(
|
|
31
|
+
"id" to "android_play_integrity_unavailable",
|
|
32
|
+
"category" to "environment",
|
|
33
|
+
"confidence" to "low",
|
|
34
|
+
"metadata" to
|
|
35
|
+
mapOf(
|
|
36
|
+
"attestation" to "unsupported",
|
|
37
|
+
"provider" to "play_integrity",
|
|
38
|
+
"reason" to "not_implemented",
|
|
39
|
+
),
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
if (options.includeDebugInfo) {
|
|
43
|
+
signal["description"] =
|
|
44
|
+
"Google Play Integrity attestation is not implemented or not available"
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return signal
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
package io.capkit.integrity.root
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.content.pm.PackageManager
|
|
5
|
+
import android.os.Build
|
|
6
|
+
import io.capkit.integrity.IntegrityCheckOptions
|
|
7
|
+
import io.capkit.integrity.IntegrityError
|
|
8
|
+
import io.capkit.integrity.IntegritySignalBuilder
|
|
9
|
+
import java.io.File
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Performs root detection using filesystem heuristics and package inspection.
|
|
13
|
+
*/
|
|
14
|
+
object IntegrityRootDetector {
|
|
15
|
+
private val suPaths =
|
|
16
|
+
listOf(
|
|
17
|
+
"/system/bin/su", "/system/xbin/su", "/sbin/su",
|
|
18
|
+
"/system/app/Superuser.apk", "/system/app/Superuser/Superuser.apk",
|
|
19
|
+
"/data/local/xbin/su", "/data/local/bin/su", "/system/sd/xbin/su",
|
|
20
|
+
"/su/bin/su", "/magisk/.core/bin/su", "/system/usr/we-need-root/su-backup/su",
|
|
21
|
+
"/system/bin/.ext/.su/su", "/system/bin/failsafe/su", "/data/local/su",
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
private val rootPackages =
|
|
25
|
+
listOf(
|
|
26
|
+
"com.noshufou.android.su",
|
|
27
|
+
"com.thirdparty.superuser",
|
|
28
|
+
"eu.chainfire.supersu",
|
|
29
|
+
"com.koushikdutta.superuser",
|
|
30
|
+
"com.zachareew.systemuituner",
|
|
31
|
+
"com.topjohnwu.magisk",
|
|
32
|
+
"com.alephzain.framaroot",
|
|
33
|
+
"org.adaway",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Cache process-lifetime for deterministic root signals.
|
|
38
|
+
*
|
|
39
|
+
* IMPORTANT:
|
|
40
|
+
* - Used ONLY for runtime checks
|
|
41
|
+
* - Boot-time checks MUST bypass this cache
|
|
42
|
+
*/
|
|
43
|
+
private var cachedRootSignals: List<Map<String, Any>>? = null
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Performs root detection using filesystem heuristics and build metadata.
|
|
47
|
+
*
|
|
48
|
+
* @param allowCache
|
|
49
|
+
* - true → reuse cached signals if available (runtime path)
|
|
50
|
+
* - false → force fresh detection (boot path)
|
|
51
|
+
*/
|
|
52
|
+
fun checkRootSignals(
|
|
53
|
+
options: IntegrityCheckOptions,
|
|
54
|
+
allowCache: Boolean = true,
|
|
55
|
+
): List<Map<String, Any>> {
|
|
56
|
+
if (allowCache) {
|
|
57
|
+
cachedRootSignals?.let { return it }
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
val signals = mutableListOf<Map<String, Any>>()
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
for (path in suPaths) {
|
|
64
|
+
if (File(path).exists()) {
|
|
65
|
+
signals.add(
|
|
66
|
+
IntegritySignalBuilder.build(
|
|
67
|
+
id = "android_root_su",
|
|
68
|
+
category = "root",
|
|
69
|
+
confidence = "high",
|
|
70
|
+
description = "Presence of su binary detected in system paths",
|
|
71
|
+
metadata = mapOf("path" to path),
|
|
72
|
+
options = options,
|
|
73
|
+
),
|
|
74
|
+
)
|
|
75
|
+
break
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
} catch (e: SecurityException) {
|
|
79
|
+
throw IntegrityError.Unavailable("Filesystem access denied while performing root checks.")
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (Build.TAGS?.contains("test-keys") == true) {
|
|
83
|
+
signals.add(
|
|
84
|
+
IntegritySignalBuilder.build(
|
|
85
|
+
id = "android_test_keys",
|
|
86
|
+
category = "root",
|
|
87
|
+
confidence = "medium",
|
|
88
|
+
description = "Device build signed with test keys",
|
|
89
|
+
metadata = mapOf("tags" to Build.TAGS),
|
|
90
|
+
options = options,
|
|
91
|
+
),
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (allowCache) {
|
|
96
|
+
cachedRootSignals = signals
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return signals
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Checks for the presence of known root management applications.
|
|
104
|
+
*/
|
|
105
|
+
fun checkRootPackages(
|
|
106
|
+
context: Context,
|
|
107
|
+
options: IntegrityCheckOptions,
|
|
108
|
+
): List<Map<String, Any>> {
|
|
109
|
+
val signals = mutableListOf<Map<String, Any>>()
|
|
110
|
+
val pm = context.packageManager
|
|
111
|
+
|
|
112
|
+
for (pkg in rootPackages) {
|
|
113
|
+
try {
|
|
114
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
115
|
+
pm.getPackageInfo(pkg, PackageManager.PackageInfoFlags.of(0))
|
|
116
|
+
} else {
|
|
117
|
+
@Suppress("DEPRECATION")
|
|
118
|
+
pm.getPackageInfo(pkg, 0)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
signals.add(
|
|
122
|
+
IntegritySignalBuilder.build(
|
|
123
|
+
id = "android_root_package",
|
|
124
|
+
category = "root",
|
|
125
|
+
confidence = "high",
|
|
126
|
+
description = "Detected known root management or related application",
|
|
127
|
+
metadata = mapOf("package" to pkg),
|
|
128
|
+
options = options,
|
|
129
|
+
),
|
|
130
|
+
)
|
|
131
|
+
} catch (_: PackageManager.NameNotFoundException) {
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return signals
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
package io.capkit.integrity.runtime
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.content.pm.PackageManager
|
|
5
|
+
import android.os.Debug
|
|
6
|
+
import io.capkit.integrity.IntegrityCheckOptions
|
|
7
|
+
import io.capkit.integrity.IntegrityError
|
|
8
|
+
import io.capkit.integrity.IntegritySignalBuilder
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Runtime integrity checks related to debugging conditions
|
|
12
|
+
* and application signing integrity.
|
|
13
|
+
*/
|
|
14
|
+
object IntegrityRuntimeChecks {
|
|
15
|
+
/**
|
|
16
|
+
* Detects debugging conditions such as attached debuggers or debuggable flags.
|
|
17
|
+
*/
|
|
18
|
+
fun checkDebugSignals(
|
|
19
|
+
context: Context,
|
|
20
|
+
options: IntegrityCheckOptions,
|
|
21
|
+
): List<Map<String, Any>> {
|
|
22
|
+
val debugSignals = mutableListOf<Map<String, Any>>()
|
|
23
|
+
val isDebuggerConnected = Debug.isDebuggerConnected()
|
|
24
|
+
val isDebuggable = (context.applicationInfo.flags and android.content.pm.ApplicationInfo.FLAG_DEBUGGABLE) != 0
|
|
25
|
+
|
|
26
|
+
if (isDebuggerConnected) {
|
|
27
|
+
debugSignals.add(
|
|
28
|
+
IntegritySignalBuilder.build(
|
|
29
|
+
id = "android_debugger_attached",
|
|
30
|
+
category = "debug",
|
|
31
|
+
confidence = "high",
|
|
32
|
+
description = "A debugger is currently attached to the running process",
|
|
33
|
+
metadata = mapOf("method" to "Debug.isDebuggerConnected"),
|
|
34
|
+
options = options,
|
|
35
|
+
),
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (isDebuggable) {
|
|
40
|
+
debugSignals.add(
|
|
41
|
+
IntegritySignalBuilder.build(
|
|
42
|
+
id = "android_runtime_debuggable",
|
|
43
|
+
category = "debug",
|
|
44
|
+
confidence = "medium",
|
|
45
|
+
description = "Process is debuggable at runtime",
|
|
46
|
+
metadata = mapOf("flag" to "FLAG_DEBUGGABLE"),
|
|
47
|
+
options = options,
|
|
48
|
+
),
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return debugSignals
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Performs a basic application signature integrity check.
|
|
57
|
+
*/
|
|
58
|
+
fun checkAppSignature(context: Context): Boolean {
|
|
59
|
+
return try {
|
|
60
|
+
val packageInfo =
|
|
61
|
+
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) {
|
|
62
|
+
context.packageManager.getPackageInfo(
|
|
63
|
+
context.packageName,
|
|
64
|
+
PackageManager.GET_SIGNING_CERTIFICATES,
|
|
65
|
+
)
|
|
66
|
+
} else {
|
|
67
|
+
@Suppress("DEPRECATION")
|
|
68
|
+
context.packageManager.getPackageInfo(
|
|
69
|
+
context.packageName,
|
|
70
|
+
PackageManager.GET_SIGNATURES,
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
val signingInfo =
|
|
75
|
+
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) {
|
|
76
|
+
packageInfo.signingInfo?.apkContentsSigners
|
|
77
|
+
} else {
|
|
78
|
+
@Suppress("DEPRECATION")
|
|
79
|
+
packageInfo.signatures
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
signingInfo?.isNotEmpty() == true
|
|
83
|
+
} catch (e: Exception) {
|
|
84
|
+
throw IntegrityError.InitFailed("Failed to read application signing information.")
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
package io.capkit.integrity.ui
|
|
2
|
+
|
|
3
|
+
import android.annotation.SuppressLint
|
|
4
|
+
import android.os.Build
|
|
5
|
+
import android.os.Bundle
|
|
6
|
+
import android.webkit.WebView
|
|
7
|
+
import android.widget.LinearLayout
|
|
8
|
+
import androidx.appcompat.app.AppCompatActivity
|
|
9
|
+
import androidx.appcompat.widget.Toolbar
|
|
10
|
+
import java.io.BufferedReader
|
|
11
|
+
import java.io.InputStreamReader
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Dedicated activity used to present the integrity block page.
|
|
15
|
+
*
|
|
16
|
+
* Responsibilities:
|
|
17
|
+
* - Display a developer-provided HTML block page
|
|
18
|
+
* - Support query parameters (e.g. "reason")
|
|
19
|
+
* - Optionally allow dismissal via native UI controls
|
|
20
|
+
*
|
|
21
|
+
* Security note:
|
|
22
|
+
* - The block page is NOT dismissible by default
|
|
23
|
+
* - Dismissal must be explicitly enabled by the host application
|
|
24
|
+
*/
|
|
25
|
+
class IntegrityBlockActivity : AppCompatActivity() {
|
|
26
|
+
/**
|
|
27
|
+
* Whether the block page can be dismissed by the user.
|
|
28
|
+
*
|
|
29
|
+
* Defaults to false (secure-by-default).
|
|
30
|
+
*/
|
|
31
|
+
private var dismissible: Boolean = false
|
|
32
|
+
|
|
33
|
+
@SuppressLint("SetJavaScriptEnabled")
|
|
34
|
+
override fun onCreate(savedInstanceState: Bundle?) {
|
|
35
|
+
super.onCreate(savedInstanceState)
|
|
36
|
+
|
|
37
|
+
// -------------------------------------------------------------------------
|
|
38
|
+
// Read options from Intent
|
|
39
|
+
// -------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
dismissible = intent.getBooleanExtra("dismissible", false)
|
|
42
|
+
|
|
43
|
+
// -------------------------------------------------------------------------
|
|
44
|
+
// Back button handling (modern API)
|
|
45
|
+
// -------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
if (!dismissible) {
|
|
48
|
+
onBackPressedDispatcher.addCallback(
|
|
49
|
+
this,
|
|
50
|
+
object : androidx.activity.OnBackPressedCallback(true) {
|
|
51
|
+
override fun handleOnBackPressed() {
|
|
52
|
+
// Intentionally disabled (secure-by-default)
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// -------------------------------------------------------------------------
|
|
59
|
+
// Root layout (vertical)
|
|
60
|
+
// -------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
val root =
|
|
63
|
+
LinearLayout(this).apply {
|
|
64
|
+
orientation = LinearLayout.VERTICAL
|
|
65
|
+
layoutParams =
|
|
66
|
+
LinearLayout.LayoutParams(
|
|
67
|
+
LinearLayout.LayoutParams.MATCH_PARENT,
|
|
68
|
+
LinearLayout.LayoutParams.MATCH_PARENT,
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// -------------------------------------------------------------------------
|
|
73
|
+
// Optional native toolbar (dismissible only)
|
|
74
|
+
// -------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
if (dismissible) {
|
|
77
|
+
val toolbar =
|
|
78
|
+
Toolbar(this).apply {
|
|
79
|
+
setNavigationIcon(android.R.drawable.ic_menu_close_clear_cancel)
|
|
80
|
+
setNavigationOnClickListener { finish() }
|
|
81
|
+
title = ""
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
root.addView(toolbar)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// -------------------------------------------------------------------------
|
|
88
|
+
// WebView setup
|
|
89
|
+
// -------------------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
val webView =
|
|
92
|
+
WebView(this).apply {
|
|
93
|
+
settings.javaScriptEnabled = true
|
|
94
|
+
settings.allowContentAccess = false
|
|
95
|
+
settings.domStorageEnabled = false
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Fill remaining space
|
|
99
|
+
val webViewParams =
|
|
100
|
+
LinearLayout.LayoutParams(
|
|
101
|
+
LinearLayout.LayoutParams.MATCH_PARENT,
|
|
102
|
+
0,
|
|
103
|
+
1f,
|
|
104
|
+
)
|
|
105
|
+
root.addView(webView, webViewParams)
|
|
106
|
+
|
|
107
|
+
setContentView(root)
|
|
108
|
+
|
|
109
|
+
// -------------------------------------------------------------------------
|
|
110
|
+
// URL handling
|
|
111
|
+
// -------------------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
// The plugin always passes the URL explicitly via Intent extras
|
|
114
|
+
val url = intent.getStringExtra("url") ?: return
|
|
115
|
+
|
|
116
|
+
// Remote URLs (http / https) are loaded directly
|
|
117
|
+
if (url.startsWith("http")) {
|
|
118
|
+
webView.loadUrl(url)
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// -------------------------------------------------------------------------
|
|
123
|
+
// Local asset loading with query support
|
|
124
|
+
// -------------------------------------------------------------------------
|
|
125
|
+
|
|
126
|
+
// Example:
|
|
127
|
+
// url = "public/integrity-block.html?reason=integrity_failed"
|
|
128
|
+
val assetPath = url.substringBefore("?")
|
|
129
|
+
val query = url.substringAfter("?", "")
|
|
130
|
+
|
|
131
|
+
// Read the HTML asset manually
|
|
132
|
+
val html = readAsset(assetPath)
|
|
133
|
+
|
|
134
|
+
// Use a synthetic base URL so that:
|
|
135
|
+
// - window.location.search is populated
|
|
136
|
+
// - relative paths continue to work
|
|
137
|
+
webView.loadDataWithBaseURL(
|
|
138
|
+
"file:///android_asset/$assetPath?$query",
|
|
139
|
+
html,
|
|
140
|
+
"text/html",
|
|
141
|
+
"UTF-8",
|
|
142
|
+
null,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
// -------------------------------------------------------------------------
|
|
146
|
+
// Back navigation blocking
|
|
147
|
+
// -------------------------------------------------------------------------
|
|
148
|
+
|
|
149
|
+
// Disable back gesture on Android 13+ (API 33+)
|
|
150
|
+
if (!dismissible && Build.VERSION.SDK_INT >= 33) {
|
|
151
|
+
onBackInvokedDispatcher.registerOnBackInvokedCallback(
|
|
152
|
+
android.window.OnBackInvokedDispatcher.PRIORITY_DEFAULT,
|
|
153
|
+
) {
|
|
154
|
+
// Intentionally disabled
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
// Utilities
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Reads a file from the android_asset directory.
|
|
165
|
+
*
|
|
166
|
+
* @param path Relative asset path (e.g. "public/integrity-block.html")
|
|
167
|
+
*/
|
|
168
|
+
private fun readAsset(path: String): String {
|
|
169
|
+
val inputStream = assets.open(path)
|
|
170
|
+
val reader = BufferedReader(InputStreamReader(inputStream))
|
|
171
|
+
return reader.use { it.readText() }
|
|
172
|
+
}
|
|
173
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
package io.capkit.integrity.ui
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.view.accessibility.AccessibilityManager
|
|
5
|
+
import io.capkit.integrity.IntegrityCheckOptions
|
|
6
|
+
import io.capkit.integrity.IntegritySignalBuilder
|
|
7
|
+
import io.capkit.integrity.IntegritySignalIds
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Detects UI-level attacks such as screen overlays and
|
|
11
|
+
* suspicious accessibility services.
|
|
12
|
+
*/
|
|
13
|
+
object IntegrityUISignals {
|
|
14
|
+
/**
|
|
15
|
+
* Checks for potential overlay attacks (Tapjacking).
|
|
16
|
+
* Combines accessibility service monitoring and window state heuristics.
|
|
17
|
+
*/
|
|
18
|
+
fun checkOverlaySignals(
|
|
19
|
+
context: Context,
|
|
20
|
+
options: IntegrityCheckOptions,
|
|
21
|
+
): Map<String, Any>? {
|
|
22
|
+
val signals = mutableMapOf<String, Any>()
|
|
23
|
+
val am = context.getSystemService(Context.ACCESSIBILITY_SERVICE) as AccessibilityManager
|
|
24
|
+
|
|
25
|
+
// Heuristic 1: Active Accessibility Services with Touch Exploration
|
|
26
|
+
// These services can read screen content and inject touches.
|
|
27
|
+
val isEnabled = am.isEnabled
|
|
28
|
+
val isTouchExplorationEnabled = am.isTouchExplorationEnabled
|
|
29
|
+
|
|
30
|
+
if (isEnabled && isTouchExplorationEnabled) {
|
|
31
|
+
return IntegritySignalBuilder.build(
|
|
32
|
+
id = IntegritySignalIds.ANDROID_OVERLAY_DETECTED,
|
|
33
|
+
category = "tamper",
|
|
34
|
+
confidence = "medium",
|
|
35
|
+
description = "Suspicious accessibility service state detected (potential overlay/UI spying)",
|
|
36
|
+
metadata =
|
|
37
|
+
mapOf(
|
|
38
|
+
"accessibility_enabled" to isEnabled,
|
|
39
|
+
"touch_exploration_enabled" to isTouchExplorationEnabled,
|
|
40
|
+
"source" to "AccessibilityManager",
|
|
41
|
+
),
|
|
42
|
+
options = options,
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Heuristic 2: Detection via Window Focus (Passive Check)
|
|
47
|
+
// If the application is active but lacks focus, an overlay might be on top.
|
|
48
|
+
// This is a passive indicator used to increase the overall tamper score.
|
|
49
|
+
return null
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Recommended native security practice:
|
|
54
|
+
* Developers should also set 'setFilterTouchesWhenObscured(true)'
|
|
55
|
+
* in their main View to prevent touches when an overlay is present.
|
|
56
|
+
*/
|
|
57
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
package io.capkit.integrity.utils
|
|
2
|
+
|
|
3
|
+
import android.util.Log
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Centralized logging utility for the Integrity plugin.
|
|
7
|
+
*
|
|
8
|
+
* This logging provides a single entry point for all native logs
|
|
9
|
+
* and supports runtime-controlled verbose logging.
|
|
10
|
+
*
|
|
11
|
+
* The goal is to avoid scattering `if (verbose)` checks across
|
|
12
|
+
* business logic and keep logging behavior consistent.
|
|
13
|
+
*/
|
|
14
|
+
object IntegrityLogger {
|
|
15
|
+
/**
|
|
16
|
+
* Logcat tag used for all plugin logs.
|
|
17
|
+
* Helps filtering logs during debugging.
|
|
18
|
+
*/
|
|
19
|
+
private const val TAG = "⚡️ Integrity"
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Controls whether debug logs are printed.
|
|
23
|
+
*
|
|
24
|
+
* This flag should be set once during plugin initialization
|
|
25
|
+
* based on configuration values.
|
|
26
|
+
*/
|
|
27
|
+
var verbose: Boolean = false
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Prints a debug / verbose log message.
|
|
31
|
+
*
|
|
32
|
+
* This method should be used for development-time diagnostics
|
|
33
|
+
* and is automatically silenced when [verbose] is false.
|
|
34
|
+
*
|
|
35
|
+
* @param messages One or more message fragments to be concatenated.
|
|
36
|
+
*/
|
|
37
|
+
fun debug(vararg messages: String) {
|
|
38
|
+
if (verbose) {
|
|
39
|
+
log(TAG, Log.DEBUG, *messages)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Prints an error log message.
|
|
45
|
+
*
|
|
46
|
+
* Error logs are always printed regardless of [verbose] state.
|
|
47
|
+
*
|
|
48
|
+
* @param message Human-readable error description.
|
|
49
|
+
* @param e Optional exception for stack trace logging.
|
|
50
|
+
*/
|
|
51
|
+
fun error(
|
|
52
|
+
message: String,
|
|
53
|
+
e: Throwable? = null,
|
|
54
|
+
) {
|
|
55
|
+
val sb = StringBuilder(message)
|
|
56
|
+
if (e != null) {
|
|
57
|
+
sb.append(" | Error: ").append(e.message)
|
|
58
|
+
}
|
|
59
|
+
Log.e(TAG, sb.toString(), e)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Internal low-level log dispatcher.
|
|
64
|
+
*
|
|
65
|
+
* Joins message fragments and forwards them to Android's Log API
|
|
66
|
+
* using the specified priority.
|
|
67
|
+
*/
|
|
68
|
+
fun log(
|
|
69
|
+
tag: String,
|
|
70
|
+
level: Int,
|
|
71
|
+
vararg messages: String,
|
|
72
|
+
) {
|
|
73
|
+
val sb = StringBuilder()
|
|
74
|
+
for (msg in messages) {
|
|
75
|
+
sb.append(msg).append(" ")
|
|
76
|
+
}
|
|
77
|
+
when (level) {
|
|
78
|
+
Log.DEBUG -> Log.d(tag, sb.toString())
|
|
79
|
+
Log.INFO -> Log.i(tag, sb.toString())
|
|
80
|
+
Log.WARN -> Log.w(tag, sb.toString())
|
|
81
|
+
Log.ERROR -> Log.e(tag, sb.toString())
|
|
82
|
+
else -> Log.v(tag, sb.toString())
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|