Skip to content

Repository files navigation

Fob

Your secrets, on your keychain.

An encrypted vault that lives on a USB drive — passwords, TOTP codes, SSH keys, and secure notes, protected by PBKDF2-HMAC-SHA256 and AES-256-GCM. Nothing installed on your computer.

CI License Rust 1.88+


Fob turns any USB stick into a cryptographic security key. Plug it in, unlock with a passphrase, and your credentials are available as a password manager, TOTP generator, and SSH agent. Unplug and everything locks.


What's in the vault

  • Passwords — store, generate, and auto-copy credentials
  • TOTP — built-in two-factor code generation with live countdown
  • SSH keys — import existing keys from the CLI or browser vault; unlocked keys are exposed via a local SSH agent socket, compatible with any SSH client (spawned automatically by the CLI on unlock — passphrase-protected keys must have that passphrase stripped first, e.g. ssh-keygen -p -N "")
  • Secure notes — encrypted free-text entries
  • Plausible deniability — decoy vault slot with realistic fake data; duress slot that destroys the vault silently
  • Browser vault — a zero-dependency HTML file that runs entirely offline, using the exact same encrypted vault format as the CLI — either can create, open, or update a vault the other made

Security

Cryptographic primitives

There is one vault format, used identically by the CLI and the browser vault — PBKDF2 (not a memory-hard KDF like Argon2id) is used specifically because WebCrypto has no native Argon2id primitive, and keeping one interoperable format was judged more valuable than a stronger KDF the browser couldn't run.

Component Algorithm
Key derivation PBKDF2-HMAC-SHA256 — 310,000 iterations
Encryption AES-256-GCM
Key separation HKDF-SHA256, per vault slot
Post-quantum Planned — ML-KEM-1024 hybrid wrapping, not yet implemented
TOTP RFC 6238 — HMAC-SHA1/SHA256/SHA512

Threat model

Threat Mitigation
USB stolen PBKDF2 at 310k iterations raises the brute-force cost; weaker than a memory-hard KDF against GPU/ASIC attackers — choose a long passphrase
Coercion Decoy vault slot opens with its own independent, realistic-looking data
Extreme coercion Duress passphrase returns the same error as a wrong passphrase and wipes the local copy (logical wipe only — see Known limitations)
Quantum adversary Not yet mitigated — ML-KEM-1024 hybrid wrapping is planned
Clipboard exfil Auto-clears 30 seconds after any copy
Memory dumps Sensitive buffers zeroized and mlocked where possible

Architecture

All cryptographic operations live in fob-core, which has no filesystem or network access. The CLI and browser vault cannot leak key material because they never handle raw secrets — passphrases are passed directly to the crypto layer and zeroized immediately after use.

