browserpass-host-rs 0.6.1

Rust port of browserpass-native (PROTOCOL.md v3.1.2) + extension actions for OTP, whole-store search, and a file-state segmented download manager.
Documentation

browserpass-host-rs

crates.io License: MIT

Rust port of browserpass-native — a drop-in replacement for the Go binary that the browserpass-extension browser extension talks to via Chrome / Firefox native messaging — plus three additive actions (OTP, whole-store search, segmented download manager) that browserpass-extension does not call.

Single static binary. Pure-Rust dependency tree (serde, serde_json, ureq with rustls). No aria2, no system OpenSSL, no Go toolchain at runtime.

Wire compatibility with upstream

browserpass-host-rs implements every action documented in browserpass-native's PROTOCOL.md v3.1.2:

Action Implementation Test pin
configure ported/request/configure.rs tests/ported_configure_helpers.rs + integration
list ported/request/list.rs tests/ported_integration.rs (4 cases)
tree ported/request/tree.rs tests/ported_integration.rs
fetch ported/request/fetch.rs tests/ported_integration.rs
save ported/request/save.rs tests/ported_integration.rs
delete ported/request/delete.rs tests/ported_integration.rs (incl. parent cleanup)
echo bin/browserpass_host_rs.rs tests/ported_integration.rs

Error codes 10–32 from errors/errors.go pin to the same integers; exit code equals the error code (matches upstream errors.ExitWithCode); version is reported as 3.1.2 (packed int 3_001_002).

Strict port discipline

The src/ported/ tree is a 1:1 Rust mirror of the upstream Go source:

  • Every file under src/ported/ mirrors a single upstream Go file by stem and relative subpath. errors/errors.gosrc/ported/errors/errors.rs, request/configure.gosrc/ported/request/configure.rs, etc.
  • Every Rust fn carries a /// Port of <name>() from <go_file>:<line> doc comment.
  • Go's PascalCase / camelCase identifiers are preserved verbatim (DetectGpgBinary, MakeConfigureResponse, parseRequestLength). File-level #![allow(non_snake_case)] makes this an explicit, audit-friendly decision rather than a style accident.
  • Every Go inline comment carries over to the Rust port with a // go:NN line-number citation on the corresponding Rust statement.
  • Local variable names match Go's (gpgPath, normalizedStorePath, parentDir, responseData).
  • No invented helpers — the two recursive store walkers inside list.rs and tree.rs are inlined private fns at the call site because they replace external Go deps (mattn/go-zglob, mattn/go-zglob/fastwalk), not Go-source fns.
  • Drift is caught by tests/ported_errors.rs (every error code pinned to its PROTOCOL.md value) + tests/ported_version.rs (version triple + packed int) + tests/ported_integration.rs (every action exercised end-to-end against the compiled binary).

This discipline is borrowed from the zshrs PORT.md rules and adapted to a Go→Rust port.

Extension actions

These are additive — browserpass-extension never sends them, so wire compatibility with upstream is preserved. They live under src/extensions/:

Action Behavior Wire shape
otp Shells pass otp <entry> with PASSWORD_STORE_DIR set to the matching store. Returns the current TOTP code. request {action:"otp", storeId, file, settings} → ok {code:"123456"}
search Host-side fuzzy + substring scoring across every configured store. Faster than client-side fzf for large stores. request {action:"search", settings, echoResponse:"<query>"} → ok {matches:[{store, path}, …]}
dl.add Spawns a detached worker (browserpass-host-rs --dl-worker <gid>) that performs a multi-segment download via ureq + Range requests. State lives at $XDG_CACHE_HOME/zpwrchrome/dl/gid_NNNNNN.json. request {action:"dl.add", url, dir?, name?, segments?, cookies?, userAgent?} → ok {gid, dest}
dl.list Reads every state file under the cache dir and returns the job array. request {action:"dl.list"} → ok {jobs:[JobState, …]}
dl.pause Writes paused=true into the state file. Worker polls the flag between chunks. request {action:"dl.pause", gid} → ok {gid, status:"paused"}
dl.resume Clears paused/cancelled flags. Respawns the worker if it had previously terminated. request {action:"dl.resume", gid} → ok {gid, status:"resumed"}
dl.cancel Writes cancelled=true. Worker removes partial dest file + state file on exit. request {action:"dl.cancel", gid} → ok {gid, status:"cancelled"}

