Secure In-Memory Sensitive Data Holding in Financial Android Apps

Table of Contents

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:

  1. 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.
  2. 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’s Keystore system is highly recommended for secure encryption key management.
  3. 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.
  4. 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 like memcpy or similar secure memory handling functions to wipe sensitive data completely from memory.
  5. Secure Memory Handling
    Consider using Android’s secure storage mechanisms like EncryptedSharedPreferences or Android 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.
  6. 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.
  7. 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

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

Kotlin
// 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)

Kotlin
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.

Kotlin
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

Kotlin
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

Kotlin
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.

Kotlin
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 the AES algorithm and stored in the AndroidKeyStore. 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.

Kotlin
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 or ByteArray 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.

Skill Up: Software & AI Updates!

Receive our latest insights and updates directly to your inbox

Related Posts

error: Content is protected !!