Known limitations

  • RSA SSH keys are stored but not signed. The rsa crate used by our SSH library carries RUSTSEC-2023-0071 (a timing side-channel), with no fixed version available upstream. RSA key entries can still be stored and viewed in the vault, but the SSH agent won't load them for signing. Use Ed25519 (the default for new keys) instead.
  • Imported SSH keys must not be passphrase-protected — the vault passphrase is already the protection layer; strip a key's own passphrase before importing it (ssh-keygen -p -N "").
  • The duress wipe overwrites the vault file's logical content, not necessarily its physical storage. On a copy-on-write filesystem (e.g. macOS APFS) or an SSD doing wear-leveling internally, an in-place overwrite can leave the pre-wipe bytes recoverable from other physical blocks via forensic tools, filesystem snapshots, or drive firmware — the OS-level guarantee "this file's contents are now random bytes" does not extend to "the old bytes are physically gone." If your threat model includes forensic recovery of the underlying storage medium, don't rely on the duress wipe alone; treat it as closing off the easy/logical recovery path, not a physical-media guarantee.
  • The browser vault can only wipe the actual vault.fob file if it was opened via the native file picker in a Chromium browser (Chrome, Edge, Opera — anything supporting the File System Access API). Opening the vault by drag-and-drop, via a plain <input type=file> fallback, or via the automatic same-directory load on a file:// page gives the page no writable handle to the original file at all — browsers don't allow arbitrary local file writes without one. In those cases (and always in Firefox/Safari, which don't implement the File System Access API), a duress passphrase still clears the browser's own IndexedDB cache, but the vault.fob file on the USB drive itself is left completely untouched. If you rely on the duress feature, use the CLI (fob), which always wipes the real file, or confirm you opened the browser vault through its file picker (not drag-and-drop) in a supported browser.
  • The vault file is trivially identifiable as a Fob vault, even without the passphrase. The first 4 bytes of vault.fob are the literal ASCII magic FOB2 — anyone with the file (a file/xxd/head away) can immediately confirm "this is a Fob password vault," before ever touching a passphrase. The decoy/duress design protects which passphrase unlocks which content once someone is already trying to open the vault; it does not hide that the file is a vault at all — the filename (vault.fob) gives that away too. If your threat model requires the file itself to be unidentifiable (e.g. deniability that you even use a password manager), rename it to something innocuous and be aware the magic bytes still identify it to anyone who inspects the content directly, not just the name.
  • Actively-edited secret fields (the master passphrase, a password, a note body, an SSH private key) aren't guaranteed zeroized in every intermediate state while you're typing. The TUI zeroizes each field's final value when its form is dropped, but Rust's String can reallocate internally as you type (e.g. via push/insert) — each old backing buffer is freed by the allocator without being zeroed first, so fragments of an in-progress passphrase can in principle linger in freed-but-unoverwritten heap memory until something else reuses that allocation. This only matters against an adversary who can already read the process's memory (a debugger, a core dump, a swapped-out page) — a much higher bar than a passing observer — but it means the README's "sensitive buffers zeroized" claim is exact for data at rest and for a field's value once you stop editing it, not for every transient buffer touched while you were actively typing it.

Installation

curl -fsSL https://raw.githubusercontent.com/Arcel-Org/Fob/main/install/install.sh | sh

Installs fob and fob-agent to ~/.fob/bin and adds that to your PATH. Every downloaded release is verified against its published SHA256 checksum before anything is installed, and against its cosign signature too if cosign is on your PATH.

Useful flags (pass them after | sh -s --, e.g. ... | sh -s -- --no-path):

Flag Effect
--version=vX.Y.Z install a specific release instead of latest
--no-path don't modify your shell rc file
--local=/path/to/fob install from a local build instead of downloading
--uninstall remove Fob's installed binaries
--help show all flags

No release built yet, or want to build it yourself? See below.


Building from source

Requires Rust 1.88+.

git clone https://github.com/Arcel-Org/Fob.git
cd Fob
cargo build --release -p fob-cli -p fob-agent

The binaries land at target/release/fob and target/release/fob-agent — both are required (the CLI spawns the agent as a sibling process for SSH agent support).


Testing

cargo test --workspace                                    # crypto/vault + TUI rendering tests
cargo clippy --workspace --all-targets -- -D warnings
cargo fmt --all -- --check

python3 web/tests/browser_interaction_test.py              # headless-chromium checks for web/index.html

The browser-vault check drives a real headless Chromium instance (requires chromium and Python's websockets package) through the actual UI — search filtering, the TOTP countdown, auto-lock on inactivity, and entry-list rendering/scrolling with many entries.

Automated tests cover the logic and rendering paths; they are not a substitute for a real person using the app. See USABILITY_TESTING.md for a first-time-user test script covering both interfaces.


Repository layout

fob/
├── crates/
│   ├── fob-core/       # cryptography and vault format — no I/O, pure logic
│   ├── fob-cli/        # TUI — USB provisioning and vault browsing/editing
│   └── fob-agent/      # SSH agent daemon
├── install/
│   └── install.sh      # one-line installer
└── web/
    ├── index.html      # zero-dependency browser vault
    └── tests/          # headless-chromium interaction checks for index.html

Contributing

Issues and pull requests welcome. For security vulnerabilities, please open a private advisory rather than a public issue.


License

Licensed under either of MIT or Apache 2.0 at your option.

About

Encrypted vault on any USB drive — passwords, TOTP, SSH keys, secure notes. Argon2id + XChaCha20-Poly1305, plausible deniability, no install required.

Topics

Resources

Contributing

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages