
Enhances secure data storage by enabling encrypted DataStore creation using `EncryptedFile`. Includes migration guides and supports fallback AEAD for previously encrypted data.
Authenticated settings storage for Kotlin Multiplatform, with compatibility APIs for existing Android/JVM encrypted files.
[!WARNING] Special thanks to the original author for laying the foundation of this repository, your work continues to inspire. DataStore 1.3.0 now provides official AEAD encryption through
androidx.datastore:datastore-tinkandAeadSerializer. Existing users can continue using this library for streaming encryption and legacy encrypted files. Switching fromStreamingAeadto the official AEAD serializer requires an explicit data migration; the ciphertext formats are different.
The new encrypted-datastore-kmp module supports Android, JVM, iOS ARM64, iOS ARM64 simulator, and macOS ARM64. It uses native AES-256-GCM providers, a versioned authenticated record, public AndroidX Okio APIs, and explicit platform key creation/opening. Existing modules and ciphertext formats remain compatible.
This module has not been published. While developing from this repository, use:
commonMain.dependencies {
implementation(project(":encrypted-datastore-kmp"))
}Create Preferences storage in shared Kotlin code, supplying a persisted cipher, a private absolute path ending in .preferences_pb, and a background scope owned by the application:
import com.dayanruben.datastore.encrypted.kmp.*
import kotlinx.coroutines.CoroutineScope
import okio.Path
fun openSettings(cipher: DataStoreCipher, path: Path, scope: CoroutineScope) =
createEncryptedPreferencesDataStore(
cipher = cipher,
storeId = "application.settings.v1",
path = path,
scope = scope,
)On Android, provision once with createAndroidKeystoreCipher(alias) and reopen with openAndroidKeystoreCipher(alias). On Apple platforms, use createAppleKeychainCipher(service, account, accessGroup) and openAppleKeychainCipher(service, account, accessGroup) with the exact entitled group, including its signing prefix. On JVM, aesGcmCipher(key) imports a 32-byte key from the application's secure key management. Never recreate keys automatically after an open failure.
The default serialized limit is 1 MiB; maxPlaintextBytes accepts 1..16_777_216. The portable API authenticates a complete bounded record before deserialization. Typed RecordSerializer writes are checked before adapter buffering. Existing Okio serializers and Preferences use a compatibility bridge: the record limit does not cap their encoding/buffering allocations, so bound application values before storing them. Large streaming files can continue using the existing Tink API below. The new format requires an explicit migration from legacy files; keep the old path and keys until the migrated file has been reopened successfully.
See KMP usage and security, validation and benchmark evidence, and the implementation plan. Apple binary consumers should apply the provider's Swift linker configuration when Xcode uses a nonstandard path.
The following coordinates are the released Android/JVM compatibility APIs.
Add the dependency:
repositories {
mavenCentral()
google()
}
dependencies {
implementation("com.dayanruben:security-crypto-datastore:1.3.0-0.10")
// Or, if you want to use Preferences DataStore:
implementation("com.dayanruben:security-crypto-datastore-preferences:1.3.0-0.10")
}Android requirements: minSdk 24; compileSdk 37 or newer.
Dependencies:
[!NOTE] Ensure that the version of this library aligns with the DataStore library version used in your project.
To create an encrypted DataStore, simply replace the dataStore method with encryptedDataStore when setting up your delegate:
// At the top level of your Kotlin file:
val Context.settingsDataStore: DataStore<Settings> by encryptedDataStore(
fileName = "settings.pb",
serializer = SettingsSerializer
)EncryptedFile creationval settingsDataStore: DataStore<Settings> = DataStoreFactory.createEncrypted(SettingsSerializer) {
EncryptedFile.Builder(
context.dataStoreFile("settings.pb"),
context,
MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC),
EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
).build()
}Similarly, you can create Preferences DataStore:
// At the top level of your Kotlin file:
val Context.dataStore by encryptedPreferencesDataStore(name = "settings")EncryptedFile creationval dataStore: DataStore<Preferences> = PreferenceDataStoreFactory.createEncrypted {
EncryptedFile.Builder(
context.preferencesDataStoreFile("settings"),
context,
MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC),
EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
).build()
}Once your encrypted DataStore is configured, you can use it in the same manner as a regular DataStore. For usage guides and examples, refer to the DataStore documentation.
If you are starting with the following code:
val dataStore = DataStoreFactory.createEncrypted(serializer) {
EncryptedFile.Builder(
context.dataStoreFile("filename"),
context,
MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC),
EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
).build()
}To simplify the creation of DataStore using a delegate, follow these steps:
Context.DataStoreFactory.createEncrypted with encryptedDataStore.val Context.dataStore by encryptedDataStore(
fileName = "filename", // Keep file the same
serializer = serializer,
)[!NOTE] This only will be interchangeable if you have used
context.dataStoreFile(...)to create the datastore file. If you have custom logic for master key creation, ensure to pass the created master key as themasterKeyparameter to the delegate.
Change the dependency in build script:
dependencies {
- implementation("com.dayanruben:encrypted-datastore:...")
+ implementation("com.dayanruben:security-crypto-datastore:...")
}New library uses StreamingAead instead of Aead under the hood, so to not lose the previously encrypted data you should specify fallbackAead:
// This AEAD was used to encrypt DataStore previously, we will use it as fallback
val aead = AndroidKeysetManager.Builder()
.withSharedPref(context, "master_keyset", "master_key_preference")
.withKeyTemplate(KeyTemplates.get("AES256_GCM"))
.withMasterKeyUri("android-keystore://master_key")
.build()
.keysetHandle
.getPrimitive(Aead::class.java)The old code to create DataStore was looking like this:
val dataStore = DataStoreFactory.create(serializer.encrypted(aead)) {
context.dataStoreFile("filename")
}The new code will look like this:
// At the top level of your Kotlin file:
val Context.dataStore by encryptedDataStore(
fileName = "filename", // Keep file the same
serializer = serializer,
encryptionOptions = {
// Specify fallback Aead to make it possible to decrypt data encrypted with it
fallbackAead = aead
}
)EncryptedFile creationval dataStore = DataStoreFactory.createEncrypted(
serializer = serializer,
encryptionOptions = { fallbackAead = aead }
) {
EncryptedFile.Builder(
context.dataStoreFile("filename"), // Keep file the same
context,
MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC),
EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
).build()
}Authenticated settings storage for Kotlin Multiplatform, with compatibility APIs for existing Android/JVM encrypted files.
[!WARNING] Special thanks to the original author for laying the foundation of this repository, your work continues to inspire. DataStore 1.3.0 now provides official AEAD encryption through
androidx.datastore:datastore-tinkandAeadSerializer. Existing users can continue using this library for streaming encryption and legacy encrypted files. Switching fromStreamingAeadto the official AEAD serializer requires an explicit data migration; the ciphertext formats are different.
The new encrypted-datastore-kmp module supports Android, JVM, iOS ARM64, iOS ARM64 simulator, and macOS ARM64. It uses native AES-256-GCM providers, a versioned authenticated record, public AndroidX Okio APIs, and explicit platform key creation/opening. Existing modules and ciphertext formats remain compatible.
This module has not been published. While developing from this repository, use:
commonMain.dependencies {
implementation(project(":encrypted-datastore-kmp"))
}Create Preferences storage in shared Kotlin code, supplying a persisted cipher, a private absolute path ending in .preferences_pb, and a background scope owned by the application:
import com.dayanruben.datastore.encrypted.kmp.*
import kotlinx.coroutines.CoroutineScope
import okio.Path
fun openSettings(cipher: DataStoreCipher, path: Path, scope: CoroutineScope) =
createEncryptedPreferencesDataStore(
cipher = cipher,
storeId = "application.settings.v1",
path = path,
scope = scope,
)On Android, provision once with createAndroidKeystoreCipher(alias) and reopen with openAndroidKeystoreCipher(alias). On Apple platforms, use createAppleKeychainCipher(service, account, accessGroup) and openAppleKeychainCipher(service, account, accessGroup) with the exact entitled group, including its signing prefix. On JVM, aesGcmCipher(key) imports a 32-byte key from the application's secure key management. Never recreate keys automatically after an open failure.
The default serialized limit is 1 MiB; maxPlaintextBytes accepts 1..16_777_216. The portable API authenticates a complete bounded record before deserialization. Typed RecordSerializer writes are checked before adapter buffering. Existing Okio serializers and Preferences use a compatibility bridge: the record limit does not cap their encoding/buffering allocations, so bound application values before storing them. Large streaming files can continue using the existing Tink API below. The new format requires an explicit migration from legacy files; keep the old path and keys until the migrated file has been reopened successfully.
See KMP usage and security, validation and benchmark evidence, and the implementation plan. Apple binary consumers should apply the provider's Swift linker configuration when Xcode uses a nonstandard path.
The following coordinates are the released Android/JVM compatibility APIs.
Add the dependency:
repositories {
mavenCentral()
google()
}
dependencies {
implementation("com.dayanruben:security-crypto-datastore:1.3.0-0.10")
// Or, if you want to use Preferences DataStore:
implementation("com.dayanruben:security-crypto-datastore-preferences:1.3.0-0.10")
}Android requirements: minSdk 24; compileSdk 37 or newer.
Dependencies:
[!NOTE] Ensure that the version of this library aligns with the DataStore library version used in your project.
To create an encrypted DataStore, simply replace the dataStore method with encryptedDataStore when setting up your delegate:
// At the top level of your Kotlin file:
val Context.settingsDataStore: DataStore<Settings> by encryptedDataStore(
fileName = "settings.pb",
serializer = SettingsSerializer
)EncryptedFile creationval settingsDataStore: DataStore<Settings> = DataStoreFactory.createEncrypted(SettingsSerializer) {
EncryptedFile.Builder(
context.dataStoreFile("settings.pb"),
context,
MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC),
EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
).build()
}Similarly, you can create Preferences DataStore:
// At the top level of your Kotlin file:
val Context.dataStore by encryptedPreferencesDataStore(name = "settings")EncryptedFile creationval dataStore: DataStore<Preferences> = PreferenceDataStoreFactory.createEncrypted {
EncryptedFile.Builder(
context.preferencesDataStoreFile("settings"),
context,
MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC),
EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
).build()
}Once your encrypted DataStore is configured, you can use it in the same manner as a regular DataStore. For usage guides and examples, refer to the DataStore documentation.
If you are starting with the following code:
val dataStore = DataStoreFactory.createEncrypted(serializer) {
EncryptedFile.Builder(
context.dataStoreFile("filename"),
context,
MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC),
EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
).build()
}To simplify the creation of DataStore using a delegate, follow these steps:
Context.DataStoreFactory.createEncrypted with encryptedDataStore.val Context.dataStore by encryptedDataStore(
fileName = "filename", // Keep file the same
serializer = serializer,
)[!NOTE] This only will be interchangeable if you have used
context.dataStoreFile(...)to create the datastore file. If you have custom logic for master key creation, ensure to pass the created master key as themasterKeyparameter to the delegate.
Change the dependency in build script:
dependencies {
- implementation("com.dayanruben:encrypted-datastore:...")
+ implementation("com.dayanruben:security-crypto-datastore:...")
}New library uses StreamingAead instead of Aead under the hood, so to not lose the previously encrypted data you should specify fallbackAead:
// This AEAD was used to encrypt DataStore previously, we will use it as fallback
val aead = AndroidKeysetManager.Builder()
.withSharedPref(context, "master_keyset", "master_key_preference")
.withKeyTemplate(KeyTemplates.get("AES256_GCM"))
.withMasterKeyUri("android-keystore://master_key")
.build()
.keysetHandle
.getPrimitive(Aead::class.java)The old code to create DataStore was looking like this:
val dataStore = DataStoreFactory.create(serializer.encrypted(aead)) {
context.dataStoreFile("filename")
}The new code will look like this:
// At the top level of your Kotlin file:
val Context.dataStore by encryptedDataStore(
fileName = "filename", // Keep file the same
serializer = serializer,
encryptionOptions = {
// Specify fallback Aead to make it possible to decrypt data encrypted with it
fallbackAead = aead
}
)EncryptedFile creationval dataStore = DataStoreFactory.createEncrypted(
serializer = serializer,
encryptionOptions = { fallbackAead = aead }
) {
EncryptedFile.Builder(
context.dataStoreFile("filename"), // Keep file the same
context,
MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC),
EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
).build()
}