Skip to main content

Android Security Implementation

Android-specific security implementations using platform security APIs, hardware-backed encryption, and Kotlin/Jetpack security libraries.

Overview

This guide covers Android-specific security implementations. For security concepts and principles, see Security Overview. For general mobile security patterns, consult the cross-platform documentation.

Core Focus Areas:

  • EncryptedSharedPreferences and Android Keystore integration
  • BiometricPrompt API for fingerprint/face authentication
  • OkHttp certificate pinning implementation
  • Room database encryption with SQLCipher
  • ProGuard/R8 code obfuscation
Related Security Topics

Android Security Layers

Android security operates through multiple defensive layers. Hardware-backed encryption (via Keystore) protects stored credentials even on rooted devices by storing keys in the Trusted Execution Environment or StrongBox (hardware security module). Biometric authentication (BiometricPrompt API) verifies users through fingerprint/face scans processed in secure hardware, never exposing biometric data to your app. Certificate pinning validates server certificates against embedded hashes, preventing man-in-the-middle attacks even with compromised certificate authorities. These layers create defense in depth as detailed in Security Overview.


Encrypted SharedPreferences

EncryptedSharedPreferences Architecture

Secure Preferences Implementation

// data/local/SecurePreferences.kt
package com.bank.paymentapp.data.local

import android.content.Context
import android.content.SharedPreferences
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
import javax.inject.Singleton

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

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

// MARK: - Auth Token

fun saveAuthToken(token: String) {
sharedPreferences.edit().putString(KEY_AUTH_TOKEN, token).apply()
}

fun getAuthToken(): String? {
return sharedPreferences.getString(KEY_AUTH_TOKEN, null)
}

fun clearAuthToken() {
sharedPreferences.edit().remove(KEY_AUTH_TOKEN).apply()
}

// MARK: - Refresh Token

fun saveRefreshToken(token: String) {
sharedPreferences.edit().putString(KEY_REFRESH_TOKEN, token).apply()
}

fun getRefreshToken(): String? {
return sharedPreferences.getString(KEY_REFRESH_TOKEN, null)
}

// MARK: - User PIN (Hashed)

fun savePinHash(hash: String) {
sharedPreferences.edit().putString(KEY_PIN_HASH, hash).apply()
}

fun getPinHash(): String? {
return sharedPreferences.getString(KEY_PIN_HASH, null)
}

// MARK: - Clear All

fun clearAll() {
sharedPreferences.edit().clear().apply()
}

companion object {
private const val KEY_AUTH_TOKEN = "auth_token"
private const val KEY_REFRESH_TOKEN = "refresh_token"
private const val KEY_PIN_HASH = "pin_hash"
}
}
Never Plain SharedPreferences

NEVER use plain SharedPreferences for sensitive data (authentication tokens, API keys, user credentials). Plain SharedPreferences stores data in unencrypted XML files that can be extracted via ADB, root access, or device backups. EncryptedSharedPreferences provides AES256-GCM encryption backed by Android Keystore, where encryption keys are stored in hardware security modules (on supported devices) making them inaccessible even with root access. This protects against data extraction attacks, malware, and unauthorized access to stored credentials.


Biometric Authentication

Biometric Authentication Flow

Biometric Helper Implementation

// presentation/auth/BiometricHelper.kt
package com.bank.paymentapp.presentation.auth

import androidx.biometric.BiometricManager
import androidx.biometric.BiometricManager.Authenticators
import androidx.biometric.BiometricPrompt
import androidx.core.content.ContextCompat
import androidx.fragment.app.FragmentActivity
import javax.inject.Inject

class BiometricHelper @Inject constructor() {

fun canAuthenticate(activity: FragmentActivity): BiometricAvailability {
val biometricManager = BiometricManager.from(activity)

return when (biometricManager.canAuthenticate(Authenticators.BIOMETRIC_STRONG)) {
BiometricManager.BIOMETRIC_SUCCESS -> {
BiometricAvailability.Available
}
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> {
BiometricAvailability.NotAvailable("No biometric hardware")
}
BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> {
BiometricAvailability.TemporarilyUnavailable("Hardware unavailable")
}
BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> {
BiometricAvailability.NotEnrolled("No biometric enrolled")
}
else -> {
BiometricAvailability.NotAvailable("Unknown error")
}
}
}

fun authenticate(
activity: FragmentActivity,
title: String = "Authenticate",
subtitle: String = "Verify your identity",
onSuccess: () -> Unit,
onError: (String) -> Unit,
onCancelled: () -> Unit = {}
) {
val executor = ContextCompat.getMainExecutor(activity)

val biometricPrompt = BiometricPrompt(
activity,
executor,
object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(
result: BiometricPrompt.AuthenticationResult
) {
super.onAuthenticationSucceeded(result)
onSuccess()
}

override fun onAuthenticationError(
errorCode: Int,
errString: CharSequence
) {
super.onAuthenticationError(errorCode, errString)
if (errorCode == BiometricPrompt.ERROR_USER_CANCELED ||
errorCode == BiometricPrompt.ERROR_NEGATIVE_BUTTON
) {
onCancelled()
} else {
onError(errString.toString())
}
}

override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
// Don't call onError here - user can retry
}
}
)

val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle(title)
.setSubtitle(subtitle)
.setNegativeButtonText("Use PIN")
.setAllowedAuthenticators(Authenticators.BIOMETRIC_STRONG)
.build()

biometricPrompt.authenticate(promptInfo)
}
}

sealed class BiometricAvailability {
data object Available : BiometricAvailability()
data class NotAvailable(val reason: String) : BiometricAvailability()
data class TemporarilyUnavailable(val reason: String) : BiometricAvailability()
data class NotEnrolled(val reason: String) : BiometricAvailability()
}

Biometric Authentication Usage

// presentation/auth/LoginScreen.kt
@Composable
fun LoginScreen(
viewModel: LoginViewModel = hiltViewModel(),
onLoginSuccess: () -> Unit
) {
val context = LocalContext.current
val activity = context as? FragmentActivity

LaunchedEffect(Unit) {
activity?.let {
viewModel.authenticateWithBiometric(it, onLoginSuccess)
}
}

// UI implementation...
}

// ViewModel
@HiltViewModel
class LoginViewModel @Inject constructor(
private val biometricHelper: BiometricHelper,
private val securePreferences: SecurePreferences
) : ViewModel() {

fun authenticateWithBiometric(
activity: FragmentActivity,
onSuccess: () -> Unit
) {
when (biometricHelper.canAuthenticate(activity)) {
is BiometricAvailability.Available -> {
biometricHelper.authenticate(
activity = activity,
title = "Login to BankApp",
subtitle = "Use fingerprint or face to login",
onSuccess = {
// Biometric success - retrieve auth token
val token = securePreferences.getAuthToken()
if (token != null) {
onSuccess()
}
},
onError = { error ->
// Show error message
},
onCancelled = {
// User chose PIN - show PIN screen
}
)
}
else -> {
// Biometric not available - show PIN screen
}
}
}
}

Certificate Pinning

Certificate Pinning Architecture

Certificate Pinning Implementation

// di/NetworkModule.kt (updated with pinning)
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {

@Provides
@Singleton
fun provideOkHttpClient(): OkHttpClient {
val certificatePinner = CertificatePinner.Builder()
.add(
"api.bank.com",
"sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" // Primary cert
)
.add(
"api.bank.com",
"sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=" // Backup cert
)
.build()

return OkHttpClient.Builder()
.certificatePinner(certificatePinner)
.addInterceptor(HttpLoggingInterceptor().apply {
level = if (BuildConfig.DEBUG) {
HttpLoggingInterceptor.Level.BODY
} else {
HttpLoggingInterceptor.Level.NONE
}
})
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.build()
}
}
Certificate Pinning in Production

Extract certificate hashes from your production API servers using:

openssl s_client -connect api.bank.com:443 | openssl x509 -pubkey -noout | openssl rsa -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64

Pin both primary and backup certificates to prevent outages during certificate rotation.


ProGuard/R8 Obfuscation

ProGuard Rules for Banking Apps

# proguard-rules.pro

# Keep domain models (for reflection)
-keep class com.bank.paymentapp.domain.model.** { *; }

# Keep DTOs (for Gson)
-keep class com.bank.paymentapp.data.remote.dto.** { *; }

# Keep Room entities
-keep class com.bank.paymentapp.data.local.entities.** { *; }

# Retrofit
-keepattributes Signature, InnerClasses, EnclosingMethod
-keepattributes RuntimeVisibleAnnotations, RuntimeVisibleParameterAnnotations
-keepclassmembers,allowshrinking,allowobfuscation interface * {
@retrofit2.http.* <methods>;
}

# Remove logging in release
-assumenosideeffects class android.util.Log {
public static *** d(...);
public static *** v(...);
public static *** i(...);
}

# Keep only error logs
-assumenosideeffects class android.util.Log {
public static *** e(...) return false;
}

Common Mistakes

Don't: Store Secrets in Plain SharedPreferences

// BAD: Plain SharedPreferences (NOT encrypted!)
val prefs = getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
prefs.edit().putString("auth_token", token).apply()

// GOOD: Use EncryptedSharedPreferences
val securePrefs = SecurePreferences(context)
securePrefs.saveAuthToken(token)

Don't: Log Sensitive Data

// BAD: Logs visible in Logcat and crash reports
Log.d("Auth", "Token: $authToken")
Log.d("Payment", "Account: $accountNumber, Amount: $amount")

// GOOD: Never log sensitive data
Log.d("Auth", "Authentication successful")
Log.d("Payment", "Payment initiated")

Don't: Use Weak Biometric Authenticators

// BAD: Allows weak biometrics (BIOMETRIC_WEAK)
BiometricPrompt.PromptInfo.Builder()
.setAllowedAuthenticators(Authenticators.BIOMETRIC_WEAK)

// GOOD: Require strong biometrics
BiometricPrompt.PromptInfo.Builder()
.setAllowedAuthenticators(Authenticators.BIOMETRIC_STRONG)

Code Review Checklist

Security (Block PR)

  • No secrets in plain SharedPreferences - Use EncryptedSharedPreferences
  • No hardcoded API keys - Use BuildConfig or secure storage
  • Certificate pinning implemented - For production API
  • No sensitive data logged - Account numbers, tokens, PINs
  • ProGuard/R8 enabled - For release builds
  • Biometric auth has PIN fallback - Support alternative auth

Watch For (Request Changes)

  • Master Key scheme - Use AES256_GCM
  • Strong biometric authenticators - BIOMETRIC_STRONG only
  • Certificate pin rotation plan - Pin backup certificates
  • Network security config - Clear text traffic disabled
  • Root detection - Consider SafetyNet API
  • Screenshot prevention - FLAG_SECURE on sensitive screens

Best Practices

  • EncryptedSharedPreferences wrapper - Centralized SecurePreferences class
  • Biometric availability check - Before attempting authentication
  • Proper error handling - User-friendly security error messages
  • Session timeout - Auto-logout after inactivity
  • PCI-DSS compliance - No card data stored locally
  • TLS 1.3 - Minimum TLS version enforced

Further Reading

Android Framework Guidelines

Security Guidelines

Mobile-Specific Guidelines

External Resources


Summary

Key Takeaways

  1. EncryptedSharedPreferences - Never plain SharedPreferences for secrets
  2. Biometric authentication - Fingerprint/face for secure, quick access
  3. Certificate pinning - Prevent MITM attacks with OkHttp CertificatePinner
  4. Android Keystore - Hardware-backed encryption keys
  5. ProGuard/R8 - Code obfuscation for release builds
  6. No sensitive logs - Never log tokens, PINs, account numbers
  7. Strong authenticators - BIOMETRIC_STRONG only
  8. Session management - Auto-logout after inactivity
  9. PCI-DSS compliance - No card data stored locally
  10. Network security config - Disable clear text traffic

Next Steps: Review Android Architecture for secure architectural patterns and Android Testing for security testing strategies.