2.1.0
OS-native key custody on JVM and Web, plus a new cross-platform key-protection diagnostic API. Drop-in upgrade — on-disk format is unchanged and existing 2.0 keys migrate on first read.
Highlights
- JVM keys now live in the OS secret store — Windows DPAPI, macOS login Keychain, or Linux Secret Service (libsecret) via JNA — instead of Base64'd next to the ciphertext in the DataStore file.
- Web keys are now non-extractable by the browser. The WebCrypto
CryptoKey(extractable = false) lives in IndexedDB; raw key bytes are no longer recoverable by XSS, extensions, or profile reads. - New
KSafe.protectionInfoAPI — instance-level diagnostic on a universally-ordered scale (SOFTWARE < SANDBOX_PROTECTED < HARDWARE_BACKED < HARDWARE_ISOLATED). Drives startup gates, telemetry, UI badges, runtime feature policy. KSafeKeyInfo.level— per-key audit on the same universal scale. Pair it withprotectionInfofor instance-level and per-key threshold checks.- Regression fix —
get/getFlowwith a nullable default on@Serializableclasses (#31, thanks @DestBro).
Security
- JVM keys now live in the OS secret store. The AES key is protected by Windows DPAPI, the macOS Keychain, or the Linux Secret Service (libsecret) via JNA, instead of being Base64-encoded next to the data in the DataStore file. When no secret store is reachable (headless Linux with no keyring, JNA link failure, …) KSafe falls back to the legacy in-file scheme and logs a one-time security warning. Keys written by KSafe ≤ 2.0 are migrated on first read: copied into the OS store, then removed from the DataStore file only after the OS store is read back and byte-verified (a buggy or again-unavailable keyring that silently no-ops cannot destroy the only copy). Migration is hybrid: lazy per-key on first read plus a one-time best-effort background sweep so a key that's never read again doesn't linger in the file. Opt out with
-Dksafe.jvm.keyVault=software(or envKSAFE_JVM_KEY_VAULT=software). - Web keys are now non-extractable. The browser engine generates an
extractable = falseAES-GCMCryptoKeyand persists the live key object in IndexedDB, instead of exporting the raw key and Base64-ing it intolocalStorage. A legacylocalStoragekey is imported as non-extractable on first access and thelocalStorageentry deleted; previously encrypted data keeps decrypting. Same hybrid lazy + background-sweep migration as JVM.
Added
-
KSafe.protectionInfo: KSafeProtectionInfo— public, cross-platform diagnostic that reports the key custody thisKSafeis actually running with, including any runtime fallback negotiated at construction. Read once at startup:val info = ksafe.protectionInfo // Gate startup check(info.effectiveLevel >= KSafeProtectionLevel.SANDBOX_PROTECTED) // Detect silent fallback (effective < intended) check(info.effectiveLevel >= info.intendedLevel) // Telemetry analytics.log("ksafe_protection", "level" to info.effectiveLevel.name, "custody" to info.custody, "notes" to info.notes.joinToString(","))
Introduces:
KSafeProtectionLevel— universally-ordered scale:SOFTWARE<SANDBOX_PROTECTED<HARDWARE_BACKED<HARDWARE_ISOLATED. One ordinal comparison works across every platform.KSafeProtectionInfo(intendedLevel, effectiveLevel, custody, notes)—effectiveLevelis the actionable field;intendedLevelis the engine's baseline target so consumers can detect when negotiation fell short.custodyis a human-readable description (display, never parse);notesis a list of stable lowercase_snake codes (jvm_os_vault_unavailable,jvm_user_opted_out,apple_secure_enclave_absent).
Per-platform population: Android / Apple report
HARDWARE_BACKEDbaselines (StrongBox / Secure Enclave remain per-write upgrades viaKSafeWriteMode.Encrypted(HARDWARE_ISOLATED)); JVM reportsSANDBOX_PROTECTEDwhen the OS vault is healthy and falls toSOFTWAREwith the appropriatenotescode when the vault self-test fails or the user opts out; Web reportsSANDBOX_PROTECTED(browser-origin sandbox). Full guide and runtime-decision patterns indocs/PROTECTION_INFO.md. -
KSafeKeyInfo.level: KSafeProtectionLevel— per-key audit now reports on the same universal scale asprotectionInfo. Layered checks become possible (gate the engine at startup and refuse to use a specific high-sensitivity key if its own custody didn't meet the bar):val tokenLevel = ksafe.getKeyInfo("auth_token")?.level check(tokenLevel != null && tokenLevel >= KSafeProtectionLevel.HARDWARE_BACKED)
Gives JVM and Web richer granularity than the legacy
KSafeKeyInfo.storage— JVM OS-vault keys and Web browser-origin keys now reportSANDBOX_PROTECTED; only the plaintext-in-file JVM fallback still reportsSOFTWARE. -
JNA dependency on the JVM target (
net.java.dev.jna+jna-platform) for the OS secret-store integration above. JVM / Desktop consumers only.
Deprecated
KSafeKeyInfo.storage: KSafeKeyStorage— superseded byKSafeKeyInfo.level: KSafeProtectionLevel.storagekeeps working with a@Deprecated(ReplaceWith("level"))annotation; planned removal in 3.0.
Fixed
get/getFlowwith a nullable default now deserialize@Serializableclasses correctly (#31, thanks @DestBro). Callingget(key, null as MyType?)orgetFlow(key, null as MyType?)on a@Serializableclass whose first property is a primitive (e.g. a leadingString) threwClassCastException: java.lang.String cannot be cast to MyType— a regression introduced in 2.0.0.primitiveKindOrNullwas descending into the class's first field for a nullable serializer and misclassifying the type as aString, so the raw stored JSON was returned instead of being decoded. A non-null default (get(key, MyType())) was unaffected.
Documentation
- New:
docs/PROTECTION_INFO.md— the newKSafe.protectionInfoAPI: model, per-platform truth table, definednotescodes, and five runtime-decision patterns (gating, tighter re-auth windows, feature disablement, UX honesty banners, intended-vs-effective delta checks). - New:
docs/JVM_PROTECTION.md— platform-by-platform deep dive on the JVM OS vaults (DPAPI / Keychain / libsecret): what each store actually is, threat model per OS, the self-test, the software fallback, the opt-out, and the per-app namespace.
Build
- Suppressed
IncorrectCompileOnlyDependencyWarningfor thecompose-runtimecompileOnlydependency on Native / JS / Wasm targets. The dep is intentionallycompileOnlyso non-Compose consumers (Ktor servers, CLI tools, plain JVM) don't pullcompose-runtimeonto their runtime classpath —@StablehasBINARYretention and no runtime cost. Native / JS / Wasm consumers using:ksafewithout Compose must declarecompose-runtimethemselves to compile against the published klib (accepted trade-off; promoting toapiwould forcecompose-runtimeonto every consumer's runtime classpath).
Upgrade notes
- No source-level changes required for existing 2.0 consumers.
ksafe.put/ksafe.get/by ksafe(0)and all delegates are unchanged. - No on-disk format change. Existing 2.0 ciphertext continues to decrypt; the AES key migrates to the OS-backed custody automatically on first read.
- The legacy
KSafeKeyInfo.storagefield still works. New code should preferlevel(IDE quick-fix offers the replacement).
Full Changelog: 2.0.0...2.1.0