Skip to content

pwmanager 2.4: browser generator, enforced vault integrity, honest passphrase entropy - #8

Open
Sebby1770 wants to merge 1 commit into
mainfrom
feat/web-generator-and-hardening
Open

pwmanager 2.4: browser generator, enforced vault integrity, honest passphrase entropy#8
Sebby1770 wants to merge 1 commit into
mainfrom
feat/web-generator-and-hardening

Conversation

@Sebby1770

Copy link
Copy Markdown
Owner

Adds the web generator and fixes four defects in the CLI — three of which weakened guarantees the README already advertised.

The website

A static generator at web/, deployed to GitHub Pages by .github/workflows/pages.yml. Five files, no build step, no dependencies.

  • Password and passphrase modes, the four CLI presets (PIN / Wi-Fi / Strong / Maximum), length and word-count sliders, character-class toggles, an exclude list, and batch generation for bulk rotation.
  • A live strength readout — entropy in bits, alphabet size, and an offline cracking estimate — using the same thresholds as strength_label.
  • A "same policy from the terminal" panel showing the exact pwmanager gen command for the current settings, so the site doubles as CLI documentation.
  • An opt-in breach check against Have I Been Pwned, k-anonymity only: five hex characters of a SHA-1 hash, on an explicit click.

Security properties, all tested:

Property How
No modulo bias crypto.getRandomValues with rejection sampling; a distribution test would fail a biased implementation
No weak fallback There is no Math.random path — without a CSPRNG it raises
Nothing persisted localStorage holds interface preferences under one key; never a secret
Nothing exfiltrated CSP default-src 'none', connect-src limited to the HIBP endpoint
Works offline Wordlist and all logic bundled; no remote assets

generator.js is shared verbatim with tests/js/run.mjs, so the browser code is covered by 35 tests rather than only by eyeballing.

Fixes

The vault integrity check was a no-op. unlock() computed the file HMAC and then discarded the result:

if not hmac.compare_digest(expected, payload["hmac"]):
    pass   # <-

A vault modified outside pwmanager opened without complaint. It now raises VaultIntegrityError — but only after the ciphertext decrypts, so an ordinary wrong password is still reported as a wrong password rather than as tampering.

The HMAC did not cover version or kdf. Both are stored unencrypted and Fernet does not authenticate them, so edits went undetected. The MAC is now domain-separated and length-prefixed over all four plain fields. Vaults carrying the older salt+ciphertext MAC still open and are upgraded on the next save.

Vault files were written through the process umask, typically 0644 — readable by every local account. They are now created 0600 from the start, temp file included, and fsynced before and after the atomic rename. doctor reports the mode of an existing vault; Vault.tighten_permissions() fixes one.

save() could destroy a vault. It re-read the file for its salt without checking the read succeeded. It now refuses to overwrite a vault it cannot parse and explains why.

Passphrase strength was overstated by roughly 3x. password_entropy_bits applied a per-character model to dictionary passphrases, reporting ~150 bits for a five-word phrase actually worth ~55. Recognisable wordlist phrases are now scored words * log2(listSize); random passwords are unchanged. This is visible in the UI — a five-word passphrase reads "Reasonable, 55.0 bits", not "Very strong".

Also

  • pwmanager gen gained --no-lower, --no-upper, --no-digits, --separator, --capitalize, --count N. The site's CLI hints previously suggested flags that did not exist; a test now fails if it ever suggests one again.
  • CI runs the Node tests and fails if web/wordlist.js has drifted from pwmanager/data/eff_short.txt.

Verification

  • 129 pytest tests pass (was 108), 35 Node tests pass.
  • End-to-end: a vault created by the CLI is -rw-------; flipping its version field makes verify fail with the integrity error; restoring it makes verify pass again.
  • The page was driven in a browser: presets, both tabs, batch, dark mode and the mobile layout all behave, with no console errors.

Note

PR #3 (feat/v2.0-sqlite-integrity-and-packaging, opened July) predates the 2.2/2.3 merges and would delete tests that now exist on main. It is stale and worth closing rather than merging.

🤖 Generated with Claude Code

…nest passphrase entropy

Adds a static web generator and fixes four defects in the CLI, three of
which weakened guarantees the README already advertised.

Web generator (web/, deployed to GitHub Pages)
- Password and passphrase generation entirely client-side, no build step.
- crypto.getRandomValues with rejection sampling — no modulo bias, and no
  Math.random fallback; it refuses to generate without a CSPRNG.
- Shares the CLI's presets, symbol set, lookalike set, wordlist and entropy
  thresholds. tests/test_web_parity.py fails if either side drifts, including
  a check that every flag the site suggests actually exists in `gen`.
- CSP allows exactly one outbound destination (the HIBP range API) and it is
  only contacted on an explicit click, sending five hex characters of a hash.
- Nothing generated is stored: localStorage holds interface preferences only.

Fixes
- unlock() computed the vault HMAC and threw the result away, so a vault
  edited outside pwmanager opened silently. It now raises VaultIntegrityError
  after decryption succeeds, so a wrong password is still a wrong password.
- The HMAC covered only salt+ciphertext, leaving the unencrypted version and
  kdf fields unauthenticated. It is now domain-separated and length-prefixed
  over all four; legacy vaults still open and upgrade on the next save.
- Vault files were written through the process umask (usually 0644). They are
  now created 0600 including the temp file, and fsynced around the rename.
- save() re-read the file for its salt without checking the read worked; it
  now refuses to overwrite a vault it cannot parse.
- password_entropy_bits scored dictionary passphrases per character, claiming
  ~150 bits for a five-word phrase worth ~55. Recognised wordlist phrases now
  use words * log2(listSize); random passwords are unchanged.

Also
- `gen` gained --no-lower/--no-upper/--no-digits/--separator/--capitalize/--count.
- `doctor` reports vault file permissions.
- CI runs the Node generator tests and fails on a stale web/wordlist.js.

129 pytest tests and 35 Node tests pass.

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