@mmerterden/multi-agent-pipeline 20.1.0 → 20.2.0

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 (49) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/docs/facts.json +5 -5
  3. package/manifest.json +52 -31
  4. package/package.json +1 -1
  5. package/pipeline/skills/.skill-manifest.json +36 -20
  6. package/pipeline/skills/.skills-index.json +75 -9
  7. package/pipeline/skills/shared/README.md +13 -7
  8. package/pipeline/skills/shared/external/android-architecture/SKILL.md +71 -0
  9. package/pipeline/skills/shared/external/android-architecture/references/patterns.md +142 -0
  10. package/pipeline/skills/shared/external/android-build-quality-gates/SKILL.md +314 -0
  11. package/pipeline/skills/shared/external/android-build-quality-gates/references/patterns.md +432 -0
  12. package/pipeline/skills/shared/external/android-datastore/SKILL.md +236 -0
  13. package/pipeline/skills/shared/external/android-datastore/references/patterns.md +297 -0
  14. package/pipeline/skills/shared/external/android-design-tokens-codegen/SKILL.md +249 -0
  15. package/pipeline/skills/shared/external/android-design-tokens-codegen/references/patterns.md +270 -0
  16. package/pipeline/skills/shared/external/android-jetpack-compose-expert/SKILL.md +62 -0
  17. package/pipeline/skills/shared/external/android-mvi-viewmodel/SKILL.md +255 -0
  18. package/pipeline/skills/shared/external/android-mvi-viewmodel/references/patterns.md +257 -0
  19. package/pipeline/skills/shared/external/android-performance/SKILL.md +86 -602
  20. package/pipeline/skills/shared/external/android-performance/references/patterns.md +659 -0
  21. package/pipeline/skills/shared/external/android-security/SKILL.md +117 -430
  22. package/pipeline/skills/shared/external/android-security/references/patterns.md +690 -0
  23. package/pipeline/skills/shared/external/{android_ui_verification → android-ui-verification}/SKILL.md +1 -1
  24. package/pipeline/skills/shared/external/api-security-best-practices/SKILL.md +35 -733
  25. package/pipeline/skills/shared/external/api-security-best-practices/references/auth.md +299 -0
  26. package/pipeline/skills/shared/external/api-security-best-practices/references/input-validation.md +255 -0
  27. package/pipeline/skills/shared/external/api-security-best-practices/references/rate-limiting.md +167 -0
  28. package/pipeline/skills/shared/external/app-intents/SKILL.md +39 -174
  29. package/pipeline/skills/shared/external/app-intents/references/appintents-advanced.md +178 -0
  30. package/pipeline/skills/shared/external/compose-components/SKILL.md +48 -0
  31. package/pipeline/skills/shared/external/compose-components/references/patterns.md +200 -0
  32. package/pipeline/skills/shared/external/compose-navigation/SKILL.md +66 -3
  33. package/pipeline/skills/shared/external/compose-navigation/references/patterns.md +191 -0
  34. package/pipeline/skills/shared/external/compose-testing/SKILL.md +107 -397
  35. package/pipeline/skills/shared/external/compose-testing/references/patterns.md +631 -0
  36. package/pipeline/skills/shared/external/gradle-kotlin-dsl/SKILL.md +121 -449
  37. package/pipeline/skills/shared/external/gradle-kotlin-dsl/references/patterns.md +715 -0
  38. package/pipeline/skills/shared/external/kotlin-coroutines-expert/SKILL.md +143 -0
  39. package/pipeline/skills/shared/external/mapkit-location/SKILL.md +27 -102
  40. package/pipeline/skills/shared/external/mapkit-location/references/mapkit-patterns.md +42 -0
  41. package/pipeline/skills/shared/external/retrofit-networking/SKILL.md +94 -383
  42. package/pipeline/skills/shared/external/retrofit-networking/references/patterns.md +640 -0
  43. package/pipeline/skills/shared/external/room-database/SKILL.md +101 -440
  44. package/pipeline/skills/shared/external/room-database/references/patterns.md +614 -0
  45. package/pipeline/skills/shared/external/storekit/SKILL.md +69 -343
  46. package/pipeline/skills/shared/external/storekit/references/core-patterns.md +371 -0
  47. package/pipeline/skills/shared/external/widgetkit/SKILL.md +25 -101
  48. package/pipeline/skills/shared/external/widgetkit/references/widgetkit-advanced.md +107 -0
  49. package/pipeline/skills/skills-index.md +8 -2
