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
75 changes: 75 additions & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
{
"env": {
"commonjs": true,
"es6": true,
"node": true
},
"extends": ["eslint:recommended", "plugin:prettier/recommended"],
"parserOptions": {
"ecmaVersion": 2018
},
"overrides": [
{
"files": ["*.ts"],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"sourceType": "module",
"ecmaFeatures": {
"modules": true
},
"project": "tsconfig.json"
},
"plugins": [
"@typescript-eslint",
"import",
"simple-import-sort",
"typesafe"
],
"extends": ["plugin:@typescript-eslint/recommended"],
"rules": {
// Rules for auto sort of imports
"simple-import-sort/imports": [
"error",
{
"groups": [
// Side effect imports.
["^\\u0000"],
// Packages.
// Things that start with a letter (or digit or underscore), or
// `@` followed by a letter.
["^@?\\w"],
// Root imports
["^(src)(/.*|$)"],
["^(tests)(/.*|$)"],
// Parent imports. Put `..` last.
["^\\.\\.(?!/?$)", "^\\.\\./?$"],
// Other relative imports. Put same-folder imports and `.` last.
["^\\./(?=.*/)(?!/?$)", "^\\.(?!/?$)", "^\\./?$"]
]
}
],
"sort-imports": "off",
"import/order": "off",
"import/first": "error",
"import/newline-after-import": "error",
"import/no-duplicates": "error",
"@typescript-eslint/no-floating-promises": 2,
"@typescript-eslint/no-unused-vars": 2,
// Disabled pending refactoring of two helper functions
// "typesafe/no-throw-sync-func": "error"
}
},
{ "files": ["*.spec.ts"], "extends": ["plugin:jest/recommended"] },
{
"files": ["*.ts", "*.js"],
"excludedFiles": ["**/*.spec.ts", "**/.spec.js", "**/__tests__/**/*.ts"],
"rules": {
"@typescript-eslint/no-non-null-assertion": "error"
}
}
],
"rules": {
"no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
"no-console": "warn"
}
}
51 changes: 51 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
name: ci
on:
push:
pull_request:
types: [opened, reopened]
jobs:
build:
name: build
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@v2
- name: Use Node.js
uses: actions/setup-node@v1
with:
node-version: '10.x'
- name: Cache Node.js modules
uses: actions/cache@v2
with:
# npm cache files are stored in `~/.npm` on Linux/macOS
path: ~/.npm
key: ${{ runner.OS }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.OS }}-node-
${{ runner.OS }}-
- run: npm ci
- run: npx lockfile-lint --type npm --path package-lock.json --validate-https --allowed-hosts npm
- run: npm run build
test:
name: test
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@v2
- name: Use Node.js
uses: actions/setup-node@v1
with:
node-version: '10.x'
- name: Cache Node.js modules
uses: actions/cache@v2
with:
# npm cache files are stored in `~/.npm` on Linux/macOS
path: ~/.npm
key: ${{ runner.OS }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.OS }}-node-
${{ runner.OS }}-
- run: npm ci
- run: npm run test-ci
- name: Submit test coverage to Coveralls
uses: coverallsapp/github-action@v1.1.2
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
19 changes: 0 additions & 19 deletions .travis.yml

This file was deleted.

53 changes: 33 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ const POST_URI = 'https://my-domain.com/submissions'
// Your form's secret key downloaded from FormSG upon form creation
const formSecretKey = process.env.FORM_SECRET_KEY

// Set to true if you need to download and decrypt attachments from submissions
const HAS_ATTACHMENTS = false

app.post(
'/submissions',
// Endpoint authentication by verifying signatures
Expand All @@ -62,20 +65,12 @@ app.post(
// Parse JSON from raw request body
express.json(),
// Decrypt the submission
function (req, res, next) {
// `req.body.data` is an object fulfilling the DecryptParams interface.
// interface DecryptParams {
// encryptedContent: EncryptedContent
// version: number
// verifiedContent?: EncryptedContent
// }
/** @type {{responses: FormField[], verified?: Record<string, any>}} */
const submission = formsg.crypto.decrypt(
formSecretKey,
// If `verifiedContent` is provided in `req.body.data`, the return object
// will include a verified key.
req.body.data
)
async function (req, res, next) {
// If `verifiedContent` is provided in `req.body.data`, the return object
// will include a verified key.
const submission = HAS_ATTACHMENTS
? await formsg.crypto.decryptWithAttachments(formSecretKey, req.body.data)
: formsg.crypto.decrypt(formSecretKey, req.body.data)

// If the decryption failed, submission will be `null`.
if (submission) {
Expand All @@ -97,12 +92,13 @@ The underlying cryptosystem is `x25519-xsalsa20-poly1305` which is implemented b

### Format of Submission Response

| Key | Type | Description |
| ---------------- | ------ | ----------------------------------- |
| formId | string | Unique form identifier. |
| submissionId | string | Unique submission identifier. |
| encryptedContent | string | The encrypted submission in base64. |
| created | string | Creation timestamp. |
| Key | Type | Description |
| ---------------------- | ---------------------- | -------------------------------------------------------------------------------------------------------- |
| formId | string | Unique form identifier. |
| submissionId | string | Unique response identifier, displayed as 'Response ID' to form respondents |
| encryptedContent | string | The encrypted submission in base64. |
| created | string | Creation timestamp. |
| attachmentDownloadUrls | Record<string, string> | (Optional) Records containing field IDs and URLs where encrypted uploaded attachments can be downloaded. |

### Format of Decrypted Submissions

Expand Down Expand Up @@ -158,6 +154,23 @@ If the decrypted content is the correct shape, then:
verified content. **If the verification fails, `null` is returned, even if
`decryptParams.encryptedContent` was successfully decrypted.**

### Processing Attachments

`formsg.crypto.decryptWithAttachments(formSecretKey: string, decryptParams: DecryptParams)` (available from version 0.9.0 onwards) behaves similarly except it will return a `Promise<DecryptedContentAndAttachments | null>`.

`DecryptedContentAndAttachments` is an object containing two fields:

- `content`: the standard form decrypted responses (same as the return type of `formsg.crypto.decrypt`)
- `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.

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.

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.

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.

_Warning:_ We do not have the ability to scan any attachments for malicious content (e.g. spyware or viruses), so careful handling is needed.

## Verifying Signatures Manually

You can use the following information to create a custom solution, although we recommend using this SDK.
Expand Down
Loading