@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.
Files changed (59) hide show
  1. package/CapKitIntegrity.podspec +17 -0
  2. package/LICENSE +21 -0
  3. package/Package.swift +26 -0
  4. package/README.md +1104 -0
  5. package/android/build.gradle +104 -0
  6. package/android/src/main/AndroidManifest.xml +21 -0
  7. package/android/src/main/java/io/capkit/integrity/IntegrityCheckOptions.kt +37 -0
  8. package/android/src/main/java/io/capkit/integrity/IntegrityConfig.kt +59 -0
  9. package/android/src/main/java/io/capkit/integrity/IntegrityError.kt +40 -0
  10. package/android/src/main/java/io/capkit/integrity/IntegrityImpl.kt +319 -0
  11. package/android/src/main/java/io/capkit/integrity/IntegrityPlugin.kt +475 -0
  12. package/android/src/main/java/io/capkit/integrity/IntegrityReportBuilder.kt +130 -0
  13. package/android/src/main/java/io/capkit/integrity/IntegritySignalBuilder.kt +72 -0
  14. package/android/src/main/java/io/capkit/integrity/emulator/IntegrityEmulatorChecks.kt +38 -0
  15. package/android/src/main/java/io/capkit/integrity/filesystem/IntegrityFilesystemChecks.kt +51 -0
  16. package/android/src/main/java/io/capkit/integrity/hook/IntegrityHookChecks.kt +61 -0
  17. package/android/src/main/java/io/capkit/integrity/remote/IntegrityRemoteAttestor.kt +49 -0
  18. package/android/src/main/java/io/capkit/integrity/root/IntegrityRootDetector.kt +136 -0
  19. package/android/src/main/java/io/capkit/integrity/runtime/IntegrityRuntimeChecks.kt +87 -0
  20. package/android/src/main/java/io/capkit/integrity/ui/IntegrityBlockActivity.kt +173 -0
  21. package/android/src/main/java/io/capkit/integrity/ui/IntegrityUISignals.kt +57 -0
  22. package/android/src/main/java/io/capkit/integrity/utils/IntegrityLogger.kt +85 -0
  23. package/android/src/main/java/io/capkit/integrity/utils/IntegrityUtils.kt +105 -0
  24. package/android/src/main/res/.gitkeep +0 -0
  25. package/android/src/main/res/values/styles.xml +5 -0
  26. package/dist/docs.json +598 -0
  27. package/dist/esm/definitions.d.ts +554 -0
  28. package/dist/esm/definitions.js +56 -0
  29. package/dist/esm/definitions.js.map +1 -0
  30. package/dist/esm/index.d.ts +15 -0
  31. package/dist/esm/index.js +16 -0
  32. package/dist/esm/index.js.map +1 -0
  33. package/dist/esm/web.d.ts +32 -0
  34. package/dist/esm/web.js +51 -0
  35. package/dist/esm/web.js.map +1 -0
  36. package/dist/plugin.cjs.js +130 -0
  37. package/dist/plugin.cjs.js.map +1 -0
  38. package/dist/plugin.js +133 -0
  39. package/dist/plugin.js.map +1 -0
  40. package/ios/Sources/IntegrityPlugin/IntegrityCheckOptions.swift +41 -0
  41. package/ios/Sources/IntegrityPlugin/IntegrityConfig.swift +135 -0
  42. package/ios/Sources/IntegrityPlugin/IntegrityEntitlementChecks.swift +58 -0
  43. package/ios/Sources/IntegrityPlugin/IntegrityError.swift +49 -0
  44. package/ios/Sources/IntegrityPlugin/IntegrityImpl.swift +397 -0
  45. package/ios/Sources/IntegrityPlugin/IntegrityPlugin.swift +345 -0
  46. package/ios/Sources/IntegrityPlugin/IntegrityReportBuilder.swift +184 -0
  47. package/ios/Sources/IntegrityPlugin/Utils/IntegrityLogger.swift +69 -0
  48. package/ios/Sources/IntegrityPlugin/Utils/IntegrityUtils.swift +144 -0
  49. package/ios/Sources/IntegrityPlugin/Version.swift +16 -0
  50. package/ios/Sources/IntegrityPlugin/filesystem/IntegrityFilesystemChecks.swift +86 -0
  51. package/ios/Sources/IntegrityPlugin/hook/IntegrityHookChecks.swift +85 -0
  52. package/ios/Sources/IntegrityPlugin/jailbreak/IntegrityJailbreakDetector.swift +74 -0
  53. package/ios/Sources/IntegrityPlugin/jailbreak/IntegrityJailbreakUrlSchemeDetector.swift +42 -0
  54. package/ios/Sources/IntegrityPlugin/remote/IntegrityRemoteAttestor.swift +40 -0
  55. package/ios/Sources/IntegrityPlugin/runtime/IntegrityRuntimeChecks.swift +63 -0
  56. package/ios/Sources/IntegrityPlugin/simulator/IntegritySimulatorChecks.swift +20 -0
  57. package/ios/Sources/IntegrityPlugin/ui/IntegrityBlockViewController.swift +143 -0
  58. package/ios/Tests/IntegrityPluginTests/IntegrityPluginTests.swift +10 -0
  59. package/package.json +106 -0
