This tutorial covers only: 1. Secure tokens 2. Encrypted data 3. Room database security 4. Avoiding password, OTP and MPIN storage

Secure Local Storage in Android

Secure local storage protects sensitive data saved on an Android device.

1. Secure Storage Architecture

flowchart TD
    A["Application receives data"] --> B{"Is data sensitive?"}
    B -- No --> C["Store using Room or DataStore"]
    B -- Yes --> D{"Must it be stored?"}
    D -- No --> E["Keep temporarily in memory"]
    D -- Yes --> F["Encrypt sensitive data"]
    F --> G["Store encrypted value"]
    H["Key generated in Android Keystore"] --> F

The most important rule is:

> The safest sensitive data is data that the application does not store.

---

2. Understanding Sensitive Data

Sensitive data includes:

* Access tokens

* Refresh tokens

* Passwords

* OTPs

* MPINs

* Card details

* Bank account information

* Personal information

* Health information

* Private documents

* Encryption keys

The required protection depends on:

* Data sensitivity

* How long it is required

* Whether offline access is required

* Effect of device compromise

* Business and regulatory requirements

---

3. Secure Tokens

What is a token?

A token represents an authenticated session. After successful authentication, the server normally returns:

* Access token

* Refresh token

* Token expiry information

sequenceDiagram
    participant U as User
    participant A as Android App
    participant S as Server

    U->>A: Enter credentials
    A->>S: Authenticate through HTTPS
    S-->>A: Access token and refresh token
    A->>A: Protect tokens locally
    A->>S: API request with access token
    S-->>A: Protected response

Access token

An access token:

* Is sent with protected API requests.

* Should normally have a short lifetime.

* Should be held in memory when possible.

* May require encrypted persistence if session restoration is required.

Refresh token

A refresh token:

* Is used to obtain a new access token.

* Usually has a longer lifetime.

* Requires stronger protection.

* Should support server-side revocation.

* Should preferably be rotated after use.

* Must be cleared during logout.

Recommended token-storage strategy

TokenRecommended storage
Active access tokenMemory when practical
Persisted access tokenEncrypted local storage
Refresh tokenEncrypted local storage with stronger controls
Token encryption keyAndroid Keystore
PasswordDo not store
OTPDo not persist
MPINDo not store as plain text or reversible local value

Token storage flow

flowchart TD
    A["Server returns tokens"] --> B["Access token used by API client"]
    B --> C["Keep active token in memory"]
    A --> D{"Persistence required?"}
    D -- No --> E["Do not write tokens to disk"]
    D -- Yes --> F["Encrypt token"]
    G["Android Keystore key"] --> F
    F --> H["Store ciphertext in private storage"]

Token-storage implementation design

Create a TokenStore abstraction:

interface TokenStore {
    suspend fun saveTokens(tokens: AuthTokens)
    suspend fun getAccessToken(): String?
    suspend fun getRefreshToken(): String?
    suspend fun clearTokens()
}
data class AuthTokens(
    val accessToken: String,
    val refreshToken: String,
    val expiresAt: Long
)

A secure implementation should:

1. Generate or load a key from Android Keystore.

2. Encrypt the token using authenticated encryption.

3. Store only ciphertext, IV and necessary metadata.

4. Decrypt only when the token is required.

5. Clear all tokens when the session ends.

Never store tokens in

* Source code

* BuildConfig

* XML resources

* Plain SharedPreferences

* Plain DataStore

* Plain Room columns

* Logs

* Crash reports

* Analytics events

* Clipboard

* URL query parameters

Important clarification

DataStore is a modern replacement for SharedPreferences, but its normal use does not automatically mean that sensitive values are encrypted. Android recommends DataStore for small datasets and Room for larger relational data. Sensitive values still need an appropriate encryption design. Android DataStore guidance

Also, EncryptedSharedPreferences and related androidx.security.crypto APIs are currently deprecated. For new security-sensitive designs, use standard cryptographic APIs and protect keys with Android Keystore. AndroidX Security Crypto API

---