The downloader is built around HTTP Range requests:

  • HEAD probe for Content-Length + Accept-Ranges
  • Pre-allocates the destination file via set_len
  • Spawns N segment threads (default 4, clamp 1–16) — each writes its byte range at its file offset
  • Retries transient failures (5xx + transport errors) with 200ms × 3ⁿ backoff, up to 4 attempts; segments resume from their local downloaded offset via Range header on retry
  • Filename collisions auto-rename foo.zipfoo (1).zip (dotfile-aware: .bashrc.bashrc (1), not (1).bashrc)
  • Cookies + User-Agent are forwarded from chrome.cookies.getAll() so logged-in downloads work the same way the browser would

Install

As a Cargo binary

cargo install browserpass-host-rs

That installs browserpass-host-rs into $CARGO_HOME/bin. Then register the native-messaging manifest so a browser will spawn the binary:

# From the source repo (clone https://github.com/MenkeTechnologies/zpwrchrome)
cd host
./install-browserpass.sh

The installer writes com.github.browserpass.native.json to every detected Chromium-family browser config dir on macOS and Linux, plus Firefox's native-messaging directory. allowed_origins / allowed_extensions are populated with the public browserpass-extension IDs from the Chrome Web Store, AMO, and Edge Add-ons.

As a drop-in for the Go binary

If the upstream browserpass-native package is already installed via a package manager (apt, brew, etc.), uninstall it first — both binaries register under the same NM name (com.github.browserpass.native) and the last one to write the manifest wins. browserpass-extension will then talk to browserpass-host-rs transparently.

Architecture

host/src/
├── lib.rs                       # pub mod ported + extensions + frame
├── frame.rs                     # NM length-prefixed JSON framing (≤1 MiB)
├── ported/                      # 1:1 Rust port of browserpass-native
│   ├── errors/errors.rs
│   ├── helpers/helpers.rs
│   ├── request/
│   │   ├── common.rs            # normalizePasswordStorePath (env expansion inlined)
│   │   ├── configure.rs         # configure + getDefaultPasswordStorePath + readDefaultSettings
│   │   ├── delete.rs            # deleteFile + parent-dir cleanup loop
│   │   ├── fetch.rs             # fetchDecryptedContents + gpg dispatch chain
│   │   ├── list.rs              # listFiles (inline std::fs walker replaces zglob)
│   │   ├── process.rs           # Process + parseRequestLength + parseRequest + request types
│   │   ├── save.rs              # saveEncryptedContents + .gpg-id recipient walk
│   │   └── tree.rs              # listDirectories (inline std::fs walker replaces fastwalk)
│   ├── response/response.rs     # ok/error envelopes, send_ok/send_err/send_raw
│   └── version/version.rs       # 3.1.2 / 3_001_002
├── extensions/                  # additive — not in upstream
│   ├── dl.rs                    # file-state segmented downloader + worker process
│   ├── otp.rs                   # shells pass otp
│   └── search.rs                # host-side fuzzy + substring scoring
└── bin/
    └── browserpass_host_rs.rs   # port of main.go + extension dispatch hook

Testing

cargo test

Currently 69 tests across:

  • Pure protocol pins (tests/ported_version.rs, tests/ported_errors.rs) — 5
  • Pure helpers (tests/ported_helpers.rs, tests/ported_common.rs, tests/ported_configure_helpers.rs) — 15
  • End-to-end with spawned binary (tests/ported_integration.rs) — 12 cases including echo round-trip, every error code path, and configure/list/tree/delete against tempdir stores
  • Frame round-trip (tests/frame_roundtrip.rs) — 6
  • Extensions: extensions_otp.rs (3), extensions_search.rs (6), extensions_dl_state.rs (16), extensions_dl_integration.rs (6 including a 2 MiB segmented download against a local HTTP server with Range support, verified byte-for-byte)

Total: 69 tests, 0 failures.

License

MIT. See LICENSE.

Credits