Join Nostr
2026-08-22 22:47:20 UTC

mstrofnone on Nostr: ...

{"kind":1618,"id":"0b42b7d99cd8cb16e476c830d90e1f28acd978932db4eec4b753af12d027f684","pubkey":"43185edecb675892824b1a37a57f3e407fbde2eda7201a3829b8cf4ba7c5b4f0","created_at":1787438486,"tags":[["a","30617:460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c:amethyst","wss://https://relay.ngit.dev";],["p","460c25e682fda7832b52d1f22d3d22b3176d972f60dcdc3212ed8c92ef85065c"],["r","6c3ff33351e7c7a1ae4fa2ac3651df4310ca9ed1"],["c","6c3ff33351e7c7a1ae4fa2ac3651df4310ca9ed1"],["clone","/amethyst.git"">https://relay.ngit.dev//amethyst.git";],["merge-base","10149d7150bc4ee93d3da5e3708802a99e9e1ff9"],["branch-name","fix/desktop-keychain-vault-v2"],["subject","fix(desktop): consolidate keychain items so cold-boot prompts once"],["alt","git Pull Request: fix(desktop): consolidate keychain items so cold-boot prompts once"]],"content":"Rebased re-submission of the vault-consolidation PR from 2026-07-29\n(event `67ab268e…`, branch `fix/desktop-keychain-single-item-batch`),\nwhich never merged. The double-prompt it fixes is still present in\nv1.14.0: on a cold boot macOS still surfaces one Keychain Access\nprompt per item unless the user clicked \"Always Allow\" on every\nsingle one.\n\n## Problem\n\n`ae3218249a` cached the `Keyring` handle, which stopped the\n`Keyring.create()` re-open storm. But macOS Keychain gates access\n**per item**, not per session. Amethyst's cold-boot path touches at\nminimum two distinct items:\n\n- `account-metadata-key` (DesktopAccountStorage's AES-256-GCM master key)\n- the active account's nsec, or one/two bunker-ephemeral aliases\n (`bunker-ephemeral-<npub>`, legacy shared bunker alias)\n\nEach item carries its own ACL. A user who clicks \"Allow\" (the default,\nsafer-looking button) instead of \"Always Allow\" gets re-prompted for\nevery subsequent item — the \"asks for my password twice at startup\"\nbug. Caching the session handle cannot help; the ACL check is per\nitem.\n\n## Fix\n\nTwo-layered, both wired into `AccountManager.loadSavedAccount()`:\n\n1. **Consolidated vault (durable fix).**\n `SecureKeyStorage.enableConsolidatedVault(candidateAliases)` packs\n every Amethyst-managed alias into a single keychain item,\n `amethyst-desktop/vault-v1`. macOS then sees exactly ONE item to\n gate regardless of how many accounts or bunker sessions exist —\n one prompt on cold boot, and one \"Always Allow\" click covers\n everything forever.\n\n Migration: on cold boot, if `vault-v1` exists, load it (zero\n legacy reads). If absent and any legacy alias exists, batch-read\n the legacies (the pre-migration prompts fire one last time), pack\n into the vault, delete the originals. Idempotent; safe to call\n every startup. Post-migration writes go into the vault, not into\n new per-alias items.\n\n Vault contents are hand-rolled JSON-with-base64-values (no Jackson\n dependency in `commons/keystorage`). The decoder is deliberately\n strict: any malformed input returns `emptyMap()` and falls back to\n legacy per-alias reads — better to prompt than to risk key\n corruption.\n\n2. **Warm cache (fallback for unmigrated users).**\n `SecureKeyStorage.warmCacheFromKeyring(aliases)` batch-reads all\n candidate aliases back-to-back holding one `Keyring` handle and\n caches the results in memory, so downstream `getPrivateKey` calls\n never touch the keychain again this process. Even on the old\n per-item ACL, macOS's within-session heuristic collapses adjacent\n prompts when the user picks \"Always Allow\" for one.\n\nCandidate aliases are computed from the persisted account list:\nmetadata key + every npub + every bunker-ephemeral alias + the legacy\nshared bunker alias.\n\n## Test coverage\n\n`SecureKeyStorageVaultTest` (326 lines, hermetic — stub `KeyringHandle`\ninjected via the package-private `keyringFactory`):\n\n- vault round-trip encode/decode, including quotes/backslashes/\n newlines/non-ASCII in secrets\n- migration packs multiple legacy aliases into one item and deletes\n the originals\n- post-migration `savePrivateKey` writes through the vault, not new\n per-alias items\n- malformed vault payloads fall back to legacy reads instead of\n corrupting state\n- warm-cache short-circuits subsequent keychain touches\n- delete-from-vault rewrites the item, and deletes the vault item\n itself when the last alias is removed\n\n## Verification\n\n`:commons:compileKotlinJvm :commons:jvmTest :desktopApp:compileKotlin\n:desktopApp:test :commons:spotlessCheck :desktopApp:spotlessCheck`\nall green on this branch (rebased onto current main, `10149d7150`).","sig":"f47ea0140fce261b663c9c92aecca7e45f2928f2688b7ce672d5bc9cb7422484712d2700dc09cd3a01966da168408f4f06de858bfa73fdf9bf5e584490220077"}