Android SharedPreferences Tutorial and Example

Encrypting Data With AES in Android

Mobile apps routinely store tokens, profile details, preferences and offline records. If those values remain as plain text, anyone who gains access to an exported backup, rooted device or compromised storage location may read them. Encryption changes the stored representation into ciphertext that requires a secret key to recover.

The Advanced Encryption Standard (AES) is a practical choice for protecting information inside an Android application. The safest modern approach is authenticated encryption, which protects confidentiality and detects unwanted changes. Android’s Keystore can generate and protect the encryption key so that application code does not have to manage raw key material directly.

This matters for apps used across Australia, from banking tools in Sydney and Melbourne to field-service applications operating with intermittent regional connectivity. An app may need to cache data while a user is travelling between towns, commuting on public transport or working away from reliable Wi-Fi.

Good encryption does not replace sound application design. Passwords should be hashed rather than reversibly encrypted, TLS should protect network traffic, and sensitive information should be retained for as little time as possible. The following pattern provides a strong foundation for local Android storage.

Requirement Recommended approach Reason
Encryption algorithm AES/GCM/NoPadding Provides confidentiality and tamper detection
Key storage Android Keystore Keeps key material in a protected system container
Initialisation vector Fresh 12-byte IV per encryption Prevents repeated ciphertext patterns
Stored format Base64 of IV plus ciphertext and tag Easy to save in SQLite or preferences
Password protection Argon2id, scrypt or PBKDF2 for key derivation Passwords should not be used directly as AES keys
Legacy compatibility Check the Android API level Keystore capabilities vary between releases

Choose authenticated AES encryption

AES supports several modes, but AES/GCM/NoPadding is generally preferable for new Android code. GCM, or Galois/Counter Mode, encrypts the value and creates an authentication tag. During decryption, a modified ciphertext or incorrect associated data causes an exception instead of silently returning corrupted content.

The initialisation vector, commonly called an IV or nonce, must be unique for every encryption performed with the same key. It does not need to be secret, so it can be stored beside the ciphertext. Never reuse an IV with the same AES-GCM key, and never use a fixed value such as "123456789012".

AES keys commonly use 128 or 256 bits. A 256-bit key is suitable where supported, but the security of the complete design depends more on correct key handling, IV generation and access control than on choosing the larger key size.

Generate a key with Android Keystore

The Keystore is designed for cryptographic key management. The following Kotlin function creates an AES key under an alias and returns the existing key when it has already been generated:

private const val KEY_ALIAS = "customer_data_key"
private const val ANDROID_KEYSTORE = "AndroidKeyStore"

private fun getOrCreateKey(): SecretKey {
    val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE).apply {
        load(null)
    }

    val existing = keyStore.getKey(KEY_ALIAS, null) as? SecretKey
    if (existing != null) return existing

    val generator = KeyGenerator.getInstance(
        KeyProperties.KEY_ALGORITHM_AES,
        ANDROID_KEYSTORE
    )

    generator.init(
        KeyGenParameterSpec.Builder(
            KEY_ALIAS,
            KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
        )
            .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
            .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
            .setKeySize(256)
            .build()
    )

    return generator.generateKey()
}

This example targets Android 6.0, API level 23 and later. A key may become unavailable if the user clears app data, removes a required lock-screen credential or changes device security settings. Applications should handle that situation deliberately rather than silently losing access to important records.

Encrypt values before saving them

A small data class can carry the IV and encrypted bytes. SecureRandom supplies a fresh IV, while Cipher performs the AES-GCM operation:

data class EncryptedValue(
    val iv: ByteArray,
    val ciphertext: ByteArray
)

fun encrypt(plainText: String): EncryptedValue {
    val cipher = Cipher.getInstance("AES/GCM/NoPadding")
    val iv = ByteArray(12)
    SecureRandom().nextBytes(iv)

    cipher.init(
        Cipher.ENCRYPT_MODE,
        getOrCreateKey(),
        GCMParameterSpec(128, iv)
    )

    val encrypted = cipher.doFinal(plainText.toByteArray(Charsets.UTF_8))
    return EncryptedValue(iv, encrypted)
}

