diff --git a/spec/util.spec.ts b/spec/util.spec.ts new file mode 100644 index 0000000..34636e0 --- /dev/null +++ b/spec/util.spec.ts @@ -0,0 +1,34 @@ +import { areAttachmentFieldIdsValid } from '../src/util/crypto' +describe('utils', () => { + describe('areAttachmentFieldIdsValid', () => { + it('should return true when all the fieldIds are within the filenames', () => { + // Arrange + const MOCK_FILENAMES = { + mock: 'file', + alsomock: 'file2', + } + const MOCK_FIELD_IDS = Object.keys(MOCK_FILENAMES) + + // Act + const actual = areAttachmentFieldIdsValid(MOCK_FIELD_IDS, MOCK_FILENAMES) + + // Assert + expect(actual).toBe(true) + }) + + it('should return false when some fieldIds are not within the filenames', () => { + // Arrange + const MOCK_FILENAMES = { + mock: 'file', + alsomock: 'file2', + } + const MOCK_FIELD_IDS = Object.keys(MOCK_FILENAMES).concat('missingField') + + // Act + const actual = areAttachmentFieldIdsValid(MOCK_FIELD_IDS, MOCK_FILENAMES) + + // Assert + expect(actual).toBe(false) + }) + }) +}) diff --git a/src/crypto.ts b/src/crypto.ts index 00955e9..7b0d4dc 100644 --- a/src/crypto.ts +++ b/src/crypto.ts @@ -12,9 +12,11 @@ import { encryptMessage, generateKeypair, verifySignedMessage, + areAttachmentFieldIdsValid, + convertEncryptedAttachmentToFileContent, } from './util/crypto' import { determineIsFormFields } from './util/validate' -import { MissingPublicKeyError } from './errors' +import { MissingPublicKeyError, AttachmentDecryptionError } from './errors' import { DecryptedAttachments, DecryptedContent, @@ -227,40 +229,42 @@ export default class Crypto { } }) - const downloadPromises: Array> = [] - for (const fieldId in attachmentRecords) { - // Original name for the file is not found - if (filenames[fieldId] === undefined) return null + const fieldIds = Object.keys(attachmentRecords) + // Check if all fieldIds are within filenames + if (!areAttachmentFieldIdsValid(fieldIds, filenames)) { + return null + } - downloadPromises.push( + const downloadPromises = fieldIds.map((fieldId) => { + return ( axios - .get(attachmentRecords[fieldId], { responseType: 'json' }) - .then((downloadResponse) => { - const encryptedAttachment: EncryptedAttachmentContent = - downloadResponse.data - const encryptedFile: EncryptedFileContent = { - submissionPublicKey: - encryptedAttachment.encryptedFile.submissionPublicKey, - nonce: encryptedAttachment.encryptedFile.nonce, - binary: decodeBase64(encryptedAttachment.encryptedFile.binary), - } - + // Retrieve all the attachments as JSON + .get(attachmentRecords[fieldId], { + responseType: 'json', + }) + // Decrypt all the attachments + .then(({ data: downloadResponse }) => { + const encryptedFile = + convertEncryptedAttachmentToFileContent(downloadResponse) return this.decryptFile(formSecretKey, encryptedFile) }) .then((decryptedFile) => { - if (decryptedFile === null) - throw new Error('Attachment decryption failed') - decryptedRecords[fieldId] = { - filename: filenames[fieldId], - content: decryptedFile, + // Check if the file exists and set the filename accordingly; otherwise, throw an error + if (decryptedFile) { + decryptedRecords[fieldId] = { + filename: filenames[fieldId], + content: decryptedFile, + } + } else { + throw new AttachmentDecryptionError() } }) ) - } + }) try { await Promise.all(downloadPromises) - } catch (e) { + } catch { return null } diff --git a/src/errors.ts b/src/errors.ts index 518c65c..69632df 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -33,8 +33,16 @@ class WebhookAuthenticateError extends Error { } } +class AttachmentDecryptionError extends Error { + constructor(message = 'Attachment decryption with the given nonce failed.') { + super(message) + this.name = this.constructor.name + } +} + export { MissingSecretKeyError, MissingPublicKeyError, WebhookAuthenticateError, + AttachmentDecryptionError, } diff --git a/src/util/crypto.ts b/src/util/crypto.ts index 21d6346..a569408 100644 --- a/src/util/crypto.ts +++ b/src/util/crypto.ts @@ -1,7 +1,12 @@ import nacl from 'tweetnacl' import { decodeBase64, encodeBase64, encodeUTF8 } from 'tweetnacl-util' -import { EncryptedContent, Keypair } from '../types' +import { + Keypair, + EncryptedContent, + EncryptedAttachmentContent, + EncryptedFileContent, +} from '../types' /** * Helper method to generate a new keypair for encryption. @@ -79,3 +84,29 @@ export const verifySignedMessage = ( throw new Error('Failed to open signed message with given public key') return JSON.parse(encodeUTF8(openedMessage)) } + +/** + * Helper method to check if all the field IDs given are within the filenames + * @param fieldIds the list of fieldIds to check + * @param filenames the filenames that should contain the fields + * @returns boolean indicating whether the fields are valid + */ +export const areAttachmentFieldIdsValid = ( + fieldIds: string[], + filenames: Record +): boolean => { + return fieldIds.every((fieldId) => filenames[fieldId]) +} + +/** + * 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), +})