fix(deps): update dependency eu.anifantakis:ksafe to v3.1.0 - #349
Open
renovate[bot] wants to merge 1 commit into
Open
fix(deps): update dependency eu.anifantakis:ksafe to v3.1.0#349renovate[bot] wants to merge 1 commit into
renovate[bot] wants to merge 1 commit into
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR contains the following updates:
3.0.0→3.1.0Release Notes
ioannisa/ksafe (eu.anifantakis:ksafe)
v3.1.0Compare Source
Added
KSafe now owns its Apple CryptoKit integration. A small bundled Swift/C bridge calls
CryptoKit.AES.GCMdirectly on iOS and macOS, while Kotlin owns validation and the frozen12-byte nonce || ciphertext || 16-byte tagenvelope. NIST AES-128/AES-256 vectors, anindependent AAD vector, and tamper tests pin the bridge's output and compatibility.
AES key strength is typed and works on every platform. Configure
KSafeConfig(aesKeySize = KSafeAesKeySize.BITS_128)or keep the defaultBITS_256.WebCrypto now honors the same choice when minting a new non-extractable key; existing keys on
every platform retain their inherent size until
rotateKeys()creates a new generation.Interrupted key rotations resume automatically at the same generation on the next KSafe
instance. A 3.1.0 rotation writes
"r":1atomically with its generation bump, before ittouches any entry, and changes that lifecycle state to
"r":0only after the entry pass andsuperseded-master sweep finish. A process death therefore leaves a readable mixed-generation
store that the next instance repairs idempotently at the same generation, even under
KSafeKeyRotationPolicy.Never; this is one lifecycle bit, not a per-entry journal.3.0.0 compatibility is deliberately conservative. The released 3.0.0 generation record
has no
rfield, so absence cannot distinguish a completed rotation from one interrupted bya crash. On its first 3.1.0 startup, KSafe treats every such record as a completed 3.0.0
rotation, adds
"r":0, preserves its generation/timestamp, and performs no resume, generationbump, entry rewrite, key sweep, or same-launch
MaxAgerotation. Normal policy resumes on thefollowing launch. Thus upgrading can never make 3.1.0 guess wrong and disturb an already
shipped 3.0.0 store; if 3.0.0 really did leave entries behind, they remain readable under
their recorded old key and a later explicit
rotateKeys()(or dueMaxAgepass) moves themnormally. Unknown future lifecycle values are preserved and rejected fail-closed.
A normally completed rotation no longer loses retryable work. If a pass returns with
skippedentries — most importantly a strictrequireUnlockedDeviceentry while the deviceis locked — KSafe records
"r":0plus"rp":N, a bounded next-instance retry budget.KSafeConfig.keyRotationRetryAttemptscontrols the initial count (3 by default; 0 disablesthis retry path). The current instance starts no timer and performs no same-run retry. Each
new KSafe instance consumes at most one attempt and retries the same generation, without
minting another key or resetting the generation-birth
"ts"clock; this lifecycle completionalso runs under
Never. The claim durably changesr:0,rp:Ntor:1,rp:N-1before work, soa crash cannot refill the budget;
r:1,rp:0can recover only the final already-claimedattempt. If
MaxAgeis already due, its fresh-generation rotation takes precedence.failedalone never arms retry because it denotes a definitive, not retryable, problem.Mode-typed views:
KSafePlain,KSafeEncrypted,KSafeHardwareIsolated. Thin wrappersover an existing
KSafeinstance that freeze the write mode at construction — no member ofthese types takes a
modeparameter, so a call site can never accidentally encrypt apreference or store a secret in plaintext by picking the wrong argument. They cover the full
write surface (
put/putDirect, theby view(...)delegate,asFlow/asWritableFlow/asStateFlow/asMutableStateFlow/getStateFlow, and — via:ksafe-compose—mutableStateOfand
rememberKSafeState), and forward the nullablekeyuntouched so key-from-property-namederivation behaves exactly as on
KSafe. All views over one instance share the same file, keynamespace and cache, so a single store keeps serving mixed-mode ezntries; in Koin the types
replace stringly
named(...)qualifiers (single { KSafePlain(get()) }), andksafe.plain/ksafe.encrypted/ksafe.hardwareIsolatedoffer the same views as one-lineaccessors.
KSafeEncrypted/KSafeHardwareIsolatedalso freeze the unlock policy: theirconstructors take
requireUnlockedDevice, defaulting to the instance's configured policy(
KSafe.defaultWriteMode), so a default-constructed view writes exactly like a modelessksafe.put. The guarantee is deliberately write-side only — reads stay mode-free andauto-detect each entry's protection — and
HARDWARE_ISOLATEDremains a request that candegrade with the usual reporting. Store-scoped operations (
rotateKeys,clearAll,close,protectionInfo,getKeyInfo,awaitCacheReady,getOrCreateSecret) are deliberatelyabsent from the views; each exposes its underlying instance as
val ksafefor those.Fixed
An encrypt in flight across a
clearAll()no longer resurrects the wiped key. On JVM andAndroid — the two platforms whose key material lives inside the store the wipe empties — an
encrypt that had already resolved its key when a sibling instance's
clearAll()landed used torepair itself by re-persisting that key, so the write stayed readable at the cost of undoing the
wipe: a pre-wipe backup of the store file became decryptable again. The repair now re-encrypts
instead — the raced write is retried under whatever legitimately owns the key slot afterwards, a
concurrent winner's key or freshly minted material — so the acknowledged write stays readable
and the destroyed key stays destroyed. Cryptographic erasure holds even against an in-flight
writer. The interleaving is forced deterministically in tests on both platforms; in the
pathological tail of repeated back-to-back wipes the failure direction is now uniformly toward
erasure, never against it. Apple and web are untouched: their keys live outside the store
(Keychain, IndexedDB), so
clearAll()never created this window there.Apple joins the other platforms in serializing concurrent biometric prompts. Android, JVM
Desktop and web queued a second concurrent
verifyBiometricbehind the first and let it skip itsprompt when the holder had just authorized the same scope; Apple ran straight to its own
LAContext, so two simultaneous calls always produced two ceremonies. It now takes the same gateand re-checks the cache when the gate changes hands. Apple never had the ordering defect fixed
below — its authorization is recorded inside the callback, before the caller resumes, which is
already inside the gate. Sequential calls are unchanged on every platform: the cached
authorization already spared them.
A second caller queued behind a biometric prompt no longer gets a redundant prompt of its own.
The authorization was recorded after the single-prompt gate was released, while a queued caller
re-checks the cache the instant the gate changes hands — so it could read a cache the holder had
not written yet and prompt again. Nightly CI caught it as a once-in-fifteen-runs failure on an
unchanged commit; the window is a few instructions wide and only opens under scheduling pressure.
The recording now happens while the gate is still held, on Android, JVM Desktop and web alike.
Never a bypass — the failure mode was an extra prompt, never a skipped one — but it defeated
exactly the de-duplication the gate exists for.
BiometricHelper.authenticate(Android's ownpublic entry point) gained an optional
onAuthorizedhook for this; existing call sites areunaffected. Apple is untouched: its prompt path does not use the gate at all, so it never had
this window — and, separately, it also has no queued-caller de-duplication to lose.
A write interrupted by process death on web can no longer read back as the wrong value.
localStoragehas no transaction API, so an entry's value and metadata records are two separatewrites and a tab that dies between them commits half of the pair. The surviving half used to be
the new metadata over the previous entry's bytes — and because a reader treats an explicit
"p":"NONE"as plaintext, switching a key from encrypted to plain and crashing mid-write handedthe old ciphertext back to the caller as its value, base64 and all. The mirror direction stranded
readable plaintext under metadata claiming encryption. Web batches now clear the value slot they
are about to rewrite before committing the new metadata, so a tear leaves the entry with no value
— reads return the caller's default — instead of one whose bytes and metadata disagree. The
ordering is asserted over every prefix of a batch, not just its final state, so the property
survives a future reshuffle. Other platforms were never affected: their backends commit a batch
atomically.
Rotation retires superseded master keys that a torn write used to pin forever. A metadata
record left without its value was counted as a live reference by the superseded-master sweep,
while being invisible to everything that could ever release it — rotation needs a ciphertext to
build a candidate, and the orphan sweep enumerates value records. One such record therefore kept
its generation's master alive for the lifetime of the store, so
rotateKeys()reported successand returned a rising generation while never destroying the old key material that still decrypts
copies of the old ciphertext. The sweep now ignores a metadata record with no value under any
layout, and the startup cleanup deletes it outright — under the commit mutex, and skipping any key
with a write in flight, since "metadata without a value" is also the transient shape of a healthy
write on a backend that commits metadata first.
Deprecated
KSafeConfig.keySize— replaced byaesKeySize, and removed in 4.0.0. WriteKSafeConfig(aesKeySize = KSafeAesKeySize.BITS_128); anIntcould not express that onlytwo values were ever legal. The old spelling keeps working meanwhile: the constructor, the
keySizeproperty andcopy(keySize = …)are all still there, still reject anything but128/256 with the same message, and still resolve to the same bytes. The cipher remains
deliberately fixed to authenticated AES-GCM — this selects key strength, not an AES mode.
Only
KSafeConfig's generatedcomponent1()changed shape, because a data class derives itfrom the primary constructor and Kotlin cannot carry two of them. That affects destructuring
a config (
val (size, …) = config) and nothing else.Removed
obtainAesGcm()is gone, along with thedev.whyoleg.cryptographydependency whose type it returned — which is why it could not be kept as a deprecation: the
return type no longer exists. Anyone actually calling it already depends on that library to
consume the result, so the replacement is the same one-liner in their own code
(
CryptographyProvider.CryptoKit.get(AES.GCM)). AES-GCM setup is now internal to KSafe.Configuration
📅 Schedule: (UTC)
🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.
♻ Rebasing: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.
🔕 Ignore: Close this PR and you won't be reminded about this update again.
This PR was generated by Mend Renovate. View the repository job log.