Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Vault data is encrypted on the client with a key derived from your master
password, and the server only ever stores ciphertext + KDF parameters. A
database breach leaks nothing usable.

![Passman vault dashboard](docs/img/vault.png)
![Passman vault dashboard with Edit, Import, and Connect actions per row](docs/img/vault.png)

The vault treats credentials as connection targets, not just `name +
password` rows: every entry carries protocol, hostname, IP, port, and
Expand All @@ -18,6 +18,14 @@ auto-clear; the server still sees only ciphertext.

![Connect dialog with JDBC, SSH, copy-command, and RDP options](docs/img/vault-connect.png)

Login is optionally protected by **TOTP 2FA** (Google Authenticator,
1Password, Authy, …) with single-use recovery codes. Vault contents
remain zero-knowledge regardless — even if the OTP secret leaks, the
master key the server never sees is still the only thing that decrypts
the vault.

![Two-factor authentication setup with QR code and recovery codes](docs/img/settings-2fa-setup-qr.png)

## Architecture at a glance

```
Expand Down
28 changes: 22 additions & 6 deletions docs/USER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ The dialog greys out actions that don't apply to a credential:

Click **+ New credential** in the top-right:

![Add credential form with Protocol, Database, Environment, and per-engine fields](img/vault-add.png)
![Add credential form with the password generator open and the SSH private-key textarea visible](img/vault-add.png)

The form is **protocol-aware**:

Expand Down Expand Up @@ -357,14 +357,20 @@ schema bump.

### 5g. Two-factor authentication (TOTP)

Open the **Settings** link in the sidebar's user card → click **Set up 2FA**.
Open the **Settings** link in the sidebar's user card. With 2FA off,
the Security section shows a single "Set up 2FA" call to action:

The setup flow has three steps:
![Settings page with 2FA disabled](img/settings-2fa-off.png)

Click **Set up 2FA** to start the three-step setup flow:

1. **Scan the QR** with your authenticator (Google Authenticator,
1Password, Authy, Microsoft Authenticator, …). If your phone can't
reach the screen, expand "Can't scan? Type this manually" and copy
the base32 secret directly into the app.

![2FA setup — scan QR step](img/settings-2fa-setup-qr.png)

2. **Confirm the first code.** Type the 6-digit code your authenticator
shows. Passman verifies against the stored secret and only then
flips the `totp_enabled` flag — if you abandon the flow before this
Expand All @@ -374,9 +380,19 @@ The setup flow has three steps:
them, download the `.txt`, or write them down — Passman keeps only
Argon2id hashes server-side, so this list cannot be retrieved later.

Once enabled, login becomes two-step: email + master password, then a
6-digit code. Recovery codes work in place of the 6-digit code (and
are consumed on first use — `9 remaining` becomes `8`).
![2FA setup — save recovery codes step](img/settings-2fa-setup-recovery.png)

Once enabled, the Settings page shows the active state + how many
recovery codes are left:

![Settings page with 2FA enabled](img/settings-2fa-on.png)

Login becomes two-step: email + master password (the existing screen),
then a 6-digit code on a second screen. Recovery codes work in place
of the 6-digit code (and are consumed on first use — `9 remaining`
becomes `8`):

![Two-factor verification step on login](img/login-otp.png)

> **Trade-off, plainly stated.** TOTP is **not** zero-knowledge —
> RFC 6238 requires the verifier to know the shared secret, so the
Expand Down
Binary file added docs/img/login-otp.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/img/settings-2fa-off.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/img/settings-2fa-on.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/img/settings-2fa-setup-qr.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/img/settings-2fa-setup-recovery.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/img/vault-add.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/img/vault-connect.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/img/vault.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 7 additions & 2 deletions docs/preview/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,14 @@ <h1>Passman screenshots</h1>
<li><a href="register.html">Register</a></li>
<li><a href="login.html">Login &mdash; default branding</a></li>
<li><a href="login-whitelabel.html">Login &mdash; with white-label override (Acme)</a></li>
<li><a href="vault.html">Vault &mdash; sidebar dashboard</a></li>
<li><a href="login-otp.html">Login &mdash; 2FA OTP step</a></li>
<li><a href="vault.html">Vault &mdash; sidebar dashboard (Edit + Import + Settings)</a></li>
<li><a href="vault-connect.html">Vault &mdash; Connect dialog (JDBC / SSH / RDP / copy-command)</a></li>
<li><a href="vault-add.html">Vault &mdash; add credential form</a></li>
<li><a href="vault-add.html">Vault &mdash; add credential form (password generator + SSH key)</a></li>
<li><a href="settings-2fa-off.html">Settings &mdash; 2FA disabled</a></li>
<li><a href="settings-2fa-setup-qr.html">Settings &mdash; 2FA setup, scan QR step</a></li>
<li><a href="settings-2fa-setup-recovery.html">Settings &mdash; 2FA setup, save recovery codes</a></li>
<li><a href="settings-2fa-on.html">Settings &mdash; 2FA enabled</a></li>
<li><a href="extension-locked.html">Extension &mdash; locked</a></li>
<li><a href="extension-unlocked.html">Extension &mdash; unlocked</a></li>
</ul>
Expand Down
37 changes: 37 additions & 0 deletions docs/preview/login-otp.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Passman — Two-factor verification</title>
<link rel="stylesheet" href="../../packages/web/src/styles.css" />
</head>
<body>
<main class="auth-container">
<div class="auth-brand">
<div class="brand-mark brand-mark-lg"></div>
<span class="auth-brand-name">Passman</span>
</div>
<h1>Two-factor verification</h1>
<p class="auth-tagline">
Enter the 6-digit code from your authenticator app, or one of your
recovery codes.
</p>
<form>
<label>
Authenticator code
<input
inputmode="numeric"
pattern="[0-9]*"
value="142 857"
placeholder="123456 or xxxx-xxxx"
/>
</label>
<button type="submit">Continue</button>
</form>
<p>
<button type="button" class="user-card-action">← Start over</button>
</p>
</main>
</body>
</html>
44 changes: 44 additions & 0 deletions docs/preview/settings-2fa-off.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Passman — Settings</title>
<link rel="stylesheet" href="../../packages/web/src/styles.css" />
</head>
<body>
<main class="auth-container settings-container">
<div class="auth-brand">
<div class="brand-mark brand-mark-lg"></div>
<span class="auth-brand-name">Passman</span>
</div>
<h1>Settings</h1>
<p class="auth-tagline">Zero-knowledge password manager</p>

<section class="settings-section">
<header>
<h2>Two-factor authentication</h2>
<p class="settings-section-hint">
Adds a 6-digit code from your authenticator app (Google Authenticator,
1Password, Authy, …) to login. <strong>Note:</strong> enabling 2FA
stores the OTP secret server-side. Vault contents stay encrypted
with your master key — the server still can't read them.
</p>
</header>

<div class="settings-row">
<div>
<div class="settings-status">
<span class="status-dot status-dot-off"></span> Not enabled
</div>
</div>
<button type="button" class="btn btn-primary">Set up 2FA</button>
</div>
</section>

<p class="settings-back">
<a href="vault.html">← Back to vault</a>
</p>
</main>
</body>
</html>
45 changes: 45 additions & 0 deletions docs/preview/settings-2fa-on.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Passman — Settings (2FA enabled)</title>
<link rel="stylesheet" href="../../packages/web/src/styles.css" />
</head>
<body>
<main class="auth-container settings-container">
<div class="auth-brand">
<div class="brand-mark brand-mark-lg"></div>
<span class="auth-brand-name">Passman</span>
</div>
<h1>Settings</h1>
<p class="auth-tagline">Zero-knowledge password manager</p>

<section class="settings-section">
<header>
<h2>Two-factor authentication</h2>
<p class="settings-section-hint">
Adds a 6-digit code from your authenticator app (Google Authenticator,
1Password, Authy, …) to login. <strong>Note:</strong> enabling 2FA
stores the OTP secret server-side. Vault contents stay encrypted
with your master key — the server still can't read them.
</p>
</header>

<div class="settings-row">
<div>
<div class="settings-status">
<span class="status-dot status-dot-on"></span> Enabled
</div>
<div class="settings-status-meta">9 recovery codes remaining</div>
</div>
<button type="button" class="btn">Disable 2FA</button>
</div>
</section>

<p class="settings-back">
<a href="vault.html">← Back to vault</a>
</p>
</main>
</body>
</html>
71 changes: 71 additions & 0 deletions docs/preview/settings-2fa-setup-qr.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Passman — Set up 2FA</title>
<link rel="stylesheet" href="../../packages/web/src/styles.css" />
</head>
<body>
<main class="auth-container settings-container">
<div class="auth-brand">
<div class="brand-mark brand-mark-lg"></div>
<span class="auth-brand-name">Passman</span>
</div>
<h1>Settings</h1>
<p class="auth-tagline">Zero-knowledge password manager</p>

<section class="settings-section" style="opacity: 0.4">
<header>
<h2>Two-factor authentication</h2>
<p class="settings-section-hint">
Adds a 6-digit code from your authenticator app to login.
</p>
</header>
<div class="settings-row">
<div>
<div class="settings-status">
<span class="status-dot status-dot-off"></span> Not enabled
</div>
</div>
<button type="button" class="btn btn-primary">Set up 2FA</button>
</div>
</section>
</main>

<!-- ============================== Setup modal ============================== -->
<div class="modal-backdrop" role="dialog" aria-modal="true">
<div class="modal modal-wide">
<div class="modal-head">
<div>
<h2>Set up two-factor authentication</h2>
<div class="target">Step 1 of 2 · Scan the QR with your authenticator</div>
</div>
<button class="x" aria-label="Close">✕</button>
</div>

<div class="modal-body">
<div class="totp-scan">
<div class="totp-qr" aria-label="QR code for your authenticator app"><svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="0 0 47 47" shape-rendering="crispEdges"><path fill="#0d0f10" d="M0 0h47v47H0z"/><path stroke="#3ECF8E" d="M1 1.5h7m2 0h2m3 0h2m1 0h1m2 0h1m2 0h1m1 0h2m3 0h1m1 0h1m3 0h1m1 0h7M1 2.5h1m5 0h1m1 0h2m4 0h1m1 0h1m1 0h1m1 0h2m3 0h2m1 0h4m1 0h1m1 0h1m2 0h1m5 0h1M1 3.5h1m1 0h3m1 0h1m2 0h2m1 0h2m1 0h1m3 0h1m1 0h3m1 0h1m3 0h2m2 0h1m1 0h1m2 0h1m1 0h3m1 0h1M1 4.5h1m1 0h3m1 0h1m3 0h1m3 0h2m4 0h1m1 0h1m1 0h5m4 0h1m1 0h2m1 0h1m1 0h3m1 0h1M1 5.5h1m1 0h3m1 0h1m1 0h4m1 0h2m3 0h1m1 0h7m2 0h2m3 0h3m1 0h1m1 0h3m1 0h1M1 6.5h1m5 0h1m6 0h5m1 0h2m3 0h2m1 0h1m2 0h1m1 0h2m4 0h1m5 0h1M1 7.5h7m1 0h1m1 0h1m1 0h1m1 0h1m1 0h1m1 0h1m1 0h1m1 0h1m1 0h1m1 0h1m1 0h1m1 0h1m1 0h1m1 0h1m1 0h1m1 0h7M14 8.5h1m5 0h2m3 0h1m1 0h3m1 0h3m1 0h1m1 0h1M1 9.5h1m1 0h1m1 0h1m1 0h1m2 0h1m1 0h1m1 0h1m2 0h1m1 0h1m1 0h9m4 0h1m2 0h1m3 0h1m2 0h1M1 10.5h1m3 0h1m2 0h1m1 0h4m1 0h3m5 0h3m3 0h1m2 0h1m1 0h1m2 0h1m4 0h1m2 0h1M1 11.5h3m1 0h1m1 0h1m1 0h4m1 0h1m4 0h1m1 0h1m1 0h1m5 0h1m1 0h2m4 0h2m1 0h6M1 12.5h1m3 0h1m2 0h1m2 0h2m2 0h4m2 0h2m2 0h2m1 0h2m2 0h2m1 0h1m4 0h1m4 0h1M3 13.5h1m1 0h1m1 0h1m1 0h1m2 0h1m2 0h5m4 0h4m2 0h1m1 0h3m1 0h1m1 0h2m1 0h1m2 0h1M2 14.5h3m1 0h1m1 0h1m3 0h1m2 0h1m1 0h3m1 0h1m2 0h1m1 0h3m1 0h4m2 0h1m1 0h1m5 0h1M2 15.5h3m1 0h3m2 0h2m1 0h1m1 0h1m2 0h1m4 0h1m1 0h3m4 0h2m10 0h1M1 16.5h3m5 0h3m2 0h1m1 0h4m1 0h1m2 0h1m2 0h3m1 0h1m1 0h1m1 0h5m1 0h3M1 17.5h1m2 0h1m1 0h3m2 0h2m1 0h1m1 0h1m1 0h1m1 0h2m2 0h7m1 0h4m3 0h3m1 0h2M2 18.5h1m6 0h1m1 0h1m1 0h1m2 0h1m1 0h2m1 0h2m1 0h6m2 0h1m1 0h1m1 0h1m2 0h2m2 0h1m1 0h1M2 19.5h2m2 0h2m4 0h1m3 0h3m4 0h1m1 0h1m2 0h1m1 0h4m4 0h1m3 0h2M1 20.5h1m1 0h4m3 0h4m1 0h2m3 0h1m6 0h3m2 0h1m2 0h9m1 0h1M1 21.5h1m3 0h5m3 0h3m3 0h1m1 0h9m1 0h1m2 0h2m1 0h5m3 0h1M3 22.5h3m3 0h1m2 0h4m2 0h4m3 0h2m2 0h2m1 0h3m1 0h2m3 0h1m2 0h2M4 23.5h2m1 0h1m1 0h3m1 0h1m1 0h4m2 0h1m1 0h1m1 0h1m3 0h2m1 0h1m3 0h2m1 0h1m1 0h1m1 0h3M1 24.5h1m1 0h3m3 0h1m2 0h3m3 0h2m1 0h1m3 0h2m2 0h1m2 0h2m1 0h1m1 0h1m3 0h1m2 0h1M1 25.5h1m1 0h7m2 0h1m1 0h1m2 0h2m1 0h11m3 0h1m2 0h6M5 26.5h1m3 0h4m1 0h1m2 0h2m4 0h2m4 0h5m2 0h3m4 0h3M2 27.5h1m2 0h1m1 0h1m2 0h3m2 0h2m1 0h2m9 0h1m2 0h1m1 0h1m2 0h1m1 0h1m1 0h5M1 28.5h1m1 0h1m1 0h2m1 0h5m2 0h8m5 0h1m4 0h2m3 0h2m1 0h1m3 0h1M1 29.5h4m2 0h1m1 0h2m4 0h2m2 0h1m1 0h2m3 0h1m2 0h1m2 0h3m2 0h4m1 0h1m1 0h1M1 30.5h1m2 0h3m3 0h5m2 0h2m1 0h1m2 0h3m1 0h3m1 0h1m1 0h1m3 0h2m3 0h3M2 31.5h2m2 0h2m1 0h2m2 0h1m1 0h3m1 0h1m1 0h2m2 0h2m1 0h1m2 0h1m1 0h1m4 0h4M4 32.5h1m5 0h1m1 0h1m1 0h1m5 0h1m3 0h2m1 0h4m2 0h1m1 0h1m1 0h4m1 0h4M1 33.5h4m1 0h2m3 0h5m1 0h3m1 0h1m3 0h1m1 0h1m1 0h1m1 0h3m4 0h1m4 0h2M1 34.5h1m2 0h1m1 0h1m1 0h2m4 0h2m1 0h2m2 0h2m2 0h2m1 0h1m4 0h1m2 0h1m1 0h2m4 0h2M5 35.5h1m1 0h4m2 0h2m1 0h2m2 0h4m1 0h1m2 0h1m3 0h1m5 0h2m2 0h1m2 0h1M2 36.5h4m2 0h1m3 0h3m2 0h2m1 0h4m1 0h1m1 0h3m1 0h3m1 0h1m2 0h7M1 37.5h1m2 0h2m1 0h6m1 0h15m1 0h2m1 0h1m1 0h1m1 0h5m2 0h1M9 38.5h1m2 0h1m2 0h1m1 0h1m2 0h2m3 0h1m1 0h2m1 0h1m4 0h3m3 0h2m2 0h1M1 39.5h7m3 0h1m4 0h1m1 0h4m1 0h1m1 0h1m3 0h1m2 0h1m4 0h1m1 0h1m1 0h1m1 0h3M1 40.5h1m5 0h1m5 0h1m4 0h1m1 0h2m3 0h1m1 0h1m1 0h5m2 0h2m3 0h1M1 41.5h1m1 0h3m1 0h1m1 0h1m3 0h3m2 0h2m1 0h5m2 0h2m2 0h2m1 0h7m2 0h2M1 42.5h1m1 0h3m1 0h1m2 0h3m1 0h1m2 0h1m2 0h3m3 0h1m3 0h1m1 0h2m2 0h3m1 0h3m1 0h2M1 43.5h1m1 0h3m1 0h1m1 0h1m1 0h1m1 0h3m1 0h3m1 0h1m1 0h2m1 0h1m2 0h1m1 0h2m4 0h1m5 0h1m1 0h1M1 44.5h1m5 0h1m3 0h1m2 0h9m2 0h1m2 0h1m3 0h2m2 0h4m4 0h1M1 45.5h7m1 0h1m4 0h2m2 0h2m1 0h2m2 0h4m2 0h7m1 0h2m3 0h2"/></svg></div>
<div class="totp-scan-info">
<p class="settings-section-hint">
Open Google Authenticator / 1Password / Authy / etc. and scan
this code. Then click <strong>Next</strong>.
</p>
<details class="totp-manual" open>
<summary>Can't scan? Type this manually</summary>
<code class="totp-secret-display">JBSW Y3DP EHPK 3PXP GS6F W2T2 EHPK</code>
<button type="button" class="icon-btn">Copy</button>
</details>
</div>
</div>
</div>

<div class="modal-foot">
<span class="spacer"></span>
<button type="button" class="btn btn-primary">Next →</button>
</div>
</div>
</div>
</body>
</html>
72 changes: 72 additions & 0 deletions docs/preview/settings-2fa-setup-recovery.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Passman — Save your recovery codes</title>
<link rel="stylesheet" href="../../packages/web/src/styles.css" />
</head>
<body>
<main class="auth-container settings-container">
<div class="auth-brand">
<div class="brand-mark brand-mark-lg"></div>
<span class="auth-brand-name">Passman</span>
</div>
<h1>Settings</h1>
<p class="auth-tagline">Zero-knowledge password manager</p>

<section class="settings-section" style="opacity: 0.4">
<header>
<h2>Two-factor authentication</h2>
<p class="settings-section-hint">
Adds a 6-digit code from your authenticator app to login.
</p>
</header>
</section>
</main>

<!-- ============================== Recovery codes modal ============================== -->
<div class="modal-backdrop" role="dialog" aria-modal="true">
<div class="modal modal-wide">
<div class="modal-head">
<div>
<h2>Save your recovery codes</h2>
<div class="target">One-time only — store these somewhere safe</div>
</div>
</div>

<div class="modal-body">
<div class="totp-recovery">
<p class="warning">
⚠️ <strong>Save these codes now.</strong> Each one logs you in
once if you lose access to your authenticator app. They are
shown <strong>only this time</strong> — Passman keeps only a
hash on the server, so this list cannot be recovered later.
</p>
<div class="totp-recovery-grid">
<code class="totp-recovery-code">a4n2-mz8q</code>
<code class="totp-recovery-code">k7vb-6xwd</code>
<code class="totp-recovery-code">tr5g-9hjf</code>
<code class="totp-recovery-code">e3p1-cdm7</code>
<code class="totp-recovery-code">y8s4-uwq2</code>
<code class="totp-recovery-code">b6kn-rt3v</code>
<code class="totp-recovery-code">x9z2-fl5p</code>
<code class="totp-recovery-code">m1qa-7gjy</code>
<code class="totp-recovery-code">w4rh-d8ke</code>
<code class="totp-recovery-code">o2vc-px6n</code>
</div>
<div class="totp-recovery-actions">
<button type="button" class="btn">Copy all</button>
<button type="button" class="btn">Download as .txt</button>
</div>
</div>
</div>

<div class="modal-foot">
<span class="spacer"></span>
<button type="button" class="btn btn-primary">I've saved them — finish</button>
</div>
</div>
</div>
</body>
</html>
Loading
Loading