Skip to content

2.0.0

Choose a tag to compare

@ioannisa ioannisa released this 12 May 21:07

Major release: KMP refactor, new macOS and Kotlin/JS targets, biometrics extracted into its own module, and significant performance work on encrypted reads/writes.

The changes listed below are in addition to the work shipped in 2.0.0-RC1 and 2.0.0-RC2 — see those sections for the complete overview of what the entire 2.0.0 encompasses, including these releases.

Highlights

  • Faster encrypted reads and writes. A new per-datastore master-key envelope (v2) eliminates Keystore/Keychain IPC on every encrypted read and write. Biggest wins on stores with many encrypted entries.
  • New default memory policy: LAZY_PLAIN_TEXT. Cheap cold start (no bulk decrypt), O(1) reads after first access. Replaces ENCRYPTED as the default on Android, iOS, macOS, and JVM.
  • New platforms. Native macOS (macosX64, macosArm64) and Kotlin/JS (IR) across all modules.
  • Biometrics is now its own module. KSafeBiometrics ships as the optional :ksafe-biometrics artifact — apps without biometrics no longer pull in androidx.biometric.
  • Migrated to AGP 9.2 + Gradle 9.4. All three modules now use the unified KMP library plugin.

Added

  • KSafeMemoryPolicy.LAZY_PLAIN_TEXT — new default on Android, iOS, macOS, JVM. Keeps ciphertext on cold start, decrypts each key on first read, then caches plaintext for the process lifetime. Web stays PLAIN_TEXT (WebCrypto is async-only).

  • Native macOS targets (macosX64, macosArm64) across :ksafe, :ksafe-compose, :ksafe-biometrics (#26, thanks @tomasjablonskis). Uses the same Keychain + CryptoKit + Secure Enclave path as iOS via shared appleMain. Intel Macs without a T2 fall back to plain Keychain.

  • SecurityChecker short-circuits on macOS — the iOS jailbreak heuristics would otherwise flag every Mac as rooted.

  • macosTest source set — 73 new tests plus the full common KSafeTest suite.

  • allowDeviceCredentialFallback on verifyBiometric / verifyBiometricDirect (#29, thanks @Trucodisparo). New optional Boolean (default true). Set false to restrict to biometrics only — no PIN/password/pattern fallback. JVM/JS/WasmJS ignore it.

    val ok = KSafeBiometrics.verifyBiometric(
        reason = "Confirm payment",
        allowDeviceCredentialFallback = false
    )

Fixed

  • Compatibility with dev.whyoleg.cryptography 0.6.0 (#27, #30, via #28, thanks @HarukeyUA @chirag38-unity). Resolves the runtime IrLinkageError on iOS when a consumer app pulled cryptography-kotlin 0.6.0 transitively.
  • Critical: Secure Enclave key destruction during 1.x → 2.0 migration (latent in RC1 and RC2). A startup-ordering race could let the orphan-cleanup sweep run against an empty DataStore snapshot and irreversibly destroy Secure Enclave EC private keys. KSafeCore now waits for the first snapshotFlow emission before migrating; orphan cleanup refuses to delete when DataStore is empty but Keychain has entries. Pinned by KSafeCoreStartupOrderingTest.
  • macOS biometrics now work on every Mac. Switched to LAPolicyDeviceOwnerAuthentication on macOS — falls back to login password or Apple Watch on Macs without Touch ID (Mac mini, many Intel Macs). iOS unchanged.
  • verifyBiometric (suspend) now dispatches evaluatePolicy on Main on Apple platforms, matching verifyBiometricDirect.

Changed

  • Default memory policy is now LAZY_PLAIN_TEXT (was ENCRYPTED) on Android, iOS, macOS, JVM. Apps that need ciphertext-at-rest semantics must opt in explicitly with KSafeMemoryPolicy.ENCRYPTED or ENCRYPTED_WITH_TIMED_CACHE. Web's forced PLAIN_TEXT is unchanged.
  • PLAIN_TEXT is now discouraged in KDoc. Its eager-decrypt-everything cold start is O(n) in encrypted keys and can push first-read latency into ANR territory on large Android stores. LAZY_PLAIN_TEXT matches its steady-state read performance with a much cheaper start. PLAIN_TEXT is still supported.
  • IosKeychainEncryptionAppleKeychainEncryption (and surrounding Ios*Apple* renames) to reflect shared iOS + macOS use. @PublishedApi internal; only consumer code that references these symbols directly is affected.
  • macOS Keychain prompt — doc-only. Factory KDoc now flags that unsandboxed Mac apps see a system password prompt on first Keychain access (suppressed by signing with a Keychain access group entitlement).

Performance

  • v2 envelope routes every KSafeProtection.DEFAULT encrypted write through one of two AES-256 master keys per datastore (a relaxed-accessibility variant and a requireUnlockedDevice = true variant), unwrapped once at construction and cached in-process. After warm-up, encrypt and decrypt are pure-CPU AES-GCM — no Keystore/Keychain IPC for the lifetime of the process. KSafeProtection.HARDWARE_ISOLATED writes still get a per-entry key (StrongBox / Secure Enclave isolation is the point). Existing v1 and legacy entries continue to read through the per-entry path unchanged — no migration, no rewrite. Entries written by 2.0 cannot be read by 1.x.
  • Parallel batch encrypt — encrypted writes in a batch deduplicate by key and run concurrently with a Semaphore(8) cap. ENCRYPTED memory policy no longer pays a write-time penalty over PLAIN_TEXT.
  • Parallel cold-start decryptupdateCache and cleanupOrphanedCiphertext now decrypt concurrently. Cold-start time on a 1500-key encrypted store drops from ~27 ms to under 1 ms.
  • detectProtection short-circuit trusts 2.0 metadata authoritatively when present, saving an allocation and a map lookup per unencrypted read.
  • AndroidKeystoreEncryption micro-optimisations — lazy companion-level KeyStore, zero-copy GCM decrypt, single-allocation encrypt buffer, collapsed containsAlias + getKey/deleteEntry IPC round-trips.
  • AppleKeychainEncryption key-byte cache — repeat encrypt/decrypt on the same key short-circuits both SecItemCopyMatching and the SE SecKeyCreateDecryptedData ECIES unwrap. Brings Apple in line with the per-alias caches Android and JVM already had.
  • Suspend put / delete now go through the write coalescer. 500 concurrent suspend writes show 5–27× lower per-op latency depending on encryption mode.
  • hasAnyEncryptedKey atomic flag lets plain-only stores skip the protectionMap lookup on every read.
  • Refreshed benchmarks in docs/BENCHMARKS.md — median of 4 runs on a Galaxy S24. Suspend-API cells now exercise concurrent coroutines, reflecting real-world usage.

Tooling

  • AGP 8.13.19.2.1, Gradle 8.14.49.4.1. :ksafe migrated from com.android.library to com.android.kotlin.multiplatform.library, aligning all three modules. androidInstrumentedTest source set renamed to androidDeviceTest. Removed obsolete gradle.properties flags now defaulted in AGP 9 (android.useAndroidX, android.nonTransitiveRClass, kotlin.kmp.isolated-projects.support).

Validation

  • :ksafe:macosArm64Test — 118 tests (73 new + 45 common).
  • :ksafe:iosSimulatorArm64Test — 127 tests, no regression.
  • Android instrumented tests — 64/64 on emulator and physical Galaxy S24.
  • All linkDebugFramework* and cross-target compile tasks pass cleanly.
  • End-to-end exercised via KSafeDemo on all six targets.

Full Changelog: 2.0.0-RC2...2.0.0