@mmerterden/multi-agent-pipeline 20.1.0 → 20.2.1

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 (52) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/docs/facts.json +6 -6
  3. package/manifest.json +55 -34
  4. package/package.json +1 -1
  5. package/pipeline/multi-agent-refs/features/usage-reporting.md +7 -3
  6. package/pipeline/schemas/prefs.schema.json +4 -0
  7. package/pipeline/scripts/usage-register.mjs +2 -0
  8. package/pipeline/skills/.skill-manifest.json +36 -20
  9. package/pipeline/skills/.skills-index.json +75 -9
  10. package/pipeline/skills/shared/README.md +13 -7
  11. package/pipeline/skills/shared/external/android-architecture/SKILL.md +71 -0
  12. package/pipeline/skills/shared/external/android-architecture/references/patterns.md +142 -0
  13. package/pipeline/skills/shared/external/android-build-quality-gates/SKILL.md +314 -0
  14. package/pipeline/skills/shared/external/android-build-quality-gates/references/patterns.md +432 -0
  15. package/pipeline/skills/shared/external/android-datastore/SKILL.md +236 -0
  16. package/pipeline/skills/shared/external/android-datastore/references/patterns.md +297 -0
  17. package/pipeline/skills/shared/external/android-design-tokens-codegen/SKILL.md +249 -0
  18. package/pipeline/skills/shared/external/android-design-tokens-codegen/references/patterns.md +270 -0
  19. package/pipeline/skills/shared/external/android-jetpack-compose-expert/SKILL.md +62 -0
  20. package/pipeline/skills/shared/external/android-mvi-viewmodel/SKILL.md +255 -0
  21. package/pipeline/skills/shared/external/android-mvi-viewmodel/references/patterns.md +257 -0
  22. package/pipeline/skills/shared/external/android-performance/SKILL.md +86 -602
  23. package/pipeline/skills/shared/external/android-performance/references/patterns.md +659 -0
  24. package/pipeline/skills/shared/external/android-security/SKILL.md +117 -430
  25. package/pipeline/skills/shared/external/android-security/references/patterns.md +690 -0
  26. package/pipeline/skills/shared/external/{android_ui_verification → android-ui-verification}/SKILL.md +1 -1
  27. package/pipeline/skills/shared/external/api-security-best-practices/SKILL.md +35 -733
  28. package/pipeline/skills/shared/external/api-security-best-practices/references/auth.md +299 -0
  29. package/pipeline/skills/shared/external/api-security-best-practices/references/input-validation.md +255 -0
  30. package/pipeline/skills/shared/external/api-security-best-practices/references/rate-limiting.md +167 -0
  31. package/pipeline/skills/shared/external/app-intents/SKILL.md +39 -174
  32. package/pipeline/skills/shared/external/app-intents/references/appintents-advanced.md +178 -0
  33. package/pipeline/skills/shared/external/compose-components/SKILL.md +48 -0
  34. package/pipeline/skills/shared/external/compose-components/references/patterns.md +200 -0
  35. package/pipeline/skills/shared/external/compose-navigation/SKILL.md +66 -3
  36. package/pipeline/skills/shared/external/compose-navigation/references/patterns.md +191 -0
  37. package/pipeline/skills/shared/external/compose-testing/SKILL.md +107 -397
  38. package/pipeline/skills/shared/external/compose-testing/references/patterns.md +631 -0
  39. package/pipeline/skills/shared/external/gradle-kotlin-dsl/SKILL.md +121 -449
  40. package/pipeline/skills/shared/external/gradle-kotlin-dsl/references/patterns.md +715 -0
  41. package/pipeline/skills/shared/external/kotlin-coroutines-expert/SKILL.md +143 -0
  42. package/pipeline/skills/shared/external/mapkit-location/SKILL.md +27 -102
  43. package/pipeline/skills/shared/external/mapkit-location/references/mapkit-patterns.md +42 -0
  44. package/pipeline/skills/shared/external/retrofit-networking/SKILL.md +94 -383
  45. package/pipeline/skills/shared/external/retrofit-networking/references/patterns.md +640 -0
  46. package/pipeline/skills/shared/external/room-database/SKILL.md +101 -440
  47. package/pipeline/skills/shared/external/room-database/references/patterns.md +614 -0
  48. package/pipeline/skills/shared/external/storekit/SKILL.md +69 -343
  49. package/pipeline/skills/shared/external/storekit/references/core-patterns.md +371 -0
  50. package/pipeline/skills/shared/external/widgetkit/SKILL.md +25 -101
  51. package/pipeline/skills/shared/external/widgetkit/references/widgetkit-advanced.md +107 -0
  52. package/pipeline/skills/skills-index.md +8 -2