@@ -0,0 +1,475 @@
1
+ package io.capkit.integrity
2
+
3
+ import android.content.BroadcastReceiver
4
+ import android.content.Context
5
+ import android.content.Intent
6
+ import android.content.IntentFilter
7
+ import android.net.Uri
8
+ import android.os.Build
9
+ import androidx.core.content.ContextCompat
10
+ import com.getcapacitor.JSObject
11
+ import com.getcapacitor.Plugin
12
+ import com.getcapacitor.PluginCall
13
+ import com.getcapacitor.PluginMethod
14
+ import com.getcapacitor.annotation.CapacitorPlugin
15
+ import com.getcapacitor.annotation.Permission
16
+ import io.capkit.integrity.utils.IntegrityLogger
17
+ import io.capkit.integrity.utils.IntegrityUtils
18
+
19
+ /**
20
+ * Capacitor bridge for the Integrity plugin (Android).
21
+ *
22
+ * CONTRACT:
23
+ * - This class is the ONLY entry point from JavaScript.
24
+ * - All PluginCall instances MUST be resolved or rejected exactly once.
25
+ *
26
+ * Responsibilities:
27
+ * - Parse JavaScript input
28
+ * - Invoke the native implementation
29
+ * - Resolve or reject PluginCall exactly once
30
+ * - Map native IntegrityError to JS-facing error codes
31
+ *
32
+ * Forbidden:
33
+ * - Platform-specific business logic
34
+ * - Direct system API usage outside lifecycle-bound orchestration
35
+ * - Throwing uncaught exceptions
36
+ */
37
+ @CapacitorPlugin(
38
+ name = "Integrity",
39
+ permissions = [
40
+ Permission(
41
+ alias = "network",
42
+ strings = [android.Manifest.permission.INTERNET],
43
+ ),
44
+ ],
45
+ )
46
+ class IntegrityPlugin : Plugin() {
47
+ // ---------------------------------------------------------------------------
48
+ // Properties
49
+ // ---------------------------------------------------------------------------
50
+
51
+ /**
52
+ * Immutable plugin configuration.
53
+ *
54
+ * CONTRACT:
55
+ * - Initialized exactly once in `load()`
56
+ * - Treated as read-only afterwards
57
+ * - MUST NOT be mutated at runtime
58
+ * - MUST NOT be accessed by the Impl layer
59
+ */
60
+ private lateinit var config: IntegrityConfig
61
+
62
+ /**
63
+ * Native implementation layer.
64
+ *
65
+ * CONTRACT:
66
+ * - Owned by the Plugin layer
67
+ * - Lifetime == plugin lifetime
68
+ * - MUST NOT access PluginCall or Capacitor APIs
69
+ * - MUST NOT perform UI operations
70
+ */
71
+ private lateinit var implementation: IntegrityImpl
72
+
73
+ // ---------------------------------------------------------------------------
74
+ // Event-related properties
75
+ // ---------------------------------------------------------------------------
76
+
77
+ /**
78
+ * In-memory buffer for integrity signals detected before
79
+ * a JavaScript listener is registered.
80
+ *
81
+ * Thread-safe implementation to prevent race conditions during
82
+ * asynchronous event emission.
83
+ */
84
+ private val bufferedSignals = java.util.Collections.synchronizedList(mutableListOf<JSObject>())
85
+
86
+ /**
87
+ * BroadcastReceiver used to observe passive system events
88
+ * relevant for integrity monitoring.
89
+ */
90
+ private lateinit var eventReceiver: IntegrityEventReceiver
91
+
92
+ private companion object {
93
+ /**
94
+ * Canonical event name emitted to the JavaScript layer.
95
+ *
96
+ * CONTRACT:
97
+ * - MUST remain stable across releases
98
+ * - MUST match the JS-side event subscription name
99
+ */
100
+ private const val EVENT_INTEGRITY_SIGNAL = "integritySignal"
101
+ }
102
+
103
+ // ---------------------------------------------------------------------------
104
+ // Lifecycle
105
+ // ---------------------------------------------------------------------------
106
+
107
+ /**
108
+ * Called once when the plugin is loaded by the Capacitor bridge.
109
+ *
110
+ * CONTRACT:
111
+ * - Called exactly once
112
+ * - This is the ONLY valid place to:
113
+ * - read static configuration
114
+ * - initialize the native implementation
115
+ * - inject configuration into the implementation
116
+ * - register system event listeners (BroadcastReceivers)
117
+ *
118
+ * WARNING:
119
+ * - Re-initializing config or implementation outside this method
120
+ * is considered a plugin defect.
121
+ */
122
+ override fun load() {
123
+ super.load()
124
+
125
+ config = IntegrityConfig(this)
126
+ implementation = IntegrityImpl(context)
127
+ implementation.updateConfig(config)
128
+ registerEventReceiver()
129
+ }
130
+
131
+ /**
132
+ * Custom addListener implementation to flush early boot signals.
133
+ *
134
+ * PURPOSE:
135
+ * - Ensures that signals captured during the early boot phase or
136
+ * while the app was in the background are delivered as soon as
137
+ * the JavaScript side registers a listener.
138
+ */
139
+ @PluginMethod
140
+ override fun addListener(call: PluginCall) {
141
+ val eventName = call.getString("eventName")
142
+ super.addListener(call)
143
+
144
+ if (eventName == EVENT_INTEGRITY_SIGNAL) {
145
+ flushBufferedSignals()
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Called when the host Activity resumes.
151
+ *
152
+ * PURPOSE:
153
+ * - Flush any integrity signals captured while no JS listeners
154
+ * were registered.
155
+ * - Perform a targeted check for debugger attachment during resume.
156
+ */
157
+ override fun handleOnResume() {
158
+ super.handleOnResume()
159
+ flushBufferedSignals()
160
+
161
+ // Immediate check for debugger attachment upon resume (Real-time monitor)
162
+ if (android.os.Debug.isDebuggerConnected()) {
163
+ val options = IntegrityCheckOptions(level = "standard", includeDebugInfo = false)
164
+ execute {
165
+ try {
166
+ val result = implementation.performCheck(options)
167
+ val jsResult = IntegrityUtils.toJSObject(result)
168
+ emitOrBufferSignal(jsResult)
169
+ } catch (e: Exception) {
170
+ IntegrityLogger.error("Real-time monitor: Debugger check failed: ${e.message}")
171
+ }
172
+ }
173
+ }
174
+ }
175
+
176
+ /**
177
+ * Flushes all buffered integrity signals to JavaScript listeners.
178
+ * Ensures FIFO delivery and prevents duplicate emissions.
179
+ *
180
+ * NOTE:
181
+ * - This method is thread-safe and idempotent.
182
+ * - Buffer is cleared immediately after dispatch.
183
+ */
184
+ private fun flushBufferedSignals() {
185
+ synchronized(bufferedSignals) {
186
+ if (hasListeners(EVENT_INTEGRITY_SIGNAL) && bufferedSignals.isNotEmpty()) {
187
+ IntegrityLogger.debug("Flushing ${bufferedSignals.size} buffered signals to JS")
188
+ val iterator = bufferedSignals.iterator()
189
+ while (iterator.hasNext()) {
190
+ val signal = iterator.next()
191
+ notifyListeners(EVENT_INTEGRITY_SIGNAL, signal, true)
192
+ iterator.remove()
193
+ }
194
+ }
195
+ }
196
+ }
197
+
198
+ /**
199
+ * Called when the plugin is being destroyed.
200
+ *
201
+ * CONTRACT:
202
+ * - All BroadcastReceivers registered by this plugin
203
+ * MUST be unregistered here.
204
+ */
205
+ override fun handleOnDestroy() {
206
+ super.handleOnDestroy()
207
+ unregisterEventReceiver()
208
+ }
209
+
210
+ // ---------------------------------------------------------------------------
211
+ // Event emission and buffering
212
+ // ---------------------------------------------------------------------------
213
+
214
+ /**
215
+ * Emits an integrity signal to JavaScript or buffers it
216
+ * if no listeners are currently registered.
217
+ *
218
+ * @param signal Fully-formed integrity signal payload.
219
+ */
220
+ private fun emitOrBufferSignal(signal: JSObject) {
221
+ synchronized(bufferedSignals) {
222
+ if (hasListeners(EVENT_INTEGRITY_SIGNAL)) {
223
+ notifyListeners(EVENT_INTEGRITY_SIGNAL, signal, true)
224
+ } else {
225
+ bufferedSignals.add(signal)
226
+ }
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Registers the BroadcastReceiver used for passive integrity signals.
232
+ *
233
+ * NOTE:
234
+ * - No polling or background services are introduced.
235
+ * - Observers are scoped strictly to the plugin lifecycle.
236
+ */
237
+ private fun registerEventReceiver() {
238
+ eventReceiver =
239
+ IntegrityEventReceiver { signal ->
240
+ emitOrBufferSignal(signal)
241
+ }
242
+
243
+ val intentFilter =
244
+ IntentFilter().apply {
245
+ addAction(Intent.ACTION_PACKAGE_ADDED)
246
+ addAction(Intent.ACTION_PACKAGE_REPLACED)
247
+ addDataScheme("package")
248
+ }
249
+
250
+ val flags =
251
+ // UPSIDE_DOWN_CAKE API LEVEL 34
252
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
253
+ ContextCompat.RECEIVER_NOT_EXPORTED
254
+ } else {
255
+ 0
256
+ }
257
+
258
+ // TIRAMISU API LEVEL 33
259
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
260
+ context.registerReceiver(eventReceiver, intentFilter, flags)
261
+ } else {
262
+ context.registerReceiver(eventReceiver, intentFilter)
263
+ }
264
+ }
265
+
266
+ /**
267
+ * Unregisters the integrity BroadcastReceiver.
268
+ *
269
+ * NOTE:
270
+ * - It is safe to call this method even if the receiver
271
+ * was never registered.
272
+ */
273
+ private fun unregisterEventReceiver() {
274
+ // It's safe to call unregisterReceiver even if the receiver was never registered
275
+ try {
276
+ context.unregisterReceiver(eventReceiver)
277
+ } catch (e: IllegalArgumentException) {
278
+ // Receiver wasn't registered, ignore
279
+ }
280
+ }
281
+
282
+ // ---------------------------------------------------------------------------
283
+ // Error mapping
284
+ // ---------------------------------------------------------------------------
285
+
286
+ /**
287
+ * Maps native IntegrityError values to JavaScript-facing error codes.
288
+ *
289
+ * CONTRACT:
290
+ * - This method is the ONLY place where native errors
291
+ * are translated into JS-visible failures.
292
+ * - Error codes MUST be:
293
+ * - stable
294
+ * - documented
295
+ * - identical across platforms
296
+ */
297
+ private fun reject(
298
+ call: PluginCall,
299
+ error: IntegrityError,
300
+ ) {
301
+ val code =
302
+ when (error) {
303
+ is IntegrityError.Unavailable -> "UNAVAILABLE"
304
+ is IntegrityError.PermissionDenied -> "PERMISSION_DENIED"
305
+ is IntegrityError.InitFailed -> "INIT_FAILED"
306
+ is IntegrityError.UnknownType -> "UNKNOWN_TYPE"
307
+ }
308
+
309
+ call.reject(error.message, code)
310
+ }
311
+
312
+ // ---------------------------------------------------------------------------
313
+ // Check
314
+ // ---------------------------------------------------------------------------
315
+
316
+ /**
317
+ * Executes an integrity check.
318
+ *
319
+ * CONTRACT:
320
+ * - Resolves exactly once on success
321
+ * - Rejects exactly once on failure
322
+ * - Never throws outside this method
323
+ *
324
+ * NOTE:
325
+ * - Option defaulting happens here by design.
326
+ * - The Impl layer MUST receive fully normalized options.
327
+ */
328
+ @PluginMethod
329
+ fun check(call: PluginCall) {
330
+ val options =
331
+ IntegrityCheckOptions(
332
+ level = call.getString("level") ?: "basic",
333
+ includeDebugInfo = call.getBoolean("includeDebugInfo") ?: false,
334
+ )
335
+
336
+ // Execute integrity checks off the plugin thread to avoid future ANR risks.
337
+ execute {
338
+ try {
339
+ val result = implementation.performCheck(options)
340
+ val jsResult = IntegrityUtils.toJSObject(result)
341
+ call.resolve(jsResult)
342
+ } catch (e: IntegrityError) {
343
+ reject(call, e)
344
+ } catch (e: Exception) {
345
+ call.reject(
346
+ "Unexpected native error during integrity check.",
347
+ "INIT_FAILED",
348
+ )
349
+ }
350
+ }
351
+ }
352
+
353
+ // ---------------------------------------------------------------------------
354
+ // PresentBlockPage
355
+ // ---------------------------------------------------------------------------
356
+
357
+ /**
358
+ * Presents the configured integrity block page, if enabled.
359
+ *
360
+ * CONTRACT:
361
+ * - This method NEVER decides when it should be called.
362
+ * - The decision is fully delegated to the host application.
363
+ *
364
+ * NOTE:
365
+ * - Returning `{ presented: false }` is NOT an error.
366
+ * - This allows deterministic branching on the JS side.
367
+ *
368
+ * WARNING:
369
+ * - UI navigation is allowed ONLY in the Plugin layer.
370
+ * - The Impl layer MUST NEVER start Activities.
371
+ */
372
+ @PluginMethod
373
+ fun presentBlockPage(call: PluginCall) {
374
+ if (!config.blockPageEnabled || config.blockPageUrl == null) {
375
+ call.resolve(JSObject().put("presented", false))
376
+ return
377
+ }
378
+
379
+ val reason = call.getString("reason")
380
+ val dismissible = call.getBoolean("dismissible") ?: false
381
+
382
+ val url =
383
+ if (reason != null) {
384
+ "${config.blockPageUrl}?reason=${Uri.encode(reason)}"
385
+ } else {
386
+ config.blockPageUrl
387
+ }
388
+
389
+ val intent =
390
+ Intent(
391
+ context,
392
+ io.capkit.integrity.ui.IntegrityBlockActivity::class.java,
393
+ ).apply {
394
+ putExtra("url", url)
395
+ putExtra("dismissible", dismissible)
396
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
397
+
398
+ // If not dismissible, clear the back stack
399
+ if (!dismissible) {
400
+ addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
401
+ }
402
+ }
403
+
404
+ context.startActivity(intent)
405
+
406
+ call.resolve(JSObject().put("presented", true))
407
+ }
408
+
409
+ // ---------------------------------------------------------------------------
410
+ // Version
411
+ // ---------------------------------------------------------------------------
412
+
413
+ /**
414
+ * Returns the native plugin version.
415
+ *
416
+ * NOTE:
417
+ * - Used exclusively for diagnostics and compatibility checks.
418
+ * - Must not be used for feature detection.
419
+ */
420
+ @PluginMethod
421
+ fun getPluginVersion(call: PluginCall) {
422
+ val ret = JSObject()
423
+ ret.put("version", BuildConfig.PLUGIN_VERSION)
424
+ call.resolve(ret)
425
+ }
426
+
427
+ // ---------------------------------------------------------------------------
428
+ // BroadcastReceiver implementation
429
+ // ---------------------------------------------------------------------------
430
+
431
+ /**
432
+ * BroadcastReceiver for passive system integrity signals.
433
+ *
434
+ * PURPOSE:
435
+ * - Reacts to system-level events that may indicate integrity changes
436
+ * (e.g. package installation or replacement).
437
+ *
438
+ * NOTE:
439
+ * - Failures are logged but NOT propagated to JavaScript.
440
+ * - This receiver is observational and non-blocking.
441
+ */
442
+ private inner class IntegrityEventReceiver(private val onSignalDetected: (JSObject) -> Unit) : BroadcastReceiver() {
443
+ override fun onReceive(
444
+ context: Context?,
445
+ intent: Intent?,
446
+ ) {
447
+ when (intent?.action) {
448
+ Intent.ACTION_PACKAGE_ADDED, Intent.ACTION_PACKAGE_REPLACED -> {
449
+ val options =
450
+ IntegrityCheckOptions(
451
+ level = "standard",
452
+ includeDebugInfo = false,
453
+ )
454
+
455
+ // Execute integrity checks off the main/plugin thread to avoid blocking
456
+ execute {
457
+ try {
458
+ val result = implementation.performCheck(options)
459
+ val jsResult = IntegrityUtils.toJSObject(result)
460
+ onSignalDetected(jsResult)
461
+ } catch (e: IntegrityError) {
462
+ IntegrityLogger.error(
463
+ "IntegrityEventReceiver: Error during package change check: ${e.message}",
464
+ )
465
+ } catch (e: Exception) {
466
+ IntegrityLogger.error(
467
+ "IntegrityEventReceiver: Unexpected error during package change check: ${e.message}",
468
+ )
469
+ }
470
+ }
471
+ }
472
+ }
473
+ }
474
+ }
475
+ }
@@ -0,0 +1,130 @@
1
+ package io.capkit.integrity
2
+
3
+ /**
4
+ * Helper responsible for assembling the final integrity report payload.
5
+ *
6
+ * Responsibilities:
7
+ * - Aggregate signals
8
+ * - Compute integrity score based on signal confidence
9
+ * - Build environment metadata
10
+ * - Produce a JS-bridge-safe map
11
+ *
12
+ * This builder contains NO platform-specific logic.
13
+ */
14
+ object IntegrityReportBuilder {
15
+ /**
16
+ * Default compromise threshold.
17
+ *
18
+ * POLICY:
19
+ * - A single high-confidence signal is sufficient to mark
20
+ * the environment as compromised.
21
+ *
22
+ * NOTE:
23
+ * - This value MUST remain aligned across platforms.
24
+ */
25
+ private const val COMPROMISE_THRESHOLD = 30
26
+
27
+ /**
28
+ * Builds the final integrity report returned to the JavaScript layer.
29
+ *
30
+ * CONTRACT:
31
+ * - Output structure MUST remain platform-agnostic
32
+ * - Scoring MUST be deterministic
33
+ * - No enforcement logic may be introduced here
34
+ */
35
+ fun buildReport(
36
+ signals: List<Map<String, Any>>,
37
+ isEmulator: Boolean,
38
+ platform: String = "android",
39
+ ): Map<String, Any> {
40
+ val score = computeScore(signals)
41
+ val scoreExplanation = buildScoreExplanation(signals)
42
+
43
+ return mapOf(
44
+ // Ordered list of all detected integrity signals.
45
+ "signals" to signals,
46
+ // Numeric integrity score derived from signal confidence.
47
+ "score" to score,
48
+ // Convenience flag indicating whether the device
49
+ // should be considered compromised.
50
+ "compromised" to (score >= COMPROMISE_THRESHOLD),
51
+ // Static environment metadata describing the runtime context.
52
+ "environment" to
53
+ mapOf(
54
+ "platform" to platform,
55
+ "isEmulator" to isEmulator,
56
+ // Reserved for future use
57
+ "isDebugBuild" to false,
58
+ ),
59
+ // Informational explanation describing how the score was derived.
60
+ // This metadata MUST NOT be treated as a security decision.
61
+ "scoreExplanation" to scoreExplanation,
62
+ // Millisecond-precision UNIX timestamp of report generation.
63
+ "timestamp" to System.currentTimeMillis(),
64
+ )
65
+ }
66
+
67
+ // ---------------------------------------------------------------------------
68
+ // Scoring
69
+ // ---------------------------------------------------------------------------
70
+
71
+ /**
72
+ * Computes a heuristic risk score from collected signals.
73
+ *
74
+ * Scoring policy (aligned with iOS):
75
+ * - high -> 30 points
76
+ * - medium -> 15 points
77
+ * - low -> 5 points
78
+ */
79
+ private fun computeScore(signals: List<Map<String, Any>>): Int {
80
+ return signals.sumOf {
81
+ when (it["confidence"]) {
82
+ "high" -> 30
83
+ "medium" -> 15
84
+ "low" -> 5
85
+ else -> 0
86
+ }
87
+ }
88
+ }
89
+
90
+ // ---------------------------------------------------------------------------
91
+ // Score explanation
92
+ // ---------------------------------------------------------------------------
93
+
94
+ /**
95
+ * Builds an informational explanation describing how the integrity
96
+ * score was derived from the detected signals.
97
+ *
98
+ * IMPORTANT:
99
+ * - This metadata is informational only.
100
+ * - It MUST NOT influence scoring or enforcement.
101
+ */
102
+ private fun buildScoreExplanation(signals: List<Map<String, Any>>): Map<String, Any> {
103
+ var high = 0
104
+ var medium = 0
105
+ var low = 0
106
+
107
+ val contributors = mutableListOf<String>()
108
+
109
+ for (signal in signals) {
110
+ (signal["id"] as? String)?.let { contributors.add(it) }
111
+
112
+ when (signal["confidence"]) {
113
+ "high" -> high++
114
+ "medium" -> medium++
115
+ "low" -> low++
116
+ }
117
+ }
118
+
119
+ return mapOf(
120
+ "totalSignals" to signals.size,
121
+ "byConfidence" to
122
+ mapOf(
123
+ "high" to high,
124
+ "medium" to medium,
125
+ "low" to low,
126
+ ),
127
+ "contributors" to contributors,
128
+ )
129
+ }
130
+ }
@@ -0,0 +1,72 @@
1
+ package io.capkit.integrity
2
+
3
+ /**
4
+ * Helper responsible for constructing standardized integrity signal maps.
5
+ *
6
+ * This mirrors the logic found in iOS IntegrityUtils to ensure
7
+ * cross-platform payload consistency.
8
+ */
9
+ object IntegritySignalBuilder {
10
+ /**
11
+ * Builds a JSON-serializable integrity signal.
12
+ *
13
+ * CONTRACT:
14
+ * - id MUST be stable across platforms
15
+ * - category MUST be platform-agnostic
16
+ * - confidence MUST be one of: low | medium | high
17
+ */
18
+ fun build(
19
+ id: String,
20
+ category: String,
21
+ confidence: String,
22
+ description: String? = null,
23
+ metadata: Map<String, Any>? = null,
24
+ options: IntegrityCheckOptions,
25
+ ): Map<String, Any> {
26
+ val signal =
27
+ mutableMapOf<String, Any>(
28
+ "id" to id,
29
+ "category" to category,
30
+ "confidence" to confidence,
31
+ )
32
+
33
+ // Include description only if requested in options to avoid leaking diagnostics
34
+ if (options.includeDebugInfo && description != null) {
35
+ signal["description"] = description
36
+ }
37
+
38
+ // Include metadata if present
39
+ if (metadata != null) {
40
+ signal["metadata"] = metadata
41
+ }
42
+
43
+ return signal
44
+ }
45
+ }
46
+
47
+ object IntegritySignalIds {
48
+ // Remote Attestation
49
+ const val ANDROID_PLAY_INTEGRITY_VERDICT = "android_play_integrity_verdict"
50
+
51
+ // Root
52
+ const val ANDROID_ROOT_SU = "android_root_su"
53
+ const val ANDROID_TEST_KEYS = "android_test_keys"
54
+ const val ANDROID_ROOT_PACKAGE = "android_root_package"
55
+
56
+ // Emulator
57
+ const val ANDROID_EMULATOR = "android_emulator"
58
+
59
+ // Debug
60
+ const val ANDROID_DEBUGGER_ATTACHED = "android_debugger_attached"
61
+ const val ANDROID_RUNTIME_DEBUGGABLE = "android_runtime_debuggable"
62
+
63
+ // Hook / Instrumentation
64
+ const val ANDROID_FRIDA_MEMORY = "android_frida_memory"
65
+ const val ANDROID_FRIDA_PORT = "android_frida_port"
66
+ const val ANDROID_FRIDA_CORRELATION = "android_frida_correlation_confirmed"
67
+
68
+ // Tamper / RASP
69
+ const val ANDROID_SANDBOX_ESCAPED = "android_sandbox_escaped"
70
+ const val ANDROID_SIGNATURE_INVALID = "android_signature_invalid"
71
+ const val ANDROID_OVERLAY_DETECTED = "android_overlay_detected"
72
+ }
@@ -0,0 +1,38 @@
1
+ package io.capkit.integrity.emulator
2
+
3
+ import android.os.Build
4
+
5
+ /**
6
+ * Detects emulators using correlated build properties.
7
+ */
8
+ object IntegrityEmulatorChecks {
9
+ /**
10
+ * Determines if the current environment matches known emulator characteristics.
11
+ *
12
+ * NOTE: This is a best-effort heuristic approach where no single signal is
13
+ * authoritative on its own.
14
+ */
15
+ fun isEmulator(): Boolean {
16
+ return try {
17
+ val fingerprint = Build.FINGERPRINT
18
+ val model = Build.MODEL
19
+ val manufacturer = Build.MANUFACTURER
20
+ val hardware = Build.HARDWARE
21
+ val product = Build.PRODUCT
22
+
23
+ fingerprint.contains("generic") ||
24
+ fingerprint.startsWith("unknown") ||
25
+ model.contains("google_sdk") ||
26
+ model.contains("Emulator", ignoreCase = true) ||
27
+ model.contains("Android SDK built for x86") ||
28
+ manufacturer.contains("Genymotion", ignoreCase = true) ||
29
+ hardware.contains("goldfish") ||
30
+ hardware.contains("ranchu") ||
31
+ product.contains("sdk_google") ||
32
+ product.contains("google_sdk") ||
33
+ product.contains("vbox86p")
34
+ } catch (_: Exception) {
35
+ false
36
+ }
37
+ }
38
+ }