@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,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
|
+
}
|