Data security is vital for financial apps. Sensitive information — whether it’s login details or payment data — needs to be protected at all times, not just when it’s stored, but also while it’s in memory. Although encryption for stored data is widely practiced, securing data in memory is sometimes underestimated. Yet, keeping data safe while it’s actively being used is equally important. In this blog, we’ll walk through best practices for handling in-memory data securely on Android. With Kotlin code, I’ll show you practical ways to protect sensitive information while it’s in use.
Why Secure In-Memory Sensitive Data Holding Matters
When your app handles sensitive information—such as user session tokens, PINs, or account numbers—it temporarily stores this data in memory during processing. If this data remains in memory longer than necessary, it becomes vulnerable to unauthorized access, particularly in environments that are rooted or debug-enabled. Attackers in these environments can potentially access memory and retrieve sensitive information from other applications. Financial apps are particularly at risk due to the highly sensitive nature of the data they handle. However, any app that stores sensitive information, such as health apps or apps handling personally identifiable information (PII), should prioritize securing in-memory data to protect user privacy and minimize the risk of exposure to attacks.
Best Practices for Securing In-Memory Data in Android
To safeguard session tokens, PINs, account numbers, and other sensitive data in memory, consider the following best practices:
- Minimal Data Exposure
Keep sensitive data in memory only for as long as necessary. Ensure that it is cleared promptly once it is no longer required. This minimizes the window of opportunity for attackers to access it. - Encryption
Always encrypt sensitive data before storing it in memory. This ensures that even if attackers manage to access the memory, they won’t be able to interpret the data without the encryption key. Using Android’sKeystore
system is highly recommended for secure encryption key management. - Obfuscation
Make it more difficult for attackers to make sense of session tokens, PINs, or account numbers in memory. While obfuscation alone is not a secure measure, it adds an additional layer of protection by making the data harder to interpret if accessed. - Data Clearing
Securely clear sensitive data from memory as soon as it is no longer needed. Ensure that memory is overwritten securely to prevent recovery using memory analysis tools. Consider using techniques likememcpy
or similar secure memory handling functions to wipe sensitive data completely from memory. - Secure Memory Handling
Consider using Android’s secure storage mechanisms likeEncryptedSharedPreferences
orAndroid Keystore
to manage sensitive information securely. These tools provide a higher level of protection for storing encryption keys or sensitive data that needs to be accessed in memory. - Anti-Debugging and Root Detection
Implement anti-debugging and root detection techniques to prevent attackers from using debugging tools to extract data from the app. While these techniques are not foolproof, they add an additional layer of defense. - Secure Coding Practices
Avoid accidentally logging or exposing sensitive data in crash reports, logs, or other outputs. Ensure that sensitive information is handled securely throughout the app’s lifecycle.
Implementing Secure In-Memory Data Handling in Kotlin
Minimize Data Exposure
To protect sensitive data, only keep it in memory for as long as absolutely necessary. Avoid storing it in global or static variables where it might remain accessible longer than needed. Here’s how we can handle this securely in Kotlin
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()
}
}
Here,
sensitiveData
is fetched and used only within the scope of the function.- Once the operation is complete, the data is cleared, minimizing its exposure time and keeping it safe.
By ensuring sensitive data only lives in memory for the shortest time possible, you reduce the risk of unauthorized access.
Use ‘CharArray’ or ‘ByteArray’ Instead of a String
In both Java and Kotlin, String
is immutable, meaning once it’s created, it cannot be modified. While this makes strings reliable for certain use cases, it can also be a security risk when dealing with sensitive data because the string cannot be cleared from memory once it’s no longer needed. On the other hand, CharArray
is mutable, so we can modify its contents, making it a safer choice for sensitive information like passwords.
Here’s a quick comparison
// Avoid
val password = "SensitivePassword123"
// Better
val password = charArrayOf('S', 'e', 'n', 's', 'i', 't', 'i', 'v', 'e', 'P', 'a', 's', 's', 'w', 'o', 'r', 'd', '1', '2', '3')
Clearing CharArray
After Use
Once you’re done using sensitive data stored in a CharArray
, it’s important to clear it immediately to prevent it from lingering in memory. You can do this by setting all characters to \u0000
(the null character)
fun clearPassword(password: CharArray) {
for (i in password.indices) {
password[i] = '\u0000'
}
}
Calling clearPassword(password)
right after you’re finished using the password ensures that no sensitive data is left exposed in memory longer than necessary, reducing the risk of unauthorized access.
The same applies to ByteArray. Since ByteArray is mutable, we can modify or clear its contents after use, providing a more secure way to handle sensitive data in memory.
class SensitiveDataHandler {
private var sensitiveData: ByteArray? = null
// Store sensitive data securely as a ByteArray
fun storeData(data: String) {
sensitiveData = data.toByteArray() // Convert String data to ByteArray
}
// Clear the sensitive data by overwriting it with zeros
fun clearData() {
sensitiveData?.fill(0) // Overwrite with zeros
sensitiveData = null // Set the reference to null
}
}
This approach helps ensure that sensitive information is overwritten and doesn’t remain in memory, mitigating security risks if memory is compromised.
Encrypting Sensitive Data in Memory
Even with minimal exposure and proper data clearing, it’s a good practice to add an extra layer of security by encrypting sensitive data while it’s in memory. This helps protect the data in case of an attack or a breach. In Kotlin, you can use the Cipher
class to easily encrypt and decrypt data on the fly.
Here’s how you can implement an encryption utility using AES-GCM for secure encryption and decryption
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
import javax.crypto.spec.GCMParameterSpec
object MemoryEncryptionUtil {
private const val TRANSFORMATION = "AES/GCM/NoPadding"
private const val TAG_LENGTH_BIT = 128
private val secretKey: SecretKey
init {
val keyGen = KeyGenerator.getInstance("AES")
keyGen.init(256) // 256-bit AES encryption
secretKey = keyGen.generateKey() // Generate a secret key
}
// Encrypt the data and return the initialization vector (IV) and encrypted data
fun encrypt(data: ByteArray): Pair<ByteArray, ByteArray> {
val cipher = Cipher.getInstance(TRANSFORMATION)
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
val iv = cipher.iv // Get the initialization vector
val encryptedData = cipher.doFinal(data) // Encrypt the data
return Pair(iv, encryptedData)
}
// Decrypt the data using the IV and encrypted data
fun decrypt(iv: ByteArray, data: ByteArray): ByteArray {
val cipher = Cipher.getInstance(TRANSFORMATION)
val spec = GCMParameterSpec(TAG_LENGTH_BIT, iv) // Specify the GCM parameter
cipher.init(Cipher.DECRYPT_MODE, secretKey, spec) // Initialize the cipher for decryption
return cipher.doFinal(data) // Decrypt and return the data
}
}
- AES-GCM (Galois/Counter Mode): This encryption mode offers both confidentiality and integrity, ensuring that the encrypted data cannot be tampered with without detection.
- encrypt(): This method encrypts the given
ByteArray
and returns a pair consisting of the initialization vector (IV) and the encrypted data. - decrypt(): This method takes the IV and encrypted data, decrypts it, and returns the original data in its unencrypted form.
Now, let’s see how to use this encryption utility with sensitive data
val sensitiveData = "SensitiveData".toByteArray()
val (iv, encryptedData) = MemoryEncryptionUtil.encrypt(sensitiveData)
// Decrypt the data later when needed
val decryptedData = MemoryEncryptionUtil.decrypt(iv, encryptedData)
Here,
- Sensitive data is first converted to a
ByteArray
. - It is then encrypted using the
encrypt()
function, which returns both the IV and the encrypted data. - When you need to access the original data, you can use the
decrypt()
function with the IV and encrypted data to safely decrypt it.
By implementing encryption in-memory, you provide an additional layer of protection for sensitive data, ensuring that even if an attacker gains access to the memory, the data remains unreadable.
Using Secure Storage for Persistent Data
When it comes to storing sensitive data beyond the session, it’s essential to use secure storage mechanisms to ensure that the data remains protected even after the app is closed or the device is restarted. Android’s Keystore System is specifically designed for this purpose, providing a secure place to store cryptographic keys and sensitive information like passwords and tokens.
In below code, we’ll use the Keystore to securely generate a key and then encrypt/decrypt sensitive data stored in a persistent manner.
import android.security.keystore.KeyProperties
import android.security.keystore.KeyGenParameterSpec
import java.security.KeyStore
import javax.crypto.KeyGenerator
import javax.crypto.Cipher
import javax.crypto.SecretKey
import javax.crypto.spec.GCMParameterSpec
object SecureStorageUtil {
private const val KEY_ALIAS = "SecureKeyAlias" // Alias for the key
private const val TRANSFORMATION = "AES/GCM/NoPadding" // Encryption mode and padding
private const val TAG_LENGTH_BIT = 128 // Tag length for GCM
// Function to retrieve or generate the encryption key
private fun getKey(): SecretKey {
val keyStore = KeyStore.getInstance("AndroidKeyStore").apply { load(null) }
// Check if the key already exists
if (!keyStore.containsAlias(KEY_ALIAS)) {
val keyGen = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")
val spec = KeyGenParameterSpec.Builder(KEY_ALIAS,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM) // Set GCM block mode
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) // No padding
.build()
keyGen.init(spec)
keyGen.generateKey() // Generate the key and store it securely
}
// Return the secret key from the keystore
return (keyStore.getEntry(KEY_ALIAS, null) as KeyStore.SecretKeyEntry).secretKey
}
// Function to encrypt data using the Keystore key
fun encrypt(data: ByteArray): Pair<ByteArray, ByteArray> {
val cipher = Cipher.getInstance(TRANSFORMATION)
cipher.init(Cipher.ENCRYPT_MODE, getKey()) // Initialize cipher with encryption key
val iv = cipher.iv // Get the initialization vector (IV)
val encryptedData = cipher.doFinal(data) // Encrypt the data
return Pair(iv, encryptedData) // Return both IV and encrypted data
}
// Function to decrypt data using the Keystore key
fun decrypt(iv: ByteArray, encryptedData: ByteArray): ByteArray {
val cipher = Cipher.getInstance(TRANSFORMATION)
cipher.init(Cipher.DECRYPT_MODE, getKey(), GCMParameterSpec(TAG_LENGTH_BIT, iv)) // Initialize cipher for decryption
return cipher.doFinal(encryptedData) // Decrypt and return the data
}
}
- Key Generation: If the key doesn’t already exist in the Keystore, it is generated using
KeyGenerator
with theAES
algorithm and stored in theAndroidKeyStore
. This ensures the key never leaves the secure hardware and is not accessible by unauthorized parties. - AES-GCM Encryption: We use AES in Galois/Counter Mode (GCM), which provides both encryption and integrity protection. The
Cipher
class is used to perform encryption and decryption operations with the key stored in the Keystore.
Data Encryption & Decryption
- encrypt(): Encrypts the input data and returns both the initialization vector (IV) and encrypted data. The IV is crucial for decrypting the data later.
- decrypt(): Uses the stored key to decrypt the data by passing the IV and encrypted data.
Here’s how we can use SecureStorageUtil to securely store and retrieve encrypted data, again, similar to the encryption utility mentioned above.
val sensitiveData = "SensitiveData".toByteArray()
// Encrypt the data
val (iv, encryptedData) = SecureStorageUtil.encrypt(sensitiveData)
// Decrypt the data later when needed
val decryptedData = SecureStorageUtil.decrypt(iv, encryptedData)
But, why do we use Keystore?
The Android Keystore System ensures that the cryptographic keys are stored securely in hardware-backed storage (if available), making them less vulnerable to attacks such as device rooting or debugging. By using Keystore for key management, you reduce the risk of exposing sensitive data, even if the app is compromised.
This approach helps ensure that sensitive data is encrypted both at rest (when stored) and in transit (while being processed), providing an additional layer of security for your financial app’s sensitive information.
Conclusion
By implementing these practices, you can significantly mitigate the risk of sensitive data being exposed in memory:
- Minimize data exposure by limiting its time in memory.
- Use mutable types like
CharArray
orByteArray
and clear them after use. - Encrypt sensitive data while in memory.
- Store persistent data securely using Android’s Keystore system.
These measures ensure that even if an attacker gains access to memory or persistent storage, they won’t be able to easily retrieve sensitive information.