Repository navigation
Conversation
…ct, unify merge semantics First step of the per-key hierarchy handoff plan: - vault-backup.test.ts: fix two fixture defects (export-name expectation now demo; mergeSource re-encrypted under sourceKek so SOURCE_PASSWORD auth reaches the real paths) and replace the vacuous tamper test with a real byte-flip on the stored target ciphertext (auth failure, target record byte-identical). Production code re-audited after green: no bugs found — all three failures were fixtures. - vault-manager.test.ts (20 cases) and vault-signer.test.ts (31 cases) rewritten for the hierarchical contract (keyIv + vaultPassword signing, selected-key-only decryption, SignResult.locked true, password cleared after use, activateVault instead of unlock, locked-readable account directory) while preserving every A4-era security assertion (post-merge lock hard contract, C2 interleavings, changePassword races, authority pre-check branches, chain-semantics key selection, recover_account exclusion, frozen-tx integrity + real txid vector). - signing-facade / panel / envelope / registry / service tests updated to the V1 literal and four-field envelope shapes; type-check to zero. - vault-manager.tsx merge form: drop the !locked submit gate and pre-unlock block; manager.importVaultDetailed now authenticates the target itself via targetPassword (probe unlock when no session), keeping the post-merge lock blueprint contract unchanged. Known items deferred to R1 review: empty-vault password semantics (any non-empty password unlocks a keyless vault — unchanged, untested), rename session wrinkle (pinned as KNOWN WRINKLE), failed-merge session retention (equivalent to old-world behavior). Part of #381
…ranch-only)
Rewrite the vault module doc from the retired whole-envelope implementation
to the per-key V1 hierarchy that landed on feat/vault-per-key-hierarchy
(three commits on top of origin/next@cee626a2; the branch is NOT merged
into next and no PR exists yet — everything documented is branch-only).
- header: R1 marked LANDED on this branch with truthful baseline/PR status
- module map: per-file responsibilities for the hierarchical envelope
validation, single-key crypto primitives, pointer-session service,
targetPassword merge probe, IV-selected-key signer, four-field backup
- invariants: rewritten around per-key {iv, ciphertext, note}, public outer
metadata, single password/salt, no global WIF-unlocked state, signing
scrub-before-broadcast, merge/authority/recover_account contracts
- pitfalls: added R1-era items (undecided empty-vault unlock semantics,
rename lingering session, failed-merge session retention, locked-readable
public directory); retired whole-envelope-era items
- extension checklist: R2/R3 pointers to the handoff task package
Only the doc changed; no code touched (pnpm type-check clean).
…rage, review cleanups (R1) R1 three-way review follow-ups (2 P1 + 2 P2 + P3 batch): - Empty-vault password anchors (2026-11-05 ruling): mergeVaultBackup rejects a target with zero key ciphertext with VAULT_EMPTY BEFORE any password is used — an empty vault has no password anchor, and the per-key target decryption loop would iterate zero times, silently re-keying the vault under whatever targetPassword was typed. changePassword likewise rejects an empty vault (unlock accepts any password for one, so the old password cannot be authenticated; the next addKey's password is the real anchor). Unlock pass-through, addKey-to-empty and independent import of an empty source stay as implemented (pinned). - Restored backup test coverage, rewritten for the per-key hierarchy: the import rollback stands down on a same-name remove+recreate (the rebuild survives byte-intact), an injected removal failure during rollback propagates the PRIMARY CAS error with the created empty vault as the residual, export propagates VAULT_SESSION_STALE when the record drifts between read and CAS (lastBackupAt untouched), and an empty-source import creates a fresh empty vault (pinned). - registry.createVault no longer derives a KEK it immediately zeroizes (dead code since the per-key rework); registry interface/header comments updated to the four-field, zero-crypto-at-create reality. - mergeVaultBackup matches the target via vaultNameComparisonKey, so a case-variant target name resolves the session vault exactly like the manager does instead of failing VAULT_LOCKED. - docs/AI-driver/09-vault.md: invariant 18 no longer claims fresh IVs for no-change sources (only revision+1 via CAS; every target ciphertext byte preserved), the header no longer pins commit counts/HEAD, invariant 3 records that the AAD deliberately does not bind key identity (relocation is the authority pre-check's job — never extend VAULT_AAD), invariants 4/18 spell out the deliberate addKey/merge per-key dedupe decryption surface, and the empty-vault pitfalls entry records the ruling (was UNDECIDED). - Merge form targetPassword autoComplete switched to off (vault passwords are not site credentials); five-field comment residue fixed (vault-manager.tsx, panel test, e2e spec); signing-facade pass-through example corrected to VAULT_EMPTY. Verified: pnpm type-check && pnpm lint && pnpm test (139 files / 1443 tests) && node tests/node/vault-wif-real-check.mjs.
R2 of the per-key vault arc (handoff §5 step 3):
- src/lib/vault/key-management.ts: read-only on-chain verification
orchestration. verifyKeysOnChain takes WIF-FREE candidates
(accountName + role + publicKey), runs ONE sorted deduplicated
batched fetchAccounts fresh:true query (signer pre-check
discipline: degraded/missing/malformed/fetch failure ->
VAULT_AUTHORITY_UNAVAILABLE, zero side effects) and reports per
candidate {matched, authority, weight, threshold, suffices}:
full key_auths scan of owner/active/posting (most privileged
match wins, malformed pairs skipped) plus memo_key comparison
(memo match = 1/1/true). Results are ADVICE, not gates:
sub-threshold and not-on-chain keys stay storable with UI
warnings; the single-key signer keeps failing closed.
- import-key-dialog.tsx: two-phase add flow (local validateVaultWif
-> chain verification -> explicit confirm -> manager.addKey).
Duplicate detection rides the service's idempotent no-op via a
before/after public-directory count -> keyAlreadyExists banner.
WIF lives only in the dialog's transient state, scrubbed on
success/cancel/unmount; never Redux, logs, analytics, network.
- vault-key-manager.tsx: locked-state browsing of the public
directory (accounts, per-role key counts, outer notes) with NO
unlock; note editing (updateKeyNote) and removal (account + role
+ IV pointer behind an explicit confirm, no password) ride the
R1 service primitives unchanged. Wired into vault-manager.tsx as
a 'keys' panel view behind the row menu's Manage keys action.
- i18n: 47 new vault-namespace keys per locale (en/zh/es/ko) for
the key manager, add-key dialog, verification verdicts and the
scene-level mergeTargetEmpty / changePasswordEmpty VAULT_EMPTY
messages; the errors table stays locked to VAULT_ERROR_CODES.
- tests: 19 key-management unit cases (batching, full-key_auths
scan, memo, fail-closed matrix), 10 component cases (locked
browsing, add branches, duplicate banner, remove confirm, note
edit, leak assertions), one panel integration case; PBKDF2 count
header corrected in vault-manager.test.ts (create 0 / import 2 /
merge 3 / changePassword 3 / unlock 1); 09-vault.md synced
(module map, R2 checklist, surviving-session pitfall).
Verified: pnpm type-check, pnpm lint (pre-existing warnings only),
targeted suites, full pnpm test (1473/1473) and
node tests/node/vault-wif-real-check.mjs (9/9).
R3 of the per-key vault arc (handoff §5 step 4): - login-form.tsx: LoginForm becomes a signing-mode DISPATCHER. The classic challenge-signed flow moves verbatim into the module-private PrivateKeyLoginForm (the file diff is insert-only: behavior, copy, remember-me and the #338 re-auth gate are byte-identical). Only a positively read 'vault' mode renders vault surfaces; loading, failed reads, an unavailable runtime and a missing provider keep the classic form (persisted default; login never hard-depends on IndexedDB). Vault Mode + embedded re-auth (fixedUsername / requiredAuthTypes) renders an explicit "not available in Vault Mode yet" notice with a Work Mode shortcut - identity selection cannot satisfy an authority requirement; A6 wires the per-key password + signing path. - vault-login-form.tsx: two native selects over the locked-readable public directory (manager.listVaults / manager.getAccounts; account names only, keyless accounts not offered). Default = active vault, else the first; selections are derived in render, a vault switch drops the account choice, directory loads are sequence-guarded and keyed by (vaultName, retry generation) so a slow stale response never lands. Empty states: no vault -> create/import entry, empty vault -> add-a-key hint (both open the Work Mode modal); storage / runtime / directory errors -> vault.errors.* + Retry, never a PrivateKey fallback. The list and directory refresh stale-while-revalidate when the Work Mode modal closes and when the tab becomes visible again. Submit dispatches setVaultIdentity and navigates (or onLoginSuccess): no unlock, no decryption, no challenge/login, no network. - auth slice: optional sessionKind ('privateKey' | 'vault'; absent = privateKey, so pre-R3 AuthState values stay valid; sessionKindOf). setVaultIdentity({ username }) sets sessionKind 'vault' and forces all six key fields null; setCredentials writes 'privateKey'; logout resets. The action carries no key material (sanitizers unchanged). - use-auth.ts: logging out a vault session clears only Redux - no /api/auth/logout, Legacy remembered username / posting key neither read nor removed. home-redirect-or-login.tsx waits for the mode and skips the Legacy remembered-username jump in Vault Mode. - work-mode-events.ts: leaf module for VAULT_WORK_MODE_OPEN_EVENT (still re-exported by app-layout) plus the new VAULT_WORK_MODE_CLOSED_EVENT, dispatched by app-layout when the modal closes; avoids a layout -> header -> login form -> layout import cycle. - R2 quick-review batch: import-key-dialog verdicts rank findings (not-on-chain > sub-threshold > authority differs from the selected role [new warn verdict verifyAuthorityDiffers] > memo > matched); countRoleKeys compares names through normalizeSteemUsername; tests for the VAULT_EMPTY scene messages (changePasswordEmpty / mergeTargetEmpty), WIF scrubbing on cancel / Esc / unmount, the not-on-chain leak check, and the non-string memo_key branch. - i18n: 16 auth.vault* keys + vault.verifyAuthorityDiffers in all four locales. - tests: new vault-login-form (19), login-form-dispatch (9, real useSigningMode), home-redirect-or-login (6), app-layout-work-mode (2); auth-slice / use-auth / key-manager / panel / key-management additions. login-form-reauth.test.tsx is unchanged and green. Guards (sequence, refresh wiring, redirect gate, verdict order, name normalization, scene routing) were mutation-checked. Full suite 145 files / 1526 tests green; real WIF check 9/9. - docs: 09-vault.md synced (status, module map, invariant 21 "a vault identity is a selection, never an authentication", R3 checklist, pitfalls); 02-auth.md notes the dispatcher and the vault logout path.
…and identity selection Handoff section 5 step 5: extend real-browser e2e over the R1-R3 per-key vault stack and run the full acceptance suite. New tests/e2e/vault-per-key.spec.ts (5 tests, real IndexedDB + real Web Crypto; /api/query/accounts intercepted with controlled responses so every chain-dependent branch is deterministic): - main journey: create vault -> add fixture WIF with note (chain-verified green) -> browse the directory LOCKED (no unlock; only the per-key encryption password is ever typed) -> identity login through the two selects -> header session at /@alice/transfers -> directory refresh when the Work Mode modal closes; - no /api/auth/challenge, /api/auth/login or /api/auth/logout request during the whole identity flow (page.on request tracking); - verdict matrix: matched (green), authority-differs (amber, stored under the selected role), not-on-chain (amber, fail-open storage with warning), plus the fail-closed VAULT_AUTHORITY_UNAVAILABLE path with in-form retry; - same-role multiple keys, duplicate (account, role, publicKey) idempotent banner, removal located by the account+role+IV row pointer; - multi-vault / multi-account login page, rapid vault switching with no cross-vault account leakage, identity submitted from the second vault; - login-page empty states (no vault / empty vault) with their Work Mode entry buttons, and the PrivateKey regression (switching the device back renders the classic username/private-key form); - UI checks: no horizontal overflow (page + scoped element + owning dialog box; the shared DialogFooter's deliberate -mx-4 full-bleed band and closed-popup <option> rects are excluded as non-overflow by design), secondary dialogs at z-60 on top of the Work Mode modal and interacted with (click-through proves they are not obscured). Decrypt-only-the-selected-key and pre-broadcast scrubbing have no signing UI to drive until A6 (vault sessions carry no keys); they stay pinned by tests/unit/vault-signer.test.ts and are NOT faked here. Product bug found and fixed (e2e-caught): useSigningMode kept per-instance local state, so a mode switch inside the Work Mode modal only updated the switcher's own instance — a login surface already mounted under the modal kept rendering the stale mode after closing it (switching back to PrivateKey left the vault identity selects with no way to log in without a manual reload). The hook now re-reads the persisted mode on VAULT_WORK_MODE_CLOSED_EVENT and on tab visibility (mirroring the R3 vault-login-form refresh pattern); unit-pinned in login-form-dispatch.test.tsx, docs/AI-driver/09-vault.md synced. Acceptance: pnpm verify green (type-check 0 errors; lint 0 errors, 3 pre-existing warnings in untouched files; coverage 84.5% lines / 75.7% branches, thresholds met; production build ok); pnpm e2e 14/14 (31s; existing smoke / wallet-main-flows / vault-mode specs unchanged and green — the PrivateKey regression evidence); node tests/node/vault-wif-real-check.mjs 9/9.
…ult sessions ClaimRewardsBanner.handleClaim fell back to tryRestoreRememberedPostingKey() whenever the session had no posting key. A Vault Mode identity session (sessionKind 'vault', all six key fields null) took the same path: it read the Legacy plaintext REMEMBERED_POSTING_KEY_KEY from localStorage, verified it against the chain, then signed and relayed claim_reward_balance with it. That breaks the R3 contract (handoff section 5 step 4: never read, write or remove the Legacy remembered posting key) and vault invariant 21 (a vault identity neither holds nor borrows key material). handleClaim now reads sessionKindOf(store.getState().auth) at click time and only attempts the remembered-key restore for non-vault sessions. A keyless vault session falls through to the existing re-auth dialog, which the LoginForm dispatcher answers with the "not available in Vault Mode" notice. privateKey sessions are unchanged. Regression: a vault-session case in claim-rewards-banner.test.tsx (built via the real setVaultIdentity reducer, same-account remembered key that would chain-verify) asserts no getItem of the remembered key (spy on Object.getPrototypeOf(localStorage)), no getAccounts, no signing, no broadcast and an open re-auth dialog. The existing case 'restores the device-remembered posting key and claims without re-auth' stays the privateKey control. 09-vault.md invariant 21 gains the matching consequence. Verification: with the gate removed the new case fails with "expected [ [ 'wallet:rememberedPostingKey' ] ] to have a length of +0 but got 1"; restored it passes (also isolated and 3x shuffled). pnpm type-check clean; pnpm lint 0 errors (3 pre-existing warnings in untouched files); banner + reauth-flow suites 19/19; pnpm test 145 files / 1528 tests passed. Part of #381
…docs Review A/B follow-ups: 06-frontend's reference key-resolution chain and 02-auth's least-privilege note now state the device-remembered posting restore is privateKey-sessions-only; 09-vault invariant 21 pins the exact dispatcher behavior, records the two deliberate non-key Legacy touches (username prefill during mode load, change-password clearing a now-invalid key) and the known pre-A6 UX gaps (market/proposals login dialogs, the hard-coded Not-authenticated text) for A6 to pick up. Part of #381
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.
Part of #381
Summary
Replaces the A0–A5 whole-envelope vault with the per-key hierarchy (handoff R1–R3) and adds real-browser coverage. One PR by design.
R1 — hierarchical V1 envelope, per-key encryption
{ version: 1, vaultName, salt, accounts }; each account hasowner/active/posting/memoarrays of{ iv, ciphertext, note }. Outer account names, roles and notes are public (readable while locked and in backups; all four locales warn that notes must not hold secrets).steem-wallet.vault.v1). No migration layer: the whole-envelope format never shipped.keyIv+vaultPassword, decrypts only the selected key, zero-fills the KEK and clears the password/WIF infinally;SignResult.lockedis always true. No global "all WIFs in memory" state exists.createdAtCAS, cross-tab hard line, post-merge lock,preMergeBackupdelivery, target-first dedupe on(accountName, role, publicKey), failure atomicity, authority pre-check traffic contract,recover_accountexclusion, memo never signs, privateKey default.targetPassword(no pre-unlock). Empty-vault rulings: merge into and change-password on a keyless vault are rejected withVAULT_EMPTY(no silent re-key under a typo); unlock and add-key on an empty vault stay allowed.R2 — key management and notes
key-management.ts: one sorted, de-duplicatedfresh: trueaccounts query; scans everykey_authsentry and compares memo againstmemo_key; degraded/missing/malformed data fails closed (VAULT_AUTHORITY_UNAVAILABLE). Only account names leave the browser.vault-key-manager.tsx/import-key-dialog.tsx: locked browsing of accounts, per-role key counts and notes; two-step add (local WIF check → on-chain verdict → confirm); idempotent duplicates; removal by account + role + IV with a second confirmation; note editing. Sub-threshold and not-on-chain keys may be stored with an explicit warning; the single-key signer still fails closed. WIF lives only in the dialog's transient state and is cleared on success, cancel and unmount.R3 — identity selection without unlocking
LoginFormis now a signing-mode dispatcher; the classic form moved verbatim intoPrivateKeyLoginForm(insert-only diff, existing login tests unchanged).vault-login-form.tsx: two native selects (vault, account) fed by the public directory; default active vault; switching clears the account; stale responses are dropped; empty/no-vault/storage-error states never fall back to PrivateKey.auth/setVaultIdentitysetssessionKind: 'vault'and nulls all six key fields. No unlock, no decryption, no/api/auth/challenge|login, no Legacy remembered-key access; vault logout is local-only.fixedUsername/requiredAuthTypes) show an explicit "not available in Vault Mode yet" notice until A6.Review
65bb7d35: empty-vault anchors, restored backup coverage, dead KEK derivation removed, case-variant merge target) → three-way verification (adversarial replays, mutation checks, end-to-end journey).202f7425(gate onsessionKindOf(auth) !== 'vault', regression test mutation-checked), then a post-fix three-way round passed: fix verification, remaining security items (raw IndexedDB scan after create/add/export/import/merge/change-password shows no WIF, password or KEK; all key consumers fail closed under a vault session; DevTools sanitizer unchanged), full verify + e2e.useSigningModeinstances held stale local state after a switch inside the Work Mode modal (300ce8c0, re-read on modal close and tab visibility).Test plan
pnpm verify: exit 0 — type-check clean; lint 0 errors (3 pre-existing warnings in untouched files); 145 files / 1528 tests; coverage 84.52% stmts / 75.68% branches / 83.23% funcs / 86.78% lines; production build OK.pnpm e2e: 14/14 (smoke 3, wallet-main-flows 4, vault-mode 2, new vault-per-key 5) against real Chromium IndexedDB + Web Crypto.node tests/node/vault-wif-real-check.mjs: 9/9.tests/fixtures/vault-development-test-vectors.jsonis byte-identical to the reviewed source.Not verified / known limits
/api/query/accountswith controlled responses (fixture accounts do not exist on chain); real-RPC verification is left to a dev deployment.useActiveSigningKey()is null), and the facade's privateKey backend is not wired. "Decrypt only the selected key" and "scrub before broadcast" are pinned byvault-signer.test.ts, not by e2e.requiredAuthTypes, so in Vault Mode they offer identity selection and a later order/vote reopens the dialog;transfer-form/delegate-formkeep their hard-coded "Not authenticated" text for a vault session.VAULT_PASSWORD_INVALID.docs/AI-driver/09-vault.mdis rewritten for the landed design (21 invariants, module map, pitfalls);02-auth.mdand06-frontend.mdare updated for the dispatcher and the vault-session exclusions.