Skip to content

Commit add008c

Browse files
authored
feat: add verified content to v3 (#121)
* add verifiedContent decryption support for cryptoV3 * move decryption of verifiedContent to decryptFromSubmissionkey; use submissionKey * remove unused variables * bumped version to 0.14.0 to match latest * fixed tests * added to tests * replace coveralls with coveralls-next * resolved tests * revert coveralls to use v2 * rethrow missingPublicKeyError * add missing } * added comment to explain v1 vs v3 decryption differences * removed coveralls package * updated explanataion for v1 vs v3 content
1 parent 76664dd commit add008c

File tree

8 files changed

+8226
-12170
lines changed

8 files changed

+8226
-12170
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ jobs:
4747
- run: npm run test-ci
4848
env:
4949
NODE_OPTIONS: '--max-old-space-size=8192'
50-
- name: Submit test coverage to Coveralls
51-
uses: coverallsapp/github-action@v1.1.2
50+
- name: Submit coverage to Coveralls
51+
uses: coverallsapp/github-action@v2
5252
with:
5353
github-token: ${{ secrets.GITHUB_TOKEN }}

package-lock.json

Lines changed: 8119 additions & 12150 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@opengovsg/formsg-sdk",
3-
"version": "0.13.0",
3+
"version": "0.14.0",
44
"repository": {
55
"type": "git",
66
"url": "https://github.com/opengovsg/formsg-javascript-sdk.git"
@@ -36,7 +36,6 @@
3636
"@types/node": "^18.18.9",
3737
"@typescript-eslint/eslint-plugin": "^4.25.0",
3838
"auto-changelog": "^2.4.0",
39-
"coveralls": "^3.1.1",
4039
"eslint-config-prettier": "^8.3.0",
4140
"eslint-plugin-import": "^2.23.3",
4241
"eslint-plugin-jest": "^24.3.6",

spec/crypto-v3.spec.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,27 @@ import {
44
ciphertext,
55
formPublicKey,
66
formSecretKey,
7-
submissionSecretKey
7+
submissionSecretKey,
8+
plainVerifiedText
89
} from './resources/crypto-v3-data-20231207'
910
import CryptoV3 from '../src/crypto-v3'
11+
import Crypto from '../src/crypto'
12+
import { SIGNING_KEYS } from '../src/resource/signing-keys'
1013

1114
const INTERNAL_TEST_VERSION = 3
1215

1316
const testFileBuffer = new Uint8Array(Buffer.from('./resources/ogp.svg'))
1417

18+
const encryptionPublicKey = SIGNING_KEYS.test.publicKey
19+
const signingSecretKey = SIGNING_KEYS.test.secretKey
20+
1521
jest.mock('axios', () => mockAxios)
1622

1723
describe('CryptoV3', function () {
1824
afterEach(() => mockAxios.reset())
1925

20-
const crypto = new CryptoV3()
26+
const crypto = new CryptoV3({ signingPublicKey: encryptionPublicKey })
27+
const cryptoV1 = new Crypto({ signingPublicKey: encryptionPublicKey })
2128

2229
it('should generate a keypair', () => {
2330
const keypair = crypto.generate()
@@ -126,4 +133,22 @@ describe('CryptoV3', function () {
126133

127134
expect(decrypted).toBeNull()
128135
})
136+
137+
it('should be able to encrypt and decrypt submissions with verifiedContent from 2023-12-07 end-to-end successfully from the form private key', () => {
138+
// Arrange
139+
const { publicKey, secretKey } = crypto.generate()
140+
141+
// Act
142+
const ciphertext = crypto.encrypt(plaintext, publicKey)
143+
const verifiedText = cryptoV1.encrypt(plainVerifiedText, ciphertext.submissionPublicKey, signingSecretKey)
144+
const decrypted = crypto.decrypt(secretKey, {
145+
...ciphertext,
146+
verifiedContent: verifiedText,
147+
version: INTERNAL_TEST_VERSION,
148+
})
149+
// Assert
150+
expect(decrypted).toHaveProperty('responses', plaintext)
151+
expect(decrypted).toHaveProperty('verified', plainVerifiedText)
152+
153+
})
129154
})

spec/resources/crypto-v3-data-20231207.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ const plaintext = {
3030
}
3131
}
3232

33+
const plainVerifiedText = { 'uinFin (Step 1)': 'S9912370B' }
34+
3335
const ciphertext = {
3436
encryptedContent:
3537
'yUW5li4+IA9q2/n3ZS+5+wrXQ8mKGrFJ1KW9Kf/eRzc=;PgZE8+y8rBvssnqLnqjnnqHDW6PngYKK:eIEuOUQjf1YkQIulZ7bCKXIl6wByg644Ulk/LjhefmLzhkVmXbTxBJVKVG6YgV0ZMcG4JPUuQ+WOW+N1/AOyL/8DJqclX74kG6s0DNXIJixkqNZCnfZapulerR9XXKSfwBjpo1nK25KCg32F/ey2HypPcluGV19hWwgj80mlms7Ya7x1X5wcdttlGrzGEnNH2VEPXjzJZHqiV1TWoQGwxSZ753fpkHUkBeKFA1UkMHS5XYnWyYD48JpfpOAz0L2ti6RHQnQLSKUHscYVfAZt5OyUGqPFmhm2ulWdycNVp8HayQrpqeY8cdu8QsmZRdNCMfMFLahZCm6xKS+8GUrJWgJr64yaZpkxQS45uPb9zxC+G/u4FZhS/YsrjDTuIIwMGS0+qsNr4075yemFFAQHIpbhWZ9QlYrNq2TAolrVezeAw3AQ/nr4sz60dvqRahcse9x8oMxB7jA55OuxH5uk6PcCIAmEi+njr6Lgbcn2mtPMyk7kGcwjNzCL57b51RxJVi0ZqNXrS0FFepvzCK3IOEqKqrKGGK0qGqF4MFsH2wdq4RFkXjLMZk4u9ZWjIRjc',
@@ -44,6 +46,7 @@ const submissionSecretKey = 'bIyKphcx5hiuBaJ4q5cwnXaFNY9Ofe5NQBqTEzf3zYA='
4446

4547
export {
4648
plaintext,
49+
plainVerifiedText,
4750
ciphertext,
4851
formPublicKey,
4952
formSecretKey,

src/crypto-v3.ts

Lines changed: 71 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,15 @@ import {
55
encodeUTF8,
66
} from 'tweetnacl-util'
77

8-
import { decryptContent, encryptMessage, generateKeypair } from './util/crypto'
8+
import {
9+
decryptContent,
10+
encryptMessage,
11+
generateKeypair,
12+
verifySignedMessage,
13+
} from './util/crypto'
914
import { determineIsFormFieldsV3 } from './util/validate'
1015
import CryptoBase from './crypto-base'
16+
import { MissingPublicKeyError } from './errors'
1117
import {
1218
DecryptedContentV3,
1319
DecryptParams,
@@ -17,8 +23,11 @@ import {
1723
} from './types'
1824

1925
export default class CryptoV3 extends CryptoBase {
20-
constructor() {
26+
signingPublicKey?: string
27+
28+
constructor({ signingPublicKey }: { signingPublicKey?: string } = {}) {
2129
super()
30+
this.signingPublicKey = signingPublicKey
2231
}
2332

2433
/**
@@ -62,7 +71,7 @@ export default class CryptoV3 extends CryptoBase {
6271
decryptParams: DecryptParams
6372
): DecryptedContentV3 | null => {
6473
try {
65-
const { encryptedContent } = decryptParams
74+
const { encryptedContent, verifiedContent } = decryptParams
6675

6776
// Do not return the transformed object in `_decrypt` function as a signed
6877
// object is not encoded in UTF8 and is encoded in Base-64 instead.
@@ -85,8 +94,47 @@ export default class CryptoV3 extends CryptoBase {
8594
responses: decryptedObject as FormFieldsV3,
8695
}
8796

97+
/**
98+
* Note on verifiedContent decryption for cryptoV3:
99+
* Although decryption is supported, verifiedContent encryption is not supported
100+
* in cryptoV3 encrypt.
101+
* This is to keep the encryption of verifiedContent and encryptedContent similar to storage mode - where
102+
* verifiedContent and encryptedContent are defined and encrypted separately.
103+
*/
104+
// decrypt verifiedContent if it exists
105+
if (verifiedContent) {
106+
if (!this.signingPublicKey) {
107+
throw new MissingPublicKeyError(
108+
'Public signing key must be provided when instantiating the Crypto class in order to verify verified content'
109+
)
110+
}
111+
112+
const decryptedVerifiedContent = decryptContent(
113+
submissionSecretKey,
114+
verifiedContent
115+
)
116+
117+
if (!decryptedVerifiedContent) {
118+
// Returns null if decrypting verified content failed.
119+
throw new Error('Failed to decrypt verified content')
120+
}
121+
122+
const decryptedVerifiedObject = verifySignedMessage(
123+
decryptedVerifiedContent,
124+
this.signingPublicKey
125+
)
126+
127+
returnedObject.verified = decryptedVerifiedObject
128+
}
129+
88130
return returnedObject
89131
} catch (err) {
132+
// Should only throw if MissingPublicKeyError.
133+
// This library should be able to be used to encrypt and decrypt content
134+
// if the content does not contain verified fields.
135+
if (err instanceof MissingPublicKeyError) {
136+
throw err
137+
}
90138
return null
91139
}
92140
}
@@ -99,24 +147,34 @@ export default class CryptoV3 extends CryptoBase {
99147
* @param decryptParams.encryptedSubmissionSecretKey The encrypted submission secret key encoded with base-64.
100148
* @param decryptParams.version The version of the payload. Used to determine the decryption process to decrypt the content with.
101149
* @returns The decrypted content if successful. Else, null will be returned.
150+
* @throws {MissingPublicKeyError} if a public key is not provided when instantiating this class and is needed for verifying signed content.
102151
*/
103152
decrypt = (
104153
formSecretKey: string,
105154
decryptParams: DecryptParamsV3
106155
): DecryptedContentV3 | null => {
107-
const { encryptedSubmissionSecretKey, ...rest } = decryptParams
156+
try {
157+
const { encryptedSubmissionSecretKey, ...rest } = decryptParams
108158

109-
const submissionSecretKey = decryptContent(
110-
formSecretKey,
111-
encryptedSubmissionSecretKey
112-
)
159+
const submissionSecretKey = decryptContent(
160+
formSecretKey,
161+
encryptedSubmissionSecretKey
162+
)
113163

114-
if (submissionSecretKey === null) return null
164+
if (submissionSecretKey === null) return null
115165

116-
return this.decryptFromSubmissionKey(
117-
encodeBase64(submissionSecretKey),
118-
rest
119-
)
166+
return this.decryptFromSubmissionKey(
167+
encodeBase64(submissionSecretKey),
168+
rest
169+
)
170+
171+
} catch (err) {
172+
if (err instanceof MissingPublicKeyError) {
173+
// rethrow to let the caller decide how to handle missing signing key
174+
throw err
175+
}
176+
return null
177+
}
120178
}
121179

122180
/**

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export = function (config: PackageInitParams = {}) {
3131
secretKey: webhookSecretKey,
3232
}),
3333
crypto: new Crypto({ signingPublicKey }),
34-
cryptoV3: new CryptoV3(),
34+
cryptoV3: new CryptoV3({ signingPublicKey }),
3535
verification: new Verification({
3636
publicKey: verificationPublicKey,
3737
secretKey: verificationOptions?.secretKey,

src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ export interface DecryptParams {
8282
export interface DecryptParamsV3 {
8383
encryptedContent: EncryptedContent
8484
encryptedSubmissionSecretKey: EncryptedContent
85+
verifiedContent?: EncryptedContent
8586
version: number
8687
}
8788

@@ -93,6 +94,7 @@ export type DecryptedContent = {
9394
export type DecryptedContentV3 = {
9495
submissionSecretKey: string
9596
responses: FormFieldsV3
97+
verified?: Record<string, any>
9698
}
9799

98100
export type DecryptedFile = {

0 commit comments

Comments
 (0)