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
49 changes: 49 additions & 0 deletions openspec/changes/crypto-item-reprompt/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Design: ask for the master password again before a sensitive item is shown or filled

## Context

At development `4c214a9d`:

- `src/crypto/reauth.js:117` `verifyMasterPassword(encryptedPrivateKey, masterPassword)` decrypts the stored private-key envelope with the entered password and returns true or false, never replacing the session key; its header says the control is advisory against a tampered client.
- `src/dialogs/ExportDialog.vue:130` uses it before a plaintext export.
- `src/components/SecretDetailSidebar.vue:205-208` reveals the value through `PasswordField` with `:resolve="resolveKey"` (`:1487`); copy buttons sit at `:187`, `:311`, `:367`, `:409`, `:529`; edit opens at `:1498`.
- `src/components/SecretListItem.vue:93` has a copy button on each list row.
- `browser-extension/src/lib/vault.js:49` `unlock()` derives the key with `decryptPrivateKey(suite.privateKey, masterPassword)`; `service-worker.js:113-125` `doFill()` decrypts and fills.
- Share copies are separate rows created by the sharing services; `lib/Service/ShareSyncService.php:317-345` syncs only the encrypted blobs.

## Goals / Non-Goals

**Goals**
- A person sitting at an unlocked, unattended session cannot show, copy or fill a flagged item without the master password.

**Non-Goals**
- A remember-for-a-while window. See D3.
- Server-side enforcement. The server cannot check a master password it never sees (ADR-003).
- Reprompting on passkey use from the extension's WebAuthn provider in this change; the extension passkey flow keeps its own user-verification step.

## Decisions

**D1. The flag is plain metadata on the holder's row.** `reprompt` boolean, default false. It says nothing about the value. A new share copy takes the owner's value; afterwards each holder controls their own row. Alternative: inside the encrypted additional fields. Rejected: the list and the extension must know an item is flagged before decrypting it.

**D2. One guard for every reveal path.** A composable `useReprompt(secret)` returns a function that resolves when the item is not flagged, or after `RepromptDialog.vue` got a password that `verifyMasterPassword()` accepts. `resolveKey`, every `CopyButton` on a flagged item, edit, clone, print and QR call it. A unit test enumerates the reveal paths so a new one cannot skip the guard unnoticed.

**D3. Every action asks.** No grace period, the same as Bitwarden's per-item re-prompt. A user who wants fewer prompts leaves the flag off. Alternative: a window of a few minutes. Rejected: it turns "ask before this item" into "ask once", which is what unlocking already does.

**D4. The extension checks inside the popup.** The popup asks for the master password and verifies it against the suite envelope the extension already fetched at unlock, then fills. Nothing new is fetched.

## Security and zero-knowledge

The master password is typed into the web app or the extension popup, checked locally against the encrypted private-key envelope and discarded. It never leaves the client, as ADR-003 requires. The flag is not sensitive and is stored in plain text. The dialog's help text states that this protects an unlocked screen, not a modified client.

## Risks / Trade-offs

- Frequent prompts on a flagged item used daily. The user chooses which items to flag.
- A missed reveal path leaves a hole. The enumeration test in D2 is the guard.

## Seed data

Keepiq owns its tables (keepiq ADR-001) and has no OpenRegister register. The dev fixture vault gets one flagged login, so the prompt appears in the e2e flows.

## Migration

One migration after `Version001000Date20260908000000`: `reprompt` (boolean, default false) on `keepiq_secrets`. `<version>` bumps.
50 changes: 50 additions & 0 deletions openspec/changes/crypto-item-reprompt/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
---
kind: code
---

# Ask for the master password again before a sensitive item is shown or filled

## Why

