diff --git a/.claude/launch.json b/.claude/launch.json new file mode 100644 index 0000000..69e7566 --- /dev/null +++ b/.claude/launch.json @@ -0,0 +1,11 @@ +{ + "version": "0.0.1", + "configurations": [ + { + "name": "passman-previews", + "runtimeExecutable": "node", + "runtimeArgs": ["docs/preview/server.mjs"], + "port": 4173 + } + ] +} diff --git a/README.md b/README.md index 763f9be..1a5d3fa 100644 --- a/README.md +++ b/README.md @@ -30,8 +30,9 @@ ciphertext + KDF parameters. A database breach leaks nothing usable. | `.github/workflows/` | CI: lint, typecheck, unit + E2E tests, CodeQL, audits. | | `docs/` | Architecture and security docs. | -Read the full design in [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) and the -threat model in [`docs/SECURITY.md`](docs/SECURITY.md). +Read the full design in [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md), the +threat model in [`docs/SECURITY.md`](docs/SECURITY.md), and a screenshot +walkthrough of every screen in [`docs/USER_GUIDE.md`](docs/USER_GUIDE.md). ## Installation diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md new file mode 100644 index 0000000..b066182 --- /dev/null +++ b/docs/USER_GUIDE.md @@ -0,0 +1,235 @@ +# Passman — User guide + +This guide walks through every screen of the Passman web vault and the +companion Chrome extension, from creating a vault to autofilling a saved +credential. + +> **Zero-knowledge reminder.** The screens below all run client-side. The +> server only ever sees ciphertext + your KDF parameters. Your master +> password and decryption key never leave the browser. + +The screenshots are reproducible from `docs/preview/*.html` — see +[Reproducing the screenshots](#reproducing-the-screenshots) at the end. + +--- + +## 1. Create your vault + +Open `http://localhost:5173` and click **Create a vault** on the login +page. The registration screen looks like this: + +![Register screen](img/register.png) + +Fields: + +- **Email** — used for login + future recovery hints; never stored + alongside your plaintext password. +- **Master password** — minimum 12 characters. **Write it down.** Passman + has no recovery channel; if you forget it, the vault is permanently + unreadable. +- **Confirm password** — typo guard. + +When you submit, the browser: + +1. Generates a random 16-byte KDF salt and a random 256-bit symmetric + vault key. +2. Derives a master key from your password via Argon2id. +3. Encrypts the symmetric key with the master key (AES-256-GCM). +4. Sends only the ciphertext + salt + KDF parameters to the server. + +You're then redirected to the login screen. + +--- + +## 2. Unlock your vault + +![Login screen](img/login.png) + +Enter the same email and master password. The browser: + +1. Asks the server for your KDF parameters (the server returns plausible + decoy parameters for unknown emails, so attackers can't enumerate). +2. Re-derives the master key locally. +3. Derives a one-way **auth key** from the master key + password and + sends only that to the server. The server has no way to reverse it + into your password. +4. On a successful login, decrypts the symmetric key locally. + +If decryption fails (wrong password) you'll see *"Vault decryption +failed."*; if the server rejects the auth key you'll see *"Invalid email +or password."* + +--- + +## 3. The vault — main grid + +After unlocking you land on the vault grid, optimised for +infrastructure credentials (DBs, hosts, services): + +![Vault — default grid view](img/vault.png) + +Each row shows: + +| Column | What it holds | +| ------------- | ------------------------------------------------------ | +| **Name** | Friendly label (e.g. `prod-pg-primary`). | +| **Hostname** | DNS name or short alias (e.g. `db-prod-01`). | +| **IP address**| IPv4 / IPv6 address of the host. | +| **User** | Account name (e.g. `postgres`, `sys`). | +| **Password** | Masked by default. **Show / Hide** + **Copy** buttons. | +| **Port** | Service port (e.g. `5432`, `1521`). | +| **Actions** | Per-row **Delete**. | + +All values are decrypted in-browser after sign-in; the server never sees +plaintext. + +### 3a. Search + +Type into the search box at the top of the page. The grid filters in +real time across **name, hostname, IP, user, port, URL and notes** — +case-insensitive substring match, performed locally on already-decrypted +items. Nothing about your query reaches the server. + +![Vault — filtered by search query "prod"](img/vault-search.png) + +### 3b. Group by + +Use the **Group by** dropdown to bucket rows under a section heading. +Available keys: `Hostname`, `IP address`, `User`, `Port`. Items missing +the chosen field land in `(unspecified)`. + +![Vault — grouped by IP address](img/vault-grouped.png) + +### 3c. Add a new credential + +Click **+ Add login** to open the inline form below the grid. + +![Vault — add credential form](img/vault-add.png) + +- **Name** and **Password** are required. Everything else is optional — + empty fields are stripped before encryption so the ciphertext stays + small. +- **Port** accepts `0–65535`. It's stored as a number, so grouping and + numeric search both work. +- **URL** is free-form; keep it for browser autofill (e.g. + `https://app.example.com/login`) or as a connection string for DB + tooling. + +Click **Save**. The form encrypts the JSON payload with your symmetric +key, sends only the ciphertext to `POST /api/vault/items`, and the new +row appears in the grid after a refresh. + +### 3d. Reveal & copy a password + +In the **Password** column: + +- **Show** flips the cell from `••••••••` to the cleartext value + (in-page only — toggling **Hide** masks it again). +- **Copy** writes the password to the system clipboard. Most browsers + clear the clipboard automatically after a short period. + +### 3e. Delete a row + +The red **Delete** button on the right asks for confirmation, then +issues `DELETE /api/vault/items/`. The server has no way to +recover deleted ciphertext. + +### 3f. Lock the vault + +Top-right **Lock & sign out** revokes your refresh token server-side +and clears the in-memory symmetric key. You'll have to re-enter the +master password to come back in. + +--- + +## 4. Chrome extension + +> ⚠️ The extension's runtime code typechecks cleanly, but a loadable +> Chrome bundle requires a Vite multi-entry config that is tracked for a +> follow-up. The screenshots below show the popup as it will appear once +> the extension is loadable. The web vault remains the primary client +> for now. + +### 4a. Locked popup + +Click the Passman toolbar icon when the vault is locked (or freshly +installed): + +![Extension popup — locked](img/extension-locked.png) + +Enter the same email + master password you use on the web vault. The +extension's service worker performs the same client-side derivation + +unlock, then keeps the decrypted symmetric key in memory only. + +### 4b. Unlocked popup + +Once unlocked the popup confirms the active session and offers a single +**Lock vault** button: + +![Extension popup — unlocked](img/extension-unlocked.png) + +While unlocked, the content script can offer **exact-origin autofill** +on saved logins — it will only suggest a credential when the page's +origin matches the URL stored on the item. + +Hitting **Lock vault** wipes the in-memory key; the next popup open +returns to the locked screen. + +--- + +## 5. Tips + +- **Master password length matters more than complexity.** Argon2id is + tuned to take ~250 ms even on a fast laptop; a 16-character random + passphrase is a far stronger choice than a clever 8-character one. +- **One vault per machine works fine.** Sessions are bound to refresh + tokens, not browsers — sign in from as many devices as you like. +- **Treat the URL field as searchable.** It's included in the search + index so you can type a substring of `postgres://…` to find a DB + credential by connection string. +- **DB-style entries.** For database credentials, fill **Hostname**, + **IP**, **User**, **Password**, and **Port**; that's the minimum + needed to reconstruct a `psql` / `mysql` / `sqlplus` invocation + directly from the row. + +--- + +## Reproducing the screenshots + +Every image in this guide is generated from a self-contained HTML +mockup under `docs/preview/`. Each mockup loads the real +`packages/web/src/styles.css`, so the rendering matches the live app. + +1. Start the bundled static server: + + ```bash + node docs/preview/server.mjs + # previews http://127.0.0.1:4173/ + ``` + +2. Browse `http://127.0.0.1:4173/docs/preview/` for an index of all + pages. + +3. Re-capture the PNGs with Chrome headless: + + ```bash + CHROME="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" + for page in register login vault vault-search vault-grouped vault-add; do + "$CHROME" --headless --disable-gpu --hide-scrollbars \ + --window-size=1280,900 \ + --screenshot="docs/img/${page}.png" \ + "http://127.0.0.1:4173/docs/preview/${page}.html" + done + for page in extension-locked extension-unlocked; do + "$CHROME" --headless --disable-gpu --hide-scrollbars \ + --window-size=400,360 \ + --screenshot="docs/img/${page}.png" \ + "http://127.0.0.1:4173/docs/preview/${page}.html" + done + ``` + + On Linux replace `$CHROME` with `chromium --headless=new` (or + `google-chrome`). + +If the live UI changes, refresh the mockups in `docs/preview/` and +re-run the loop. diff --git a/docs/img/extension-locked.png b/docs/img/extension-locked.png new file mode 100644 index 0000000..12cf912 Binary files /dev/null and b/docs/img/extension-locked.png differ diff --git a/docs/img/extension-unlocked.png b/docs/img/extension-unlocked.png new file mode 100644 index 0000000..b1c1e88 Binary files /dev/null and b/docs/img/extension-unlocked.png differ diff --git a/docs/img/login.png b/docs/img/login.png new file mode 100644 index 0000000..6f97fda Binary files /dev/null and b/docs/img/login.png differ diff --git a/docs/img/register.png b/docs/img/register.png new file mode 100644 index 0000000..fd076d9 Binary files /dev/null and b/docs/img/register.png differ diff --git a/docs/img/vault-add.png b/docs/img/vault-add.png new file mode 100644 index 0000000..8318334 Binary files /dev/null and b/docs/img/vault-add.png differ diff --git a/docs/img/vault-grouped.png b/docs/img/vault-grouped.png new file mode 100644 index 0000000..4bd8d60 Binary files /dev/null and b/docs/img/vault-grouped.png differ diff --git a/docs/img/vault-search.png b/docs/img/vault-search.png new file mode 100644 index 0000000..d4a5dea Binary files /dev/null and b/docs/img/vault-search.png differ diff --git a/docs/img/vault.png b/docs/img/vault.png new file mode 100644 index 0000000..496b229 Binary files /dev/null and b/docs/img/vault.png differ diff --git a/docs/preview/extension-locked.html b/docs/preview/extension-locked.html new file mode 100644 index 0000000..fe3c0f2 --- /dev/null +++ b/docs/preview/extension-locked.html @@ -0,0 +1,40 @@ + + + + + Passman — Extension popup (locked) + + + +
+ +
+ + diff --git a/docs/preview/extension-unlocked.html b/docs/preview/extension-unlocked.html new file mode 100644 index 0000000..be33c93 --- /dev/null +++ b/docs/preview/extension-unlocked.html @@ -0,0 +1,34 @@ + + + + + Passman — Extension popup (unlocked) + + + +
+ +
+ + diff --git a/docs/preview/index.html b/docs/preview/index.html new file mode 100644 index 0000000..f1d72fa --- /dev/null +++ b/docs/preview/index.html @@ -0,0 +1,27 @@ + + + + + Passman previews + + + + +
+

