Skip to content

Commit

Permalink
refactor: update crypto HOF to Crypto class (#36)
Browse files Browse the repository at this point in the history
* feat: add util/crypto.ts for helper methods used by new Crypto class

* feat: rewrite crypto.ts functions as Crypto class and use in init

* feat: update thrown error name in JSDoc of Webhook.authenticate

* test(Crypto): update tests to use new class based instantiation

* refactor(Crypto): change constructor to optional `publicSigningKey`

Instead of `publicKey`, `publicSigningKey` makes it more obvious of what the use of the key is for.
Making constructor optional also allows users of the library to encrypt and decrypt without having to provide a public signing key if they do not want to and still be able to decrypt content without verified fields.

Note that the default package initialisation still instantiates the library with a fallback key.

* refactor: allow optional constructor params

* test(Crypto): add tests for 100% coverage

* refactor: update import order

* test: remove misleading comment
  • Loading branch information
karrui committed Jun 11, 2020
1 parent 864d2f6 commit f0acd09
Show file tree
Hide file tree
Showing 6 changed files with 306 additions and 229 deletions.
133 changes: 90 additions & 43 deletions spec/crypto.spec.ts
Original file line number Diff line number Diff line change
@@ -1,49 +1,56 @@
import Crypto from '../src/crypto'
import { SIGNING_KEYS } from '../src/resource/signing-keys'
import formsgPackage from '../src/index'

import {
plaintext,
ciphertext,
formSecretKey,
formPublicKey,
} from './resources/crypto-data-20200322'

import { plaintextMultiLang } from './resources/crypto-data-20200604'

const formsg = formsgPackage({ mode: 'test' })
import { MissingPublicKeyError } from '../src/errors'

const INTERNAL_TEST_VERSION = 1

const encryptionPublicKey = SIGNING_KEYS.test.publicKey
const signingSecretKey = SIGNING_KEYS.test.secretKey
const testFileBuffer = new Uint8Array(Buffer.from('./resources/ogp.svg'))

describe('Crypto', function () {
const signingSecretKey = SIGNING_KEYS.test.secretKey
const testFileBuffer = new Uint8Array(Buffer.from('./resources/ogp.svg'))
const crypto = new Crypto({ publicSigningKey: encryptionPublicKey })

const mockVerifiedContent = {
uinFin: 'S12345679Z',
somethingElse: 99,
}

it('should generate a keypair', () => {
const keypair = formsg.crypto.generate()
expect(Object.keys(keypair)).toContain('secretKey')
expect(Object.keys(keypair)).toContain('publicKey')
const keypair = crypto.generate()
expect(keypair).toHaveProperty('secretKey')
expect(keypair).toHaveProperty('publicKey')
})

it('should generate a keypair that is valid', () => {
const { publicKey, secretKey } = formsg.crypto.generate()
expect(formsg.crypto.valid(publicKey, secretKey)).toBe(true)
const { publicKey, secretKey } = crypto.generate()
expect(crypto.valid(publicKey, secretKey)).toBe(true)
})

it('should validate an existing keypair', () => {
expect(formsg.crypto.valid(formPublicKey, formSecretKey)).toBe(true)
expect(crypto.valid(formPublicKey, formSecretKey)).toBe(true)
})

it('should invalidate unassociated keypairs', () => {
// Act
const { secretKey } = formsg.crypto.generate()
const { publicKey } = formsg.crypto.generate()
const { secretKey } = crypto.generate()
const { publicKey } = crypto.generate()

// Assert
expect(formsg.crypto.valid(publicKey, secretKey)).toBe(false)
expect(crypto.valid(publicKey, secretKey)).toBe(false)
})

it('should decrypt the submission ciphertext from 2020-03-22 successfully', () => {
// Act
const decrypted = formsg.crypto.decrypt(formSecretKey, {
const decrypted = crypto.decrypt(formSecretKey, {
encryptedContent: ciphertext,
version: INTERNAL_TEST_VERSION,
})
Expand All @@ -54,7 +61,7 @@ describe('Crypto', function () {

it('should return null on unsuccessful decryption', () => {
expect(
formsg.crypto.decrypt('random', {
crypto.decrypt('random', {
encryptedContent: ciphertext,
version: INTERNAL_TEST_VERSION,
})
Expand All @@ -63,15 +70,15 @@ describe('Crypto', function () {

it('should return null when successfully decrypted content does not fit FormField type shape', () => {
// Arrange
const { publicKey, secretKey } = formsg.crypto.generate()
const { publicKey, secretKey } = crypto.generate()
const malformedContent = 'just a string, not an object with FormField shape'
const malformedEncrypt = formsg.crypto.encrypt(malformedContent, publicKey)
const malformedEncrypt = crypto.encrypt(malformedContent, publicKey)

// Assert
// Using correct secret key, but the decrypted object should not fit the
// expected shape and thus return null.
expect(
formsg.crypto.decrypt(secretKey, {
crypto.decrypt(secretKey, {
encryptedContent: malformedEncrypt,
version: INTERNAL_TEST_VERSION,
})
Expand All @@ -80,11 +87,11 @@ describe('Crypto', function () {

it('should be able to encrypt and decrypt submissions from 2020-03-22 end-to-end successfully', () => {
// Arrange
const { publicKey, secretKey } = formsg.crypto.generate()
const { publicKey, secretKey } = crypto.generate()

// Act
const ciphertext = formsg.crypto.encrypt(plaintext, publicKey)
const decrypted = formsg.crypto.decrypt(secretKey, {
const ciphertext = crypto.encrypt(plaintext, publicKey)
const decrypted = crypto.decrypt(secretKey, {
encryptedContent: ciphertext,
version: INTERNAL_TEST_VERSION,
})
Expand All @@ -94,11 +101,11 @@ describe('Crypto', function () {

it('should be able to encrypt and decrypt multi-language submission from 2020-06-04 end-to-end successfully', () => {
// Arrange
const { publicKey, secretKey } = formsg.crypto.generate()
const { publicKey, secretKey } = crypto.generate()

// Act
const ciphertext = formsg.crypto.encrypt(plaintextMultiLang, publicKey)
const decrypted = formsg.crypto.decrypt(secretKey, {
const ciphertext = crypto.encrypt(plaintextMultiLang, publicKey)
const decrypted = crypto.decrypt(secretKey, {
encryptedContent: ciphertext,
version: INTERNAL_TEST_VERSION,
})
Expand All @@ -108,12 +115,12 @@ describe('Crypto', function () {

it('should be able to encrypt submissions without signing if signingPrivateKey is missing', () => {
// Arrange
const { publicKey, secretKey } = formsg.crypto.generate()
const { publicKey, secretKey } = crypto.generate()

// Act
// Signing key (last parameter) is omitted.
const ciphertext = formsg.crypto.encrypt(plaintext, publicKey)
const decrypted = formsg.crypto.decrypt(secretKey, {
const ciphertext = crypto.encrypt(plaintext, publicKey)
const decrypted = crypto.decrypt(secretKey, {
encryptedContent: ciphertext,
version: INTERNAL_TEST_VERSION,
})
Expand All @@ -124,23 +131,19 @@ describe('Crypto', function () {

it('should be able to encrypt and sign submissions if signingPrivateKey is given', () => {
// Arrange
const { publicKey, secretKey } = formsg.crypto.generate()
const mockVerifiedContent = {
uinFin: 'S12345679Z',
somethingElse: 99,
}
const { publicKey, secretKey } = crypto.generate()

// Act
// Encrypt content that is not signed.
const ciphertext = formsg.crypto.encrypt(plaintext, publicKey)
const ciphertext = crypto.encrypt(plaintext, publicKey)
// Sign and encrypt the desired content.
const signedAndEncryptedText = formsg.crypto.encrypt(
const signedAndEncryptedText = crypto.encrypt(
mockVerifiedContent,
publicKey,
signingSecretKey
)
// Decrypt encrypted content along with our signed+encrypted content.
const decrypted = formsg.crypto.decrypt(secretKey, {
const decrypted = crypto.decrypt(secretKey, {
encryptedContent: ciphertext,
verifiedContent: signedAndEncryptedText,
version: INTERNAL_TEST_VERSION,
Expand All @@ -153,17 +156,17 @@ describe('Crypto', function () {

it('should be able to encrypt and decrypt files end-to-end', async () => {
// Arrange
const { publicKey, secretKey } = formsg.crypto.generate()
const { publicKey, secretKey } = crypto.generate()

// Act
// Encrypt
const encrypted = await formsg.crypto.encryptFile(testFileBuffer, publicKey)
const encrypted = await crypto.encryptFile(testFileBuffer, publicKey)
expect(encrypted).toHaveProperty('submissionPublicKey')
expect(encrypted).toHaveProperty('nonce')
expect(encrypted).toHaveProperty('binary')

// Decrypt
const decrypted = await formsg.crypto.decryptFile(secretKey, encrypted)
const decrypted = await crypto.decryptFile(secretKey, encrypted)

if (!decrypted) {
throw new Error('File should be able to decrypt successfully.')
Expand All @@ -174,14 +177,58 @@ describe('Crypto', function () {
})

it('should return null if file could not be decrypted', async () => {
const { publicKey, secretKey } = formsg.crypto.generate()
const { publicKey, secretKey } = crypto.generate()

const encrypted = await formsg.crypto.encryptFile(testFileBuffer, publicKey)
const encrypted = await crypto.encryptFile(testFileBuffer, publicKey)
// Rewrite binary with invalid Uint8Array.
encrypted.binary = new Uint8Array([1, 2])

const decrypted = await formsg.crypto.decryptFile(secretKey, encrypted)
const decrypted = await crypto.decryptFile(secretKey, encrypted)

expect(decrypted).toBeNull()
})

it('should throw error if class was not instantiated with a public signing key while verifying decrypted content ', () => {
// Arrange
const cryptoNoKey = new Crypto()
const { publicKey, secretKey } = cryptoNoKey.generate()

// Act
// Encrypt content that is not signed.
const ciphertext = cryptoNoKey.encrypt(plaintext, publicKey)
// Sign and encrypt the desired content.
const signedAndEncryptedText = cryptoNoKey.encrypt(
mockVerifiedContent,
publicKey,
signingSecretKey
)

// Assert
// Attempt to decrypt encrypted content along with our signed+encrypted
// content should throw an error
expect(() =>
cryptoNoKey.decrypt(secretKey, {
encryptedContent: ciphertext,
verifiedContent: signedAndEncryptedText,
version: INTERNAL_TEST_VERSION,
})
).toThrow(MissingPublicKeyError)
})

it('should return null if decrypting encrypted verified content failed', () => {
// Arrange
const { publicKey, secretKey } = crypto.generate()
// Encrypt content that is not signed.
const ciphertext = crypto.encrypt(plaintext, publicKey)
// Create rubbish verified content
const rubbishVerifiedContent = 'abcdefg'

// Act + Assert
const decryptResult = crypto.decrypt(secretKey, {
encryptedContent: ciphertext,
verifiedContent: rubbishVerifiedContent,
version: INTERNAL_TEST_VERSION,
})
expect(decryptResult).toBeNull()
})
})

0 comments on commit f0acd09

Please sign in to comment.