2.0.0-RC1
Pre-releaseRelease candidate for the 2.0 line. Major internal refactor; new standalone :ksafe-biometrics module; Kotlin/JS (IR) target alongside the existing wasmJs target.
Release candidate. On-disk format is stable and the public storage API is source-compatible with 1.8.x. Please file feedback before 2.0.0 final.
Install
dependencies {
implementation("eu.anifantakis:ksafe:2.0.0-RC1")
// Compose property delegate (var x by ksafe.mutableStateOf(...))
implementation("eu.anifantakis:ksafe-compose:2.0.0-RC1")
// Biometric verification — now its own artifact (see Breaking changes)
implementation("eu.anifantakis:ksafe-biometrics:2.0.0-RC1")
}Targets: Android, iOS (x64 / arm64 / simulatorArm64), JVM, Kotlin/JS (IR), Kotlin/WASM.
New Features - Updates
Browser support beyond WasmGC — new Kotlin/JS (IR) target
:ksafe, :ksafe-compose, and :ksafe-biometrics now publish a Kotlin/JS (IR) artifact alongside the existing wasmJs artifact. Projects that build for the legacy JS toolchain — or that need to ship to browsers without WasmGC (anything older than Chrome 119 / Firefox 120 / Safari 18) — are no longer dead-ended.
kotlin {
js(IR) { browser() } // new
@OptIn(ExperimentalWasmDsl::class)
wasmJs { browser() } // existing
}Behaviour is identical to the wasmJs target — same key layout in localStorage, same AES-256-GCM via WebCrypto, same async-only crypto contract — so a project can switch between the two targets and read the same data back.
Custom storage directory (#25, thanks @DeStilleGast)
KSafe(...) now accepts an optional override for where the underlying DataStore file lives — useful for noBackupFilesDir, XDG-compliant locations, test isolation, etc.
// JVM
KSafe(fileName = "settings", baseDir = File(xdgDataHome, "myapp"))
// Android
KSafe(context, fileName = "vault", baseDir = File(context.noBackupFilesDir, "ksafe"))
// iOS
KSafe(fileName = "vault", directory = "/some/path")POSIX 0700 permissions are now also applied to user-supplied paths on JVM (previously skipped), and clearAll() now correctly cleans up files under the custom path on JVM (previously hardcoded to home dir).
iOS default storage moved to Application Support with automatic migration
Pre-2.0 iOS stored the DataStore in NSDocumentDirectory — user-visible and iCloud-syncable by default, both wrong defaults for KSafe content. 2.0 moves the default to NSApplicationSupportDirectory.
Apps upgrading from 1.x do not need to change any code. On first launch under 2.0, KSafe checks for a legacy file at the old Documents path and moves it. The migration is idempotent and best-effort (a failed move logs a warning and leaves the legacy file in place; the app can recover by passing directory = "<old Documents path>").
KSafe data on iOS is effectively device-local regardless of backup state — encryption keys live in the Keychain with …ThisDeviceOnly accessibility, and HARDWARE_ISOLATED keys never leave the Secure Enclave. Even if the DataStore file is included in an iCloud Backup, its encrypted bytes are undecryptable on a restored device.
Single shared KSafeCore orchestrator
Everything that was duplicated across the four platform shells — hot cache, write coalescer, protection-metadata classifier, orphan-ciphertext cleanup, raw get/put/delete/getFlow plumbing — now lives in one KSafeCore class in commonMain.
| File | Before 2.0 | After 2.0 |
|---|---|---|
commonMain (KSafe + KSafeCore + storage interface + concurrency shims) |
~500 | ~1,500 |
KSafe.jvm.kt |
1,360 | 212 |
KSafe.android.kt |
1,584 | 176 |
KSafe.ios.kt |
1,938 | 225 |
KSafe.web.kt |
1,052 | 128 |
| Platform shells, total | 5,934 | 741 |
KSafe itself is now a regular common class — no more expect/actual — with all storage methods (including the inline reified ones) defined exactly once. Bug fixes ship once instead of four times. The consumer call site is unchanged: KSafe(...) on every platform is still spelled the same way.
Breaking changes
Biometric verification moved to its own module (#14, thanks @Coding-Meet)
verifyBiometric, verifyBiometricDirect, clearBiometricAuth, BiometricAuthorizationDuration, and BiometricHelper no longer live on KSafe. They belong to a new static KSafeBiometrics API published as a separate, optional artifact.
// build.gradle.kts
implementation("eu.anifantakis:ksafe-biometrics:2.0.0-RC1"):ksafe-biometrics has zero dependency on :ksafe — apps that only need biometric verification don't pull in the storage library, and apps that don't need biometrics no longer pay for the androidx.biometric / androidx.fragment transitive deps that :ksafe used to drag in.
KSafeBiometrics is a Kotlin object — no DI wiring, no Context parameter, no Application.onCreate init. Android auto-initializes via a ContentProvider declared in the merged manifest (the same pattern WorkManager / Firebase / AppCompat use). The call shape is identical on every target:
val ok = KSafeBiometrics.verifyBiometric("Authenticate")
KSafeBiometrics.verifyBiometricDirect("Authenticate") { success -> /* ... */ }
KSafeBiometrics.clearBiometricAuth()Migration:
// Before (1.x)
import eu.anifantakis.lib.ksafe.BiometricAuthorizationDuration
ksafe.verifyBiometricDirect(reason, BiometricAuthorizationDuration(60_000L)) { ok -> }
// After (2.0)
import eu.anifantakis.lib.ksafe.biometrics.KSafeBiometrics
import eu.anifantakis.lib.ksafe.biometrics.BiometricAuthorizationDuration
KSafeBiometrics.verifyBiometricDirect(reason, BiometricAuthorizationDuration(60_000L)) { ok -> }Method names and signatures are preserved; only the receiver and import paths change. BiometricHelper.confirmationRequired and BiometricHelper.promptTitle continue to be configured the same way, just imported from the new package. Apps that don't use biometric verification need no changes.
iOS default file path changed (auto-migrated)
See iOS default storage moved to Application Support under Highlights. No code change required for upgraders; the library moves the legacy file on first launch.
Fixes
Serializer-kind dispatch in convertStoredValue
Two latent bugs shared a root cause — the read path dispatched on the runtime class of the defaultValue parameter (when (defaultValue) { is Int -> …, is Long -> … }):
- Kotlin/JS: Float / Double reads collapsed into the Int branch.
FloatandDoubleshare a single runtime representation withInton JS (0f is Intreturnstrue), so float / double reads routed into the Int branch and returned the default. - Nullable-typed reads with a
nulldefault lost stored primitives. Forget<Int?>("counter", null), nois Xbranch matched (becausenulldoesn't satisfyis Int), and the path returnednullregardless of whether a value was stored.
Dispatch now runs through the serializer's PrimitiveKind, which preserves the declared type regardless of how the runtime represents the value or how the default is typed. One code path, uniform across every platform.
Transient keystore decrypt errors now propagate on every platform
Pre-2.0 only Android's read path re-threw "device is locked" / "Keystore" errors instead of silently returning defaultValue; iOS and JVM swallowed the same errors. The check now runs in KSafeCore so a locked device (or analogous iOS Keychain error) reliably surfaces to the caller for retry handling.
iOS Keychain orphan sweep strengthened
The startup Keychain sweep — which removes Keychain entries whose DataStore counterpart is gone — was refactored into a standalone, unit-testable function and now covers two item classes: generic-password items (plain AES keys and SE-wrapped blobs) and kSecClassKey EC private keys (the SE-held ECIES keys used for HARDWARE_ISOLATED writes). The second pass catches SE keys that exist without a matching generic-password item — e.g. after a crash between SE-key creation and wrapped-AES-key storage.
Internal changes (no consumer impact)
- New
KSafePlatformStorageinterface; sharedDataStoreStorageadapter for Android / iOS / JVM lives in a newdatastoreMainintermediate source set; web keeps its ownLocalStorageStorageadapter forlocalStorage. KSafeEncryptiongainedencryptSuspend/decryptSuspend/deleteKeySuspend;WebSoftwareEncryptionoverrides them with real WebCrypto calls (the blocking variants throw — WebCrypto is async-only).- The bulk of the previous
wasmJsMainimplementation moved into a sharedwebMainsource set used by bothwasmJsandjs(IR). The fullKSafeTestsuite now runs on both targets via a sharedwebTest/WebKSafeTest.kt. - New cross-type migration tests (
testCrossTypeIntToLongPlain,testCrossTypeLongToIntPlainOutOfRange,testSequentialTypeMigrationIntThenLong, etc.) lock in the safe-narrow / refuse-truncate / read-after-rewrite contracts that were implicit in pre-refactor code. - New
WebInteropSmokeTestruns on both wasmJs and js and asserts the per-target actuals:localStorageround-trip, enumeration,currentTimeMillisWeb()plausibility,secureRandomBytes()non-determinism. - Internal types (
KSafeCore,KSafePlatformStorage,KSafeEncryption,KeySafeMetadataManager,SecurityChecker,KSafeSecureRandom, the per-platform engines, allexpect/actualshims) moved toeu.anifantakis.lib.ksafe.internal. Public-facing types stay at the root package — no consumer imports break.
Upgrade notes
- On-disk format is unchanged. Existing 1.8.x data reads cleanly.
- Storage API is unchanged.
import eu.anifantakis.lib.ksafe.KSafeand friends still resolve;ksafe.put(...)/ksafe.get(...)/by ksafe(0)delegates continue to work. - Biometric API moved. See the Breaking changes section for the migration to
:ksafe-biometrics. If your project doesn't use biometric verification, no action needed. - iOS users: the first launch under 2.0 migrates your DataStore from
DocumentstoApplication Support. No code change needed. - Recommended smoke-test on upgrade: write a handful of values of each type in a pre-2.0 build, upgrade the library, and verify reads on first launch. The cross-type migration tests give high confidence, but custom
KSerializeror@Contextualusers should validate their specific types once. - Known follow-up:
isStringSerializerininternal/KSafeSerializerUtil.ktis unused (superseded byprimitiveKindOrNulldispatch). Kept for one release for anyone inlining against it; will be removed in 2.1.
Acknowledgements
- @DeStilleGast — custom storage directory (#25)
- @Coding-Meet — biometric module suggestion (#14)
- @HansHolz09 —
BiometricHelper.confirmationRequired(shipped in 1.8.1, carried into 2.0)
Full Changelog: 1.8.1...2.0.0-RC1