4. Android Keystore

What is Android Keystore?

Android Keystore is a system-provided container for cryptographic keys.

A key generated inside Keystore can remain non-exportable, meaning the application can request cryptographic operations without directly reading the raw key material.

It can also restrict:

* Which operations may use the key

* Which cryptographic modes are allowed

* Whether user authentication is required

* Whether biometric authentication is required

Android Keystore documentation

Correct design

Wrong:

Encryption key → Hard-coded in APK
Sensitive data → Encrypted using hard-coded key
Recommended:

Encryption key → Android Keystore
Sensitive data → Encrypted using Keystore-protected key
Ciphertext → DataStore, Room or private file

Generating an AES key

private const val KEY_ALIAS = "local_data_key"

fun generateKey() {
    val keyGenerator = KeyGenerator.getInstance(
        KeyProperties.KEY_ALGORITHM_AES,
        "AndroidKeyStore"
    )

    val specification = KeyGenParameterSpec.Builder(
        KEY_ALIAS,
        KeyProperties.PURPOSE_ENCRYPT or
            KeyProperties.PURPOSE_DECRYPT
    )
        .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
        .setEncryptionPaddings(
            KeyProperties.ENCRYPTION_PADDING_NONE
        )
        .build()

    keyGenerator.init(specification)
    keyGenerator.generateKey()
}

Retrieving the key

fun getSecretKey(): SecretKey {
    val keyStore = KeyStore.getInstance("AndroidKeyStore")
    keyStore.load(null)

    return keyStore.getKey(
        KEY_ALIAS,
        null
    ) as SecretKey
}

Why use AES-GCM?

AES-GCM provides:

* Confidentiality: Data cannot be read without the key.

* Integrity: Modified ciphertext can be detected.

* Authentication: Encryption output includes an authentication tag.

---

5. Encrypting Sensitive Data

Encryption flow

flowchart LR
    A["Plain data"] --> B["AES-GCM encryption"]
    K["Keystore key"] --> B
    B --> C["Ciphertext"]
    B --> D["Unique IV"]
    C --> E["Private local storage"]
    D --> E

Encryption example

data class EncryptedValue(
    val cipherText: ByteArray,
    val initializationVector: ByteArray
)

fun encrypt(
    plainText: ByteArray,
    secretKey: SecretKey
): EncryptedValue {
    val cipher = Cipher.getInstance("AES/GCM/NoPadding")
    cipher.init(Cipher.ENCRYPT_MODE, secretKey)

    val encryptedBytes = cipher.doFinal(plainText)

    return EncryptedValue(
        cipherText = encryptedBytes,
        initializationVector = cipher.iv
    )
}

Decryption example

fun decrypt(
    encryptedValue: EncryptedValue,
    secretKey: SecretKey
): ByteArray {
    val cipher = Cipher.getInstance("AES/GCM/NoPadding")

    val parameterSpec = GCMParameterSpec(
        128,
        encryptedValue.initializationVector
    )

    cipher.init(
        Cipher.DECRYPT_MODE,
        secretKey,
        parameterSpec
    )

    return cipher.doFinal(encryptedValue.cipherText)
}

Important encryption rules

* Generate the key using Android Keystore.

* Use modern authenticated encryption such as AES-GCM.

* Use a unique IV for every encryption operation.

* Store the IV with the ciphertext.

* Never reuse an IV with the same AES-GCM key.

* Never hard-code the encryption key.

* Never create a custom cryptographic algorithm.

* Never treat Base64 as encryption.

Is the IV secret?

No. The IV does not generally need to be secret.

It can be stored with the ciphertext:

Stored value = IV + Ciphertext

However, it must be unique for each encryption with the same key.

Base64 usage

Binary ciphertext and IV may be converted to Base64 for storage:

val encodedCipherText = Base64.encodeToString(
    encryptedValue.cipherText,
    Base64.NO_WRAP
)

Base64 is only an encoding format. It does not secure plain data.

---

6. Room Database Security

What is Room?

