Skip to content

Add Bitwarden support and stop passing passwords on the command line - #4

Open
FilipHert wants to merge 3 commits into
jaydenthorup:mainfrom
FilipHert:feature/bitwarden
Open

Add Bitwarden support and stop passing passwords on the command line#4
FilipHert wants to merge 3 commits into
jaydenthorup:mainfrom
FilipHert:feature/bitwarden

Conversation

@FilipHert

Copy link
Copy Markdown

Summary

Adds Bitwarden as a second password manager alongside 1Password, and fixes the
way passwords are handed to the connection clients.

Connection passwords can now be stored as bw://<item-id> references, resolved
through the Bitwarden CLI at connect time. This works with bitwarden.com,
self-hosted Bitwarden and Vaultwarden.

Why a provider abstraction

OnePasswordProvider was referenced directly from the config manager, the
launcher and the GUI, and op:// was hard-coded in five places including
crypto.ShouldEncrypt. Rather than duplicate all of that, secret handling now
goes through a small secrets.Provider interface and a process-wide
secrets.Registry. 1Password behaves exactly as before; adding a third
provider later is a single file plus one line in Default().

The registry is a singleton because config.NewManager, launcher.NewLauncher
and MainWindow.openConfig each construct providers, and Bitwarden owns a
helper process that must exist only once.

How Bitwarden is reached

Bitwarden has no library interface, so the provider runs bw serve as a hidden
child process bound to 127.0.0.1 on a random free port and talks to its local
REST API. It starts lazily on first use and is stopped on exit. On Windows the
child is placed in a kill-on-close job object; on Linux it gets a parent death
signal. That matters because bw serve has no authentication of its own, so an
orphaned server would leave an unlocked vault reachable on loopback.

The child inherits BW_SESSION, so the user unlocks the vault once in their
shell. MremoteGO never asks for, sees or stores the master password. The
provider is written so a future POST /unlock dialog would be a small addition.

GUI

The add and edit connection dialogs share a new control group instead of
duplicating the 1Password block. It adds a Bitwarden... button that lists
and searches vault login items and writes back a reference, and a Store
password in Bitwarden
option that creates a login item from a typed password.

The start-up authentication warning is now generic across providers, only asks
about providers the config actually references, and runs off the UI goroutine
because starting the CLI takes a second or two.

Security fix

Independently of Bitwarden, the connection password was visible in the process
list for the lifetime of the client process, so any local user could read it:

putty.exe -ssh ... -pw <password> host
sshpass -p <password> ssh ...
cmdkey /generic:TERMSRV/host /user:u /pass:<password>
  • PuTTY now gets -pwfile with a private temporary file, which PuTTY itself
    recommends over -pw. The file is removed once the process is up, and files
    left by a killed run are cleaned at start-up.
  • sshpass gets the password through SSHPASS. It cannot be set from Go
    because the terminal emulator hop does not forward the environment, so the
    generated snippet reads the file into the variable and deletes it before
    ssh runs.
  • RDP credentials are written through the Credential Manager API. The blob is
    UTF-16LE, which is what mstsc expects; UTF-8 stores a credential that looks
    valid but silently fails to log in.

Known limitation, left alone deliberately: xfreerdp on Linux is still invoked
with /p:.

Testing

go test ./... was previously empty. This PR adds:

  • the Bitwarden client and provider against a fake bw serve built on
    httptest, covering resolution, locked and unauthenticated vaults, item
    creation, list filtering and the absence of an Origin header, which the
    real server rejects;
  • reference parsing, the provider registry, crypto.ShouldEncrypt and the
    password file handling;
  • two tests that use the real system when available and skip otherwise: the
    Bitwarden CLI lifecycle, and a comparison proving the credential written
    through the API is byte for byte what cmdkey stores.

A go test ./... step was added to CI, and the Go version there was bumped from
1.23 to 1.24 to match go.mod.

