@opengovsg/formsg-sdk 0.8.4-beta.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.eslintrc ADDED
@@ -0,0 +1,75 @@
1
+ {
2
+ "env": {
3
+ "commonjs": true,
4
+ "es6": true,
5
+ "node": true
6
+ },
7
+ "extends": ["eslint:recommended", "plugin:prettier/recommended"],
8
+ "parserOptions": {
9
+ "ecmaVersion": 2018
10
+ },
11
+ "overrides": [
12
+ {
13
+ "files": ["*.ts"],
14
+ "parser": "@typescript-eslint/parser",
15
+ "parserOptions": {
16
+ "sourceType": "module",
17
+ "ecmaFeatures": {
18
+ "modules": true
19
+ },
20
+ "project": "tsconfig.json"
21
+ },
22
+ "plugins": [
23
+ "@typescript-eslint",
24
+ "import",
25
+ "simple-import-sort",
26
+ "typesafe"
27
+ ],
28
+ "extends": ["plugin:@typescript-eslint/recommended"],
29
+ "rules": {
30
+ // Rules for auto sort of imports
31
+ "simple-import-sort/imports": [
32
+ "error",
33
+ {
34
+ "groups": [
35
+ // Side effect imports.
36
+ ["^\\u0000"],
37
+ // Packages.
38
+ // Things that start with a letter (or digit or underscore), or
39
+ // `@` followed by a letter.
40
+ ["^@?\\w"],
41
+ // Root imports
42
+ ["^(src)(/.*|$)"],
43
+ ["^(tests)(/.*|$)"],
44
+ // Parent imports. Put `..` last.
45
+ ["^\\.\\.(?!/?$)", "^\\.\\./?$"],
46
+ // Other relative imports. Put same-folder imports and `.` last.
47
+ ["^\\./(?=.*/)(?!/?$)", "^\\.(?!/?$)", "^\\./?$"]
48
+ ]
49
+ }
50
+ ],
51
+ "sort-imports": "off",
52
+ "import/order": "off",
53
+ "import/first": "error",
54
+ "import/newline-after-import": "error",
55
+ "import/no-duplicates": "error",
56
+ "@typescript-eslint/no-floating-promises": 2,
57
+ "@typescript-eslint/no-unused-vars": 2,
58
+ // Disabled pending refactoring of two helper functions
59
+ // "typesafe/no-throw-sync-func": "error"
60
+ }
61
+ },
62
+ { "files": ["*.spec.ts"], "extends": ["plugin:jest/recommended"] },
63
+ {
64
+ "files": ["*.ts", "*.js"],
65
+ "excludedFiles": ["**/*.spec.ts", "**/.spec.js", "**/__tests__/**/*.ts"],
66
+ "rules": {
67
+ "@typescript-eslint/no-non-null-assertion": "error"
68
+ }
69
+ }
70
+ ],
71
+ "rules": {
72
+ "no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
73
+ "no-console": "warn"
74
+ }
75
+ }
@@ -0,0 +1,51 @@
1
+ name: ci
2
+ on:
3
+ push:
4
+ pull_request:
5
+ types: [opened, reopened]
6
+ jobs:
7
+ build:
8
+ name: build
9
+ runs-on: ubuntu-18.04
10
+ steps:
11
+ - uses: actions/checkout@v2
12
+ - name: Use Node.js
13
+ uses: actions/setup-node@v1
14
+ with:
15
+ node-version: '10.x'
16
+ - name: Cache Node.js modules
17
+ uses: actions/cache@v2
18
+ with:
19
+ # npm cache files are stored in `~/.npm` on Linux/macOS
20
+ path: ~/.npm
21
+ key: ${{ runner.OS }}-node-${{ hashFiles('**/package-lock.json') }}
22
+ restore-keys: |
23
+ ${{ runner.OS }}-node-
24
+ ${{ runner.OS }}-
25
+ - run: npm ci
26
+ - run: npx lockfile-lint --type npm --path package-lock.json --validate-https --allowed-hosts npm
27
+ - run: npm run build
28
+ test:
29
+ name: test
30
+ runs-on: ubuntu-18.04
31
+ steps:
32
+ - uses: actions/checkout@v2
33
+ - name: Use Node.js
34
+ uses: actions/setup-node@v1
35
+ with:
36
+ node-version: '10.x'
37
+ - name: Cache Node.js modules
38
+ uses: actions/cache@v2
39
+ with:
40
+ # npm cache files are stored in `~/.npm` on Linux/macOS
41
+ path: ~/.npm
42
+ key: ${{ runner.OS }}-node-${{ hashFiles('**/package-lock.json') }}
43
+ restore-keys: |
44
+ ${{ runner.OS }}-node-
45
+ ${{ runner.OS }}-
46
+ - run: npm ci
47
+ - run: npm run test-ci
48
+ - name: Submit test coverage to Coveralls
49
+ uses: coverallsapp/github-action@v1.1.2
50
+ with:
51
+ github-token: ${{ secrets.GITHUB_TOKEN }}
package/CHANGELOG.md ADDED
@@ -0,0 +1,120 @@
1
+ ### Changelog
2
+
3
+ All notable changes to this project will be documented in this file. Dates are displayed in UTC.
4
+
5
+ Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
6
+
7
+ #### [v0.10.0](https://github.com/opengovsg/formsg-javascript-sdk/compare/v0.9.0...v0.10.0)
8
+
9
+ > 16 December 2022
10
+
11
+ - fix: allow empty field titles in decrypted responses [`#86`](https://github.com/opengovsg/formsg-javascript-sdk/pull/86)
12
+ - chore: run npm audit fix and update relevant deps [`#80`](https://github.com/opengovsg/formsg-javascript-sdk/pull/80)
13
+ - chore(deps): Upgrade vulnerable axios version [`#78`](https://github.com/opengovsg/formsg-javascript-sdk/pull/78)
14
+ - chore: merge v0.9.0 into develop [`#76`](https://github.com/opengovsg/formsg-javascript-sdk/pull/76)
15
+ - fix: run npm audit fix so typescript packages are the correct versions [`2bdbf89`](https://github.com/opengovsg/formsg-javascript-sdk/commit/2bdbf897614833cc50605554ddaa82ea567b8a09)
16
+ - chore: bump version to v0.10.0 [`e094041`](https://github.com/opengovsg/formsg-javascript-sdk/commit/e0940414e529012afc03c54165f82e9f5ce16b12)
17
+
18
+ #### [v0.9.0](https://github.com/opengovsg/formsg-javascript-sdk/compare/v0.8.2...v0.9.0)
19
+
20
+ > 16 December 2022
21
+
22
+ - build: add separate keypairs for dev environment [`#73`](https://github.com/opengovsg/formsg-javascript-sdk/pull/73)
23
+ - docs: specify version for attachment functionality [`#72`](https://github.com/opengovsg/formsg-javascript-sdk/pull/72)
24
+ - build(ci): migrate to GitHub Actions [`#70`](https://github.com/opengovsg/formsg-javascript-sdk/pull/70)
25
+ - refactor(crypto): minor refactor of decryptWithAttachments [`#69`](https://github.com/opengovsg/formsg-javascript-sdk/pull/69)
26
+ - feat: Adding eslint functionality for the repository [`#66`](https://github.com/opengovsg/formsg-javascript-sdk/pull/66)
27
+ - build(deps): bump lodash from 4.17.19 to 4.17.21 [`#64`](https://github.com/opengovsg/formsg-javascript-sdk/pull/64)
28
+ - add downloadAndDecryptAttachments for downloading and decrypting attachments [`#62`](https://github.com/opengovsg/formsg-javascript-sdk/pull/62)
29
+ - build(deps): bump y18n from 4.0.0 to 4.0.1 [`#59`](https://github.com/opengovsg/formsg-javascript-sdk/pull/59)
30
+ - Update README.md [`#61`](https://github.com/opengovsg/formsg-javascript-sdk/pull/61)
31
+ - build: merge Release 0.8.4 back to develop branch [`#58`](https://github.com/opengovsg/formsg-javascript-sdk/pull/58)
32
+ - build: release v0.8.4-beta.0 [`#56`](https://github.com/opengovsg/formsg-javascript-sdk/pull/56)
33
+ - Revert "build(npm): update repo entry (#43)" [`#55`](https://github.com/opengovsg/formsg-javascript-sdk/pull/55)
34
+ - fix: update .npmignore to ignore misc unneeded files [`#54`](https://github.com/opengovsg/formsg-javascript-sdk/pull/54)
35
+ - ref: use mode init parameter again [`#52`](https://github.com/opengovsg/formsg-javascript-sdk/pull/52)
36
+ - chore: Update JSDoc and print warning message if deprecated mode parameter is used [`#51`](https://github.com/opengovsg/formsg-javascript-sdk/pull/51)
37
+ - fix(FormField): account for table responses [`#44`](https://github.com/opengovsg/formsg-javascript-sdk/pull/44)
38
+ - build(npm): update repo entry [`#43`](https://github.com/opengovsg/formsg-javascript-sdk/pull/43)
39
+ - feat: add more webhook tests to check for undefined params [`#41`](https://github.com/opengovsg/formsg-javascript-sdk/pull/41)
40
+ - Bump lodash from 4.17.15 to 4.17.19 [`#42`](https://github.com/opengovsg/formsg-javascript-sdk/pull/42)
41
+ - fix: JSON body parser required for demo code to work [`#40`](https://github.com/opengovsg/formsg-javascript-sdk/pull/40)
42
+ - refactor: update verification HOF to Verification class [`#38`](https://github.com/opengovsg/formsg-javascript-sdk/pull/38)
43
+ - refactor: update crypto HOF to Crypto class [`#36`](https://github.com/opengovsg/formsg-javascript-sdk/pull/36)
44
+ - refactor: update webhooks HOF to Webhooks class [`#34`](https://github.com/opengovsg/formsg-javascript-sdk/pull/34)
45
+ - docs: fix code snippet typos [`#37`](https://github.com/opengovsg/formsg-javascript-sdk/pull/37)
46
+ - feat: add publicKey param to package initialization parameters [`#33`](https://github.com/opengovsg/formsg-javascript-sdk/pull/33)
47
+ - chore: add `test-ci` script for Travis to run instead of `test` [`#32`](https://github.com/opengovsg/formsg-javascript-sdk/pull/32)
48
+ - Release 0.8.3 - Allow tolerance for clock drift when authenticating webhooks [`#47`](https://github.com/opengovsg/formsg-javascript-sdk/pull/47)
49
+ - fix(epoch): allow for possible clock drift [`a3ce917`](https://github.com/opengovsg/formsg-javascript-sdk/commit/a3ce917c6c38a387edcfad06ae5176b1f92b0a46)
50
+ - fix(epoch): allow for possible clock drift [`5ba253d`](https://github.com/opengovsg/formsg-javascript-sdk/commit/5ba253d61d04d3082f4d752ecfc9efd3639acc0f)
51
+ - chore: bump version to 0.9.0 [`32111b1`](https://github.com/opengovsg/formsg-javascript-sdk/commit/32111b1a2c85955a33d478dbc06966dda7261e33)
52
+
53
+ #### [v0.8.2](https://github.com/opengovsg/formsg-javascript-sdk/compare/v0.4.1...v0.8.2)
54
+
55
+ > 4 June 2020
56
+
57
+ - Release v0.8.2 - Patch for encrypting non-UTF8 characters [`#35`](https://github.com/opengovsg/formsg-javascript-sdk/pull/35)
58
+ - Release v0.8.1 - Export types [`#29`](https://github.com/opengovsg/formsg-javascript-sdk/pull/29)
59
+ - Fix: Export declared types in the package [`#28`](https://github.com/opengovsg/formsg-javascript-sdk/pull/28)
60
+ - Replace tweetnacl-util with StableLib [`#27`](https://github.com/opengovsg/formsg-javascript-sdk/pull/27)
61
+ - v0.8.0 - Update decrypt signature [`#25`](https://github.com/opengovsg/formsg-javascript-sdk/pull/25)
62
+ - Test and build coverage badges with TravsiCI and Coveralls.io [`#23`](https://github.com/opengovsg/formsg-javascript-sdk/pull/23)
63
+ - chore: update README for increased clarity on decrypt [`#22`](https://github.com/opengovsg/formsg-javascript-sdk/pull/22)
64
+ - v0.7.0 - Verification module for SDK [`#19`](https://github.com/opengovsg/formsg-javascript-sdk/pull/19)
65
+ - fix: use req.get() in README sample code [`#21`](https://github.com/opengovsg/formsg-javascript-sdk/pull/21)
66
+ - Enforce minimum level of test coverage [`#20`](https://github.com/opengovsg/formsg-javascript-sdk/pull/20)
67
+ - Update encrypt/decryptFile function signature from blob to UInt8Array [`#18`](https://github.com/opengovsg/formsg-javascript-sdk/pull/18)
68
+ - Utility functions for attachments [`#17`](https://github.com/opengovsg/formsg-javascript-sdk/pull/17)
69
+ - Replace Joi with custom validation [`#16`](https://github.com/opengovsg/formsg-javascript-sdk/pull/16)
70
+ - Link GitHub to package.json [`#15`](https://github.com/opengovsg/formsg-javascript-sdk/pull/15)
71
+ - Fix broken links in readme [`#14`](https://github.com/opengovsg/formsg-javascript-sdk/pull/14)
72
+ - Extend crypto module for verified fields and migrate to TypeScript [`#12`](https://github.com/opengovsg/formsg-javascript-sdk/pull/12)
73
+ - Patch readme sample code [`#11`](https://github.com/opengovsg/formsg-javascript-sdk/pull/11)
74
+
75
+ #### [v0.4.1](https://github.com/opengovsg/formsg-javascript-sdk/compare/v0.4.0...v0.4.1)
76
+
77
+ > 30 March 2020
78
+
79
+ - Release v0.4.1 - Remove dangling reference to source in require [`#10`](https://github.com/opengovsg/formsg-javascript-sdk/pull/10)
80
+
81
+ #### [v0.4.0](https://github.com/opengovsg/formsg-javascript-sdk/compare/v0.3.2...v0.4.0)
82
+
83
+ > 30 March 2020
84
+
85
+ - Pre-publish transpile pipeline with Babel [`#9`](https://github.com/opengovsg/formsg-javascript-sdk/pull/9)
86
+
87
+ #### [v0.3.2](https://github.com/opengovsg/formsg-javascript-sdk/compare/v0.3.1...v0.3.2)
88
+
89
+ > 30 March 2020
90
+
91
+ - Avoid default arguments in destructuring [`#8`](https://github.com/opengovsg/formsg-javascript-sdk/pull/8)
92
+
93
+ #### [v0.3.1](https://github.com/opengovsg/formsg-javascript-sdk/compare/v0.3.0...v0.3.1)
94
+
95
+ > 30 March 2020
96
+
97
+ - Destructure options in the function body [`#7`](https://github.com/opengovsg/formsg-javascript-sdk/pull/7)
98
+
99
+ #### [v0.3.0](https://github.com/opengovsg/formsg-javascript-sdk/compare/v0.2.0...v0.3.0)
100
+
101
+ > 24 March 2020
102
+
103
+ - Release 0.3.0 [`#5`](https://github.com/opengovsg/formsg-javascript-sdk/pull/5)
104
+ - Add cryptographic functions to SDK [`#4`](https://github.com/opengovsg/formsg-javascript-sdk/pull/4)
105
+ - Update readme to clarify that this repository is the SDK and not the FormSG system. [`#3`](https://github.com/opengovsg/formsg-javascript-sdk/pull/3)
106
+
107
+ #### [v0.2.0](https://github.com/opengovsg/formsg-javascript-sdk/compare/v0.1.1...v0.2.0)
108
+
109
+ > 2 March 2020
110
+
111
+ - Version 0.2.0 - Add signing functions, end-to-end testing [`#2`](https://github.com/opengovsg/formsg-javascript-sdk/pull/2)
112
+
113
+ #### v0.1.1
114
+
115
+ > 2 March 2020
116
+
117
+ - Version 0.1.1 - Readme, configuration bugfix [`#1`](https://github.com/opengovsg/formsg-javascript-sdk/pull/1)
118
+ - Add signature authentication [`a23a6ff`](https://github.com/opengovsg/formsg-javascript-sdk/commit/a23a6ffee1aaa65f805207b3f7ed17ed05590010)
119
+ - gitignore [`1b288fa`](https://github.com/opengovsg/formsg-javascript-sdk/commit/1b288fa9cff6df70efb09bf477570159516605e1)
120
+ - install dependencies [`d8fc078`](https://github.com/opengovsg/formsg-javascript-sdk/commit/d8fc078b29ffb0043adbb105fe4a39abed5a8cf9)
package/README.md CHANGED
@@ -47,6 +47,9 @@ const POST_URI = 'https://my-domain.com/submissions'
47
47
  // Your form's secret key downloaded from FormSG upon form creation
48
48
  const formSecretKey = process.env.FORM_SECRET_KEY
49
49
 
50
+ // Set to true if you need to download and decrypt attachments from submissions
51
+ const HAS_ATTACHMENTS = false
52
+
50
53
  app.post(
51
54
  '/submissions',
52
55
  // Endpoint authentication by verifying signatures
@@ -62,20 +65,12 @@ app.post(
62
65
  // Parse JSON from raw request body
63
66
  express.json(),
64
67
  // Decrypt the submission
65
- function (req, res, next) {
66
- // `req.body.data` is an object fulfilling the DecryptParams interface.
67
- // interface DecryptParams {
68
- // encryptedContent: EncryptedContent
69
- // version: number
70
- // verifiedContent?: EncryptedContent
71
- // }
72
- /** @type {{responses: FormField[], verified?: Record<string, any>}} */
73
- const submission = formsg.crypto.decrypt(
74
- formSecretKey,
75
- // If `verifiedContent` is provided in `req.body.data`, the return object
76
- // will include a verified key.
77
- req.body.data
78
- )
68
+ async function (req, res, next) {
69
+ // If `verifiedContent` is provided in `req.body.data`, the return object
70
+ // will include a verified key.
71
+ const submission = HAS_ATTACHMENTS
72
+ ? await formsg.crypto.decryptWithAttachments(formSecretKey, req.body.data)
73
+ : formsg.crypto.decrypt(formSecretKey, req.body.data)
79
74
 
80
75
  // If the decryption failed, submission will be `null`.
81
76
  if (submission) {
@@ -97,12 +92,13 @@ The underlying cryptosystem is `x25519-xsalsa20-poly1305` which is implemented b
97
92
 
98
93
  ### Format of Submission Response
99
94
 
100
- | Key | Type | Description |
101
- | ---------------- | ------ | ----------------------------------- |
102
- | formId | string | Unique form identifier. |
103
- | submissionId | string | Unique submission identifier. |
104
- | encryptedContent | string | The encrypted submission in base64. |
105
- | created | string | Creation timestamp. |
95
+ | Key | Type | Description |
96
+ | ---------------------- | ---------------------- | -------------------------------------------------------------------------------------------------------- |
97
+ | formId | string | Unique form identifier. |
98
+ | submissionId | string | Unique response identifier, displayed as 'Response ID' to form respondents |
99
+ | encryptedContent | string | The encrypted submission in base64. |
100
+ | created | string | Creation timestamp. |
101
+ | attachmentDownloadUrls | Record<string, string> | (Optional) Records containing field IDs and URLs where encrypted uploaded attachments can be downloaded. |
106
102
 
107
103
  ### Format of Decrypted Submissions
108
104
 
@@ -158,6 +154,23 @@ If the decrypted content is the correct shape, then:
158
154
  verified content. **If the verification fails, `null` is returned, even if
159
155
  `decryptParams.encryptedContent` was successfully decrypted.**
160
156
 
157
+ ### Processing Attachments
158
+
159
+ `formsg.crypto.decryptWithAttachments(formSecretKey: string, decryptParams: DecryptParams)` (available from version 0.9.0 onwards) behaves similarly except it will return a `Promise<DecryptedContentAndAttachments | null>`.
160
+
161
+ `DecryptedContentAndAttachments` is an object containing two fields:
162
+
163
+ - `content`: the standard form decrypted responses (same as the return type of `formsg.crypto.decrypt`)
164
+ - `attachments`: A `Record<string, DecryptedFile>` containing a map of field ids of the attachment fields to a object containing the original user supplied filename and a `Uint8Array` containing the contents of the uploaded file.
165
+
166
+ If the contents of any file fails to decrypt or there is a mismatch between the attachments and submission (e.g. the submission doesn't contain the original file name), then `null` will be returned.
167
+
168
+ Attachments are downloaded using S3 pre-signed URLs, with a expiry time of _one hour_. You must call `decryptWithAttachments` within this time window, or else the URL to the encrypted files will become invalid.
169
+
170
+ Attachments are end-to-end encrypted in the same way as normal form submissions, so any eavesdropper will not be able to view form attachments without your secret key.
171
+
172
+ _Warning:_ We do not have the ability to scan any attachments for malicious content (e.g. spyware or viruses), so careful handling is needed.
173
+
161
174
  ## Verifying Signatures Manually
162
175
 
163
176
  You can use the following information to create a custom solution, although we recommend using this SDK.
package/dist/crypto.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { DecryptParams, DecryptedContent, EncryptedFileContent } from './types';
1
+ import { DecryptedContent, DecryptedContentAndAttachments, DecryptParams, EncryptedFileContent } from './types';
2
2
  export default class Crypto {
3
3
  signingPublicKey?: string;
4
4
  constructor({ signingPublicKey }?: {
@@ -51,4 +51,12 @@ export default class Crypto {
51
51
  * @param encrypted.blob The encrypted file as a Blob object
52
52
  */
53
53
  decryptFile: (formSecretKey: string, { submissionPublicKey, nonce, binary: encryptedBinary, }: EncryptedFileContent) => Promise<Uint8Array | null>;
54
+ /**
55
+ * Decrypts an encrypted submission, and also download and decrypt any attachments alongside it.
56
+ * @param formSecretKey Secret key as a base-64 string
57
+ * @param decryptParams The params containing encrypted content and information.
58
+ * @returns A promise of the decrypted submission, including attachments (if any). Or else returns null if a decryption error decrypting any part of the submission.
59
+ * @throws {MissingPublicKeyError} if a public key is not provided when instantiating this class and is needed for verifying signed content.
60
+ */
61
+ decryptWithAttachments: (formSecretKey: string, decryptParams: DecryptParams) => Promise<DecryptedContentAndAttachments | null>;
54
62
  }
package/dist/crypto.js CHANGED
@@ -39,11 +39,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
39
39
  return (mod && mod.__esModule) ? mod : { "default": mod };
40
40
  };
41
41
  Object.defineProperty(exports, "__esModule", { value: true });
42
+ var axios_1 = __importDefault(require("axios"));
42
43
  var tweetnacl_1 = __importDefault(require("tweetnacl"));
43
44
  var tweetnacl_util_1 = require("tweetnacl-util");
45
+ var crypto_1 = require("./util/crypto");
44
46
  var validate_1 = require("./util/validate");
45
47
  var errors_1 = require("./errors");
46
- var crypto_1 = require("./util/crypto");
47
48
  var Crypto = /** @class */ (function () {
48
49
  function Crypto(_a) {
49
50
  var _this = this;
@@ -74,7 +75,7 @@ var Crypto = /** @class */ (function () {
74
75
  */
75
76
  this.decrypt = function (formSecretKey, decryptParams) {
76
77
  try {
77
- var encryptedContent = decryptParams.encryptedContent, verifiedContent = decryptParams.verifiedContent, version = decryptParams.version;
78
+ var encryptedContent = decryptParams.encryptedContent, verifiedContent = decryptParams.verifiedContent;
78
79
  // Do not return the transformed object in `_decrypt` function as a signed
79
80
  // object is not encoded in UTF8 and is encoded in Base-64 instead.
80
81
  var decryptedContent = crypto_1.decryptContent(formSecretKey, encryptedContent);
@@ -171,6 +172,81 @@ var Crypto = /** @class */ (function () {
171
172
  });
172
173
  });
173
174
  };
175
+ /**
176
+ * Decrypts an encrypted submission, and also download and decrypt any attachments alongside it.
177
+ * @param formSecretKey Secret key as a base-64 string
178
+ * @param decryptParams The params containing encrypted content and information.
179
+ * @returns A promise of the decrypted submission, including attachments (if any). Or else returns null if a decryption error decrypting any part of the submission.
180
+ * @throws {MissingPublicKeyError} if a public key is not provided when instantiating this class and is needed for verifying signed content.
181
+ */
182
+ this.decryptWithAttachments = function (formSecretKey, decryptParams) { return __awaiter(_this, void 0, void 0, function () {
183
+ var decryptedRecords, filenames, attachmentRecords, decryptedContent, fieldIds, downloadPromises, _a;
184
+ var _this = this;
185
+ var _b;
186
+ return __generator(this, function (_c) {
187
+ switch (_c.label) {
188
+ case 0:
189
+ decryptedRecords = {};
190
+ filenames = {};
191
+ attachmentRecords = (_b = decryptParams.attachmentDownloadUrls) !== null && _b !== void 0 ? _b : {};
192
+ decryptedContent = this.decrypt(formSecretKey, decryptParams);
193
+ if (decryptedContent === null)
194
+ return [2 /*return*/, null
195
+ // Retrieve all original filenames for attachments for easy lookup
196
+ ];
197
+ // Retrieve all original filenames for attachments for easy lookup
198
+ decryptedContent.responses.forEach(function (response) {
199
+ if (response.fieldType === 'attachment' && response.answer) {
200
+ filenames[response._id] = response.answer;
201
+ }
202
+ });
203
+ fieldIds = Object.keys(attachmentRecords);
204
+ // Check if all fieldIds are within filenames
205
+ if (!crypto_1.areAttachmentFieldIdsValid(fieldIds, filenames)) {
206
+ return [2 /*return*/, null];
207
+ }
208
+ downloadPromises = fieldIds.map(function (fieldId) {
209
+ return (axios_1.default
210
+ // Retrieve all the attachments as JSON
211
+ .get(attachmentRecords[fieldId], {
212
+ responseType: 'json',
213
+ })
214
+ // Decrypt all the attachments
215
+ .then(function (_a) {
216
+ var downloadResponse = _a.data;
217
+ var encryptedFile = crypto_1.convertEncryptedAttachmentToFileContent(downloadResponse);
218
+ return _this.decryptFile(formSecretKey, encryptedFile);
219
+ })
220
+ .then(function (decryptedFile) {
221
+ // Check if the file exists and set the filename accordingly; otherwise, throw an error
222
+ if (decryptedFile) {
223
+ decryptedRecords[fieldId] = {
224
+ filename: filenames[fieldId],
225
+ content: decryptedFile,
226
+ };
227
+ }
228
+ else {
229
+ throw new errors_1.AttachmentDecryptionError();
230
+ }
231
+ }));
232
+ });
233
+ _c.label = 1;
234
+ case 1:
235
+ _c.trys.push([1, 3, , 4]);
236
+ return [4 /*yield*/, Promise.all(downloadPromises)];
237
+ case 2:
238
+ _c.sent();
239
+ return [3 /*break*/, 4];
240
+ case 3:
241
+ _a = _c.sent();
242
+ return [2 /*return*/, null];
243
+ case 4: return [2 /*return*/, {
244
+ content: decryptedContent,
245
+ attachments: decryptedRecords,
246
+ }];
247
+ }
248
+ });
249
+ }); };
174
250
  this.signingPublicKey = signingPublicKey;
175
251
  }
176
252
  return Crypto;
package/dist/errors.d.ts CHANGED
@@ -7,4 +7,7 @@ declare class MissingPublicKeyError extends Error {
7
7
  declare class WebhookAuthenticateError extends Error {
8
8
  constructor(message: string);
9
9
  }
10
- export { MissingSecretKeyError, MissingPublicKeyError, WebhookAuthenticateError, };
10
+ declare class AttachmentDecryptionError extends Error {
11
+ constructor(message?: string);
12
+ }
13
+ export { MissingSecretKeyError, MissingPublicKeyError, WebhookAuthenticateError, AttachmentDecryptionError, };
package/dist/errors.js CHANGED
@@ -54,3 +54,14 @@ var WebhookAuthenticateError = /** @class */ (function (_super) {
54
54
  return WebhookAuthenticateError;
55
55
  }(Error));
56
56
  exports.WebhookAuthenticateError = WebhookAuthenticateError;
57
+ var AttachmentDecryptionError = /** @class */ (function (_super) {
58
+ __extends(AttachmentDecryptionError, _super);
59
+ function AttachmentDecryptionError(message) {
60
+ if (message === void 0) { message = 'Attachment decryption with the given nonce failed.'; }
61
+ var _this = _super.call(this, message) || this;
62
+ _this.name = _this.constructor.name;
63
+ return _this;
64
+ }
65
+ return AttachmentDecryptionError;
66
+ }(Error));
67
+ exports.AttachmentDecryptionError = AttachmentDecryptionError;
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
+ import Crypto from './crypto';
1
2
  import { PackageInitParams } from './types';
2
3
  import Verification from './verification';
3
4
  import Webhooks from './webhooks';
4
- import Crypto from './crypto';
5
5
  declare const _default: (config?: PackageInitParams) => {
6
6
  webhooks: Webhooks;
7
7
  crypto: Crypto;
package/dist/index.js CHANGED
@@ -3,9 +3,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  var publicKey_1 = require("./util/publicKey");
6
+ var crypto_1 = __importDefault(require("./crypto"));
6
7
  var verification_1 = __importDefault(require("./verification"));
7
8
  var webhooks_1 = __importDefault(require("./webhooks"));
8
- var crypto_1 = __importDefault(require("./crypto"));
9
9
  module.exports = function (config) {
10
10
  if (config === void 0) { config = {}; }
11
11
  var webhookSecretKey = config.webhookSecretKey, mode = config.mode, verificationOptions = config.verificationOptions;
@@ -4,6 +4,7 @@ export declare const SIGNING_KEYS: {
4
4
  };
5
5
  development: {
6
6
  publicKey: string;
7
+ secretKey: string;
7
8
  };
8
9
  production: {
9
10
  publicKey: string;
@@ -1,13 +1,14 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ // keys generated using nacl.sign.keyPair() from tweetnacl
3
4
  exports.SIGNING_KEYS = {
4
5
  staging: {
5
6
  // staging must never contain secret keys
6
7
  publicKey: 'rjv41kYqZwcbe3r6ymMEEKQ+Vd+DPuogN+Gzq3lP2Og=',
7
8
  },
8
9
  development: {
9
- // Using the same keys for staging.
10
- publicKey: 'rjv41kYqZwcbe3r6ymMEEKQ+Vd+DPuogN+Gzq3lP2Og=',
10
+ publicKey: 'Tl5gfszlKcQj99/0uafLwVpT6JAu4C0dHGvLq1cHzFE=',
11
+ secretKey: 'HDBXpu+2/gu10bLHpy8HjpN89xbA6boH9GwibPGJA8BOXmB+zOUpxCP33/S5p8vBWlPokC7gLR0ca8urVwfMUQ==',
11
12
  },
12
13
  production: {
13
14
  // production must never contain secret keys
@@ -4,6 +4,7 @@ export declare const VERIFICATION_KEYS: {
4
4
  };
5
5
  development: {
6
6
  publicKey: string;
7
+ secretKey: string;
7
8
  };
8
9
  production: {
9
10
  publicKey: string;
@@ -1,13 +1,14 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ // keys generated using nacl.sign.keyPair() from tweetnacl
3
4
  exports.VERIFICATION_KEYS = {
4
5
  staging: {
5
6
  // staging must never contain secret keys
6
7
  publicKey: 'bDgK1223JbrDNePFIrj7b0z02Z5nSiBzkRYRqDdVPfA=',
7
8
  },
8
9
  development: {
9
- // Using the same keys for staging.
10
- publicKey: 'bDgK1223JbrDNePFIrj7b0z02Z5nSiBzkRYRqDdVPfA=',
10
+ publicKey: 'SZ4pV0JXgj8dhFU69uHllqYcxTtliYmi+d6Ml56lnQU=',
11
+ secretKey: 'iGkfOuI6uxrlfw+7CZFFUZBwk86I+pu6v+g7EWA6qJpJnilXQleCPx2EVTr24eWWphzFO2WJiaL53oyXnqWdBQ==',
11
12
  },
12
13
  production: {
13
14
  // production must never contain secret keys
package/dist/types.d.ts CHANGED
@@ -21,20 +21,38 @@ export declare type FormField = {
21
21
  answerArray: string[] | string[][];
22
22
  });
23
23
  export declare type EncryptedContent = string;
24
+ export declare type EncryptedAttachmentRecords = Record<string, string>;
24
25
  export interface DecryptParams {
25
26
  encryptedContent: EncryptedContent;
26
27
  version: number;
27
28
  verifiedContent?: EncryptedContent;
29
+ attachmentDownloadUrls?: EncryptedAttachmentRecords;
28
30
  }
29
31
  export declare type DecryptedContent = {
30
32
  responses: FormField[];
31
33
  verified?: Record<string, any>;
32
34
  };
35
+ export declare type DecryptedFile = {
36
+ filename: string;
37
+ content: Uint8Array;
38
+ };
39
+ export declare type DecryptedAttachments = Record<string, DecryptedFile>;
40
+ export declare type DecryptedContentAndAttachments = {
41
+ content: DecryptedContent;
42
+ attachments: DecryptedAttachments;
43
+ };
33
44
  export declare type EncryptedFileContent = {
34
45
  submissionPublicKey: string;
35
46
  nonce: string;
36
47
  binary: Uint8Array;
37
48
  };
49
+ export declare type EncryptedAttachmentContent = {
50
+ encryptedFile: {
51
+ submissionPublicKey: string;
52
+ nonce: string;
53
+ binary: string;
54
+ };
55
+ };
38
56
  export declare type Keypair = {
39
57
  publicKey: string;
40
58
  secretKey: string;
@@ -1,4 +1,4 @@
1
- import { Keypair } from '../types';
1
+ import { Keypair, EncryptedAttachmentContent, EncryptedFileContent } from '../types';
2
2
  /**
3
3
  * Helper method to generate a new keypair for encryption.
4
4
  * @returns The generated keypair.
@@ -27,3 +27,16 @@ export declare const decryptContent: (formPrivateKey: string, encryptedContent:
27
27
  * @throws {Error} if the message cannot be verified
28
28
  */
29
29
  export declare const verifySignedMessage: (msg: Uint8Array, publicKey: string) => Record<string, any>;
30
+ /**
31
+ * Helper method to check if all the field IDs given are within the filenames
32
+ * @param fieldIds the list of fieldIds to check
33
+ * @param filenames the filenames that should contain the fields
34
+ * @returns boolean indicating whether the fields are valid
35
+ */
36
+ export declare const areAttachmentFieldIdsValid: (fieldIds: string[], filenames: Record<string, string>) => boolean;
37
+ /**
38
+ * Converts an encrypted attachment to encrypted file content
39
+ * @param encryptedAttachment The encrypted attachment
40
+ * @returns EncryptedFileContent The encrypted file content
41
+ */
42
+ export declare const convertEncryptedAttachmentToFileContent: (encryptedAttachment: EncryptedAttachmentContent) => EncryptedFileContent;
@@ -3,8 +3,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- var tweetnacl_util_1 = require("tweetnacl-util");
7
6
  var tweetnacl_1 = __importDefault(require("tweetnacl"));
7
+ var tweetnacl_util_1 = require("tweetnacl-util");
8
8
  /**
9
9
  * Helper method to generate a new keypair for encryption.
10
10
  * @returns The generated keypair.
@@ -58,3 +58,22 @@ exports.verifySignedMessage = function (msg, publicKey) {
58
58
  throw new Error('Failed to open signed message with given public key');
59
59
  return JSON.parse(tweetnacl_util_1.encodeUTF8(openedMessage));
60
60
  };
61
+ /**
62
+ * Helper method to check if all the field IDs given are within the filenames
63
+ * @param fieldIds the list of fieldIds to check
64
+ * @param filenames the filenames that should contain the fields
65
+ * @returns boolean indicating whether the fields are valid
66
+ */
67
+ exports.areAttachmentFieldIdsValid = function (fieldIds, filenames) {
68
+ return fieldIds.every(function (fieldId) { return filenames[fieldId]; });
69
+ };
70
+ /**
71
+ * Converts an encrypted attachment to encrypted file content
72
+ * @param encryptedAttachment The encrypted attachment
73
+ * @returns EncryptedFileContent The encrypted file content
74
+ */
75
+ exports.convertEncryptedAttachmentToFileContent = function (encryptedAttachment) { return ({
76
+ submissionPublicKey: encryptedAttachment.encryptedFile.submissionPublicKey,
77
+ nonce: encryptedAttachment.encryptedFile.nonce,
78
+ binary: tweetnacl_util_1.decodeBase64(encryptedAttachment.encryptedFile.binary),
79
+ }); };
@@ -14,6 +14,7 @@ var stage_1 = __importDefault(require("./stage"));
14
14
  function getSigningPublicKey(mode) {
15
15
  switch (mode) {
16
16
  case stage_1.default.development:
17
+ return signing_keys_1.SIGNING_KEYS.development.publicKey;
17
18
  case stage_1.default.staging:
18
19
  return signing_keys_1.SIGNING_KEYS.staging.publicKey;
19
20
  case stage_1.default.test:
@@ -31,6 +32,7 @@ exports.getSigningPublicKey = getSigningPublicKey;
31
32
  function getVerificationPublicKey(mode) {
32
33
  switch (mode) {
33
34
  case stage_1.default.development:
35
+ return verification_keys_1.VERIFICATION_KEYS.development.publicKey;
34
36
  case stage_1.default.staging:
35
37
  return verification_keys_1.VERIFICATION_KEYS.staging.publicKey;
36
38
  case stage_1.default.test:
@@ -14,7 +14,9 @@ function determineIsFormFields(tbd) {
14
14
  internal.isHeader) &&
15
15
  internal._id &&
16
16
  internal.fieldType &&
17
- internal.question;
17
+ // The field is still valid even when the question title is empty string
18
+ // (even though it is not intended behavior).
19
+ typeof internal.question === 'string';
18
20
  });
19
21
  return filter.length === tbd.length;
20
22
  }
@@ -8,8 +8,8 @@ var __importStar = (this && this.__importStar) || function (mod) {
8
8
  };
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
10
  var url = __importStar(require("url"));
11
- var signature_1 = require("./signature");
12
11
  var errors_1 = require("../errors");
12
+ var signature_1 = require("./signature");
13
13
  /**
14
14
  * Helper function to construct the basestring and verify the signature of an
15
15
  * incoming request
@@ -1,4 +1,4 @@
1
- import { VerificationSignatureOptions, VerificationAuthenticateOptions, VerificationOptions } from '../types';
1
+ import { VerificationAuthenticateOptions, VerificationOptions, VerificationSignatureOptions } from '../types';
2
2
  export default class Verification {
3
3
  verificationPublicKey?: string;
4
4
  verificationSecretKey?: string;
package/dist/webhooks.js CHANGED
@@ -8,8 +8,8 @@ var __importStar = (this && this.__importStar) || function (mod) {
8
8
  };
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
10
  var url = __importStar(require("url"));
11
- var signature_1 = require("./util/signature");
12
11
  var parser_1 = require("./util/parser");
12
+ var signature_1 = require("./util/signature");
13
13
  var webhooks_1 = require("./util/webhooks");
14
14
  var errors_1 = require("./errors");
15
15
  var Webhooks = /** @class */ (function () {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengovsg/formsg-sdk",
3
- "version": "0.8.4-beta.0",
3
+ "version": "0.10.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/opengovsg/formsg-javascript-sdk.git"
@@ -10,10 +10,11 @@
10
10
  "types": "./dist/index.d.ts",
11
11
  "scripts": {
12
12
  "test": "jest",
13
- "test-ci": "jest --coverage && coveralls < coverage/lcov.info && rm -rf ./coverage",
14
- "test:watch": "jest --watch",
13
+ "test-ci": "jest --coverage",
14
+ "test-watch": "jest --watch",
15
15
  "build": "tsc",
16
- "prepare": "npm run build"
16
+ "prepare": "npm run build",
17
+ "version": "auto-changelog -p && git add CHANGELOG.md"
17
18
  },
18
19
  "keywords": [
19
20
  "formsg",
@@ -23,6 +24,7 @@
23
24
  "author": "Open Government Products (FormSG)",
24
25
  "license": "MIT",
25
26
  "dependencies": {
27
+ "axios": "^0.24.0",
26
28
  "tweetnacl": "^1.0.3",
27
29
  "tweetnacl-util": "^0.15.1"
28
30
  },
@@ -30,11 +32,20 @@
30
32
  "@babel/cli": "^7.8.4",
31
33
  "@babel/core": "^7.9.0",
32
34
  "@babel/preset-env": "^7.9.0",
33
- "@types/jest": "^25.2.1",
35
+ "@types/jest": "^27.0.3",
34
36
  "@types/node": "^13.11.1",
35
- "coveralls": "^3.1.0",
36
- "jest": "^25.3.0",
37
- "ts-jest": "^25.3.1",
37
+ "@typescript-eslint/eslint-plugin": "^4.25.0",
38
+ "auto-changelog": "^2.4.0",
39
+ "coveralls": "^3.1.1",
40
+ "eslint-config-prettier": "^8.3.0",
41
+ "eslint-plugin-import": "^2.23.3",
42
+ "eslint-plugin-jest": "^24.3.6",
43
+ "eslint-plugin-prettier": "^3.4.0",
44
+ "eslint-plugin-simple-import-sort": "^7.0.0",
45
+ "eslint-plugin-typesafe": "^0.5.2",
46
+ "jest": "^27.3.1",
47
+ "jest-mock-axios": "^4.4.0",
48
+ "ts-jest": "^27.0.7",
38
49
  "typescript": "^3.8.3"
39
50
  }
40
51
  }