Skip to content

Latest commit

 

History

History
183 lines (142 loc) · 5.34 KB

14-android-security.md

File metadata and controls

183 lines (142 loc) · 5.34 KB

Android Security

EncryptedSharedPreferences

Never store secrets in plain SharedPreferences — anyone with access to the device file can read them. Use EncryptedSharedPreferences to encrypt values at rest.

// build.gradle.kts
// implementation("androidx.security:security-crypto:1.1.0-alpha06")

import android.content.Context
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey

class SecureStore(context: Context) {

    private val masterKey = MasterKey.Builder(context)
        .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
        .build()

    private val prefs = EncryptedSharedPreferences.create(
        context,
        "secret_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)
}

Keys and values are encrypted on disk. The MasterKey is stored in the Android Keystore.

BiometricPrompt

Use BiometricPrompt to require a fingerprint or face before showing sensitive data.

// build.gradle.kts
// implementation("androidx.biometric:biometric:1.1.0")

import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt
import androidx.fragment.app.FragmentActivity

fun showBiometric(
    activity: FragmentActivity,
    onSuccess: () -> Unit,
    onError: (String) -> Unit
) {
    val canAuth = BiometricManager.from(activity)
        .canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG)

    if (canAuth != BiometricManager.BIOMETRIC_SUCCESS) {
        onError("Biometric not available")
        return
    }

    val executor = ContextCompat.getMainExecutor(activity)
    val prompt = BiometricPrompt(activity, executor,
        object : BiometricPrompt.AuthenticationCallback() {
            override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
                onSuccess()
            }
            override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
                onError(errString.toString())
            }
        }
    )

    prompt.authenticate(
        BiometricPrompt.PromptInfo.Builder()
            .setTitle("Unlock app")
            .setSubtitle("Use your fingerprint")
            .setNegativeButtonText("Cancel")
            .build()
    )
}
// Output (success):
// (system shows fingerprint dialog) → onSuccess() runs
// Output (cancel):
// onError("Cancel") runs

Always check canAuthenticate() first so you can show a fallback on devices without biometrics.

Network Security Config

Restrict cleartext (HTTP) traffic and trust only specific CAs. Add a config file:

<!-- res/xml/network_security_config.xml -->
<network-security-config>
    <base-config cleartextTrafficPermitted="false">
        <trust-anchors>
            <certificates src="system" />
        </trust-anchors>
    </base-config>
    <domain-config cleartextTrafficPermitted="true">
        <domain includeSubdomains="true">localhost</domain>
    </domain-config>
</network-security-config>

Reference it in the manifest:

<application
    android:networkSecurityConfig="@xml/network_security_config"
    ...>

This blocks all HTTP traffic except localhost, which is useful for local development.

Certificate Pinning

Pin a certificate so the app rejects attackers who use a fake CA. With Ktor, install the plugin:

import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.*

val client = HttpClient(CIO) {
    install(HttpTimeout) { requestTimeoutMillis = 10_000 }
    // For OkHttp engine you can use CertificatePinner; with CIO, validate hashes in a custom interceptor
}

For OkHttp-based clients, use CertificatePinter:

val pinner = CertificatePinner.Builder()
    .add("api.example.com", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
    .build()

If the server certificate does not match the pin, the request fails. Update pins carefully — wrong pins lock users out.

R8 and ProGuard Minification

R8 shrinks, obfuscates, and optimizes your release build. It removes unused code and renames classes, which makes reverse engineering harder.

// app/build.gradle.kts
android {
    buildTypes {
        release {
            isMinifyEnabled = true
            isShrinkResources = true
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
        }
    }
}

Keep rules for classes that use reflection. For Room, serialization, and Koin, add the recommended rules:

# proguard-rules.pro
-keep class kotlinx.serialization.** { *; }
-keep class com.example.myapp.data.** { *; }
-keepclassmembers class * {
    @kotlinx.serialization.Serializable *;
}

Always test the release build on a real device — minification bugs only show up there.

Practice Exercise

Create a SecureStore that saves an API token with EncryptedSharedPreferences. Add a biometric gate so the token is only read after a successful fingerprint check. Build a release APK with isMinifyEnabled = true and confirm the token still works after minification.