@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.
- package/CHANGELOG.md +37 -0
- package/docs/facts.json +6 -6
- package/manifest.json +55 -34
- package/package.json +1 -1
- package/pipeline/multi-agent-refs/features/usage-reporting.md +7 -3
- package/pipeline/schemas/prefs.schema.json +4 -0
- package/pipeline/scripts/usage-register.mjs +2 -0
- package/pipeline/skills/.skill-manifest.json +36 -20
- package/pipeline/skills/.skills-index.json +75 -9
- package/pipeline/skills/shared/README.md +13 -7
- package/pipeline/skills/shared/external/android-architecture/SKILL.md +71 -0
- package/pipeline/skills/shared/external/android-architecture/references/patterns.md +142 -0
- package/pipeline/skills/shared/external/android-build-quality-gates/SKILL.md +314 -0
- package/pipeline/skills/shared/external/android-build-quality-gates/references/patterns.md +432 -0
- package/pipeline/skills/shared/external/android-datastore/SKILL.md +236 -0
- package/pipeline/skills/shared/external/android-datastore/references/patterns.md +297 -0
- package/pipeline/skills/shared/external/android-design-tokens-codegen/SKILL.md +249 -0
- package/pipeline/skills/shared/external/android-design-tokens-codegen/references/patterns.md +270 -0
- package/pipeline/skills/shared/external/android-jetpack-compose-expert/SKILL.md +62 -0
- package/pipeline/skills/shared/external/android-mvi-viewmodel/SKILL.md +255 -0
- package/pipeline/skills/shared/external/android-mvi-viewmodel/references/patterns.md +257 -0
- package/pipeline/skills/shared/external/android-performance/SKILL.md +86 -602
- package/pipeline/skills/shared/external/android-performance/references/patterns.md +659 -0
- package/pipeline/skills/shared/external/android-security/SKILL.md +117 -430
- package/pipeline/skills/shared/external/android-security/references/patterns.md +690 -0
- package/pipeline/skills/shared/external/{android_ui_verification → android-ui-verification}/SKILL.md +1 -1
- package/pipeline/skills/shared/external/api-security-best-practices/SKILL.md +35 -733
- package/pipeline/skills/shared/external/api-security-best-practices/references/auth.md +299 -0
- package/pipeline/skills/shared/external/api-security-best-practices/references/input-validation.md +255 -0
- package/pipeline/skills/shared/external/api-security-best-practices/references/rate-limiting.md +167 -0
- package/pipeline/skills/shared/external/app-intents/SKILL.md +39 -174
- package/pipeline/skills/shared/external/app-intents/references/appintents-advanced.md +178 -0
- package/pipeline/skills/shared/external/compose-components/SKILL.md +48 -0
- package/pipeline/skills/shared/external/compose-components/references/patterns.md +200 -0
- package/pipeline/skills/shared/external/compose-navigation/SKILL.md +66 -3
- package/pipeline/skills/shared/external/compose-navigation/references/patterns.md +191 -0
- package/pipeline/skills/shared/external/compose-testing/SKILL.md +107 -397
- package/pipeline/skills/shared/external/compose-testing/references/patterns.md +631 -0
- package/pipeline/skills/shared/external/gradle-kotlin-dsl/SKILL.md +121 -449
- package/pipeline/skills/shared/external/gradle-kotlin-dsl/references/patterns.md +715 -0
- package/pipeline/skills/shared/external/kotlin-coroutines-expert/SKILL.md +143 -0
- package/pipeline/skills/shared/external/mapkit-location/SKILL.md +27 -102
- package/pipeline/skills/shared/external/mapkit-location/references/mapkit-patterns.md +42 -0
- package/pipeline/skills/shared/external/retrofit-networking/SKILL.md +94 -383
- package/pipeline/skills/shared/external/retrofit-networking/references/patterns.md +640 -0
- package/pipeline/skills/shared/external/room-database/SKILL.md +101 -440
- package/pipeline/skills/shared/external/room-database/references/patterns.md +614 -0
- package/pipeline/skills/shared/external/storekit/SKILL.md +69 -343
- package/pipeline/skills/shared/external/storekit/references/core-patterns.md +371 -0
- package/pipeline/skills/shared/external/widgetkit/SKILL.md +25 -101
- package/pipeline/skills/shared/external/widgetkit/references/widgetkit-advanced.md +107 -0
- 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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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 |
|
|
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
|
-
|
|
|
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
|
-
##
|
|
57
|
+
## Encrypted DataStore for Secrets
|
|
143
58
|
|
|
144
|
-
|
|
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
|
-
|
|
147
|
-
|
|
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
|
-
|
|
71
|
+
## Database Key Hierarchy
|
|
204
72
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
|
|
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/
|
|
247
|
-
.add("api.example.com", "sha256/
|
|
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
|
-
|
|
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
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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
|
-
|
|
379
|
-
|
|
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
|
-
|
|
386
|
-
|
|
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
|
-
|
|
454
|
-
|
|
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
|
-
|
|
459
|
-
|
|
460
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
521
|
-
|
|
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
|
-
-
|
|
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
|
|
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 |
|
|
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
|
-
- [ ]
|
|
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
|