feat(sdk): decryptVersioned and V4-aware crypto.decrypt (MRF envelope support) (2/4)#9596
Draft
kevin9foong wants to merge 9 commits into
Draft
feat(sdk): decryptVersioned and V4-aware crypto.decrypt (MRF envelope support) (2/4)#9596kevin9foong wants to merge 9 commits into
kevin9foong wants to merge 9 commits into
Conversation
…9586) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…sioned (#9586) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…9586) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ies (#9586) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…fied round-trips (#9586) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…sions (#9586) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… matching prior decrypt order Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
kevin9foong
commented
Jun 11, 2026
| // An empty record is a valid V4 submission with no answered fields; | ||
| // `isFieldResponsesV4` alone cannot claim it since it checks the first entry. | ||
| const isV4Record = | ||
| Object.keys(decryptedObject).length === 0 || |
Contributor
Author
There was a problem hiding this comment.
isFieldResponsesV4 returns false for {} (it inspects the first entry), but the PRD defines an empty record as a valid V4 submission with no answered fields. The empty check therefore runs before the first-entry check rather than tightening isFieldResponsesV4, which ADR 0001 says must be reused unchanged.
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.
Problem
Agencies consume FormSG webhooks through the SDK's
crypto.decrypt, which only understands V1 content from storage-mode forms. Multirespondent (MRF) webhooks now deliver V4 content wrapped in the MRF envelope (encryptedSubmissionSecretKeyon the payload), and the SDK returnsnullfor them, leaving webhook integrators unable to decrypt MRF responses. Closes #9586.Solution
DecryptParamsgains an optionalencryptedSubmissionSecretKey. When present, decryption opens the MRF envelope (submission secret key decrypted with the form key, content with the submission key); when absent, content decrypts directly with the form key. The newcrypto.decryptVersionedreturns the content in its submitted version —DecryptedContent | DecryptedContentV4 | null, discriminated byArray.isArray(result.responses)— with the V4 arm carrying the honestly-populated per-submission secret key.crypto.decryptkeeps its exact signature: it now delegates todecryptVersionedand adapts the V4 arm throughadaptV4ToV1, dropping the key. Content version is detected by plaintext shape (array → V1, provenance-carrying record → V4, empty record → empty V4, anything else →null); theversionparam stays ignored as it always has been. Design is per ADR 0001; the README gains the consumer-contract section "Decrypting Multirespondent (V4) Submissions".Alternatives considered
submissionSecretKeyset to the form secret key, but that hands consumers a key with form-wide blast radius disguised as a per-submission key, so it returnsnullinstead. MakingsubmissionSecretKeyoptional on the V4 arm was also skipped — it would widen the publishedDecryptedContentV4type thatcryptoV3.decryptToV4already returns with the key required.versionfield instead of shape detection, but there is no submission-version constant for V4 content (MRF submissions are version 3 whether their plaintext is V3 or V4), and making a long-ignored parameter load-bearing would break integrators who pass junk versions today (per ADR 0001).decryptordering — otherwise a malformed payload with verified content and no signing key would start throwingMissingPublicKeyErrorwhere it previously returnednull.Breaking Changes
No - backwards compatible.
decrypt's signature and V1 behaviour are unchanged; the pre-existing crypto suite passes unmodified.Tests
TC1: MRF webhook round-trip
encryptedSubmissionSecretKey) tocrypto.decrypt— responses arrive asFormField[]crypto.decryptVersioned— V4 structure withsubmissionSecretKeymatching the envelopeTC2: storage-mode regression
crypto.decryptoutput is unchanged from the previous SDK version