Skip to content

chore(deps): bump eu.anifantakis:ksafe from 2.2.1 to 3.0.0 - #210

Open
dependabot[bot] wants to merge 1 commit into
mainfrom
dependabot/gradle/eu.anifantakis-ksafe-3.0.0
Open

chore(deps): bump eu.anifantakis:ksafe from 2.2.1 to 3.0.0#210
dependabot[bot] wants to merge 1 commit into
mainfrom
dependabot/gradle/eu.anifantakis-ksafe-3.0.0

Conversation

@dependabot

@dependabot dependabot Bot commented on behalf of github Aug 1, 2026

Copy link
Copy Markdown
Contributor

Bumps eu.anifantakis:ksafe from 2.2.1 to 3.0.0.

Release notes

Sourced from eu.anifantakis:ksafe's releases.

3.0.0

Major Feature: Key Rotation

KSafe can now rotate its encryption keys — on Android, iOS, macOS, JVM Desktop, and the web.

Upgrading to 3.0.0 is safe and boring: your existing data is untouched, byte for byte, and nothing migrates on first launch. The interesting part only starts when you decide to rotate.

implementation("eu.anifantakis:ksafe:3.0.0")
implementation("eu.anifantakis:ksafe-compose:3.0.0")     // ← Compose state (optional)
implementation("eu.anifantakis:ksafe-biometrics:3.0.0")  // ← Biometric auth (optional)

✨ New Features

Key rotation

Your encrypted data is protected by a key. Rotation replaces that key with a fresh one and re-encrypts everything under it, then deletes the old key. Security standards ask for this periodically; after a suspected compromise, it's the whole ballgame.

It's one call:

val result = ksafe.rotateKeys()
// KSafeRotationResult(rotated = 42, skipped = 0, failed = 0, keyGeneration = 2)

Doing this safely is harder than it looks, so here is what KSafe guarantees:

If the app crashes mid-rotation, nothing is lost. Every entry remembers which key generation encrypts it. So a rotation that dies halfway leaves you with some entries on the old key and some on the new — and both still read fine. Old keys stick around until the last entry using them is gone. Call rotateKeys() again and it picks up where it left off. No journal, no repair step, no "corrupted store" state.

Writes that happen during a rotation are never lost. If your app saves a value at the exact moment rotation is re-encrypting that same value, the new value wins. Rotation can't overwrite it, and it can't bring back the old one.

Your data never changes — only the key does. This matters most for getOrCreateSecret. If you store a SQLCipher passphrase there, rotation does not generate a new passphrase. The passphrase stays identical; only the key protecting it changes. Your database still opens.

Locked devices are handled, not failed. Entries marked requireUnlockedDevice = true can only be rotated while the device is unlocked. If the device is locked, those entries come back in skipped — not failed — and the next rotation picks them up.

You can also rotate automatically, on a schedule:

KSafeConfig(keyRotationPolicy = KSafeKeyRotationPolicy.MaxAge(90.days))

This checks once at startup, in the background — it never blocks your app's launch or any read. The default is Never, on purpose: key material lives in hardware and doesn't expire, so automatic rotation is a compliance/hygiene choice, not something everyone needs.

... (truncated)

Changelog

Sourced from eu.anifantakis:ksafe's changelog.

[3.0.0] - 2026-07-26

Major release: key rotation on every platform.

