Data Security in Android Apps: Proven Practices & Kotlin Implementation

Table of Contents

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.

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

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

Kotlin
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, while getString() 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.

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

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

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()
    }
}

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.

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

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

Skill Up: Software & AI Updates!

Receive our latest insights and updates directly to your inbox

Related Posts

error: Content is protected !!