Skip to content

fix(keychain): derive a key's public half instead of refusing to deploy - #161

Merged
kipavy merged 1 commit into
devfrom
feat/derive-public-key-and-export-ux
Aug 22, 2026
Merged

fix(keychain): derive a key's public half instead of refusing to deploy#161
kipavy merged 1 commit into
devfrom
feat/derive-public-key-and-export-ux

Conversation

@kipavy

@kipavy kipavy commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

The bug

"Add to host" fails with "Public key not found" on any key that was imported private-only. The public half is optional at import and nothing derived it, so the key saves without complaint and refuses to deploy later.

addKeyToHost read key:<id>:public and threw when it was absent (src/services/keyExport.ts). The export panel offered the action for any key and only reported the problem after a host was picked and the button pressed — and the message rendered at the bottom of the scroll area, below the fold, so the click looked like it did nothing.

The fix

Derivation. New backend command ssh_public_key_from_private (src-tauri/src/commands/keygen.rs) computes the public half from the private one, decrypting with a passphrase when there is one. It fails with codes — ENCRYPTED / INVALID — so the UI can tell "give me the passphrase" from "that is not a key" without matching on message text.

ensurePublicKey (src/services/publicKeyStore.ts) reads the stored half, else derives from the private half plus the saved passphrase, validates the result with isValidSshPublicKey, and backfills both the local vault and the key's team vault.

  • KeyForm and IdentityForm's inline key material fill the visible public field as soon as a complete private half is entered (shared useDerivedPublicKey hook), so the value can be read and corrected instead of being conjured at deploy time.
  • addKeyToHost now goes through the store — this fixes the plugin/MCP keys.addToHost route too.
  • Mobile "copy public key" derives as well.

Export panel UX.

  • The public half is checked when the panel opens: a key with nothing derivable shows a warning and a disabled button, before a host is even picked.
  • Failures pin into the sticky footer directly above the button (max-h-20, scrolls) instead of the scroll area.
  • Success raises a toast and closes the panel.

New strings in en / fr / ru / zh.

Verification

cargo test --lib 335 passed · vitest run 486 files / 3778 tests passed · tsc --noEmit clean · cargo fmt --check clean.

New tests: 4 Rust (plaintext + encrypted round-trip, ENCRYPTED, INVALID), 8 for publicKeyStore, 4 for KeyExportPanel, 4 for the forms.

Live-run against the headless stack and a real ssh-host-1:

  1. Pasting a private-only key auto-fills the public field with exactly the matching .pub.
  2. A key seeded with a private half and no public secret (the pre-fix state) deploys: the remote authorized_keys gained the derived line, and key:<id>:public is stored afterwards.
  3. Success → toast ("Key exported successfully", confirmed in notification history) + panel closes.
  4. Wrong credentials → error pinned in the footer, fully in viewport, panel stays open, button re-enabled. An encrypted key with no stored passphrase → warning card + disabled button.

Deliberately out of scope

  • The key copy/duplicate and import/export paths still read key:<id>:public raw; they copy an absent public half rather than deriving one.
  • No passphrase prompt: an encrypted key with no saved passphrase is reported, not challenged.
  • The 18 existing { id: "system" | "core", name: "Voltius" } toast literals are left alone — the source id drives grouping, so unifying them is its own change.

"Add to host" read `key:<id>:public` and threw "Public key not found" when
nothing was stored. The public half is optional at import, and nothing ever
derived it, so a key pasted or dropped private-only saved without complaint
and refused to deploy — often weeks later.

A new backend command computes the public half from the private one
(`ssh_public_key_from_private`, error codes ENCRYPTED / INVALID so the UI can
tell "needs a passphrase" from "not a key"). `ensurePublicKey` reads the
stored half, else derives from the private half and the saved passphrase,
validates it, and backfills both the local vault and the key's team vault.

- KeyForm and IdentityForm's inline key material fill the visible public
  field as soon as a complete private half is entered, so the value can be
  read and corrected rather than conjured at deploy time.
- addKeyToHost goes through the store, which fixes the plugin/MCP route too.
- The export panel asks before a host is picked: a key with no derivable
  public half shows a warning and a disabled button, instead of failing after
  the host was chosen.
- The outcome no longer renders at the bottom of the scroll area, where a
  click looked like it did nothing: failures pin into the sticky footer above
  the button, and success raises a toast and closes the panel.
@kipavy
kipavy merged commit 6f10033 into dev Aug 22, 2026
4 checks passed
@kipavy
kipavy deleted the feat/derive-public-key-and-export-ux branch August 22, 2026 22:04
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.

1 participant