Passman screenshots

+ +
+ + diff --git a/docs/preview/login.html b/docs/preview/login.html new file mode 100644 index 0000000..8f7b012 --- /dev/null +++ b/docs/preview/login.html @@ -0,0 +1,28 @@ + + + + + + Passman — Unlock + + + +
+

Unlock your vault

+
+ + + +
+

+ New here? Create a vault +

+
+ + diff --git a/docs/preview/register.html b/docs/preview/register.html new file mode 100644 index 0000000..e28baa4 --- /dev/null +++ b/docs/preview/register.html @@ -0,0 +1,33 @@ + + + + + + Passman — Register + + + +
+

Create your vault

+

+ ⚠️ Your master password is the only key. We can't recover it. If you + forget it, your vault is permanently inaccessible. +

+
+ + + + +
+
+ + diff --git a/docs/preview/server.mjs b/docs/preview/server.mjs new file mode 100644 index 0000000..aeb5274 --- /dev/null +++ b/docs/preview/server.mjs @@ -0,0 +1,45 @@ +// Tiny static-file server for screenshotting Passman page mockups. +// Serves the worktree root so relative links into packages/web/src/styles.css resolve. +import http from "node:http"; +import { readFile } from "node:fs/promises"; +import { extname, join, normalize, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = fileURLToPath(new URL(".", import.meta.url)); +const ROOT = resolve(__dirname, "..", ".."); +const PORT = Number(process.env.PORT ?? 4173); + +const MIME = { + ".html": "text/html; charset=utf-8", + ".css": "text/css; charset=utf-8", + ".js": "text/javascript; charset=utf-8", + ".mjs": "text/javascript; charset=utf-8", + ".json": "application/json; charset=utf-8", + ".svg": "image/svg+xml", + ".png": "image/png", + ".jpg": "image/jpeg", +}; + +const server = http.createServer(async (req, res) => { + try { + let url = decodeURIComponent((req.url ?? "/").split("?")[0]); + if (url === "/") url = "/docs/preview/index.html"; + const filePath = normalize(join(ROOT, url)); + if (!filePath.startsWith(ROOT)) { + res.writeHead(403); + res.end("forbidden"); + return; + } + const data = await readFile(filePath); + const type = MIME[extname(filePath)] ?? "application/octet-stream"; + res.writeHead(200, { "Content-Type": type, "Cache-Control": "no-store" }); + res.end(data); + } catch (e) { + res.writeHead(404, { "Content-Type": "text/plain" }); + res.end(`not found: ${req.url}\n${e instanceof Error ? e.message : ""}`); + } +}); + +server.listen(PORT, "127.0.0.1", () => { + console.log(`previews http://127.0.0.1:${PORT}/`); +}); diff --git a/docs/preview/vault-add.html b/docs/preview/vault-add.html new file mode 100644 index 0000000..39f5359 --- /dev/null +++ b/docs/preview/vault-add.html @@ -0,0 +1,98 @@ + + + + + + Passman — Vault (add credential) + + + +
+
+

