In Android app development, data security is a top priority, especially when working with sensitive information. Protecting user data from unauthorized access and misuse is a responsibility we can’t overlook. In this guide, I’ll walk you through practical methods to secure data in Android apps, and show you how to implement these techniques effectively in your projects.
We’ll start with Local Session Timeouts to limit data exposure during inactivity, then move on to Disabling App Data Backup to keep sensitive data out of potentially insecure backups. Next, we’ll discuss Protecting Configuration Data to secure app settings, In-Memory Sensitive Data Holding to prevent unintentional leaks, and finally, Secure Input for PIN Entry to guard against interception. Each section is crafted to help you build more robust and secure Android apps. Let’s dive in!
Data Security
Data security is all about keeping user information safe, whether it’s stored (at rest) or moving from one place to another (in transit). This means using encryption to protect data, securely storing it, and handling sensitive information with extra care.
Local Session Timeout
A local session timeout is a security feature that helps keep user data safe by tracking inactivity. If a user hasn’t interacted with the app for a set amount of time, the app will automatically log them out. This feature is especially important in financial apps, where protecting sensitive information is a top priority.
In financial apps, leaving a session open can be a serious security risk. If someone else picks up the user’s phone, they could access the app and potentially perform unauthorized actions. By adding a session timeout, we:
- Reduce the risk of unauthorized access,
- Safeguard sensitive financial data, and
- Ensure compliance with security standards in the financial industry.
To implement a local session timeout in Kotlin, we can use a CountDownTimer that resets each time the user interacts with the app.
const val TIMEOUT_DURATION = 5 * 60 * 1000L // 5 minutes in milliseconds
class SessionManager(private val context: Context) {
private var timer: CountDownTimer? = null
// Start or restart the inactivity timer
fun startSessionTimeout() {
timer?.cancel() // cancel any existing timer
timer = object : CountDownTimer(TIMEOUT_DURATION, 1000L) {
override fun onTick(millisUntilFinished: Long) {
// Optionally, add logging or other feedback here
}
override fun onFinish() {
onSessionTimeout()
}
}.start()
}
// Reset the timer on user interaction
fun resetSessionTimeout() {
startSessionTimeout()
}
// Handle session timeout (e.g., log the user out)
private fun onSessionTimeout() {
// Example action: Redirect to login screen
context.startActivity(Intent(context, LoginActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
})
}
// Cancel the timer when the session ends
fun endSession() {
timer?.cancel()
}
}
class MainActivity : AppCompatActivity() {
private lateinit var sessionManager: SessionManager
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
sessionManager = SessionManager(this)
// Start the session timer when the activity is created
sessionManager.startSessionTimeout()
}
override fun onUserInteraction() {
super.onUserInteraction()
// Reset the session timeout on any user interaction
sessionManager.resetSessionTimeout()
}
override fun onDestroy() {
super.onDestroy()
// End the session when the activity is destroyed
sessionManager.endSession()
}
}
startSessionTimeout()
: Starts a countdown timer that will log the user out after the set duration.onUserInteraction()
: Resets the timer whenever the user interacts with the app to prevent unintended logouts.
Disabling App Data Backup
When building financial apps, security should always be a top priority. Sensitive information like banking credentials, personal details, and financial records must be protected at all costs. One often-overlooked security measure is disabling app data backups. While Android’s automatic cloud backup feature is convenient, it can expose sensitive data if not managed properly.
By default, Android automatically backs up an app’s data to Google Drive, including SharedPreferences, files, and other persistent data. This process is controlled by the android:allowBackup
attribute in the app’s AndroidManifest.xml
. By setting this attribute to false
, the app ensures its data is not backed up, which is essential for securing financial apps and other apps that handle sensitive information.
<application
android:name=".FinancialApp"
android:allowBackup="false"
android:fullBackupContent="false"
... >
<!-- other configurations -->
</application>
android:allowBackup=”false”: Prevents Android from backing up any data from this app.
android:fullBackupContent=”false”: Ensures that no full data backup occurs, even if the device supports full data backups.
While both allowBackup="false"
and fullBackupContent="false"
significantly reduce the chances of unauthorized backups and data exposure, they do not provide 100% protection, especially on rooted or compromised devices. That’s why, in financial apps, we check if the device is rooted and implement additional tampering checks to enhance security.
Configuration Data Protection
Sensitive configuration data, like API keys or access tokens, shouldn’t be hardcoded directly into the app. Instead, it’s safer to encrypt them or store them securely in the Android Keystore, which serves as a secure container for cryptographic keys. Hardcoding sensitive information exposes it to potential attackers, who can easily extract it from the app’s binary. In contrast, the Android Keystore provides tamper-resistant storage, ensuring that your sensitive data remains protected.
Encrypted SharedPreferences
SharedPreferences is commonly used to store small data values in Android, but the issue with standard SharedPreferences is that it saves data in plain text, which is vulnerable if the device is compromised. For sensitive data like API keys or user credentials, it’s best to use EncryptedSharedPreferences, which ensures your data is encrypted and stored securely. Let’s take a look at how to implement this.
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKeys
fun getSecureSharedPreferences(context: Context): SharedPreferences {
val masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)
return EncryptedSharedPreferences.create(
"secure_preferences", // Name of the preferences file
masterKeyAlias, // The master key for encryption
context,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
}
fun saveConfigData(context: Context, apiKey: String) {
val sharedPreferences = getSecureSharedPreferences(context)
with(sharedPreferences.edit()) {
putString("api_key", apiKey)
apply() // Save the data securely
}
}
fun getConfigData(context: Context): String? {
val sharedPreferences = getSecureSharedPreferences(context)
return sharedPreferences.getString("api_key", null) // Retrieve the secure data
}
Here,
MasterKeys.getOrCreate()
creates a master key using AES-256 encryption. This key is used to encrypt the data.EncryptedSharedPreferences.create()
initializes the EncryptedSharedPreferences instance with the specified encryption schemes for both the keys and values.putString()
securely saves sensitive data like API keys, whilegetString()
retrieves the encrypted value.
Encrypting API Keys and Tokens
Hardcoding API keys and tokens directly into your app’s code can create serious security vulnerabilities. If someone decompiles your app or gains unauthorized access, these sensitive credentials could be exposed. Instead, it’s safer to store them in an encrypted format and decrypt them only when needed during runtime.
Here’s how you can use AES encryption in Kotlin to securely handle your API keys and tokens.
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
import javax.crypto.spec.GCMParameterSpec
import android.util.Base64
// Encrypting a string with AES
fun encryptData(plainText: String, secretKey: SecretKey): String {
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
val iv = cipher.iv
val encryptedData = cipher.doFinal(plainText.toByteArray())
val ivAndEncryptedData = iv + encryptedData
return Base64.encodeToString(ivAndEncryptedData, Base64.DEFAULT)
}
// Decrypting the encrypted string
fun decryptData(encryptedText: String, secretKey: SecretKey): String {
val ivAndEncryptedData = Base64.decode(encryptedText, Base64.DEFAULT)
val iv = ivAndEncryptedData.sliceArray(0 until 12) // Extract the 12-byte IV
val encryptedData = ivAndEncryptedData.sliceArray(12 until ivAndEncryptedData.size)
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
val gcmParameterSpec = GCMParameterSpec(128, iv) // 128-bit authentication tag length
cipher.init(Cipher.DECRYPT_MODE, secretKey, gcmParameterSpec)
val decryptedData = cipher.doFinal(encryptedData)
return String(decryptedData)
}
// Generate Secret Key for AES
fun generateSecretKey(): SecretKey {
val keyGenerator = KeyGenerator.getInstance("AES")
keyGenerator.init(256) // AES 256-bit encryption
return keyGenerator.generateKey()
}
- AES/GCM/NoPadding: This mode provides strong encryption and also ensures no unnecessary padding is added, keeping the data size as small as possible.
- Initialization Vector (IV): The IV is crucial for ensuring that even if the same data is encrypted multiple times, the output will differ. It’s stored alongside the encrypted data and is required for decryption.
generateSecretKey()
: This method creates a 256-bit AES key, which can be used for both encryption and decryption. To further enhance security, you can store this key in the Android Keystore.
Android Keystore for Secure Key Management
Storing encryption keys directly in the app can leave them vulnerable to attacks. To avoid this, we can use the Android Keystore system, which securely stores keys either in hardware or a secure enclave, ensuring that only the app has access to them. This adds a significant layer of protection, especially for sensitive data.
Here’s how you can generate and securely manage keys using the Keystore:
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import java.security.KeyStore
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
// Generate and store a key in Android Keystore
fun createKey() {
val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")
val keyGenParameterSpec = KeyGenParameterSpec.Builder(
"SecureKeyAlias",
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
).setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.build()
keyGenerator.init(keyGenParameterSpec)
keyGenerator.generateKey()
}
// Retrieve the secret key from Keystore
fun getSecretKey(): SecretKey? {
val keyStore = KeyStore.getInstance("AndroidKeyStore")
keyStore.load(null)
return keyStore.getKey("SecureKeyAlias", null) as SecretKey?
}
- KeyGenParameterSpec.Builder: This part sets the encryption requirements, such as the encryption block mode and padding. In this case, we’re using AES with GCM mode, which is both secure and efficient.
createKey()
: This function creates a new AES encryption key and securely stores it in the Keystore with the aliasSecureKeyAlias
. The key is only accessible to the app, making it safe from potential leaks.getSecretKey()
: This function retrieves the stored key from the Keystore when needed for encryption or decryption. The key is never exposed in the code, adding an extra layer of security.
Secure In-Memory Sensitive Data Holding
When your app processes sensitive information like user session tokens, PINs, or account numbers, this data is temporarily stored in memory. If this information is kept in memory for too long, it becomes vulnerable to unauthorized access—especially in rooted or debug-enabled environments where attackers could potentially retrieve it from other applications. Financial apps are particularly at risk because they handle highly sensitive data, so securing session tokens, PINs, and account numbers in memory is essential for protecting user privacy and minimizing exposure to attacks.
Best Practices for Securing In-Memory Data in Android
To keep session tokens, PINs, account numbers, and other sensitive data safe in memory, consider these three core principles:
Minimal Data Exposure: Only keep sensitive data in memory for as long as absolutely necessary, and clear it promptly once it’s no longer needed.
fun performSensitiveOperation() {
val sensitiveData = fetchSensitiveData() // Example: fetching from secure storage
try {
// Use the sensitive data within a limited scope
processSensitiveData(sensitiveData)
} finally {
// Clear sensitive data once it's no longer needed
sensitiveData.clear()
}
}
Data Clearing: Ensure that sensitive data is swiftly and thoroughly cleared from memory when it’s no longer required. We can use ByteArray
and clear the data immediately after use.
class SensitiveDataHandler {
fun processSensitiveData(data: ByteArray) {
try {
// Process the sensitive data securely
} finally {
data.fill(0) // Clear data from memory immediately
}
}
}
Obfuscation: Make it difficult for attackers to make sense of session tokens, PINs, or account numbers if they gain access to memory.
Secure Input for PIN Entry
Imagine a user is logging into their banking app while grabbing coffee in a crowded cafe. They quickly type in their PIN, maybe not noticing someone glancing over their shoulder — or that a vulnerability in the app could put their data at risk. That’s exactly why secure PIN entry is so important, especially in financial apps where a PIN is more than just a few numbers; it’s a gateway to sensitive information.
To securely capture PINs, use Android’s secure input types, and avoid storing PINs in plain text. Always hash sensitive data and use Base64 encoding before encrypting and storing it.
import android.content.Context
import android.text.InputType
import android.widget.EditText
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKeys
import java.security.MessageDigest
import java.util.*
class SecurePinManager(context: Context) {
private val masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)
private val encryptedPrefs = EncryptedSharedPreferences.create(
"secure_prefs",
masterKeyAlias,
context,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
fun setupPinInputField(editText: EditText) {
editText.inputType = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_VARIATION_PASSWORD
}
fun savePin(pin: String) {
val hashedPin = hashPin(pin) // Hash the PIN before saving
encryptedPrefs.edit().putString("user_pin", hashedPin).apply()
}
fun verifyPin(inputPin: String): Boolean {
val storedHashedPin = encryptedPrefs.getString("user_pin", null)
val inputHashedPin = hashPin(inputPin) // Hash the input before comparison
return storedHashedPin == inputHashedPin
}
// Hashes the PIN using SHA-256
private fun hashPin(pin: String): String {
val digest = MessageDigest.getInstance("SHA-256")
val hashedBytes = digest.digest(pin.toByteArray())
return Base64.getEncoder().encodeToString(hashedBytes) // Encode the hashed bytes in Base64
}
}
Here,
- PIN Hashing: The PIN is now hashed using SHA-256 before saving and comparing. This adds a layer of security by ensuring the raw PIN is never stored.
- Base64 Encoding: The hashed PIN is encoded using Base64 to store it as a string in
EncryptedSharedPreferences
.
Conclusion
Securing sensitive data in Android requires a combination of best coding practices and taking advantage of built-in security features. Here’s a quick recap of the key points we covered:
- Local Session Timeout: Automatically log users out after a period of inactivity to reduce the risk of unauthorized access.
- Disabling App Data Backup: Prevent backups of sensitive data, ensuring that it doesn’t get exposed in case of a backup breach.
- Configuration Data Protection: Encrypt configuration data to keep it safe from unauthorized access.
- Secure In-Memory Data Holding: Only store sensitive data in memory when absolutely necessary, and make sure it’s cleared once it’s no longer needed.
- Secure Input for PIN Entry: Use secure input types and enable accessibility settings to protect PIN entries from being exposed.
By implementing these simple practices, you can create a more secure and trustworthy Android app. For apps handling sensitive data, these steps are crucial to ensuring that user information stays private and safe!