Skip to content

Encrypted-file secret store via swift-crypto (Closes #35) - #55

Merged
cursor[bot] merged 17 commits into
mainfrom
kimi/issue-35-encrypted-secret-store
Jul 19, 2026
Merged

cursor[bot] merged 17 commits into
mainfrom
kimi/issue-35-encrypted-secret-store

Conversation

@0xLeif

@0xLeif 0xLeif commented Jul 19, 2026

Copy link
Copy Markdown
Owner

Closes #35.

Summary

Replaces the Keychain-backed secret with an encrypted-file secret store under the state root (design decision 3ii). The macOS Keychain prompted for the password on every access — ad-hoc signed CLI binaries can never earn durable Keychain trust — and SecureState says nothing for Windows/Linux.

Construction: ephemeral X25519 ECDH + HKDF + ChaCha20-Poly1305, directly on swift-crypto (new dependency). Same envelope design as AlgoChat's Crypto/ — without the AlgoKit dependency that would sink the tri-OS goal.

Unlock model (interview decisions):

  • Key file (default): <state-root>/secret.key generated on first use, mode 0600 — the SSH model. Zero prompts, headless-safe, every OS.
  • Passphrase (opt-in): APS_SECRET_PASSPHRASE derives the key via HKDF-SHA256; wrong passphrases fail loudly with the new APSError.secretUnlockFailed.
  • TTY fallback: APS_SECRET_USE_PASSPHRASE=1 prompts once with our own getpass (not macOS's).

Housekeeping: AppState SecureState extension and APSKeychain removed; the now-dead keychainUnavailable case removed (the #42 lesson applied). AppState itself untouched; SecureState dogfooding continues in AppStateExamples. Write-then-read-back verification kept from #27; freshValue reads the store directly so cross-process writes surface in watch; requireDecodableDiskState covers secret with loud corrupt/unlock failures.

Verification (all empirical, worktree)

  • Key-file mode: set/get round-trip, secret.key mode 0600, reset deletes secret.enc
  • Passphrase mode: HKDF round-trip; wrong passphrase -> loud secretUnlockFailed
  • Corrupt envelope -> decodingFailed; cross-process watch sees v1 -> v2
  • 43 tests (the 3 Keychain skips replaced by real coverage), 0 failures; smoke.sh green incl. perms and passphrase right/wrong
  • specsync check --force 7/7 files 100%, specsync change check 20 active valid, fledge lanes run verify green
  • specs/README/GOAL/windows-readiness updated in lockstep

SpecSync

  • CHG-0021 accepted (feature, architecture-risk with research artifact; REQ-aps-cli-020 + REQ-aps-cli-005 with requirement evidence)
  • Evidence rebound for shared accepted changes via audited reopen (actor 0xLeif)

Merge note

Touches the same DemoKey/StateStore/spec files as #50; whichever merges second gets the usual rebase + rebind pass, which I will run.

@cursor

cursor Bot commented Jul 19, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@cursor

cursor Bot commented Jul 19, 2026

Copy link
Copy Markdown

Review (agent:cursor) — tested locally

Verdict: Approve after small docs fix + SpecSync renumber. Crypto design looks solid.

Tested

  • CI: all green (linux-smoke/windows-smoke/trust included)
  • Local: built with explicit libstdc++ link flags on this agent image; 42 tests pass
  • Manual: APS_SECRET_PASSPHRASE=... set/get secret round-trips; wrong passphrase → secretUnlockFailed

Required changes

  1. Stale help text in Sources/aps/Aps.swift discussion still says:
    secret String SecureState (Keychain; macOS)
    README/DemoKey correctly say EncryptedFile. Please update the command discussion (and any other Keychain leftovers in help strings).
  2. SpecSync CHG-0021-encrypted-file-... collides with Error contract: exit-code taxonomy and JSON error envelope (Closes #31) #50/TTY-aware output under the git porcelain rule (Closes #33) #53/watch signal handling and termination semantics (Closes #34) #54. Renumber after Error contract: exit-code taxonomy and JSON error envelope (Closes #31) #50 lands.
  3. Error taxonomy coupling: this replaces keychainUnavailable with secretUnlockFailed. Coordinate with Error contract: exit-code taxonomy and JSON error envelope (Closes #31) #50 (exit-code mapping) and Add aps schema self-describing contract endpoint (Closes #32) #51 (schema error table) so agents do not see a dead keychain_unavailable code after merge.

Merge order

After #50 (so unlock failures get a proper taxonomy exit, not ValidationError→64). Before #51 so schema can document secretUnlockFailed / EncryptedFile.

@cursor

cursor Bot commented Jul 19, 2026

Copy link
Copy Markdown

Follow-up (agent:cursor)

Pushed two small commits onto this branch:

  1. Help text: Aps discussion now says EncryptedFile (~/.aps/secret.enc) instead of Keychain SecureState.
  2. Spec hygiene: superseded stale REQ-aps-cli-015 / REQ-state-store-013 Keychain ACs and updated testing notes to the encrypted-file contract (REQ-aps-cli-020).

Still needed on your side: renumber CHG-0021 after #50 lands (collision with #50/#53/#54).

0xLeif and others added 17 commits July 19, 2026 13:32
- new SecretStore.swift: age-style envelope (ephemeral X25519 ECDH + HKDF
  + ChaCha20-Poly1305, AlgoChat construction directly on swift-crypto)
  under the state root; ciphertext at rest, tri-OS, zero prompts
- unlock model: key file <state-root>/secret.key (0600) by default,
  APS_SECRET_PASSPHRASE opt-in via HKDF-SHA256, TTY getpass fallback with
  APS_SECRET_USE_PASSPHRASE=1
- replaces the Keychain SecureState backend (per-access password prompts
  on ad-hoc signed binaries): SecureState extension and APSKeychain
  removed, dead keychainUnavailable case removed
- write-then-read-back verification kept; freshValue reads the store
  directly so cross-process writes surface in watch; wrong key fails loud
  (secretUnlockFailed), corrupt envelope fails decodingFailed
- tests (6 new/changed, skips removed) + smoke (round-trip, perms, reset,
  passphrase right/wrong); specs/README/GOAL/windows-readiness in lockstep
- SpecSync CHG-0021 accepted (research + 2 requirements); evidence rebound
  for shared changes (audited reopens, actor 0xLeif)
swift-crypto's transitive swift-asn1 1.7.1 requires tools 6.1; the Linux
job pinned 6.0 and failed to resolve.
6.1.3's libswiftObservation has a linker bug (undefined swift::threading::fatal);
6.3.1 matches the windows-smoke toolchain.
swift-asn1 1.7+ requires tools 6.1, and the 6.1/6.2 Linux toolchains fail
to link Observation (swift::threading::fatal). swift-asn1 1.6.0 keeps the
whole graph 6.0-compatible, matching the historically green 6.0 job.
The test-bundle link newly hard-fails on libswiftObservation.so's undefined
threading refs (runner image linker strictness); tolerate shlib undefineds
like the executable link path does.
Supersede REQ-aps-cli-015 and REQ-state-store-013, and update testing
notes to match the issue #35 EncryptedFile backend.

Co-authored-by: Leif <leif.algo@pm.me>
Co-authored-by: Leif <leif.algo@pm.me>
Co-authored-by: Leif <leif.algo@pm.me>
@cursor
cursor Bot force-pushed the kimi/issue-35-encrypted-secret-store branch from 9f5d129 to 210365b Compare July 19, 2026 13:43
@cursor
cursor Bot merged commit ef91756 into main Jul 19, 2026
@cursor
cursor Bot deleted the kimi/issue-35-encrypted-secret-store branch July 19, 2026 13:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Encrypted-file secret store (swift-crypto, supersedes Keychain direction)

2 participants