Read this before upgrading. Installing 3.0.0 is a drop-in upgrade — an un-rotated store's existing entries keep the exact key names and bytes 2.2.x used, so nothing migrates and nothing breaks on first launch. But two things you do after upgrading cannot be undone:

  • Calling rotateKeys(), and
  • any write to a strict HARDWARE_ISOLATED entry (these now key under 3.0.0's strict alias variant — see the unlock-policy fix below).

Entries in either state are unreadable by a pre-3.0.0 binary, and a 2.2.1 binary does not merely fail to read them — its startup orphan sweep deletes rows it cannot decrypt. So downgrading after a rotation or a strict write destroys those entries. That one-way door, not the API changes, is why this is a major version: until you rotate or write strict, you can still go back.

The API changes themselves are small. Three removals, each a one-line migration — see Removed below. Nothing else in the public surface changes. One internal symbol did change shape, though — the failure hook that Compose's inline mutableStateOf bakes into calling code — so a library compiled against 2.x's ksafe-compose has to be rebuilt against 3.0.0 rather than swapped in at runtime. Rebuilding is all it takes; no source changes.

Added

  • KSafe.rotateKeys() — one-call key rotation on Android, iOS/macOS, JVM Desktop, and the web. Re-encrypts every encrypted entry under a fresh key generation and deletes every superseded key nothing references anymore; returns KSafeRotationResult(rotated, skipped, failed, keyGeneration). Design guarantees:
    • Crash-safe and resumable without a journal: each entry's metadata records the key generation that decrypts it, so an interrupted rotation leaves a mixed-generation store where everything stays readable — old keys are retained until the last entry referencing them is gone, and the next call rotates the remainder.
    • Concurrent writes always win: each entry commits under a compare-and-swap on its stored ciphertext, serialized on KSafe's write consumer — a rotation can never clobber or resurrect a value.
    • Values are sacred: rotation changes key material and envelopes, never data. getOrCreateSecret secrets keep their value (only the wrapping key changes), so database passphrases survive.
    • Strict entries (requireUnlockedDevice) rotate only while the device is unlocked; locked ones are reported as skipped and picked up by a later pass.
    • Legacy (pre-2.x envelope) entries are upgraded to the current envelope as a side effect.
    • Every instance follows the store's generation, including a lazyLoad one with no background collector: it re-reads the persisted generation rather than trusting the value it sampled on its first batch, so a sibling's rotation cannot leave it minting keys under a superseded one.
  • KSafeConfig.keyRotationPolicy — declarative automatic rotation. KSafeKeyRotationPolicy.MaxAge(duration) checks once per startup (in the background, never blocking startup or reads) whether the current key generation is older than allowed and rotates if so; the age clock starts at the first launch under the policy and restarts on every rotation. The default remains Never — key material is hardware/OS-protected and does not expire, so automatic rotation is an opt-in hygiene/compliance control, matching Keystore/Keychain/Tink norms.
  • KSafeKeyInfo.keyGeneration — reports the generation that decrypts a specific entry (1 = never rotated), so a rotation can be audited per key.
  • Authenticated encryption envelope (v3) via rotation. Rotating a store upgrades its entries to an AES-GCM envelope whose associated data binds each ciphertext to its identity (user key) and security metadata (protection tier, unlock policy, key generation). An attacker with raw file access can no longer relocate a ciphertext to another entry, or tamper the metadata that routes it, and have it decrypt in the wrong context — the read fails closed to the caller's default. A generation-1 (un-rotated) store keeps the exact pre-3.0.0 bytes, so identity authentication begins at the first rotateKeys(). (Threaded through every platform engine: JVM AES-GCM, Android DEK + TEE + KEK, Apple CryptoKit, and both web WebCrypto actuals.)
  • KSafeProtectionInfo.isEncryptionOperational — a one-line, cross-platform preflight for "will encrypted writes actually succeed?". Written once in common code (no platform guards), it is true wherever encryption works — including the weaker-but-still-working JVM-software and iOS-Simulator fallbacks — and false only where encrypted ops genuinely can't run: a web page served outside a secure context (no crypto.subtle), or a JVM whose OS key vault exists but is unreachable at startup (a locked Keychain/keyring, or a headless launch) so KSafe refuses to mint keys — in both cases an encrypted write would otherwise fail silently. Lets an app refuse to proceed (e.g. block login until it is served over HTTPS) rather than shipping a store that can't persist. Deliberately distinct from protection strength (effectiveLevel vs intendedLevel), so it never false-blocks a functional-but-weaker fallback.
  • Biometric prompt text is configurable from common code — and on the web it finally names the passkey. KSafeBiometrics.defaultTitle / defaultReason / defaultCancelLabel set process-wide defaults once at startup, and verifyBiometric / verifyBiometricDirect gained matching per-call title / cancelLabel parameters (appended after the existing ones, so every existing call compiles and behaves unchanged). The title is the app/service name: Android shows it as the prompt title, and the web uses it to name the passkey (rp.name, user.name, user.displayName) — previously hardcoded, so every KSafe-using site was listed as "ksafe" in the user's password manager. Apple and JVM prompts have no title and ignore it. The reason keeps its existing per-platform mapping (Android subtitle, Apple localizedReason, Windows Hello / macOS message) and now defaults to whatever the app sets instead of a built-in English string. Blank text counts as unset everywhere, so a string resource that resolved to "" falls back to the platform default instead of failing the prompt; and on Android cancelLabel applies only to a biometrics-only prompt (allowDeviceCredentialFallback = false), because the platform forbids a negative button once it offers its own PIN/pattern affordance. The web passkey name is written once, at registration — see docs/BIOMETRICS.md. resetRegistration() now also tells the passkey provider that the credential it abandons is no longer recognised (WebAuthn signalUnknownCredential), so the orphan it used to leave beside the next enrollment is cleaned up where the browser supports it — best-effort and feature-detected, and a browser or provider that ignores it simply keeps the old behaviour. KSafeBiometricsWeb.isRegistered / registeredTitle expose what is currently enrolled, so an app that renames itself can re-enroll exactly once (registeredTitle != defaultTitleresetRegistration()) instead of on every launch; both describe KSafe's own local record, not the authenticator's state.
  • New doc: https://github.com/ioannisa/KSafe/blob/main/docs/KEY_ROTATION.md, including an honest cryptographic-erasure section (per-platform what "deleted" means, and why clearAll() empties the store rather than shredding the file).

Deprecated

  • Everything still carrying a @Deprecated warning will be removed in 4.0.0. Nothing changes in 3.0.0 — these keep working exactly as before — but this is the last major that carries them, so migrate at your convenience rather than under pressure:
    • the encrypted: Boolean parameter on getDirect / get / putDirect / put / getFlow / getStateFlow, the ksafe(...) delegate, and Compose's mutableStateOf — replace with mode = KSafeWriteMode.Encrypted() or KSafeWriteMode.Plain (deprecated since 1.7.0);
    • the protection parameter on the read overloads — protection is auto-detected on reads, so just drop it;
    • the per-instance useStrongBox (Android) and useSecureEnclave (Apple) constructor flags — use a per-write KSafeWriteMode.Encrypted(KSafeEncryptedProtection.HARDWARE_ISOLATED), which is per-entry rather than per-store;
    • KSafeKeyInfo.storage — use level (KSafeProtectionLevel), which is universal across platforms.

Removed

  • BiometricHelper.promptTitle / BiometricHelper.promptSubtitle are gone. They were Android-only globals that could not be reached from shared code and never influenced the web passkey name. Replaced by KSafeBiometrics.defaultTitle / defaultReason, which are common and feed every platform. Migration is a rename: BiometricHelper.promptTitle = xKSafeBiometrics.defaultTitle = x, BiometricHelper.promptSubtitle = xKSafeBiometrics.defaultReason = x. BiometricHelper.confirmationRequired is unchanged.
  • KSafeConfig.androidAuthValiditySeconds is gone. It was documented "reserved for future use" and never had any effect on any platform; keeping a dead knob in the public constructor only invited misplaced trust. Source-incompatible only for callers that passed it explicitly — delete the argument; behavior is unchanged.
  • encodeBase64(ByteArray): String is no longer public on JVM Desktop. It exposed nothing of KSafe's — it was a one-line wrapper over the standard library's kotlin.io.encoding.Base64, public by accident rather than by design, and the stated reason for keeping it (that the test suites need it) was never true. Callers should use Base64.encode from the standard library directly. This is a source- and binary-incompatible removal for JVM code that called it; nothing else in the library's surface changes.
  • On Android, secureRandomBytes moved to the KSafeSecureRandom_jvmKt facade class (from KSafeSecureRandom_androidKt). Hosting the byte-identical Android and JVM implementations in one source file means one @JvmName covers both, and it was pinned to the JVM spelling to keep that published ABI intact. Source-compatible — the import and the call are unchanged and only a rebuild is needed — but a pre-built Android binary that called it will not resolve the old class. Worth stating because the JVM ABI dump cannot catch this class of change: binary-compatibility-validator does not enumerate the Android target, so a green apiCheck says nothing about it.

Fixed

  • Destructive per-entry key sweeps are ownership-gated, and master-sentinel user keys are rejected. A dotted user key's derived engine alias can be byte-identical to another store's live key (the default store's key "vault.__ksafe_master__" aliases the named store "vault"'s master), so delete()/clearAll() now issue per-entry key deletions only for entries that provably own one, post-commit cleanup never deletes an alias a live entry still resolves to, and user keys whose trailing dot-segments spell a reserved sentinel are rejected at the API boundary.
  • clearAll() races can no longer resurrect wiped data. A lazy first-load cache merge whose snapshot predated a concurrent clearAll() could republish wiped secrets into RAM after the wipe returned; merges now detect the wipe and redo from the post-clear snapshot. An encrypted-to-plain overwrite forgot the entry's platform key ever existed; the superseded alias is now reclaimed like delete() does. Engine key-deletion failures during sweeps are now logged instead of vanishing, and clearAll()'s key deletion is documented as best-effort next to its loud data wipe.
  • iOS/macOS: clearAll() now also deletes the DataStore corruption-quarantine copy (<base>.preferences_pb.corrupt), which still holds decryptable ciphertext. JVM/Desktop has swept its quarantine copies since 2.2.1; Apple never did, so a wipe left a readable copy of the whole store on disk. Every platform now matches the bare .corrupt marker, so Android and JVM/Desktop also remove a <base>.preferences_pb.corrupt copy — they write only the timestamped .corrupt-<ts> form themselves, so that name is one a user (or a restored Apple backup) put there by hand.
  • Co-existing same-file instances now share one commit lock, and JVM/Apple encrypts survive a sibling's clearAll(). Two live instances on one file could interleave inside each other's batch commits and key sweeps and lose an acknowledged write's key; batch commits are now serialized per physical store. The JVM and Apple engines also validate the key's survival after each encrypt — re-asserting it or re-encrypting under the winner — matching the Android DEK re-persist, so an acknowledged write can't become undecryptable because a sibling wipe raced the encrypt. The startup orphan-ciphertext sweep's final check and delete also run under that lock now: a sibling's in-flight writes are invisible to the sweeping instance, so a value committed between the sweep's last look and its delete batch used to be silently swept as an orphan.

... (truncated)

Commits
  • 9d20165 ci: pin setup-gradle to v5, not v6
  • 7afa2f1 ci: bump actions to their node24-native majors
  • 63d613f release: 3.0.0
  • a1d6d26 Update README.md
  • d22d93b docs(release): fold 2.2.0 into a single 2.2.1 changelog entry, bump doc versions
  • c461a7e fix(jvm): sweep FileKeyVault crash-leftover temp files holding the plaintext ...
  • da93de7 fix(core): harden the snapshot collector, teardown, and null-sentinel handling
  • 54a71fa docs(comments): strip non-essential commentary from the 2.2.x biometrics/simu...
  • See full diff in compare view

Dependabot compatibility score

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


Dependabot commands and options

You can trigger Dependabot actions by commenting on this PR:

  • @dependabot rebase will rebase this PR
  • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
  • @dependabot show <dependency name> ignore conditions will show all of the ignore conditions of the specified dependency
  • @dependabot ignore this major version will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
  • @dependabot ignore this minor version will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
  • @dependabot ignore this dependency will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)

Bumps [eu.anifantakis:ksafe](https://github.com/ioannisa/ksafe) from 2.2.1 to 3.0.0.
- [Release notes](https://github.com/ioannisa/ksafe/releases)
- [Changelog](https://github.com/ioannisa/KSafe/blob/main/CHANGELOG.md)
- [Commits](ioannisa/KSafe@2.2.1...3.0.0)

---
updated-dependencies:
- dependency-name: eu.anifantakis:ksafe
  dependency-version: 3.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
@dependabot dependabot Bot added dependencies Pull requests that update a dependency file java Pull requests that update java code labels Aug 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file java Pull requests that update java code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants