
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 ships KSAFE_SKILL.md — an agentskills.io-compatible skill that teaches any AI agent (Claude Code, Codex, Gemini CLI, Copilot CLI, Junie) KSafe's patterns, anti-patterns, and gotchas. Restart your agent session after installing — skills load at session start.
for agent in claude codex gemini copilot junie; do
mkdir -p "$HOME/.$agent/skills/ksafe" && \
curl -fsSL https://raw.githubusercontent.com/ioannisa/KSafe/main/KSAFE_SKILL.md \
> "$HOME/.$agent/skills/ksafe/SKILL.md"
doneEdit the loop to skip agents you don't use. If you've already cloned this repo, cp KSAFE_SKILL.md "$HOME/.<agent>/skills/ksafe/SKILL.md" works too (faster, offline).
KSafe is a secure-by-default Kotlin Multiplatform key/value persistence library. Persist ordinary Kotlin variables, Compose MutableState, MutableStateFlow, and @Serializable objects across app restarts with one API on Android, iOS, macOS, JVM/Desktop, WASM, and Kotlin/JS. Encrypted (AES-256-GCM) by default; plain per-entry with mode = KSafeWriteMode.Plain.
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.002 ms); writes encrypt and flush in the background — synchronous, but never blocking. Reach for the suspend API (get / put) only when you want to await the disk flush.
Extras when you encrypt: biometrics (Face ID / Touch ID / Fingerprint — optional standalone ksafe-biometrics module) · root/jailbreak detection (WARN/BLOCK + analytics callback) · memory policy (RAM-exposure modes) · a one-line hardware-isolated DB passphrase for SQLCipher / SQLDelight / Room.
KSafe in action across many scenarios: KSafeDemo — Compose Multiplatform app.
| 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 |
// commonMain or Android-only build.gradle(.kts)
implementation("eu.anifantakis:ksafe:2.1.1")
implementation("eu.anifantakis:ksafe-compose:2.1.1") // ← Compose state (optional)
implementation("eu.anifantakis:ksafe-biometrics:2.1.1") // ← 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 / macOS / JVM / WASM / JS
val ksafe = KSafe()With Koin (recommended for KMP):
// Android
actual val platformModule = module {
single { KSafe(androidApplication()) }
}
// iOS / macOS / 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 / macOS — for example to align with $XDG_DATA_HOME, noBackupFilesDir, or a sandboxed Mac app's container), 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
// 5. Suspend API — when you want to await the disk flush
viewModelScope.launch {
ksafe.put("profile", user)
val loaded: User = ksafe.get("profile", User())
}
// 6. 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 with any KSafe instance —
var x by myKsafe(default)makesmyKsafethe storage backend. The barevar x by ksafe(default)form requires an in-scopeksafe(the conventional name, typically your default instance). See docs/SETUP.md for the multi-instance pattern.
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.
Android and iOS keystores are OS-sandboxed per app. The JVM/Desktop OS secret store (macOS Keychain / Linux Secret Service) is per-OS-user, shared by every process, and Web IndexedDB/localStorage is shared per browser origin — so two apps using the same fileName could collide on the same key. Set a stable, app-unique namespace:
val ksafe = KSafe(config = KSafeConfig(appNamespace = "com.example.myapp"))Production desktop apps should set it explicitly. Only the key-store destination is namespaced — KSafe ≤ 2.0 data still migrates unchanged. See docs/USAGE.md.
For production Compose Desktop release distributables, add these to your nativeDistributions block — they give KSafe OS-backed key custody (Keychain / DPAPI / Secret Service):
compose.desktop {
application {
nativeDistributions {
// OS-backed key custody: JNA + DataStore's protobuf need sun.misc.Unsafe (jlink trims it).
// java.management → only for a non-default KSafeSecurityPolicy.
modules("jdk.unsupported", "java.management")
}
}
}Without it KSafe still persists (at a software key tier) and migrates your data forward when you add the module — the trade-off and the key-file risk are in docs/JVM_PROTECTION.md; KSafeDemo shows it live on its Security screen.
Two small cross-platform helpers:
import eu.anifantakis.lib.ksafe.internal.secureRandomBytes
// Secure random bytes (SecureRandom / SecRandomCopyBytes / 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/macOS/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 | ✅ 4 policies (LAZY_PLAIN_TEXT / PLAIN_TEXT / ENCRYPTED / ENCRYPTED_WITH_TIMED_CACHE) |
| Hot cache | ✅ Synchronized HashMap
|
❌ No (Flow only) | ✅ Platform-native cache | ❌ No | ✅ ConcurrentHashMap + optimistic writes |
| Write batching | ❌ No | ❌ No | ❌ No | ❌ No | ✅ 16ms coalescing |
* Nullability flows uniformly through every API shape — primitives,
@Serializableobjects, and all delegate / Compose / Flow forms.nullis a distinct, persisted state, not "missing." Full examples: docs/USAGE.md#nullable-values.
| API | Read | Write | Best For |
|---|---|---|---|
getDirect/putDirect
|
0.002 ms | 0.004 ms | UI, hot cache, fire-and-forget |
get/put (suspend) |
0.021 ms | 0.62 ms | Must guarantee persistence; multiple concurrent callers |
vs competitors (encrypted): ~21× faster reads than KVault and ~24× faster than EncryptedSharedPreferences; ~127× faster encrypted writes than KVault and ~14× faster than EncryptedSharedPreferences. Unencrypted writes are ~3× faster than MMKV and ~3× faster than SharedPreferences.
Numbers reflect the v2 envelope introduced in 2.0 (per-datastore master AES key cached in-process, eliminating per-entry Keystore IPC for non-isolated encrypted ops). Measured on an AOSP Emulator (API 37) running on a MacBook Pro (Apple Silicon). Suspend API benchmarks issue all iterations as concurrent coroutines (
GlobalScope.launch+joinAll) — the natural usage pattern when multiple coroutines persist values in parallel. 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); Secure Enclave on real devices |
| macOS (native) | macOS 11+ (macosArm64, macosX64) |
Same Keychain + CryptoKit path as iOS; Secure Enclave on Apple Silicon and T2-equipped Macs |
| JVM/Desktop | JDK 11+ | Key in OS secret store — Windows DPAPI / macOS Keychain / Linux Secret Service (libsecret); software fallback + warning when none is available |
| Kotlin/WASM (Browser) | Browsers with WasmGC (Chrome 119+, Firefox 120+, Safari 18+) | WebCrypto API; non-extractable key in IndexedDB, values in localStorage |
| Kotlin/JS (Browser) | Any modern browser | WebCrypto API; non-extractable key in IndexedDB, values in 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 + macOS) 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, macOS, 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.1.1"), 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.
Find out what key custody this KSafe instance actually got — including any silent fallback (e.g. JVM dropping from SANDBOX_PROTECTED to SOFTWARE when no OS vault is reachable):
val info = ksafe.protectionInfo
// info.intendedLevel = SANDBOX_PROTECTED // engine baseline
// info.effectiveLevel = SOFTWARE // vault self-test failed
// info.custody = "DataStore (software, ...)" // human-readable
// info.notes = ["jvm_os_vault_unavailable"] // stable code
// Gate startup, drive feature logic, or surface a UX banner
check(info.effectiveLevel >= KSafeProtectionLevel.SANDBOX_PROTECTED)KSafeProtectionLevel is a universally-ordered scale — SOFTWARE < SANDBOX_PROTECTED < HARDWARE_BACKED < HARDWARE_ISOLATED. One ordinal comparison works across every platform. Per-platform truth table, runtime-decision patterns (gating, tighter re-auth windows, feature disablement, UX honesty banners, intended-vs-effective delta), and all defined notes codes: docs/PROTECTION_INFO.md.
Trade off performance vs. security for data in RAM:
val ksafe = KSafe(
fileName = "secrets",
memoryPolicy = KSafeMemoryPolicy.LAZY_PLAIN_TEXT // Default
)| Policy | Best For | RAM Contents | Read Cost | Security |
|---|---|---|---|---|
LAZY_PLAIN_TEXT (Default) |
General-purpose: settings, tokens, app state | Ciphertext at rest; plaintext appears after first read of each key and stays | First read decrypts, then O(1) forever | Low (after first read) — same exposure as PLAIN_TEXT for keys you've actually touched |
PLAIN_TEXT (discouraged) |
Apps that want decrypt failures surfaced synchronously at startup | Plaintext (forever, eagerly decrypted at cold start) | O(1) lookup | Low — all data exposed in memory; cold start pays $O(n)$ Keystore round-trips up front |
ENCRYPTED |
Tokens, passwords, financial data | Ciphertext only | AES-GCM decrypt every read | High — nothing plaintext in RAM |
ENCRYPTED_WITH_TIMED_CACHE |
Compose/SwiftUI screens accessing the same encrypted value many times per frame | Ciphertext + short-lived plaintext (TTL) | First read of a window 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 |
|---|---|
| KSafe Skill for AI agents | Self-contained skill file teaching any agentskills.io-compatible agent (Claude Code, Codex, Gemini CLI, Copilot CLI, Junie, …) the patterns, anti-patterns, and gotchas for KSafe. Install instructions at the top of this README. |
| 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 |
| Protection Info | Instance-level diagnostic API: KSafe.protectionInfo, the cross-platform KSafeProtectionLevel scale, per-platform truth table, consumer gating / telemetry / UI patterns |
| JVM Key Protection | Deep dive on how the AES key is held on each JVM host: Windows DPAPI, macOS login Keychain, Linux Secret Service (libsecret), the software fallback, the opt-out, and the per-app namespace |
| 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 ships KSAFE_SKILL.md — an agentskills.io-compatible skill that teaches any AI agent (Claude Code, Codex, Gemini CLI, Copilot CLI, Junie) KSafe's patterns, anti-patterns, and gotchas. Restart your agent session after installing — skills load at session start.
for agent in claude codex gemini copilot junie; do
mkdir -p "$HOME/.$agent/skills/ksafe" && \
curl -fsSL https://raw.githubusercontent.com/ioannisa/KSafe/main/KSAFE_SKILL.md \
> "$HOME/.$agent/skills/ksafe/SKILL.md"
doneEdit the loop to skip agents you don't use. If you've already cloned this repo, cp KSAFE_SKILL.md "$HOME/.<agent>/skills/ksafe/SKILL.md" works too (faster, offline).
KSafe is a secure-by-default Kotlin Multiplatform key/value persistence library. Persist ordinary Kotlin variables, Compose MutableState, MutableStateFlow, and @Serializable objects across app restarts with one API on Android, iOS, macOS, JVM/Desktop, WASM, and Kotlin/JS. Encrypted (AES-256-GCM) by default; plain per-entry with mode = KSafeWriteMode.Plain.
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.002 ms); writes encrypt and flush in the background — synchronous, but never blocking. Reach for the suspend API (get / put) only when you want to await the disk flush.
Extras when you encrypt: biometrics (Face ID / Touch ID / Fingerprint — optional standalone ksafe-biometrics module) · root/jailbreak detection (WARN/BLOCK + analytics callback) · memory policy (RAM-exposure modes) · a one-line hardware-isolated DB passphrase for SQLCipher / SQLDelight / Room.
KSafe in action across many scenarios: KSafeDemo — Compose Multiplatform app.
| 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 |
// commonMain or Android-only build.gradle(.kts)
implementation("eu.anifantakis:ksafe:2.1.1")
implementation("eu.anifantakis:ksafe-compose:2.1.1") // ← Compose state (optional)
implementation("eu.anifantakis:ksafe-biometrics:2.1.1") // ← 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 / macOS / JVM / WASM / JS
val ksafe = KSafe()With Koin (recommended for KMP):
// Android
actual val platformModule = module {
single { KSafe(androidApplication()) }
}
// iOS / macOS / 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 / macOS — for example to align with $XDG_DATA_HOME, noBackupFilesDir, or a sandboxed Mac app's container), 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
// 5. Suspend API — when you want to await the disk flush
viewModelScope.launch {
ksafe.put("profile", user)
val loaded: User = ksafe.get("profile", User())
}
// 6. 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 with any KSafe instance —
var x by myKsafe(default)makesmyKsafethe storage backend. The barevar x by ksafe(default)form requires an in-scopeksafe(the conventional name, typically your default instance). See docs/SETUP.md for the multi-instance pattern.
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.
Android and iOS keystores are OS-sandboxed per app. The JVM/Desktop OS secret store (macOS Keychain / Linux Secret Service) is per-OS-user, shared by every process, and Web IndexedDB/localStorage is shared per browser origin — so two apps using the same fileName could collide on the same key. Set a stable, app-unique namespace:
val ksafe = KSafe(config = KSafeConfig(appNamespace = "com.example.myapp"))Production desktop apps should set it explicitly. Only the key-store destination is namespaced — KSafe ≤ 2.0 data still migrates unchanged. See docs/USAGE.md.
For production Compose Desktop release distributables, add these to your nativeDistributions block — they give KSafe OS-backed key custody (Keychain / DPAPI / Secret Service):
compose.desktop {
application {
nativeDistributions {
// OS-backed key custody: JNA + DataStore's protobuf need sun.misc.Unsafe (jlink trims it).
// java.management → only for a non-default KSafeSecurityPolicy.
modules("jdk.unsupported", "java.management")
}
}
}Without it KSafe still persists (at a software key tier) and migrates your data forward when you add the module — the trade-off and the key-file risk are in docs/JVM_PROTECTION.md; KSafeDemo shows it live on its Security screen.
Two small cross-platform helpers:
import eu.anifantakis.lib.ksafe.internal.secureRandomBytes
// Secure random bytes (SecureRandom / SecRandomCopyBytes / 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/macOS/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 | ✅ 4 policies (LAZY_PLAIN_TEXT / PLAIN_TEXT / ENCRYPTED / ENCRYPTED_WITH_TIMED_CACHE) |
| Hot cache | ✅ Synchronized HashMap
|
❌ No (Flow only) | ✅ Platform-native cache | ❌ No | ✅ ConcurrentHashMap + optimistic writes |
| Write batching | ❌ No | ❌ No | ❌ No | ❌ No | ✅ 16ms coalescing |
* Nullability flows uniformly through every API shape — primitives,
@Serializableobjects, and all delegate / Compose / Flow forms.nullis a distinct, persisted state, not "missing." Full examples: docs/USAGE.md#nullable-values.
| API | Read | Write | Best For |
|---|---|---|---|
getDirect/putDirect
|
0.002 ms | 0.004 ms | UI, hot cache, fire-and-forget |
get/put (suspend) |
0.021 ms | 0.62 ms | Must guarantee persistence; multiple concurrent callers |
vs competitors (encrypted): ~21× faster reads than KVault and ~24× faster than EncryptedSharedPreferences; ~127× faster encrypted writes than KVault and ~14× faster than EncryptedSharedPreferences. Unencrypted writes are ~3× faster than MMKV and ~3× faster than SharedPreferences.
Numbers reflect the v2 envelope introduced in 2.0 (per-datastore master AES key cached in-process, eliminating per-entry Keystore IPC for non-isolated encrypted ops). Measured on an AOSP Emulator (API 37) running on a MacBook Pro (Apple Silicon). Suspend API benchmarks issue all iterations as concurrent coroutines (
GlobalScope.launch+joinAll) — the natural usage pattern when multiple coroutines persist values in parallel. 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); Secure Enclave on real devices |
| macOS (native) | macOS 11+ (macosArm64, macosX64) |
Same Keychain + CryptoKit path as iOS; Secure Enclave on Apple Silicon and T2-equipped Macs |
| JVM/Desktop | JDK 11+ | Key in OS secret store — Windows DPAPI / macOS Keychain / Linux Secret Service (libsecret); software fallback + warning when none is available |
| Kotlin/WASM (Browser) | Browsers with WasmGC (Chrome 119+, Firefox 120+, Safari 18+) | WebCrypto API; non-extractable key in IndexedDB, values in localStorage |
| Kotlin/JS (Browser) | Any modern browser | WebCrypto API; non-extractable key in IndexedDB, values in 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 + macOS) 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, macOS, 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.1.1"), 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.
Find out what key custody this KSafe instance actually got — including any silent fallback (e.g. JVM dropping from SANDBOX_PROTECTED to SOFTWARE when no OS vault is reachable):
val info = ksafe.protectionInfo
// info.intendedLevel = SANDBOX_PROTECTED // engine baseline
// info.effectiveLevel = SOFTWARE // vault self-test failed
// info.custody = "DataStore (software, ...)" // human-readable
// info.notes = ["jvm_os_vault_unavailable"] // stable code
// Gate startup, drive feature logic, or surface a UX banner
check(info.effectiveLevel >= KSafeProtectionLevel.SANDBOX_PROTECTED)KSafeProtectionLevel is a universally-ordered scale — SOFTWARE < SANDBOX_PROTECTED < HARDWARE_BACKED < HARDWARE_ISOLATED. One ordinal comparison works across every platform. Per-platform truth table, runtime-decision patterns (gating, tighter re-auth windows, feature disablement, UX honesty banners, intended-vs-effective delta), and all defined notes codes: docs/PROTECTION_INFO.md.
Trade off performance vs. security for data in RAM:
val ksafe = KSafe(
fileName = "secrets",
memoryPolicy = KSafeMemoryPolicy.LAZY_PLAIN_TEXT // Default
)| Policy | Best For | RAM Contents | Read Cost | Security |
|---|---|---|---|---|
LAZY_PLAIN_TEXT (Default) |
General-purpose: settings, tokens, app state | Ciphertext at rest; plaintext appears after first read of each key and stays | First read decrypts, then O(1) forever | Low (after first read) — same exposure as PLAIN_TEXT for keys you've actually touched |
PLAIN_TEXT (discouraged) |
Apps that want decrypt failures surfaced synchronously at startup | Plaintext (forever, eagerly decrypted at cold start) | O(1) lookup | Low — all data exposed in memory; cold start pays $O(n)$ Keystore round-trips up front |
ENCRYPTED |
Tokens, passwords, financial data | Ciphertext only | AES-GCM decrypt every read | High — nothing plaintext in RAM |
ENCRYPTED_WITH_TIMED_CACHE |
Compose/SwiftUI screens accessing the same encrypted value many times per frame | Ciphertext + short-lived plaintext (TTL) | First read of a window 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 |
|---|---|
| KSafe Skill for AI agents | Self-contained skill file teaching any agentskills.io-compatible agent (Claude Code, Codex, Gemini CLI, Copilot CLI, Junie, …) the patterns, anti-patterns, and gotchas for KSafe. Install instructions at the top of this README. |
| 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 |
| Protection Info | Instance-level diagnostic API: KSafe.protectionInfo, the cross-platform KSafeProtectionLevel scale, per-platform truth table, consumer gating / telemetry / UI patterns |
| JVM Key Protection | Deep dive on how the AES key is held on each JVM host: Windows DPAPI, macOS login Keychain, Linux Secret Service (libsecret), the software fallback, the opt-out, and the per-app namespace |
| 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.