The final bytes contain the encrypted content and the GCM authentication tag. Keep the IV with those bytes, but do not expose the Keystore key. For larger records, encrypt each record separately so that a single damaged item does not prevent the application from reading every other record.

Decrypt and validate stored data

Decryption uses the saved IV and the same key alias. If the authentication tag fails, doFinal() throws an exception. That result should be treated as a security or data-integrity event, not as an invitation to display partially decoded text.

fun decrypt(value: EncryptedValue): String {
    val cipher = Cipher.getInstance("AES/GCM/NoPadding")

    cipher.init(
        Cipher.DECRYPT_MODE,
        getOrCreateKey(),
        GCMParameterSpec(128, value.iv)
    )

    val plainBytes = cipher.doFinal(value.ciphertext)
    return plainBytes.toString(Charsets.UTF_8)
}

In production code, catch AEADBadTagException and related cryptographic exceptions at the repository boundary. Record a privacy-safe diagnostic, avoid logging the original value, and decide whether the record should be discarded, restored from a trusted source or marked as unavailable.

Store ciphertext safely in Android

SQLite columns such as BLOB are suitable for the IV and ciphertext. If an existing schema uses text columns, Base64 encoding is convenient:

val encrypted = encrypt("offline customer note")
val ivText = Base64.encodeToString(encrypted.iv, Base64.NO_WRAP)
val dataText = Base64.encodeToString(encrypted.ciphertext, Base64.NO_WRAP)

For small settings, encrypted preferences can hold the encoded values, but ordinary SharedPreferences does not encrypt its contents by itself. AndroidX Security’s EncryptedSharedPreferences can simplify common preference storage, although teams should review its current support status and migration requirements before adopting it in a long-lived application.

Do not place keys in source code, resource files, BuildConfig fields or unprotected preferences. Also avoid logging ciphertext, tokens, database rows or exception messages that include user data. Australian users often expect apps to work offline in regional areas, so encrypted local caching should have a clear expiry and deletion policy.

Protect keys and manage app lifecycle

A Keystore key protects data on the device, but it does not automatically protect data after an attacker has full control of an unlocked application session. Limit decryption to the screen or repository operation that needs it, clear sensitive strings and byte arrays where practical, and avoid passing decrypted values through global singletons.

Backup and restore require special care. Restoring an encrypted database to another handset may fail because the original Keystore key is device-bound. Configure backup rules according to the data’s sensitivity, and design a re-authentication or server-resynchronisation path rather than assuming encrypted files can be moved freely.

For financial, health or identity-related applications serving the Australian market, review the Privacy Act 1988, the Australian Privacy Principles and, where relevant, the Notifiable Data Breaches scheme. Organisations covered by those rules should document why data is collected, how long it is retained and what happens after a suspected breach. Encryption reduces exposure, but it does not remove reporting or governance responsibilities.

Test the complete encryption flow

Unit tests should confirm that encrypting and then decrypting ordinary text returns the original value, including Unicode characters used in Australian names and addresses. Test empty strings, long records, database persistence, application restarts and values containing emojis or line breaks. Each encryption operation should produce a different IV and normally different ciphertext.

Add negative tests that modify one ciphertext byte, modify the IV, use the wrong key alias or truncate the stored value. Every case should fail safely. Test on current Android versions and on the minimum supported API level, including devices commonly used by customers in Brisbane, Perth and regional communities.

Before release, inspect backup behaviour, crash reports and debug logs. Confirm that plaintext is absent from SQLite, preferences, analytics events and notification payloads. A reliable AES implementation is valuable only when the surrounding storage, lifecycle and privacy controls preserve the same standard.