KSafe

Effortlessly encrypts and persists data using hardware-backed security, offering one-code-path simplicity, seamless integration, and protection for sensitive information like OAuth tokens.

Android JVMJVMKotlin/NativeWasmJS
GitHub stars272
Authorsioannisa
Open issues1
LicenseApache License 2.0
Creation date11 months ago

Last activityabout 15 hours ago
Latest release2.0.0-RC2 (about 20 hours ago)

KSafe — Universal Key/Value Persistence for Kotlin Multiplatform and Android

  • Encrypted by default. Plain (unencrypted) when needed.
  • Persist variables, Compose State, StateFlow, and serializable objects across Android, iOS, Desktop, and Web
  • Easy to use by design

Maven Central License

image

Demo Application

KSafe in action across multiple scenarios: Demo CMP App Using KSafe.

YouTube Demos

From the author and the community:

Author's Video Philipp Lackner's Video Jimmy Plazas's Video
image image image
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 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.

  • Easy? ✔ Yes — one-line setup, property-delegate API
  • Encrypted by default? ✔ Yes — AES-256-GCM by default
  • Plain storage? ✔ Yes — opt out with one parameter
  • Synchronous? ✔ Yes — non-blocking hot-cache reads when you do not want coroutines
  • Asynchronous? ✔ Yes — full suspend API when you want guaranteed disk flushes Extras when you encrypt:
  • Biometrics? ✔ Yes — Face ID / Touch ID / Fingerprint on Android + iOS, with auth caching. Ships as the standalone optional ksafe-biometrics module so apps that don't need it pay nothing.
  • Root/jailbreak detection? ✔ Yes — configurable WARN/BLOCK actions + analytics callback
  • Memory policy? ✔ Yes — three RAM modes trading security vs performance
  • Database passphrase in one line? ✔ Yes — hardware-isolated 256-bit secret for SQLCipher / SQLDelight / Room

Where KSafe fits

KSafe brings together two things that are usually separate in Kotlin Multiplatform:

  1. effortless key/value persistence
  2. and serious encrypted storage.

Use it as a general-purpose persistence layer for settings, preferences, app state, Compose MutableState, and MutableStateFlow.

  • By default, KSafe stores values with AES-256-GCM encryption, backed by platform security primitives where available.
  • When encryption is not required for a specific entry, you can opt into plain storage with 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.

One line. Encrypted by default.

var counter by ksafe(0)
counter++   // auto-encrypted (AES-256-GCM), auto-persisted, survives process death

Read 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.

Don't need encryption? Same one-liner.

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.

Compose MutableState? MutableStateFlow? Plain delegate? All persisted.

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 emission

All 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, and getDirect/putDirect are 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 the suspend API (get / put) only when you want to.

Prefer coroutines? put and get too.

// 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.

Need a passphrase to encrypt databases? Also one line. (v1.8.0)

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.

Complex Objects? Of course.

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.

Table of Contents


Quickstart

// 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).


Setup

Maven Central

1 - Add the Dependency

// 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-compose if you don't use Jetpack Compose or mutableStateOf persistence.

Skip ksafe-biometrics if you don't need Face ID / Touch ID / Fingerprint verification. The biometrics module is fully independent — it has no dependency on :ksafe and can be used on its own to protect any action in your app.

Note: kotlinx-serialization-json comes in transitively — don't add it yourself.

2 - Apply the kotlinx-serialization plugin

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)
}

3 - Instantiate

// 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.

Basic Usage

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.

Custom JSON Serialization

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.


Cryptographic Utilities

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")

secureRandomBytes lives under eu.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.


Why use KSafe?

  • Hardware-backed security — AES-256-GCM, keys in Android Keystore / iOS Keychain / JVM software / WebCrypto. Per-property control via KSafeWriteMode + KSafeEncryptedProtection tiers
  • Biometric auth — Face ID, Touch ID, Fingerprint, with auth caching
  • Root & jailbreak detection — configurable WARN/BLOCK actions
  • Clean reinstalls — automatic cleanup on fresh install
  • One code path — no expect/actual juggling; common code owns the vault
  • Ease of usevar launchCount by ksafe(0), that is literally it
  • Versatility — primitives, data classes, sealed hierarchies, lists, sets, nullables
  • Performance — zero-latency UI reads via hybrid hot cache
  • Desktop & Web — full JVM/Desktop and browser support on both Kotlin/WASM and Kotlin/JS alongside Android and iOS

How KSafe Compares

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:

  • PrimitivesBoolean?, Int?, Long?, Float?, Double?, String? all round-trip through get / put / getDirect / putDirect / getFlow. null is 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 @Serializable class can be stored nullably. null round-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 scope

By contrast, multiplatform-settings exposes nullability only through separate getStringOrNull / getIntOrNull scalar 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.


Performance Benchmarks

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.

Compatibility

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)

Advanced Topics


Biometric Authentication

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 KSafe itself. In 2.0 they moved to a separate module. Add implementation("eu.anifantakis:ksafe-biometrics:2.0.0-RC2"), change import eu.anifantakis.lib.ksafe.BiometricAuthorizationDurationimport eu.anifantakis.lib.ksafe.biometrics.BiometricAuthorizationDuration, replace ksafe.verifyBiometric(...) with KSafeBiometrics.verifyBiometric(...). Method names and signatures are unchanged. No instance to construct, no DI wiring needed.


Runtime Security Policy

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.


Memory Security Policy

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.


Deep-Dive Documentation

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: BooleanKSafeWriteMode), and v1.1.x → v1.2.0+
Alternatives & Comparison KSafe vs EncryptedSharedPrefs, KVault, SQLCipher, and more

Licence

