# Android Security -- Reference Patterns

Full code for the patterns summarized in `SKILL.md`. Load the section you need
when implementing that specific security feature.

## Contents

- [Secure Data Storage](#secure-data-storage)
- [Encrypted DataStore](#encrypted-datastore)
- [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)

## Secure Data Storage

### EncryptedSharedPreferences (legacy)

The AndroidX Security `EncryptedSharedPreferences` API is deprecated. Keep the
snippet below only for reading an existing store during migration; write new
key-value secrets through [Encrypted DataStore](#encrypted-datastore). It uses
AES-256 encryption under the hood.

```kotlin
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey

class SecureStorage @Inject constructor(
    @ApplicationContext private val context: Context,
) {
    private val masterKey = MasterKey.Builder(context)
        .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
        .build()

    private val prefs = EncryptedSharedPreferences.create(
        context,
        "secure_prefs",
        masterKey,
        EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
        EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM,
    )

    fun saveToken(token: String) {
        prefs.edit().putString("auth_token", token).apply()
    }

    fun getToken(): String? = prefs.getString("auth_token", null)

    fun clearToken() {
        prefs.edit().remove("auth_token").apply()
    }

    fun clearAll() {
        prefs.edit().clear().apply()
    }
}
```

### Android Keystore

For cryptographic key management. Keys never leave the hardware.

```kotlin
import java.security.KeyStore
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
import javax.crypto.spec.GCMParameterSpec
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties

class KeystoreManager {

    companion object {
        private const val KEYSTORE_PROVIDER = "AndroidKeyStore"
        private const val KEY_ALIAS = "app_encryption_key"
        private const val TRANSFORMATION = "AES/GCM/NoPadding"
    }

    private fun getOrCreateKey(): SecretKey {
        val keyStore = KeyStore.getInstance(KEYSTORE_PROVIDER).apply { load(null) }

        keyStore.getEntry(KEY_ALIAS, null)?.let { entry ->
            return (entry as KeyStore.SecretKeyEntry).secretKey
        }

        val keyGenerator = KeyGenerator.getInstance(
            KeyProperties.KEY_ALGORITHM_AES,
            KEYSTORE_PROVIDER,
        )

        keyGenerator.init(
            KeyGenParameterSpec.Builder(
                KEY_ALIAS,
                KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT,
            )
            .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
            .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
            .setKeySize(256)
            .setUserAuthenticationRequired(false)
            .build()
        )

        return keyGenerator.generateKey()
    }

    fun encrypt(data: ByteArray): Pair<ByteArray, ByteArray> {
        val cipher = Cipher.getInstance(TRANSFORMATION)
        cipher.init(Cipher.ENCRYPT_MODE, getOrCreateKey())
        val encrypted = cipher.doFinal(data)
        return Pair(cipher.iv, encrypted)
    }

    fun decrypt(iv: ByteArray, encryptedData: ByteArray): ByteArray {
        val cipher = Cipher.getInstance(TRANSFORMATION)
        val spec = GCMParameterSpec(128, iv)
        cipher.init(Cipher.DECRYPT_MODE, getOrCreateKey(), spec)
        return cipher.doFinal(encryptedData)
    }
}
```

## Encrypted DataStore

`EncryptedSharedPreferences` is deprecated. For typed key-value secrets, encrypt
at the DataStore serializer layer: the delegate serializer produces plaintext
bytes, a Keystore-held AES/GCM key encrypts them, and only ciphertext (IV
prepended) reaches disk. The DataStore wiring itself (the `dataStore` delegate,
`Flow` reads, `updateData` writes) is not repeated here; see the
`android-datastore` skill for it. This serializer plugs into that setup.

```kotlin
class KeystoreCrypto(private val keystore: KeystoreManager) {
    fun seal(plaintext: ByteArray): ByteArray {
        val (iv, ciphertext) = keystore.encrypt(plaintext)
        return ByteArray(1 + iv.size + ciphertext.size).also {
            it[0] = iv.size.toByte()
            iv.copyInto(it, destinationOffset = 1)
            ciphertext.copyInto(it, destinationOffset = 1 + iv.size)
        }
    }

    fun open(sealed: ByteArray): ByteArray {
        val ivSize = sealed[0].toInt()
        val iv = sealed.copyOfRange(1, 1 + ivSize)
        val ciphertext = sealed.copyOfRange(1 + ivSize, sealed.size)
        return keystore.decrypt(iv, ciphertext)
    }
}

class EncryptedSerializer<T>(
    private val delegate: Serializer<T>,
    private val crypto: KeystoreCrypto,
) : Serializer<T> {

    override val defaultValue: T = delegate.defaultValue

    override suspend fun readFrom(input: InputStream): T {
        val sealed = input.readBytes()
        if (sealed.isEmpty()) return defaultValue
        val plaintext = crypto.open(sealed)
        return delegate.readFrom(ByteArrayInputStream(plaintext))
    }

    override suspend fun writeTo(t: T, output: OutputStream) {
        val buffer = ByteArrayOutputStream()
        delegate.writeTo(t, buffer)
        output.write(crypto.seal(buffer.toByteArray()))
    }
}
```

## Database Key Hierarchy

Envelope encryption for a database passphrase (for example an encrypted SQLCipher
database). A random 32-byte data key encrypts the database; a non-exportable
Keystore AES/GCM key wraps that data key. Only the wrapped key plus its IV are
persisted. The plaintext data key is decrypted just in time and its bytes are
zeroed after the database opens. The SQLCipher `SupportFactory` wiring that
consumes the passphrase lives in the `room-database` skill.

```kotlin
interface WrappedKeyStore {
    fun read(): Pair<ByteArray, ByteArray>?
    fun write(wrapped: ByteArray, iv: ByteArray)
}

class DatabaseKeyProvider(private val store: WrappedKeyStore) {

    private companion object {
        const val WRAPPING_KEY_ALIAS = "db_key_wrapping_key"
        const val TRANSFORMATION = "AES/GCM/NoPadding"
        const val DATA_KEY_SIZE_BYTES = 32
        const val GCM_TAG_BITS = 128
    }

    fun loadOrCreateDataKey(): ByteArray {
        store.read()?.let { (wrapped, iv) -> return unwrap(wrapped, iv) }
        val dataKey = ByteArray(DATA_KEY_SIZE_BYTES).also { SecureRandom().nextBytes(it) }
        val (wrapped, iv) = wrap(dataKey)
        store.write(wrapped, iv)
        return dataKey
    }

    private fun wrap(dataKey: ByteArray): Pair<ByteArray, ByteArray> {
        val cipher = Cipher.getInstance(TRANSFORMATION)
        cipher.init(Cipher.ENCRYPT_MODE, wrappingKey())
        return cipher.doFinal(dataKey) to cipher.iv
    }

    private fun unwrap(wrapped: ByteArray, iv: ByteArray): ByteArray {
        val cipher = Cipher.getInstance(TRANSFORMATION)
        cipher.init(Cipher.DECRYPT_MODE, wrappingKey(), GCMParameterSpec(GCM_TAG_BITS, iv))
        return cipher.doFinal(wrapped)
    }

    private fun wrappingKey(): SecretKey {
        val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
        (keyStore.getEntry(WRAPPING_KEY_ALIAS, null) as? KeyStore.SecretKeyEntry)
            ?.let { return it.secretKey }

        return KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore").apply {
            init(
                KeyGenParameterSpec.Builder(
                    WRAPPING_KEY_ALIAS,
                    KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT,
                )
                    .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
                    .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
                    .setKeySize(256)
                    .build()
            )
        }.generateKey()
    }
}
```

Decrypt just in time and zero the plaintext key bytes once the database owns them:

```kotlin
val dataKey = keyProvider.loadOrCreateDataKey()
try {
    openEncryptedDatabase(passphrase = dataKey) // room-database: SupportFactory wiring
} finally {
    dataKey.fill(0)
}
```

## Build-Type Security Gating

Certificate pinning and any traffic inspector are decided 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 and added last so they observe
the final, fully-decorated request. Bind the debug interceptor set from a module
in the `src/debug` source set and an empty set from `src/release`, so release
builds cannot even link the inspector.

```kotlin
// src/main -- assembly, no build-type branching in the class body
class OkHttpClientFactory @Inject constructor(
    private val authInterceptor: AuthInterceptor,
    private val certificatePinner: CertificatePinner,
    private val observers: Set<@JvmSuppressWildcards Interceptor>,
) {
    fun create(): OkHttpClient = OkHttpClient.Builder()
        .certificatePinner(certificatePinner)
        .addInterceptor(authInterceptor)
        .apply { observers.forEach(::addInterceptor) } // logging/inspection, added last
        .build()
}
```

```kotlin
// src/release -- production pins, no observers
@Module
@InstallIn(SingletonComponent::class)
object ReleaseNetworkModule {

    @Provides
    fun certificatePinner(): CertificatePinner = CertificatePinner.Builder()
        .add("api.example.com", "sha256/AAAA...=")
        .add("api.example.com", "sha256/BBBB...=")
        .build()

    @Provides
    @ElementsIntoSet
    fun observers(): Set<Interceptor> = emptySet()
}
```

```kotlin
// src/debug -- no pins, inspector + logging observers
@Module
@InstallIn(SingletonComponent::class)
object DebugNetworkModule {

    @Provides
    fun certificatePinner(): CertificatePinner = CertificatePinner.Builder().build()

    @Provides
    @IntoSet
    fun logging(): Interceptor = HttpLoggingInterceptor()
        .apply { level = HttpLoggingInterceptor.Level.BODY }
}
```

## Biometric Authentication

### BiometricPrompt Setup

```kotlin
class BiometricAuthenticator @Inject constructor() {

    fun authenticate(
        activity: FragmentActivity,
        title: String = "Authenticate",
        subtitle: String = "Verify your identity",
        onSuccess: () -> Unit,
        onError: (String) -> Unit,
    ) {
        val biometricManager = BiometricManager.from(activity)
        val canAuthenticate = biometricManager.canAuthenticate(
            BiometricManager.Authenticators.BIOMETRIC_STRONG or
            BiometricManager.Authenticators.DEVICE_CREDENTIAL
        )

        if (canAuthenticate != BiometricManager.BIOMETRIC_SUCCESS) {
            onError(mapBiometricError(canAuthenticate))
            return
        }

        val promptInfo = BiometricPrompt.PromptInfo.Builder()
            .setTitle(title)
            .setSubtitle(subtitle)
            .setAllowedAuthenticators(
                BiometricManager.Authenticators.BIOMETRIC_STRONG or
                BiometricManager.Authenticators.DEVICE_CREDENTIAL
            )
            .build()

        val callback = object : BiometricPrompt.AuthenticationCallback() {
            override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
                onSuccess()
            }

            override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
                onError(errString.toString())
            }

            override fun onAuthenticationFailed() {
                // Called on failed attempt but user can retry
            }
        }

        val prompt = BiometricPrompt(activity, callback)
        prompt.authenticate(promptInfo)
    }

    private fun mapBiometricError(code: Int): String = when (code) {
        BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> "No biometric hardware"
        BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> "Biometric hardware unavailable"
        BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> "No biometrics enrolled"
        else -> "Biometric authentication unavailable"
    }
}
```

### Biometric + Keystore (Crypto-Based Auth)

```kotlin
fun authenticateWithCrypto(
    activity: FragmentActivity,
    onSuccess: (Cipher) -> Unit,
) {
    val key = getOrCreateBiometricKey()
    val cipher = Cipher.getInstance("AES/GCM/NoPadding")
    cipher.init(Cipher.ENCRYPT_MODE, key)

    val cryptoObject = BiometricPrompt.CryptoObject(cipher)

    val prompt = BiometricPrompt(activity, object : BiometricPrompt.AuthenticationCallback() {
        override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
            result.cryptoObject?.cipher?.let(onSuccess)
        }
    })

    prompt.authenticate(promptInfo, cryptoObject)
}

private fun getOrCreateBiometricKey(): SecretKey {
    val keyGenerator = KeyGenerator.getInstance("AES", "AndroidKeyStore")
    keyGenerator.init(
        KeyGenParameterSpec.Builder("biometric_key",
            KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
            .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
            .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
            .setUserAuthenticationRequired(true)
            .setInvalidatedByBiometricEnrollment(true)
            .build()
    )
    return keyGenerator.generateKey()
}
```

## Certificate Pinning

### OkHttp CertificatePinner

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

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

### Generating Pin Hashes

```bash
# From a domain
openssl s_client -connect api.example.com:443 -servername api.example.com \
    | openssl x509 -pubkey -noout \
    | openssl pkey -pubin -outform der \
    | openssl dgst -sha256 -binary \
    | openssl enc -base64

# From a certificate file
openssl x509 -in cert.pem -pubkey -noout \
    | openssl pkey -pubin -outform der \
    | openssl dgst -sha256 -binary \
    | openssl enc -base64
```

### Network Security Config (XML-based)

```xml
<!-- res/xml/network_security_config.xml -->
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <domain-config cleartextTrafficPermitted="false">
        <domain includeSubdomains="true">api.example.com</domain>
        <pin-set expiration="2026-01-01">
            <pin digest="SHA-256">AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</pin>
            <pin digest="SHA-256">BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=</pin>
        </pin-set>
    </domain-config>

    <!-- Block cleartext for all domains -->
    <base-config cleartextTrafficPermitted="false" />
</network-security-config>
```

```xml
<!-- AndroidManifest.xml -->
<application android:networkSecurityConfig="@xml/network_security_config">
```

## API Key Protection

### local.properties (Gitignored)

```properties
# local.properties -- NEVER commit this file
MAPS_API_KEY=AIzaSyB...
BASE_URL=https://api.example.com/
```

### Build-Time Injection

```kotlin
// build.gradle.kts
import java.util.Properties

val localProperties = Properties().apply {
    val file = rootProject.file("local.properties")
    if (file.exists()) load(file.inputStream())
}

android {
    defaultConfig {
        buildConfigField("String", "MAPS_API_KEY",
            "\"${localProperties["MAPS_API_KEY"] ?: ""}\"")
        buildConfigField("String", "BASE_URL",
            "\"${localProperties["BASE_URL"] ?: "https://api.example.com/"}\"")
    }
}
```

### Usage in Code

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

### Manifest Placeholder

```kotlin
android {
    defaultConfig {
        manifestPlaceholders["MAPS_API_KEY"] = localProperties["MAPS_API_KEY"] ?: ""
    }
}
```

```xml
<meta-data
    android:name="com.google.android.geo.API_KEY"
    android:value="${MAPS_API_KEY}" />
```

## R8 Obfuscation

R8 generates `mapping.txt` for deobfuscating crash reports:

```kotlin
android {
    buildTypes {
        release {
            isMinifyEnabled = true
            isShrinkResources = true
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro",
            )
        }
    }
}
```

Upload `mapping.txt` to Google Play Console and Firebase Crashlytics for
readable stack traces.

## Play Integrity API

Verify that API requests come from a genuine app on a genuine device.

```kotlin
class IntegrityVerifier @Inject constructor(
    @ApplicationContext private val context: Context,
) {
    suspend fun getIntegrityToken(nonce: String): String? {
        return try {
            val integrityManager = IntegrityManagerFactory.create(context)

            val request = IntegrityTokenRequest.builder()
                .setNonce(nonce)
                .build()

            val response = integrityManager
                .requestIntegrityToken(request)
                .await()

            response.token()
        } catch (e: Exception) {
            null
        }
    }
}
```

Send the token to your server for verification. Never verify integrity tokens
on the client -- the server must decode and validate them via the Play Integrity
API.

## Root Detection

Root detection is defense-in-depth. Determined attackers can bypass it, but it
raises the bar.

```kotlin
object RootDetector {

    fun isDeviceRooted(): Boolean =
        checkRootBinaries() || checkSuExists() || checkRootApps()

    private fun checkRootBinaries(): Boolean {
        val paths = listOf(
            "/system/bin/su",
            "/system/xbin/su",
            "/sbin/su",
            "/system/app/Superuser.apk",
            "/system/app/SuperSU.apk",
        )
        return paths.any { File(it).exists() }
    }

    private fun checkSuExists(): Boolean = try {
        Runtime.getRuntime().exec("which su").inputStream.bufferedReader().readLine() != null
    } catch (e: Exception) {
        false
    }

    private fun checkRootApps(): Boolean {
        val rootPackages = listOf(
            "com.topjohnwu.magisk",
            "eu.chainfire.supersu",
            "com.koushikdutta.superuser",
        )
        // Check if any are installed (requires QUERY_ALL_PACKAGES or specific queries)
        return false // Simplified -- use PackageManager in production
    }
}
```

Use root detection as one signal among many. Combine with Play Integrity for
stronger guarantees.

## Secure Network Communication

### Enforce HTTPS

```xml
<!-- network_security_config.xml -->
<network-security-config>
    <base-config cleartextTrafficPermitted="false" />
</network-security-config>
```

### Debug-Only Cleartext (for local development)

```xml
<network-security-config>
    <base-config cleartextTrafficPermitted="false" />
    <domain-config cleartextTrafficPermitted="true">
        <domain includeSubdomains="false">10.0.2.2</domain>
        <domain includeSubdomains="false">localhost</domain>
    </domain-config>
</network-security-config>
```

### TLS Configuration

```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()
```

## Secure Coding Patterns

### Never Log Sensitive Data

```kotlin
// WRONG
Log.d("Auth", "Token: $token")
Log.d("Payment", "Card: $cardNumber")

// CORRECT
Log.d("Auth", "Token refresh successful")
Log.d("Payment", "Payment processed")
```

### Input Validation

```kotlin
fun validateDeepLink(uri: Uri): Boolean {
    val allowedHosts = setOf("example.com", "www.example.com")
    val allowedSchemes = setOf("https")

    return uri.scheme in allowedSchemes &&
           uri.host in allowedHosts &&
           !uri.path.orEmpty().contains("..")
}
```

### Secure WebView

```kotlin
webView.settings.apply {
    javaScriptEnabled = false       // Enable ONLY if required
    allowFileAccess = false
    allowContentAccess = false
    domStorageEnabled = false
}
```
