Repository navigation
feat: safe keycard regeneration SDK support (WCN-3922) - #10145
yashaanand wants to merge 1 commit into
Conversation
|
|
e5bcfaa to
9b3dbe8
Compare
|
@claude review |
There was a problem hiding this comment.
Besides the inline findings, I checked whether the new rootKeychains (encrypted prvs and reducedEncryptedPrv) could leak through express. Neither handler sends them: handleV2GenerateSafe returns only safe.toJSON(), and handleV2GenerateSafeKeys returns only { rootKeys }.
Extended reasoning...
The PR adds safe keycard regeneration: it re-encrypts and reduces MPC key shares in sdk-core, widens the safe keycard codecs and generation to slot subsets, and changes the return type of generateSafe. The inline comments already cover the open findings, so this note only records an extra check on the express handlers in modules/express/src/clientRoutes.ts. Both handlers destructure or project the result, so no encrypted key material is added to the express responses.
Findings marked 🟡 are optional suggestions and need no follow-up push.
Additional findings (outside the current diff — GitHub can't attach inline comments there):
-
🟡
modules/sdk-lib-safes/src/codecs.ts— nit: the new exported helpervalidateJSONAgainstCodecusesanytwice, which CLAUDE.md asks to avoid. Its signature usesA extends Record<string, any>andcodec: t.Type<A, any, unknown>. Fix: useRecord<string, unknown>andt.Type<A, unknown, unknown>, which is the form the siblingdecodeWithCodecin the same file already uses.Why this was flagged
CLAUDE.md line 88 says "Avoid
anytype... preferunknown". The new exportedvalidateJSONAgainstCodecin modules/sdk-lib-safes/src/codecs.ts declaresA extends Record<string, any>andcodec: t.Type<A, any, unknown>. It is part of the package's public API through modules/sdk-lib-safes/src/index.ts.decodeWithCodec, a few lines above in the same file, already types its codec ast.Type<A, unknown, unknown>. No runtime failure. This is a convention slip in new public code.Verification: nit. The diff adds two
anyuses to new exported code. In modules/sdk-lib-safes/src/codecs.ts, lines 28 and 30 (not line 1108), the helper is exported publicly through modules/sdk-lib-safes/src/index.ts. CLAUDE.md line 88 says to avoid theanytype. The siblingdecodeWithCodecat line 9 already usest.Type<A, unknown, unknown>. Nothing fails at runtime.
sdk-lib-mpc: pure CBOR reduce helpers (reduceDklsKeyShare, reduceMpsKeyShare) extracted from the DKG classes, with byte-equality goldens against Dkg.getReducedKeyShare. sdk-lib-safes: parseSafeKeycardBox accepts any non-empty declared subset of root slots (aligned with the unmerged WCN-2734 branch, adapted for the ed25519 chain-code entry). key-card: safe keycard generation accepts a slot subset and gains a blob-returning variant (generateSafeKeycardBlob) for mobile. sdk-core: Safe.requestKeycardAccess / claimKeycardMaterial (WCN-3921 route contract), buildSafeKeycardRoots (Box A re-encrypt under the creation password, MPC reduce + re-envelope in one encryption session, backup envelopes byte-identical), Box D decrypt helper, generateSafe takes passcodeEncryptionCode (stored as originalPasscodeEncryptionCode on the multisig and MPC user/backup roots) and returns the root triplets incl. reducedEncryptedPrv, decrypt/derive tolerate slot subsets. Express handlers keep their documented wire contracts (root key ids only; no key material over the wire). Experimental/beta surface; no runtime flag — the routes are gated server-side by wp_safe_keycard_access. Ticket: WCN-3922
9b3dbe8 to
72dba15
Compare
|
@claude review |
There was a problem hiding this comment.
Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.
Additional findings (outside the current diff — GitHub can't attach inline comments there):
-
🟡
modules/sdk-core/src/bitgo/safe/keycard.ts— Keycard regeneration fails with a false wrong-password error if WP returnsreducedEncryptedPrv: nullon an MPC backup. The code never falls back to the backup's fullencryptedPrv. The guardbackupReducedEncryptedPrv !== undefinedat keycard.ts:513 lets null through, soverifyKeycardPassphrase(null)runs and throwsIncorrectPasswordError. Fix: test for a usable value (typeof backupReducedEncryptedPrv === 'string' && backupReducedEncryptedPrv.length > 0), so null and empty values fall through to theencryptedPrvreduce branch.Why this was flagged
The trigger is claimed material whose MPC backup record serializes the absent field as
reducedEncryptedPrv: null. This code already expects WP to use null for missing fields:SafeKeycardMaterialCodecacceptspasscodeEncryptionCode: t.union([t.string, t.null, t.undefined]). The passthroughClaimedKeychainCodec(keycard.ts:327) lets the null through, andSafe.claimKeycardMaterialcasts it toKeychainunchanged (safe.ts). InbuildSafeKeycardRoots, the null passes the!== undefinedcheck at keycard.ts:513, andbitgo.decrypt({ input: null, ... })throws. The error is then wrapped asthe keycard (creation) password does not open the safe ${slot} backup root. The user has typed the correct creation password, is told it is wrong, and cannot regenerate the keycard. Theelse if (backupEncryptedPrv)branch would have reduced the full backup envelope successfully. This is a new function, so the base branch has no regeneration path to compare against.Verification: Severity: normal. ClaimedKeychainCodec (keycard.ts:22) accepts any value for
reducedEncryptedPrv, including null. keycard.ts:208 checksif (backupReducedEncryptedPrv !== undefined), which lets null through. keycard.ts:210 then callsverifyKeycardPassphrase(null, slot), which throwsIncorrectPasswordError, so the fallback at keycard.ts:212 (else if (backupEncryptedPrv)) never runs.
Summary
Client-side SDK support for Safe keycard regeneration (WCN-3922, M2 of the Safe Keycard Regeneration TDD). Four module areas:
reduceDklsKeyShare/reduceMpsKeyShareextracted from the DKG classes, with byte-equality golden tests againstDkg.getReducedKeySharefor both curves.parseSafeKeycardBoxaccepts any non-empty declared subset of root slots; unknown keys, non-string/entry values, and empty slot sets are rejected; canonical key order preserved. PortsvalidateJSONAgainstCodecfrom the unmerged WCN-2734 branch, adapted to master's{prv, chainCode}ed25519 entry union.generateSafeQrData,buildSafeBoxData) and gains a blob-returning variantgenerateSafeKeycardBlobfor mobile; the savinggenerateSafeKeycardis unchanged.Safe.requestKeycardAccess/Safe.claimKeycardMaterial(WCN-3921 route contract, decoded minimally with passthrough records),buildSafeKeycardRoots(user envelopes re-encrypted under the creation password in one shared KDF session, multisig backup envelopes byte-identical, MPC roots reduced + re-wrapped into the ceremony-shapedreducedEncryptedPrv),decryptSafeKeycardBoxD,generateSafetakespasscodeEncryptionCode(stored asoriginalPasscodeEncryptionCodeon the multisig and MPC user/backup roots) and returns the root triplets, anddecryptSafeKeycard/deriveSafeWalletKeystolerate slot subsets with named missing-root errors.Verification
@bitgo/sdk-lib-mpc: 273 passing, incl. the two golden reduce assertions (DKLS + MPS byte equality).@bitgo/sdk-lib-safes: 96 passing (subset codec +validateJSONAgainstCodeccoverage).@bitgo/key-card: 42 passing (2-slot subset round-trip, full-4 regression, empty-roots rejection, blob smoke test).@bitgo/sdk-coresafe + keychain suites: 196 passing (buildSafeKeycardRoots same-password/password-change/wrong-password/missing-envelope cases, 2-slot round-trip throughparseSafeKeycardBox→decryptSafeKeycard→deriveSafeWalletKeys, Box D decrypt, claim mapping,generateSafetriplet return + passcodeEncryptionCode threading, rotation skip for envelope-less backup roots).@bitgo/sdk-coin-algosafe recovery (consumer of the widenedderiveSafeWalletKeys): 242 passing.signPayloadfull-suite isolation flake (4/4 pass standalone) and an occasional supertest socket race, both unrelated to this diff.yarnat root): all packages compile clean.lint-staged+ direct eslint).yarn lint-changedadditionally fails in@bitgo/passkey-crypto, which is byte-identical to master with 1798 pre-existing prettier errors — unrelated.Breaking changes
ISafes.generateSafenow returnsSafeCreationResult({ safe, rootKeychains }) andcreateSafeKeysreturnsSafeKeys & { rootKeychains }— the root triplets (incl.reducedEncryptedPrv) the keycard needs. This@experimentalsurface's only caller is retail'screateSafeWithSdk, bumped in M3 (WCN-3923). Express handlers keep their documented wire contracts (safes.generatereturnsSafeData;keys/generatereturns only root key ids — no key material over the wire).decryptSafeKeycardreturnsPartial<DecryptedSafeRoots>; full records remain assignable, andderiveSafeWalletKeysthrows a named error for absent slots.Coordination & scope notes
031757ec9dis unmerged onrecovery/WCN-2734and predates master'sEd25519ChainCodeEntry; this PR ports its approach adapted to current master (union entry codec kept). If WCN-2734 merges first, rebase and prefer this superset version.buildSafeKeycardRootsunit tests and the existingrecovery.tssuite (full-4 decrypt + per-slot derive).sdk-coin-ethE403 republish failure (BitGoJS fix(scripts): reconcile publish conflicts and wait for visibility (SI-2040) #9997) — out of scope; release tooling owns publishing.@experimentalsurface and the WP routes are gated server-side bywp_safe_keycard_access(WCN-3919/3921); subset/blob keycard generation is pure client-side formatting.Ticket: WCN-3922