Skip to content

fix(identity): keep an unbacked key safe on Replace and unblock a corrupt saved key - #3233

Merged
loganj merged 3 commits into
mainfrom
larry/key-safety
Nov 11, 2026
Merged

loganj merged 3 commits into
mainfrom
larry/key-safety

Conversation

@loganj

@loganj loganj commented Nov 10, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Replace an unbacked key: when first run found another account's key on this computer, Replace deleted it even when it had no confirmed backup, and the dialog offered no way to copy it first. Now the dialog says to copy the key first and shows the same private-key controls as Settings' Remove from device. Replace stays disabled until the key is revealed or copied and I have my key is checked, the same gate as Settings. A key with a confirmed backup keeps the one-step Replace.
  • Corrupt saved key: a saved key Buzz can't parse stopped launch on "Saved identity is malformed… contact support" with Retry. Retry reads the same bytes, so it could never work, and the app never opened, so Settings was out of reach. Now that page explains the problem and offers Remove from device. It confirms, then signs out as Sign out of Buzz does without erase. The next launch deletes the key and opens first run, which restores the account's backup or imports a key.
  • Duplicate keyring entries (Windows, Linux): several keyring items matching one entry were reported as a malformed key. Deleting refuses for them too, so Remove from device would stop every later launch. They now get their own message, which says to remove the extra entries with the keyring app, and keep Retry.
  • The launch read's "access was denied" message no longer says "no identity was created", which was wrong for a read.

Details

  • src/features/identity/Welcome.tsx:
    • The replace dialog renders PrivateKey and an I have my key checkbox when the key has no confirmed backup. The checkbox is enabled after a reveal or copy (onInteraction), and Replace needs both. Each new question starts over.
    • The launch error page matches native's malformed answer (MALFORMED_IDENTITY) and shows Remove from device in place of Retry secure storage access. Its confirmation calls identity.signOut({ wipe: false, removeAgents: false }). A native refusal stays in the dialog with its reason, and the button can be pressed again. Other launch errors keep Retry.
  • src/features/identity/service.ts: MALFORMED_IDENTITY holds native's exact text. A test reads identity.rs and checks it still matches, as the recovery messages already do.
  • crates/credential-store: a new Error::Ambiguous for keyring's Ambiguous, which was Corrupt. Every other caller maps it as it mapped Corrupt, so only the identity read changes.
  • src-tauri/src/identity.rs: Ambiguous gets its own AMBIGUOUS message, so the launch page keeps Retry. The launch read's denied message now ends "nothing was changed". Writes keep "no identity was created".
  • docs/identity.md and docs/buzz-help.md describe both changes.
  • Tests (identity.test.tsx):
    • An unbacked key: Replace is disabled until Copy private key and I have my key, the clipboard gets the key, and then Replace signs out. This includes both kgoose refusals.
    • A failed copy leaves the checkbox and Replace disabled. A reveal alone leaves Replace disabled; unchecking disables it again.
    • A backed-up key shows no copy controls, and Replace is enabled.
    • A malformed key: no Retry, Remove from device confirms, Cancel changes nothing, and confirming signs out without erase. A refusal shows its reason and can be tried again.
    • The native malformed text is pinned.

Known limits

  • The in-app linked-key prompt (LinkedKeyCheck) is unchanged. It already warns, and Settings → Identity is reachable there for a copy.
  • If secure storage refuses to export the key, Replace can't be enabled, and Cancel is the only way out until access works. That is the safe failure.
  • The duplicate-entries case is traced from code; it wasn't run on Linux.
  • A key that native can read but that is wrong in some other way is not covered. Only native's malformed answer offers removal.

🤖 Generated with Claude Code

loganj and others added 2 commits November 10, 2026 15:21
…rupt key

First run's Replace deleted the key saved here even when it had no
confirmed backup, with no way to copy it. Now it shows the private-key
controls and Replace waits until the key was revealed or copied. A
backed-up key keeps the one-step Replace.

