Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions spec/util.spec.ts
Original file line number Diff line number Diff line change
@@ -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)
})
})
})
52 changes: 28 additions & 24 deletions src/crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -227,40 +229,42 @@ export default class Crypto {
}
})

const downloadPromises: Array<Promise<void>> = []
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<EncryptedAttachmentContent>(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
}

Expand Down
8 changes: 8 additions & 0 deletions src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

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

out of curiosity, why is this line necessary?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

it isn't! i actually left it in because i thought it was convention @__@ i'll remove it!

Copy link
Contributor Author

Choose a reason for hiding this comment

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

i went to investigate more and it was q enlightening for me! the reason why this is done is because it's an error. when js (and by extension, ts) throws an error, the error printed to the output is done using error.name + ' ' + error.message - this means that when the error here is thrown, because it's missing the name property, it will default to Error (as it calls super(message)). While the argument might be that it's guaranteed to never be thrown, i think this is a relatively small change that helps in the future so i'd just leave it in!

}
}

export {
MissingSecretKeyError,
MissingPublicKeyError,
WebhookAuthenticateError,
AttachmentDecryptionError,
}
33 changes: 32 additions & 1 deletion src/util/crypto.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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<string, string>
): 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),
})