Vault

+ +
+ +
+ + +
+ +
+
+
+ Name + Hostname + IP address + User + Password + Port + +
+
+ prod-pg-primary + db-prod-01 + 10.0.0.42 + postgres + + •••••••• + + + + 5432 + + + +
+
+
+ +
+

New credential

+ +
+ + + +
+ + + +
+ + +
+
+
+ + diff --git a/docs/preview/vault-grouped.html b/docs/preview/vault-grouped.html new file mode 100644 index 0000000..0846bc3 --- /dev/null +++ b/docs/preview/vault-grouped.html @@ -0,0 +1,152 @@ + + + + + + Passman — Vault (grouped by IP) + + + +
+
+

Vault

+ +
+ +
+ + + +
+ +
+

10.0.0.42 (1)

+
+
+ Name + Hostname + IP address + User + Password + Port + +
+
+ prod-pg-primary + db-prod-01 + 10.0.0.42 + postgres + + •••••••• + + + + 5432 + + + +
+
+
+ +
+

10.0.0.43 (1)

+
+
+ Name + Hostname + IP address + User + Password + Port + +
+
+ prod-pg-replica + db-prod-02 + 10.0.0.43 + replicator + + •••••••• + + + + 5432 + + + +
+
+
+ +
+

10.0.1.10 (1)