A saved key native can't parse left launch on a Retry that could never
work, with Settings out of reach. That page now offers Remove from
device, which confirms and signs out as Sign out of Buzz does; the next
launch deletes the key and opens first run to restore or import.

The launch read's denied message no longer says no identity was created.

Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Logan Johnson <[email protected]>
…licate keyring entries

Replace of a device key with no confirmed backup now needs a reveal or
copy and "I have my key", the same gate as Settings' Remove from device.

Keyring's Ambiguous (several items matching one entry) was mapped to
Corrupt, so the launch page offered Remove from device. Deletes refuse
for it too, so the pending sign-out would fail every later launch. A
new credential-store Error::Ambiguous keeps every other caller's
mapping and gives the identity read its own message with Retry.

Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Logan Johnson <[email protected]>
@loganj
loganj marked this pull request as ready for review November 11, 2026 18:55
@loganj
loganj requested a review from a team as a code owner November 11, 2026 18:55
kalvinnchau

This comment was marked as resolved.

Removing the acknowledgement reset left every Replace test passing. A
second sign-in after Cancel must start with Replace disabled and "I have
my key" unchecked and disabled.

Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Logan Johnson <[email protected]>

@kalvinnchau kalvinnchau left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Approved based on review of 32790b9. The PR has since advanced to 215ad26; this approval does not validate that later delta.

No production defect was established in the reviewed identity replacement/removal flows. Confirmation and acknowledgement gates, account binding, credential-error distinctions, and reuse of the existing non-wiping sign-out contract were reviewed. Browser checks exercised the actual application with modeled native IPC/account replies in Chromium and WebKit; credential-store tests passed. Real native credential deletion/restart remains unverified, and the broad local test run did not complete cleanly.

The existing P3 comment is non-blocking: add a regression test proving that Cancel followed by signing in again starts replacement with a fresh, unchecked acknowledgement. Current behavior passed that scenario; the gap is durable test coverage.

@loganj
loganj merged commit 98b8f51 into main Nov 11, 2026
63 of 64 checks passed
@loganj
loganj deleted the larry/key-safety branch November 11, 2026 20:08
joahg added a commit to joahg/buzz-app that referenced this pull request Nov 11, 2026
* origin/main:
  NEXUS-4029: Offer folder chips in the composer (block#2789)
  perf(css): narrow effort button invalidation (block#3297)
  NEXUS-4433: Expose saved plugin installation source kind (block#3242)
  fix(identity): keep an unbacked key safe on Replace and unblock a corrupt saved key (block#3233)
  Expose Solo region state before a component mounts (block#3296)
  fix(builderlab): say a 401 was refused, not expired, and show each sign-in failure once (block#3241)
  feat(agents): file new Solo chats in explicit personal workspaces (block#2857)
  perf(css): limit broad style invalidation (block#3267)
  Delay and fade in the Bestie badge celebration (block#3292)
  fix(github): show private-repo body images from signed links (block#3252)
  NEXUS-4029: Preserve semantic chips in the core composer (block#2303)
  Make plugin signature enforcement opt-in (block#2212)
  NEXUS-4156: Add workspace Leads and choose a chat's workspace (block#2788)
  feat(plugins): report conclusively retired update providers (block#3263)
  feat(messages): let attachment renderers open files from the host file card (block#2239)
  feat(charts): bundle a Charts code-block renderer (block#3226)
  Expose native reply grouping through Conversation.ui (block#3245)

Signed-off-by: Joah Gerstenberg <[email protected]>

# Conflicts:
#	src/features/conversation/ComposerCompletions.test.tsx
#	src/features/conversation/ComposerCompletions.tsx
#	src/features/messages/MessageComposer.tsx
loganj added a commit that referenced this pull request Nov 11, 2026
The Replace tests from #3233 read the copied key from the browser
clipboard. This branch copies a private key through the host on every
platform and clears it after a minute, so the tests now answer that
command and check the host write.

Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Signed-off-by: Logan Johnson <[email protected]>
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.

2 participants