Room is an abstraction layer over SQLite used for storing structured data locally.

Common uses include:

* Cached API responses

* User-generated records

* Offline data

* Transaction history

* Application configuration

Android Room documentation

Does Room encrypt data automatically?

No.

A normal Room database is stored as a regular SQLite database inside the application’s private storage.

The application sandbox protects it from ordinary apps, but the database may still be exposed through:

* Compromised or rooted devices

* Insecure backups

* Debug builds

* Accidental file sharing

* Application vulnerabilities

* Physical extraction in some threat models

Room security levels

Level 1: Store only non-sensitive data

Best option when possible:

@Entity(tableName = "articles")
data class ArticleEntity(
    @PrimaryKey val id: String,
    val title: String,
    val summary: String
)

No additional encryption is required if the data is public and the threat model does not require it.

Level 2: Encrypt selected fields

Encrypt only sensitive columns:

@Entity(tableName = "customer")
data class CustomerEntity(
    @PrimaryKey val customerId: String,
    val displayName: String,
    val encryptedAccountNumber: String,
    val accountNumberIv: String
)
flowchart LR
    A["Sensitive field"] --> B["Encrypt"]
    B --> C["Room stores ciphertext"]
    C --> D["Read ciphertext"]
    D --> E["Decrypt when required"]

Advantages:

* Only sensitive fields are encrypted.

* Non-sensitive columns remain searchable.

* Smaller performance impact.

Limitations:

* More application code is required.

* Encrypted fields cannot be searched or sorted normally.

* Developers must manage IVs and key lifecycle correctly.

Level 3: Encrypt the complete database

For highly sensitive applications, the entire SQLite database can be encrypted using a carefully reviewed database-encryption solution.

Considerations include:

* Encryption-library maintenance

* Database performance

* Key storage

* Migration strategy

* Backup handling

* Key rotation

* Recovery after Keystore invalidation

* Compatibility with Room versions

Whole-database encryption is not built into Room itself.

Field encryption with a converter

A simplified conceptual converter:

class SecureStringConverter(
    private val cryptoManager: CryptoManager
) {
    @TypeConverter
    fun encrypt(value: SensitiveString?): String? {
        return value?.let(cryptoManager::encrypt)
    }

    @TypeConverter
    fun decrypt(value: String?): SensitiveString? {
        return value?.let(cryptoManager::decrypt)
    }
}

Be careful with global converters because accidentally applying encryption to IDs or searchable fields can break queries. Explicit conversion in the repository or data-mapping layer is often easier to understand and audit.

Database backup protection

Sensitive Room data should not be copied into uncontrolled backups.

Possible manifest configuration:

<application
    android:allowBackup="false"
    android:fullBackupContent="false">
</application>
If the application requires backups, use backup rules to exclude sensitive database, token and key-related files rather than automatically disabling every backup.

Room logging

Avoid enabling database query logging for sensitive production data.

Never log:

Log.d("Database", "Account number: $accountNumber")

Also review:

* Debug database inspectors

* Analytics

* Crash reports

* SQL error messages

* Test fixtures

* Screenshots

Room data deletion

On logout, delete user-specific data when required:

@Dao
interface AccountDao {

    @Query("DELETE FROM accounts")
    suspend fun deleteAllAccounts()
}

A logout operation may need to clear:

* Tokens

* User profile

* Cached accounts

* Transaction information

* Pending sensitive operations

* User-specific files

* Sensitive notifications

Do not delete data that must legally or functionally remain without confirming the business requirements.

---

7. Avoid Password Storage

Why should passwords not be stored?

A password is a reusable authentication secret. If the application stores it, an attacker may use it:

* On the same application

* On the application’s website

* On other services where the user reused it

Correct password flow

flowchart TD
    A["User enters password"] --> B["Send through HTTPS"]
    B --> C["Server validates password"]
    C --> D["Server returns tokens"]
    D --> E["Remove password from app state"]
    E --> F["Use tokens for later requests"]

Never store passwords in

* SharedPreferences

* DataStore

* Room

* Files

* Logs

