Browser NodeVault key crypto module

Task 5288 · Done · Coder · created · updated

## Original requirement (epic 5269, verbatim) Build NodeVault feature: - Is a product that can be activated on a billing plan (once it lands in 5246) - When NodeVault is activated for an org, Org Admin can start Setup in Settings / NodeVault, that initiates a stepper: - Explanation with a diagram - On the backend System generates a Temporary NodeVault Key and sends it to all nodes (encoding each with node key) in the organization. Nodes store it in a secure place (next to other sensitive information, such as node token) - Generate NodeVault Key / Upload NodeVault Key. When Generate NodeVault Key - browser generates a key-pair (same algo as Temporary) encodes it with Temporary NodeVault Key public key and sends to CP to fan out to all nodes. Stores public key in the DB - Migrate credentials to NodeVault: all, select. Backend goes through all credentials that user selected and that are KMS encoded, decodes them and re-encodes them with NodeVault Key public key. No cred logging. - Show the result to the user - Credentials get a checkbox NodeVault (on by default for each new cred) - Credentials remember their current NodeVault key hash - When a broker requests a credentials and the credential is NodeVault, the broker uses the private NodeVault Key to decode it - Admin can rotate the NodeVault in Settings / NodeVault. The rotation procedure is a stepper: - Generate / Upload - same as at Setup - Migrate credentials: some of the creds may still be KMS - offer to migrate them - The new key is encrypted with current key (in browser) and fanned out to all nodes. Nodes decode and store the new key, keeping the old one - CP goes through each key and sends them to nodes to rekey. Nodes decode the cred, reencode it with new public and send them back to CP - All these workflows must be resilient and retry when something fails. Use DBOS to make it as reliable and idempotent as it gets Goal: reliable, provable way of Jaah not seeing and not being able to obtain access to client's credentials with all paths hermetic and auditable. ## Settled inputs (binding — realize, do not reopen) Full documents: BA PRD = Task 5273's `prd` artifact; Architecture = Task 5274's `architecture` artifact (`task_read` 5273 / 5274, then `task_read_artifact`). READ the architecture sections this child names — §2 crypto constructions, §3.2 data model, §4 interface contracts are PINNED and every child builds against them exactly. Binding decisions: D1 node-only (CP-consumed creds stay KMS) · D2 per-node identity key pairs (NIK) replace the CP temp key · D3 Jaah-hosted nodes included · D4 mandatory passphrase backup, no Jaah escrow · D5 later-joining nodes get the key automatically from a peer · D6 NodeVault is a plan parameter with per-org override · D7 OAuth2 excluded · D8 old keys kept until admin retires (refused while any credential uses it) · D9 passive-breach-proof + audited, no claim vs malicious Jaah · D11 plan off after setup ⇒ existing NV creds keep working, setup/rotation/migration/retire/new-NV frozen · D12 Test disabled for NV · D13 rogue-node enrollment is detectable (audit + node list), not impossible · E1 vendor `cryptography` into jaahd · E2 rotation chain-wraps new key under CURRENT key (K1: rotation does not recover from a leaked current key) · E3 DBOS for every workflow (carve-out from ADR-0074) · E4 AC-9.3 dropped (no admin notification on key delivery) · E5 node key files daemon-owned 0600 in a 0700 dir. Pinned crypto (arch §2): RSA-4096 e=65537 RSAES-OAEP SHA-256/MGF1-SHA-256 everywhere; AES-256-GCM 12-byte nonce; key hash = hex(sha256(SPKI DER)); envelope key id `nv:<hash>`; secret envelope = ADR-0137 v2 tier-2 unchanged; key blob `{v:1,k,alg,ek,n,ct}` AAD `nodevault.key|v1|<org_id>|<key_hash>|<recipient>`; backup PBKDF2-SHA256 600k. SECURITY SURFACE: this epic is credentials/crypto. Never log/checkpoint/return a secret value, private key, data key, passphrase or backup. The merge needs the security review lane or human approval per branching policy. Quality bar: "technically passes" is the floor. A build that satisfies the words but leaks key material to the CP, marks a step done before the node acknowledged it, or shows raw ids/stack traces to the user FAILS this child. ## This child — scope (HOW) Web half of arch §2 (part of architecture child C8, split out because every NodeVault UI depends on it). `apps/web/src/lib/nodeVaultKeys.ts`, WebCrypto only, no new dependency: - generate an RSA-4096 OAEP-256 NVK (extractable only long enough to export PKCS#8, then drop every reference); key hash = hex(sha256(SPKI DER)), short form = first 12; - key blob wrap (§2 shape + AAD `nodevault.key|v1|<org_id>|<key_hash>|<recipient>`) for a node NIK (`node:<node_id>`, k=`nik:<fp>`) and the rotation chain (`chain:<current hash>`, k=`nv:<current hash>`); NIK fingerprint recomputed from the SPKI, never trusted from the server; - backup file JSON `{format:"jaah-nodevault-backup",v:1,org_id,key_hash,public_key,kdf:{alg:"PBKDF2-SHA256",iterations:600000,salt},n,ct}` AAD `nodevault.backup|v1|<key_hash>`, passphrase ≥12 chars; restore from backup + passphrase returning the key and recomputed hash, with a typed error for wrong passphrase / corrupt file; - reuse `credentialSeal.ts` sealSecret for secret envelopes (it takes the key as a parameter). - Cross-language interop test consuming `apps/jaahd/tests/fixtures/nodevault_vectors.json` (pattern `tools/tests/test_login_seal_interop.py`): browser blob → Python open AND Python blob → browser open, for node, chain and backup shapes. ## Acceptance criteria carried by this child (PRD, verbatim unless marked amended) - AC-4.6 (browser): key hash equals `openssl pkey -pubin -outform DER | sha256sum`. - AC-4.3 (crypto half, Assumed in PRD): the backup is passphrase-encrypted in the browser (passphrase entered twice, minimum length). *Verify:* opening the file without the passphrase reveals no key material. - AC-4.4 (crypto half): a wrong passphrase or corrupt file is detected (UI copy "Couldn't open this backup — check the passphrase" lands with the setup UI child) and nothing is produced. - AC-6.2 (crypto half): the chain blob is wrapped under the current NodeVault public key in the browser. Out of scope: Do NOT build any page, dialog or route; do NOT add a crypto npm dependency (WebCrypto only); do NOT change credentialSeal.ts's existing behaviour. Epic out of scope (PRD §6, verbatim): Don't change encryption or consumption for CP-consumed credentials (forge/git, Jira, Slack, notify targets, cloud-launch AWS, org shared store, LLM, Bedrock, Stripe, embeddings, headscale, MFA stay on ADR-0137 KMS; consumers aren't re-plumbed to nodes). Don't touch tier 1 (CREDENTIAL_ENCRYPTION_KEY). Don't change the node-token mechanism (`node_credentials.py`, /etc/skynet/node.env), the login-seal keys (ADR-0133), or the `jaah-credential` broker names for git, GitHub, Bedrock and account credentials. No OAuth2 in NodeVault; don't alter `oauth2_tokens.py`. Don't build anything in 5246's area beyond the one NodeVault plan parameter. No hosted-node hardening and no "Jaah-hosted" label. No node-side Test button, node-side project env resolution, transcript redaction, BYOK/XKS or external secret references (F1–F8 deferred). No Jaah-held escrow of any NodeVault private key or backup. No logging of any secret value, private key, wrapped-key blob contents or backup passphrase anywhere (logs, activity events, DBOS, Slack, task summaries). No admin notification on key delivery (AC-9.3 dropped, E4). Done means: - Unit tests: generate→hash matches an openssl-computed hash of the exported SPKI; backup round-trip; wrong passphrase ⇒ typed error; tampered backup ⇒ typed error — cite output. - Interop test passes both directions for node, chain and backup vectors — cite output. - Test asserting the exported PKCS#8 / JWK `d` never appears in any object returned for sending (only blobs + public key) — cite. - `./dc check full` green; PR merged into develop through the security review lane / approval; rendered report cites file:line per AC and the observed output of every oracle above. Required checks: test-stack

Outcome

nodeVaultKeys.ts (WebCrypto NVK gen, node/chain blobs, PBKDF2 backup) + 2-way Python interop test merged via #4664 (security gate human-approved, CI gate green). Local full rerun OOM-reaped; operator chose CI as gate. UI caller-contract follow-ups in report.

Pull request

Artifacts

Activity

  • Draft
  • Ready
  • Runner started
  • Awaiting response
  • Awaiting response
  • Runner resumed
  • Awaiting response
  • Awaiting response
  • Runner resumed
  • Runner ended (done)