+
+
+ Name + Hostname + IP address + User + Password + Port + +
+
+ oracle-erp + erp-db-01 + 10.0.1.10 + sys + + •••••••• + + + + 1521 + + + +
+
+
+ +
+

10.0.2.21 (1)

+
+
+ Name + Hostname + IP address + User + Password + Port + +
+
+ mysql-reporting + rpt-db-01 + 10.0.2.21 + analytics + + •••••••• + + + + 3306 + + + +
+
+
+
+ + diff --git a/docs/preview/vault-search.html b/docs/preview/vault-search.html new file mode 100644 index 0000000..3aa6ffc --- /dev/null +++ b/docs/preview/vault-search.html @@ -0,0 +1,78 @@ + + + + + + Passman — Vault (search) + + + +
+
+

Vault

+ +
+ +
+ + + +
+ +
+
+
+ Name + Hostname + IP address + User + Password + Port + +
+ +
+ prod-pg-primary + db-prod-01 + 10.0.0.42 + postgres + + •••••••• + + + + 5432 + + + +
+ +
+ prod-pg-replica + db-prod-02 + 10.0.0.43 + replicator + + •••••••• + + + + 5432 + + + +
+
+
+
+ + diff --git a/docs/preview/vault.html b/docs/preview/vault.html new file mode 100644 index 0000000..1dc1828 --- /dev/null +++ b/docs/preview/vault.html @@ -0,0 +1,142 @@ + + + + + + Passman — Vault + + + +
+
+

Vault

+ +
+ +
+ + + +
+ +
+
+
+ Name + Hostname + IP address + User + Password + Port + +
+ +
+ prod-pg-primary + db-prod-01 + 10.0.0.42 + postgres + + •••••••• + + + + 5432 + + + +
+ +
+ prod-pg-replica + db-prod-02 + 10.0.0.43 + replicator + + •••••••• + + + + 5432 + + + +
+ +
+ oracle-erp + erp-db-01 + 10.0.1.10 + sys + + •••••••• + + + + 1521 + + + +
+ +
+ mysql-reporting + rpt-db-01 + 10.0.2.21 + analytics + + P@ssM4nDemo! + + + + 3306 + + + +
+ +
+ redis-cache + cache-01 + 10.0.3.5 + default + + •••••••• + + + + 6379 + + + +
+ +
+ mongo-shard-a + mongo-01 + 10.0.4.11 + admin + + •••••••• + + + + 27017 + + + +
+
+
+
+ +