* ViewModel after authentication

* SavedStateHandle

* savedInstanceState

* Clipboard

* Analytics

* Crash reports

“Remember me” implementation

“Remember me” should not mean saving the password.

It should mean:

User authenticates
      ↓
Server issues revocable token
      ↓
App securely stores protected token
      ↓
App restores session using token

The server must be able to expire and revoke the session.

---

8. Avoid OTP Storage

What is an OTP?

OTP means One-Time Password. It should be:

* Short-lived

* Single-use

* Validated by the server

* Removed after validation or expiry

Correct OTP flow

sequenceDiagram
    participant U as User
    participant A as Android App
    participant S as Server

    S-->>U: Send OTP
    U->>A: Enter OTP
    A->>S: Submit OTP through HTTPS
    S->>S: Validate OTP and expiry
    S-->>A: Success or failure
    A->>A: Clear OTP immediately

Do not store OTP in

* Room

* DataStore

* SharedPreferences

* SavedStateHandle

* Logs

* Analytics

* Clipboard

* Notification history

* Files

Temporary OTP handling

Keep it only as temporary UI state:

var otp by mutableStateOf("")
    private set

fun submitOtp() {
    viewModelScope.launch {
        try {
            repository.verifyOtp(otp)
        } finally {
            otp = ""
        }
    }
}

Also clear the OTP when:

* Validation succeeds

* Validation fails according to policy

* OTP expires

* User leaves the flow

* ViewModel is cleared

The server, not the mobile app, should decide whether the OTP is valid, expired or already used.

---

9. Avoid MPIN Storage

What is an MPIN?

An MPIN is a short numeric secret used to authenticate the user or authorize sensitive operations.

Because it has a small number of possible combinations, storing it locally is especially dangerous.

For example, a four-digit MPIN has only:
0000 to 9999 = 10,000 possibilities
If attackers obtain a locally stored MPIN hash, they may attempt every possible value quickly unless the security design includes strong server-side controls.

Recommended MPIN flow

flowchart TD
    A["User enters MPIN"] --> B["Send through secure channel"]
    B --> C["Server validates MPIN"]
    C --> D{"Valid?"}
    D -- Yes --> E["Issue or continue secure session"]
    D -- No --> F["Apply retry and lockout policy"]
    E --> G["Clear MPIN from memory"]
    F --> G

Never store

* Plain MPIN

* Encrypted MPIN for later recovery

* MPIN in logs

* MPIN in database

* MPIN in DataStore

* MPIN in SavedStateHandle

* MPIN inside the APK

Encryption is reversible. If the application can decrypt a stored MPIN automatically, an attacker who compromises the application environment may attempt to do the same.

Local biometric login

Instead of storing an MPIN, use:

* BiometricPrompt

* Device credentials

* A Keystore key requiring user authentication

* A server-issued, revocable session token

Biometric authentication
        ↓
Unlock Keystore-protected key
        ↓
Decrypt protected session token
        ↓
Server validates session

Biometrics or device credentials protect access to the key. The server still performs final authorization.

---

10. Storage Decision Table

DataStore locally?Recommended approach
PasswordNoSend securely and discard
OTPNoKeep temporarily in memory
MPINNoValidate securely; discard immediately
Access tokenOnly if requiredMemory or encrypted local storage
Refresh tokenIf session restoration requires itEncrypt with Keystore-protected key
Encryption keyYesAndroid Keystore
Public API cacheYesRoom
Sensitive API cacheOnly if requiredEncrypted fields or encrypted database
User settingsYesDataStore
Sensitive user settingOnly if requiredEncrypt before persistence
Card CVVNoNever persist
Account detailsOnly if requiredMinimize and encrypt sensitive fields

---

11. Complete Secure Storage Flow

