Handling Application Security
1. Encrypting Data with KeyStore
The Android Keystore system provides a secure way to store cryptographic keys and sensitive data. It ensures that keys are never exposed in plaintext, even if the application is compromised.
| Property | Description | Notes |
|---|---|---|
| KeyStore | Android's secure storage system for cryptographic keys | Keys are stored in a secure hardware-backed environment when available |
| KeyGenerator | Class used to generate keys for encryption | Must be initialized with a KeyStore instance and key alias |
| Cipher | Class used to perform encryption and decryption operations | Uses the keys stored in the Keystore for cryptographic operations |
Example: Generate a Key in Keystore
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import java.security.KeyStore
val keyStore = KeyStore.getInstance("AndroidKeyStore")
keyStore.load(null)
val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")
val keyGenParameterSpec = KeyGenParameterSpec.Builder(
"my_key",
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
).setBlockModes(KeyProperties.BLOCK_MODE_CBC)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
.build()
keyGenerator.init(keyGenParameterSpec)
keyGenerator.generateKey()
Example: Encrypt Data with Keystore Key
import javax.crypto.Cipher
import android.security.keystore.KeyProperties
val cipher = Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/" +
KeyProperties.BLOCK_MODE_CBC + "/" +
KeyProperties.ENCRYPTION_PADDING_PKCS7)
val keyStore = KeyStore.getInstance("AndroidKeyStore")
keyStore.load(null)
val secretKey = keyStore.getKey("my_key", null) as SecretKey
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
val encryptedData = cipher.doFinal("Sensitive data".toByteArray())
Example: Decrypt Data with Keystore Key
import javax.crypto.Cipher
import android.security.keystore.KeyProperties
val cipher = Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/" +
KeyProperties.BLOCK_MODE_CBC + "/" +
KeyProperties.ENCRYPTION_PADDING_PKCS7)
val keyStore = KeyStore.getInstance("AndroidKeyStore")
keyStore.load(null)
val secretKey = keyStore.getKey("my_key", null) as SecretKey
cipher.init(Cipher.DECRYPT_MODE, secretKey)
val decryptedData = cipher.doFinal(encryptedData)
2. Using BiometricPrompt
The BiometricPrompt API in Android provides a standardized way to handle biometric authentication (fingerprint, face, iris, etc.) in Kotlin applications. It offers a consistent user experience and integrates well with Android's security model.
| Property | Description | Values |
|---|---|---|
| BiometricPrompt.Builder | Used to configure and build a BiometricPrompt |
Instance of BiometricPrompt.Builder |
| setNegativeButton | Configures the negative button (e.g., "Cancel") | CharSequence, Executor, DialogInterface.OnClickListener |
| setConfirmationRequired | Controls whether confirmation is required after biometric authentication | Boolean |
| setDeviceCredentialAllowed | Allows fallback to device credentials (e.g., PIN, pattern) | Boolean |
Example: Initializing BiometricPrompt
import android.security.biometrics.BiometricPrompt
import android.security.biometrics.BiometricManager
import androidx.fragment.app.FragmentActivity
class BiometricAuthFragment : FragmentActivity() {
private lateinit var biometricPrompt: BiometricPrompt
private lateinit var promptInfo: BiometricPrompt.PromptInfo
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
biometricPrompt = BiometricPrompt(this, object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
// Handle successful authentication
}
override fun onAuthenticationFailed() {
// Handle failed authentication
}
})
promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle("Authenticate")
.setSubtitle("Use fingerprint to proceed")
.setDescription("Touch sensor to authenticate")
.setConfirmationRequired(true)
.setDeviceCredentialAllowed(true)
.build()
}
fun authenticate() {
biometricPrompt.authenticate(promptInfo)
}
}
BiometricPrompt requires the USE_BIOMETRIC permission in the manifest.
| Feature | Support Status | Notes |
|---|---|---|
| Fingerprint Authentication | Supported | Available on Android 6.0+ |
| Face Recognition | Supported | Available on Android 10+ |
| Iris Scanning | Limited | Not widely supported on Android devices |
| Device Credential Fallback | Supported | Allows fallback to PIN or pattern |
3. Implementing Certificate Pinning
Certificate pinning is a security technique that ensures your app only accepts connections to specific certificates or public keys, mitigating man-in-the-middle (MITM) attacks.
Example: Using OkHttp with Certificate Pinning
import okhttp3.*
import java.security.cert.Certificate
import java.security.cert.CertificateFactory
import java.io.InputStream
class PinningInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val response = chain.proceed(request)
// Validate certificate against pinned values
if (!isCertificateValid(response)) {
throw IOException("Certificate pinning failed")
}
return response
}
private fun isCertificateValid(response: Response): Boolean {
val certificates = response.handshake?.peerCertificates ?: return false
// Compare with pinned certificates (simplified)
return true
}
}
// Usage in OkHttpClient
val client = OkHttpClient.Builder()
.addInterceptor(PinningInterceptor())
.build()
| Property | Description | Values |
|---|---|---|
| Certificate Pinning | Ensures only trusted certificates are accepted | Enabled / Disabled |
| Certificate Hash | SHA-256 hash of the pinned certificate | sha256/... |
| Certificate Chain | Full certificate chain to validate | List of certificates |
4. Validating Input Data
Example: Basic Input Validation
fun validateEmail(email: String): Boolean {
val emailRegex = Regex("^[A-Za-z0-9+_.-]+@([A-Za-z0-9.-]+\\.[A-Za-z]{2,})$")
return emailRegex.matches(email)
}
fun validatePhone(phone: String): Boolean {
val phoneRegex = Regex("^\\+?[1-9]\\d{1,14}$")
return phoneRegex.matches(phone)
}
| Validation Type | Method | Description |
|---|---|---|
| Email Validation | Regex("^[A-Za-z0-9+_.-]+@([A-Za-z0-9.-]+\\.[A-Za-z]{2,})$") |
Checks for valid email format |
| Phone Number Validation | Regex("^\\+?[1-9]\\d{1,14}$") |
Validates E.164 format phone numbers |
| Length Validation | input.length <= maxLen |
Ensures input does not exceed maximum length |
Example: Sanitizing User Input
fun sanitizeInput(input: String): String {
return input
.replace("<", "<")
.replace(">", ">")
.replace("&", "&")
.replace("\"", """)
}
5. Securing SharedPreferences
| Property | Description | Security Level | Notes |
|---|---|---|---|
| MODE_PRIVATE | Default mode; data is only accessible by the app | High | Recommended for most use cases |
| MODE_WORLD_READABLE | Allows other apps to read the file | Low | DEPRECATED in API 17+ |
| MODE_WORLD_WRITEABLE | Allows other apps to write to the file | Low | DEPRECATED in API 17+ |
Example: Secure SharedPreferences Usage
val prefs = getSharedPreferences("secure_prefs", Context.MODE_PRIVATE)
val editor = prefs.edit()
editor.putString("user_token", "abc123xyz")
editor.apply()
MODE_WORLD_READABLE or MODE_WORLD_WRITEABLE exposes data to other apps.| Encryption Method | Security Level | Implementation |
|---|---|---|
| AES-256 | High | Use AndroidKeyStore with Cipher |
| Base64 Encoding | Medium | Simple encoding, not encryption |
| No Encryption | Low | Data stored in plain text |
6. Using HTTPS Only
| Property | Description | Notes |
|---|---|---|
| networkSecurityConfig | Reference to a network security configuration XML file | Used to define security policies for the app |
| cleartextTrafficPermitted | Boolean flag to allow cleartext (HTTP) traffic | Set to false to enforce HTTPS only |
Example: Enforcing HTTPS Only
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="false">
<trust-anchors>
<certificates src="@raw/trusted_certs"/>
</trust-anchors>
</base-config>
</network-security-config>
Example: Applying Network Security Config
<application
android:name=".MyApplication"
android:networkSecurityConfig="@xml/network_security_config"
... >
</application>
cleartextTrafficPermitted to false blocks all HTTP traffic and enforces HTTPS.
HTTPS Enforcement Summary
- All network requests must use HTTPS
- HTTP traffic is blocked by default
- Network security config allows fine-grained control
7. Implementing App Signing
| Aspect | Description | Details |
|---|---|---|
| Key Store | Storage for signing keys | Contains private key and certificate chain to sign the app |
| Key Alias | Identifier for the key | Unique name within the keystore to reference the signing key |
| Key Password | Password for the key | Protects access to the private key in the keystore |
| Keystore Password | Password for the keystore | Protects access to the entire keystore file |
Example: Generate Keystore Using Keytool
keytool -genkey -v -keystore my-release-key.keystore -alias my-key-alias -keyalg RSA -keysize 2048 -validity 10000
Example: Configure Gradle for Release Build
android {
signingConfigs {
release {
storeFile file('my-release-key.keystore')
storePassword 'password'
keyAlias 'my-key-alias'
keyPassword 'password'
}
}
buildTypes {
release {
signingConfig signingConfigs.release
}
}
}
8. Protecting Against SQL Injection
SQL injection is a common vulnerability where malicious input is used to manipulate SQL queries. In Android-Kotlin, it's crucial to use parameterized queries and avoid string concatenation in SQL statements.
| Technique | Description | Example |
|---|---|---|
| Parameterized Queries | Use parameterized queries to prevent SQL injection by separating SQL logic from data. | |
| Room Database | Room ORM provides compile-time SQL validation and parameter binding to prevent injection. | |
| Raw SQL with Caution | When using raw SQL, always sanitize and validate input before constructing queries. | |
Example: Room Query with Parameters
@Dao
interface UserDao {
@Query("SELECT * FROM users WHERE email = :email")
fun findUserByEmail(email: String): User?
}
9. Using R8 for Obfuscation
R8 is the default code shrinker, optimizer, and obfuscator for Android apps. It helps reduce APK size and makes reverse engineering more difficult by renaming classes, methods, and fields to shorter, less meaningful names.
| Feature | Description | Notes |
|---|---|---|
| Proguard/R8 Rules | Configuration files that define which code should not be obfuscated or removed. | Use proguard-rules.pro or consumer-proguard-rules.pro |
| Shrinking | Removes unused code from the app. | Enabled by default in release builds |
| Optimization | Improves code execution performance. | Enabled by default in release builds |
| Obfuscation | Renames classes, methods, and fields to shorter names. | Enabled by default in release builds |
Example: Basic R8 Configuration
# Keep the main application class
-keep public class com.example.MyApplication {
public static void main(java.lang.String[]);
}
# Keep all public classes and methods in a specific package
-keep public class com.example.api.** {
public <methods>;
}
# Do not obfuscate specific classes
-keepnames class com.example.MyClass
# Keep annotations
-keepattributes *Annotation*
10. Securing API Keys
| Approach | Description | Implementation | Notes |
|---|---|---|---|
| Build-time Injection | Inject API keys during build process using Gradle properties or build variants. | |
Keys are embedded in the APK but not readable in source code. |
| String Resources | Store keys in res/values/strings.xml and access via getString(). |
<string name="api_key"><your_api_key></string> |
Not secure for production; can be extracted from APK. |
| Environment Variables | Use environment variables during development and CI/CD pipeline. | |
Secure in CI but not in app binary. |
| Secure Storage (Keystore) | Use Android Keystore to store sensitive keys securely. | |
Recommended for sensitive keys in production apps. |
Example: Using BuildConfig for API Key
class ApiClient {
companion object {
const val API_KEY = BuildConfig.API_KEY
}
}