---
name: android-security
description: "Secure Android apps with ProGuard/R8 obfuscation, OkHttp certificate pinning, BiometricPrompt authentication, EncryptedSharedPreferences, Play Integrity API, root detection, secure network communication, and API key protection via local.properties and BuildConfig. Use when implementing security features, auditing app security, handling sensitive data, or preparing for production release."
metadata:
  source: multi-agent-pipeline
---

# Android Security

Security patterns for Android apps covering data storage, network security,
authentication, integrity verification, and obfuscation. Targets 2024-2025
best practices with AndroidX Security, BiometricPrompt, and Play Integrity.

Full code for every pattern below lives in
[`references/patterns.md`](references/patterns.md), under headings that mirror
these sections. This file is the guide: read a section for the decision, then
load the matching reference section when writing the implementation.

## Contents

- [Secure Data Storage](#secure-data-storage)
- [Encrypted DataStore for Secrets](#encrypted-datastore-for-secrets)
- [Database Key Hierarchy](#database-key-hierarchy)
- [Build-Type Security Gating](#build-type-security-gating)
- [Biometric Authentication](#biometric-authentication)
- [Certificate Pinning](#certificate-pinning)
- [API Key Protection](#api-key-protection)
- [R8 Obfuscation](#r8-obfuscation)
- [Play Integrity API](#play-integrity-api)
- [Root Detection](#root-detection)
- [Secure Network Communication](#secure-network-communication)
- [Secure Coding Patterns](#secure-coding-patterns)
- [Do's and Don'ts](#dos-and-donts)
- [Troubleshooting](#troubleshooting)
- [Review Checklist](#review-checklist)

## Secure Data Storage

The Android Keystore holds cryptographic keys (hardware-backed, keys never leave
the secure hardware). `EncryptedSharedPreferences` is deprecated: keep it only to
read an existing store during migration, and route new key-value secrets through
[Encrypted DataStore for Secrets](#encrypted-datastore-for-secrets).

See `references/patterns.md` -> "Secure Data Storage" for the legacy
`EncryptedSharedPreferences` reader and the `KeystoreManager` (encrypt/decrypt
with a Keystore-held AES/GCM key) that the newer patterns build on.

### What Goes Where

| Data Type | Storage | Why |
|-----------|---------|-----|
| Auth tokens | Encrypted DataStore (Keystore-backed serializer) | Typed, async, encrypted at the serializer layer |
| Passwords | Android Keystore + encryption | Hardware-backed security |
| API keys | BuildConfig (build-time injection) | Not in source; obfuscated by R8 |
| Database passphrase | Envelope-wrapped by a Keystore key | Data key never persisted in the clear |
| User preferences (non-sensitive) | Preferences DataStore | No encryption needed |
| Large sensitive files | Encrypted file + Keystore key | AES encryption with hardware key |

## Encrypted DataStore for Secrets

For typed key-value secrets, prefer a typed DataStore encrypted at the serializer
layer over the deprecated `EncryptedSharedPreferences`. The delegate serializer
produces plaintext bytes, a non-exportable Keystore AES/GCM key seals them (IV
prepended), and only ciphertext reaches disk. This skill owns the security
rationale and the key material; the DataStore mechanics (the `dataStore`
delegate, `Flow` reads, `updateData` writes) belong to the `android-datastore`
skill -- do not reproduce them here.

See `references/patterns.md` -> "Encrypted DataStore" for the `KeystoreCrypto`
seal/open helpers and the `EncryptedSerializer<T>` that wraps a plaintext
serializer.

## Database Key Hierarchy

Protect a database passphrase with envelope encryption rather than storing it
directly. Generate a random 32-byte data key, wrap it with a non-exportable
AndroidKeystore AES/GCM key, and persist only the wrapped key plus its IV.
Decrypt the data key just in time, hand it to the database, and zero the
plaintext key bytes in a `finally` block once the database owns them. The
SQLCipher `SupportFactory` wiring that consumes the passphrase lives in the
`room-database` skill.

See `references/patterns.md` -> "Database Key Hierarchy" for the
`DatabaseKeyProvider` (wrap/unwrap against the Keystore) and the
decrypt-then-zero consumer.

## Build-Type Security Gating

Decide certificate pinning and traffic inspection in one place, by build type.
Pinning is production-only -- a debug proxy would fail the pins -- and the
inspection and logging interceptors are debug-only, added last so they observe
the final decorated request. Bind the debug interceptor set from a module in the
`src/debug` source set and an empty set from `src/release`, so a release build
cannot link the inspector at all.

See `references/patterns.md` -> "Build-Type Security Gating" for the
`OkHttpClientFactory` assembly and the paired `src/release` / `src/debug` Hilt
modules.

## Biometric Authentication

Use `BiometricPrompt` with `BIOMETRIC_STRONG` for high-security flows. Always call
`canAuthenticate()` first and map the result before showing the prompt. For flows
that must prove a fresh authentication (unlocking a key), bind a `CryptoObject`
backed by a Keystore key created with `setUserAuthenticationRequired(true)` and
`setInvalidatedByBiometricEnrollment(true)`.

See `references/patterns.md` -> "Biometric Authentication" for the
`BiometricAuthenticator` setup (with error mapping) and the crypto-based
`authenticateWithCrypto` variant.

## Certificate Pinning

Pin with OkHttp `CertificatePinner` or a Network Security Config XML. Include at
least two pins (primary + backup) so certificate rotation does not brick clients.

```kotlin
val certificatePinner = CertificatePinner.Builder()
    .add("api.example.com", "sha256/AAAA...=") // Primary
    .add("api.example.com", "sha256/BBBB...=") // Backup
    .build()

val client = OkHttpClient.Builder()
    .certificatePinner(certificatePinner)
    .build()
```

See `references/patterns.md` -> "Certificate Pinning" for the `openssl` pin-hash
generation commands and the equivalent `network_security_config.xml` `pin-set`.

## API Key Protection

Keep secrets out of source. Put them in gitignored `local.properties`, inject
them at build time via `buildConfigField`, and read them from `BuildConfig` (R8
obfuscates them in release builds).

```kotlin
// Access via BuildConfig -- obfuscated by R8 in release builds
val apiKey = BuildConfig.MAPS_API_KEY
val baseUrl = BuildConfig.BASE_URL
```

See `references/patterns.md` -> "API Key Protection" for the `local.properties`
layout, the `build.gradle.kts` injection block, and the manifest-placeholder
route for keys consumed from `AndroidManifest.xml`.

## R8 Obfuscation

R8 is enabled automatically when `isMinifyEnabled = true`. It performs:
- Code shrinking (removes unused code)
- Resource shrinking (removes unused resources)
- Obfuscation (renames classes/methods)
- Optimization (inlining, dead code elimination)

R8 generates `mapping.txt` for deobfuscating crash reports. Upload it to Google
Play Console and Firebase Crashlytics for readable stack traces. Add `-keep`
rules for `@Serializable` and other reflectively-accessed classes.

See `references/patterns.md` -> "R8 Obfuscation" for the release `buildTypes`
block.

## Play Integrity API

Verify that API requests come from a genuine app on a genuine device. Request an
integrity token on the client, then send it to your server. Never verify
integrity tokens on the client -- the server must decode and validate them via
the Play Integrity API.

See `references/patterns.md` -> "Play Integrity API" for the `IntegrityVerifier`
token request.

## Root Detection

Root detection is defense-in-depth. Determined attackers can bypass it, but it
raises the bar. Use it as one signal among many and combine it with Play
Integrity for stronger guarantees.

See `references/patterns.md` -> "Root Detection" for the `RootDetector` object
(root binaries, `su` probe, known root packages).

## Secure Network Communication

Enforce HTTPS by setting `cleartextTrafficPermitted="false"` in the Network
Security Config, add a debug-only cleartext exception for local dev hosts
(`10.0.2.2`, `localhost`), and restrict TLS to 1.2/1.3.

```kotlin
val spec = ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
    .tlsVersions(TlsVersion.TLS_1_2, TlsVersion.TLS_1_3)
    .build()

val client = OkHttpClient.Builder()
    .connectionSpecs(listOf(spec))
    .build()
```

See `references/patterns.md` -> "Secure Network Communication" for the HTTPS-only
and debug-cleartext XML configs.

## Secure Coding Patterns

- Never log tokens, passwords, or card numbers -- log outcomes, not values.
- Validate deep-link URIs: allow-list scheme and host, reject `..` paths.
- Lock down WebView: disable JavaScript, file access, content access, and DOM
  storage unless a feature strictly requires them.

See `references/patterns.md` -> "Secure Coding Patterns" for the logging,
`validateDeepLink`, and secure-WebView snippets.

## Do's and Don'ts

### Do's
- Encrypt key-value secrets at the DataStore serializer layer with a Keystore key
- Use Android Keystore for cryptographic keys
- Envelope-wrap a database passphrase and zero the plaintext key after use
- Gate certificate pinning and inspectors by build type in one place
- Enable R8 with `isMinifyEnabled = true` for release builds
- Upload `mapping.txt` to Play Console and Crashlytics
- Use Network Security Config to enforce HTTPS
- Include at least two certificate pins (primary + backup)
- Verify Play Integrity tokens server-side only
- Use BiometricPrompt with BIOMETRIC_STRONG for high-security flows

### Don'ts
- Do not store secrets in plain SharedPreferences, or reach for the deprecated `EncryptedSharedPreferences` for new code
- Do not persist a database passphrase or data key in the clear
- Do not link a traffic inspector or body logging into release builds
- Do not hardcode API keys in Kotlin/Java source files
- Do not commit `local.properties` or keystore files to version control
- Do not log tokens, passwords, or card numbers
- Do not verify integrity tokens on the client
- Do not trust root detection as the sole security measure
- Do not enable JavaScript in WebView unless absolutely necessary
- Do not use `cleartextTrafficPermitted="true"` in production

## Troubleshooting

| Problem | Cause | Fix |
|---------|-------|-----|
| `KeyPermanentlyInvalidatedException` | Biometric enrollment changed | Delete and recreate the Keystore key |
| EncryptedSharedPreferences crash on upgrade | MasterKey corrupted | Migrate to encrypted DataStore; handle exception, recreate storage, re-authenticate |
| 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 |
| Debug proxy shows no traffic in release | Inspector correctly excluded from `src/release` | Expected; inspect on a debug build |
| Certificate pinning fails after cert rotation | Pin hash mismatch | Update pins; always maintain a backup pin |
| R8 strips serialization classes | Missing keep rules | Add `-keep` rules for `@Serializable` classes |
| Play Integrity token is null | Missing Play Core dependency or not on Play device | Check `com.google.android.play:integrity` dependency |
| Cleartext traffic blocked | Network Security Config enforcing HTTPS | Use HTTPS or add debug-only exception for local dev |
| BiometricPrompt not showing | Wrong authenticator type or no enrollment | Check `canAuthenticate()` before prompting |
| Keystore key inaccessible after OS update | Keystore compatibility issue | Wrap in try/catch; recreate key if needed |

## Review Checklist

- [ ] Key-value secrets in a Keystore-backed encrypted DataStore, not plain or deprecated SharedPreferences
- [ ] Database passphrase envelope-wrapped by a Keystore key; plaintext data key zeroed after use
- [ ] Certificate pinning and inspectors gated by build type; no inspector in release
- [ ] No hardcoded API keys or secrets in source files
- [ ] `local.properties` is in `.gitignore`
- [ ] R8 enabled with `isMinifyEnabled = true` for release
- [ ] `mapping.txt` uploaded to Play Console and Crashlytics
- [ ] Network Security Config enforces HTTPS (`cleartextTrafficPermitted="false"`)
- [ ] Certificate pinning with primary + backup pins
- [ ] BiometricPrompt uses `BIOMETRIC_STRONG` for sensitive operations
- [ ] No sensitive data in logs
- [ ] Play Integrity tokens verified server-side
- [ ] WebView has minimal permissions (JavaScript disabled by default)
- [ ] Input and deep link URLs validated before processing
