Files
asepharyana 1cdb82a76f Sync config from arch
- hypr/apps.lua
- hypr/autostart.lua
- hypr/envs.lua
- hypr/hyprland.lua
- hypr/hyprsunset.conf
- hypr/input.lua
- hypr/looknfeel.lua
- hypr/omasettings.lua
- hypr/xdph.conf
- omarchy/branding/about.txt
- omarchy/branding/screensaver.txt
- omarchy/extensions/omarchy-menu.jsonc
- omarchy/hooks/battery-low.d/play-warning-sound.sample
- omarchy/hooks/font-set.d/show-font-notification.sample
- omarchy/hooks/post-boot.d/weather.sample
- omarchy/hooks/post-update.d/install-voxtype.hook
- omarchy/hooks/post-update.d/setup-agent.hook
- omarchy/hooks/post-update.d/setup-fingerprint.hook
- omarchy/hooks/post-update.d/show-update-notification.sample
- omarchy/hooks/pre-refresh-pacman.d/add-custom-repo.sample
- omarchy/hooks/theme-set.d/show-theme-notification.sample
- omarchy/shell.json
- omarchy/shell.toml
- omarchy/theme.name
- omarchy/themes/azure-glow/README.md
- omarchy/themes/azure-glow/alacritty.toml
- omarchy/themes/azure-glow/btop.theme
- omarchy/themes/azure-glow/hyprland.conf
- omarchy/themes/azure-glow/hyprlock.conf
- omarchy/themes/azure-glow/icons.theme
- … 269 more
2026-09-23 15:19:12 +07:00

7.1 KiB

4. SSH key creation stays out of the plugin, on private-key grounds

Date: 2026-09-03

Status

Accepted. Confirms two entries in docs/ideas/ssh-agent.md ("Not Doing Initially") — Key generation or import and SSH item creation, editing, or cloning in QML — and replaces the reason recorded for one of them.

Context

The question keeps coming back: the panel lists SSH keys, serves them to ssh and Git, and can create every other item type. Why not create one?

The design deferred it twice, and the second entry gives a reason that is worth re-reading:

SSH item creation, editing, or cloning in QML: the CLI edit contract round-trips the complete cipher, so these need an opaque metadata-patch design that never exposes the existing private key to QML.

That reason is correct about editing and does not apply to creation. buildEditPayload starts from a deep clone of rawObject, so editing a type-5 item would put the stored private key in QML. A key being created has no stored private material to expose — whatever it holds, this plugin generated a moment ago. The two cases were filed together and are not the same case.

So creation was re-examined on its own.

What the CLI can actually do

The obvious first question was whether the vault boundary this plugin refuses to cross can write a type-5 item at all. The first evidence said no:

$ bw get template item.sshKey
Unknown template object.

and the CLI's own template switch (@bitwarden/cli 2026.2.0, build/bw.js) confirms the gap is deliberate — it has cases for item.card, item.field, item.identity, item.login, item.login.uri and item.securenote, and no case for item.sshKey.

That reading was wrong, and it is recorded here because it is the wrong conclusion a reader is most likely to reach independently. The base item template carries the field:

$ bw get template item
{... "card":null,"identity":null,"sshKey":null,"reprompt":0}

and the encrypt path — the one a create actually goes through — handles the type in full:

case CipherType.SshKey:
    cipher.sshKey = new SshKey();
    yield this.encryptObjProperty(model.sshKey, cipher.sshKey,
        { privateKey: null, publicKey: null, keyFingerprint: null }, key);
    return;

The CLI can encrypt and create a type-5 item. The missing template is a convenience gap, not a capability gap, and a hand-built payload carrying privateKey, publicKey and keyFingerprint is very likely to be accepted. This decision therefore cannot rest on "the CLI will not let us", because it will.

Decision

The plugin does not create SSH keys. The reason is private-key custody, not CLI capability.

Every design that puts a new key in the vault has to answer where the private key is generated and what it touches on the way. There are two candidates and each one gives up a property the SSH work was built around.

Generate in the helper. It already holds private keys, in memory, never on disk — that is the entire point of it being a separate process. But it has no random-number generator, and that is deliberate. rand_core is a dev-dependency, commented "Test-only key generation, so no private key material is committed", and selftest.rs explains the refusal directly:

the only honest ways to get one are to generate it — which would put a random-number generator into a key-holding binary's dependency tree for the sake of a smoke test — or to embed one, which is exactly what this project refuses to do anywhere else.

Reversing that means a new release dependency in the audited supply chain, a rebuilt binary, new committed bytes, a new SHA256SUMS, and a fresh provenance attestation. /agent/ and /bin/ are CODEOWNERS-flagged for precisely this class of change. It is not prohibitive — but it is a supply-chain decision, not a feature decision, and it should be taken as one.

Generate with ssh-keygen. No helper change, no new dependency. It writes the private key to disk, and "never on disk" is a property this feature states plainly. A FIFO does not rescue it: ssh-keygen wants to write two real files.

And either way, the private key must reach bw through the panel's QSBW_ITEM environment payload. That route is already trusted with passwords, so it is not new machinery — but it is new for SSH private keys, which this design has kept exclusively inside the helper, and it would mean QML briefly holds the one class of secret it has never held.

None of that is unsolvable. It is a security design with a threat model to revisit, and it does not belong in a release that is about card items and a settings screen.

Consequences

Users create SSH keys in the web vault or the browser extension, where the feature shipped in 2025.1.0. The panel lists, serves and signs with them the moment they exist. This is a gap in convenience, not in function.

The type-5 read-only guards stay. createItemCommand, editItemCommand, deleteItemCommand, buildCreatePayload, buildEditPayload and startEditItem all refuse type 5, and the sanitizing filter continues to reduce type-5 items to public metadata before QML sees them. Those guards now have a decision behind them rather than an unexamined default.

A hazard for anyone tempted to test this quickly. Confirming the create path empirically means writing a real type-5 item, and the failure mode is not symmetric. Bitwarden CLI below 2026.8.0 fails to decrypt SSH key items with a null public key or fingerprint, and one such item breaks the entire vault list — in this plugin and in every other client on that CLI, until the item is removed. A malformed write is therefore not a harmless experiment on a vault someone depends on. Test against a throwaway account or a local Vaultwarden.

If this is revisited, helper-side generation is the design to start from. The dependency cost is real and reviewable; the alternative trades away two properties — private keys never on disk, private keys never in QML — that are harder to win back than a line in Cargo.toml.

Alternatives considered

  • Import an existing key rather than generate one. Avoids the RNG question entirely, and the user already has the private key in a file. It still routes private material through QML and bw, so it clears the smaller obstacle and leaves the larger one, and it is a strictly less useful feature.
  • Create the item with only public material and fill the private key in later. This is the malformed-item shape named above. It would break vault listing for every client on a CLI below 2026.8.0.
  • Have the helper write the item to bw itself, keeping the private key out of QML entirely. The most promising variant, and the one worth designing if this is picked up: it would need the helper to hold a session token, which is a widening of its role from "signs with keys it was given" to "acts on the vault", and that deserves its own review.
  • Wait for a CLI template. The absence of item.sshKey suggests Bitwarden does not consider CLI creation a supported path yet. Waiting costs nothing and may produce a supported shape to build against.