The password vault and autofill
PasswordVault (services/vault.py)
stores logins in vault.bin. Nothing sensitive is ever written to disk in plain text.
Encryption#
vault.bin (JSON header)
├── mode: "dpapi" | "master"
├── wrapped_key ── the 256-bit vault key, protected by DPAPI or by a key derived from the master password
├── salt, kdf, key_nonce (master-password mode only)
└── nonce, data ── the entries, sealed with AES-256-GCM under the vault key
- The vault key is a random 256-bit AES key generated when the vault is created.
- The entries (origin, username, password, space, note, timestamps) and the never save list are serialised to
JSON and encrypted with AES-256-GCM (
cryptography), with a fresh random nonce on every save and the associated datajbrowser-vault-data-v1. - The vault key itself is stored wrapped:
- DPAPI mode (the default):
CryptProtectDataties it to the Windows user account (platform/win.py). The vault unlocks transparently; another Windows user, or the file copied to another PC, can't decrypt it. - Master-password mode: a key-encryption key is derived from the password with Scrypt (n = 2^17 for passwords set since 1.5.2 (older vaults keep their stored n = 2^15), r = 8,
p = 1, a random salt), and wraps the vault key with AES-GCM (associated data
jbrowser-vault-key-v1). The vault is locked until the password is entered.
- DPAPI mode (the default):
- Switching modes re-wraps the same vault key; entries are not re-encrypted.
- The file is written with
atomic_write_bytes(), so a crash can't corrupt it.
Capturing and filling#
The password logic in the page runs in JBrowser's isolated world (AUTOFILL_JS,
page scripts), so the page's own scripts can't read the bridge, the fill function or a
credential before it is placed in the form.
- When a visible password field appears, the script calls
jbBridge.loginFormDetected(count). TabController.on_login_form()looks up saved logins for the page's origin (and space). With exactly one match andpasswords.autofillon, it callswindow.__jbFill(username, password)in the isolated world.- When a login form is submitted, the script calls
jbBridge.credentialsSubmitted(username, password). on_credentials()offers Save or Update in an info bar (passwords.offer_save), unless the space is incognito, the site is on the never list, or the login is already saved.
Logins can be saved for one space or for all spaces (Credential.space_id == "").
Other features#
- CSV import from Chrome, Edge and Firefox exports (
import_csv). It also reads a ZIP: plain, or AES-encrypted (it asks for the password), such as JBrowser's own export. - New in 1.5.4 Encrypted export. Export… in the password manager writes every login as CSV (the
name,url,username,password,notecolumns Chrome, Edge and Firefox use, fromexport_csv()) inside a ZIP locked with a password the user chooses (at least 10 characters, typed twice). With a master password, it is asked for first. The CSV never touches the disk unencrypted. See the encrypted ZIP format below. - Password health:
password_problems()flags weak passwords (shorter than 8 characters, a common password, 3 or fewer distinct characters, or shorter than 12 with only one kind of character) and passwords reused on more than one site, all locally. - Generator and clipboard auto-clear in the password manager window (ui/dialogs/passwords.py).
The encrypted ZIP format#
New in 1.5.4 Python's zipfile only writes the old ZipCrypto encryption, which a known-plaintext attack breaks, so
core/aeszip.py writes WinZip AES (AE-2) with AES-256 itself, on top of
cryptography:
| Part | Value |
|---|---|
| Compression method | 99, with the 0x9901 extra field: vendor version 2 (AE-2), AE, strength 3 (256-bit), real method 8 (deflate) |
| Key derivation | PBKDF2-HMAC-SHA1(password, random 16-byte salt, 1000 iterations) → 66 bytes: AES key, HMAC key, 2-byte password check |
| Encryption | AES-256 in counter mode with a 128-bit little-endian counter starting at 1 (the key stream is made with ECB, because cryptography's CTR counts big-endian) |
| Authentication | the first 10 bytes of HMAC-SHA1 over the encrypted data; AE-2 stores no CRC |
7-Zip, WinRAR, PeaZip, Keka, The Unarchiver and libarchive (Windows' tar.exe) open these files; Windows Explorer's
Extract All doesn't support AES. The format's 1000 PBKDF2 iterations are fixed, so the password's strength matters,
hence the 10-character minimum. read_zip() reads plain and AES (AE-1/AE-2, any key strength) ZIPs, checks the
password and the HMAC, and refuses ZipCrypto. It was checked against pyzipper in both directions and against
tar.exe.
Security boundaries#
- Anything that reads decrypted passwords runs in the main process; the renderer only sees a password when it is filled into a form the user is looking at.
- Master-password mode protects against someone using the same Windows account; DPAPI mode does not, by design, in exchange for no prompts.
- Report vault issues privately (SECURITY.md); they are explicitly in scope.