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
- Security Overview - Security principles and Zero Trust architecture
- Authentication - OAuth 2.0, JWT, token management strategies
- Data Protection - Encryption patterns, PII handling
- Input Validation - Validation concepts, injection prevention
- Security Testing - Testing security controls
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 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()
}
}
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
- Android Overview - Project setup and security configuration
- Android Data & Networking - Secure API communication and certificate pinning
- Android Architecture - Secure architecture patterns
Security Guidelines
- Security Overview - Cross-platform security principles
- Authentication - Authentication patterns and strategies
- Authorization - Authorization and access control
- Data Protection - Encryption strategies and key management
- Input Validation - Input validation patterns
- Security Testing - Security testing approaches
Mobile-Specific Guidelines
- Mobile Overview - Cross-platform mobile security
External Resources
- Android Security Best Practices
- EncryptedSharedPreferences
- Biometric Authentication
- Network Security Config
- SafetyNet API
Summary
Key Takeaways
- EncryptedSharedPreferences - Never plain SharedPreferences for secrets
- Biometric authentication - Fingerprint/face for secure, quick access
- Certificate pinning - Prevent MITM attacks with OkHttp CertificatePinner
- Android Keystore - Hardware-backed encryption keys
- ProGuard/R8 - Code obfuscation for release builds
- No sensitive logs - Never log tokens, PINs, account numbers
- Strong authenticators - BIOMETRIC_STRONG only
- Session management - Auto-logout after inactivity
- PCI-DSS compliance - No card data stored locally
- Network security config - Disable clear text traffic
Next Steps: Review Android Architecture for secure architectural patterns and Android Testing for security testing strategies.