Step 14 of 15
EncryptedSharedPreferences, BiometricPrompt, network security config, R8
Android security — encryption, biometric auth, network security, ProGuard/R8
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.
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.
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.
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 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.
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.