Skip to content

feat(sdk-api): SJCL-free v1 decrypt with temp fallback - #9508

Open
pranavjain97 wants to merge 2 commits into
masterfrom
pranavjain/wcn-43-remove-sjcl-decrypt-pr1
Open

feat(sdk-api): SJCL-free v1 decrypt with temp fallback#9508
pranavjain97 wants to merge 2 commits into
masterfrom
pranavjain/wcn-43-remove-sjcl-decrypt-pr1

Conversation

@pranavjain97

@pranavjain97pranavjain97 commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Summary

Introduces a native (SJCL-free) v1 decrypt path in @bitgo/sdk-api using node:crypto on the server and crypto-browserify via webpack's shim in the browser. Public decrypt() routes v1 envelopes through the native path with a
temporary sjcl.decrypt fallback so callers are never blocked if native fails on an unexpected envelope shape.

Part of the ongoing SJCL migration. Also introduces an io-ts codec that validates v1 envelope shape and bounds params before any KDF work.

Behavior

  • v1 envelopes: native node:crypto → SJCL fallback + console.warn on native failure
  • v2 envelopes: unchanged (already native Argon2id)
  • Envelope validation throws before the fallback — no SJCL bypass on malformed input
  • Success on native is silent; a warn is a real signal to investigate

Test evidence

  • 32 Node parity tests against SJCL-produced envelopes (aes-128/256, tag sizes, adata, UTF-8, >64 KiB plaintext, 50 randomised inputs, malformed rejection)
  • 12 browser-shim parity tests through crypto-browserify
  • 8 real BitGo keycard fixture assertions: byte-for-byte identical output between SJCL, node:crypto, and crypto-browserify on user + backup keys from a throwaway testnet Solana wallet
  • 3 fallback behavior tests
  • 250 sdk-api tests pass, lint + tsc clean

Test plan

  • v1-encrypted encryptedPrv/keycards decrypts correctly in Express/sdk
  • Tx signing flow end-to-end on express

@linear-code

Copy link
Copy Markdown
Contributor

WCN-43

@pranavjain97pranavjain97 changed the title feat(sdk-api): SJCL-free v1 decrypt; Node default, browser opt-infeat(sdk-api): SJCL-free v1 decrypt with temporary fallbackAug 14, 2026
@pranavjain97pranavjain97 changed the title feat(sdk-api): SJCL-free v1 decrypt with temporary fallbackfeat(sdk-api): SJCL-free v1 decrypt with temp fallbackAug 14, 2026
@pranavjain97
pranavjain97force-pushed the pranavjain/wcn-43-remove-sjcl-decrypt-pr1 branch from 72f280a to 9c43b24CompareAugust 14, 2026 18:45
@pranavjain97
pranavjain97 marked this pull request as ready for review August 14, 2026 18:49
@pranavjain97
pranavjain97 requested review from a team as code ownersAugust 14, 2026 18:49
@pranavjain97
pranavjain97force-pushed the pranavjain/wcn-43-remove-sjcl-decrypt-pr1 branch from 9c43b24 to 7cd08edCompareAugust 14, 2026 19:14
Comment threadmodules/sdk-api/test/unit/decryptV1.ts Outdated
Comment threadmodules/sdk-api/test/unit/decryptV1.ts
Comment threadmodules/sdk-api/test/unit/decryptV1.browser.ts Outdated
Comment on lines +85 to +92
try {
return await decryptV1(password, ciphertext);
} catch (nativeErr) {
const message = nativeErr instanceof Error ? nativeErr.message : String(nativeErr);
// eslint-disable-next-line no-console
console.warn('[bitgo-sdk] v1 native decrypt failed on well-formed envelope; using SJCL fallback:', message);
return sjcl.decrypt(password, ciphertext);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in this try/catch a wrong password / bad auth tag still hits the sjcl fallback + console.warn? might want to rethrow actual auth failures, and only have the fallback on unexpected crypto errors.

try{returnawaitdecryptV1(password,ciphertext);}catch(nativeErr){if(isAuthFailure(nativeErr)){thrownativeErr;}constmessage=nativeErrinstanceofError ? nativeErr.message : String(nativeErr);// eslint-disable-next-line no-consoleconsole.warn('[bitgo-sdk] v1 native decrypt failed on well-formed envelope; using SJCL fallback:',message);returnsjcl.decrypt(password,ciphertext);}

Introduces decryptV1, a native (SJCL-free) replacement for v1 envelope
decrypt using node:crypto on the server and crypto-browserify via the
existing webpack shim in browser bundles. Byte-for-byte compatible with
SJCL's envelope format.
Public decrypt() wraps the native path in a temporary SJCL fallback so
callers are never blocked if native fails on an unexpected envelope
shape. Envelope validation (parseV1Envelope) runs BEFORE the try/catch,
so malformed input still throws immediately -- only crypto-level
failures on well-formed envelopes fall through to sjcl.decrypt. Auth
failures (wrong password, tampered ciphertext) are rethrown as-is; only
unexpected native errors emit a console.warn and enter the fallback.
decryptV1WithCrypto accepts an injected crypto module so the browser
shim test can exercise the real decrypt code with crypto-browserify
instead of duplicating the CCM logic.
decryptV1WithFallback is exported and accepts an injected native fn so
tests can trigger the fallback path deterministically without stub
frameworks.
An io-ts codec enforces an iter cap of 100k on v1 envelopes up front,
before any KDF work runs.
Test coverage (251 total, all passing):
- 32 Node parity tests (aes-128/256, adata, UTF-8, >64KiB, 128-bit tags,
50 randomised inputs, malformed envelope rejection)
- 12 browser-shim parity tests via crypto-browserify
- 8 real BitGo keycard fixture assertions across both paths
- 4 fallback behavior tests including a deterministic non-auth fallback
test that injects a broken native fn
TICKET: WCN-2079
@pranavjain97
pranavjain97force-pushed the pranavjain/wcn-43-remove-sjcl-decrypt-pr1 branch from 7cd08ed to 2fab2b3CompareAugust 14, 2026 19:43

@danielpeng1danielpeng1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bump the CI tests are failing

…word'
BitGoAPI.decrypt() historically mapped SJCL's `"ccm: tag doesn't match"`
and WebCrypto AES-GCM's `"operation failed for an operation-specific
reason"` to a stable `"incorrect password"` error. Now that v1 decrypt
routes through node:crypto's AES-CCM, the auth-failure surface is
`"Unsupported state or unable to authenticate data"`. Downstream coin
tests expect `"incorrect password"` -- add the third message to the
mapping to keep the contract stable.
TICKET: WCN-2079
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@pranavjain97@danielpeng1