Release: merge development into beta - #691
Open
github-actions[bot] wants to merge 128 commits into
Open
github-actions[bot] wants to merge 128 commits into
github-actions[bot] wants to merge 128 commits into
Conversation
OpenSpec change harden-vault-key-material-guards: proposal, design, tasks, and spec deltas for a verified master-password proof (VaultKeyProof) gating the irreversible key-material operations, plus a migration abort route. Reproduced end-to-end against development; findings 1 and 2 documented in the proposal. Spec only — implementation lands in a separate commit. Refs #673 Assisted-by: ClaudeCode:claude-opus-5
…674) OpenSpec change migrate-emergency-access-on-rotation: proposal, design, tasks, and spec deltas. A compromise-recovery rotation re-envelopes each reachable emergency contact under the new key (buildRecoveryEnvelope with the new private key + the grantee's current certificate) and invalidates only the residual, correcting the spec's claim that the owner cannot re-wrap it alone. Spec only — implementation lands separately. Refs #674 Assisted-by: ClaudeCode:claude-opus-5
The "any completed rotation silently costs emergency access" open question is resolved by #674 (migrate-emergency-access-on-rotation), which re-envelopes reachable contacts under the new key. The lost-password route's destructive mechanics (the revocation warning and the refuse-while-a-usable-contact-exists gate) are likewise folded into #674; this note records where each piece now lives. Refs #673 Assisted-by: ClaudeCode:claude-opus-5
…674 Extends this change to carry #395's lost-password-route safeguard: revoking a user suite still clears its emergency envelopes, but must now warn plainly (secrets gone, emergency access deleted, accessor must retrieve first while the suite is active), refuse while a usable emergency contact exists unless an explicit override is given, and surface the count of usable contacts (never identities). Belongs here because the guard in #673 makes revocation the only forgotten-password route, and the clearing is emergency-access lifecycle on a suite key-state transition — the surface this change owns. Adds spec scenarios (refuse-without-override, proceed-with-override), a design decision D5, a tasks section 4b, and proposal/impact notes. Refs #674 Assisted-by: ClaudeCode:claude-opus-5
…673) Correcting the abort spec discovered during implementation: revoking the unused successor suite would run EncryptionSuiteRevokedListener, which for a user suite sweeps the owner's incoming ShareTargets and promotes their delegations — destroying real state over a migration the abort exists to undo. The successor is brand-new and empty, so it is deleted outright. Refs #673 Assisted-by: ClaudeCode:claude-opus-5
The abort route the compromiseRecovery refusal already promises but that did not exist — the remedy named in the error message. It is the non-destructive terminal: completion carries the vault forward to the new suite and marks the old one compromised; abort carries it back to the old suite, which stays active and readable. Abort is permitted only while no record has been committed to the new suite (MigrationWorkService::countCommitted). Once a record has moved, both outcomes lose data, so the migration stays in_progress and the caller is pointed at resuming — a 409 carrying the committed count. This restriction is also exactly what makes abort safe against the session-only lockout: producing a valid re-encrypted record needs the master password, so a hostile session that never held it can never have committed one and can always be aborted away. On success: status -> aborted, the successor suite is deleted (not revoked, which would cascade the user-suite lost-identity teardown), failure accounting is cleared, the write lock is released, and SuiteMigrationAbortedEvent fires — NOT SuiteMigrationCompletedEvent, so the terminal cascade (compromise-flagging, link-share revocation, emergency-access invalidation) never runs. Its one listener unlocks the SecretRequests locked at start, keeping them on the old suite. Frontend: an "Abort and keep my old key" control on the resume banner plus the abortMigration store action. Backend + store fully unit-tested (abort restores/deletes; refused-after-commit with count; idempotent; aborted-not- completed event); phpmd clean; prettier clean. Refs #673 Assisted-by: ClaudeCode:claude-opus-5
The core of the fix for the session-only lockout (#395). A destructive operation on vault key material now requires a VaultKeyProof: a signature, made with the caller's suite private key, over a server-issued challenge bound to the operation's own parameters. The private key is obtainable only by decrypting its envelope with the master password, so a verified proof is a server-verifiable proof of the master password — a stolen session, a leaked app password, or XSS in an unlocked tab no longer suffices, because the session key is non-extractable and decrypt-only and so cannot sign. - `#[VaultKeyProofRequired(binds, subject, purpose)]` declares the guard on a method; the binding lives on the attribute because the middleware cannot read the request body (the framework decodes JSON and drops the raw bytes), so the proof commits to NAMED parameters, hashed individually in order. - `VaultKeyProofMiddleware` enforces it: reads the attribute by reflection, resolves the subject suite, collects the bound params, delegates to the service, and maps a failure to 403 `key_proof_required`. It consults no auth backend and honours no token scope, so it is not waived for SSO/app-password sessions — its authority is key material, not the login method. - `VaultKeyProofService` issues a STATELESS, expiring, HMAC-authenticated nonce (no ICacheFactory — a null cache on a default install would break the flow) and verifies an RSASSA-PKCS1-v1_5 SHA-256 signature. Replay is a non-issue because the signature commits to the operation's parameters. - Challenge endpoint `GET /api/v1/suites/{id}/proof-challenge` (ungated). - Guard applied to compromiseRecovery, updatePrivateKey, complete, and the emergency-contact destroy. `VaultKeyProofAttributesTest` enumerates them and fails the build if one drops the attribute (a declarative guard fails open by omission), with a documented exclusion list (challenge, abort). Service crypto and middleware dispatch fully unit-tested (valid verifies; wrong key / altered value / tampered nonce / expired / wrong purpose / wrong user / missing all refused; attribute dispatch, subject resolution, foreign suite, 403 mapping). phpmd clean. The client half (proveMasterPassword + wiring the four flows) lands next — until then the guarded routes 403 by design. Refs #673 Assisted-by: ClaudeCode:claude-opus-5
…#673) The client half of the guard. `proveMasterPassword` (reauth.js) decrypts the suite envelope with the freshly entered master password, re-imports the PKCS#8 bytes for SIGNING (RSASSA-PKCS1-v1_5 SHA-256 — a distinct capability from the session key, which is non-extractable and decrypt-only and so cannot sign), signs the challenge bound to the operation's parameters, and discards every derived key. `keyProof.js` fetches a challenge and returns the two proof headers, so the four flows do not each re-implement it. Wired the flows that already hold the master password, so they keep working against the now-guarded routes: - compromise-recovery START — proof over the OLD key (old password) bound to the new key material; - migration COMPLETE on the initiate path — proof over the NEW key (new password) bound to the migration id; - routine password change (updatePrivateKey) — proof over the current key (old password) bound to the new envelope; the old key is already materialised there, so no extra prompt. Tested: proveMasterPassword round-trips under RSASSA-PKCS1-v1_5 (verifies over the exact server-rebuilt message; wrong password throws before signing; a changed bound value fails verification), and the session key is pinned non-extractable / decrypt-only. Store tests still green. prettier + eslint clean (0 errors). REMAINING (tracked in tasks §4.7-4.8): the emergency-contact delete and the resume-path completion both need a master-password prompt at the point of action (no password in hand there), plus the 403 re-enter-and-retry UX. Until those land, those two paths return 403 by design. Refs #673 Assisted-by: ClaudeCode:claude-opus-5
… §4.7-4.8) Finishes the client wiring so all four guarded flows work end to end. Emergency-contact delete (§4.7): `emergencyAccess.revoke(id, masterPassword)` builds a proof (subject active, bound to the contact id) and the delete carries it. `EmergencyAccessView` gained a master-password confirm dialog — deleting a contact destroys its recovery envelope, so it must prove the master password, which is why a session alone can no longer do it. Completion (§4.8): completion's proof is now over the OLD (retiring) key rather than the new one, via a new middleware subject `migrationOldSuite` that resolves the migration's old suite. Both suites are active at completion so 'active' was ambiguous, and — the point — the old key is the one BOTH the initiate and resume paths already hold the password for, so a resumed run finalises with no extra prompt. The "Finish anyway" acknowledgement path builds the proof from the retained (or re-entered) old password, and the form re-shows the password field on a `key_proof_required` refusal — the re-enter-and-retry UX. Coverage and middleware tests updated for the new subject; the middleware gains SuiteMigrationMapper to resolve the old suite. Backend + frontend tests green (79 PHP incl. the new migrationOldSuite resolution test; 22 frontend incl. proveMasterPassword). phpmd clean; prettier + eslint 0 errors. Refs #673 Assisted-by: ClaudeCode:claude-opus-5
… §7) §6.3: VaultKeyProofCrossImplTest verifies a signature produced by the browser's scheme (WebCrypto RSASSA-PKCS1-v1_5 SHA-256, the one proveMasterPassword uses) with PHP openssl_verify over VaultKeyProofService::signedMessage — proving the two implementations agree on both the signature scheme and the message construction, the one interop risk a same-language test cannot catch. A tampered bound value breaks it. Fixture at tests/fixtures/vault-key-proof.json, regenerated by generate-vault-key-proof-fixture.mjs. §7: documented the guard in docs/ARCHITECTURE.md §4.2 — the guarded-route table, the attribute contract, the load-bearing design points (sign-not- decrypt; stateless nonce; not waived for any session type; complete proves the old key; abort deliberately unguarded), and the rule that a new destructive route MUST be added to VaultKeyProofAttributesTest. Confirmed gate-110 does not apply (no migration, info.xml version unchanged). Change now at 42/47. Remaining: 6.5/6.6 (a full request-pipeline / live without-proof assertion — belongs with the §7.6 live reproduction and a Newman e2e), and the human submission steps (§7.1 CI gates, §7.5 PR disclosure, §7.6 independent verification). Refs #673 Assisted-by: ClaudeCode:claude-opus-5
Adds a component test for the master-password gate on emergency-contact revocation: clicking Revoke opens the confirmation without calling the store; confirming passes the entered password through to store.revoke (which builds the proof); a key_proof_required refusal is surfaced and the dialog stays open to retry; a successful revoke closes it. There was no prior EmergencyAccessView test, so this is a new file rather than an extension. Refs #673 Assisted-by: ClaudeCode:claude-opus-5
CI quality checks flagged three things the local per-file runs missed: - phpmd: `VaultKeyProofMiddleware::afterException` has unused `$controller`/ `$methodName` (mandated by the Middleware override) — suppressed with the same annotation MigrationController uses. Adding VaultKeyProofService pushed `EncryptionSuiteController` to coupling 13 — suppressed with justification, as two sibling controllers already do. - phpcs: `VaultKeyProofService` called its own `b64url()`/`mac()` with positional args (the codebase requires named params for internal calls), and the `EncryptionSuiteController` constructor docblock was missing the `$proofService` @PARAM. Full `lib/` is back to 0 errors. - test:l10n / l10n-parity: the 8 new UI strings (abort control, emergency revoke dialog, re-auth field) were added to `l10n/en.json` and seeded into all 36 required locales. Non-English values are English placeholders pending Transifex, consistent with how new source strings enter the pipeline. Refs #673 Assisted-by: ClaudeCode:claude-opus-5
…stener (#673) The coverage-baseline guard failed because new code in MODIFIED files was untested, dropping their coverage against the merge base: - EncryptionSuiteController::proofChallenge had no test — added three (issue on a valid purpose; 400 on an unknown purpose; 404 on a foreign suite); - MigrationWorkService::countCommitted was only ever mocked (in MigrationServiceTest), so its body was uncovered — added a direct test summing the new-suite rows across the three stores, plus the zero case; - SuiteMigrationAbortedListener (a new file) gained a test: it unlocks the SecretRequests keeping the old suite, and ignores other events. Refs #673 Assisted-by: ClaudeCode:claude-opus-5
The l10n/*.js browser catalogues are compiled from l10n/*.json, so the 8 new UI strings left them stale (check:l10n-js failed). Ran `npm run l10n:build` to regenerate all 37; the diff is purely additive and prettier-clean. Refs #673 Assisted-by: ClaudeCode:claude-opus-5
…ds (#673) Three mechanical gates were red on the guard-hardening change: - gate-46 (spec-anchor-existence): the abort @SPEC anchors pointed at openspec/specs/encryption-suites, but that requirement lives in the not-yet-archived change delta. Repoint the six abort anchors to openspec/changes/harden-vault-key-material-guards/specs/... so they resolve. - gate-16 (spec-coverage): add the missing @SPEC tags on VaultKeyProofService::issueChallenge/verify/signedMessage and on EmergencyAccessView's cancelRevoke. - gate-13 (modal-isolation): the revoke-confirmation NcDialog was written inline in EmergencyAccessView. Extract it to src/dialogs/EmergencyRevokeDialog.vue per ADR-004. The guard state (target id, busy flag, refusal message) stays with the view; the dialog is presentational and passes the entered master password back through its confirm event. Assisted-by: ClaudeCode:claude-opus-5
…ke dialog (#673) vue/attributes-order requires the two-way binding to precede plain prop bindings; the extracted EmergencyRevokeDialog had :open first, failing the lint-check and Vue Quality (eslint) CI jobs. Reorder only — no behaviour change. Assisted-by: ClaudeCode:claude-opus-5
Assisted-by: ClaudeCode:claude-opus-5
…on (#674) Backend of migrate-emergency-access-on-rotation, tasks 1.1–1.6 and 4.1–4.2. A compromise-recovery rotation used to invalidate every emergency-access recovery envelope. But the owner holds the new private key mid-rotation and can fetch the grantee's certificate, so the envelope can be MIGRATED, not destroyed: the browser mints a fresh envelope escrowing the new key and posts it here. - New endpoint POST /api/v1/migrations/{id}/emergency-contacts/{contactId} (MigrationController::reEnvelopeEmergencyContact), owner- and old-suite-scoped through the same requireOwnMigration guard as the other migration writes, and deliberately NOT routed through commitRecord: emergency contacts are outside the completion gate (design D2), so a contact the browser cannot carry is left on the old suite for the sweep, never recorded as a gate-blocking failure. - EmergencyEnvelopeInvalidationService::reEnvelopeForRotation re-points the contact to the new suite, keeps it `granted`, clears any invalidated reason, and audits a (re-)grant. The grantor cannot open the envelope, so it is shape-checked (parses, v/alg, non-empty ciphertext fields) and the declared grantee suite is asserted to be the grantee's CURRENT active suite — an envelope sealed to a stale grantee key would be unopenable. - invalidateForGrantorRotation is now documented at its call site as a residual SWEEP: it finds only the contacts the loop could not carry (unreachable grantee), because migrated ones no longer sit on the old suite. Tests: the re-point service (re-point + granted + reason-cleared + audit; foreign grantor / wrong suite / missing contact / malformed envelope / suite mismatch / grantee without an active suite; residual sweep touches only old-suite rows) and the controller endpoint (success, 400 missing params, 409 terminated, 403/404/400 exception mapping). Existing MigrationController / EmergencyAccessService tests updated for the new constructor dependency. Assisted-by: ClaudeCode:claude-opus-5
…#674) Backend of the destructive-revocation safeguard, tasks 4b.1–4b.2. Revoking a user suite deletes its emergency-access recovery envelopes outright (the revocation listener runs clearForGrantorRevocation), and revocation is the last-resort route for an owner who lost their master password — exactly the owner most likely to still need their emergency contact. Today that deletion is silent. - EncryptionSuiteController::revoke gains an acceptEmergencyLoss flag. While a usable (non-invalidated) emergency contact exists and the flag is not set, revocation is refused with 409 and the COUNT of usable contacts — never their identities, which stay grantor-private. The guard sits before revokeSuite, because the envelope clear happens asynchronously in the revocation listener downstream of the event that call dispatches; gating any later would be too late. - EmergencyEnvelopeInvalidationService::countUsableForGrantorSuite counts the non-invalidated contacts bound to the suite. Tests: refusal returns the count and never reaches the service nor discloses identities; the override proceeds and clears; the no-contact case revokes unchanged; the count excludes invalidated contacts. Existing EncryptionSuite- Controller tests updated for the new constructor dependency. Assisted-by: ClaudeCode:claude-opus-5
…oke (#674) Frontend of migrate-emergency-access-on-rotation, tasks 2.x / 3.x / 4.3 / 4.5 / 4b.3, plus the residual-surfacing and revoke-safeguard UI. - initiateCompromiseRecovery now re-envelopes emergency contacts BEFORE completion (migrateEmergencyContacts): for each non-invalidated contact the browser fetches the grantee's current certificate, builds a fresh envelope escrowing the new private key, and posts it to the re-point endpoint. A grantee with no reachable certificate or a transient failure is collected as residual, never fatal — emergency contacts are outside the completion gate. The new private key PEM only ever leaves as envelope ciphertext (ADR-003). Resume cannot re-envelope (it holds only a non-extractable session key), so it keeps the pre-change invalidate-and-prompt fallback, as the design accepts. - CompromiseRecoveryForm surfaces the residual: it names exactly the contacts that could not be carried and prompts re-establishment, and shows nothing when every contact migrated. - The suite-revoke UI (App.vue) now carries the destructive-revocation safeguard: revokeSuite sends acceptEmergencyLoss; on the server's 409 emergency_access_present refusal the UI shows the count and the retrieve-first warning, and the confirm button escalates to an explicit "Revoke and delete emergency access". - Eight new UI strings seeded into en.json and all 36 locales (English placeholders); l10n/*.js catalogues rebuilt. ARCHITECTURE.md documents emergency contacts as a migrated store and invalidateForGrantorRotation as a residual sweep. Tests: the re-envelope loop (reachable → post + no residual; unreachable → residual + no post; invalidated skipped; per-contact failure isolated; index failure safe), the residual prompt, and the revoke store contract (flag carried, 409 refusal propagated without evicting the cache). Assisted-by: ClaudeCode:claude-opus-5
Backend, frontend, surfacing, revoke safeguard, l10n, gates and docs are done and verified. 4.4 (cross-impl sanity) is substantially covered by the service test's JS-shaped envelope; 4.6 (two rotations) composes by construction — both noted as optional follow-ups. 5.5 (PR description) is pending the human-opened PR. Assisted-by: ClaudeCode:claude-opus-5
…he test edits Two CI failures on the PR against development: - Frontend Check (format): the two test files I added cases to were not prettier-formatted (I checked the new files but not these edits). Reformatted; logic unchanged. - Hydra Gates gate-16: inserting cancelRevoke before handleRevoke pushed handleRevoke's docblock above cancelRevoke, so handleRevoke lost the docblock directly above it and gate-16 read it as missing @SPEC. Reordered so each method carries its own docblock. The remaining two gate-16 findings (compromiseRecovery, updatePrivateKey) are #673's guard-attributed methods: they enter scope only when the PR is diffed against development (not against feature/673), and a gate blind spot on the closing `)]` of a multi-line #[VaultKeyProofRequired(...)] attribute then misses their docblock. They pass when the PR is based on feature/673; the clean fix is to merge #673 into development first. Assisted-by: ClaudeCode:claude-opus-5
The 0.3.2 release bumped the version on main. Without this, development stays behind main and the next development -> main promotion conflicts on the version file. Version files resolve to development's side, which is the higher line, so this never moves a version backwards.
…60912202807 chore(release): 0.3.4-unstable.20260912202807
chore(release): sync main back into development
…260913184350 chore(sync): carry beta back into development
Wilco's Strict-review blocker #2: suites/{id}/revoke was the one destructive route outside the guard — session-only, hard-deletes ShareTargets, promotes delegations, blocks every secret read, and reinstate is admin-only, so a stolen cookie could inflict the exact #395 lockout this change exists to close. - New purpose VaultKeyProofService::PURPOSE_REVOKE_SUITE + PROOF_PURPOSE.REVOKE_SUITE. - EncryptionSuiteController::revoke gains #[VaultKeyProofRequired(binds: ['reason'], subject: 'routeParam:id', purpose: PURPOSE_REVOKE_SUITE)] — the verifying key is resolved from the suite being revoked, so a re-aimed id breaks the signature, and the reason is bound so a captured proof can't be replayed against another request. Added to VaultKeyProofAttributesTest so a future drop of the attribute fails CI. - Frontend: revokeSuite(reason, masterPassword) signs the proof and attaches the headers; the revoke confirmation now asks for the master password (which signs and is never sent). A stolen session, lacking the master password, can no longer revoke. An owner who has LOST the password uses the separate admin recovery path (to be designed in its own PR), never this one. Both @SPEC tags kept on the touched methods (retrofit + the new vault-key-proof requirement): a deleted @SPEC would trip gate-16's whole-file re-evaluation, which mis-reads the multi-line #[VaultKeyProofRequired] attribute on the other guarded methods (the checker bug noted on #678) — kept additive to avoid it. Assisted-by: ClaudeCode:claude-opus-5
feat(parity): capability matrix for keepiq, 191 rows against six competitors
fix(parity): apply round 3 cross-lane corrections
fix(parity): drop the stale #184 gap from the sharing-11 note
…gnal rows, sources per system (work in progress)
…ined rows (work in progress)
…s settled (work in progress)
…he killed readers (work in progress)
… with changelog origins (work in progress)
…e read (work in progress)
… from its proposals (work in progress)
feat(parity): source reads of Bitwarden, Passbolt, Vault and Nextcloud Passwords, sources and 38 demand rows
… first use) (#763) nextcloud-vue 2.57.1 imports Dexie lazily in openDb(), so keepiq-main.js no longer embeds Dexie; it now lives only in a separate lazy chunk. The lockfile also moves postcss 8.5.26 to 8.5.28 (a transitive refresh).
…ly (#765) @conduction/nextcloud-vue 2.57.1 imports Dexie on first use of the offline database, so this app's main bundle no longer carries it. The exact pin and the Dependabot ignore existed to keep every app on one Dexie version because every page evaluated it; the pin goes back to a caret range and Dependabot may bump dexie here again.
…11 changes (#767) Gap decisions for all 91 keepiq rows (build 49 in 31 changes, defer 24, decided no 18), the matrix edits for them, and the first 11 OpenSpec changes. Specs only.
… changes (#769) Ten admin and apps OpenSpec changes for 14 keepiq gap rows, with those rows specified in the matrix. Specs only.
…, crypto and sharing changes (#781) Ten audit, clients, crypto and sharing OpenSpec changes for 14 keepiq gap rows, with those rows specified in the matrix and the clients-01 note corrected. Specs only.
… cache (#798) The repo carried two Docusaurus sites. Only docs/ is built: the shared documentation workflow defaults to source-folder "docs", so docusaurus/ was template scaffolding from 2026-03-30 that never deployed. Its CNAME pointed at keepiq.app, which has no DNS record. docs/.docusaurus/ (26 files) was Docusaurus' build cache, committed with absolute paths from a developer machine. It is now untracked and ignored. README, CONTRIBUTING, .prettierignore and the code-quality comment no longer describe a second tree. CONTRIBUTING's release steps now match documentation.yml (development branch, Cloudflare Worker).
Round 3 of the Strict review on #712. The two guards I added to Application::register() in round 2 (the gate on the prelude's return value and recordFailure() in the Bootstrap catch) had no unit seam, and both survived mutation. That made the PR's "every guard turns a test red" claim wrong again. Between them sat a third silent path: an enabled OpenRegister with no loadable AppHost\Bootstrap. Examples are an OpenRegister older than AppHost, or a partial deploy. The nested `if` just ended, nothing was recorded, and /api/health and /api/metrics answered 500 with nothing in the log. OpenRegisterAutoloader::bootstrapAppHost(callable $bootstrap) is now the whole of the wiring. It runs register() and does nothing when that refuses. It checks inside its try that Bootstrap is loadable, so a missing class is recorded, and a ParseError from a truncated Bootstrap.php no longer escapes from class_exists() to abort every registrar below it. It then runs the closure and records any throw. Application makes one call and passes a closure that calls Bootstrap::register(). The test-only parameters (IAppManager, class name) reach every branch. psalm needed the class_exists() narrowing in Application to accept the Bootstrap call, so a declaration-only AppHost\Bootstrap stub is added for psalm and phpstan. It is never loaded at runtime or by the tests. Also from round 3: - a disable after registration now takes the loader off the chain, instead of only answering false; - the Requirement body lists "enabled but missing from disk" among the quiet states, as scenario 3 and the code already did, and names the new cases; - "logged once at boot" reads "once per request" in the test docblock too. Mutation-checked, 6 mutations, all red in OpenRegisterAutoloaderTest: ignoring register()'s answer, dropping the class check, moving it outside the try, not recording, never running the closure, and leaving the loader after a disable. Not covered by a unit test: deleting the single bootstrapAppHost() call in Application. That breaks every AppHost route, which the Newman collection checks. Unit suite green on the host (1369, 12 skipped), psalm 0 errors, phpcs and phpmd add nothing. phpstan did not run locally. Assisted-by: ClaudeCode:claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
fix(apphost): Nextcloud 35 support — public-API OpenRegister autoload prelude + derived CI matrix (max-version 35)
* test(cli): decrypt the envelope the server really sends (red, #793) The CLI tests faked the server with the CLI's own envelope shape. This adds cli/testdata/machine_envelope.json, written by the real MachineSecretEnvelopeService::serialize() over EncryptService ciphertext with a throwaway RSA-4096 key, and a PHPUnit guard that fails when serialize() stops producing exactly that envelope. The Go tests decrypt it through fetchDecrypt and ci fetch and fail today with unexpected envelope scheme "". * fix(cli): parse encryption.scheme and ciphertext.key from the server envelope (#793) MachineEnvelope read a top-level scheme and payload.value, which the server never sends, so ci fetch and ci run stopped on every real secret with unexpected envelope scheme "". The struct now mirrors MachineSecretEnvelopeService::serialize() (secret, encryption, ciphertext) and fetchDecrypt checks encryption.scheme and decrypts ciphertext.key. The client test serves the real serializer fixture instead of the CLI's own shape.
* fix(secrets): refuse a folder the secret's owner does not own SecretService copied folderId from the request as is on create, update and the application paths, so a user could file a secret in another user's folder by its id. The folder owner's delete counts and purges the folder's secrets without an owner filter, so a planted secret held up or was caught by their delete. create() and update() now check the folder with FolderOwnershipGuard against the owner, createForApplication() against the writing user, and the machine-token paths refuse any folder, since folders belong to users. Without the guard wired every folder is refused. The create endpoint maps a foreign folder to 403 and a missing one to 404. WIP at wind-down: update() is now 103 lines against phpmd's 100 line threshold; check:strict not yet run. Fixes #795 * refactor(secrets): one folder ownership check shared by create, update and the application path The keepiq#795 guard was written out three times, which put update() at 103 lines against phpmd's 100. requireFolderOwnedBy() holds the check once. * test(secrets): wire the folder guard into SecretServiceTest (#795) The metadata-edit test moves a secret into folder-2. With keepiq#795 a move is checked against the owner, and an unwired guard refuses every folder, so the suite now builds SecretService with the real FolderOwnershipGuard over a mapper where alice owns every folder.
* test(import): a restored backup keeps every secret type (red, #749) Runs a vault of server secrets (typeId UUIDs, no type name) through the real serializeVault, encryptBackup, the registered backup parser and the import store's commit, plus an older backup that stores type names. Both fail today: every restored secret is posted without a typeId, so the server files it under the default type. * fix(import): restore every secret type from a backup by type id or name (#749) A backup stores the server secret's typeId, a UUID, and the import store only stamped a typeId for the names totp, passkey, card and identity, so every restored secret fell back to the default type. commit() now resolves each row's type once through typeIdResolver(): a type id the vault knows is kept, a type name maps to the vault's type of that name for every type (a system type wins over a custom one of the same name), and anything else still falls back to the default. The serializer spec now uses UUID type ids as the server sends them. Only the restore-type half of point 3; source-row numbering and the other points of #749 stay open.
…t or send (#813) * test(export): a secret that cannot be decrypted is counted and shown (red, #794) decryptAllSecrets() drops a secret it cannot decrypt in an empty catch, and openExport() and openCxp() hand only the decrypted list on, so the export file and the CXP transfer miss it without a word. These tests decrypt three secrets with one throwing, and expect the skipped count to reach both dialogs, the warning to render, and Export and Send to wait until the user continues. They fail today. * fix(export): show how many secrets could not be decrypted before export or send (#794, wip) decryptAllSecrets() now returns { secrets, skipped } and counts every secret it cannot decrypt instead of dropping it in an empty catch. openExport() and openCxp() pass the count to ExportDialog and CxpTransferDialog, which show it in a warning and keep Export and Send disabled (and their handlers refuse) until the user chooses to continue without those secrets. Cancel and Close stay available. WIP: the three new strings are not yet in l10n/*.json and l10n/*.js, so test:l10n and the locale parity check fail on this commit. * i18n(export): translate the skipped-secrets warning into every locale Adds the three keepiq#794 strings to all 37 locale files (English fallback for rm, lb, ga and mt, as the earlier encryption-suites strings did) and regenerates the .js catalogues with l10n:build. * docs(export): tag onUpdateOpen with the export-in-silence requirement (#794)
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Automated PR to sync development changes to beta for beta release.
Merging this PR will trigger the beta release workflow.