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
SharedPreferences
andDataStore
. -
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
MutableState
delegates 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
EncryptionManager
for 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
MutableState
delegates 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.gradle
for 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-compose
dependency.
SecurePersist is modularized to provide flexibility based on your project's requirements:
-
secure-persist
(Core Library):- Purpose: Provides secure data persistence capabilities using
SharedPreferences
andDataStore
. - 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
put
operations are non-blocking, ensuring efficient data storage. However, retrieving preferences withget
is 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
key
isnull
or 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:
-
sharedPrefs
specialized for encrypted preferences via EncryptedSharedPreferences -
dataStorePrefs
specialized for preferences via DataStore with encryption via theEncryptionManager
class 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
EncryptionManager
provides 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
DataStore
preferences 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:
put
get
delete
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
Direct
versions 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 implementation
s 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 setsStorage
toDATA_STORE_ENCRYPTED
to 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 setsStorage
toDATA_STORE
to 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:
Boolean
Int
Long
Float
Double
String
- 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
KeyStore
for hardware-backed encryption, where keys are securely managed within the device's hardware chip. -
External Keys: Allows you to generate and manage your own
SecretKey
outside 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,
EncryptionManager
uses secure settings without requiring any configuration - Custom configurations are available as an option when using
EncryptionManager
directly for raw data or file encryption - When using custom configurations:
CBC
mode requiresPKCS7
paddingGCM
mode requires no padding
PersistManager
always 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.