@@ -9,9 +9,17 @@ Security patterns for Android apps covering data storage, network security,
9
9
  authentication, integrity verification, and obfuscation. Targets 2024-2025
10
10
  best practices with AndroidX Security, BiometricPrompt, and Play Integrity.
11
11
 
12
+ Full code for every pattern below lives in
13
+ [`references/patterns.md`](references/patterns.md), under headings that mirror
14
+ these sections. This file is the guide: read a section for the decision, then
15
+ load the matching reference section when writing the implementation.
16
+
12
17
  ## Contents
13
18
 
14
19
  - [Secure Data Storage](#secure-data-storage)
20
+ - [Encrypted DataStore for Secrets](#encrypted-datastore-for-secrets)
21
+ - [Database Key Hierarchy](#database-key-hierarchy)
22
+ - [Build-Type Security Gating](#build-type-security-gating)
15
23
  - [Biometric Authentication](#biometric-authentication)
16
24
  - [Certificate Pinning](#certificate-pinning)
17
25
  - [API Key Protection](#api-key-protection)
@@ -26,225 +34,88 @@ best practices with AndroidX Security, BiometricPrompt, and Play Integrity.
26
34
 
27
35
  ## Secure Data Storage
28
36
 
29
- ### EncryptedSharedPreferences
30
-
31
- For sensitive key-value data. Uses AES-256 encryption under the hood.
32
-
33
- ```kotlin
34
- import androidx.security.crypto.EncryptedSharedPreferences
35
- import androidx.security.crypto.MasterKey
36
-
37
- class SecureStorage @Inject constructor(
38
- @ApplicationContext private val context: Context,
39
- ) {
40
- private val masterKey = MasterKey.Builder(context)
41
- .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
42
- .build()
43
-
44
- private val prefs = EncryptedSharedPreferences.create(
45
- context,
46
- "secure_prefs",
47
- masterKey,
48
- EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
49
- EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
50
- )
51
-
52
- fun saveToken(token: String) {
53
- prefs.edit().putString("auth_token", token).apply()
54
- }
55
-
56
- fun getToken(): String? = prefs.getString("auth_token", null)
57
-
58
- fun clearToken() {
59
- prefs.edit().remove("auth_token").apply()
60
- }
61
-
62
- fun clearAll() {
63
- prefs.edit().clear().apply()
64
- }
65
- }
66
- ```
37
+ The Android Keystore holds cryptographic keys (hardware-backed, keys never leave
38
+ the secure hardware). `EncryptedSharedPreferences` is deprecated: keep it only to
39
+ read an existing store during migration, and route new key-value secrets through
40
+ [Encrypted DataStore for Secrets](#encrypted-datastore-for-secrets).
67
41
 
68
- ### Android Keystore
69
-
70
- For cryptographic key management. Keys never leave the hardware.
71
-
72
- ```kotlin
73
- import java.security.KeyStore
74
- import javax.crypto.Cipher
75
- import javax.crypto.KeyGenerator
76
- import javax.crypto.SecretKey
77
- import javax.crypto.spec.GCMParameterSpec
78
- import android.security.keystore.KeyGenParameterSpec
79
- import android.security.keystore.KeyProperties
80
-
81
- class KeystoreManager {
82
-
83
- companion object {
84
- private const val KEYSTORE_PROVIDER = "AndroidKeyStore"
85
- private const val KEY_ALIAS = "app_encryption_key"
86
- private const val TRANSFORMATION = "AES/GCM/NoPadding"
87
- }
88
-
89
- private fun getOrCreateKey(): SecretKey {
90
- val keyStore = KeyStore.getInstance(KEYSTORE_PROVIDER).apply { load(null) }
91
-
92
- keyStore.getEntry(KEY_ALIAS, null)?.let { entry ->
93
- return (entry as KeyStore.SecretKeyEntry).secretKey
94
- }
95
-
96
- val keyGenerator = KeyGenerator.getInstance(
97
- KeyProperties.KEY_ALGORITHM_AES,
98
- KEYSTORE_PROVIDER,
99
- )
100
-
101
- keyGenerator.init(
102
- KeyGenParameterSpec.Builder(
103
- KEY_ALIAS,
104
- KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT,
105
- )
106
- .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
107
- .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
108
- .setKeySize(256)
109
- .setUserAuthenticationRequired(false)
110
- .build()
111
- )
112
-
113
- return keyGenerator.generateKey()
114
- }
115
-
116
- fun encrypt(data: ByteArray): Pair<ByteArray, ByteArray> {
117
- val cipher = Cipher.getInstance(TRANSFORMATION)
118
- cipher.init(Cipher.ENCRYPT_MODE, getOrCreateKey())
119
- val encrypted = cipher.doFinal(data)
120
- return Pair(cipher.iv, encrypted)
121
- }
122
-
123
- fun decrypt(iv: ByteArray, encryptedData: ByteArray): ByteArray {
124
- val cipher = Cipher.getInstance(TRANSFORMATION)
125
- val spec = GCMParameterSpec(128, iv)
126
- cipher.init(Cipher.DECRYPT_MODE, getOrCreateKey(), spec)
127
- return cipher.doFinal(encryptedData)
128
- }
129
- }
130
- ```
42
+ See `references/patterns.md` -> "Secure Data Storage" for the legacy
43
+ `EncryptedSharedPreferences` reader and the `KeystoreManager` (encrypt/decrypt
44
+ with a Keystore-held AES/GCM key) that the newer patterns build on.
131
45
 
132
46
  ### What Goes Where
133
47
 
134
48
  | Data Type | Storage | Why |
135
49
  |-----------|---------|-----|
136
- | Auth tokens | EncryptedSharedPreferences | Encrypted at rest, simple API |
50
+ | Auth tokens | Encrypted DataStore (Keystore-backed serializer) | Typed, async, encrypted at the serializer layer |
137
51
  | Passwords | Android Keystore + encryption | Hardware-backed security |
138
52
  | API keys | BuildConfig (build-time injection) | Not in source; obfuscated by R8 |
139
- | User preferences (non-sensitive) | SharedPreferences / DataStore | No encryption needed |
53
+ | Database passphrase | Envelope-wrapped by a Keystore key | Data key never persisted in the clear |
54
+ | User preferences (non-sensitive) | Preferences DataStore | No encryption needed |
140
55
  | Large sensitive files | Encrypted file + Keystore key | AES encryption with hardware key |
141
56
 
142
- ## Biometric Authentication
57
+ ## Encrypted DataStore for Secrets
143
58
 
144
- ### BiometricPrompt Setup
59
+ For typed key-value secrets, prefer a typed DataStore encrypted at the serializer
60
+ layer over the deprecated `EncryptedSharedPreferences`. The delegate serializer
61
+ produces plaintext bytes, a non-exportable Keystore AES/GCM key seals them (IV
62
+ prepended), and only ciphertext reaches disk. This skill owns the security
63
+ rationale and the key material; the DataStore mechanics (the `dataStore`
64
+ delegate, `Flow` reads, `updateData` writes) belong to the `android-datastore`
65
+ skill -- do not reproduce them here.
145
66
 
146
- ```kotlin
147
- class BiometricAuthenticator @Inject constructor() {
148
-
149
- fun authenticate(
150
- activity: FragmentActivity,
151
- title: String = "Authenticate",
152
- subtitle: String = "Verify your identity",
153
- onSuccess: () -> Unit,
154
- onError: (String) -> Unit,
155
- ) {
156
- val biometricManager = BiometricManager.from(activity)
157
- val canAuthenticate = biometricManager.canAuthenticate(
158
- BiometricManager.Authenticators.BIOMETRIC_STRONG or
159
- BiometricManager.Authenticators.DEVICE_CREDENTIAL
160
- )
161
-
162
- if (canAuthenticate != BiometricManager.BIOMETRIC_SUCCESS) {
163
- onError(mapBiometricError(canAuthenticate))
164
- return
165
- }
166
-
167
- val promptInfo = BiometricPrompt.PromptInfo.Builder()
168
- .setTitle(title)
169
- .setSubtitle(subtitle)
170
- .setAllowedAuthenticators(
171
- BiometricManager.Authenticators.BIOMETRIC_STRONG or
172
- BiometricManager.Authenticators.DEVICE_CREDENTIAL
173
- )
174
- .build()
175
-
176
- val callback = object : BiometricPrompt.AuthenticationCallback() {
177
- override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
178
- onSuccess()
179
- }
180
-
181
- override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
182
- onError(errString.toString())
183
- }
184
-
185
- override fun onAuthenticationFailed() {
186
- // Called on failed attempt but user can retry
187
- }
188
- }
189
-
190
- val prompt = BiometricPrompt(activity, callback)
191
- prompt.authenticate(promptInfo)
192
- }
193
-
194
- private fun mapBiometricError(code: Int): String = when (code) {
195
- BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> "No biometric hardware"
196
- BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> "Biometric hardware unavailable"
197
- BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> "No biometrics enrolled"
198
- else -> "Biometric authentication unavailable"
199
- }
200
- }
201
- ```
67
+ See `references/patterns.md` -> "Encrypted DataStore" for the `KeystoreCrypto`
68
+ seal/open helpers and the `EncryptedSerializer<T>` that wraps a plaintext
69
+ serializer.
202
70
 
203
- ### Biometric + Keystore (Crypto-Based Auth)
71
+ ## Database Key Hierarchy
204
72
 
205
- ```kotlin
206
- fun authenticateWithCrypto(
207
- activity: FragmentActivity,
208
- onSuccess: (Cipher) -> Unit,
209
- ) {
210
- val key = getOrCreateBiometricKey()
211
- val cipher = Cipher.getInstance("AES/GCM/NoPadding")
212
- cipher.init(Cipher.ENCRYPT_MODE, key)
213
-
214
- val cryptoObject = BiometricPrompt.CryptoObject(cipher)
215
-
216
- val prompt = BiometricPrompt(activity, object : BiometricPrompt.AuthenticationCallback() {
217
- override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
218
- result.cryptoObject?.cipher?.let(onSuccess)
219
- }
220
- })
221
-
222
- prompt.authenticate(promptInfo, cryptoObject)
223
- }
224
-
225
- private fun getOrCreateBiometricKey(): SecretKey {
226
- val keyGenerator = KeyGenerator.getInstance("AES", "AndroidKeyStore")
227
- keyGenerator.init(
228
- KeyGenParameterSpec.Builder("biometric_key",
229
- KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
230
- .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
231
- .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
232
- .setUserAuthenticationRequired(true)
233
- .setInvalidatedByBiometricEnrollment(true)
234
- .build()
235
- )
236
- return keyGenerator.generateKey()
237
- }
238
- ```
73
+ Protect a database passphrase with envelope encryption rather than storing it
74
+ directly. Generate a random 32-byte data key, wrap it with a non-exportable
75
+ AndroidKeystore AES/GCM key, and persist only the wrapped key plus its IV.
76
+ Decrypt the data key just in time, hand it to the database, and zero the
77
+ plaintext key bytes in a `finally` block once the database owns them. The
78
+ SQLCipher `SupportFactory` wiring that consumes the passphrase lives in the
79
+ `room-database` skill.
80
+
81
+ See `references/patterns.md` -> "Database Key Hierarchy" for the
82
+ `DatabaseKeyProvider` (wrap/unwrap against the Keystore) and the
83
+ decrypt-then-zero consumer.
84
+
85
+ ## Build-Type Security Gating
86
+
87
+ Decide certificate pinning and traffic inspection in one place, by build type.
88
+ Pinning is production-only -- a debug proxy would fail the pins -- and the
89
+ inspection and logging interceptors are debug-only, added last so they observe
90
+ the final decorated request. Bind the debug interceptor set from a module in the
91
+ `src/debug` source set and an empty set from `src/release`, so a release build
92
+ cannot link the inspector at all.
93
+
94
+ See `references/patterns.md` -> "Build-Type Security Gating" for the
95
+ `OkHttpClientFactory` assembly and the paired `src/release` / `src/debug` Hilt
96
+ modules.
97
+
98
+ ## Biometric Authentication
99
+
100
+ Use `BiometricPrompt` with `BIOMETRIC_STRONG` for high-security flows. Always call
101
+ `canAuthenticate()` first and map the result before showing the prompt. For flows
102
+ that must prove a fresh authentication (unlocking a key), bind a `CryptoObject`
103
+ backed by a Keystore key created with `setUserAuthenticationRequired(true)` and
104
+ `setInvalidatedByBiometricEnrollment(true)`.
105
+
106
+ See `references/patterns.md` -> "Biometric Authentication" for the
107
+ `BiometricAuthenticator` setup (with error mapping) and the crypto-based
108
+ `authenticateWithCrypto` variant.
239
109
 
240
110
  ## Certificate Pinning
241
111
 
242
- ### OkHttp CertificatePinner
112
+ Pin with OkHttp `CertificatePinner` or a Network Security Config XML. Include at
113
+ least two pins (primary + backup) so certificate rotation does not brick clients.
243
114
 
244
115
  ```kotlin
245
116
  val certificatePinner = CertificatePinner.Builder()
246
- .add("api.example.com", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") // Primary
247
- .add("api.example.com", "sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=") // Backup
117
+ .add("api.example.com", "sha256/AAAA...=") // Primary
118
+ .add("api.example.com", "sha256/BBBB...=") // Backup
248
119
  .build()
249
120
 
250
121
  val client = OkHttpClient.Builder()
@@ -252,79 +123,14 @@ val client = OkHttpClient.Builder()
252
123
  .build()
253
124
  ```
254
125
 
255
- ### Generating Pin Hashes
256
-
257
- ```bash
258
- # From a domain
259
- openssl s_client -connect api.example.com:443 -servername api.example.com \
260
- | openssl x509 -pubkey -noout \
261
- | openssl pkey -pubin -outform der \
262
- | openssl dgst -sha256 -binary \
263
- | openssl enc -base64
264
-
265
- # From a certificate file
266
- openssl x509 -in cert.pem -pubkey -noout \
267
- | openssl pkey -pubin -outform der \
268
- | openssl dgst -sha256 -binary \
269
- | openssl enc -base64
270
- ```
271
-
272
- ### Network Security Config (XML-based)
273
-
274
- ```xml
275
- <!-- res/xml/network_security_config.xml -->
276
- <?xml version="1.0" encoding="utf-8"?>
277
- <network-security-config>
278
- <domain-config cleartextTrafficPermitted="false">
279
- <domain includeSubdomains="true">api.example.com</domain>
280
- <pin-set expiration="2026-01-01">
281
- <pin digest="SHA-256">AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</pin>
282
- <pin digest="SHA-256">BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=</pin>
283
- </pin-set>
284
- </domain-config>
285
-
286
- <!-- Block cleartext for all domains -->
287
- <base-config cleartextTrafficPermitted="false" />
288
- </network-security-config>
289
- ```
290
-
291
- ```xml
292
- <!-- AndroidManifest.xml -->
293
- <application android:networkSecurityConfig="@xml/network_security_config">
294
- ```
126
+ See `references/patterns.md` -> "Certificate Pinning" for the `openssl` pin-hash
127
+ generation commands and the equivalent `network_security_config.xml` `pin-set`.
295
128
 
296
129
  ## API Key Protection
297
130
 
298
- ### local.properties (Gitignored)
299
-
300
- ```properties
301
- # local.properties -- NEVER commit this file
302
- MAPS_API_KEY=AIzaSyB...
303
- BASE_URL=https://api.example.com/
304
- ```
305
-
306
- ### Build-Time Injection
307
-
308
- ```kotlin
309
- // build.gradle.kts
310
- import java.util.Properties
311
-
312
- val localProperties = Properties().apply {
313
- val file = rootProject.file("local.properties")
314
- if (file.exists()) load(file.inputStream())
315
- }
316
-
317
- android {
318
- defaultConfig {
319
- buildConfigField("String", "MAPS_API_KEY",
320
- "\"${localProperties["MAPS_API_KEY"] ?: ""}\"")
321
- buildConfigField("String", "BASE_URL",
322
- "\"${localProperties["BASE_URL"] ?: "https://api.example.com/"}\"")
323
- }
324
- }
325
- ```
326
-
327
- ### Usage in Code
131
+ Keep secrets out of source. Put them in gitignored `local.properties`, inject
132
+ them at build time via `buildConfigField`, and read them from `BuildConfig` (R8
133
+ obfuscates them in release builds).
328
134
 
329
135
  ```kotlin
330
136
  // Access via BuildConfig -- obfuscated by R8 in release builds
@@ -332,21 +138,9 @@ val apiKey = BuildConfig.MAPS_API_KEY
332
138
  val baseUrl = BuildConfig.BASE_URL
333
139
  ```
334
140
 
335
- ### Manifest Placeholder
336
-
337
- ```kotlin
338
- android {
339
- defaultConfig {
340
- manifestPlaceholders["MAPS_API_KEY"] = localProperties["MAPS_API_KEY"] ?: ""
341
- }
342
- }
343
- ```
344
-
345
- ```xml
346
- <meta-data
347
- android:name="com.google.android.geo.API_KEY"
348
- android:value="${MAPS_API_KEY}" />
349
- ```
141
+ See `references/patterns.md` -> "API Key Protection" for the `local.properties`
142
+ layout, the `build.gradle.kts` injection block, and the manifest-placeholder
143
+ route for keys consumed from `AndroidManifest.xml`.
350
144
 
351
145
  ## R8 Obfuscation
352
146
 
@@ -356,127 +150,37 @@ R8 is enabled automatically when `isMinifyEnabled = true`. It performs:
356
150
  - Obfuscation (renames classes/methods)
357
151
  - Optimization (inlining, dead code elimination)
358
152
 
359
- ### Mapping File
360
-
361
- R8 generates `mapping.txt` for deobfuscating crash reports:
362
-
363
- ```kotlin
364
- android {
365
- buildTypes {
366
- release {
367
- isMinifyEnabled = true
368
- isShrinkResources = true
369
- proguardFiles(
370
- getDefaultProguardFile("proguard-android-optimize.txt"),
371
- "proguard-rules.pro",
372
- )
373
- }
374
- }
375
- }
376
- ```
153
+ R8 generates `mapping.txt` for deobfuscating crash reports. Upload it to Google
154
+ Play Console and Firebase Crashlytics for readable stack traces. Add `-keep`
155
+ rules for `@Serializable` and other reflectively-accessed classes.
377
156
 
378
- Upload `mapping.txt` to Google Play Console and Firebase Crashlytics for
379
- readable stack traces.
157
+ See `references/patterns.md` -> "R8 Obfuscation" for the release `buildTypes`
158
+ block.
380
159
 
381
160
  ## Play Integrity API
382
161
 
383
- Verify that API requests come from a genuine app on a genuine device.
162
+ Verify that API requests come from a genuine app on a genuine device. Request an
163
+ integrity token on the client, then send it to your server. Never verify
164
+ integrity tokens on the client -- the server must decode and validate them via
165
+ the Play Integrity API.
384
166
 
385
- ```kotlin
386
- class IntegrityVerifier @Inject constructor(
387
- @ApplicationContext private val context: Context,
388
- ) {
389
- suspend fun getIntegrityToken(nonce: String): String? {
390
- return try {
391
- val integrityManager = IntegrityManagerFactory.create(context)
392
-
393
- val request = IntegrityTokenRequest.builder()
394
- .setNonce(nonce)
395
- .build()
396
-
397
- val response = integrityManager
398
- .requestIntegrityToken(request)
399
- .await()
400
-
401
- response.token()
402
- } catch (e: Exception) {
403
- null
404
- }
405
- }
406
- }
407
- ```
408
-
409
- Send the token to your server for verification. Never verify integrity tokens
410
- on the client -- the server must decode and validate them via the Play Integrity
411
- API.
167
+ See `references/patterns.md` -> "Play Integrity API" for the `IntegrityVerifier`
168
+ token request.
412
169
 
413
170
  ## Root Detection
414
171
 
415
172
  Root detection is defense-in-depth. Determined attackers can bypass it, but it
416
- raises the bar.
417
-
418
- ```kotlin
419
- object RootDetector {
420
-
421
- fun isDeviceRooted(): Boolean =
422
- checkRootBinaries() || checkSuExists() || checkRootApps()
423
-
424
- private fun checkRootBinaries(): Boolean {
425
- val paths = listOf(
426
- "/system/bin/su",
427
- "/system/xbin/su",
428
- "/sbin/su",
429
- "/system/app/Superuser.apk",
430
- "/system/app/SuperSU.apk",
431
- )
432
- return paths.any { File(it).exists() }
433
- }
434
-
435
- private fun checkSuExists(): Boolean = try {
436
- Runtime.getRuntime().exec("which su").inputStream.bufferedReader().readLine() != null
437
- } catch (e: Exception) {
438
- false
439
- }
440
-
441
- private fun checkRootApps(): Boolean {
442
- val rootPackages = listOf(
443
- "com.topjohnwu.magisk",
444
- "eu.chainfire.supersu",
445
- "com.koushikdutta.superuser",
446
- )
447
- // Check if any are installed (requires QUERY_ALL_PACKAGES or specific queries)
448
- return false // Simplified -- use PackageManager in production
449
- }
450
- }
451
- ```
173
+ raises the bar. Use it as one signal among many and combine it with Play
174
+ Integrity for stronger guarantees.
452
175
 
453
- Use root detection as one signal among many. Combine with Play Integrity for
454
- stronger guarantees.
176
+ See `references/patterns.md` -> "Root Detection" for the `RootDetector` object
177
+ (root binaries, `su` probe, known root packages).
455
178
 
456
179
  ## Secure Network Communication
457
180
 
458
- ### Enforce HTTPS
459
-
460
- ```xml
461
- <!-- network_security_config.xml -->
462
- <network-security-config>
463
- <base-config cleartextTrafficPermitted="false" />
464
- </network-security-config>
465
- ```
466
-
467
- ### Debug-Only Cleartext (for local development)
468
-
469
- ```xml
470
- <network-security-config>
471
- <base-config cleartextTrafficPermitted="false" />
472
- <domain-config cleartextTrafficPermitted="true">
473
- <domain includeSubdomains="false">10.0.2.2</domain>
474
- <domain includeSubdomains="false">localhost</domain>
475
- </domain-config>
476
- </network-security-config>
477
- ```
478
-
479
- ### TLS Configuration
181
+ Enforce HTTPS by setting `cleartextTrafficPermitted="false"` in the Network
182
+ Security Config, add a debug-only cleartext exception for local dev hosts
183
+ (`10.0.2.2`, `localhost`), and restrict TLS to 1.2/1.3.
480
184
 
481
185
  ```kotlin
482
186
  val spec = ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
@@ -488,49 +192,26 @@ val client = OkHttpClient.Builder()
488
192
  .build()
489
193
  ```
490
194
 
491
- ## Secure Coding Patterns
492
-
493
- ### Never Log Sensitive Data
494
-
495
- ```kotlin
496
- // WRONG
497
- Log.d("Auth", "Token: $token")
498
- Log.d("Payment", "Card: $cardNumber")
499
-
500
- // CORRECT
501
- Log.d("Auth", "Token refresh successful")
502
- Log.d("Payment", "Payment processed")
503
- ```
504
-
505
- ### Input Validation
195
+ See `references/patterns.md` -> "Secure Network Communication" for the HTTPS-only
196
+ and debug-cleartext XML configs.
506
197
 
507
- ```kotlin
508
- fun validateDeepLink(uri: Uri): Boolean {
509
- val allowedHosts = setOf("example.com", "www.example.com")
510
- val allowedSchemes = setOf("https")
511
-
512
- return uri.scheme in allowedSchemes &&
513
- uri.host in allowedHosts &&
514
- !uri.path.orEmpty().contains("..")
515
- }
516
- ```
198
+ ## Secure Coding Patterns
517
199
 
518
- ### Secure WebView
200
+ - Never log tokens, passwords, or card numbers -- log outcomes, not values.
201
+ - Validate deep-link URIs: allow-list scheme and host, reject `..` paths.
202
+ - Lock down WebView: disable JavaScript, file access, content access, and DOM
203
+ storage unless a feature strictly requires them.
519
204
 
520
- ```kotlin
521
- webView.settings.apply {
522
- javaScriptEnabled = false // Enable ONLY if required
523
- allowFileAccess = false
524
- allowContentAccess = false
525
- domStorageEnabled = false
526
- }
527
- ```
205
+ See `references/patterns.md` -> "Secure Coding Patterns" for the logging,
206
+ `validateDeepLink`, and secure-WebView snippets.
528
207
 
529
208
  ## Do's and Don'ts
530
209
 
531
210
  ### Do's
532
- - Use EncryptedSharedPreferences for sensitive key-value data
211
+ - Encrypt key-value secrets at the DataStore serializer layer with a Keystore key
533
212
  - Use Android Keystore for cryptographic keys
213
+ - Envelope-wrap a database passphrase and zero the plaintext key after use
214
+ - Gate certificate pinning and inspectors by build type in one place
534
215
  - Enable R8 with `isMinifyEnabled = true` for release builds
535
216
  - Upload `mapping.txt` to Play Console and Crashlytics
536
217
  - Use Network Security Config to enforce HTTPS
@@ -539,7 +220,9 @@ webView.settings.apply {
539
220
  - Use BiometricPrompt with BIOMETRIC_STRONG for high-security flows
540
221
 
541
222
  ### Don'ts
542
- - Do not store secrets in SharedPreferences (use EncryptedSharedPreferences)
223
+ - Do not store secrets in plain SharedPreferences, or reach for the deprecated `EncryptedSharedPreferences` for new code
224
+ - Do not persist a database passphrase or data key in the clear
225
+ - Do not link a traffic inspector or body logging into release builds
543
226
  - Do not hardcode API keys in Kotlin/Java source files
544
227
  - Do not commit `local.properties` or keystore files to version control
545
228
  - Do not log tokens, passwords, or card numbers
@@ -553,7 +236,9 @@ webView.settings.apply {
553
236
  | Problem | Cause | Fix |
554
237
  |---------|-------|-----|
555
238
  | `KeyPermanentlyInvalidatedException` | Biometric enrollment changed | Delete and recreate the Keystore key |
556
- | EncryptedSharedPreferences crash on upgrade | MasterKey corrupted | Handle exception, recreate storage, re-authenticate user |
239
+ | EncryptedSharedPreferences crash on upgrade | MasterKey corrupted | Migrate to encrypted DataStore; handle exception, recreate storage, re-authenticate |
240
+ | Database fails to open after reinstall | Wrapping key lost with the Keystore; wrapped data key undecryptable | Treat encrypted data as unrecoverable; recreate the store and re-sync |
241
+ | Debug proxy shows no traffic in release | Inspector correctly excluded from `src/release` | Expected; inspect on a debug build |
557
242
  | Certificate pinning fails after cert rotation | Pin hash mismatch | Update pins; always maintain a backup pin |
558
243
  | R8 strips serialization classes | Missing keep rules | Add `-keep` rules for `@Serializable` classes |
559
244
  | Play Integrity token is null | Missing Play Core dependency or not on Play device | Check `com.google.android.play:integrity` dependency |
@@ -563,7 +248,9 @@ webView.settings.apply {
563
248
 
564
249
  ## Review Checklist
565
250
 
566
- - [ ] Sensitive data in EncryptedSharedPreferences or Keystore, not plain SharedPreferences
251
+ - [ ] Key-value secrets in a Keystore-backed encrypted DataStore, not plain or deprecated SharedPreferences
252
+ - [ ] Database passphrase envelope-wrapped by a Keystore key; plaintext data key zeroed after use
253
+ - [ ] Certificate pinning and inspectors gated by build type; no inspector in release
567
254
  - [ ] No hardcoded API keys or secrets in source files
568
255
  - [ ] `local.properties` is in `.gitignore`
569
256
  - [ ] R8 enabled with `isMinifyEnabled = true` for release