Repository navigation
fix(storage): preserve malformed Keychain authority - #196
Conversation
The injectable Keychain operations protocol was inserted between the KeychainPassphraseStore doc comment and the struct, so Quick Help showed the store's documentation on the protocol and none on the store. Move the protocol and its system implementation above the comment and give the protocol its own short description.
johnny4young
left a comment
There was a problem hiding this comment.
Summary: A Keychain item that exists but is not a 64-hex database key now throws Failure.malformedKey instead of being deleted and regenerated. Keychain I/O is made injectable (PassphraseKeychainOperations) so this path can be tested.
Verdict: ready. This also fixes a serious latent data-loss path. On main, the "unreadable item" branch called deleteKey(), which deletes with kSecAttrSynchronizableAny. A malformed local item therefore also wiped a perfectly valid synchronizable key, and the next open generated a fresh key and archived the user's database as unreadable. Failing closed is the right call.
Checked:
isStoredKeymatchesgenerateKey(), which produces 64 lowercase hex characters, and also accepts uppercase. Both production callers (GRDBClipboardStore.encryptedandStoreContentKey.load) use the default service/account, so no other secret format flows through this store.- On failure,
StoreBootstrap.liveOpenerturns the throw into the existing ephemeral-store warning. Nothing is reset silently.
Fixed and pushed (d8df4e0): 🟡 the new protocol and its system implementation sat between the long KeychainPassphraseStore doc comment and the struct. As a result, Quick Help attached the store's documentation to PassphraseKeychainOperations and showed none on the store. I moved both declarations above the comment and gave the protocol a two-line description. It is a pure move of comments and declarations with no code change. It was not compiled locally because there is no Swift toolchain here; CI will confirm.
Question for the owner (not a blocker): after this change, a malformed item leaves the user on the ephemeral fallback indefinitely, with no in-app way out. Do you want a "Reset encrypted store" recovery action that explicitly deletes the malformed item, so this state is recoverable without Keychain Access?
Generated by Claude Code
…sync keys - Store fake adds in the local or synchronizable slot the query names, so a regression that writes the key into the wrong scope fails the suite - Cover a malformed synchronizable key with no local key: it fails closed, reads both scopes and never adds or deletes - Spell the accepted hex ranges with ASCII literals and correct the doc: keys are generated lowercase and uppercase is tolerated
Summary
A Keychain item that exists but is not a 64-hex database key now fails closed with
Failure.malformedKeyinstead of being deleted and regenerated. Onmainthat path deleted withkSecAttrSynchronizableAny, so a malformed local item also destroyed a valid synchronizable key and pushed the next open into fresh-key recovery.Review follow-ups
d8df4e0— docs(storage): reattach passphrase store documentation to its type. The newPassphraseKeychainOperationsprotocol had been inserted between the longKeychainPassphraseStoredoc comment and the struct, so Quick Help showed the store's documentation on the protocol. Pure move of declarations/comments; no behaviour change. CI green on this head.Decisions taken
Known limitations & follow-ups
Merge order
Independent; merge before the v0.10.0 release PR #193.
Original description (state at
36d9553):Problem
A successful but malformed preferred database-key read deleted both Keychain scopes, risking reachable encryption authority and unintended fresh-store recovery.
Repair
Fail closed with a content-free malformed-key error. Do not add, delete, regenerate or fall through to another scope when a preferred item exists but is malformed. Accept documented 64 ASCII-hex authority verbatim, including uppercase legacy encodings. Inject Security read/add/delete operations so regressions never need the host Keychain.
Regression coverage added
Malformed local plus valid synchronizable item; non-Data successful read; invalid encoding; unchanged local/synchronizable values; true absence creates once; duplicate-add contender reads the winner; preferred/fallback behavior and legacy bytes. Existing native Keychain round-trip tests remain intact.
Compatibility
No key rotation, schema or blob-framing change. Arbitrary passphrases remain available to explicit store initializers; the Keychain-managed item uses the historically generated ASCII-hex format.
Exact-head validation
Published head: 36d9553
Uploaded/local staged tree: b88b7f6e54017298b44ab36188d1dce2c4dc9845 (verified identical before publication)
Combined source review
All five repair trees plus unmerged MCP PR195 at c90e07d combine without textual conflicts in a disposable local tree. Native combined execution remains unverified. MCP optional binary content may become nil after legitimate deletion/GC; the captured row hash is never replaced by a newer body lookup. No stronger availability promise for deleted content or new public-stack dependency is inferred.
Ready for human review only. No merge/release, live Keychain, clipboard, real user library or backup access.
Human-review readiness
Scoped implementation and regression coverage are complete at unchanged head 36d9553. Fresh verification confirms both required native jobs and GitGuardian successful, the expected non-blocking beta-canary skip, and no pending discussion comments, submitted reviews or unresolved inline review threads. Review readiness does not authorize merge, release or deployment.
🤖 Generated with Claude Code
https://claude.ai/code/session_01ExmnfUngrDYjAheGvYwMnn
Generated by Claude Code