From 8acf5498010dbcf74b4c02e9053b525e4177345f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 30 Apr 2024 19:09:47 +0000 Subject: [PATCH 1/5] fix(deps): bump zod from 3.23.4 to 3.23.5 in /shared (#7301) Bumps [zod](https://github.com/colinhacks/zod) from 3.23.4 to 3.23.5. - [Release notes](https://github.com/colinhacks/zod/releases) - [Changelog](https://github.com/colinhacks/zod/blob/master/CHANGELOG.md) - [Commits](https://github.com/colinhacks/zod/compare/v3.23.4...v3.23.5) --- updated-dependencies: - dependency-name: zod dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- shared/package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/shared/package-lock.json b/shared/package-lock.json index fd43355049..2089313b33 100644 --- a/shared/package-lock.json +++ b/shared/package-lock.json @@ -895,9 +895,9 @@ "dev": true }, "node_modules/zod": { - "version": "3.23.4", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.4.tgz", - "integrity": "sha512-/AtWOKbBgjzEYYQRNfoGKHObgfAZag6qUJX1VbHo2PRBgS+wfWagEY2mizjfyAPcGesrJOcx/wcl0L9WnVrHFw==", + "version": "3.23.5", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.5.tgz", + "integrity": "sha512-fkwiq0VIQTksNNA131rDOsVJcns0pfVUjHzLrNBiF/O/Xxb5lQyEXkhZWcJ7npWsYlvs+h0jFWXXy4X46Em1JA==", "funding": { "url": "https://github.com/sponsors/colinhacks" } @@ -1499,9 +1499,9 @@ "dev": true }, "zod": { - "version": "3.23.4", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.4.tgz", - "integrity": "sha512-/AtWOKbBgjzEYYQRNfoGKHObgfAZag6qUJX1VbHo2PRBgS+wfWagEY2mizjfyAPcGesrJOcx/wcl0L9WnVrHFw==" + "version": "3.23.5", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.5.tgz", + "integrity": "sha512-fkwiq0VIQTksNNA131rDOsVJcns0pfVUjHzLrNBiF/O/Xxb5lQyEXkhZWcJ7npWsYlvs+h0jFWXXy4X46Em1JA==" } } } From 16b099d43dbf2726612a058e57a7c95b5ceec243 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 May 2024 19:27:40 +0000 Subject: [PATCH 2/5] fix(deps): bump type-fest from 4.18.0 to 4.18.1 in /shared (#7303) Bumps [type-fest](https://github.com/sindresorhus/type-fest) from 4.18.0 to 4.18.1. - [Release notes](https://github.com/sindresorhus/type-fest/releases) - [Commits](https://github.com/sindresorhus/type-fest/compare/v4.18.0...v4.18.1) --- updated-dependencies: - dependency-name: type-fest dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- shared/package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/shared/package-lock.json b/shared/package-lock.json index 2089313b33..f5353776b1 100644 --- a/shared/package-lock.json +++ b/shared/package-lock.json @@ -865,9 +865,9 @@ } }, "node_modules/type-fest": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.18.0.tgz", - "integrity": "sha512-+dbmiyliDY/2TTcjCS7NpI9yV2iEFlUDk5TKnsbkN7ZoRu5s7bT+zvYtNFhFXC2oLwURGT2frACAZvbbyNBI+w==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.18.1.tgz", + "integrity": "sha512-qXhgeNsX15bM63h5aapNFcQid9jRF/l3ojDoDFmekDQEUufZ9U4ErVt6SjDxnHp48Ltrw616R8yNc3giJ3KvVQ==", "engines": { "node": ">=16" }, @@ -1478,9 +1478,9 @@ } }, "type-fest": { - "version": "4.18.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.18.0.tgz", - "integrity": "sha512-+dbmiyliDY/2TTcjCS7NpI9yV2iEFlUDk5TKnsbkN7ZoRu5s7bT+zvYtNFhFXC2oLwURGT2frACAZvbbyNBI+w==" + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.18.1.tgz", + "integrity": "sha512-qXhgeNsX15bM63h5aapNFcQid9jRF/l3ojDoDFmekDQEUufZ9U4ErVt6SjDxnHp48Ltrw616R8yNc3giJ3KvVQ==" }, "util-deprecate": { "version": "1.0.2", From 1c8a364e7ec6045c1cccf28422f881e27326719e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 May 2024 19:30:16 +0000 Subject: [PATCH 3/5] fix(deps): bump ejs from 3.1.8 to 3.1.10 (#7304) Bumps [ejs](https://github.com/mde/ejs) from 3.1.8 to 3.1.10. - [Release notes](https://github.com/mde/ejs/releases) - [Commits](https://github.com/mde/ejs/compare/v3.1.8...v3.1.10) --- updated-dependencies: - dependency-name: ejs dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 11 +++++++---- package.json | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6d5517a675..aee9f81aad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,7 +45,7 @@ "dd-trace": "^3.36.0", "dedent-js": "~1.0.1", "dotenv": "^16.0.3", - "ejs": "^3.1.8", + "ejs": "^3.1.10", "express": "^4.19.2", "express-rate-limit": "^6.7.0", "express-request-id": "^1.4.1", @@ -12312,8 +12312,9 @@ "license": "MIT" }, "node_modules/ejs": { - "version": "3.1.8", - "license": "Apache-2.0", + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", "dependencies": { "jake": "^10.8.5" }, @@ -36665,7 +36666,9 @@ "version": "1.1.1" }, "ejs": { - "version": "3.1.8", + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", "requires": { "jake": "^10.8.5" } diff --git a/package.json b/package.json index 4b329a95a5..12ed65707a 100644 --- a/package.json +++ b/package.json @@ -91,7 +91,7 @@ "dd-trace": "^3.36.0", "dedent-js": "~1.0.1", "dotenv": "^16.0.3", - "ejs": "^3.1.8", + "ejs": "^3.1.10", "express": "^4.19.2", "express-rate-limit": "^6.7.0", "express-request-id": "^1.4.1", From ed00a826e9ecc23975ac3875fd1c4b96c90e3c0d Mon Sep 17 00:00:00 2001 From: Ken Lee Shu Ming Date: Fri, 3 May 2024 10:17:41 +0800 Subject: [PATCH 4/5] fix(mrf): attachment v2 (#7281) * fix: add localhost domain into cors, used by attachment uploads * fix: remove attachment contents out of encrypted fields, update attachment encryption to submission level * chore: fix typo on throw -> throws * feat: handle attachments for admin flow, backward compatibility * chore: update chromium version for production Dockerfile * chore: update error --------- Co-authored-by: Justyn Oh --- .../responses/AdminSubmissionsService.ts | 4 +- .../IndividualResponsePage/DecryptedRow.tsx | 20 ++-- .../IndividualResponsePage.tsx | 24 ++++- .../storage/worker/decryption.worker.ts | 16 +++- .../public-form/PublicFormContext.tsx | 3 +- .../public-form/PublicFormProvider.tsx | 92 ++++++++++++++++--- .../features/public-form/PublicFormService.ts | 23 ++++- .../components/FormFields/FormFields.tsx | 16 +++- .../FormFields/FormFieldsContainer.tsx | 5 +- frontend/src/features/public-form/queries.ts | 4 +- frontend/src/features/public-form/types.ts | 8 ++ .../public-form/utils/createSubmission.ts | 2 +- .../public-form/utils/decryptSubmission.ts | 51 ++++++++++ frontend/src/utils/bufferToFile.ts | 5 +- shared/types/submission.ts | 7 +- src/app/models/submission.server.model.ts | 5 + .../multirespondent-submission.controller.ts | 4 + .../multirespondent-submission.middleware.ts | 23 +++-- .../multirespondent-submission.utils.ts | 3 +- src/types/api/multirespondent_submission.ts | 1 + src/types/submission.ts | 3 + 21 files changed, 270 insertions(+), 49 deletions(-) create mode 100644 frontend/src/features/public-form/types.ts diff --git a/frontend/src/features/admin-form/responses/AdminSubmissionsService.ts b/frontend/src/features/admin-form/responses/AdminSubmissionsService.ts index a2a7d9f2df..6bfbb5f78b 100644 --- a/frontend/src/features/admin-form/responses/AdminSubmissionsService.ts +++ b/frontend/src/features/admin-form/responses/AdminSubmissionsService.ts @@ -91,7 +91,7 @@ export const getDecryptedSubmissionById = async ({ submissionId, }) - let processedContent, submissionSecretKey + let processedContent, submissionSecretKey, mrfVersion switch (encryptedSubmission.submissionType) { case SubmissionType.Encrypt: { const decryptedContent = formsgSdk.crypto.decrypt(secretKey, { @@ -119,6 +119,7 @@ export const getDecryptedSubmissionById = async ({ decryptedContent, ) submissionSecretKey = decryptedContent.submissionSecretKey + mrfVersion = encryptedSubmission.mrfVersion break } } @@ -138,6 +139,7 @@ export const getDecryptedSubmissionById = async ({ ? encryptedSubmission.payment : undefined, responses, + mrfVersion, } } diff --git a/frontend/src/features/admin-form/responses/IndividualResponsePage/DecryptedRow.tsx b/frontend/src/features/admin-form/responses/IndividualResponsePage/DecryptedRow.tsx index e4f97c3bf8..e4c07ac043 100644 --- a/frontend/src/features/admin-form/responses/IndividualResponsePage/DecryptedRow.tsx +++ b/frontend/src/features/admin-form/responses/IndividualResponsePage/DecryptedRow.tsx @@ -16,7 +16,7 @@ export interface DecryptedRowBaseProps { row: AugmentedDecryptedResponse } type DecryptedRowProps = DecryptedRowBaseProps & { - secretKey: string + attachmentDecryptionKey: string } const DecryptedQuestionLabel = ({ row }: DecryptedRowBaseProps) => { @@ -62,17 +62,20 @@ const DecryptedTableRow = ({ row }: DecryptedRowBaseProps): JSX.Element => { ) } -const DecryptedAttachmentRow = ({ row, secretKey }: DecryptedRowProps) => { +const DecryptedAttachmentRow = ({ + row, + attachmentDecryptionKey, +}: DecryptedRowProps) => { const { downloadAttachmentMutation } = useMutateDownloadAttachments() const handleDownload = useCallback(() => { if (!row.downloadUrl || !row.answer) return return downloadAttachmentMutation.mutate({ url: row.downloadUrl, - secretKey, + secretKey: attachmentDecryptionKey, fileName: row.answer, }) - }, [downloadAttachmentMutation, row, secretKey]) + }, [downloadAttachmentMutation, row, attachmentDecryptionKey]) return ( @@ -102,12 +105,17 @@ const DecryptedAttachmentRow = ({ row, secretKey }: DecryptedRowProps) => { } export const DecryptedRow = memo( - ({ row, secretKey }: DecryptedRowProps): JSX.Element => { + ({ row, attachmentDecryptionKey }: DecryptedRowProps): JSX.Element => { switch (row.fieldType) { case BasicField.Section: return case BasicField.Attachment: - return + return ( + + ) case BasicField.Table: return default: diff --git a/frontend/src/features/admin-form/responses/IndividualResponsePage/IndividualResponsePage.tsx b/frontend/src/features/admin-form/responses/IndividualResponsePage/IndividualResponsePage.tsx index 8b082dec9a..b09612120d 100644 --- a/frontend/src/features/admin-form/responses/IndividualResponsePage/IndividualResponsePage.tsx +++ b/frontend/src/features/admin-form/responses/IndividualResponsePage/IndividualResponsePage.tsx @@ -83,6 +83,16 @@ export const IndividualResponsePage = (): JSX.Element => { const { secretKey } = useStorageResponsesContext() const { data, isLoading, isError } = useIndividualSubmission() + // Logic to determine which key to use to decrypt attachments. + const attachmentDecryptionKey = + // If no submission secret key present, it is a storage mode form. So, use form secret key. + !data?.submissionSecretKey + ? secretKey + : // It's an mrf, but old version + !data.mrfVersion + ? secretKey + : data.submissionSecretKey + const attachmentDownloadUrls = useMemo(() => { const attachmentDownloadUrls = new Map() data?.responses.forEach(({ questionNumber, downloadUrl, answer }) => { @@ -98,20 +108,20 @@ export const IndividualResponsePage = (): JSX.Element => { const { downloadAttachmentsAsZipMutation } = useMutateDownloadAttachments() const handleDownload = useCallback(() => { - if (attachmentDownloadUrls.size === 0 || !secretKey) return + if (attachmentDownloadUrls.size === 0 || !attachmentDecryptionKey) return return downloadAttachmentsAsZipMutation.mutate({ attachmentDownloadUrls, - secretKey, + secretKey: attachmentDecryptionKey, fileName: `RefNo ${submissionId}.zip`, }) }, [ attachmentDownloadUrls, downloadAttachmentsAsZipMutation, - secretKey, + attachmentDecryptionKey, submissionId, ]) - if (!secretKey) + if (!secretKey || !attachmentDecryptionKey) return ( } @@ -192,7 +202,11 @@ export const IndividualResponsePage = (): JSX.Element => { <> }> {data?.responses.map((r, idx) => ( - + ))} diff --git a/frontend/src/features/admin-form/responses/ResponsesPage/storage/worker/decryption.worker.ts b/frontend/src/features/admin-form/responses/ResponsesPage/storage/worker/decryption.worker.ts index 7dbbae7072..e37340e7b8 100644 --- a/frontend/src/features/admin-form/responses/ResponsesPage/storage/worker/decryption.worker.ts +++ b/frontend/src/features/admin-form/responses/ResponsesPage/storage/worker/decryption.worker.ts @@ -101,7 +101,7 @@ async function decryptIntoCsv(data: LineData): Promise { : undefined, ) try { - let decryptedSubmission + let decryptedSubmission, submissionSecretKey switch (submission.submissionType) { case SubmissionType.Encrypt: { const decryptedObject = formsgSdk.crypto.decrypt(secretKey, { @@ -125,6 +125,7 @@ async function decryptIntoCsv(data: LineData): Promise { if (!decryptedObject) { throw new Error('Invalid decryption for multirespondent response') } + submissionSecretKey = decryptedObject.submissionSecretKey decryptedSubmission = await processDecryptedContentV3( submission.form_fields, decryptedObject, @@ -150,6 +151,17 @@ async function decryptIntoCsv(data: LineData): Promise { } if (downloadAttachments) { + // Logic to determine which key to use to decrypt attachments. + const attachmentDecryptionKey = + // If no submission secret key present, it is a storage mode form. So, use form secret key. + !submissionSecretKey + ? secretKey + : // It's an mrf, but old version + submission.submissionType === SubmissionType.Multirespondent && + !submission.mrfVersion + ? secretKey + : submissionSecretKey + let questionCount = 0 decryptedSubmission.forEach((field) => { @@ -170,7 +182,7 @@ async function decryptIntoCsv(data: LineData): Promise { downloadBlob = await queue.add(() => downloadAndDecryptAttachmentsAsZip( attachmentDownloadUrls, - secretKey, + attachmentDecryptionKey, ), ) csvRecord.setStatus( diff --git a/frontend/src/features/public-form/PublicFormContext.tsx b/frontend/src/features/public-form/PublicFormContext.tsx index d17a84bf4f..b354d44100 100644 --- a/frontend/src/features/public-form/PublicFormContext.tsx +++ b/frontend/src/features/public-form/PublicFormContext.tsx @@ -68,7 +68,8 @@ export interface PublicFormContextProps encryptedPreviousSubmission?: MultirespondentSubmissionDto previousSubmission?: ReturnType - setPreviousSubmission: ( + previousAttachments?: Record + setPreviousSubmission?: ( previousSubmission: ReturnType, ) => void } diff --git a/frontend/src/features/public-form/PublicFormProvider.tsx b/frontend/src/features/public-form/PublicFormProvider.tsx index 53b267faf7..033f3958ad 100644 --- a/frontend/src/features/public-form/PublicFormProvider.tsx +++ b/frontend/src/features/public-form/PublicFormProvider.tsx @@ -62,7 +62,7 @@ import { } from '~features/verifiable-fields' import { FormNotFound } from './components/FormNotFound' -import { decryptSubmission } from './utils/decryptSubmission' +import { decryptAttachment, decryptSubmission } from './utils/decryptSubmission' import { usePublicAuthMutations, usePublicFormMutations } from './mutations' import { PublicFormContext, SubmissionData } from './PublicFormContext' import { useEncryptedSubmission, usePublicFormView } from './queries' @@ -140,6 +140,9 @@ export const PublicFormProvider = ({ /* enabled= */ !submissionData, ) + const { isNotFormId, toast, vfnToastIdRef, expiryInMs, ...commonFormValues } = + useCommonFormProvider(formId) + const { data: encryptedPreviousSubmission, isLoading: isSubmissionLoading, @@ -159,21 +162,90 @@ export const PublicFormProvider = ({ const [isSubmissionSecretKeyInvalid, setIsSubmissionSecretKeyInvalid] = useState(false) + const [previousAttachments, setPreviousAttachments] = useState< + Record + >({}) + const [searchParams] = useSearchParams() + // MRF key + let submissionSecretKey = '' + try { + submissionSecretKey = decodeURIComponent(searchParams.get('key') ?? '') + } catch (e) { + console.log(e) + } + + useEffect(() => { + // Function to decrypt attachments retrieved from S3 using the submission secret key + const decryptAttachments = async () => { + const decryptedAttachments: Record = {} + if (!encryptedPreviousSubmission) return + const isValid = isKeypairValid( + encryptedPreviousSubmission.submissionPublicKey, + submissionSecretKey, + ) + if (!isValid) return + + const decryptionTasks = Object.keys( + encryptedPreviousSubmission.encryptedAttachments, + ).map(async (id) => { + const attachment = encryptedPreviousSubmission.encryptedAttachments[id] + let decryptedContent + try { + decryptedContent = await decryptAttachment( + attachment, + submissionSecretKey, + ) + } catch (e) { + console.error(e, 'failed to decrypt attachment', id) + toast({ + status: 'danger', + description: 'Failed to decrypt attachment', + }) + } + if (!decryptedContent) return + + decryptedAttachments[id] = decryptedContent + }) + await Promise.all(decryptionTasks) + setPreviousAttachments(decryptedAttachments) + } + + if (encryptedPreviousSubmission?.mrfVersion === 1) { + if (submissionSecretKey) decryptAttachments() + } else { + // Backward compatibility to retrieve attachments from the DB itself once + // the previous submission responses are decrypted. + if (previousSubmission) { + // Backward compatibility + const previousAttachments: Record = {} + Object.keys(previousSubmission.responses).forEach((id) => { + const response = previousSubmission.responses[id] + if (response.fieldType === BasicField.Attachment) { + previousAttachments[id] = Uint8Array.from( + //@ts-expect-error 'content' required for backward compatibility, but + // does not exist on AttachmentFieldResponseV3 in mrfVersion === 1 versions + response.answer.content.data, + ) + } + }) + setPreviousAttachments(previousAttachments) + } + } + }, [ + encryptedPreviousSubmission, + previousSubmission, + submissionSecretKey, + toast, + ]) + if ( previousSubmissionId && encryptedPreviousSubmission && !previousSubmission && !isSubmissionSecretKeyInvalid ) { - let submissionSecretKey = '' - try { - submissionSecretKey = decodeURIComponent(searchParams.get('key') ?? '') - } catch (e) { - console.log(e) - } - const isValid = isKeypairValid( encryptedPreviousSubmission.submissionPublicKey, submissionSecretKey, @@ -266,9 +338,6 @@ export const PublicFormProvider = ({ captchaType = CaptchaTypes.Recaptcha } - const { isNotFormId, toast, vfnToastIdRef, expiryInMs, ...commonFormValues } = - useCommonFormProvider(formId) - const isPaymentEnabled = data?.form.responseMode === FormResponseMode.Encrypt && data.form.payments_field.enabled @@ -763,6 +832,7 @@ export const PublicFormProvider = ({ setNumVisibleFields, encryptedPreviousSubmission, previousSubmission, + previousAttachments, setPreviousSubmission, ...commonFormValues, ...data, diff --git a/frontend/src/features/public-form/PublicFormService.ts b/frontend/src/features/public-form/PublicFormService.ts index defcebfc45..50aeaa73fa 100644 --- a/frontend/src/features/public-form/PublicFormService.ts +++ b/frontend/src/features/public-form/PublicFormService.ts @@ -43,7 +43,9 @@ import { createClearSubmissionWithVirusScanningFormDataV3, getAttachmentsMap, } from './utils/createSubmission' +import { convertEncryptedAttachmentToFileContent } from './utils/decryptSubmission' import { filterHiddenInputs } from './utils/filterHiddenInputs' +import { MultirespondentSubmissionDtoWithAttachments } from './types' export const PUBLIC_FORMS_ENDPOINT = '/forms' @@ -103,10 +105,27 @@ export const getMultirespondentSubmissionById = async ({ }: { formId: string submissionId: string -}): Promise => { +}): Promise => { return ApiService.get( `${PUBLIC_FORMS_ENDPOINT}/${formId}/submissions/${submissionId}`, - ).then(({ data }) => data) + ).then(async ({ data }) => { + const encryptedAttachments: MultirespondentSubmissionDtoWithAttachments['encryptedAttachments'] = + {} + const downloadTasks = Object.keys(data.attachmentMetadata).map( + async (id) => { + const url = data.attachmentMetadata[id] + const attachmentJson = await fetch(url).then((response) => + response.json(), + ) + encryptedAttachments[id] = + convertEncryptedAttachmentToFileContent(attachmentJson) + }, + ) + + await Promise.all(downloadTasks) + + return { ...data, encryptedAttachments } + }) } export type SubmitEmailFormArgs = { diff --git a/frontend/src/features/public-form/components/FormFields/FormFields.tsx b/frontend/src/features/public-form/components/FormFields/FormFields.tsx index 64faf38c0a..6c1f08b89b 100644 --- a/frontend/src/features/public-form/components/FormFields/FormFields.tsx +++ b/frontend/src/features/public-form/components/FormFields/FormFields.tsx @@ -38,6 +38,7 @@ import { VisibleFormFields } from './VisibleFormFields' export interface FormFieldsProps { previousResponses?: FieldResponsesV3 + previousAttachments?: Record formFields: FormFieldDto[] formLogics: LogicDto[] workflowStep?: FormWorkflowStepDto @@ -54,6 +55,7 @@ export type PrefillMap = { export const FormFields = ({ previousResponses, + previousAttachments, formFields, formLogics, workflowStep, @@ -106,9 +108,12 @@ export const FormFields = ({ case BasicField.Attachment: { const attachmentData = previousResponse.answer as AttachmentFieldResponseV3 - const fileData = attachmentData.content.data const fileName = attachmentData.answer - acc[field._id] = bufferToFile(fileData, fileName) + const fileData = previousAttachments?.[field._id] + if (fileData) { + acc[field._id] = bufferToFile(fileData, fileName) + } + break } default: @@ -140,7 +145,12 @@ export const FormFields = ({ } return acc }, {}) - }, [augmentedFormFields, previousResponses, fieldPrefillMap]) + }, [ + augmentedFormFields, + previousResponses, + fieldPrefillMap, + previousAttachments, + ]) // payment prefills - only for variable payments if (searchParams.has(PAYMENT_VARIABLE_INPUT_AMOUNT_FIELD_ID)) { diff --git a/frontend/src/features/public-form/components/FormFields/FormFieldsContainer.tsx b/frontend/src/features/public-form/components/FormFields/FormFieldsContainer.tsx index fa2834962e..c70c7956a9 100644 --- a/frontend/src/features/public-form/components/FormFields/FormFieldsContainer.tsx +++ b/frontend/src/features/public-form/components/FormFields/FormFieldsContainer.tsx @@ -19,6 +19,7 @@ export const FormFieldsContainer = (): JSX.Element | null => { submissionData, encryptedPreviousSubmission, previousSubmission, + previousAttachments, } = usePublicFormContext() const { workflowStep } = encryptedPreviousSubmission ?? {} @@ -42,6 +43,7 @@ export const FormFieldsContainer = (): JSX.Element | null => { return ( { isLoading, form, isAuthRequired, - previousSubmission, + previousSubmission?.responses, + previousAttachments, workflowStep, handleSubmitForm, ]) diff --git a/frontend/src/features/public-form/queries.ts b/frontend/src/features/public-form/queries.ts index 50de30a0cd..ea21b436c5 100644 --- a/frontend/src/features/public-form/queries.ts +++ b/frontend/src/features/public-form/queries.ts @@ -1,6 +1,5 @@ import { useQuery, UseQueryResult } from 'react-query' -import { MultirespondentSubmissionDto } from '~shared/types' import { PublicFormViewDto } from '~shared/types/form/form' import { ApiError } from '~typings/core' @@ -11,6 +10,7 @@ import { getMultirespondentSubmissionById, getPublicFormView, } from './PublicFormService' +import { MultirespondentSubmissionDtoWithAttachments } from './types' export const publicFormKeys = { // All keys map to either an array or function returning an array for @@ -45,7 +45,7 @@ export const useEncryptedSubmission = ( submissionId?: string, /** Extra override to determine whether query is enabled */ enabled = true, -): UseQueryResult => { +): UseQueryResult => { return useQuery( publicFormKeys.submission(formId, submissionId), () => diff --git a/frontend/src/features/public-form/types.ts b/frontend/src/features/public-form/types.ts new file mode 100644 index 0000000000..78535a1852 --- /dev/null +++ b/frontend/src/features/public-form/types.ts @@ -0,0 +1,8 @@ +import { EncryptedFileContent } from '@opengovsg/formsg-sdk/dist/types' + +import { MultirespondentSubmissionDto } from '~shared/types' + +export type MultirespondentSubmissionDtoWithAttachments = + MultirespondentSubmissionDto & { + encryptedAttachments: Record + } diff --git a/frontend/src/features/public-form/utils/createSubmission.ts b/frontend/src/features/public-form/utils/createSubmission.ts index 43a5f4ce21..952ca76dec 100644 --- a/frontend/src/features/public-form/utils/createSubmission.ts +++ b/frontend/src/features/public-form/utils/createSubmission.ts @@ -33,7 +33,7 @@ import { validateResponses } from './validateResponses' /** * @returns StorageModeSubmissionContentDto - * @throw Error if form inputs are invalid. + * @throws Error if form inputs are invalid. */ export const createEncryptedSubmissionData = async ({ formFields, diff --git a/frontend/src/features/public-form/utils/decryptSubmission.ts b/frontend/src/features/public-form/utils/decryptSubmission.ts index 113d413d7f..8fab9ff3b6 100644 --- a/frontend/src/features/public-form/utils/decryptSubmission.ts +++ b/frontend/src/features/public-form/utils/decryptSubmission.ts @@ -1,7 +1,20 @@ +import { + EncryptedAttachmentContent, + EncryptedFileContent, +} from '@opengovsg/formsg-sdk/dist/types' +import { decode as decodeBase64 } from '@stablelib/base64' + import { FieldResponsesV3, MultirespondentSubmissionDto } from '~shared/types' import formsgSdk from '~utils/formSdk' +/** + * Decrypts a submission using the secret key + * @param param0 + * @returns + * @throws Error('Encrypted submission undefined') + * @throws Error('Secret key undefined') + */ export const decryptSubmission = ({ submission, secretKey, @@ -32,3 +45,41 @@ export const decryptSubmission = ({ submissionSecretKey: secretKey, } } + +/** + * Decrypts an attachment using the secret key + * @param attachment + * @param secretKey + * @returns + * @throws Error('Encrypted submission undefined') + * @throws Error('Secret key undefined') + */ +export const decryptAttachment = async ( + attachment: EncryptedFileContent, + secretKey: string, +): Promise => { + if (!attachment) throw Error('Encrypted submission undefined') + if (!secretKey) throw Error('Secret key undefined') + + const decryptedContent = await formsgSdk.crypto.decryptFile( + secretKey, + attachment, + ) + + if (!decryptedContent) throw new Error('Could not decrypt the response') + + return decryptedContent +} + +/** + * Converts an encrypted attachment to encrypted file content + * @param encryptedAttachment The encrypted attachment + * @returns EncryptedFileContent The encrypted file content + */ +export const convertEncryptedAttachmentToFileContent = ( + encryptedAttachment: EncryptedAttachmentContent, +): EncryptedFileContent => ({ + submissionPublicKey: encryptedAttachment.encryptedFile.submissionPublicKey, + nonce: encryptedAttachment.encryptedFile.nonce, + binary: decodeBase64(encryptedAttachment.encryptedFile.binary), +}) diff --git a/frontend/src/utils/bufferToFile.ts b/frontend/src/utils/bufferToFile.ts index cc95174027..4141ad819d 100644 --- a/frontend/src/utils/bufferToFile.ts +++ b/frontend/src/utils/bufferToFile.ts @@ -4,9 +4,8 @@ * @param filename * @returns */ -const bufferToFile = (data: Iterable, filename: string): File => { - const bufferArray = Uint8Array.from(data) - const blob = new Blob([bufferArray]) +const bufferToFile = (data: ArrayBuffer, filename: string): File => { + const blob = new Blob([data]) const file = new File([blob], filename) return file diff --git a/shared/types/submission.ts b/shared/types/submission.ts index 9d06b5ea1e..c2ef8671b0 100644 --- a/shared/types/submission.ts +++ b/shared/types/submission.ts @@ -92,6 +92,7 @@ export const MultirespondentSubmissionBase = SubmissionBase.extend({ attachmentMetadata: z.map(z.string(), z.string()).optional(), version: z.number(), workflowStep: z.number(), + mrfVersion: z.number().optional(), }) export type MultirespondentSubmissionBase = z.infer< @@ -150,10 +151,11 @@ export type MultirespondentSubmissionDto = SubmissionDtoBase & { submissionPublicKey: string encryptedSubmissionSecretKey: string encryptedContent: string - //verified?: string attachmentMetadata: Record - version: number workflowStep: number + + version: number + mrfVersion: number } export type SubmissionDto = @@ -184,6 +186,7 @@ export const MultirespondentSubmissionStreamDto = encryptedSubmissionSecretKey: true, encryptedContent: true, version: true, + mrfVersion: true, }).extend({ attachmentMetadata: z.record(z.string()), _id: SubmissionId, diff --git a/src/app/models/submission.server.model.ts b/src/app/models/submission.server.model.ts index 49b8273f99..f8115cc4ce 100644 --- a/src/app/models/submission.server.model.ts +++ b/src/app/models/submission.server.model.ts @@ -498,6 +498,9 @@ export const MultirespondentSubmissionSchema = new Schema< type: Number, required: true, }, + mrfVersion: { + type: Number, + }, }) MultirespondentSubmissionSchema.statics.findSingleMetadata = function ( @@ -611,6 +614,7 @@ MultirespondentSubmissionSchema.statics.getSubmissionCursorByFormId = function ( attachmentMetadata: 1, created: 1, version: 1, + mrfVersion: 1, id: 1, }) .batchSize(2000) @@ -645,6 +649,7 @@ MultirespondentSubmissionSchema.statics.findEncryptedSubmissionById = function ( created: 1, version: 1, workflowStep: 1, + mrfVersion: 1, }) .exec() } diff --git a/src/app/modules/submission/multirespondent-submission/multirespondent-submission.controller.ts b/src/app/modules/submission/multirespondent-submission/multirespondent-submission.controller.ts index d44a88fc95..3efbe725c1 100644 --- a/src/app/modules/submission/multirespondent-submission/multirespondent-submission.controller.ts +++ b/src/app/modules/submission/multirespondent-submission/multirespondent-submission.controller.ts @@ -149,6 +149,7 @@ const submitMultirespondentForm = async ( encryptedContent, responseMetadata, version, + mrfVersion, } = encryptedPayload const submissionContent = { @@ -164,6 +165,7 @@ const submitMultirespondentForm = async ( attachmentMetadata, version, workflowStep: 0, + mrfVersion, } return _createSubmission({ @@ -382,6 +384,7 @@ const updateMultirespondentSubmission = async ( version, workflowStep, responses, + mrfVersion, } = encryptedPayload // Save Responses to Database @@ -419,6 +422,7 @@ const updateMultirespondentSubmission = async ( submission.version = version submission.workflowStep = workflowStep submission.attachmentMetadata = attachmentMetadata + submission.mrfVersion = mrfVersion try { await submission.save() diff --git a/src/app/modules/submission/multirespondent-submission/multirespondent-submission.middleware.ts b/src/app/modules/submission/multirespondent-submission/multirespondent-submission.middleware.ts index b489c977f7..e9ed32d15e 100644 --- a/src/app/modules/submission/multirespondent-submission/multirespondent-submission.middleware.ts +++ b/src/app/modules/submission/multirespondent-submission/multirespondent-submission.middleware.ts @@ -603,19 +603,19 @@ export const encryptSubmission = async ( } } - const encryptedAttachments = - await getEncryptedAttachmentsMapFromAttachmentsMap( - attachmentsMap, - formPublicKey, - req.body.version, - ) - const { encryptedContent, encryptedSubmissionSecretKey, submissionSecretKey, submissionPublicKey, - } = formsgSdk.cryptoV3.encrypt(responses, formPublicKey) + } = formsgSdk.cryptoV3.encrypt(strippedAttachmentResponses, formPublicKey) + + const encryptedAttachments = + await getEncryptedAttachmentsMapFromAttachmentsMap( + attachmentsMap, + submissionPublicKey, + req.body.version, + ) req.formsg.encryptedPayload = { attachments: encryptedAttachments, @@ -627,6 +627,13 @@ export const encryptSubmission = async ( version: req.body.version, workflowStep: req.body.workflowStep, responses, + /** + * MRF Version: 1 + * ==================== + * - Encrypted payload does not contain attachment contents + * - Encrypted Attachment now encrypted by mrf / submission Public Key instead of Form Public Key + */ + mrfVersion: 1, } return next() diff --git a/src/app/modules/submission/multirespondent-submission/multirespondent-submission.utils.ts b/src/app/modules/submission/multirespondent-submission/multirespondent-submission.utils.ts index d8d03facfe..ff0247d48e 100644 --- a/src/app/modules/submission/multirespondent-submission/multirespondent-submission.utils.ts +++ b/src/app/modules/submission/multirespondent-submission/multirespondent-submission.utils.ts @@ -13,7 +13,7 @@ import { MultirespondentSubmissionData } from '../../../../types' import { InvalidWorkflowTypeError } from '../submission.errors' /** - * Creates and returns a StorageModeSubmissionDto object from submissionData and + * Creates and returns a MultirespondentSubmissionDto object from submissionData and * attachment presigned urls. */ export const createMultirespondentSubmissionDto = ( @@ -37,6 +37,7 @@ export const createMultirespondentSubmissionDto = ( attachmentMetadata: attachmentPresignedUrls, version: submissionData.version, workflowStep: submissionData.workflowStep, + mrfVersion: submissionData.mrfVersion, } } diff --git a/src/types/api/multirespondent_submission.ts b/src/types/api/multirespondent_submission.ts index e2e35c9fe8..9c31a6a42d 100644 --- a/src/types/api/multirespondent_submission.ts +++ b/src/types/api/multirespondent_submission.ts @@ -37,4 +37,5 @@ export type MultirespondentSubmissionDto = { responseMetadata?: ResponseMetadata workflowStep: number responses: FieldResponsesV3 + mrfVersion: number } diff --git a/src/types/submission.ts b/src/types/submission.ts index 2ad8911653..0d3483c6af 100644 --- a/src/types/submission.ts +++ b/src/types/submission.ts @@ -91,6 +91,7 @@ export interface IMultirespondentSubmissionSchema form: any submissionType: SubmissionType.Multirespondent getWebhookView(): Promise + mrfVersion: number } // When retrieving from database, the attachmentMetadata type becomes an object @@ -117,6 +118,7 @@ export type MultirespondentSubmissionCursorData = Pick< | 'created' | 'id' | 'version' + | 'mrfVersion' > & { attachmentMetadata?: Record } & Document export type SubmissionCursorData = @@ -150,6 +152,7 @@ export type MultirespondentSubmissionData = { | 'created' | 'version' | 'workflowStep' + | 'mrfVersion' > & Document From 91a38bf30c4b91f764a7f33e1c3721a8329bb5cf Mon Sep 17 00:00:00 2001 From: Ken Date: Sun, 5 May 2024 23:19:13 +0800 Subject: [PATCH 5/5] chore: bump version to v6.119.0 --- CHANGELOG.md | 12 +++++++++--- frontend/package-lock.json | 4 ++-- frontend/package.json | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 5 files changed, 15 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 610345764e..67af3a680c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,14 +4,20 @@ All notable changes to this project will be documented in this file. Dates are d Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). -#### [v6.118.0](https://github.com/opengovsg/FormSG/compare/v6.118.0...v6.118.0) +#### [v6.119.0](https://github.com/opengovsg/FormSG/compare/v6.118.0...v6.119.0) -- fix(btn): use different growthbook api [`#7299`](https://github.com/opengovsg/FormSG/pull/7299) +- build: merge release v6.118 to develop [`#7306`](https://github.com/opengovsg/FormSG/pull/7306) +- fix(mrf): attachment v2 [`#7281`](https://github.com/opengovsg/FormSG/pull/7281) +- fix(deps): bump ejs from 3.1.8 to 3.1.10 [`#7304`](https://github.com/opengovsg/FormSG/pull/7304) +- fix(deps): bump type-fest from 4.18.0 to 4.18.1 in /shared [`#7303`](https://github.com/opengovsg/FormSG/pull/7303) +- fix(deps): bump zod from 3.23.4 to 3.23.5 in /shared [`#7301`](https://github.com/opengovsg/FormSG/pull/7301) +- chore: bump version to v6.118.0 [`#7300`](https://github.com/opengovsg/FormSG/pull/7300) #### [v6.118.0](https://github.com/opengovsg/FormSG/compare/v6.117.0...v6.118.0) > 30 April 2024 +- fix(btn): use different growthbook api [`#7299`](https://github.com/opengovsg/FormSG/pull/7299) - fix(payments): allow 0 cents [`#7298`](https://github.com/opengovsg/FormSG/pull/7298) - fix(deps): bump type-fest from 4.17.0 to 4.18.0 in /shared [`#7297`](https://github.com/opengovsg/FormSG/pull/7297) - fix: update chromium version [`#7294`](https://github.com/opengovsg/FormSG/pull/7294) @@ -23,7 +29,7 @@ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). - build: merge release v6.117.0 into develop [`#7284`](https://github.com/opengovsg/FormSG/pull/7284) - build: release v6.117.0 [`#7266`](https://github.com/opengovsg/FormSG/pull/7266) - fix(deps): bump zod from 3.23.0 to 3.23.4 in /shared [`#7283`](https://github.com/opengovsg/FormSG/pull/7283) -- chore: bump version to v6.118.0 [`9bcc6a0`](https://github.com/opengovsg/FormSG/commit/9bcc6a0140e3f10d6b8df4c1333fcf701ca24d10) +- chore: bump version to v6.118.0 [`6f415b1`](https://github.com/opengovsg/FormSG/commit/6f415b1be388afb78d7d7ae25e3c0cd779c9b6a4) #### [v6.117.0](https://github.com/opengovsg/FormSG/compare/v6.116.0...v6.117.0) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 6bdeecd22c..f04bf4aef6 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "form-frontend", - "version": "6.118.0", + "version": "6.119.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "form-frontend", - "version": "6.118.0", + "version": "6.119.0", "hasInstallScript": true, "dependencies": { "@chakra-ui/react": "^1.8.6", diff --git a/frontend/package.json b/frontend/package.json index f965aa41d5..101e6083e6 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "form-frontend", - "version": "6.118.0", + "version": "6.119.0", "homepage": ".", "private": true, "dependencies": { diff --git a/package-lock.json b/package-lock.json index 59236a9c74..c7f320a4aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "FormSG", - "version": "6.118.0", + "version": "6.119.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "FormSG", - "version": "6.118.0", + "version": "6.119.0", "hasInstallScript": true, "dependencies": { "@aws-sdk/client-cloudwatch-logs": "^3.536.0", diff --git a/package.json b/package.json index 4560d569d3..69ef7d9961 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "FormSG", "description": "Form Manager for Government", - "version": "6.118.0", + "version": "6.119.0", "homepage": "https://form.gov.sg", "authors": [ "FormSG "