by Ioannis Anifantakis
In modern mobile apps, safeguarding user data is crucial. Whether it’s user credentials, API tokens, or any sensitive information, SecurePersist allows developers to implement robust encryption and persistence with minimal effort.
Imagine needing to securely store users’ authentication tokens in a finance app — this library will help you do that efficiently while keeping the process simple.
Android Secure Persist Library is a pure Kotlin library designed to provide secure and efficient storage of preferences, complex data types, and files in Android applications. By leveraging the Android KeyStore and modern encryption techniques, SecurePersist ensures that sensitive data — including complex objects and files — is stored safely, protecting it from unauthorized access.
With the latest addition of State Persistence, SecurePersist now also offers seamless integration with Jetpack Compose, enabling developers to persist and restore UI state effortlessly. This ensures that your Compose-based interfaces maintain consistent and secure state across recompositions and app restarts without additional boilerplate.
This library allows out of the box with zero-configuration encrypting and decrypting preferences using SharedPreferences and DataStore, supports serialization of complex data types, and provides robust raw data and file encryption capabilities.
One big plus is due to property delegation support, with this library, you can handle encrypted data just like regular objects you read and write, making data security effortless!
So this library makes it easy for developers to
- implement comprehensive encrypted storage solutions
- encrypt raw data and files with ease
This library offers a wide range of features for securely persisting data, while also providing encryption services for raw data and files when needed.
-
Secure Preferences Management: Easily encrypts and decrypts preferences utilizing
SharedPreferencesandDataStore. -
Support for Complex Data Types: Automatically serializes and securely stores complex objects, including custom classes and collections.
-
File Encryption and Decryption: Securely encrypts and decrypts files, ensuring sensitive data remains protected even when stored externally.
-
Property Delegation: Uses Kotlin property delegation for seamless integration of encrypted preferences.
-
Asynchronous Operations: Efficiently handles preferences with non-blocking operations using
DataStore. -
Jetpack Compose State Persistence: Seamlessly integrates with Jetpack Compose by providing
MutableStatedelegates that automatically persist and restore UI state. This ensures your Compose components maintain consistent and secure state across recompositions and app restarts without additional boilerplate.
-
Raw Data Encryption: Directly encrypts and decrypts raw data and files using
EncryptionManagerfor additional flexibility. -
External Key Management: Allows for custom external keys for scenarios requiring cross-device data decryption or storing the key on a remote server.
-
Security: Protects sensitive data with robust encryption techniques, including complex objects and files.
-
Ease of Use: Simplifies the process of managing encrypted preferences and data with a user-friendly API.
-
State Persistence: Seamlessly integrates with Jetpack Compose by providing
MutableStatedelegates that automatically persist and restore UI state. This ensures your Compose components maintain consistent and secure state across recompositions and app restarts without additional boilerplate. -
Versatility: Supports a variety of data types, including primitives, complex objects, and files, integrating seamlessly with existing Android components.
-
Performance: Ensures non-blocking operations for a smooth user experience.
-
Flexibility: Allows for external key management, enabling secure data storage and retrieval across devices or from remote servers.
- Add this to your dependencies
implementation("com.github.ioannisa.secured-android-persist:secure-persist:2.5.6")
implementation("com.github.ioannisa.secured-android-persist:secure-persist-compose:2.5.6")- Add Jitpack as a dependencies repository in your
settings.gradle(or at Project'sbuild.gradlefor older Android projects) for gradle to know how to fetch dependencies served by that repository:
repositories {
google()
mavenCentral()
maven(url = "https://jitpack.io") // <-- add this line
}Important: Similar to Option 1, if your project does not use Jetpack Compose, you can omit the
secure-persist-composedependency.
SecurePersist is modularized to provide flexibility based on your project's requirements:
-
secure-persist(Core Library):- Purpose: Provides secure data persistence capabilities using
SharedPreferencesandDataStore. - Usage: Ideal for projects that need to securely store preferences, complex data types, and files
- Purpose: Provides secure data persistence capabilities using
-
secure-persist-compose(Jetpack Compose Integration):- Purpose: Extends the core library by adding Jetpack Compose state persistence functionality.
- Usage: Specifically for projects utilizing Jetpack Compose, allowing developers to persist and restore UI state effortlessly using property delegation.
Reason for Separation: Not all Android projects use Jetpack Compose. By keeping this functionality in a separate module, developers can include it only when needed, reducing unnecessary dependencies and potential overhead in projects that do not use Compose.
Summary: - Include Both Modules: If your project uses Jetpack Compose and you want to leverage state persistence alongside secure data storage. - Include Only
secure-persist: If your project does not use Jetpack Compose or you do not require state persistence.
The PersistManager is the core component of SecurePersist, responsible for managing encrypted preferences using both SharedPreferences and DataStore with zero-configuration.
It utilizes the EncryptionManager's cryptographic algorithms to securely handle data. Additionally, it supports encryption of complex data types, including custom objects and collections, with automatic serialization powered by the Gson library in the background.
When initializing PersistManager, an instance of EncryptionManager is automatically created to manage the encryption and decryption of persisted data. If your encryption needs are limited to preferences, creating an additional EncryptionManager instance is unnecessary.
However, if you need to apply encryption to raw data or files outside of preference management, you can directly utilize the EncryptionManager included in the library for custom encryption tasks unrelated to persistence. This flexibility allows you to securely handle both persistent and non-persistent data within a single solution.
During PersistManager initialization, you can optionally specify a KeyStore alias. If none is provided, a default alias ("keyAlias") will be generated for you. This alias serves as an identifier for the pool of keys stored in the KeyStore, so even if you don’t define a custom alias, the default will function without any issues.
// Create a PersistManager instance with a custom KeyStore alias
val persistManager = PersistManager(context, "your_key_alias")
// Create a PersistManager instance with the default KeyStore alias ("keyAlias")
val persistManager = PersistManager(context)PersistManager provides a unified and flexible way to handle encrypted preferences using both SharedPreferences and DataStore.
The PersistManager class currently provides multiple ways to manage SharedPreferences and DataStore preferences, offering flexibility based on your needs:
via the sharedPrefs instance and the dataStorePrefs instance exposed by the PersistManager class, you can access the EncryptedSharedPreferences and DataStore Preferences - With Encryption Support directly and via delegation:
— sharedPrefs instance variable that leads to the SharedPreferencesManager class, responsible for managing encrypted shared preferences.
— dataStorePrefs instance variable that leads to the DataStoreManager class, responsible for managing DataStore Preferences with encryption.
As mentioned, PersistManager simplifies the usage of encrypted SharedPreferences or DataStore preferences by supporting property delegation (using the by keyword). This makes handling advanced persistence as straightforward as working with regular Kotlin properties, while also managing encryption and serialization behind the scenes.
Via the preference of the sharedPrefs and the dataStorePrefs you can use delegation.
Both methods offer a seamless and secure way to manage preferences with minimal effort.
Important Note: When handling DataStore Preferences via the property delegation approaches, it's crucial to note that the system manages coroutines internally. This means that
putoperations are non-blocking, ensuring efficient data storage. However, retrieving preferences withgetis handled in a blocking manner, which may impact performance during data access.
1. preference function utilizing the EncryptedSharedPreferences
// assuming key to be the variable name (myKey)
var myKey by persistManager.sharedPrefs.preference( "default value")
// declaring the key to be "myKey"
var myPref by persistManager.sharedPrefs.preference("default value", "myKey")2. preference fuction utilizing the DataStore
// declaring the key to be "myKey"
// declaring Storage to Encrypted DataStore Preferences
var myPref by persistManager.dataStorePrefs.preference(
defaultValue = "default value",
key = "myKey",
)
// declaring the key to be "myKey"
// declaring Storage to Unencrypted DataStore Preferences
var myPref by persistManager.dataStorePrefs.preference(
defaultValue = "default value",
key = "myKey",
encrypted = false
)Notes:
- If
keyisnullor empty, the property name will be used as the key. - When using
DataStore, you can specify whether the data should be encrypted by choosing the appropriate Storage.
In the example below, we declare an encrypted shared preference managed by PersistManager, which handles an instance of a data class AuthInfo.
When we declare it, PersistManager automatically uses the variable's name as the key for the encrypted shared preference.
Since we don't specify a Storage type, PersistManager defaults to SHARED_PREFERENCES. It serializes the object into JSON using the Gson library and stores it encrypted.
Whenever we access this object, the get method of the property delegate is triggered. This method decrypts and deserializes the stored data, reconstructing an instance of AuthInfo.
To delete a preference, we need to use the delete function provided by PersistManager.
data class AuthInfo(
val accessToken: String = "",
val refreshToken: String = "",
val expiresIn: Long = 0L
)
// EncryptedSharedPreferendes, and key="authInfo"
var authInfo by persistManager.sharedPrefs.preference(AuthInfo())
// Update authInfo as if it was a normal variable
authInfo = AuthInfo(
accessToken = "token123",
refreshToken = "refresh123",
expiresIn = 3600L
)
// Access as if it was a normal variable
// It retrieves the encrypted shared preference
println(authInfo)
// Deleting data
// if you try to access the delegate again it will return default value
persistManager.delete("authInfo")The PersistManager class supports all the mentioned functionalities, while also offering traditional coding methods alongside the two property delegate approaches previously discussed.
For that, it exposes two public instance variables, each tailored for a specific use case:
-
sharedPrefsspecialized for encrypted preferences via EncryptedSharedPreferences -
dataStorePrefsspecialized for preferences via DataStore with encryption via theEncryptionManagerclass which is contained in this library.
These variables allow you to choose the appropriate preference management method based on your needs, ensuring flexibility and security across different storage mechanisms.
This will introduce you to the PersistManager’s sharedPrefs instance variable to handle encrypted shared preferences.
// Encrypt and save a preference
persistManager.sharedPrefs.put("key1", "secureValue")
// Decrypt and retrieve a preference
val value: String = persistManager.sharedPrefs.get("key1", "defaultValue")
// Delete a preference
persistManager.sharedPrefs.delete("key1")To handle Double and complex data types, the library uses serialization via the gson library, while standard types are supported natively by the SharedPreferences avoid serialization.
| Data Type | Supported Directly | Handled via gson Serialization |
|---|---|---|
| Boolean | Yes | No |
| Int | Yes | No |
| Float | Yes | No |
| Long | Yes | No |
| Double | No | Yes |
| String | Yes | No |
| Custom Objects (e.g., Data Classes) | No | Yes |
DataStore Preferences is a modern, non-blocking, and highly efficient solution for managing application preferences, built around Kotlin’s coroutine architecture. Unlike SharedPreferences, which operates synchronously and can block the main thread, DataStore performs operations asynchronously, ensuring a smoother and more responsive user experience.
However, a key limitation of DataStore is the lack of built-in encryption, unlike EncryptedSharedPreferences. This makes secure implementations more challenging, leading some developers to revert to SharedPreferences despite DataStore's superior performance and flexibility.
-
To address this, our library’s
EncryptionManagerprovides the missing encryption layer forDataStore, allowing developers to securely handle preferences without compromising on performance or having to revert toSharedPreferences. -
Additionally, the library allows for automatic serialization of complex data
-
And finally, our library can provide an extra way of accessing the DataStore preferences by internally handling coroutines, enabling direct access to
DataStorepreferences with non-blocking functions for storing and deleting preferences. For retrieval operations, blocking access is provided, making usage as simple as withEncryptedSharedPreferences, but more performant for storing and deleting data.
Note: For DataStore preferences handled via PersistManager, encryption is enabled by default unless specified otherwise.
Exposing coroutines directly to the developer allows for non-blocking and performant way of storing, deleting and retrieving data, just as designed by DataStore, but with the extra ability to have encryption and without having to make any initialization.
By exposing coroutines directly to the developer, our library enables a non-blocking, efficient way to put, delete, and get data — just as DataStore was designed to do.
This approach maintains the performance benefits of DataStore, while adding the critical capability of encryption, all without requiring any additional setup or initialization from the developer. This seamless integration ensures secure, asynchronous data management with minimal effort.
Related Functions:
putgetdelete
SecurePersist extends encryption capabilities to DataStore, supporting both primitive and complex data types. Since DataStore operations are suspend functions, you need to call them within a coroutine or another suspend function.
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
// 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")
}If you prefer ease of use without dealing with coroutines directly, PersistManager has you covered! The second approach for accessing DataStore preferences in PersistManager handles coroutines behind the scenes, delivering the same performance benefits while simplifying usage.
This method still offers superior performance over EncryptedSharedPreferences, particularly for put and delete operations, as they are executed in a non-blocking manner. The only blocking operation is retrieving data with get.
To access DataStore preferences without coroutines, use the
Directversions of the functions.
Related Functions:
putDirect(non-blocking)getDirect(blocking)deleteDirect(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: String = 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: String = persistManager.dataStorePrefs.getDirect("key1", "defaultValue", encrypted = false)
// Delete the DataStore preference without using coroutines
persistManager.dataStorePrefs.deleteDirect("key1")When storing data in DataStore with encryption enabled, all data types are serialized, as DataStore does not natively support encryption. Here's how the library handles different scenarios:
-- Without Encryption: If you use DataStore without encryption, only complex types (e.g., custom objects, collections) and Double values are serialized, as these require transformation for storage.
| Data Type | Supported Directly | Handled via gson Serialization |
|---|---|---|
| Boolean | Yes | No |
| Int | Yes | No |
| Float | Yes | No |
| Long | Yes | No |
| Double | No | Yes |
| String | Yes | No |
| Custom Objects (e.g., Data Classes) | No | Yes |
--With Encryption: If encryption is enabled, everything - regardless of type - is serialized and stored as an encrypted string to ensure data security.
| Data Type | Supported Directly | Handled via gson Serialization |
|---|---|---|
| Boolean | No | Yes |
| Int | No | Yes |
| Float | No | Yes |
| Long | No | Yes |
| Double | No | Yes |
| String | No | Yes |
| Custom Objects (e.g., Data Classes) | No | Yes |
This approach ensures that encrypted data remains secure, while unencrypted data is only serialized when necessary, optimizing performance and storage.
You can delete preferences using the delete function from inside the PersistManager or utilize the delete functions of the equivalent storage types like that:
// Delete a preference from both SharedPreferences and DataStore
persistManager.delete("key1")
// Delete a preference from SharedPreferences
persistManager.sharedPrefs.delete("key1")
// Delete a preference from DataStore Preferences using coroutines
CoroutineScope(Dispatchers.IO).launch {
persistManager.dataStorePrefs.delete("key1")
}
// Delete a preference from DataStore Preferences directly without coroutines
persistManager.dataStorePrefs.deleteDirect("key1")If you included the secure-persist-compose add-on module to the secure-persist module in your implementations then you are ready to utilize another zero-configuration secure persistence with state management.
SecurePersist seamlessly integrates with Jetpack Compose, enabling developers to persist and restore UI state effortlessly. By leveraging on more time Kotlin's property delegation, you can bind your Compose state variables to secure storage mechanisms, ensuring that your UI remains consistent across recompositions and app restarts.
Usage Example
Below is a code snippet demonstrating how to implement state persistence in a ViewModel using SecurePersist:
import androidx.lifecycle.ViewModel
import eu.anifantakis.lib.securepersist.PersistManager
import eu.anifantakis.lib.securepersist.compose.mutableStateOf
class LibCounterViewModel(
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(3000, encrypted = false))
private set
fun increment() {
count1++
count2++
count3++
}
}- Usage Scenario: When you want to store a sensitive value using the default secure storage mechanism.
- Behavior: Automatically uses the property name as the key and stores the value in
EncryptedSharedPreferences, ensuring that count1 is securely persisted without additional configuration.
- Usage Scenario: When you need to use a custom key and prefer DataStore with encryption for storing sensitive data.
- Behavior: Allows specifying a custom key (
counter2Key) and setsStoragetoDATA_STORE_ENCRYPTEDto specify DataStore with encryption usage. This is useful when you want to manage keys explicitly or need the benefits of DataStore over SharedPreferences.
- Usage Scenario: When storing non-sensitive data and prefer using DataStore without encryption for better performance or simplicity.
- Behavior: Uses the property name (
count3) as the key and setsStoragetoDATA_STOREto specify DataStore without encryption. This is ideal for scenarios where data security is not a primary concern, and you want to leverage DataStore's advantages like type safety and better asynchronous handling.
The library provides comprehensive support for complex data types through automatic serialization/deserialization:
These are data class instances. While Gson can serialize data classes that contain only base types (primitives and Strings), this library automatically handles additional types without having to provide a custom adapter class to handle the following types:
- Base Type Support
- Primitive Types:
BooleanIntLongFloatDoubleString
- Data classes containing any combination of the above types
- Additional Type Support
The library also automatically handles objects containing these advanced types:
Uri: Android URI objectsBigDecimal: Precise decimal numbersDate: Java Date objectsDuration: Kotlin Duration objectsUUID: Universally Unique IdentifiersPattern: Regular Expression patternsTimeZone: Java TimeZone objectsLocale: Language and region settingsCalendar: Java Calendar objects
The EncryptionManager can be used independently of PersistManager, offering robust functionality for encrypting and decrypting raw data, files, and complex objects.
It provides two flexible key management options:
-
KeyStore Integration: Leverages Android's
KeyStorefor hardware-backed encryption, where keys are securely managed within the device's hardware chip. -
External Keys: Allows you to generate and manage your own
SecretKeyoutside of KeyStore. This is useful if you need to store keys remotely, for instance, when encrypting data that will also be stored on a remote server.
// Simple initialization with default secure configuration
val encryptionManager = EncryptionManager(context, "your_key_alias")// Generate an external key with default secure configuration
val externalKey = EncryptionManager.generateExternalKey()
// Initialize with external key
val encryptionManager = EncryptionManager(context, externalKey)EncryptionManager provides secure encryption using Android's recommended security standards by default:
- Algorithm: AES (Advanced Encryption Standard)
- Mode: GCM (Galois/Counter Mode)
- Padding: No Padding (GCM handles it internally)
- Key Strength: 256-bit keys for strong encryption
This default configuration provides high security and is the same one used by Android's EncryptedSharedPreferences. You can use EncryptionManager with zero configuration and get this secure encryption automatically.
For advanced use cases, EncryptionManager also allows custom configurations via the EncryptionConfig class:
data class EncryptionConfig(
val keyAlgorithm: String = KeyProperties.KEY_ALGORITHM_AES,
val blockMode: BlockMode = BlockMode.GCM,
val encryptionPadding: String = KeyProperties.ENCRYPTION_PADDING_NONE,
val keySize: KeySize = KeySize.BITS_256,
val tagSize: TagSize = TagSize.BITS_128
)When needed, you can customize:
- Block Modes: GCM (default), CBC
- Key Sizes: 128, 192, 256 bits
- Padding (for CBC): PKCS7
- Tag Sizes (for GCM): 96, 104, 112, 120, 128 bits
Important Notes:
- By default,
EncryptionManageruses secure settings without requiring any configuration - Custom configurations are available as an option when using
EncryptionManagerdirectly for raw data or file encryption - When using custom configurations:
CBCmode requiresPKCS7paddingGCMmode requires no padding
PersistManageralways uses the default configuration to maintain compatibility withEncryptedSharedPreferences
// Initialize with default secure configuration
val encryptionManager = EncryptionManager(context, "your_key_alias")
// 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")When needed, you can specify custom encryption parameters:
// Create custom configuration
val customConfig = EncryptionConfig(
blockMode = BlockMode.CBC,
encryptionPadding = KeyProperties.ENCRYPTION_PADDING_PKCS7,
keySize = KeySize.BITS_128
)
// Initialize with custom config
val encryptionManager = EncryptionManager(context, "myKeyAlias", customConfig)
// All encryption operations will now use the custom configuration
val encryptedData = encryptionManager.encryptData("sensitive data")
val decryptedData = encryptionManager.decryptData(encryptedData)EncryptionManager supports external key management:
// Generate an external key (uses default secure configuration)
val externalKey = EncryptionManager.generateExternalKey()
// Create an EncryptionManager instance with the external key
val encryptionManager = EncryptionManager(context, externalKey)
// Or use external key for specific operations while maintaining a default instance
val defaultManager = EncryptionManager(context, "myKeyAlias")
val encryptedValue = EncryptionManager.encryptValue("valueToEncrypt", secretKey = externalKey)The library provides methods to safely convert SecretKeys to/from strings for storage or transmission:
// Exporting key
val originalKey = EncryptionManager.generateExternalKey()
val encodedKey: String = EncryptionManager.encodeSecretKey(originalKey)
// Importing key
val decodedKey: SecretKey = EncryptionManager.decodeSecretKey(encodedKey)EncryptionManager provides straightforward file encryption:
val encryptionManager = EncryptionManager(context, "your_key_alias")
// Encrypt a file
val inputFile = File(context.filesDir, "plain.txt")
encryptionManager.encryptFile(inputFile, "encrypted.dat")
// Decrypt a file
val decryptedContent = encryptionManager.decryptFile("encrypted.dat")
val decryptedText = String(decryptedContent)The file operations can also use custom configurations when needed:
// With custom configuration
val customConfig = EncryptionConfig(
blockMode = BlockMode.CBC,
encryptionPadding = KeyProperties.ENCRYPTION_PADDING_PKCS7
)
val encryptionManager = EncryptionManager(context, "your_key_alias", customConfig)
// File operations will use the custom configuration
encryptionManager.encryptFile(inputFile, "encrypted.dat")You can find extensive tests inside the androidTest folder for both the PersistManager and the EncryptionManager, providing more examples and ensuring reliability.
Contributions are welcome! Please open an issue or submit a pull request on GitHub.