Licensed under the Apache License 2.0 — see http://www.apache.org/licenses/LICENSE-2.0. Distributed "AS IS", without warranties of any kind.

Android JVMJVMKotlin/NativeWasmJS
GitHub stars272
Authorsioannisa
Open issues1
LicenseApache License 2.0
Creation date11 months ago

Last activityabout 15 hours ago
Latest release2.0.0-RC2 (about 20 hours ago)

KSafe — Universal Key/Value Persistence for Kotlin Multiplatform and Android

  • Encrypted by default. Plain (unencrypted) when needed.
  • Persist variables, Compose State, StateFlow, and serializable objects across Android, iOS, Desktop, and Web
  • Easy to use by design

Maven Central License

image

Demo Application

KSafe in action across multiple scenarios: Demo CMP App Using KSafe.

YouTube Demos

From the author and the community:

Author's Video Philipp Lackner's Video Jimmy Plazas's Video
image image image
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 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.

  • Easy? ✔ Yes — one-line setup, property-delegate API
  • Encrypted by default? ✔ Yes — AES-256-GCM by default
  • Plain storage? ✔ Yes — opt out with one parameter
  • Synchronous? ✔ Yes — non-blocking hot-cache reads when you do not want coroutines
  • Asynchronous? ✔ Yes — full suspend API when you want guaranteed disk flushes Extras when you encrypt:
  • Biometrics? ✔ Yes — Face ID / Touch ID / Fingerprint on Android + iOS, with auth caching. Ships as the standalone optional ksafe-biometrics module so apps that don't need it pay nothing.
  • Root/jailbreak detection? ✔ Yes — configurable WARN/BLOCK actions + analytics callback
  • Memory policy? ✔ Yes — three RAM modes trading security vs performance
  • Database passphrase in one line? ✔ Yes — hardware-isolated 256-bit secret for SQLCipher / SQLDelight / Room

Where KSafe fits

KSafe brings together two things that are usually separate in Kotlin Multiplatform:

  1. effortless key/value persistence
  2. and serious encrypted storage.

Use it as a general-purpose persistence layer for settings, preferences, app state, Compose MutableState, and MutableStateFlow.

  • By default, KSafe stores values with AES-256-GCM encryption, backed by platform security primitives where available.
  • When encryption is not required for a specific entry, you can opt into plain storage with 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.

One line. Encrypted by default.

var counter by ksafe(0)
counter++   // auto-encrypted (AES-256-GCM), auto-persisted, survives process death

Read 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.

Don't need encryption? Same one-liner.

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.

Compose MutableState? MutableStateFlow? Plain delegate? All persisted.

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 emission

All 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, and getDirect/putDirect are 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 the suspend API (get / put) only when you want to.

Prefer coroutines? put and get too.

// 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.

Need a passphrase to encrypt databases? Also one line. (v1.8.0)

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.

Complex Objects? Of course.

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.

Table of Contents


Quickstart

// 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).


Setup

Maven Central

1 - Add the Dependency

// 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-compose if you don't use Jetpack Compose or mutableStateOf persistence.

Skip ksafe-biometrics if you don't need Face ID / Touch ID / Fingerprint verification. The biometrics module is fully independent — it has no dependency on :ksafe and can be used on its own to protect any action in your app.

Note: kotlinx-serialization-json comes in transitively — don't add it yourself.

2 - Apply the kotlinx-serialization plugin

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)
}

3 - Instantiate

// 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.

Basic Usage

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.

Custom JSON Serialization

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.


Cryptographic Utilities

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")

secureRandomBytes lives under eu.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.


Why use KSafe?

  • Hardware-backed security — AES-256-GCM, keys in Android Keystore / iOS Keychain / JVM software / WebCrypto. Per-property control via KSafeWriteMode + KSafeEncryptedProtection tiers
  • Biometric auth — Face ID, Touch ID, Fingerprint, with auth caching
  • Root & jailbreak detection — configurable WARN/BLOCK actions
  • Clean reinstalls — automatic cleanup on fresh install
  • One code path — no expect/actual juggling; common code owns the vault
  • Ease of usevar launchCount by ksafe(0), that is literally it
  • Versatility — primitives, data classes, sealed hierarchies, lists, sets, nullables
  • Performance — zero-latency UI reads via hybrid hot cache
  • Desktop & Web — full JVM/Desktop and browser support on both Kotlin/WASM and Kotlin/JS alongside Android and iOS

How KSafe Compares

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:

  • PrimitivesBoolean?, Int?, Long?, Float?, Double?, String? all round-trip through get / put / getDirect / putDirect / getFlow. null is 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 @Serializable class can be stored nullably. null round-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 scope

By contrast, multiplatform-settings exposes nullability only through separate getStringOrNull / getIntOrNull scalar 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.


Performance Benchmarks

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.

Compatibility

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)

Advanced Topics


Biometric Authentication

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 KSafe itself. In 2.0 they moved to a separate module. Add implementation("eu.anifantakis:ksafe-biometrics:2.0.0-RC2"), change import eu.anifantakis.lib.ksafe.BiometricAuthorizationDurationimport eu.anifantakis.lib.ksafe.biometrics.BiometricAuthorizationDuration, replace ksafe.verifyBiometric(...) with KSafeBiometrics.verifyBiometric(...). Method names and signatures are unchanged. No instance to construct, no DI wiring needed.


Runtime Security Policy

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.


Memory Security Policy

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.


Deep-Dive Documentation

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: BooleanKSafeWriteMode), and v1.1.x → v1.2.0+
Alternatives & Comparison KSafe vs EncryptedSharedPrefs, KVault, SQLCipher, and more

Licence

Licensed under the Apache License 2.0 — see http://www.apache.org/licenses/LICENSE-2.0. Distributed "AS IS", without warranties of any kind.