Effortlessly encrypts and persists data using hardware-backed security, offering one-code-path simplicity, seamless integration, and protection for sensitive information like OAuth tokens.
KSafe in action across multiple scenarios: Demo CMP App Using KSafe.
From the author and the community:
| Author's Video | Philipp Lackner's Video | Jimmy Plazas's Video |
|---|---|---|
| KSafe - Kotlin Multiplatform Encrypted DataStore Persistence Library | How to Encrypt Local Preferences In KMP With KSafe | Encripta datos localmente en Kotlin Multiplatform con KSafe - Ejemplo + Arquitectura |
KSafe is a secure-by-default Kotlin Multiplatform key/value persistence library.
It lets you persist ordinary Kotlin variables, Compose MutableState, and MutableStateFlow across app restarts using one API across Android, iOS, JVM/Desktop, WASM, and Kotlin/JS.
Encrypted storage is the default. Plain storage is available per entry with mode = KSafeWriteMode.Plain.
Fast. Easy. Synchronous or asynchronous. Encrypted or plain.
KSafe gives you a complete persistence layer for Kotlin Multiplatform: property delegates or coroutines, encrypted or plain storage, hot in-memory reads, atomic DataStore writes, automatic serialization, and one API across platforms.
ksafe-biometrics module so apps that don't need it pay nothing.KSafe brings together two things that are usually separate in Kotlin Multiplatform:
Use it as a general-purpose persistence layer for settings, preferences, app state, Compose MutableState, and MutableStateFlow.
mode = KSafeWriteMode.Plain.This gives you the best of both worlds: the simplicity of ordinary key/value storage and the protection of a secure storage layer, without changing APIs or rewriting your persistence model.
var counter by ksafe(0)
counter++ // auto-encrypted (AES-256-GCM), auto-persisted, survives process deathRead and write it like any normal Kotlin variable — no suspend, no runBlocking, no DataStore boilerplate, no explicit encrypt/decrypt. Reads hit a hot in-memory cache (~0.007ms); writes encrypt and flush in the background.
var counter by ksafe(0, mode = KSafeWriteMode.Plain)One argument change and you have the simplicity of SharedPreferences / NSUserDefaults — multiplatform, type-safe, object-aware, backed by atomic DataStore writes.
Every persistence shape you reach for, with the same guarantees behind each:
// 1. Plain property delegate — no Compose, no Flow, no coroutines required
var token by ksafe("")
// 2. Compose MutableState (ViewModel / class field) — reactive UI, persisted, encrypted
var username by ksafe.mutableStateOf("Guest")
// 3. Compose MutableState (inside a @Composable body) — rememberSaveable, but persists across app restarts
// No ViewModel required for trivial UI state like this. Storage key auto-resolves to the property name.
@Composable
fun TabbedScreen(ksafe: KSafe) {
var currentTab by ksafe.rememberKSafeState(Tab.Home)
}
// 4. Read-only StateFlow — observe from anywhere, writes go through ksafe.put()
val user: StateFlow<User> by ksafe.asStateFlow(User(), viewModelScope)
// 5. Read/write MutableStateFlow — the classic _state / state pattern, persisted
private val _state by ksafe.asMutableStateFlow(MoviesState(), viewModelScope)
val state = _state.asStateFlow()
// 6. Read-only Flow alone — the simplest reactive shape; pair with a plain `ksafe(...)` delegate
// when you also need to write back to the same key (two bindings, kept in sync by the matching key)
val theme: Flow<ThemeMode> by ksafe.asFlow(ThemeMode.DEVICE, key = "theme")
private var themeWriter: ThemeMode by ksafe(ThemeMode.DEVICE, key = "theme")
fun setTheme(mode: ThemeMode) { themeWriter = mode } // collectors of `theme` see the new value
// 7. Read/write Flow — collapses (6) into a single declaration: one binding instead of two,
// no scope needed, no key sync to keep right
val theme: WritableKSafeFlow<ThemeMode> by ksafe.asWritableFlow(ThemeMode.DEVICE)
theme.set(ThemeMode.NIGHT) // persists; collectors see it on the next emissionAll seven survive process death, are AES-256-GCM encrypted by default (except rememberKSafeState, which defaults to plain since it's typically UI ephemera — pass mode = KSafeWriteMode.Encrypted(...) to opt in), and can be made plain with mode = KSafeWriteMode.Plain. Zero boilerplate, on every target.
DataStore without the coroutines tax. The property delegate,
mutableStateOf,rememberKSafeState, andgetDirect/putDirectare fully synchronous — but never blocking. Reads come from a hot in-memory cache; writes update the cache immediately and enqueue the encrypt-and-flush onto a background thread. Call sites return instantly. Use thesuspendAPI (get/put) only when you want to.
// inside any coroutine / suspend function
ksafe.put("profile", user) // awaits the disk flush
val profile: User = ksafe.get("profile", User())Same encryption, same cache, same DataStore — just an API shape that awaits the write instead of enqueueing it. Reach for this when you want a guaranteed flush (payments, critical writes) or when the call site is already a coroutine.
KSafe isn't just for key/value pairs — it's the simplest way to bootstrap an encrypted SQLCipher / SQLDelight / Room database too:
// Generates a 256-bit secret on first call, returns the same one thereafter.
// Stored hardware-isolated (StrongBox on Android, Secure Enclave on iOS).
val passphrase = ksafe.getOrCreateSecret("main.db")
Room.databaseBuilder(context, AppDatabase::class.java, "main.db")
.openHelperFactory(SupportFactory(passphrase))
.build()One line replaces: secure random generation, hardware-backed key storage, persistence, and retrieval.
Ktor bearer authentication with zero encryption boilerplate:
@Serializable
data class AuthTokens(val accessToken: String = "", val refreshToken: String = "")
// One line to encrypt, persist, and serialize the whole object — that's it.
var tokens by ksafe(AuthTokens())
install(Auth) {
bearer {
loadTokens {
// Reads atomic object from hot cache (~0.007ms). No disk. No suspend.
BearerTokens(tokens.accessToken, tokens.refreshToken)
}
refreshTokens {
val newInfo = api.refreshAuth(tokens.refreshToken)
// Atomic update: encrypts & persists as JSON in background (~13μs)
tokens = AuthTokens(newInfo.accessToken, newInfo.refreshToken)
BearerTokens(tokens.accessToken, tokens.refreshToken)
}
}
}Under the hood, each platform uses its native crypto engine — Android Keystore, iOS Keychain + CryptoKit, JVM's javax.crypto, browser WebCrypto (on both Kotlin/WASM and Kotlin/JS) — unified behind one API. Values are AES-256-GCM encrypted and persisted to DataStore (localStorage on the web targets). Cross-screen sync (scope =), biometric auth, memory policies, and runtime security detection are all built in.
// 1. Create instance (Android needs context, others don't)
val ksafe = KSafe(context) // Android
val ksafe = KSafe() // iOS / JVM / WASM / JS / JS
// 2. Store & retrieve with property delegation
var counter by ksafe(0)
counter++ // Auto-encrypted, auto-persisted
// 3. Compose state (read/write, reactive to external changes)
var username by ksafe.mutableStateOf("Guest", scope = viewModelScope)
// 4. Reactive flows — read-only StateFlow, read/write MutableStateFlow, or read/write WritableKSafeFlow (no scope)
val user: StateFlow<User> by ksafe.asStateFlow(User(), viewModelScope)
private val _state by ksafe.asMutableStateFlow(UiState(), viewModelScope)
val state = _state.asStateFlow()
val themeMode: WritableKSafeFlow<ThemeMode> by ksafe.asWritableFlow(ThemeMode.DEVICE)
// themeMode.set(ThemeMode.NIGHT)
// 5. Or use suspend API
viewModelScope.launch {
ksafe.put("user_token", token)
val token = ksafe.get("user_token", "")
}
// 6. Protect actions with biometrics (optional — add `ksafe-biometrics`)
KSafeBiometrics.verifyBiometricDirect("Confirm payment") { success ->
if (success) processPayment()
}Data is now AES-256-GCM encrypted — keys in Android Keystore, iOS Keychain, software-backed on JVM, WebCrypto on the browser targets (Kotlin/WASM and Kotlin/JS).
// commonMain or Android-only build.gradle(.kts)
implementation("eu.anifantakis:ksafe:2.0.0-RC2")
implementation("eu.anifantakis:ksafe-compose:2.0.0-RC2") // ← Compose state (optional)
implementation("eu.anifantakis:ksafe-biometrics:2.0.0-RC2") // ← Biometric auth (optional)Skip
ksafe-composeif you don't use Jetpack Compose ormutableStateOfpersistence.Skip
ksafe-biometricsif you don't need Face ID / Touch ID / Fingerprint verification. The biometrics module is fully independent — it has no dependency on:ksafeand can be used on its own to protect any action in your app.
Note:
kotlinx-serialization-jsoncomes in transitively — don't add it yourself.
Required only if you store @Serializable data classes. Add it to libs.versions.toml:
[versions]
kotlin = "2.2.21"
[plugins]
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }then apply it in build.gradle.kts:
plugins {
//...
alias(libs.plugins.kotlin.serialization)
}// Android
val ksafe = KSafe(context)
// iOS / JVM / WASM / JS
val ksafe = KSafe()With Koin (recommended for KMP):
// Android
actual val platformModule = module {
single { KSafe(androidApplication()) }
}
// iOS / JVM / WASM / JS
actual val platformModule = module {
single { KSafe() }
}Multi-instance setups, web awaitCacheReady() (wasmJs + js), full per-platform Koin examples, the custom storage directory option (baseDir on JVM/Android, directory on iOS — for example to align with $XDG_DATA_HOME or noBackupFilesDir), and the optional KSafe.close() for apps that re-create instances mid-process: docs/SETUP.md.
A handful of examples cover 95% of real-world use. Full reference (Compose policy, cross-screen sync, write modes, nullables, deletion, full ViewModel): docs/USAGE.md.
// 1. Property delegate — synchronous, non-blocking, encrypted, persisted
var counter by ksafe(0)
counter++
// 2. Compose state on a ViewModel / class field — reactive UI + persistence (requires ksafe-compose)
var username by ksafe.mutableStateOf("Guest")
// 3. Compose state inside a @Composable body — the rememberSaveable analogue, but persists across app restarts
// var currentTab by ksafe.rememberKSafeState(Tab.Home) // key auto-resolves to "currentTab"; no ViewModel needed
// 4. Reactive flows — read-only StateFlow, read/write MutableStateFlow, or read/write Flow without a scope
val user: StateFlow<User> by ksafe.asStateFlow(User(), viewModelScope) // read-only
private val _state by ksafe.asMutableStateFlow(MoviesState(), viewModelScope) // read/write, hot
val state = _state.asStateFlow()
val themeMode: WritableKSafeFlow<ThemeMode> by ksafe.asWritableFlow(ThemeMode.DEVICE) // read/write, cold; set() to write
// 4. Suspend API — when you want to await the disk flush
viewModelScope.launch {
ksafe.put("profile", user)
val loaded: User = ksafe.get("profile", User())
}
// 5. Direct API — non-suspend, hot-cache reads, background-flushed writes (~1000x faster for bulk ops)
ksafe.putDirect("counter", 42)
val n = ksafe.getDirect("counter", 0)Per-entry plain / encrypted toggle via KSafeWriteMode:
var theme by ksafe("light", mode = KSafeWriteMode.Plain)
ksafe.putDirect(
"pin", pin,
mode = KSafeWriteMode.Encrypted(
protection = KSafeEncryptedProtection.HARDWARE_ISOLATED,
requireUnlockedDevice = true
)
)Complex objects — just mark them @Serializable; JSON and encryption are automatic:
@Serializable
data class AuthInfo(val accessToken: String = "", val refreshToken: String = "")
var authInfo by ksafe(AuthInfo())
authInfo = authInfo.copy(accessToken = "newToken")Note: The property delegate works only with the default KSafe instance. For named instances, use the suspend or direct APIs — see docs/SETUP.md.
For third-party types you can't annotate (UUID, Instant, BigDecimal…), register a KSerializer via KSafeConfig(json = customJson) and use @Contextual fields at the call site. Full walkthrough: docs/SERIALIZATION.md.
Two small cross-platform helpers:
import eu.anifantakis.lib.ksafe.internal.secureRandomBytes
// Secure random bytes (SecureRandom / arc4random_buf / WebCrypto)
val nonce = secureRandomBytes(16)
// Generate-or-retrieve a hardware-isolated 256-bit secret (great for DB passphrases)
val passphrase = ksafe.getOrCreateSecret("main.db")
secureRandomByteslives undereu.anifantakis.lib.ksafe.internal— it's the same primitive KSafe uses internally, exposed for app code that needs a CSPRNG.
Sizes, protection tiers, Room + SQLCipher / SQLDelight examples: docs/SECURITY.md#cryptographic-utilities.
KSafeWriteMode + KSafeEncryptedProtection tiersvar launchCount by ksafe(0), that is literally it| Feature | SharedPrefs | DataStore | multiplatform-settings | KVault | KSafe |
|---|---|---|---|---|---|
| Thread safety | ❌ ANRs possible | ✅ Coroutine-safe | ✅ Platform-native | ✅ Thread-safe | ✅ ConcurrentHashMap + coroutines |
| Type safety | ❌ Runtime crashes | ✅ Compile-time | ✅ Generic API | ✅ Generic API | ✅ Reified generics + serialization |
| Data corruption | ❌ Crash = data loss | ✅ Atomic | ❌ Platform-dependent | ✅ Atomic | ✅ Uses DataStore atomicity |
| API style | ❌ Callbacks | ✅ Flow | ✅ Sync | ✅ Sync | ✅ Both sync & async |
| Encryption | ❌ None | ❌ None | ❌ None | ✅ Hardware-backed | ✅ Hardware-backed |
| Cross-platform | ❌ Android only | ❌ Android only | ✅ KMP | ✅ KMP | ✅ Android/iOS/JVM/WASM/JS |
| Nullable support | ❌ No | ❌ No | ✅ Primitives (*OrNull getters) |
✅ Primitives | ✅ Primitives + objects + delegates * |
| Complex types | ❌ Manual | ❌ Manual/Proto | ❌ Manual | ❌ Manual | ✅ Auto-serialization |
| Biometric auth | ❌ Manual | ❌ Manual | ❌ Manual | ❌ Manual | ✅ Built-in |
| Memory policy | N/A | N/A | N/A | N/A | ✅ 3 policies (PLAIN_TEXT / ENCRYPTED / TIMED_CACHE) |
| Hot cache | ✅ Synchronized HashMap
|
❌ No (Flow only) | ✅ Platform-native cache | ❌ No | ✅ ConcurrentHashMap + optimistic writes |
| Write batching | ❌ No | ❌ No | ❌ No | ❌ No | ✅ 16ms coalescing |
* "Primitives + objects + delegates" — nullability flows uniformly through every API shape and every type KSafe stores, not just scalar getters. Specifically:
- Primitives —
Boolean?,Int?,Long?,Float?,Double?,String?all round-trip throughget/put/getDirect/putDirect/getFlow.nullis a distinct state, not "missing"; it's preserved on reads and persisted on writes.ksafe.putDirect("nickname", null as String?) // stored as null, not "default" val nickname: String? = ksafe.getDirect("nickname", "Guest") // returns null, NOT "Guest"- Objects — any
@Serializableclass can be stored nullably.nullround-trips through the same encrypted JSON path as the payload; no extra boilerplate.@Serializable data class User(val id: String, val name: String) var currentUser: User? by ksafe(null) // encrypted, persisted, nullable currentUser = User("u1", "Alice") // -> encrypted JSON currentUser = null // -> null sentinel, round-trips- Delegates — every delegate shape accepts a nullable default, including Compose state and
MutableStateFlow. The persisted null survives process death and emits correctly through Flow observers.var token: String? by ksafe(null) // plain delegate var profile: User? by ksafe.mutableStateOf(null) // Compose state val user: StateFlow<User?> by ksafe.asStateFlow(null, scope) // read-only StateFlow private val _state by ksafe.asMutableStateFlow<User?>(null, scope) // read/write MutableStateFlow val themeMode: WritableKSafeFlow<ThemeMode?> by ksafe.asWritableFlow(null) // read/write Flow, no scopeBy contrast,
multiplatform-settingsexposes nullability only through separategetStringOrNull/getIntOrNullscalar getters — there is no nullable support for custom types, property delegates, or Compose state, because the library has no serialization or delegate layer. KVault is similar: nullable return types on its primitive getters, no object or delegate support.
| API | Read | Write | Best For |
|---|---|---|---|
getDirect/putDirect
|
0.007 ms | 0.022 ms | UI, bulk ops, high throughput |
get/put (suspend) |
0.010 ms | 22 ms | When you must guarantee persistence |
vs competitors (encrypted): 14× faster reads than KVault, 15× faster than EncryptedSharedPreferences. Unencrypted writes match SharedPreferences.
Measured on representative Android hardware under a synthetic but realistic workload. Real-world numbers depend on device, workload, and data size — see docs/BENCHMARKS.md for the methodology, full tables, cold-start numbers, and architecture notes.
| Platform | Minimum Version | Notes |
|---|---|---|
| Android | API 24 (Android 7.0) | Hardware-backed Keystore on supported devices |
| iOS | iOS 13+ | Keychain-backed symmetric keys (protected by device passcode) |
| JVM/Desktop | JDK 11+ | Software-backed encryption |
| Kotlin/WASM (Browser) | Browsers with WasmGC (Chrome 119+, Firefox 120+, Safari 18+) | WebCrypto API + localStorage |
| Kotlin/JS (Browser) | Any modern browser | WebCrypto API + localStorage — use this for older browsers or pre-existing JS builds |
| Dependency | Tested Version |
|---|---|
| Kotlin | 2.0.0+ |
| Kotlin Coroutines | 1.8.0+ |
| DataStore Preferences | 1.1.0+ |
| Compose Multiplatform | 1.6.0+ (for ksafe-compose) |
A standalone biometric helper (Android + iOS) that can gate any action in your app — not just KSafe ops. Ships as the optional :ksafe-biometrics artifact and depends on nothing else from KSafe, so apps that need only biometric verification can use it on its own.
Static API. No instance, no DI wiring, no Context parameter. On Android the library auto-initializes via a ContentProvider declared in its merged manifest (the same pattern WorkManager / Firebase use), so consumers don't need to touch their Application class.
// Same call shape on every platform — Android, iOS, JVM, web.
// Callback-based
KSafeBiometrics.verifyBiometricDirect("Authenticate to increment") { success ->
if (success) secureCounter++
}
// Suspend-based
if (KSafeBiometrics.verifyBiometric("Authenticate to increment")) {
secureCounter++
}Auth caching, scoped sessions, platform setup, complete examples: docs/BIOMETRICS.md.
Migrating from KSafe ≤1.x? Biometric methods used to live on
KSafeitself. In 2.0 they moved to a separate module. Addimplementation("eu.anifantakis:ksafe-biometrics:2.0.0-RC2"), changeimport eu.anifantakis.lib.ksafe.BiometricAuthorizationDuration→import eu.anifantakis.lib.ksafe.biometrics.BiometricAuthorizationDuration, replaceksafe.verifyBiometric(...)withKSafeBiometrics.verifyBiometric(...). Method names and signatures are unchanged. No instance to construct, no DI wiring needed.
Detect and respond to runtime threats — root/jailbreak, debugger, emulator, debug builds:
val ksafe = KSafe(
context = context,
securityPolicy = KSafeSecurityPolicy(
rootedDevice = SecurityAction.WARN, // IGNORE, WARN, or BLOCK
debuggerAttached = SecurityAction.BLOCK,
debugBuild = SecurityAction.WARN,
emulator = SecurityAction.IGNORE,
onViolation = { violation ->
analytics.log("Security: ${violation.name}")
}
)
)Preset policies, BLOCK exception handling, Compose stability, detection methods: docs/SECURITY.md.
Trade off performance vs. security for data in RAM:
val ksafe = KSafe(
fileName = "secrets",
memoryPolicy = KSafeMemoryPolicy.ENCRYPTED // Default
)| Policy | Best For | RAM Contents | Read Cost | Security |
|---|---|---|---|---|
PLAIN_TEXT |
User settings, themes | Plaintext (forever) | O(1) lookup | Low — all data exposed in memory |
ENCRYPTED (Default) |
Tokens, passwords | Ciphertext only | AES-GCM decrypt every read | High — nothing plaintext in RAM |
ENCRYPTED_WITH_TIMED_CACHE |
Compose/SwiftUI screens | Ciphertext + short-lived plaintext | First read decrypts, then O(1) for TTL | Medium — plaintext only for recently-accessed keys, only for seconds |
Timed cache details, constructor params, lock-state policies, multi-instance lock policies: docs/MEMORY.md.
Internals, advanced features, reference material:
| Topic | Description |
|---|---|
| Complete Usage Guide | Every API shape: delegates, flow delegates, Compose state, suspend/direct APIs, write modes, nullables, full ViewModel |
| Setup with Koin | Multi-instance setups (prefs vs vault), web awaitCacheReady() (wasmJs + js), full platform examples, custom storage directory (baseDir / directory) |
| Custom JSON Serialization | Registering KSerializers for UUID, Instant, and other third-party types |
| Performance Benchmarks | Full benchmark tables, cold start numbers, architecture deep-dive |
| Biometric Authentication | Authorization caching, scoped sessions, platform setup, complete examples |
| Security | Runtime security policy, encryption internals, threat model, hardware isolation, key storage queries, crypto utilities |
| Encryption Proof | Per-platform automated proof tests + manual commands to inspect the raw stored bytes and see the ciphertext yourself |
| Memory Policy | Timed cache, constructor parameters, encryption config, device lock-state policies |
| Architecture | The conceptual model: three modules, three rings (public API / KSafeCore orchestrator / platform shells), hot cache + write coalescer, the KSafePlatformStorage and KSafeEncryption interfaces, memory policies, and how 2.0 consolidated ~5,900 lines of duplicated platform logic into ~890 |
| Source-tree tour | File-by-file walkthrough of every Kotlin source file in :ksafe: where each behaviour lives and why. Companion to the Architecture doc — Architecture is "the model," TOUR is "the map." |
| Testing | Running tests, building iOS test app, test features |
| Migration Guide | Upgrading from v1.x → v2.0 (biometric module extraction, iOS path migration), v1.6.x → v1.7.0 (encrypted: Boolean → KSafeWriteMode), and v1.1.x → v1.2.0+ |
| Alternatives & Comparison | KSafe vs EncryptedSharedPrefs, KVault, SQLCipher, and more |
Licensed under the Apache License 2.0 — see http://www.apache.org/licenses/LICENSE-2.0. Distributed "AS IS", without warranties of any kind.
KSafe in action across multiple scenarios: Demo CMP App Using KSafe.
From the author and the community:
| Author's Video | Philipp Lackner's Video | Jimmy Plazas's Video |
|---|---|---|
| KSafe - Kotlin Multiplatform Encrypted DataStore Persistence Library | How to Encrypt Local Preferences In KMP With KSafe | Encripta datos localmente en Kotlin Multiplatform con KSafe - Ejemplo + Arquitectura |
KSafe is a secure-by-default Kotlin Multiplatform key/value persistence library.
It lets you persist ordinary Kotlin variables, Compose MutableState, and MutableStateFlow across app restarts using one API across Android, iOS, JVM/Desktop, WASM, and Kotlin/JS.
Encrypted storage is the default. Plain storage is available per entry with mode = KSafeWriteMode.Plain.
Fast. Easy. Synchronous or asynchronous. Encrypted or plain.
KSafe gives you a complete persistence layer for Kotlin Multiplatform: property delegates or coroutines, encrypted or plain storage, hot in-memory reads, atomic DataStore writes, automatic serialization, and one API across platforms.
ksafe-biometrics module so apps that don't need it pay nothing.KSafe brings together two things that are usually separate in Kotlin Multiplatform:
Use it as a general-purpose persistence layer for settings, preferences, app state, Compose MutableState, and MutableStateFlow.
mode = KSafeWriteMode.Plain.This gives you the best of both worlds: the simplicity of ordinary key/value storage and the protection of a secure storage layer, without changing APIs or rewriting your persistence model.
var counter by ksafe(0)
counter++ // auto-encrypted (AES-256-GCM), auto-persisted, survives process deathRead and write it like any normal Kotlin variable — no suspend, no runBlocking, no DataStore boilerplate, no explicit encrypt/decrypt. Reads hit a hot in-memory cache (~0.007ms); writes encrypt and flush in the background.
var counter by ksafe(0, mode = KSafeWriteMode.Plain)One argument change and you have the simplicity of SharedPreferences / NSUserDefaults — multiplatform, type-safe, object-aware, backed by atomic DataStore writes.
Every persistence shape you reach for, with the same guarantees behind each:
// 1. Plain property delegate — no Compose, no Flow, no coroutines required
var token by ksafe("")
// 2. Compose MutableState (ViewModel / class field) — reactive UI, persisted, encrypted
var username by ksafe.mutableStateOf("Guest")
// 3. Compose MutableState (inside a @Composable body) — rememberSaveable, but persists across app restarts
// No ViewModel required for trivial UI state like this. Storage key auto-resolves to the property name.
@Composable
fun TabbedScreen(ksafe: KSafe) {
var currentTab by ksafe.rememberKSafeState(Tab.Home)
}
// 4. Read-only StateFlow — observe from anywhere, writes go through ksafe.put()
val user: StateFlow<User> by ksafe.asStateFlow(User(), viewModelScope)
// 5. Read/write MutableStateFlow — the classic _state / state pattern, persisted
private val _state by ksafe.asMutableStateFlow(MoviesState(), viewModelScope)
val state = _state.asStateFlow()
// 6. Read-only Flow alone — the simplest reactive shape; pair with a plain `ksafe(...)` delegate
// when you also need to write back to the same key (two bindings, kept in sync by the matching key)
val theme: Flow<ThemeMode> by ksafe.asFlow(ThemeMode.DEVICE, key = "theme")
private var themeWriter: ThemeMode by ksafe(ThemeMode.DEVICE, key = "theme")
fun setTheme(mode: ThemeMode) { themeWriter = mode } // collectors of `theme` see the new value
// 7. Read/write Flow — collapses (6) into a single declaration: one binding instead of two,
// no scope needed, no key sync to keep right
val theme: WritableKSafeFlow<ThemeMode> by ksafe.asWritableFlow(ThemeMode.DEVICE)
theme.set(ThemeMode.NIGHT) // persists; collectors see it on the next emissionAll seven survive process death, are AES-256-GCM encrypted by default (except rememberKSafeState, which defaults to plain since it's typically UI ephemera — pass mode = KSafeWriteMode.Encrypted(...) to opt in), and can be made plain with mode = KSafeWriteMode.Plain. Zero boilerplate, on every target.
DataStore without the coroutines tax. The property delegate,
mutableStateOf,rememberKSafeState, andgetDirect/putDirectare fully synchronous — but never blocking. Reads come from a hot in-memory cache; writes update the cache immediately and enqueue the encrypt-and-flush onto a background thread. Call sites return instantly. Use thesuspendAPI (get/put) only when you want to.
// inside any coroutine / suspend function
ksafe.put("profile", user) // awaits the disk flush
val profile: User = ksafe.get("profile", User())Same encryption, same cache, same DataStore — just an API shape that awaits the write instead of enqueueing it. Reach for this when you want a guaranteed flush (payments, critical writes) or when the call site is already a coroutine.
KSafe isn't just for key/value pairs — it's the simplest way to bootstrap an encrypted SQLCipher / SQLDelight / Room database too:
// Generates a 256-bit secret on first call, returns the same one thereafter.
// Stored hardware-isolated (StrongBox on Android, Secure Enclave on iOS).
val passphrase = ksafe.getOrCreateSecret("main.db")
Room.databaseBuilder(context, AppDatabase::class.java, "main.db")
.openHelperFactory(SupportFactory(passphrase))
.build()One line replaces: secure random generation, hardware-backed key storage, persistence, and retrieval.
Ktor bearer authentication with zero encryption boilerplate:
@Serializable
data class AuthTokens(val accessToken: String = "", val refreshToken: String = "")
// One line to encrypt, persist, and serialize the whole object — that's it.
var tokens by ksafe(AuthTokens())
install(Auth) {
bearer {
loadTokens {
// Reads atomic object from hot cache (~0.007ms). No disk. No suspend.
BearerTokens(tokens.accessToken, tokens.refreshToken)
}
refreshTokens {
val newInfo = api.refreshAuth(tokens.refreshToken)
// Atomic update: encrypts & persists as JSON in background (~13μs)
tokens = AuthTokens(newInfo.accessToken, newInfo.refreshToken)
BearerTokens(tokens.accessToken, tokens.refreshToken)
}
}
}Under the hood, each platform uses its native crypto engine — Android Keystore, iOS Keychain + CryptoKit, JVM's javax.crypto, browser WebCrypto (on both Kotlin/WASM and Kotlin/JS) — unified behind one API. Values are AES-256-GCM encrypted and persisted to DataStore (localStorage on the web targets). Cross-screen sync (scope =), biometric auth, memory policies, and runtime security detection are all built in.
// 1. Create instance (Android needs context, others don't)
val ksafe = KSafe(context) // Android
val ksafe = KSafe() // iOS / JVM / WASM / JS / JS
// 2. Store & retrieve with property delegation
var counter by ksafe(0)
counter++ // Auto-encrypted, auto-persisted
// 3. Compose state (read/write, reactive to external changes)
var username by ksafe.mutableStateOf("Guest", scope = viewModelScope)
// 4. Reactive flows — read-only StateFlow, read/write MutableStateFlow, or read/write WritableKSafeFlow (no scope)
val user: StateFlow<User> by ksafe.asStateFlow(User(), viewModelScope)
private val _state by ksafe.asMutableStateFlow(UiState(), viewModelScope)
val state = _state.asStateFlow()
val themeMode: WritableKSafeFlow<ThemeMode> by ksafe.asWritableFlow(ThemeMode.DEVICE)
// themeMode.set(ThemeMode.NIGHT)
// 5. Or use suspend API
viewModelScope.launch {
ksafe.put("user_token", token)
val token = ksafe.get("user_token", "")
}
// 6. Protect actions with biometrics (optional — add `ksafe-biometrics`)
KSafeBiometrics.verifyBiometricDirect("Confirm payment") { success ->
if (success) processPayment()
}Data is now AES-256-GCM encrypted — keys in Android Keystore, iOS Keychain, software-backed on JVM, WebCrypto on the browser targets (Kotlin/WASM and Kotlin/JS).
// commonMain or Android-only build.gradle(.kts)
implementation("eu.anifantakis:ksafe:2.0.0-RC2")
implementation("eu.anifantakis:ksafe-compose:2.0.0-RC2") // ← Compose state (optional)
implementation("eu.anifantakis:ksafe-biometrics:2.0.0-RC2") // ← Biometric auth (optional)Skip
ksafe-composeif you don't use Jetpack Compose ormutableStateOfpersistence.Skip
ksafe-biometricsif you don't need Face ID / Touch ID / Fingerprint verification. The biometrics module is fully independent — it has no dependency on:ksafeand can be used on its own to protect any action in your app.
Note:
kotlinx-serialization-jsoncomes in transitively — don't add it yourself.
Required only if you store @Serializable data classes. Add it to libs.versions.toml:
[versions]
kotlin = "2.2.21"
[plugins]
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }then apply it in build.gradle.kts:
plugins {
//...
alias(libs.plugins.kotlin.serialization)
}// Android
val ksafe = KSafe(context)
// iOS / JVM / WASM / JS
val ksafe = KSafe()With Koin (recommended for KMP):
// Android
actual val platformModule = module {
single { KSafe(androidApplication()) }
}
// iOS / JVM / WASM / JS
actual val platformModule = module {
single { KSafe() }
}Multi-instance setups, web awaitCacheReady() (wasmJs + js), full per-platform Koin examples, the custom storage directory option (baseDir on JVM/Android, directory on iOS — for example to align with $XDG_DATA_HOME or noBackupFilesDir), and the optional KSafe.close() for apps that re-create instances mid-process: docs/SETUP.md.
A handful of examples cover 95% of real-world use. Full reference (Compose policy, cross-screen sync, write modes, nullables, deletion, full ViewModel): docs/USAGE.md.
// 1. Property delegate — synchronous, non-blocking, encrypted, persisted
var counter by ksafe(0)
counter++
// 2. Compose state on a ViewModel / class field — reactive UI + persistence (requires ksafe-compose)
var username by ksafe.mutableStateOf("Guest")
// 3. Compose state inside a @Composable body — the rememberSaveable analogue, but persists across app restarts
// var currentTab by ksafe.rememberKSafeState(Tab.Home) // key auto-resolves to "currentTab"; no ViewModel needed
// 4. Reactive flows — read-only StateFlow, read/write MutableStateFlow, or read/write Flow without a scope
val user: StateFlow<User> by ksafe.asStateFlow(User(), viewModelScope) // read-only
private val _state by ksafe.asMutableStateFlow(MoviesState(), viewModelScope) // read/write, hot
val state = _state.asStateFlow()
val themeMode: WritableKSafeFlow<ThemeMode> by ksafe.asWritableFlow(ThemeMode.DEVICE) // read/write, cold; set() to write
// 4. Suspend API — when you want to await the disk flush
viewModelScope.launch {
ksafe.put("profile", user)
val loaded: User = ksafe.get("profile", User())
}
// 5. Direct API — non-suspend, hot-cache reads, background-flushed writes (~1000x faster for bulk ops)
ksafe.putDirect("counter", 42)
val n = ksafe.getDirect("counter", 0)Per-entry plain / encrypted toggle via KSafeWriteMode:
var theme by ksafe("light", mode = KSafeWriteMode.Plain)
ksafe.putDirect(
"pin", pin,
mode = KSafeWriteMode.Encrypted(
protection = KSafeEncryptedProtection.HARDWARE_ISOLATED,
requireUnlockedDevice = true
)
)Complex objects — just mark them @Serializable; JSON and encryption are automatic:
@Serializable
data class AuthInfo(val accessToken: String = "", val refreshToken: String = "")
var authInfo by ksafe(AuthInfo())
authInfo = authInfo.copy(accessToken = "newToken")Note: The property delegate works only with the default KSafe instance. For named instances, use the suspend or direct APIs — see docs/SETUP.md.
For third-party types you can't annotate (UUID, Instant, BigDecimal…), register a KSerializer via KSafeConfig(json = customJson) and use @Contextual fields at the call site. Full walkthrough: docs/SERIALIZATION.md.
Two small cross-platform helpers:
import eu.anifantakis.lib.ksafe.internal.secureRandomBytes
// Secure random bytes (SecureRandom / arc4random_buf / WebCrypto)
val nonce = secureRandomBytes(16)
// Generate-or-retrieve a hardware-isolated 256-bit secret (great for DB passphrases)
val passphrase = ksafe.getOrCreateSecret("main.db")
secureRandomByteslives undereu.anifantakis.lib.ksafe.internal— it's the same primitive KSafe uses internally, exposed for app code that needs a CSPRNG.
Sizes, protection tiers, Room + SQLCipher / SQLDelight examples: docs/SECURITY.md#cryptographic-utilities.
KSafeWriteMode + KSafeEncryptedProtection tiersvar launchCount by ksafe(0), that is literally it| Feature | SharedPrefs | DataStore | multiplatform-settings | KVault | KSafe |
|---|---|---|---|---|---|
| Thread safety | ❌ ANRs possible | ✅ Coroutine-safe | ✅ Platform-native | ✅ Thread-safe | ✅ ConcurrentHashMap + coroutines |
| Type safety | ❌ Runtime crashes | ✅ Compile-time | ✅ Generic API | ✅ Generic API | ✅ Reified generics + serialization |
| Data corruption | ❌ Crash = data loss | ✅ Atomic | ❌ Platform-dependent | ✅ Atomic | ✅ Uses DataStore atomicity |
| API style | ❌ Callbacks | ✅ Flow | ✅ Sync | ✅ Sync | ✅ Both sync & async |
| Encryption | ❌ None | ❌ None | ❌ None | ✅ Hardware-backed | ✅ Hardware-backed |
| Cross-platform | ❌ Android only | ❌ Android only | ✅ KMP | ✅ KMP | ✅ Android/iOS/JVM/WASM/JS |
| Nullable support | ❌ No | ❌ No | ✅ Primitives (*OrNull getters) |
✅ Primitives | ✅ Primitives + objects + delegates * |
| Complex types | ❌ Manual | ❌ Manual/Proto | ❌ Manual | ❌ Manual | ✅ Auto-serialization |
| Biometric auth | ❌ Manual | ❌ Manual | ❌ Manual | ❌ Manual | ✅ Built-in |
| Memory policy | N/A | N/A | N/A | N/A | ✅ 3 policies (PLAIN_TEXT / ENCRYPTED / TIMED_CACHE) |
| Hot cache | ✅ Synchronized HashMap
|
❌ No (Flow only) | ✅ Platform-native cache | ❌ No | ✅ ConcurrentHashMap + optimistic writes |
| Write batching | ❌ No | ❌ No | ❌ No | ❌ No | ✅ 16ms coalescing |
* "Primitives + objects + delegates" — nullability flows uniformly through every API shape and every type KSafe stores, not just scalar getters. Specifically:
- Primitives —
Boolean?,Int?,Long?,Float?,Double?,String?all round-trip throughget/put/getDirect/putDirect/getFlow.nullis a distinct state, not "missing"; it's preserved on reads and persisted on writes.ksafe.putDirect("nickname", null as String?) // stored as null, not "default" val nickname: String? = ksafe.getDirect("nickname", "Guest") // returns null, NOT "Guest"- Objects — any
@Serializableclass can be stored nullably.nullround-trips through the same encrypted JSON path as the payload; no extra boilerplate.@Serializable data class User(val id: String, val name: String) var currentUser: User? by ksafe(null) // encrypted, persisted, nullable currentUser = User("u1", "Alice") // -> encrypted JSON currentUser = null // -> null sentinel, round-trips- Delegates — every delegate shape accepts a nullable default, including Compose state and
MutableStateFlow. The persisted null survives process death and emits correctly through Flow observers.var token: String? by ksafe(null) // plain delegate var profile: User? by ksafe.mutableStateOf(null) // Compose state val user: StateFlow<User?> by ksafe.asStateFlow(null, scope) // read-only StateFlow private val _state by ksafe.asMutableStateFlow<User?>(null, scope) // read/write MutableStateFlow val themeMode: WritableKSafeFlow<ThemeMode?> by ksafe.asWritableFlow(null) // read/write Flow, no scopeBy contrast,
multiplatform-settingsexposes nullability only through separategetStringOrNull/getIntOrNullscalar getters — there is no nullable support for custom types, property delegates, or Compose state, because the library has no serialization or delegate layer. KVault is similar: nullable return types on its primitive getters, no object or delegate support.
| API | Read | Write | Best For |
|---|---|---|---|
getDirect/putDirect
|
0.007 ms | 0.022 ms | UI, bulk ops, high throughput |
get/put (suspend) |
0.010 ms | 22 ms | When you must guarantee persistence |
vs competitors (encrypted): 14× faster reads than KVault, 15× faster than EncryptedSharedPreferences. Unencrypted writes match SharedPreferences.
Measured on representative Android hardware under a synthetic but realistic workload. Real-world numbers depend on device, workload, and data size — see docs/BENCHMARKS.md for the methodology, full tables, cold-start numbers, and architecture notes.
| Platform | Minimum Version | Notes |
|---|---|---|
| Android | API 24 (Android 7.0) | Hardware-backed Keystore on supported devices |
| iOS | iOS 13+ | Keychain-backed symmetric keys (protected by device passcode) |
| JVM/Desktop | JDK 11+ | Software-backed encryption |
| Kotlin/WASM (Browser) | Browsers with WasmGC (Chrome 119+, Firefox 120+, Safari 18+) | WebCrypto API + localStorage |
| Kotlin/JS (Browser) | Any modern browser | WebCrypto API + localStorage — use this for older browsers or pre-existing JS builds |
| Dependency | Tested Version |
|---|---|
| Kotlin | 2.0.0+ |
| Kotlin Coroutines | 1.8.0+ |
| DataStore Preferences | 1.1.0+ |
| Compose Multiplatform | 1.6.0+ (for ksafe-compose) |
A standalone biometric helper (Android + iOS) that can gate any action in your app — not just KSafe ops. Ships as the optional :ksafe-biometrics artifact and depends on nothing else from KSafe, so apps that need only biometric verification can use it on its own.
Static API. No instance, no DI wiring, no Context parameter. On Android the library auto-initializes via a ContentProvider declared in its merged manifest (the same pattern WorkManager / Firebase use), so consumers don't need to touch their Application class.
// Same call shape on every platform — Android, iOS, JVM, web.
// Callback-based
KSafeBiometrics.verifyBiometricDirect("Authenticate to increment") { success ->
if (success) secureCounter++
}
// Suspend-based
if (KSafeBiometrics.verifyBiometric("Authenticate to increment")) {
secureCounter++
}Auth caching, scoped sessions, platform setup, complete examples: docs/BIOMETRICS.md.
Migrating from KSafe ≤1.x? Biometric methods used to live on
KSafeitself. In 2.0 they moved to a separate module. Addimplementation("eu.anifantakis:ksafe-biometrics:2.0.0-RC2"), changeimport eu.anifantakis.lib.ksafe.BiometricAuthorizationDuration→import eu.anifantakis.lib.ksafe.biometrics.BiometricAuthorizationDuration, replaceksafe.verifyBiometric(...)withKSafeBiometrics.verifyBiometric(...). Method names and signatures are unchanged. No instance to construct, no DI wiring needed.
Detect and respond to runtime threats — root/jailbreak, debugger, emulator, debug builds:
val ksafe = KSafe(
context = context,
securityPolicy = KSafeSecurityPolicy(
rootedDevice = SecurityAction.WARN, // IGNORE, WARN, or BLOCK
debuggerAttached = SecurityAction.BLOCK,
debugBuild = SecurityAction.WARN,
emulator = SecurityAction.IGNORE,
onViolation = { violation ->
analytics.log("Security: ${violation.name}")
}
)
)Preset policies, BLOCK exception handling, Compose stability, detection methods: docs/SECURITY.md.
Trade off performance vs. security for data in RAM:
val ksafe = KSafe(
fileName = "secrets",
memoryPolicy = KSafeMemoryPolicy.ENCRYPTED // Default
)| Policy | Best For | RAM Contents | Read Cost | Security |
|---|---|---|---|---|
PLAIN_TEXT |
User settings, themes | Plaintext (forever) | O(1) lookup | Low — all data exposed in memory |
ENCRYPTED (Default) |
Tokens, passwords | Ciphertext only | AES-GCM decrypt every read | High — nothing plaintext in RAM |
ENCRYPTED_WITH_TIMED_CACHE |
Compose/SwiftUI screens | Ciphertext + short-lived plaintext | First read decrypts, then O(1) for TTL | Medium — plaintext only for recently-accessed keys, only for seconds |
Timed cache details, constructor params, lock-state policies, multi-instance lock policies: docs/MEMORY.md.
Internals, advanced features, reference material:
| Topic | Description |
|---|---|
| Complete Usage Guide | Every API shape: delegates, flow delegates, Compose state, suspend/direct APIs, write modes, nullables, full ViewModel |
| Setup with Koin | Multi-instance setups (prefs vs vault), web awaitCacheReady() (wasmJs + js), full platform examples, custom storage directory (baseDir / directory) |
| Custom JSON Serialization | Registering KSerializers for UUID, Instant, and other third-party types |
| Performance Benchmarks | Full benchmark tables, cold start numbers, architecture deep-dive |
| Biometric Authentication | Authorization caching, scoped sessions, platform setup, complete examples |
| Security | Runtime security policy, encryption internals, threat model, hardware isolation, key storage queries, crypto utilities |
| Encryption Proof | Per-platform automated proof tests + manual commands to inspect the raw stored bytes and see the ciphertext yourself |
| Memory Policy | Timed cache, constructor parameters, encryption config, device lock-state policies |
| Architecture | The conceptual model: three modules, three rings (public API / KSafeCore orchestrator / platform shells), hot cache + write coalescer, the KSafePlatformStorage and KSafeEncryption interfaces, memory policies, and how 2.0 consolidated ~5,900 lines of duplicated platform logic into ~890 |
| Source-tree tour | File-by-file walkthrough of every Kotlin source file in :ksafe: where each behaviour lives and why. Companion to the Architecture doc — Architecture is "the model," TOUR is "the map." |
| Testing | Running tests, building iOS test app, test features |
| Migration Guide | Upgrading from v1.x → v2.0 (biometric module extraction, iOS path migration), v1.6.x → v1.7.0 (encrypted: Boolean → KSafeWriteMode), and v1.1.x → v1.2.0+ |
| Alternatives & Comparison | KSafe vs EncryptedSharedPrefs, KVault, SQLCipher, and more |
Licensed under the Apache License 2.0 — see http://www.apache.org/licenses/LICENSE-2.0. Distributed "AS IS", without warranties of any kind.