KSafe
Meet KSafe. An Effortless Enterprise-Grade Encrypted key-value storage for Kotlin Multiplatform and Native Android with Hardware-Backed Security.
Install / Use
/learn @ioannisa/KSafeREADME
KSafe — Secure Persist Library for Kotlin Multiplatform
The Universal Persistence Layer: Effortless Enterprise-Grade Security AND Lightning-Fast Plain-Text Storage for Android, iOS, Desktop, and Web.
Demo Application
To see KSafe in action on several scenarios, I invite you to check out my demo application here: Demo CMP App Using KSafe
YouTube Demos
Check out my own video about how easy it is to adapt KSafe into your project and get seamless encrypted persistence, but also more videos from other content creators.
| Author's Video | Philipp Lackner's Video | Jimmy Plazas's Video | |:--------------:|:---------------:|:---------------:| | <img width="200" alt="image" src="https://github.com/user-attachments/assets/8c317a36-4baa-491e-8c88-4c44b8545bad" /> | <img width="200" alt="image" src="https://github.com/user-attachments/assets/59cce32b-634e-4b17-bb5f-5e084dff899f" /> | <img width="200" alt="image" src="https://github.com/user-attachments/assets/65dba780-9c80-470c-9ad0-927a86510a26" /> | | 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 |
What is KSafe
KSafe is the
- easiest to use
- most secure
- fastest
library to persist encrypted and unencrypted data in Kotlin Multiplatform.
With simple property delegation, values feel like normal variables — you just read and write them, and KSafe handles the underlying cryptography, caching, and atomic DataStore persistence transparently across all four platforms: Android, iOS, JVM/Desktop, and WASM/JS (Browser).
⚡ The Dual-Purpose Advantage: Not Just for Secrets
Think KSafe is overkill for a simple "Dark Mode" toggle? Think again.
By setting mode = KSafeWriteMode.Plain, KSafe completely bypasses the cryptographic engine. What remains is a lightning-fast, zero-boilerplate wrapper around AndroidX DataStore with a concurrent hot-cache.
Setting up raw KMP DataStore requires writing expect/actual file paths across 4 platforms, managing CoroutineScopes, and dealing with async-only Flow reads. KSafe abstracts 100% of that. You get synchronous, O(1) reads backed by asynchronous disk writes—all in one line of code. Unencrypted KSafe writes are actually benchmarked to be faster than native Android SharedPreferences.
Whether you are storing a harmless UI state or a highly sensitive biometric token, KSafe is the only local persistence dependency your KMP app needs.
Real-World Example
Here's what that looks like in a real app — 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
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(
accessToken = newInfo.accessToken,
refreshToken = newInfo.refreshToken
)
BearerTokens(tokens.accessToken, tokens.refreshToken)
}
}
}
No explicit encrypt/decrypt calls. No DataStore boilerplate. No runBlocking. Tokens are AES-256-GCM encrypted at rest, served from the hot cache at runtime, and survive process death — all through regular Kotlin property access.
Under the hood, each platform uses its native crypto engine — Android Keystore, iOS Keychain + CryptoKit, JVM's javax.crypto, and browser WebCrypto — unified behind a single API. Values are AES-256-GCM encrypted and persisted to DataStore (or localStorage on WASM). Beyond property delegation, KSafe also offers Compose state integration (ksafe.mutableStateOf()), reactive flows (getFlow() / getStateFlow()), built-in biometric authentication, configurable memory policies, and runtime security detection (root/jailbreak, debugger, emulator) — all out of the box.
Quickstart
// 1. Create instance (Android needs context, others don't)
val ksafe = KSafe(context) // Android
val ksafe = KSafe() // iOS / JVM / WASM
// 2. Store & retrieve with property delegation
var counter by ksafe(0)
counter++ // Auto-encrypted, auto-persisted
// 3. Or use suspend API
viewModelScope.launch {
ksafe.put("user_token", token)
val token = ksafe.get("user_token", "")
}
// 4. Protect actions with biometrics
ksafe.verifyBiometricDirect("Confirm payment") { success ->
if (success) processPayment()
}
That's it. Your data is now AES-256-GCM encrypted with keys stored in Android Keystore, iOS Keychain, software-backed on JVM, or WebCrypto on WASM.
Setup
1 - Add the Dependency
// commonMain or Android-only build.gradle(.kts)
implementation("eu.anifantakis:ksafe:1.7.1")
implementation("eu.anifantakis:ksafe-compose:1.7.1") // ← Compose state (optional)
Skip
ksafe-composeif your project doesn't use Jetpack Compose, or if you don't intend to use the library'smutableStateOfpersistence option
Note:
kotlinx-serialization-jsonis exposed as a transitive dependency — you do not need to add it manually to your project. KSafe already provides it.
2 - Apply the kotlinx-serialization plugin
If you want to use the library with data classes, you need to enable Serialization at your project.
Add Serialization definition to your plugins section of your libs.versions.toml
[versions]
kotlin = "2.2.21"
[plugins]
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
and apply it at the same section of your build.gradle.kts file.
plugins {
//...
alias(libs.plugins.kotlin.serialization)
}
3 - Instantiate with Koin (Recommended)
Koin is the defacto DI solution for Kotlin Multiplatform, and is the ideal tool to provide KSafe as a singleton.
Performance guidance — "prefs" vs "vault": Encryption adds overhead to every write (AES-GCM + Keystore/Keychain round-trip). For data that doesn't need confidentiality — theme preferences, last-visited screen, UI flags — use
mode = KSafeWriteMode.Plainto get SharedPreferences-level speed. Reserve encryption for secrets like tokens, passwords, and PII. The easiest way to enforce this is to create two named singletons:
// ──────────────────────────────────────────────
// common
// ──────────────────────────────────────────────
expect val platformModule: Module
// ──────────────────────────────────────────────
// Android
// ──────────────────────────────────────────────
actual val platformModule = module {
// Fast, unencrypted — for everyday preferences
single(named("prefs")) {
KSafe(
context = androidApplication(),
fileName = "prefs",
memoryPolicy = KSafeMemoryPolicy.PLAIN_TEXT
)
}
// Encrypted — for secrets (tokens, passwords, PII)
single(named("vault")) {
KSafe(
context = androidApplication(),
fileName = "vault"
)
}
}
// ──────────────────────────────────────────────
// iOS
// ──────────────────────────────────────────────
actual val platformModule = module {
single(named("prefs")) {
KSafe(
fileName = "prefs",
memoryPolicy = KSafeMemoryPolicy.PLAIN_TEXT
)
}
single(named("vault")) {
KSafe(
fileName = "vault"
)
}
}
// ──────────────────────────────────────────────
// JVM/Desktop
// ──────────────────────────────────────────────
actual val platformModule = module {
single(named("prefs")) {
KSafe(
fileName = "prefs",
memoryPolicy = KSafeMemoryPolicy.PLAIN_TEXT
)
}
single(named("vault")) {
KSafe(fileName = "vault")
}
}
// ──────────────────────────────────────────────
// WASM — call ksafe.awaitCacheReady() before first encrypted read (see note below)
// ──────────────────────────────────────────────
actual val platformModule = module {
single(named("prefs")) {
KSafe(
fileName = "prefs",
memoryPolicy = KSafeMemoryPolicy.PLAIN_TEXT
)
}
single(named("vault")) {
KSafe(fileName = "vault")
}
}
Then inject by name in your ViewModels:
class MyViewModel(
private val prefs: KSafe, // @Named("prefs") — fast, unencrypted
private val vault: KSafe // @Named("vault") — encrypted secrets
) : ViewModel() {
// UI preferences — no encryption overhead
var theme by prefs("dark", mode = KSafeWriteMode.Plain)
var lastScreen by prefs("home", mode = KSafeWriteMode.Plain)
var onboarded by prefs(false, mode = KSafeWriteMode.Plain)
// Secrets — AES-256-GCM encrypted, hardware-backed keys
var authToken by vault("")
var refreshToken by vault("")
var userPin
