android-sdd 1.0.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.
- package/dist/index.js +143 -0
- package/package.json +27 -0
- package/skills/Android Ecosystem/Baseline Profile Generator/SKILL.md +277 -0
- package/skills/Android Ecosystem/Glance/SKILL.md +315 -0
- package/skills/Android Platform/Configuration/SKILL.md +201 -0
- package/skills/Android Platform/Filesystem/SKILL.md +216 -0
- package/skills/Android Platform/Lifecycle/SKILL.md +233 -0
- package/skills/Android Platform/Manifest/SKILL.md +226 -0
- package/skills/Android Platform/Process Death Recovery/SKILL.md +214 -0
- package/skills/Android Platform/Resources/SKILL.md +234 -0
- package/skills/Android Platform/SavedStateHandle/SKILL.md +217 -0
- package/skills/Android Platform/State Restoration/SKILL.md +210 -0
- package/skills/Architecture/Bounded Context/SKILL.md +207 -0
- package/skills/Architecture/Clean Architecture/SKILL.md +229 -0
- package/skills/Architecture/Domain Modeling/SKILL.md +236 -0
- package/skills/Architecture/Entity Design/SKILL.md +243 -0
- package/skills/Architecture/Feature Isolation/SKILL.md +216 -0
- package/skills/Architecture/MVI/SKILL.md +224 -0
- package/skills/Architecture/MVVM/SKILL.md +198 -0
- package/skills/Architecture/Modularization/SKILL.md +194 -0
- package/skills/Architecture/Offline First/SKILL.md +249 -0
- package/skills/Architecture/Repository Pattern/SKILL.md +216 -0
- package/skills/Architecture/Side Effect Management/SKILL.md +278 -0
- package/skills/Architecture/State Management/SKILL.md +229 -0
- package/skills/Architecture/Unidirectional Data Flow/SKILL.md +196 -0
- package/skills/Architecture/Use Case Design/SKILL.md +244 -0
- package/skills/Architecture/Value Object/SKILL.md +226 -0
- package/skills/Build Infrastructure/Build Orchestration/SKILL.md +257 -0
- package/skills/Build Infrastructure/Dependency Compatibility Resolver/SKILL.md +259 -0
- package/skills/Build Infrastructure/Environment Validator/SKILL.md +311 -0
- package/skills/Build System/Build Cache/SKILL.md +233 -0
- package/skills/Build System/Build Flavor Strategy/SKILL.md +171 -0
- package/skills/Build System/Build Variant/SKILL.md +215 -0
- package/skills/Build System/Convention Plugin/SKILL.md +288 -0
- package/skills/Build System/Dependency Management/SKILL.md +261 -0
- package/skills/Build System/Gradle/SKILL.md +284 -0
- package/skills/Build System/Incremental Build/SKILL.md +199 -0
- package/skills/Build System/KAPT/SKILL.md +198 -0
- package/skills/Build System/KSP/SKILL.md +263 -0
- package/skills/Build System/Module Dependency Graph Validation/SKILL.md +223 -0
- package/skills/Build System/Specialized/C++/SKILL.md +308 -0
- package/skills/Build System/Specialized/JNI/SKILL.md +306 -0
- package/skills/Build System/Specialized/NDK/SKILL.md +264 -0
- package/skills/Build System/Version Catalog/SKILL.md +304 -0
- package/skills/Concurrency/Background Processing/SKILL.md +185 -0
- package/skills/Concurrency/Channel/SKILL.md +207 -0
- package/skills/Concurrency/Coroutine/SKILL.md +200 -0
- package/skills/Concurrency/Flow/SKILL.md +179 -0
- package/skills/Concurrency/Mutex Strategy/SKILL.md +185 -0
- package/skills/Concurrency/SharedFlow/SKILL.md +171 -0
- package/skills/Concurrency/StateFlow/SKILL.md +175 -0
- package/skills/Concurrency/Structured Concurrency/SKILL.md +197 -0
- package/skills/Concurrency/Synchronization Policy/SKILL.md +192 -0
- package/skills/Core Language/Annotation Processing/SKILL.md +224 -0
- package/skills/Core Language/DSL/SKILL.md +186 -0
- package/skills/Core Language/Extension Functions Design/SKILL.md +191 -0
- package/skills/Core Language/Immutability/SKILL.md +156 -0
- package/skills/Core Language/KMP/SKILL.md +182 -0
- package/skills/Core Language/Kotlin/SKILL.md +187 -0
- package/skills/Core Language/Reactive State Management/SKILL.md +228 -0
- package/skills/Core Language/Reactive Streams/SKILL.md +235 -0
- package/skills/Core Language/Serialization/SKILL.md +191 -0
- package/skills/Data Layer/Cache Strategy/SKILL.md +261 -0
- package/skills/Data Layer/Conflict Resolution/SKILL.md +248 -0
- package/skills/Data Layer/DAO/SKILL.md +225 -0
- package/skills/Data Layer/DTO Mapping/SKILL.md +269 -0
- package/skills/Data Layer/DataStore/SKILL.md +264 -0
- package/skills/Data Layer/Database Versioning Strategy/SKILL.md +215 -0
- package/skills/Data Layer/Encrypted Database/SKILL.md +212 -0
- package/skills/Data Layer/File Storage/SKILL.md +247 -0
- package/skills/Data Layer/Indexing/SKILL.md +184 -0
- package/skills/Data Layer/Key-Value Store Strategy/SKILL.md +185 -0
- package/skills/Data Layer/Merge Strategy/SKILL.md +240 -0
- package/skills/Data Layer/Migration/SKILL.md +243 -0
- package/skills/Data Layer/Paging/SKILL.md +264 -0
- package/skills/Data Layer/Proto DataStore/SKILL.md +250 -0
- package/skills/Data Layer/Room/SKILL.md +244 -0
- package/skills/Data Layer/SQLite/SKILL.md +255 -0
- package/skills/Data Layer/Sync Engine/SKILL.md +268 -0
- package/skills/Dependency Injection/Dagger/SKILL.md +283 -0
- package/skills/Dependency Injection/Hilt/SKILL.md +345 -0
- package/skills/Dependency Injection/Koin/SKILL.md +282 -0
- package/skills/Developer Experience/Detekt/SKILL.md +272 -0
- package/skills/Developer Experience/Lint Rule/SKILL.md +281 -0
- package/skills/Google Ecosystem/Analytics/SKILL.md +281 -0
- package/skills/Google Ecosystem/Crashlytics/SKILL.md +234 -0
- package/skills/Google Ecosystem/Firebase/SKILL.md +200 -0
- package/skills/Google Ecosystem/Firebase Messaging/SKILL.md +266 -0
- package/skills/Media/Audio/SKILL.md +257 -0
- package/skills/Media/Camera/SKILL.md +229 -0
- package/skills/Media/CameraX/SKILL.md +295 -0
- package/skills/Media/ExoPlayer/SKILL.md +258 -0
- package/skills/Media/Video/SKILL.md +228 -0
- package/skills/Meta Skills/Domain Error Model/SKILL.md +238 -0
- package/skills/Meta Skills/Error Handling/SKILL.md +255 -0
- package/skills/Meta Skills/Error Mapping/SKILL.md +232 -0
- package/skills/Meta Skills/Failure Strategy/SKILL.md +294 -0
- package/skills/Meta Skills/Migration Strategy/SKILL.md +305 -0
- package/skills/Meta Skills/User Friendly Errors/SKILL.md +334 -0
- package/skills/Navigation/Deep Navigation/SKILL.md +209 -0
- package/skills/Navigation/Navigation/SKILL.md +215 -0
- package/skills/Navigation/Nested Navigation/SKILL.md +214 -0
- package/skills/Networking/API Contract/SKILL.md +220 -0
- package/skills/Networking/Authentication/SKILL.md +210 -0
- package/skills/Networking/Certificate Pinning/SKILL.md +167 -0
- package/skills/Networking/Fallback Strategy/SKILL.md +182 -0
- package/skills/Networking/Ktor/SKILL.md +219 -0
- package/skills/Networking/Multipart Upload/SKILL.md +213 -0
- package/skills/Networking/OkHttp/SKILL.md +193 -0
- package/skills/Networking/REST/SKILL.md +178 -0
- package/skills/Networking/Rate Limiting/SKILL.md +170 -0
- package/skills/Networking/Retrofit/SKILL.md +241 -0
- package/skills/Networking/Retry-Backoff/SKILL.md +181 -0
- package/skills/Networking/Server-Sent Events (SSE)/SKILL.md +196 -0
- package/skills/Networking/WebSocket/SKILL.md +224 -0
- package/skills/Observability/Crash Reporting/SKILL.md +219 -0
- package/skills/Observability/Logging/SKILL.md +168 -0
- package/skills/Observability/Metrics/SKILL.md +227 -0
- package/skills/Observability/Structured Logging/SKILL.md +234 -0
- package/skills/Performance/ANR Prevention/SKILL.md +192 -0
- package/skills/Performance/Allocation Optimization/SKILL.md +179 -0
- package/skills/Performance/App Startup/SKILL.md +183 -0
- package/skills/Performance/Baseline Profile/SKILL.md +205 -0
- package/skills/Performance/Battery Optimization/SKILL.md +192 -0
- package/skills/Performance/Benchmark/SKILL.md +182 -0
- package/skills/Performance/Bitmap Optimization/SKILL.md +178 -0
- package/skills/Performance/Compose Optimization/SKILL.md +187 -0
- package/skills/Performance/Heap Management/SKILL.md +184 -0
- package/skills/Performance/Macrobenchmark/SKILL.md +214 -0
- package/skills/Performance/Memory Leak Prevention/SKILL.md +218 -0
- package/skills/Performance/Rendering Performance/SKILL.md +205 -0
- package/skills/Performance/Startup Optimization/SKILL.md +219 -0
- package/skills/Security/Biometric/SKILL.md +224 -0
- package/skills/Security/Certificate Transparency/SKILL.md +158 -0
- package/skills/Security/Cryptography/SKILL.md +244 -0
- package/skills/Security/Encrypted Storage/SKILL.md +273 -0
- package/skills/Security/Frida Detection/SKILL.md +230 -0
- package/skills/Security/Hook Detection/SKILL.md +197 -0
- package/skills/Security/Keystore/SKILL.md +272 -0
- package/skills/Security/Network Security Config/SKILL.md +186 -0
- package/skills/Security/Obfuscation/SKILL.md +226 -0
- package/skills/Security/Proguard/SKILL.md +202 -0
- package/skills/Security/R8/SKILL.md +234 -0
- package/skills/Security/Reverse Engineering Resistance/SKILL.md +267 -0
- package/skills/Security/Root Detection/SKILL.md +220 -0
- package/skills/Security/Secure Networking/SKILL.md +220 -0
- package/skills/System Integration/AlarmManager/SKILL.md +182 -0
- package/skills/System Integration/App Widget/SKILL.md +182 -0
- package/skills/System Integration/Deep Link/SKILL.md +187 -0
- package/skills/System Integration/Foreground Service/SKILL.md +212 -0
- package/skills/System Integration/Notification/SKILL.md +237 -0
- package/skills/System Integration/WorkManager/SKILL.md +256 -0
- package/skills/System Integration/clipboard/SKILL.md +155 -0
- package/skills/System Integration/share-intent/SKILL.md +182 -0
- package/skills/Testing/Compose Testing/SKILL.md +296 -0
- package/skills/Testing/Espresso/SKILL.md +292 -0
- package/skills/Testing/Fake Data/SKILL.md +245 -0
- package/skills/Testing/Integration Testing/SKILL.md +288 -0
- package/skills/Testing/Mocking/SKILL.md +229 -0
- package/skills/Testing/Snapshot Testing/SKILL.md +259 -0
- package/skills/Testing/UI Testing/SKILL.md +293 -0
- package/skills/Testing/Unit Testing/SKILL.md +309 -0
- package/skills/UI System/Bottom Sheet Patterns/SKILL.md +279 -0
- package/skills/UI System/Compose/SKILL.md +296 -0
- package/skills/UI System/Compose Animation/SKILL.md +281 -0
- package/skills/UI System/Compose Multiplatform/SKILL.md +261 -0
- package/skills/UI System/Compose Navigation/SKILL.md +255 -0
- package/skills/UI System/Compose Performance/SKILL.md +274 -0
- package/skills/UI System/Design System/SKILL.md +217 -0
- package/skills/UI System/Empty State Strategy/SKILL.md +208 -0
- package/skills/UI System/Keyboard Navigation/SKILL.md +214 -0
- package/skills/UI System/Loading Strategy/SKILL.md +254 -0
- package/skills/UI System/Material 3/SKILL.md +279 -0
- package/skills/UI System/RTL/SKILL.md +179 -0
- package/src/index.ts +182 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: biometric
|
|
3
|
+
description: >
|
|
4
|
+
Biometric authentication for Android apps.
|
|
5
|
+
Load this skill when implementing fingerprint or face authentication,
|
|
6
|
+
using BiometricPrompt, tying biometric auth to Keystore keys,
|
|
7
|
+
handling fallback to device credentials, or checking biometric availability.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Biometric
|
|
11
|
+
|
|
12
|
+
## Overview
|
|
13
|
+
Android's BiometricPrompt API provides a unified interface for fingerprint, face, and iris authentication. Biometric auth can be used standalone (gate access to a screen) or tied to a Keystore key (unlock a cryptographic operation). The latter is stronger — authentication is required each time the key is used.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Core Principles
|
|
18
|
+
|
|
19
|
+
- Use **BiometricPrompt** — never use FingerprintManager directly (deprecated)
|
|
20
|
+
- **Crypto-based auth** (tied to Keystore key) is stronger than confirmation-only auth
|
|
21
|
+
- Always provide **fallback to device credentials** (PIN/pattern/password) for accessibility
|
|
22
|
+
- Handle `KeyPermanentlyInvalidatedException` — thrown when new biometrics are enrolled
|
|
23
|
+
- Check availability before showing biometric UI — degrade gracefully on unsupported devices
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Check Availability
|
|
28
|
+
|
|
29
|
+
```kotlin
|
|
30
|
+
// ✅ Check if biometric is available before using it
|
|
31
|
+
class BiometricAvailabilityChecker @Inject constructor(
|
|
32
|
+
@ApplicationContext private val context: Context
|
|
33
|
+
) {
|
|
34
|
+
sealed interface Availability {
|
|
35
|
+
data object Available : Availability
|
|
36
|
+
data object NotEnrolled : Availability
|
|
37
|
+
data object NotSupported : Availability
|
|
38
|
+
data object HardwareUnavailable : Availability
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
fun check(): Availability {
|
|
42
|
+
val manager = BiometricManager.from(context)
|
|
43
|
+
return when (manager.canAuthenticate(
|
|
44
|
+
BiometricManager.Authenticators.BIOMETRIC_STRONG or
|
|
45
|
+
BiometricManager.Authenticators.DEVICE_CREDENTIAL
|
|
46
|
+
)) {
|
|
47
|
+
BiometricManager.BIOMETRIC_SUCCESS -> Availability.Available
|
|
48
|
+
BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> Availability.NotEnrolled
|
|
49
|
+
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> Availability.NotSupported
|
|
50
|
+
BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> Availability.HardwareUnavailable
|
|
51
|
+
else -> Availability.NotSupported
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Simple Biometric Prompt (Confirmation Only)
|
|
60
|
+
|
|
61
|
+
```kotlin
|
|
62
|
+
// ✅ Gate screen access — no Keystore key involved
|
|
63
|
+
class BiometricAuthManager @Inject constructor() {
|
|
64
|
+
|
|
65
|
+
sealed interface AuthResult {
|
|
66
|
+
data object Success : AuthResult
|
|
67
|
+
data class Error(val message: String) : AuthResult
|
|
68
|
+
data object Cancelled : AuthResult
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
fun authenticate(
|
|
72
|
+
activity: FragmentActivity,
|
|
73
|
+
title: String = "Authenticate",
|
|
74
|
+
subtitle: String = "Use biometric or device credential",
|
|
75
|
+
onResult: (AuthResult) -> Unit
|
|
76
|
+
) {
|
|
77
|
+
val executor = ContextCompat.getMainExecutor(activity)
|
|
78
|
+
|
|
79
|
+
val prompt = BiometricPrompt(activity, executor,
|
|
80
|
+
object : BiometricPrompt.AuthenticationCallback() {
|
|
81
|
+
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
|
|
82
|
+
onResult(AuthResult.Success)
|
|
83
|
+
}
|
|
84
|
+
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
|
85
|
+
if (errorCode == BiometricPrompt.ERROR_USER_CANCELED ||
|
|
86
|
+
errorCode == BiometricPrompt.ERROR_NEGATIVE_BUTTON) {
|
|
87
|
+
onResult(AuthResult.Cancelled)
|
|
88
|
+
} else {
|
|
89
|
+
onResult(AuthResult.Error(errString.toString()))
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
override fun onAuthenticationFailed() {
|
|
93
|
+
// Called on each failed attempt — not terminal; don't handle as final failure
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
val promptInfo = BiometricPrompt.PromptInfo.Builder()
|
|
99
|
+
.setTitle(title)
|
|
100
|
+
.setSubtitle(subtitle)
|
|
101
|
+
.setAllowedAuthenticators(
|
|
102
|
+
BiometricManager.Authenticators.BIOMETRIC_STRONG or
|
|
103
|
+
BiometricManager.Authenticators.DEVICE_CREDENTIAL
|
|
104
|
+
)
|
|
105
|
+
.build()
|
|
106
|
+
|
|
107
|
+
prompt.authenticate(promptInfo)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## Crypto-Based Biometric (Keystore-Tied)
|
|
115
|
+
|
|
116
|
+
```kotlin
|
|
117
|
+
// ✅ Biometric auth that unlocks a Keystore key — stronger guarantee
|
|
118
|
+
class BiometricCryptoManager @Inject constructor(
|
|
119
|
+
private val keystoreCrypto: KeystoreCrypto
|
|
120
|
+
) {
|
|
121
|
+
private val keyAlias = KeystoreAliases.BIOMETRIC_KEY
|
|
122
|
+
|
|
123
|
+
// ✅ Prepare cipher before showing prompt — pass to authenticate()
|
|
124
|
+
fun prepareCipher(): Cipher? {
|
|
125
|
+
return try {
|
|
126
|
+
val keyStore = KeyStore.getInstance("AndroidKeyStore").also { it.load(null) }
|
|
127
|
+
if (!keyStore.containsAlias(keyAlias)) generateBiometricBoundKey(keyAlias)
|
|
128
|
+
|
|
129
|
+
val key = keyStore.getKey(keyAlias, null) as SecretKey
|
|
130
|
+
Cipher.getInstance("AES/GCM/NoPadding").also {
|
|
131
|
+
it.init(Cipher.ENCRYPT_MODE, key)
|
|
132
|
+
}
|
|
133
|
+
} catch (e: KeyPermanentlyInvalidatedException) {
|
|
134
|
+
// New biometrics enrolled — delete and recreate key
|
|
135
|
+
KeyStore.getInstance("AndroidKeyStore").also { it.load(null) }.deleteEntry(keyAlias)
|
|
136
|
+
null
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
fun authenticate(
|
|
141
|
+
activity: FragmentActivity,
|
|
142
|
+
cipher: Cipher,
|
|
143
|
+
onSuccess: (Cipher) -> Unit,
|
|
144
|
+
onFailure: () -> Unit
|
|
145
|
+
) {
|
|
146
|
+
val executor = ContextCompat.getMainExecutor(activity)
|
|
147
|
+
val cryptoObject = BiometricPrompt.CryptoObject(cipher)
|
|
148
|
+
|
|
149
|
+
val prompt = BiometricPrompt(activity, executor,
|
|
150
|
+
object : BiometricPrompt.AuthenticationCallback() {
|
|
151
|
+
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
|
|
152
|
+
result.cryptoObject?.cipher?.let { onSuccess(it) } ?: onFailure()
|
|
153
|
+
}
|
|
154
|
+
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
|
|
155
|
+
onFailure()
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
val promptInfo = BiometricPrompt.PromptInfo.Builder()
|
|
161
|
+
.setTitle("Confirm identity")
|
|
162
|
+
.setSubtitle("Touch sensor to continue")
|
|
163
|
+
.setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG)
|
|
164
|
+
.setNegativeButtonText("Cancel")
|
|
165
|
+
.build()
|
|
166
|
+
|
|
167
|
+
prompt.authenticate(promptInfo, cryptoObject)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## Compose Integration
|
|
175
|
+
|
|
176
|
+
```kotlin
|
|
177
|
+
// ✅ Trigger biometric from Compose via Activity
|
|
178
|
+
@Composable
|
|
179
|
+
fun BiometricGatedScreen(
|
|
180
|
+
onAuthenticated: () -> Unit,
|
|
181
|
+
content: @Composable () -> Unit
|
|
182
|
+
) {
|
|
183
|
+
val activity = LocalContext.current as FragmentActivity
|
|
184
|
+
val biometricManager = remember { BiometricAuthManager() }
|
|
185
|
+
var isAuthenticated by rememberSaveable { mutableStateOf(false) }
|
|
186
|
+
|
|
187
|
+
LaunchedEffect(Unit) {
|
|
188
|
+
biometricManager.authenticate(
|
|
189
|
+
activity = activity,
|
|
190
|
+
onResult = { result ->
|
|
191
|
+
if (result is BiometricAuthManager.AuthResult.Success) {
|
|
192
|
+
isAuthenticated = true
|
|
193
|
+
onAuthenticated()
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (isAuthenticated) {
|
|
200
|
+
content()
|
|
201
|
+
} else {
|
|
202
|
+
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
|
203
|
+
CircularProgressIndicator()
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## Anti-Patterns
|
|
212
|
+
|
|
213
|
+
- Using `FingerprintManager` — deprecated since API 28; use `BiometricPrompt`
|
|
214
|
+
- Not handling `KeyPermanentlyInvalidatedException` — crashes when user enrolls new fingerprint
|
|
215
|
+
- No fallback to device credential — locks out users with biometric issues
|
|
216
|
+
- Calling `authenticate()` from a background thread — must be called on main thread
|
|
217
|
+
- Storing sensitive data in plain storage after biometric success — biometric should unlock Keystore key
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
## Related Skills
|
|
222
|
+
- `keystore` — generating biometric-bound Keystore keys
|
|
223
|
+
- `encrypted-storage` — protecting data unlocked by biometric
|
|
224
|
+
- `cryptography` — cipher operations after biometric success
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: certificate-transparency
|
|
3
|
+
description: >
|
|
4
|
+
Certificate Transparency (CT) enforcement for Android apps.
|
|
5
|
+
Load this skill when enforcing CT log verification for TLS connections,
|
|
6
|
+
understanding CT requirements on Android, or configuring OkHttp to
|
|
7
|
+
verify certificates are publicly logged.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Certificate Transparency
|
|
11
|
+
|
|
12
|
+
## Overview
|
|
13
|
+
Certificate Transparency is a public logging framework that requires CAs to log every issued certificate to a public CT log. This makes mis-issued certificates detectable. On Android, CT enforcement is built into the OS (Android 7+) for system CAs, and can be enforced at the app level via OkHttp's `certificatetransparency` library for additional guarantees.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Core Principles
|
|
18
|
+
|
|
19
|
+
- **CT is enforced by Android OS** for system CA-signed certificates since Android 7+
|
|
20
|
+
- App-level CT enforcement provides stricter, per-host control
|
|
21
|
+
- CT does not replace certificate pinning — they address different threats
|
|
22
|
+
- CT verifies the certificate is in public logs — pinning verifies it's a specific certificate
|
|
23
|
+
- Use both for highest security: CT catches mis-issuance; pinning catches MITM
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Android OS Built-in CT
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
Android 7.0+ enforces CT for:
|
|
31
|
+
- Certificates signed by public CAs trusted by the system
|
|
32
|
+
- Connections where the certificate chain includes a public CA
|
|
33
|
+
|
|
34
|
+
Android does NOT enforce CT for:
|
|
35
|
+
- Certificates in the user certificate store
|
|
36
|
+
- Privately issued certificates
|
|
37
|
+
- Certificates added via Network Security Config
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## App-Level CT with OkHttp (certificatetransparency-android)
|
|
43
|
+
|
|
44
|
+
```kotlin
|
|
45
|
+
// dependency: com.appmattus.certificatetransparency:certificatetransparency-android
|
|
46
|
+
|
|
47
|
+
// ✅ Add CT verification to OkHttp
|
|
48
|
+
fun buildCtEnabledOkHttpClient(): OkHttpClient {
|
|
49
|
+
val ctInterceptor = certificateTransparencyInterceptor {
|
|
50
|
+
// ✅ Enforce CT for all hosts
|
|
51
|
+
+("*.*")
|
|
52
|
+
|
|
53
|
+
// ✅ Or enforce only for specific hosts
|
|
54
|
+
+("api.example.com")
|
|
55
|
+
+("*.example.com")
|
|
56
|
+
|
|
57
|
+
// ✅ Exclude hosts where CT is not applicable (e.g. internal/dev)
|
|
58
|
+
-("internal.dev.example.com")
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return OkHttpClient.Builder()
|
|
62
|
+
.addNetworkInterceptor(ctInterceptor)
|
|
63
|
+
.build()
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ✅ Alternative: use CTHostnameVerifier
|
|
67
|
+
fun buildCtEnabledClient(): OkHttpClient {
|
|
68
|
+
val ctHostnameVerifier = certificateTransparencyHostnameVerifier {
|
|
69
|
+
+("api.example.com")
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return OkHttpClient.Builder()
|
|
73
|
+
.hostnameVerifier(ctHostnameVerifier)
|
|
74
|
+
.build()
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Network Security Config + CT
|
|
81
|
+
|
|
82
|
+
```xml
|
|
83
|
+
<!-- res/xml/network_security_config.xml -->
|
|
84
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
85
|
+
<network-security-config>
|
|
86
|
+
<base-config cleartextTrafficPermitted="false">
|
|
87
|
+
<trust-anchors>
|
|
88
|
+
<!-- ✅ System CAs — Android 7+ enforces CT for these -->
|
|
89
|
+
<certificates src="system" />
|
|
90
|
+
</trust-anchors>
|
|
91
|
+
</base-config>
|
|
92
|
+
|
|
93
|
+
<!-- ✅ For debug: allow user-added CAs without CT enforcement -->
|
|
94
|
+
<debug-overrides>
|
|
95
|
+
<trust-anchors>
|
|
96
|
+
<certificates src="system" />
|
|
97
|
+
<certificates src="user" />
|
|
98
|
+
</trust-anchors>
|
|
99
|
+
</debug-overrides>
|
|
100
|
+
</network-security-config>
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## CT vs Certificate Pinning
|
|
106
|
+
|
|
107
|
+
| | Certificate Transparency | Certificate Pinning |
|
|
108
|
+
|---|---|---|
|
|
109
|
+
| What it verifies | Certificate is publicly logged | Certificate matches expected value |
|
|
110
|
+
| Protects against | Mis-issued certificates by CAs | MITM with any valid certificate |
|
|
111
|
+
| Maintenance | Low (automatic) | High (pin rotation required) |
|
|
112
|
+
| Failure mode | Rejects unlogged certificates | Rejects wrong certificates |
|
|
113
|
+
| Use for | All public API connections | High-security endpoints |
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Hilt Integration
|
|
118
|
+
|
|
119
|
+
```kotlin
|
|
120
|
+
@Module
|
|
121
|
+
@InstallIn(SingletonComponent::class)
|
|
122
|
+
object NetworkModule {
|
|
123
|
+
|
|
124
|
+
@Provides
|
|
125
|
+
@Singleton
|
|
126
|
+
fun provideOkHttpClient(
|
|
127
|
+
authInterceptor: AuthInterceptor,
|
|
128
|
+
certificatePinner: CertificatePinner
|
|
129
|
+
): OkHttpClient {
|
|
130
|
+
val ctInterceptor = certificateTransparencyInterceptor {
|
|
131
|
+
+("api.example.com")
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return OkHttpClient.Builder()
|
|
135
|
+
.addInterceptor(authInterceptor)
|
|
136
|
+
.addNetworkInterceptor(ctInterceptor) // ✅ CT after TLS handshake
|
|
137
|
+
.certificatePinner(certificatePinner)
|
|
138
|
+
.build()
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## Anti-Patterns
|
|
146
|
+
|
|
147
|
+
- Assuming OS-level CT is sufficient for all use cases — app-level gives per-host control
|
|
148
|
+
- Disabling CT verification in production builds — removes mis-issuance protection
|
|
149
|
+
- Using CT as a replacement for certificate pinning — they complement each other
|
|
150
|
+
- Not testing CT enforcement — a misconfigured CT check can block all connections silently
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Related Skills
|
|
155
|
+
- `certificate-pinning` — pinning specific certificates against MITM
|
|
156
|
+
- `secure-networking` — overall TLS and network security
|
|
157
|
+
- `network-security-config` — Android XML-based network policy
|
|
158
|
+
- `okhttp` — OkHttp configuration and interceptors
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: cryptography
|
|
3
|
+
description: >
|
|
4
|
+
Cryptographic operations for Android apps.
|
|
5
|
+
Load this skill when implementing hashing, symmetric encryption,
|
|
6
|
+
asymmetric encryption, digital signatures, key derivation,
|
|
7
|
+
HMAC, or secure random generation.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Cryptography
|
|
11
|
+
|
|
12
|
+
## Overview
|
|
13
|
+
Android provides the standard Java Cryptography Architecture (JCA) with hardware-backed key storage via Keystore. This skill covers the correct algorithms and patterns for common cryptographic needs — encryption, hashing, signing, and key derivation — without relying on Keystore (see `keystore` skill for that).
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Core Principles
|
|
18
|
+
|
|
19
|
+
- **Use proven algorithms** — AES-GCM, ChaCha20-Poly1305, RSA-OAEP, SHA-256, PBKDF2
|
|
20
|
+
- **Never roll your own crypto** — use standard JCA or Tink
|
|
21
|
+
- **Always use authenticated encryption** (AES-GCM, not AES-CBC without MAC)
|
|
22
|
+
- **Never reuse IV/nonce** with the same key
|
|
23
|
+
- **Use Tink for high-level operations** — fewer footguns than raw JCA
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Symmetric Encryption (AES-GCM)
|
|
28
|
+
|
|
29
|
+
```kotlin
|
|
30
|
+
object AesGcm {
|
|
31
|
+
private const val ALGORITHM = "AES/GCM/NoPadding"
|
|
32
|
+
private const val KEY_SIZE = 256
|
|
33
|
+
private const val IV_SIZE = 12 // 96 bits — standard for GCM
|
|
34
|
+
private const val TAG_SIZE = 128 // authentication tag bits
|
|
35
|
+
|
|
36
|
+
// ✅ Generate a random AES key (for non-Keystore use)
|
|
37
|
+
fun generateKey(): SecretKey {
|
|
38
|
+
val generator = KeyGenerator.getInstance("AES")
|
|
39
|
+
generator.init(KEY_SIZE)
|
|
40
|
+
return generator.generateKey()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ✅ Encrypt — returns IV prepended to ciphertext
|
|
44
|
+
fun encrypt(key: SecretKey, plaintext: ByteArray): ByteArray {
|
|
45
|
+
val iv = ByteArray(IV_SIZE).also { SecureRandom().nextBytes(it) }
|
|
46
|
+
val cipher = Cipher.getInstance(ALGORITHM)
|
|
47
|
+
cipher.init(Cipher.ENCRYPT_MODE, key, GCMParameterSpec(TAG_SIZE, iv))
|
|
48
|
+
val ciphertext = cipher.doFinal(plaintext)
|
|
49
|
+
return iv + ciphertext // prepend IV for storage
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ✅ Decrypt — expects IV prepended to ciphertext
|
|
53
|
+
fun decrypt(key: SecretKey, data: ByteArray): ByteArray {
|
|
54
|
+
val iv = data.copyOfRange(0, IV_SIZE)
|
|
55
|
+
val ciphertext = data.copyOfRange(IV_SIZE, data.size)
|
|
56
|
+
val cipher = Cipher.getInstance(ALGORITHM)
|
|
57
|
+
cipher.init(Cipher.DECRYPT_MODE, key, GCMParameterSpec(TAG_SIZE, iv))
|
|
58
|
+
return cipher.doFinal(ciphertext)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Hashing
|
|
66
|
+
|
|
67
|
+
```kotlin
|
|
68
|
+
object Hashing {
|
|
69
|
+
// ✅ SHA-256 hash
|
|
70
|
+
fun sha256(input: ByteArray): ByteArray =
|
|
71
|
+
MessageDigest.getInstance("SHA-256").digest(input)
|
|
72
|
+
|
|
73
|
+
fun sha256Hex(input: String): String =
|
|
74
|
+
sha256(input.toByteArray(Charsets.UTF_8))
|
|
75
|
+
.joinToString("") { "%02x".format(it) }
|
|
76
|
+
|
|
77
|
+
// ✅ SHA-256 of a file (streaming — no OOM for large files)
|
|
78
|
+
fun sha256File(file: File): ByteArray {
|
|
79
|
+
val digest = MessageDigest.getInstance("SHA-256")
|
|
80
|
+
file.inputStream().use { stream ->
|
|
81
|
+
val buffer = ByteArray(8192)
|
|
82
|
+
var read: Int
|
|
83
|
+
while (stream.read(buffer).also { read = it } != -1) {
|
|
84
|
+
digest.update(buffer, 0, read)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return digest.digest()
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ✅ HMAC-SHA256 — for message authentication
|
|
91
|
+
fun hmacSha256(key: ByteArray, message: ByteArray): ByteArray {
|
|
92
|
+
val secretKey = SecretKeySpec(key, "HmacSHA256")
|
|
93
|
+
return Mac.getInstance("HmacSHA256").run {
|
|
94
|
+
init(secretKey)
|
|
95
|
+
doFinal(message)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Password Hashing (PBKDF2)
|
|
104
|
+
|
|
105
|
+
```kotlin
|
|
106
|
+
// ✅ Hash passwords for local verification — NOT for sending to server
|
|
107
|
+
object PasswordHasher {
|
|
108
|
+
private const val ITERATIONS = 100_000
|
|
109
|
+
private const val KEY_LENGTH = 256
|
|
110
|
+
private const val SALT_SIZE = 16
|
|
111
|
+
private const val ALGORITHM = "PBKDF2WithHmacSHA256"
|
|
112
|
+
|
|
113
|
+
data class HashedPassword(val hash: ByteArray, val salt: ByteArray) {
|
|
114
|
+
fun toStorageString(): String =
|
|
115
|
+
"${Base64.encodeToString(hash, Base64.NO_WRAP)}:${Base64.encodeToString(salt, Base64.NO_WRAP)}"
|
|
116
|
+
|
|
117
|
+
companion object {
|
|
118
|
+
fun fromStorageString(value: String): HashedPassword {
|
|
119
|
+
val (hashB64, saltB64) = value.split(":")
|
|
120
|
+
return HashedPassword(
|
|
121
|
+
hash = Base64.decode(hashB64, Base64.NO_WRAP),
|
|
122
|
+
salt = Base64.decode(saltB64, Base64.NO_WRAP)
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
fun hash(password: CharArray): HashedPassword {
|
|
129
|
+
val salt = ByteArray(SALT_SIZE).also { SecureRandom().nextBytes(it) }
|
|
130
|
+
val spec = PBEKeySpec(password, salt, ITERATIONS, KEY_LENGTH)
|
|
131
|
+
val factory = SecretKeyFactory.getInstance(ALGORITHM)
|
|
132
|
+
val hash = factory.generateSecret(spec).encoded
|
|
133
|
+
spec.clearPassword()
|
|
134
|
+
return HashedPassword(hash, salt)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
fun verify(password: CharArray, stored: HashedPassword): Boolean {
|
|
138
|
+
val spec = PBEKeySpec(password, stored.salt, ITERATIONS, KEY_LENGTH)
|
|
139
|
+
val factory = SecretKeyFactory.getInstance(ALGORITHM)
|
|
140
|
+
val hash = factory.generateSecret(spec).encoded
|
|
141
|
+
spec.clearPassword()
|
|
142
|
+
return MessageDigest.isEqual(hash, stored.hash)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## Key Derivation (HKDF)
|
|
150
|
+
|
|
151
|
+
```kotlin
|
|
152
|
+
// ✅ Derive multiple keys from one master key
|
|
153
|
+
object KeyDerivation {
|
|
154
|
+
fun deriveKey(masterKey: ByteArray, info: String, outputLength: Int = 32): ByteArray {
|
|
155
|
+
// HKDF using HMAC-SHA256
|
|
156
|
+
val salt = ByteArray(32) // zero salt
|
|
157
|
+
val prk = Hashing.hmacSha256(salt, masterKey)
|
|
158
|
+
val infoBytes = info.toByteArray(Charsets.UTF_8)
|
|
159
|
+
val okm = Hashing.hmacSha256(prk, infoBytes + byteArrayOf(1))
|
|
160
|
+
return okm.copyOfRange(0, outputLength)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Usage: derive separate keys for encryption and authentication from one master
|
|
165
|
+
val encryptionKey = KeyDerivation.deriveKey(masterKey, "encryption")
|
|
166
|
+
val authKey = KeyDerivation.deriveKey(masterKey, "authentication")
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## Secure Random
|
|
172
|
+
|
|
173
|
+
```kotlin
|
|
174
|
+
// ✅ Always use SecureRandom for cryptographic purposes
|
|
175
|
+
object SecureRandomUtils {
|
|
176
|
+
fun randomBytes(size: Int): ByteArray =
|
|
177
|
+
ByteArray(size).also { SecureRandom().nextBytes(it) }
|
|
178
|
+
|
|
179
|
+
fun randomHex(size: Int): String =
|
|
180
|
+
randomBytes(size).joinToString("") { "%02x".format(it) }
|
|
181
|
+
|
|
182
|
+
fun randomBase64(size: Int): String =
|
|
183
|
+
Base64.encodeToString(randomBytes(size), Base64.NO_WRAP)
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## Tink (Recommended for Production)
|
|
190
|
+
|
|
191
|
+
```kotlin
|
|
192
|
+
// ✅ Google Tink — high-level crypto library, fewer footguns
|
|
193
|
+
// dependency: com.google.crypto.tink:tink-android
|
|
194
|
+
|
|
195
|
+
// Initialize once at app startup
|
|
196
|
+
TinkConfig.register()
|
|
197
|
+
|
|
198
|
+
// ✅ Authenticated encryption
|
|
199
|
+
val keysetHandle = KeysetHandle.generateNew(AeadKeyTemplates.AES256_GCM)
|
|
200
|
+
val aead = keysetHandle.getPrimitive(Aead::class.java)
|
|
201
|
+
val ciphertext = aead.encrypt(plaintext, associatedData)
|
|
202
|
+
val decrypted = aead.decrypt(ciphertext, associatedData)
|
|
203
|
+
|
|
204
|
+
// ✅ Store keysets securely with Keystore integration
|
|
205
|
+
val keysetHandle = AndroidKeysetManager.Builder()
|
|
206
|
+
.withSharedPref(context, "tink_keyset", "tink_prefs")
|
|
207
|
+
.withKeyTemplate(AeadKeyTemplates.AES256_GCM)
|
|
208
|
+
.withMasterKeyUri("android-keystore://tink_master_key")
|
|
209
|
+
.build()
|
|
210
|
+
.keysetHandle
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## Algorithm Selection Guide
|
|
216
|
+
|
|
217
|
+
| Need | Algorithm | Avoid |
|
|
218
|
+
|---|---|---|
|
|
219
|
+
| Symmetric encryption | AES-256-GCM | AES-ECB, AES-CBC without MAC |
|
|
220
|
+
| Password hashing | PBKDF2 (100k+ iterations) | MD5, SHA-1, unsalted hash |
|
|
221
|
+
| General hashing | SHA-256 / SHA-3 | MD5, SHA-1 |
|
|
222
|
+
| Message authentication | HMAC-SHA256 | Custom MAC schemes |
|
|
223
|
+
| Asymmetric encryption | RSA-OAEP-SHA256 | RSA-PKCS1v1.5 |
|
|
224
|
+
| Digital signature | ECDSA P-256, Ed25519 | RSA with SHA-1 |
|
|
225
|
+
| Key agreement | ECDH P-256 | DH without authentication |
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## Anti-Patterns
|
|
230
|
+
|
|
231
|
+
- `Random()` instead of `SecureRandom()` — predictable, not suitable for crypto
|
|
232
|
+
- AES-ECB mode — same plaintext block → same ciphertext; patterns visible
|
|
233
|
+
- Reusing IV with the same key in GCM — catastrophic key recovery attack
|
|
234
|
+
- MD5 or SHA-1 for security purposes — cryptographically broken
|
|
235
|
+
- Storing keys as hex strings in SharedPreferences — use Android Keystore
|
|
236
|
+
- Rolling custom encryption schemes — use standard JCA or Tink
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
## Related Skills
|
|
241
|
+
- `keystore` — hardware-backed key storage for Android
|
|
242
|
+
- `encrypted-storage` — applying crypto to local storage
|
|
243
|
+
- `biometric` — biometric-gated crypto operations
|
|
244
|
+
- `secure-networking` — TLS and certificate pinning
|