flowchart TD
    A["Data received"] --> B{"Password, OTP, MPIN or CVV?"}
    B -- Yes --> C["Use temporarily and discard"]
    B -- No --> D{"Does it need persistence?"}
    D -- No --> E["Keep only in memory"]
    D -- Yes --> F{"Is it sensitive?"}
    F -- No --> G["Room, DataStore or private file"]
    F -- Yes --> H["Encrypt using authenticated encryption"]
    I["Key protected by Android Keystore"] --> H
    H --> J["Store ciphertext in private storage"]

---

12. Common Mistakes

1. Treating private storage as encryption

Internal storage is sandboxed, but it does not automatically encrypt application-level values.

2. Storing a token in plain DataStore

DataStore improves data-storage APIs but plain DataStore does not automatically provide the complete encryption design needed for sensitive tokens.

3. Hard-coding the encryption key

const val KEY = "my-secret-key" // Insecure

APK files can be reverse-engineered.

4. Storing the encryption key beside the encrypted value

Encrypted token → preferences
Encryption key → same preferences

An attacker obtaining both may decrypt the token.

5. Assuming Room is encrypted

Room provides structured persistence over SQLite. It does not automatically encrypt database contents.

6. Reusing an AES-GCM IV

An IV must be unique for each encryption operation performed with the same key.

7. Logging authentication data

Tokens, passwords, OTPs and MPINs must not appear in logs, analytics or crash reports.

8. Storing a password for “Remember me”

Store an appropriately protected, revocable session token—not the password.

9. Hashing a short MPIN locally

A four- or six-digit MPIN has limited possibilities. A local hash alone does not provide safe authentication.

10. Forgetting logout cleanup

Logout should clear protected tokens, sensitive caches and user-specific background operations according to business requirements.

---

13. Interview Questions

1. What is secure local storage?

It is a design that minimizes sensitive data stored on the device and protects necessary persisted data using private storage, encryption and securely managed keys.

2. Where should encryption keys be stored?

They should generally be generated and protected using Android Keystore.

3. Does Android Keystore store normal application data?

No. It primarily protects cryptographic keys. The encrypted application data is normally stored in Room, DataStore or private files.

4. Does DataStore encrypt data automatically?

Normal DataStore usage should not be assumed to encrypt sensitive values. Encrypt sensitive information using an appropriate, reviewed design.

5. Does Room encrypt its database?

No. Room is an abstraction over SQLite and does not provide automatic database encryption.

6. How can sensitive Room data be protected?

Minimize stored data, encrypt selected fields or use a reviewed whole-database encryption solution, while protecting keys through Android Keystore.

7. Should access tokens be stored?

Keep access tokens in memory when practical. If persistence is required, encrypt them and protect the encryption key using Android Keystore.

8. Why does a refresh token require stronger protection?

It is longer-lived and can be used to obtain new access tokens.

9. Should an Android application store a password?

No. It should submit the password securely for authentication, receive tokens and discard the password.

10. Should OTP be stored in SavedStateHandle?

No. SavedStateHandle may persist its content for state restoration. OTP should remain temporary and be cleared quickly.

11. Can an MPIN be stored after encryption?

Usually, no. Authentication secrets such as MPINs should not be locally recoverable. Prefer server validation or secure platform authentication.

12. Is Base64 an encryption mechanism?

No. Base64 is reversible encoding and provides no security.

13. Why is AES-GCM commonly used?

It provides both confidentiality and integrity protection.

14. What happens to encrypted data if a Keystore key becomes unavailable?

The application may no longer be able to decrypt that data. The design needs a recovery strategy, such as clearing the local session and requiring authentication again.

15. What should happen on logout?

Revoke the session where supported, clear tokens and remove sensitive user-specific cached data according to application requirements.

---

Quick Revision

Secure tokens
    → Keep in memory when possible
    → Encrypt if persistence is required
    → Protect keys with Android Keystore

Encrypted data
    → Use authenticated encryption such as AES-GCM
    → Use a unique IV
    → Never hard-code keys

Room security
    → Room is not encrypted automatically
    → Minimize data
    → Encrypt sensitive fields or use reviewed DB encryption

Password
    → Never store

OTP
    → Keep temporarily in memory and discard

MPIN
    → Do not store as plain text or a recoverable local value