Verified manually on Windows 11: build with CGO, GUI starts, bw:// resolution
reports a clear error with no login, no bw.exe left behind after exit.

Not addressed

ExtraArgs is still appended as a single argv element rather than split, which
predates this change.

🤖 Generated with Claude Code

Filip Hert and others added 3 commits September 3, 2026 19:27
Connection passwords can now be stored as bw://<item-id> references and
are resolved through the Bitwarden CLI when a connection is launched.
This works with bitwarden.com, self-hosted Bitwarden and Vaultwarden.

Bitwarden has no library interface, so the provider runs "bw serve" as a
hidden child process bound to 127.0.0.1 on a random free port and talks
to its local REST API. The server starts lazily on first use and is
stopped when the application exits; on Windows it is placed in a
kill-on-close job object and on Linux it gets a parent death signal, so
a crash cannot leave an orphaned server holding an unlocked vault.

The child inherits BW_SESSION from the environment, so the vault is
unlocked once in the user's shell. MremoteGO never asks for, sees or
stores the master password.

To avoid wiring a second password manager directly into the config
manager, launcher and GUI, secret handling now goes through a small
Provider interface and a process-wide Registry. 1Password keeps working
unchanged; the crypto package uses the registry to decide that
references must not be encrypted at rest.

The connection dialogs gain a "Bitwarden..." button that lists and
searches vault login items and writes back a reference, and a "Store
password in Bitwarden" option that creates a login item from a typed
password. The startup authentication warning is now generic across
providers, only asks about providers the config actually uses, and runs
off the UI goroutine because starting the CLI takes a moment.

Covered by unit tests that exercise the client against a fake bw serve,
so neither the CLI nor a GUI is required to run them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
On every platform the connection password was visible in the process
list for the lifetime of the client process, which means any local user
could read it:

  putty.exe -ssh ... -pw <password> host
  sshpass -p <password> ssh ...
  cmdkey /generic:TERMSRV/host /user:u /pass:<password>

PuTTY itself documents -pw as insecure and recommends -pwfile, so the
password now goes into a private temporary file that PuTTY reads while
parsing its arguments; the file is removed as soon as the process is up,
or after a short grace period. Files left behind by a killed run are
cleaned up at start-up.

sshpass cannot take the password from the environment directly here,
because the terminal emulator hop (gnome-terminal, Terminal.app) does
not forward environment variables or file descriptors. The generated
shell snippet therefore reads the file into SSHPASS and deletes it
before ssh is executed, so the password is neither in argv nor on disk
for longer than necessary.

RDP credentials are written through the Windows Credential Manager API
instead of shelling out to cmdkey. The blob is encoded as UTF-16LE,
which is what mstsc expects; UTF-8 would store a credential that looks
valid but silently fails to log in.

launchInTerminal now takes the prepared shell snippet plus an equivalent
argument vector, used only on systems with no terminal emulator, and the
host name is passed explicitly rather than guessed from the arguments.
Every value interpolated into a snippet is single-quote escaped, as
before.

Known limitation: xfreerdp on Linux is still invoked with /p:.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Bitwarden CLI refuses to start "bw serve" at all when no user is
logged in: it prints "You are not logged in." on stderr and exits. That
surfaced as "bw serve exited before it became ready", which tells the
user nothing about what to do.

The start-up failure output is now classified, so a missing login
reports ErrNotAuthenticated and a locked vault reports ErrVaultLocked,
the same errors the running server would produce. Connecting with an
unusable vault now says "not logged in to bitwarden" and the start-up
dialog offers the sign-in instructions.

Also adds two tests that run against the real system when it is
available and skip otherwise:

  - the Bitwarden CLI, to check that the helper process starts, answers
    and is stopped, treating an unauthenticated CLI as a valid outcome;
  - cmdkey, to check that the credential written through the Credential
    Manager API is byte for byte identical to what cmdkey stores. This
    pins the UTF-16LE blob encoding and enterprise persistence that
    mstsc relies on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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