05/18 Update CLAUDE.md and README.md
This commit is contained in:
@@ -48,8 +48,10 @@ passkeeper/
|
||||
│ │ ├── vault_item.py # enc_data, iv, enc_name, iv_name
|
||||
│ │ ├── folder.py
|
||||
│ │ ├── token_blacklist.py # JWT revocation (jti + expires_at)
|
||||
│ │ ├── shared_item.py # ECDH-encrypted cross-user shares
|
||||
│ │ ├── totp_used_code.py # TOTP replay prevention (one-time use per user)
|
||||
│ │ ├── shared_item.py # ECDH-encrypted cross-user shares; enc_name/iv_name
|
||||
│ │ ├── emergency_access.py # State machine
|
||||
│ │ ├── recovery_challenge.py # Server-side recovery challenge (multi-worker safe)
|
||||
│ │ └── audit_log.py
|
||||
│ ├── routes/
|
||||
│ │ ├── auth.py # Register, login, MFA, logout, refresh, change-password, recovery
|
||||
@@ -58,7 +60,8 @@ passkeeper/
|
||||
│ │ ├── sharing.py
|
||||
│ │ └── emergency.py
|
||||
│ ├── services/
|
||||
│ │ └── auth_service.py # Argon2id, JWT, blacklist, @require_jwt, TOTP encrypt/decrypt
|
||||
│ │ └── auth_service.py # Argon2id, JWT, blacklist, @require_jwt, TOTP encrypt/decrypt,
|
||||
│ │ # TOTP replay helpers (is_totp_code_used / mark_totp_code_used)
|
||||
│ ├── static/
|
||||
│ │ ├── css/app.css # Full responsive stylesheet; collapsible group styles; tag badge styles
|
||||
│ │ └── js/
|
||||
@@ -66,13 +69,13 @@ passkeeper/
|
||||
│ │ │ # encryptName, decryptName, generateSalt
|
||||
│ │ ├── auth.js # Login/register + TOTP MFA + VaultSession
|
||||
│ │ ├── recover.js
|
||||
│ │ ├── sharing.js # ECDH P-256
|
||||
│ │ ├── sharing.js # ECDH P-256; encryptName/decryptName for shared item names
|
||||
│ │ └── vault.js # Full vault UI — see "Key Features" below
|
||||
│ └── templates/vault/
|
||||
│ └── index.html # Tags field, Auto-Lock setting, Import/Export view + sidebar
|
||||
├── extension/
|
||||
│ ├── manifest.json # MV3 (Chrome/Edge); Ctrl+Shift+L keyboard shortcut
|
||||
│ ├── manifest.firefox.json # MV2 (Firefox)
|
||||
│ ├── manifest.json # MV3 (Chrome/Edge); web_accessible_resources: []
|
||||
│ ├── manifest.firefox.json # MV2 (Firefox); web_accessible_resources: []
|
||||
│ ├── background.js # Chrome SW: badges, pending-save "!" badge, CLEAR_SAVE_BADGE
|
||||
│ ├── background.firefox.js # Firefox: in-memory session shim, setTimeout idle lock
|
||||
│ ├── shared/
|
||||
@@ -94,7 +97,11 @@ passkeeper/
|
||||
│ ├── 71d7158dd3b9_add_audit_log_tables_fix_sharing_public_.py
|
||||
│ ├── a1b2c3d4e5f6_encrypt_totp_secret_at_rest.py
|
||||
│ ├── b2c3d4e5f6a7_add_account_recovery_columns.py
|
||||
│ └── c3d4e5f6a7b8_encrypt_vault_item_name.py # adds enc_name + iv_name
|
||||
│ ├── c3d4e5f6a7b8_encrypt_vault_item_name.py
|
||||
│ ├── d4e5f6a7b8c9_add_lockout_and_mfa_backup_codes.py
|
||||
│ ├── e5f6a7b8c9d0_add_recovery_challenges_table.py
|
||||
│ ├── f6a7b8c9d0e1_add_totp_used_codes_table.py # TOTP replay prevention
|
||||
│ └── g7h8i9j0k1l2_encrypt_shared_item_name.py # enc_name/iv_name on shared_items
|
||||
├── scripts/
|
||||
│ ├── reencrypt_totp_secrets.py
|
||||
│ ├── backup_db.sh / backup.cron / passkeeper-logrotate
|
||||
@@ -120,6 +127,7 @@ CREATE TABLE users (
|
||||
totp_secret VARCHAR(255), -- AES-256-GCM ciphertext
|
||||
totp_iv VARCHAR(64),
|
||||
totp_enabled TINYINT(1) DEFAULT 0,
|
||||
mfa_backup_codes TEXT, -- JSON array of Argon2id-hashed codes
|
||||
sharing_public_key VARCHAR(128),
|
||||
sharing_private_key_enc TEXT,
|
||||
sharing_private_key_iv VARCHAR(64),
|
||||
@@ -143,19 +151,61 @@ CREATE TABLE vault_items (
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (folder_id) REFERENCES folders(id) ON DELETE SET NULL
|
||||
);
|
||||
-- (folders, token_blacklist, shared_items, emergency_access, audit_logs — unchanged)
|
||||
|
||||
-- Shared Items
|
||||
CREATE TABLE shared_items (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
item_id INT UNSIGNED NOT NULL,
|
||||
owner_id INT UNSIGNED NOT NULL,
|
||||
recipient_email VARCHAR(255) NOT NULL,
|
||||
recipient_id INT UNSIGNED,
|
||||
item_name VARCHAR(255) NOT NULL, -- non-sensitive fallback label (= item_type)
|
||||
item_type VARCHAR(20) NOT NULL DEFAULT 'password',
|
||||
enc_data TEXT NOT NULL, -- ECDH-encrypted item payload
|
||||
iv VARCHAR(64) NOT NULL,
|
||||
enc_name VARCHAR(512), -- ECDH-encrypted display name (nullable for legacy)
|
||||
iv_name VARCHAR(64),
|
||||
accepted TINYINT(1) DEFAULT 0,
|
||||
created_at DATETIME NOT NULL
|
||||
);
|
||||
|
||||
-- TOTP Used Codes (replay prevention)
|
||||
CREATE TABLE totp_used_codes (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT UNSIGNED NOT NULL,
|
||||
code VARCHAR(6) NOT NULL,
|
||||
expires_at DATETIME NOT NULL,
|
||||
UNIQUE KEY uq_totp_used_user_code (user_id, code),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Recovery Challenges (multi-worker safe)
|
||||
CREATE TABLE recovery_challenges (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT UNSIGNED NOT NULL UNIQUE,
|
||||
nonce VARCHAR(64) NOT NULL,
|
||||
expected_proof VARCHAR(64) NOT NULL,
|
||||
expires_at DATETIME NOT NULL,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- (folders, token_blacklist, emergency_access, audit_logs — standard schemas)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Migration History
|
||||
|
||||
| Revision | Description |
|
||||
| -------------- | ------------------------------------------- |
|
||||
| `71d7158dd3b9` | Add audit_logs; fix sharing_public_key type |
|
||||
| `a1b2c3d4e5f6` | Encrypt TOTP secret at rest |
|
||||
| `b2c3d4e5f6a7` | Add account recovery columns |
|
||||
| `c3d4e5f6a7b8` | Add enc_name + iv_name to vault_items |
|
||||
| Revision | Description |
|
||||
| ---------------- | ------------------------------------------------ |
|
||||
| `71d7158dd3b9` | Add audit_logs; fix sharing_public_key type |
|
||||
| `a1b2c3d4e5f6` | Encrypt TOTP secret at rest |
|
||||
| `b2c3d4e5f6a7` | Add account recovery columns |
|
||||
| `c3d4e5f6a7b8` | Add enc_name + iv_name to vault_items |
|
||||
| `d4e5f6a7b8c9` | Add lockout columns + MFA backup codes |
|
||||
| `e5f6a7b8c9d0` | Add recovery_challenges table |
|
||||
| `f6a7b8c9d0e1` | Add totp_used_codes table (TOTP replay prevent.) |
|
||||
| `g7h8i9j0k1l2` | Add enc_name/iv_name to shared_items |
|
||||
|
||||
---
|
||||
|
||||
@@ -165,16 +215,20 @@ CREATE TABLE vault_items (
|
||||
- `authHash = PBKDF2(masterPassword, email, 100k iter)` → auth only
|
||||
- `vaultKey = PBKDF2(masterPassword, enc_key_salt, 600k iter)` → browser memory only
|
||||
- All vault data (payload + name + tags) encrypted client-side (AES-256-GCM)
|
||||
- **Item name:** `enc_name`/`iv_name`; server `name` column = item type only
|
||||
- **Item name:** `enc_name`/`iv_name` in `vault_items`; server `name` column = item type only
|
||||
- **Shared item name:** `enc_name`/`iv_name` encrypted with ECDH shared key; server `item_name` = item type only
|
||||
- **Tags:** `plain.tags: string[]` inside `enc_data`; server never sees them
|
||||
- **Argon2id:** double-hashes `authHash` server-side
|
||||
- **Argon2id:** double-hashes `authHash` server-side; transparently rehashes on login if parameters are upgraded
|
||||
- **JWT:** HS256, 15 min access / 7 day refresh, JTI blacklisted on logout
|
||||
- **MFA:** TOTP secret AES-256-GCM encrypted at rest
|
||||
- **Sharing:** ECDH P-256 zero-knowledge re-encryption
|
||||
- **MFA:** TOTP secret AES-256-GCM encrypted at rest; each code is single-use (replay prevented via `totp_used_codes` table, 120s TTL)
|
||||
- **Sharing:** ECDH P-256 zero-knowledge re-encryption; item name also encrypted with shared key
|
||||
- **Password generator:** fully CSPRNG (`_cryptoRandInt` rejection-sampling)
|
||||
- **Decrypted vault data:** `chrome.storage.session` only — never to disk
|
||||
- **Clipboard auto-clear:** 30 s after any password/username copy (web + extension)
|
||||
- **Breach detection:** HIBP k-anonymity — only 5-char SHA-1 prefix transmitted
|
||||
- **Account recovery:** challenge-response via HMAC-SHA256; `enc_key_salt` NOT returned by `/recovery/data` — client must derive it by decrypting the recovery blob (proves possession of recovery code without transmitting it); challenge rotated on each `/recovery/items` call to prevent proof replay
|
||||
- **folder_id ownership:** validated server-side on all create/update/import operations — user cannot assign items to another user's folder
|
||||
- **Audit logs:** never contain plaintext item names, shared item names, or vault data
|
||||
|
||||
---
|
||||
|
||||
@@ -198,12 +252,41 @@ Requires Chrome 111+. `content.js` `onChanged` listener watches `area === 'sessi
|
||||
|
||||
## Key Implementation Details
|
||||
|
||||
### TOTP replay prevention
|
||||
|
||||
Every accepted TOTP code is recorded in `totp_used_codes` (user_id + code, 120s TTL). A second attempt with the same code within that window returns `400 Verification code already used`. Applies to: `mfa_enable`, `mfa_disable`, `mfa_verify`, `mfa_backup_codes_regenerate`. The table is pruned by the APScheduler cleanup job alongside `token_blacklist` and `recovery_challenges`.
|
||||
|
||||
### Argon2 transparent rehash
|
||||
|
||||
`verify_auth_token(auth_hash, stored_hash, user=user)` calls `ph.check_needs_rehash()` on success. If the stored hash uses outdated parameters (e.g. after raising `ARGON2_TIME_COST`), the hash is silently upgraded in the same DB commit as the login success. Pass `user=user` at all login callsites.
|
||||
|
||||
### folder_id ownership validation
|
||||
|
||||
`_validate_folder_id(folder_id, user_id)` in `vault.py` queries `Folder` by both `id` and `user_id`. Returns sanitised int or `None`. Raises `ValueError` on mismatch. Applied in `create_item` (400 on invalid), `update_item` (400 on invalid), `import_items` (silently clears to `None` — item still imports to root).
|
||||
|
||||
### Account recovery flow
|
||||
|
||||
```
|
||||
1. GET /recovery/data → server creates challenge; returns nonce + recovery blob
|
||||
enc_key_salt NOT returned (client must decrypt blob)
|
||||
2. Client decrypts blob with recovery code → gets enc_key_salt
|
||||
3. Client computes proof = HMAC-SHA256(enc_key_salt, nonce)
|
||||
4. GET /recovery/items → validates proof; consumes challenge; re-issues fresh challenge
|
||||
with same expected_proof + new nonce (prevents proof replay)
|
||||
5. POST /recover → validates proof again; consumes rotated challenge; atomically
|
||||
re-encrypts vault + resets password
|
||||
```
|
||||
|
||||
### Shared item name encryption
|
||||
|
||||
When creating a share, the client encrypts `item.name` with `SharingCrypto.encryptName(sharedKey, name)` → `enc_name`/`iv_name`. The server receives `item_name = item.item_type` (non-sensitive type label) for the `NOT NULL` column. On the recipient's inbox, `enc_name`/`iv_name` are stored in data attributes on the View button and decrypted client-side with `SharingCrypto.decryptName()` when the user clicks View. Legacy shares (pre-migration, no `enc_name`) fall back to showing `item_name` (the type string).
|
||||
|
||||
### Vault item tags
|
||||
|
||||
- Stored as `plain.tags: string[]` inside `enc_data`. No schema change ever needed.
|
||||
- **Web app:** `_parseTags(str)` → comma-split, lowercase, dedup, sort. `_allTags()` collects across all items. `renderTagList()` builds sidebar. Tags field in modal has live badge preview.
|
||||
- **Extension:** `.pk-tag` badges below item subtitle. Favorites tab = items where `tags.includes('favorite')`. Favorited items show gold ★ in site label.
|
||||
- To favourite an item: add tag `favorite` in the web app edit modal.
|
||||
- `handleSearch()` pool correctly branches on `itemType` / `folder` / `tag` filter types.
|
||||
|
||||
### Collapsible folder groups
|
||||
|
||||
@@ -215,8 +298,13 @@ Requires Chrome 111+. `content.js` `onChanged` listener watches `area === 'sessi
|
||||
- **Encrypted JSON export:** `GET /api/vault` → versioned envelope → download. Zero-knowledge.
|
||||
- **CSV export:** decrypt client-side → `name, url, username, password, notes`.
|
||||
- **JSON import:** sends encrypted blobs to `POST /api/vault/import` directly.
|
||||
- **CSV import:** parses Chrome / Bitwarden / 1Password formats; encrypts client-side before POSTing.
|
||||
- **CSV import:** RFC 4180-compliant parser (handles `""` embedded quotes); parses Chrome / Bitwarden / 1Password formats; encrypts client-side before POSTing.
|
||||
- Both endpoints write audit logs: `vault_item.export`, `vault_item.import`.
|
||||
- Import view resets state (file input, preview, result) every time the view is entered.
|
||||
|
||||
### `apiFetch` error handling
|
||||
|
||||
`apiFetch` throws on **all** non-ok responses including 404. Thrown error carries `e.status` for callers that need to branch. Callers should use `try/catch` rather than checking `res.ok` after the call.
|
||||
|
||||
### Web-app session timeout
|
||||
|
||||
@@ -262,6 +350,10 @@ Requires Chrome 111+. `content.js` `onChanged` listener watches `area === 'sessi
|
||||
|
||||
Load in Firefox: `about:debugging` → This Firefox → Load Temporary Add-on → `manifest.firefox.json`.
|
||||
|
||||
### `web_accessible_resources`
|
||||
|
||||
Both manifests declare `web_accessible_resources: []` (MV3) / `[]` (MV2). This prevents any external page from loading extension resources via `chrome-extension://` or `moz-extension://` URLs, blocking extension fingerprinting.
|
||||
|
||||
### `_normaliseUrl()` — bare-domain matching
|
||||
|
||||
```js
|
||||
@@ -300,40 +392,47 @@ Inspects added/removed nodes — if all carry `__pk` prefix, returns early. Prev
|
||||
```python
|
||||
db.session.add(item)
|
||||
db.session.flush() # populates item.id
|
||||
AuditLog.log(user_id=..., action='vault_item.create', resource_id=item.id, ...)
|
||||
AuditLog.log(user_id=..., action='vault_item.create', resource_id=item.id,
|
||||
detail=f'Created {item_type} item (id={item.id})') # NO plaintext name
|
||||
db.session.commit() # atomic
|
||||
```
|
||||
|
||||
For deletes: capture `id` and `name` before flush.
|
||||
Audit log details **never** contain plaintext item names, shared item names, or any decrypted vault data. Use `item_type` and `id` only.
|
||||
|
||||
### Common gotchas
|
||||
|
||||
- `item_type`: use `db.String(20)`, not `db.Enum(ItemType)` — enum lazy-load breaks `.value`
|
||||
- `INTEGER(unsigned=True)`: requires `from sqlalchemy.dialects.mysql import INTEGER`
|
||||
- All datetimes: naive UTC `datetime.utcnow()` — never mix with timezone-aware
|
||||
- All datetimes: naive UTC `datetime.now(timezone.utc).replace(tzinfo=None)` — never mix with timezone-aware
|
||||
- Extension vault key: `extractable: true` (web app uses `false`)
|
||||
- PyJWT `sub`: `str(user_id)` on encode, `int(payload['sub'])` on decode
|
||||
- TOTP key generation: `python -c "import secrets; print(secrets.token_hex(32))"`
|
||||
- `#vault-list` ID must not be renamed — `vault.js` renders into it directly
|
||||
- `_validate_folder_id` must be called for any user-supplied `folder_id` before DB write
|
||||
- `verify_auth_token` must receive `user=user` at login to enable Argon2 rehash
|
||||
- APScheduler cleanup job handles `TokenBlacklist`, `RecoveryChallenge`, AND `TotpUsedCode`
|
||||
|
||||
---
|
||||
|
||||
## Audit Action Catalog
|
||||
|
||||
| Module | Action | Trigger |
|
||||
| -------------- | ------------------------------------------------------ | ------------------------ |
|
||||
| `auth.py` | `auth.register` | New account |
|
||||
| `auth.py` | `auth.login` / `auth.login_failed` | Login success/fail |
|
||||
| `auth.py` | `auth.mfa_enable/disable/verify` | TOTP actions |
|
||||
| `auth.py` | `auth.change_password` / `auth.change_password_failed` | Password change |
|
||||
| `auth.py` | `auth.delete_account` / `auth.delete_account_failed` | Deletion |
|
||||
| `auth.py` | `auth.recovery_setup/failed/items_denied/success` | Recovery |
|
||||
| `vault.py` | `vault_item.create/update/delete` | CRUD |
|
||||
| `vault.py` | `vault_item.export` / `vault_item.import` | Import/Export |
|
||||
| `folders.py` | `folder.create/update/delete` | Folder CRUD |
|
||||
| `sharing.py` | `sharing_keys.create/update` | ECDH key setup |
|
||||
| `sharing.py` | `shared_item.create/delete/accept` | Sharing |
|
||||
| `emergency.py` | `emergency_access.*` | All EA state transitions |
|
||||
| Module | Action | Trigger |
|
||||
| -------------- | ------------------------------------------------------ | ------------------------------ |
|
||||
| `auth.py` | `auth.register` | New account |
|
||||
| `auth.py` | `auth.login` / `auth.login_failed` | Login success/fail |
|
||||
| `auth.py` | `auth.account_locked` | Failed login lockout |
|
||||
| `auth.py` | `auth.mfa_enable/disable/verify` | TOTP actions |
|
||||
| `auth.py` | `auth.mfa_backup_code_used` | Backup code login |
|
||||
| `auth.py` | `auth.mfa_backup_codes_regenerated` | Backup code regen |
|
||||
| `auth.py` | `auth.change_password` / `auth.change_password_failed` | Password change |
|
||||
| `auth.py` | `auth.delete_account` / `auth.delete_account_failed` | Deletion |
|
||||
| `auth.py` | `auth.recovery_setup/failed/items_denied/success` | Recovery |
|
||||
| `vault.py` | `vault_item.create/update/delete` | CRUD (detail: type + id only) |
|
||||
| `vault.py` | `vault_item.export` / `vault_item.import` | Import/Export |
|
||||
| `folders.py` | `folder.create/update/delete` | Folder CRUD |
|
||||
| `sharing.py` | `sharing_keys.create/update` | ECDH key setup |
|
||||
| `sharing.py` | `shared_item.create/delete/accept` | Sharing (detail: type + id) |
|
||||
| `emergency.py` | `emergency_access.*` | All EA state transitions |
|
||||
|
||||
---
|
||||
|
||||
@@ -342,11 +441,11 @@ For deletes: capture `id` and `name` before flush.
|
||||
```
|
||||
POST /api/auth/register|login|logout|refresh|recover
|
||||
GET /api/auth/me|mfa/status|mfa/setup|recovery/status|recovery/data|recovery/items
|
||||
POST /api/auth/mfa/enable|disable|verify|change-password|recovery/setup
|
||||
POST /api/auth/mfa/enable|disable|verify|backup-codes/regenerate|change-password|recovery/setup
|
||||
DELETE /api/auth/account
|
||||
|
||||
GET /api/vault # list all encrypted items
|
||||
POST /api/vault # { name, item_type, folder_id, enc_data, iv, enc_name?, iv_name? }
|
||||
POST /api/vault # { name, item_type, folder_id?, enc_data, iv, enc_name?, iv_name? }
|
||||
GET /api/vault/<id>
|
||||
PUT /api/vault/<id>
|
||||
DELETE /api/vault/<id>
|
||||
@@ -358,9 +457,9 @@ PUT|DELETE /api/folders/<id>
|
||||
|
||||
GET|POST /api/sharing/keys
|
||||
GET /api/sharing/public-key
|
||||
GET|POST /api/sharing
|
||||
DELETE /api/sharing/<id>
|
||||
GET /api/sharing/inbox
|
||||
GET|POST /api/sharing # POST: { item_id, recipient_email, enc_data, iv,
|
||||
DELETE /api/sharing/<id> # item_name (=item_type), item_type,
|
||||
GET /api/sharing/inbox # enc_name, iv_name }
|
||||
POST /api/sharing/inbox/<id>/accept
|
||||
|
||||
GET|POST /api/emergency
|
||||
@@ -404,7 +503,7 @@ redis>=5.0 # Rate-limit storage (required in production)
|
||||
|
||||
## Notes
|
||||
|
||||
- Never log decrypted vault data server-side
|
||||
- Never log decrypted vault data server-side — audit details use `item_type` + `id` only
|
||||
- Tags live in `enc_data` as `plain.tags: string[]` — no schema change ever needed
|
||||
- `vault_items_cs` is in `chrome.storage.session` — decrypted data never written to disk
|
||||
- HIBP checks run progressively — synchronous sections render first, then parallel async checks
|
||||
@@ -480,4 +579,4 @@ browser extension — not from PassKeeper. These can be ignored.
|
||||
The `[PassKeeper] decorateFields: host=…, matched=0 of 0 items` log from
|
||||
`content.js` is a routine debug message (not an error): the content script found
|
||||
no stored credentials matching the current hostname, which is expected on the vault
|
||||
page itself.
|
||||
page itself.
|
||||
Reference in New Issue
Block a user