Skip to content

Commit

Permalink
Merge pull request #7309 from opengovsg/release_v6.119.0
Browse files Browse the repository at this point in the history
* 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](colinhacks/zod@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] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* 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](sindresorhus/type-fest@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] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* 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](mde/ejs@v3.1.8...v3.1.10)

---
updated-dependencies:
- dependency-name: ejs
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* 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 <justynoh@gmail.com>

* chore: bump version to v6.119.0

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Justyn Oh <justynoh@gmail.com>
  • Loading branch information
3 people committed May 6, 2024
2 parents 69d73d2 + 91a38bf commit 598c483
Show file tree
Hide file tree
Showing 27 changed files with 305 additions and 75 deletions.
12 changes: 9 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)

Expand Down
4 changes: 2 additions & 2 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "form-frontend",
"version": "6.118.0",
"version": "6.119.0",
"homepage": ".",
"private": true,
"dependencies": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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, {
Expand Down Expand Up @@ -119,6 +119,7 @@ export const getDecryptedSubmissionById = async ({
decryptedContent,
)
submissionSecretKey = decryptedContent.submissionSecretKey
mrfVersion = encryptedSubmission.mrfVersion
break
}
}
Expand All @@ -138,6 +139,7 @@ export const getDecryptedSubmissionById = async ({
? encryptedSubmission.payment
: undefined,
responses,
mrfVersion,
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export interface DecryptedRowBaseProps {
row: AugmentedDecryptedResponse
}
type DecryptedRowProps = DecryptedRowBaseProps & {
secretKey: string
attachmentDecryptionKey: string
}

const DecryptedQuestionLabel = ({ row }: DecryptedRowBaseProps) => {
Expand Down Expand Up @@ -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 (
<Stack>
Expand Down Expand Up @@ -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 <DecryptedHeaderRow row={row} />
case BasicField.Attachment:
return <DecryptedAttachmentRow row={row} secretKey={secretKey} />
return (
<DecryptedAttachmentRow
row={row}
attachmentDecryptionKey={attachmentDecryptionKey}
/>
)
case BasicField.Table:
return <DecryptedTableRow row={row} />
default:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) => {
Expand All @@ -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 (
<SecretKeyVerification
heroSvg={<FormActivationSvg />}
Expand Down Expand Up @@ -192,7 +202,11 @@ export const IndividualResponsePage = (): JSX.Element => {
<>
<Stack spacing="1.5rem" divider={<StackDivider />}>
{data?.responses.map((r, idx) => (
<DecryptedRow row={r} secretKey={secretKey} key={idx} />
<DecryptedRow
row={r}
attachmentDecryptionKey={attachmentDecryptionKey}
key={idx}
/>
))}
<Box />
</Stack>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ async function decryptIntoCsv(data: LineData): Promise<MaterializedCsvRecord> {
: undefined,
)
try {
let decryptedSubmission
let decryptedSubmission, submissionSecretKey
switch (submission.submissionType) {
case SubmissionType.Encrypt: {
const decryptedObject = formsgSdk.crypto.decrypt(secretKey, {
Expand All @@ -125,6 +125,7 @@ async function decryptIntoCsv(data: LineData): Promise<MaterializedCsvRecord> {
if (!decryptedObject) {
throw new Error('Invalid decryption for multirespondent response')
}
submissionSecretKey = decryptedObject.submissionSecretKey
decryptedSubmission = await processDecryptedContentV3(
submission.form_fields,
decryptedObject,
Expand All @@ -150,6 +151,17 @@ async function decryptIntoCsv(data: LineData): Promise<MaterializedCsvRecord> {
}

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) => {
Expand All @@ -170,7 +182,7 @@ async function decryptIntoCsv(data: LineData): Promise<MaterializedCsvRecord> {
downloadBlob = await queue.add(() =>
downloadAndDecryptAttachmentsAsZip(
attachmentDownloadUrls,
secretKey,
attachmentDecryptionKey,
),
)
csvRecord.setStatus(
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/features/public-form/PublicFormContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@ export interface PublicFormContextProps

encryptedPreviousSubmission?: MultirespondentSubmissionDto
previousSubmission?: ReturnType<typeof decryptSubmission>
setPreviousSubmission: (
previousAttachments?: Record<string, ArrayBuffer>
setPreviousSubmission?: (
previousSubmission: ReturnType<typeof decryptSubmission>,
) => void
}
Expand Down
92 changes: 81 additions & 11 deletions frontend/src/features/public-form/PublicFormProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -140,6 +140,9 @@ export const PublicFormProvider = ({
/* enabled= */ !submissionData,
)

const { isNotFormId, toast, vfnToastIdRef, expiryInMs, ...commonFormValues } =
useCommonFormProvider(formId)

const {
data: encryptedPreviousSubmission,
isLoading: isSubmissionLoading,
Expand All @@ -159,21 +162,90 @@ export const PublicFormProvider = ({
const [isSubmissionSecretKeyInvalid, setIsSubmissionSecretKeyInvalid] =
useState(false)

const [previousAttachments, setPreviousAttachments] = useState<
Record<string, ArrayBuffer>
>({})

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<string, Uint8Array> = {}
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<string, ArrayBuffer> = {}
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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -763,6 +832,7 @@ export const PublicFormProvider = ({
setNumVisibleFields,
encryptedPreviousSubmission,
previousSubmission,
previousAttachments,
setPreviousSubmission,
...commonFormValues,
...data,
Expand Down
Loading

0 comments on commit 598c483

Please sign in to comment.