SecureAndroidPersist — Android Persistence and Encryption Made Simple
Introduction
Securing user data in modern mobile apps is more important than ever. Whether you’re storing sensitive user credentials, API tokens, or other confidential information, SecureAndroidPersist makes it easy to encrypt, persist, and manage that data. This library is designed to give developers robust security with minimal setup and coding effort.
You can find the AndroidSerurePersist library here
but I urge you to read through before doing so.
What this library does
SecureAndroidPersist is an Android library that streamlines the encryption and persistence of sensitive data. It can handle SharedPreferences, DataStore, complex objects, files, and raw data — all while seamlessly integrating into your existing code.
With property delegation, you can treat encrypted data just like any other variable, making it feel natural and straightforward to use.
Key Benefits
- Easily implement secure encrypted storage solutions without extensive configuration.
- Effortlessly encrypt raw data and files.
- Automatically serialize and protect complex objects.
- Use Kotlin property delegation to handle encrypted data like regular variables.
Feature Highlights
This library offers a wide range of features for securely persisting data, while also providing encryption services for raw data and files when needed.
Feature Set 1 — Encrypted Persistence
- Secure Preferences Management: Encrypt and decrypt data with SharedPreferences and DataStore, no extra steps needed.
- Complex Data Types: Save and restore custom objects and collections, fully encrypted.
- File Encryption: Securely encrypt files so they remain protected even if stored externally.
- Kotlin Property Delegation: Treat encrypted preferences as normal variables.
- Jetpack Compose State Persistence: Easily persist and restore UI state securely with Compose, avoiding extra boilerplate.
- Asynchronous Operations: Use non-blocking operations with DataStore for smooth performance.
Feature Set 2 — Raw Encryption
- Raw Data and File Encryption: Encrypt and decrypt any raw data or files as needed.
- External Key Management: Utilize custom keys stored remotely or shared across devices for flexible encryption strategies.
Why Choose SecureAndroidPersist?
- Security: Your data is protected with robust, proven encryption techniques.
- Simplicity: A developer-friendly API means less code, fewer headaches.
- Versatility: Works with primitives, complex objects, files, and integrates smoothly with Android components.
- Performance: Non-blocking operations ensure a responsive user experience.
- Flexibility: Gives the option to manage keys externally and adapt the approach to your own security policies.
Library installation
Since SecureAndroidPersist is available via Jitpack, first add the Jitpack repository to your settings.gradle
or project-level build.gradle
:
// at your settings.gradle you have this code
repositories {
google()
mavenCentral()
maven(url = "https://jitpack.io") // <-- add this line
}
Then add the dependencies to your app/build.gradle
:
// core library
implementation("com.github.ioannisa.secured-android-persist:secure-persist:2.5.1")
// additional library to automatically handle state in Jetpack Compose
implementation("com.github.ioannisa.secured-android-persist:secure-persist-compose:2.5.1")
Code Samples — Encrypted Persistence
First, you need to initialize the PersistManager
class which requires a context
.
val persistManager = PersistManager(context)
The library allows you to take two paths to handling encrypted persisted data, and that is via the sharedPrefs
and the dataStorePrefs
instance variables.
sharedPrefs
and dataStorePrefs instance variable
This allows you full access to the EncryptedSharedPreferences
with zero-configuration.
Using the
SharedPrefferences
with this library utilizes theEncryptedSharePreferences
for native security, thus this library does not include non-encrypted usage ofSharedPreferences
sharedPrefs
directlysharedPrefs
via property delegation
You will come to see that using sharedPrefs
via property delegation is very easy and the defacto way to go when using this library.
SharedPrefs
directly
Let the following code snippets show you how to use the sharedPrefs
instance directly:
sharedPrefs
:
For sharedPrefs things are pretty straight-forward:
// "put" an encrypted sharedPref for an integer for key "myKey"
persistManager.sharedPrefs.put(key="myKey", value="string value")
// "get" an encrypted sharedPref's value
persistManager.sharedPrefs.get(key="myKey", defaultValue="not found")
// delete a key from sharedPreferenes
persistManager.sharedPrefs.delete(key="myKey")
// you can also add and delete complex objects with automatic serialization
data class AuthInfo(
val accessToken: String = "",
val refreshToken: String = "",
val expiresIn: Long = 0L
)
// "put"
persistManager.sharedPrefs.put(key="authKey", value=AuthInfo())
// "get"
val auth = persistManager.sharedPrefs.get("authKey", defaultValue=AuthInfo())
dataStorePrefs
:
If you know DataStore
, then you know it uses coroutines
. We take two routes. One is to use normally via coroutines, and the other is directly with coroutines working behind the scenes for your comfort.
dataStorePrefs
uses encryption by default, unless you set encrypted=false
.
dataStorePrefs
using coroutines:
using coroutines we have the following functions
put
to put dataget
to get datadelete
to delete data
// Encrypt and save a preference
CoroutineScope(Dispatchers.IO).launch {
persistManager.dataStorePrefs.put("key1", "secureValue")
}
// Decrypt and retrieve a preference
CoroutineScope(Dispatchers.IO).launch {
val value = persistManager.dataStorePrefs.get("key1", "defaultValue")
println("Retrieved value: $value")
}
// Delete a preference
CoroutineScope(Dispatchers.IO).launch {
persistManager.dataStorePrefs.delete("key1")
}
dataStorePrefs
handling coroutines on the background:
For that we can use the “direct” variant of the dataStorePrefs. avoiding to execute from a suspended function.
putDirect
to put data (non-blocking)getDirect
to get data (blocking)deleteDirec
to delete data (non-blocking)
// assuming encryption - Encrypt and store in a non-blocking way to DataStore
persistManager.dataStorePrefs.putDirect("key1", "secureValue")
// assuming encryption - Decrypt and get in a blocking way the value from DataStore
val value = persistManager.dataStorePrefs.getDirect("key1", "defaultValue")
// no encryption - store unencrypted in a non-blocking way to DataStore
persistManager.dataStorePrefs.putDirect("key1", "secureValue", encrypted = false)
// no encryption - get unencrypted in a blocking way the value from DataStore
val value = persistManager.dataStorePrefs.getDirect("key1", "defaultValue", encrypted = false)
// Delete the DataStore preference without using coroutines
persistManager.dataStorePrefs.deleteDirect("key1")
SharedPrefs and dataStorePrefs
delegated
Let the following code snippets show you how to use the sharedPrefs
instance via property delegation and as you see it is the preferred way to go.
Here the logic is “intuition”. We treat the preferences as normal variables. Here the idea is identical for both the sharedPrefs
and for dataStorePrefs
// lets make a counter using sharedPref
var pref1 by persistManager.sharedPrefs.preference(1000)
pref1 = 100
pref1++
pref1++
// lets make a counter using dataStorePref
var pref1 by persistManager.dataStorePrefs.preference(1000)
pref1 = 100
pref1++
pref1++
Persitence — Jetpack Compose State
The library supports seamless integration also with jetpack compose’s state by allowing you to create mutable state properties that utilize the underlaying structure seamlessly.
You can literally say from the following example that persisting securely to EncryptedSharedPreferences
and DataStore Preferences
using state
cannot get any easier!
import androidx.lifecycle.ViewModel
import eu.anifantakis.lib.securepersist.PersistManager
import eu.anifantakis.lib.securepersist.compose.mutableStateOf
class CounterViewModel(
persistManager: PersistManager
) : ViewModel() {
// If key is unspecified, property name becomes the key
// Defaults to EncryptedSharedPreferences and uses the property name as the key
var count1 by persistManager.sharedPrefs.mutableStateOf(1000)
private set
// Sets a custom key and uses DataStorePreferences with encryption
var count2 by persistManager.dataStorePrefs.mutableStateOf(
defaultValue = 2000,
key = "counter2Key"
)
private set
// Uses the property name as the key and sets storage to Unencrypted DataStorePreferences
var count3 by persistManager.dataStorePrefs.mutableStateOf(
defaultValue = 3000,
encrypted = false
)
private set
fun increment() {
count1++
count2++
count3++
}
}
Code Samples — Raw Encryption
The encryption algorithms used inside PersistManager
have been exposed to you directly so you can utilize them for raw-data encryption, and file encryption.
Encryption utilizes KeyStore
for securely storing your keys into your phone’s chip for maximum protection, but also allows you to generate and export/import custom keys incase you want to save raw encrypted data to some remote server.
Initializing is also simple here. For KeyStore
it is pretty simple
//initializing for KeyStore
val encryptionManager = EncryptionManager(context)
If you want to have a key that can be stored outside your phone so you can reuse encrypted data stored on some server on multiple devices, you need to generate first that “exportable-key” which can be sent to that remote server, and then initialize EncryptionManager
with that key.
// First, generate an external key:
val externalKey = EncryptionManager.generateExternalKey()
// Then, use the generated key to create an instance of EncryptionManager:
val encryptionManager = EncryptionManager(context, externalKey)
Example Encrypting/Decrypting Raw data with KeyStore
val encryptionManager = EncryptionManager(context)
// Encrypt data
val encryptedData = encryptionManager.encryptData("plainText")
// Decrypt data
val decryptedData = encryptionManager.decryptData(encryptedData)
val plainText = String(decryptedData, Charsets.UTF_8)
// Encrypt a value and encode it to a Base64 string
val encryptedValue = encryptionManager.encryptValue("valueToEncrypt")
// Decrypt a Base64 encoded string and return the original value
val decryptedValue: String = encryptionManager.decryptValue(encryptedValue, "defaultValue")
Storing the externalKey (SecretKey) to some other medium
Other than using instance of EncryptionManager, you can use it in a static context too in order to handle some data that was encrypted/decrypted with some other key without having to create more instances.
In this example we will combine this static context demo with SecretKey
usage.
Scenario:
— Generate a SecretKey, use it in static context to encrypt some data.
— Create an encoding of that key so you can save it as String to a remote server
— Retrieve the encoded secret-key string from the remote server and decode it to build back again a SecretKey binary.
— Use it to decode the data that was encoded with that key
If you have generated a key and want to securely transmit it to some API or retrieve it, then this library provides two convenience static methods for encoding
and decoding
that key to a string so you can easily transfer it.
Exporting custom key to save it to some server
// generate a key
val originalKey = EncryptionManager.generateExternalKey()
// encrypt your data with that external key
val encryptedData = EncryptionManager.encryptData("Hello, Secure World!", originalKey)
// encode key to stringify it so you can send it to some server
val encodedKey: String = EncryptionManager.encodeSecretKey(originalKey)
// make network call and save the encoded key string to some server
// ...
Retrieving the custom key from the server to use it in the app
val encodedKey = ... // fetch the string with encoded key from a remote server
// construct a SecretKey from that encodedKey retrieved from the server
val decodedKey: SecretKey = EncryptionManager.decodeSecretKey(encodedKey)
// as you can see, you can decode the encrypted data using the key that was reconstructed from the encoded string
val decryptedText = EncryptionManager.decryptData(encryptedData, decodedKey)
FILES Encryption/Decription
Additionally, EncryptionManager
offers encryptFile
and decryptFile
functions, enabling you to securely encrypt data and store it in a file, as well as read and decrypt the data from the file when needed.
These functions provide a seamless way to handle file-level encryption and decryption, ensuring that sensitive data remains secure during both storage and retrieval.
// Create an EncryptionManager instance
val encryptionManager = EncryptionManager(context, "your_key_alias")
// Specify the input file and the name for the encrypted file
val testFile = File(context.filesDir, "plain.txt")
val encryptedFileName = "encrypted.dat"
// Encrypt the file
encryptionManager.encryptFile(testFile, encryptedFileName)
// Decrypt the file
val decryptedContent: ByteArray = encryptionManager.decryptFile(encryptedFileName)
val decryptedText = String(decryptedContent)