@@ -0,0 +1,690 @@
1
+ # Android Security -- Reference Patterns
2
+
3
+ Full code for the patterns summarized in `SKILL.md`. Load the section you need
4
+ when implementing that specific security feature.
5
+
6
+ ## Contents
7
+
8
+ - [Secure Data Storage](#secure-data-storage)
9
+ - [Encrypted DataStore](#encrypted-datastore)
10
+ - [Database Key Hierarchy](#database-key-hierarchy)
11
+ - [Build-Type Security Gating](#build-type-security-gating)
12
+ - [Biometric Authentication](#biometric-authentication)
13
+ - [Certificate Pinning](#certificate-pinning)
14
+ - [API Key Protection](#api-key-protection)
15
+ - [R8 Obfuscation](#r8-obfuscation)
16
+ - [Play Integrity API](#play-integrity-api)
17
+ - [Root Detection](#root-detection)
18
+ - [Secure Network Communication](#secure-network-communication)
19
+ - [Secure Coding Patterns](#secure-coding-patterns)
20
+
21
+ ## Secure Data Storage
22
+
23
+ ### EncryptedSharedPreferences (legacy)
24
+
25
+ The AndroidX Security `EncryptedSharedPreferences` API is deprecated. Keep the
26
+ snippet below only for reading an existing store during migration; write new
27
+ key-value secrets through [Encrypted DataStore](#encrypted-datastore). It uses
28
+ AES-256 encryption under the hood.
29
+
30
+ ```kotlin
31
+ import androidx.security.crypto.EncryptedSharedPreferences
32
+ import androidx.security.crypto.MasterKey
33
+
34
+ class SecureStorage @Inject constructor(
35
+ @ApplicationContext private val context: Context,
36
+ ) {
37
+ private val masterKey = MasterKey.Builder(context)
38
+ .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
39
+ .build()
40
+
41
+ private val prefs = EncryptedSharedPreferences.create(
42
+ context,
43
+ "secure_prefs",
44
+ masterKey,
45
+ EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
46
+ EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
47
+ )
48
+
49
+ fun saveToken(token: String) {
50
+ prefs.edit().putString("auth_token", token).apply()
51
+ }
52
+
53
+ fun getToken(): String? = prefs.getString("auth_token", null)
54
+
55
+ fun clearToken() {
56
+ prefs.edit().remove("auth_token").apply()
57
+ }
58
+
59
+ fun clearAll() {
60
+ prefs.edit().clear().apply()
61
+ }
62
+ }
63
+ ```
64
+
65
+ ### Android Keystore
66
+
67
+ For cryptographic key management. Keys never leave the hardware.
68
+
69
+ ```kotlin
70
+ import java.security.KeyStore
71
+ import javax.crypto.Cipher
72
+ import javax.crypto.KeyGenerator
73
+ import javax.crypto.SecretKey
74
+ import javax.crypto.spec.GCMParameterSpec
75
+ import android.security.keystore.KeyGenParameterSpec
76
+ import android.security.keystore.KeyProperties
77
+
78
+ class KeystoreManager {
79
+
80
+ companion object {
81
+ private const val KEYSTORE_PROVIDER = "AndroidKeyStore"
82
+ private const val KEY_ALIAS = "app_encryption_key"
83
+ private const val TRANSFORMATION = "AES/GCM/NoPadding"
84
+ }
85
+
86
+ private fun getOrCreateKey(): SecretKey {
87
+ val keyStore = KeyStore.getInstance(KEYSTORE_PROVIDER).apply { load(null) }
88
+
89
+ keyStore.getEntry(KEY_ALIAS, null)?.let { entry ->
90
+ return (entry as KeyStore.SecretKeyEntry).secretKey
91
+ }
92
+
93
+ val keyGenerator = KeyGenerator.getInstance(
94
+ KeyProperties.KEY_ALGORITHM_AES,
95
+ KEYSTORE_PROVIDER,
96
+ )
97
+
98
+ keyGenerator.init(
99
+ KeyGenParameterSpec.Builder(
100
+ KEY_ALIAS,
101
+ KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT,
102
+ )
103
+ .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
104
+ .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
105
+ .setKeySize(256)
106
+ .setUserAuthenticationRequired(false)
107
+ .build()
108
+ )
109
+
110
+ return keyGenerator.generateKey()
111
+ }
112
+
113
+ fun encrypt(data: ByteArray): Pair<ByteArray, ByteArray> {
114
+ val cipher = Cipher.getInstance(TRANSFORMATION)
115
+ cipher.init(Cipher.ENCRYPT_MODE, getOrCreateKey())
116
+ val encrypted = cipher.doFinal(data)
117
+ return Pair(cipher.iv, encrypted)
118
+ }
119
+
120
+ fun decrypt(iv: ByteArray, encryptedData: ByteArray): ByteArray {
121
+ val cipher = Cipher.getInstance(TRANSFORMATION)
122
+ val spec = GCMParameterSpec(128, iv)
123
+ cipher.init(Cipher.DECRYPT_MODE, getOrCreateKey(), spec)
124
+ return cipher.doFinal(encryptedData)
125
+ }
126
+ }
127
+ ```
128
+
129
+ ## Encrypted DataStore
130
+
131
+ `EncryptedSharedPreferences` is deprecated. For typed key-value secrets, encrypt
132
+ at the DataStore serializer layer: the delegate serializer produces plaintext
133
+ bytes, a Keystore-held AES/GCM key encrypts them, and only ciphertext (IV
134
+ prepended) reaches disk. The DataStore wiring itself (the `dataStore` delegate,
135
+ `Flow` reads, `updateData` writes) is not repeated here; see the
136
+ `android-datastore` skill for it. This serializer plugs into that setup.
137
+
138
+ ```kotlin
139
+ class KeystoreCrypto(private val keystore: KeystoreManager) {
140
+ fun seal(plaintext: ByteArray): ByteArray {
141
+ val (iv, ciphertext) = keystore.encrypt(plaintext)
142
+ return ByteArray(1 + iv.size + ciphertext.size).also {
143
+ it[0] = iv.size.toByte()
144
+ iv.copyInto(it, destinationOffset = 1)
145
+ ciphertext.copyInto(it, destinationOffset = 1 + iv.size)
146
+ }
147
+ }
148
+
149
+ fun open(sealed: ByteArray): ByteArray {
150
+ val ivSize = sealed[0].toInt()
151
+ val iv = sealed.copyOfRange(1, 1 + ivSize)
152
+ val ciphertext = sealed.copyOfRange(1 + ivSize, sealed.size)
153
+ return keystore.decrypt(iv, ciphertext)
154
+ }
155
+ }
156
+
157
+ class EncryptedSerializer<T>(
158
+ private val delegate: Serializer<T>,
159
+ private val crypto: KeystoreCrypto,
160
+ ) : Serializer<T> {
161
+
162
+ override val defaultValue: T = delegate.defaultValue
163
+
164
+ override suspend fun readFrom(input: InputStream): T {
165
+ val sealed = input.readBytes()
166
+ if (sealed.isEmpty()) return defaultValue
167
+ val plaintext = crypto.open(sealed)
168
+ return delegate.readFrom(ByteArrayInputStream(plaintext))
169
+ }
170
+
171
+ override suspend fun writeTo(t: T, output: OutputStream) {
172
+ val buffer = ByteArrayOutputStream()
173
+ delegate.writeTo(t, buffer)
174
+ output.write(crypto.seal(buffer.toByteArray()))
175
+ }
176
+ }
177
+ ```
178
+
179
+ ## Database Key Hierarchy
180
+
181
+ Envelope encryption for a database passphrase (for example an encrypted SQLCipher
182
+ database). A random 32-byte data key encrypts the database; a non-exportable
183
+ Keystore AES/GCM key wraps that data key. Only the wrapped key plus its IV are
184
+ persisted. The plaintext data key is decrypted just in time and its bytes are
185
+ zeroed after the database opens. The SQLCipher `SupportFactory` wiring that
186
+ consumes the passphrase lives in the `room-database` skill.
187
+
188
+ ```kotlin
189
+ interface WrappedKeyStore {
190
+ fun read(): Pair<ByteArray, ByteArray>?
191
+ fun write(wrapped: ByteArray, iv: ByteArray)
192
+ }
193
+
194
+ class DatabaseKeyProvider(private val store: WrappedKeyStore) {
195
+
196
+ private companion object {
197
+ const val WRAPPING_KEY_ALIAS = "db_key_wrapping_key"
198
+ const val TRANSFORMATION = "AES/GCM/NoPadding"
199
+ const val DATA_KEY_SIZE_BYTES = 32
200
+ const val GCM_TAG_BITS = 128
201
+ }
202
+
203
+ fun loadOrCreateDataKey(): ByteArray {
204
+ store.read()?.let { (wrapped, iv) -> return unwrap(wrapped, iv) }
205
+ val dataKey = ByteArray(DATA_KEY_SIZE_BYTES).also { SecureRandom().nextBytes(it) }
206
+ val (wrapped, iv) = wrap(dataKey)
207
+ store.write(wrapped, iv)
208
+ return dataKey
209
+ }
210
+
211
+ private fun wrap(dataKey: ByteArray): Pair<ByteArray, ByteArray> {
212
+ val cipher = Cipher.getInstance(TRANSFORMATION)
213
+ cipher.init(Cipher.ENCRYPT_MODE, wrappingKey())
214
+ return cipher.doFinal(dataKey) to cipher.iv
215
+ }
216
+
217
+ private fun unwrap(wrapped: ByteArray, iv: ByteArray): ByteArray {
218
+ val cipher = Cipher.getInstance(TRANSFORMATION)
219
+ cipher.init(Cipher.DECRYPT_MODE, wrappingKey(), GCMParameterSpec(GCM_TAG_BITS, iv))
220
+ return cipher.doFinal(wrapped)
221
+ }
222
+
223
+ private fun wrappingKey(): SecretKey {
224
+ val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
225
+ (keyStore.getEntry(WRAPPING_KEY_ALIAS, null) as? KeyStore.SecretKeyEntry)
226
+ ?.let { return it.secretKey }
227
+
228
+ return KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore").apply {
229
+ init(
230
+ KeyGenParameterSpec.Builder(
231
+ WRAPPING_KEY_ALIAS,
232
+ KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT,
233
+ )
234
+ .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
235
+ .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
236
+ .setKeySize(256)
237
+ .build()
238
+ )
239
+ }.generateKey()
240
+ }
241
+ }
242
+ ```
243
+
244
+ Decrypt just in time and zero the plaintext key bytes once the database owns them:
245
+
246
+ ```kotlin
247
+ val dataKey = keyProvider.loadOrCreateDataKey()
248
+ try {
249
+ openEncryptedDatabase(passphrase = dataKey) // room-database: SupportFactory wiring
250
+ } finally {
251
+ dataKey.fill(0)
252
+ }
253
+ ```
254
+
255
+ ## Build-Type Security Gating
256
+
257
+ Certificate pinning and any traffic inspector are decided in one place, by build
258
+ type. Pinning is production-only (a debug proxy would fail the pins), and the
259
+ inspection and logging interceptors are debug-only and added last so they observe
260
+ the final, fully-decorated request. Bind the debug interceptor set from a module
261
+ in the `src/debug` source set and an empty set from `src/release`, so release
262
+ builds cannot even link the inspector.
263
+
264
+ ```kotlin
265
+ // src/main -- assembly, no build-type branching in the class body
266
+ class OkHttpClientFactory @Inject constructor(
267
+ private val authInterceptor: AuthInterceptor,
268
+ private val certificatePinner: CertificatePinner,
269
+ private val observers: Set<@JvmSuppressWildcards Interceptor>,
270
+ ) {
271
+ fun create(): OkHttpClient = OkHttpClient.Builder()
272
+ .certificatePinner(certificatePinner)
273
+ .addInterceptor(authInterceptor)
274
+ .apply { observers.forEach(::addInterceptor) } // logging/inspection, added last
275
+ .build()
276
+ }
277
+ ```
278
+
279
+ ```kotlin
280
+ // src/release -- production pins, no observers
281
+ @Module
282
+ @InstallIn(SingletonComponent::class)
283
+ object ReleaseNetworkModule {
284
+
285
+ @Provides
286
+ fun certificatePinner(): CertificatePinner = CertificatePinner.Builder()
287
+ .add("api.example.com", "sha256/AAAA...=")
288
+ .add("api.example.com", "sha256/BBBB...=")
289
+ .build()
290
+
291
+ @Provides
292
+ @ElementsIntoSet
293
+ fun observers(): Set<Interceptor> = emptySet()
294
+ }
295
+ ```
296
+
297
+ ```kotlin
298
+ // src/debug -- no pins, inspector + logging observers
299
+ @Module
300
+ @InstallIn(SingletonComponent::class)
301
+ object DebugNetworkModule {
302
+
303
+ @Provides
304
+ fun certificatePinner(): CertificatePinner = CertificatePinner.Builder().build()
305
+
306
+ @Provides
307
+ @IntoSet
308
+ fun logging(): Interceptor = HttpLoggingInterceptor()
309
+ .apply { level = HttpLoggingInterceptor.Level.BODY }
310
+ }
311
+ ```
312
+
313
+ ## Biometric Authentication
314
+
315
+ ### BiometricPrompt Setup
316
+
317
+ ```kotlin
318
+ class BiometricAuthenticator @Inject constructor() {
319
+
320
+ fun authenticate(
321
+ activity: FragmentActivity,
322
+ title: String = "Authenticate",
323
+ subtitle: String = "Verify your identity",
324
+ onSuccess: () -> Unit,
325
+ onError: (String) -> Unit,
326
+ ) {
327
+ val biometricManager = BiometricManager.from(activity)
328
+ val canAuthenticate = biometricManager.canAuthenticate(
329
+ BiometricManager.Authenticators.BIOMETRIC_STRONG or
330
+ BiometricManager.Authenticators.DEVICE_CREDENTIAL
331
+ )
332
+
333
+ if (canAuthenticate != BiometricManager.BIOMETRIC_SUCCESS) {
334
+ onError(mapBiometricError(canAuthenticate))
335
+ return
336
+ }
337
+
338
+ val promptInfo = BiometricPrompt.PromptInfo.Builder()
339
+ .setTitle(title)
340
+ .setSubtitle(subtitle)
341
+ .setAllowedAuthenticators(
342
+ BiometricManager.Authenticators.BIOMETRIC_STRONG or
343
+ BiometricManager.Authenticators.DEVICE_CREDENTIAL
344
+ )
345
+ .build()
346
+
347
+ val callback = object : BiometricPrompt.AuthenticationCallback() {
348
+ override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
349
+ onSuccess()
350
+ }
351
+
352
+ override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
353
+ onError(errString.toString())
354
+ }
355
+
356
+ override fun onAuthenticationFailed() {
357
+ // Called on failed attempt but user can retry
358
+ }
359
+ }
360
+
361
+ val prompt = BiometricPrompt(activity, callback)
362
+ prompt.authenticate(promptInfo)
363
+ }
364
+
365
+ private fun mapBiometricError(code: Int): String = when (code) {
366
+ BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> "No biometric hardware"
367
+ BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> "Biometric hardware unavailable"
368
+ BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> "No biometrics enrolled"
369
+ else -> "Biometric authentication unavailable"
370
+ }
371
+ }
372
+ ```
373
+
374
+ ### Biometric + Keystore (Crypto-Based Auth)
375
+
376
+ ```kotlin
377
+ fun authenticateWithCrypto(
378
+ activity: FragmentActivity,
379
+ onSuccess: (Cipher) -> Unit,
380
+ ) {
381
+ val key = getOrCreateBiometricKey()
382
+ val cipher = Cipher.getInstance("AES/GCM/NoPadding")
383
+ cipher.init(Cipher.ENCRYPT_MODE, key)
384
+
385
+ val cryptoObject = BiometricPrompt.CryptoObject(cipher)
386
+
387
+ val prompt = BiometricPrompt(activity, object : BiometricPrompt.AuthenticationCallback() {
388
+ override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
389
+ result.cryptoObject?.cipher?.let(onSuccess)
390
+ }
391
+ })
392
+
393
+ prompt.authenticate(promptInfo, cryptoObject)
394
+ }
395
+
396
+ private fun getOrCreateBiometricKey(): SecretKey {
397
+ val keyGenerator = KeyGenerator.getInstance("AES", "AndroidKeyStore")
398
+ keyGenerator.init(
399
+ KeyGenParameterSpec.Builder("biometric_key",
400
+ KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
401
+ .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
402
+ .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
403
+ .setUserAuthenticationRequired(true)
404
+ .setInvalidatedByBiometricEnrollment(true)
405
+ .build()
406
+ )
407
+ return keyGenerator.generateKey()
408
+ }
409
+ ```
410
+
411
+ ## Certificate Pinning
412
+
413
+ ### OkHttp CertificatePinner
414
+
415
+ ```kotlin
416
+ val certificatePinner = CertificatePinner.Builder()
417
+ .add("api.example.com", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") // Primary
418
+ .add("api.example.com", "sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=") // Backup
419
+ .build()
420
+
421
+ val client = OkHttpClient.Builder()
422
+ .certificatePinner(certificatePinner)
423
+ .build()
424
+ ```
425
+
426
+ ### Generating Pin Hashes
427
+
428
+ ```bash
429
+ # From a domain
430
+ openssl s_client -connect api.example.com:443 -servername api.example.com \
431
+ | openssl x509 -pubkey -noout \
432
+ | openssl pkey -pubin -outform der \
433
+ | openssl dgst -sha256 -binary \
434
+ | openssl enc -base64
435
+
436
+ # From a certificate file
437
+ openssl x509 -in cert.pem -pubkey -noout \
438
+ | openssl pkey -pubin -outform der \
439
+ | openssl dgst -sha256 -binary \
440
+ | openssl enc -base64
441
+ ```
442
+
443
+ ### Network Security Config (XML-based)
444
+
445
+ ```xml
446
+ <!-- res/xml/network_security_config.xml -->
447
+ <?xml version="1.0" encoding="utf-8"?>
448
+ <network-security-config>
449
+ <domain-config cleartextTrafficPermitted="false">
450
+ <domain includeSubdomains="true">api.example.com</domain>
451
+ <pin-set expiration="2026-01-01">
452
+ <pin digest="SHA-256">AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</pin>
453
+ <pin digest="SHA-256">BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=</pin>
454
+ </pin-set>
455
+ </domain-config>
456
+
457
+ <!-- Block cleartext for all domains -->
458
+ <base-config cleartextTrafficPermitted="false" />
459
+ </network-security-config>
460
+ ```
461
+
462
+ ```xml
463
+ <!-- AndroidManifest.xml -->
464
+ <application android:networkSecurityConfig="@xml/network_security_config">
465
+ ```
466
+
467
+ ## API Key Protection
468
+
469
+ ### local.properties (Gitignored)
470
+
471
+ ```properties
472
+ # local.properties -- NEVER commit this file
473
+ MAPS_API_KEY=AIzaSyB...
474
+ BASE_URL=https://api.example.com/
475
+ ```
476
+
477
+ ### Build-Time Injection
478
+
479
+ ```kotlin
480
+ // build.gradle.kts
481
+ import java.util.Properties
482
+
483
+ val localProperties = Properties().apply {
484
+ val file = rootProject.file("local.properties")
485
+ if (file.exists()) load(file.inputStream())
486
+ }
487
+
488
+ android {
489
+ defaultConfig {
490
+ buildConfigField("String", "MAPS_API_KEY",
491
+ "\"${localProperties["MAPS_API_KEY"] ?: ""}\"")
492
+ buildConfigField("String", "BASE_URL",
493
+ "\"${localProperties["BASE_URL"] ?: "https://api.example.com/"}\"")
494
+ }
495
+ }
496
+ ```
497
+
498
+ ### Usage in Code
499
+
500
+ ```kotlin
501
+ // Access via BuildConfig -- obfuscated by R8 in release builds
502
+ val apiKey = BuildConfig.MAPS_API_KEY
503
+ val baseUrl = BuildConfig.BASE_URL
504
+ ```
505
+
506
+ ### Manifest Placeholder
507
+
508
+ ```kotlin
509
+ android {
510
+ defaultConfig {
511
+ manifestPlaceholders["MAPS_API_KEY"] = localProperties["MAPS_API_KEY"] ?: ""
512
+ }
513
+ }
514
+ ```
515
+
516
+ ```xml
517
+ <meta-data
518
+ android:name="com.google.android.geo.API_KEY"
519
+ android:value="${MAPS_API_KEY}" />
520
+ ```
521
+
522
+ ## R8 Obfuscation
523
+
524
+ R8 generates `mapping.txt` for deobfuscating crash reports:
525
+
526
+ ```kotlin
527
+ android {
528
+ buildTypes {
529
+ release {
530
+ isMinifyEnabled = true
531
+ isShrinkResources = true
532
+ proguardFiles(
533
+ getDefaultProguardFile("proguard-android-optimize.txt"),
534
+ "proguard-rules.pro",
535
+ )
536
+ }
537
+ }
538
+ }
539
+ ```
540
+
541
+ Upload `mapping.txt` to Google Play Console and Firebase Crashlytics for
542
+ readable stack traces.
543
+
544
+ ## Play Integrity API
545
+
546
+ Verify that API requests come from a genuine app on a genuine device.
547
+
548
+ ```kotlin
549
+ class IntegrityVerifier @Inject constructor(
550
+ @ApplicationContext private val context: Context,
551
+ ) {
552
+ suspend fun getIntegrityToken(nonce: String): String? {
553
+ return try {
554
+ val integrityManager = IntegrityManagerFactory.create(context)
555
+
556
+ val request = IntegrityTokenRequest.builder()
557
+ .setNonce(nonce)
558
+ .build()
559
+
560
+ val response = integrityManager
561
+ .requestIntegrityToken(request)
562
+ .await()
563
+
564
+ response.token()
565
+ } catch (e: Exception) {
566
+ null
567
+ }
568
+ }
569
+ }
570
+ ```
571
+
572
+ Send the token to your server for verification. Never verify integrity tokens
573
+ on the client -- the server must decode and validate them via the Play Integrity
574
+ API.
575
+
576
+ ## Root Detection
577
+
578
+ Root detection is defense-in-depth. Determined attackers can bypass it, but it
579
+ raises the bar.
580
+
581
+ ```kotlin
582
+ object RootDetector {
583
+
584
+ fun isDeviceRooted(): Boolean =
585
+ checkRootBinaries() || checkSuExists() || checkRootApps()
586
+
587
+ private fun checkRootBinaries(): Boolean {
588
+ val paths = listOf(
589
+ "/system/bin/su",
590
+ "/system/xbin/su",
591
+ "/sbin/su",
592
+ "/system/app/Superuser.apk",
593
+ "/system/app/SuperSU.apk",
594
+ )
595
+ return paths.any { File(it).exists() }
596
+ }
597
+
598
+ private fun checkSuExists(): Boolean = try {
599
+ Runtime.getRuntime().exec("which su").inputStream.bufferedReader().readLine() != null
600
+ } catch (e: Exception) {
601
+ false
602
+ }
603
+
604
+ private fun checkRootApps(): Boolean {
605
+ val rootPackages = listOf(
606
+ "com.topjohnwu.magisk",
607
+ "eu.chainfire.supersu",
608
+ "com.koushikdutta.superuser",
609
+ )
610
+ // Check if any are installed (requires QUERY_ALL_PACKAGES or specific queries)
611
+ return false // Simplified -- use PackageManager in production
612
+ }
613
+ }
614
+ ```
615
+
616
+ Use root detection as one signal among many. Combine with Play Integrity for
617
+ stronger guarantees.
618
+
619
+ ## Secure Network Communication
620
+
621
+ ### Enforce HTTPS
622
+
623
+ ```xml
624
+ <!-- network_security_config.xml -->
625
+ <network-security-config>
626
+ <base-config cleartextTrafficPermitted="false" />
627
+ </network-security-config>
628
+ ```
629
+
630
+ ### Debug-Only Cleartext (for local development)
631
+
632
+ ```xml
633
+ <network-security-config>
634
+ <base-config cleartextTrafficPermitted="false" />
635
+ <domain-config cleartextTrafficPermitted="true">
636
+ <domain includeSubdomains="false">10.0.2.2</domain>
637
+ <domain includeSubdomains="false">localhost</domain>
638
+ </domain-config>
639
+ </network-security-config>
640
+ ```
641
+
642
+ ### TLS Configuration
643
+
644
+ ```kotlin
645
+ val spec = ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
646
+ .tlsVersions(TlsVersion.TLS_1_2, TlsVersion.TLS_1_3)
647
+ .build()
648
+
649
+ val client = OkHttpClient.Builder()
650
+ .connectionSpecs(listOf(spec))
651
+ .build()
652
+ ```
653
+
654
+ ## Secure Coding Patterns
655
+
656
+ ### Never Log Sensitive Data
657
+
658
+ ```kotlin
659
+ // WRONG
660
+ Log.d("Auth", "Token: $token")
661
+ Log.d("Payment", "Card: $cardNumber")
662
+
663
+ // CORRECT
664
+ Log.d("Auth", "Token refresh successful")
665
+ Log.d("Payment", "Payment processed")
666
+ ```
667
+
668
+ ### Input Validation
669
+
670
+ ```kotlin
671
+ fun validateDeepLink(uri: Uri): Boolean {
672
+ val allowedHosts = setOf("example.com", "www.example.com")
673
+ val allowedSchemes = setOf("https")
674
+
675
+ return uri.scheme in allowedSchemes &&
676
+ uri.host in allowedHosts &&
677
+ !uri.path.orEmpty().contains("..")
678
+ }
679
+ ```
680
+
681
+ ### Secure WebView
682
+
683
+ ```kotlin
684
+ webView.settings.apply {
685
+ javaScriptEnabled = false // Enable ONLY if required
686
+ allowFileAccess = false
687
+ allowContentAccess = false
688
+ domStorageEnabled = false
689
+ }
690
+ ```
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: android_ui_verification
2
+ name: android-ui-verification
3
3
  description: "Automated end-to-end UI testing and verification on an Android Emulator using ADB. Use when an Android build needs driving on an emulator to verify its UI end to end."
4
4
  risk: safe
5
5
  source: community