Once the vault is unlocked, every secret in it can be shown, copied and filled until the session times out or is locked. For most logins that is right. For a few (the domain admin account, the bank's payment login, the break-glass root key) a user wants a second look at who is at the keyboard, the way the plaintext export already asks: it verifies the master password again before writing a plain file (`src/dialogs/ExportDialog.vue:130`, `src/crypto/reauth.js:117` `verifyMasterPassword`). Nothing else uses that check. The reveal field (`src/components/SecretDetailSidebar.vue:205-208` `resolveKey`), the copy buttons (`:187`, `:311`, `:367`, `:409`, `:529`, `src/components/SecretListItem.vue:93`) and the extension's fill (`browser-extension/src/background/service-worker.js:113-125`) act at once.

### Matrix rows (keepiq `openspec/parity/capabilities.json`)

| row | capability | Keepiq today |
|---|---|---|
| `crypto-20` | Ask for the master password again before a sensitive item is shown or filled. | `no`: master password re-entry guards the plaintext export only |

### Demand

- Feature request, https://community.bitwarden.com/t/require-master-password-re-prompt-for-some-items/41

### Competitors rated yes

- Bitwarden: "src/Core/Vault/Entities/Cipher.cs:27 Reprompt; ... additional-options-section.component.ts:50 reprompt toggle; libs/vault/src/services/password-reprompt.service.ts:42 passwordRepromptCheck(); apps/browser/src/autofill/services/autofill.service.ts:53 openVaultItemPasswordRepromptPopout before fill Note: Per-item master password re-prompt, set in the item form and enforced before view, copy and autofill."
- Passbolt: "getPassphraseService.js:35 passphrase requested for every decrypt unless the user ticked remember; ... InputPassphrase.js:210 remember-me durations Note: By default the passphrase is asked before any secret is shown, copied or filled; users can opt to remember it for a set time. It is global, not a per-item flag."

## What Changes

- **A per-item switch.** The create and edit dialogs get "Ask for my master password before showing or filling this item". It is stored as a plain flag on the holder's row. A share copy starts with the owner's setting.
- **Enforced in the web app.** For a flagged item, revealing the value, copying the value or username, opening the edit dialog, cloning, printing and showing a QR code first ask for the master password and check it with `verifyMasterPassword()`. The list shows a lock marker on flagged items.
- **Enforced in the extension.** Filling a flagged item from the popup first asks for the master password inside the popup and checks it against the vault key.
- **No grace period.** Each action asks again. The check is client-side and says so, like the export check.

## Capabilities

### New Capabilities

- `item-master-password-reprompt`: a per-item flag that makes the web app and the browser extension verify the master password before the item's value is shown, copied or filled.

### Modified Capabilities

- None in delta form.

## Impact

- **Backend**: a boolean column `reprompt` on `keepiq_secrets`, accepted on create and update and copied onto new share copies.
- **Frontend**: a checkbox in `SecretCreateDialog.vue` and `SecretEditDialog.vue`, a shared `useReprompt()` guard wrapping `resolveKey`, `CopyButton` and the edit, clone, print and QR actions, a lock marker in `SecretListItem.vue`, a `RepromptDialog.vue` under `src/dialogs/`.
- **Browser extension**: a master password prompt in the popup before `doFill()` for flagged items.
- **Database**: one migration; `<version>` bump.
- **Security**: the master password never leaves the browser or extension; the flag is not sensitive. The control stops a person at an unlocked, unattended screen; it does not protect against a tampered client, which the dialog's help text says.
- **Cross-app**: none.
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
## ADDED Requirements

### Requirement: A per-item master password re-prompt

The system MUST let the holder of a secret switch on "Ask for my master password before showing or filling this item" in the create and edit dialogs, stored as the `reprompt` flag on the holder's row. A new share copy MUST start with the owner's value. The vault list MUST mark flagged items.

#### Scenario: A vault user flags a sensitive login

- **GIVEN** a vault user editing the login "Domain admin" in the edit dialog
- **WHEN** the user switches on the re-prompt option and saves
- **THEN** the login shows a lock marker in the vault list at /secrets

### Requirement: The web app verifies the master password before revealing a flagged item

For a flagged secret, the web app MUST verify the master password in the browser, against the user's encrypted private-key envelope, before it reveals the value, copies the value or username, opens the edit dialog, clones, prints or shows a QR code. Each such action MUST ask again. A wrong password MUST reveal nothing. The master password MUST NOT be sent to the server.

#### Scenario: A colleague at an unlocked screen

- **GIVEN** a vault user who left their unlocked vault open with the flagged login "Domain admin"
- **WHEN** someone clicks the reveal button on that login in the secret detail sidebar and enters a wrong master password
- **THEN** the value stays hidden
- **AND** no request carrying the entered password is made

#### Scenario: The owner reveals the value

- **GIVEN** the same flagged login
- **WHEN** the user clicks copy on the password and enters the right master password
- **THEN** the password is copied
- **AND** clicking copy again asks for the master password again

### Requirement: The extension verifies the master password before filling a flagged item

The browser extension MUST ask for the master password in its popup and verify it against the vault key envelope before filling a flagged item. A wrong or missing password MUST fill nothing.

#### Scenario: Filling a flagged login from the extension

- **GIVEN** an unlocked extension on the sign-in page of the flagged login's site
- **WHEN** the user picks the login in the popup
- **THEN** the popup asks for the master password
- **AND** the login is filled only after the right password is entered
24 changes: 24 additions & 0 deletions openspec/changes/crypto-item-reprompt/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Tasks: ask for the master password again before a sensitive item is shown or filled

## 1. Backend

- [ ] 1.1 Add the `reprompt` column and entity field, accept it on create and update, return it in `jsonSerialize()`, and copy it onto new share copies; bump `<version>`. Verify: PHPUnit for create, update and a new share copy.

## 2. Web app

- [ ] 2.1 Add `RepromptDialog.vue` and the `useReprompt()` guard on top of `verifyMasterPassword()`. Verify: vitest for accept, reject and cancel.
- [ ] 2.2 Guard `resolveKey`, every copy button, edit, clone, print and QR for flagged items, and add the checkbox to the create and edit dialogs and a lock marker to `SecretListItem.vue`. Verify: a vitest that enumerates the reveal paths of `SecretDetailSidebar.vue` and `SecretListItem.vue`, and a Playwright flow reveal a flagged item with a wrong and then the right master password.

## 3. Browser extension

- [ ] 3.1 Ask for and verify the master password in the popup before `doFill()` of a flagged item. Verify: extension unit test that a flagged fill without the password fills nothing.

## 4. Docs

- [ ] 4.1 Document the flag and what it does and does not protect against. Verify: docs build.

## Acceptance criteria

- A flagged item cannot be shown, copied, edited, cloned, printed, shown as a QR code or filled without entering the master password again.
- Each of those actions asks again; there is no window.
- The master password is never sent to the server.
50 changes: 50 additions & 0 deletions openspec/changes/crypto-vault-encryption-details/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Design: show which algorithms and key sizes protect the vault

## Context

At development `4c214a9d`:

- `src/crypto/rsa.js:2-8` RSA-OAEP-SHA256, `RSA_KEY_BITS = 4096`, chunking above 446 bytes; `:20-21` key generation with `modulusLength`; `:64`, `:187` import with SHA-256.
- `src/crypto/aes.js:2` AES-256-GCM with PBKDF2-SHA256; `:15` `PBKDF2_ITERATIONS = 600000`; `:55` `encryptPrivateKey()`, `:79` `decryptPrivateKey()`.
- `src/crypto/argon2.js:19-34` `ARGON2_MEMORY_KIB = 65536`, `ARGON2_ITERATIONS = 3`, `ARGON2_PARALLELISM = 1`, `SALT_LENGTH = 16`; used by the encrypted backup (`src/export/backup.js`), link shares and ephemeral sends.
- Attachments: AES-GCM file keys wrapped with RSA (`src/store/modules/attachment.js:115`).
- `src/App.vue:166-186` Encryption section: status, created, suite id.
- `lib/Service/CertificateLifecycleService.php:180-187` parsed certificate metadata: subject, issuer, serial, SHA-256 fingerprint, notBefore, notAfter; `lib/Controller/CertificateController.php:95` `inventory()`.
- `src/views/CertificateInventoryView.vue:116-137` vault certificate table: owner, subject, expires.
- `docs/ARCHITECTURE.md:65-117` describes the model in prose.

## Goals / Non-Goals

**Goals**
- Anyone with a vault can see what protects it, in words they understand and names an auditor recognises.
- The screen cannot say something the code does not do.

**Non-Goals**
- Changing any algorithm or parameter.
- A per-secret view. Every secret of a suite uses the same scheme.

## Decisions

**D1. Read the parameters from the code, not from copy.** Each crypto module exports its parameters (`RSA_PARAMETERS`, `MASTER_KEY_PARAMETERS`, `ARGON2_PARAMETERS`) and `src/crypto/parameters.js` assembles `CRYPTO_PARAMETERS`. The component renders from that object. A vitest asserts the exported values are the ones the functions use, so changing a constant changes the screen.

**D2. Read the certificate facts from the certificate.** The server already parses the vault certificate for the inventory. It adds `keyType` and `keyBits` from `openssl_pkey_get_details()` on the certificate's public key and `signatureAlgorithm` from the parsed certificate. The browser does not re-derive them.

**D3. Plain words first, technical names second.** Each line reads like "Your passwords are encrypted with your own 4096-bit RSA key (RSA-OAEP, SHA-256)", following the hydra writing rules, with the technical name for auditors in the same line.

**D4. The docs page is generated.** The docs build imports `src/crypto/parameters.js` and renders the same table, so the public page and the app agree.

## Security and zero-knowledge

Everything shown is public: algorithm names, parameter values, and the certificate's public fields. No private key, envelope, salt or secret is read or displayed. Publishing parameters does not weaken them.

## Risks / Trade-offs

- A reader may compare numbers across vendors without context (4096-bit RSA against 2048-bit, 600,000 against 1,000,000 iterations). The docs page explains what each number protects.

## Seed data

Keepiq owns its tables (keepiq ADR-001) and has no OpenRegister register. No fixture is needed; every dev vault has a suite.

## Migration

None.
51 changes: 51 additions & 0 deletions openspec/changes/crypto-vault-encryption-details/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
---
kind: code
---

# Show which algorithms and key sizes protect the vault

## Why

Keepiq's encryption is stated in code constants and in the architecture document, not on any screen. The secret values are RSA-OAEP with SHA-256 under 4096-bit keys (`src/crypto/rsa.js:8`, `:20-21`, `:64`), the private key is wrapped with AES-256-GCM under a key derived from the master password with PBKDF2-SHA256 at 600,000 iterations (`src/crypto/aes.js:2`, `:15`), and backups and link shares use Argon2id with 64 MiB, 3 iterations and parallelism 1 (`src/crypto/argon2.js:19-25`). The user settings Encryption section shows only the suite's status, creation date and id (`src/App.vue:166-186`), and the certificates page shows the vault certificate's owner, subject and expiry (`src/views/CertificateInventoryView.vue:116-137`). A security officer who has to fill in a supplier questionnaire, or a user who wants to know what "zero-knowledge" means here, has to read source code.

### Matrix rows (keepiq `openspec/parity/capabilities.json`)

| row | capability | Keepiq today |
|---|---|---|
| `crypto-13` | See which algorithms and key sizes protect your vault. | `no`: no screen names the algorithms or key sizes; the certificates page shows subject and expiry only |

### Demand

No demand row. Three competitors rate it yes.

### Competitors rated yes

- 1Password: AES256-GCM, RSA-OAEP 2048, PBKDF2-HMAC-SHA256 (https://1passwordstatic.com/files/security/1password-white-paper.pdf).
- Passbolt: "src/react-extension/components/UserSetting/DisplayUserGpgInformation/DisplayUserGpgInformation.js:111 algorithm type, :272 algorithm cell (plus length, fingerprint, created, expires); passbolt/[email protected] config/routes.php:119 GET /gpgkeys Note: The keys inspector shows key algorithm, length, fingerprint and expiry."
- Keeper: AES-256 record keys, PBKDF2 1,000,000 iterations, ECC secp256r1, RSA-2048 documented (https://docs.keeper.io/enterprise-guide/keeper-encryption-model).

## What Changes

- **An encryption overview in user settings.** The Encryption section lists, in plain words with the technical names next to them: how secret values are encrypted, how the private key is protected by the master password, how attachments are encrypted, how backups, link shares and sends are protected, and the vault certificate's key size, signature algorithm, fingerprint, issuer and validity.
- **One source of truth.** The web app exports the parameters from the crypto modules and the overview reads them from there, so the screen cannot drift from the code. The certificate facts are read from the certificate itself by the server.
- **The certificates page shows key size and algorithm** for the vault certificate next to its subject and expiry.
- **A documentation page** repeats the overview for readers without an account, generated from the same constants in the docs build.

## Capabilities

### New Capabilities

- `vault-encryption-details`: a user-facing account of the algorithms, key sizes and key derivation settings that protect the vault, taken from the code and the certificate.

### Modified Capabilities

- None in delta form. `certificate-lifecycle` keeps its inventory requirement; the added certificate fields are this change's own requirement.

## Impact

- **Backend**: `CertificateLifecycleService` adds `keyType`, `keyBits` and `signatureAlgorithm` to the parsed certificate metadata of the inventory.
- **Frontend**: a `CRYPTO_PARAMETERS` export assembled from `src/crypto/rsa.js`, `aes.js` and `argon2.js`; a `VaultEncryptionDetails.vue` in the Encryption section of `App.vue`; two columns on `CertificateInventoryView.vue`.
- **Docs**: a generated section in `docs/ARCHITECTURE.md` or a new page under `docs/`.
- **Database**: none.
- **Security**: only public parameters and public certificate fields are shown; no key material.
- **Cross-app**: none.
Loading
Loading