@opengovsg/formsg-sdk 0.15.0 → 7.1.2

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.
Files changed (78) hide show
  1. package/README.md +8 -9
  2. package/cjs-entry.cjs +5 -0
  3. package/dist/{crypto-base.js → cjs/crypto-base.js} +7 -9
  4. package/dist/{crypto.js → cjs/crypto.js} +2 -2
  5. package/dist/{index.d.ts → cjs/index.d.ts} +2 -2
  6. package/dist/{index.js → cjs/index.js} +12 -2
  7. package/dist/{util → cjs/util}/publicKey.js +2 -3
  8. package/dist/{util → cjs/util}/signature.js +19 -10
  9. package/dist/{util → cjs/util}/validate.js +2 -3
  10. package/dist/{util → cjs/util}/webhooks.js +17 -7
  11. package/dist/{webhooks.js → cjs/webhooks.js} +17 -7
  12. package/dist/esm/crypto-base.d.ts +25 -0
  13. package/dist/esm/crypto-base.js +86 -0
  14. package/dist/esm/crypto-v3.d.ts +41 -0
  15. package/dist/esm/crypto-v3.js +168 -0
  16. package/dist/esm/crypto.d.ts +41 -0
  17. package/dist/esm/crypto.js +228 -0
  18. package/dist/esm/errors.d.ts +13 -0
  19. package/dist/esm/errors.js +64 -0
  20. package/dist/esm/index.d.ts +20 -0
  21. package/dist/esm/index.js +39 -0
  22. package/dist/esm/resource/signing-keys.d.ts +16 -0
  23. package/dist/esm/resource/signing-keys.js +19 -0
  24. package/dist/esm/resource/verification-keys.d.ts +16 -0
  25. package/dist/esm/resource/verification-keys.js +19 -0
  26. package/dist/esm/types.d.ts +101 -0
  27. package/dist/esm/types.js +1 -0
  28. package/dist/esm/util/crypto.d.ts +42 -0
  29. package/dist/esm/util/crypto.js +74 -0
  30. package/dist/esm/util/parser.d.ts +24 -0
  31. package/dist/esm/util/parser.js +35 -0
  32. package/dist/esm/util/publicKey.d.ts +14 -0
  33. package/dist/esm/util/publicKey.js +38 -0
  34. package/dist/esm/util/signature.d.ts +16 -0
  35. package/dist/esm/util/signature.js +22 -0
  36. package/dist/esm/util/stage.d.ts +5 -0
  37. package/dist/esm/util/stage.js +7 -0
  38. package/dist/esm/util/validate.d.ts +4 -0
  39. package/dist/esm/util/validate.js +32 -0
  40. package/dist/esm/util/webhooks.d.ts +20 -0
  41. package/dist/esm/util/webhooks.js +33 -0
  42. package/dist/esm/verification/index.d.ts +18 -0
  43. package/dist/esm/verification/index.js +77 -0
  44. package/dist/esm/verification/utils.d.ts +12 -0
  45. package/dist/esm/verification/utils.js +18 -0
  46. package/dist/esm/webhooks.d.ts +48 -0
  47. package/dist/esm/webhooks.js +75 -0
  48. package/package.json +48 -32
  49. package/.eslintrc +0 -75
  50. package/.github/workflows/ci.yml +0 -53
  51. package/CHANGELOG.md +0 -177
  52. /package/dist/{crypto-base.d.ts → cjs/crypto-base.d.ts} +0 -0
  53. /package/dist/{crypto-v3.d.ts → cjs/crypto-v3.d.ts} +0 -0
  54. /package/dist/{crypto-v3.js → cjs/crypto-v3.js} +0 -0
  55. /package/dist/{crypto.d.ts → cjs/crypto.d.ts} +0 -0
  56. /package/dist/{errors.d.ts → cjs/errors.d.ts} +0 -0
  57. /package/dist/{errors.js → cjs/errors.js} +0 -0
  58. /package/dist/{resource → cjs/resource}/signing-keys.d.ts +0 -0
  59. /package/dist/{resource → cjs/resource}/signing-keys.js +0 -0
  60. /package/dist/{resource → cjs/resource}/verification-keys.d.ts +0 -0
  61. /package/dist/{resource → cjs/resource}/verification-keys.js +0 -0
  62. /package/dist/{types.d.ts → cjs/types.d.ts} +0 -0
  63. /package/dist/{types.js → cjs/types.js} +0 -0
  64. /package/dist/{util → cjs/util}/crypto.d.ts +0 -0
  65. /package/dist/{util → cjs/util}/crypto.js +0 -0
  66. /package/dist/{util → cjs/util}/parser.d.ts +0 -0
  67. /package/dist/{util → cjs/util}/parser.js +0 -0
  68. /package/dist/{util → cjs/util}/publicKey.d.ts +0 -0
  69. /package/dist/{util → cjs/util}/signature.d.ts +0 -0
  70. /package/dist/{util → cjs/util}/stage.d.ts +0 -0
  71. /package/dist/{util → cjs/util}/stage.js +0 -0
  72. /package/dist/{util → cjs/util}/validate.d.ts +0 -0
  73. /package/dist/{util → cjs/util}/webhooks.d.ts +0 -0
  74. /package/dist/{verification → cjs/verification}/index.d.ts +0 -0
  75. /package/dist/{verification → cjs/verification}/index.js +0 -0
  76. /package/dist/{verification → cjs/verification}/utils.d.ts +0 -0
  77. /package/dist/{verification → cjs/verification}/utils.js +0 -0
  78. /package/dist/{webhooks.d.ts → cjs/webhooks.d.ts} +0 -0
@@ -0,0 +1,74 @@
1
+ import nacl from 'tweetnacl';
2
+ import { decodeBase64, encodeBase64, encodeUTF8 } from 'tweetnacl-util';
3
+ /**
4
+ * Helper method to generate a new keypair for encryption.
5
+ * @returns The generated keypair.
6
+ */
7
+ export var generateKeypair = function () {
8
+ var kp = nacl.box.keyPair();
9
+ return {
10
+ publicKey: encodeBase64(kp.publicKey),
11
+ secretKey: encodeBase64(kp.secretKey),
12
+ };
13
+ };
14
+ /**
15
+ * Helper function to encrypt input with a unique keypair for each submission.
16
+ * @param msg The message to encrypt
17
+ * @param theirPublicKey The base-64 encoded public key
18
+ * @returns The encrypted basestring
19
+ * @throws error if any of the encrypt methods fail
20
+ */
21
+ export var encryptMessage = function (msg, theirPublicKey) {
22
+ var submissionKeypair = generateKeypair();
23
+ var nonce = nacl.randomBytes(24);
24
+ var encrypted = encodeBase64(nacl.box(msg, nonce, decodeBase64(theirPublicKey), decodeBase64(submissionKeypair.secretKey)));
25
+ return "".concat(submissionKeypair.publicKey, ";").concat(encodeBase64(nonce), ":").concat(encrypted);
26
+ };
27
+ /**
28
+ * Helper method to decrypt an encrypted submission.
29
+ * @param formPrivateKey base64
30
+ * @param encryptedContent encrypted string encoded in base64
31
+ * @return The decrypted content, or null if decryption failed.
32
+ */
33
+ export var decryptContent = function (formPrivateKey, encryptedContent) {
34
+ try {
35
+ var _a = encryptedContent.split(';'), submissionPublicKey = _a[0], nonceEncrypted = _a[1];
36
+ var _b = nonceEncrypted.split(':').map(decodeBase64), nonce = _b[0], encrypted = _b[1];
37
+ return nacl.box.open(encrypted, nonce, decodeBase64(submissionPublicKey), decodeBase64(formPrivateKey));
38
+ }
39
+ catch (err) {
40
+ return null;
41
+ }
42
+ };
43
+ /**
44
+ * Helper method to verify a signed message.
45
+ * @param msg the message to verify
46
+ * @param publicKey the public key to authenticate the signed message with
47
+ * @returns the signed message if successful, else an error will be thrown
48
+ * @throws {Error} if the message cannot be verified
49
+ */
50
+ export var verifySignedMessage = function (msg, publicKey) {
51
+ var openedMessage = nacl.sign.open(msg, decodeBase64(publicKey));
52
+ if (!openedMessage)
53
+ throw new Error('Failed to open signed message with given public key');
54
+ return JSON.parse(encodeUTF8(openedMessage));
55
+ };
56
+ /**
57
+ * Helper method to check if all the field IDs given are within the filenames
58
+ * @param fieldIds the list of fieldIds to check
59
+ * @param filenames the filenames that should contain the fields
60
+ * @returns boolean indicating whether the fields are valid
61
+ */
62
+ export var areAttachmentFieldIdsValid = function (fieldIds, filenames) {
63
+ return fieldIds.every(function (fieldId) { return filenames[fieldId]; });
64
+ };
65
+ /**
66
+ * Converts an encrypted attachment to encrypted file content
67
+ * @param encryptedAttachment The encrypted attachment
68
+ * @returns EncryptedFileContent The encrypted file content
69
+ */
70
+ export var convertEncryptedAttachmentToFileContent = function (encryptedAttachment) { return ({
71
+ submissionPublicKey: encryptedAttachment.encryptedFile.submissionPublicKey,
72
+ nonce: encryptedAttachment.encryptedFile.nonce,
73
+ binary: decodeBase64(encryptedAttachment.encryptedFile.binary),
74
+ }); };
@@ -0,0 +1,24 @@
1
+ export type HeaderSignature = {
2
+ v1: string;
3
+ t: number;
4
+ s: string;
5
+ f: string;
6
+ };
7
+ export type VerificationSignature = {
8
+ v: string;
9
+ t: number;
10
+ s: string;
11
+ f: string;
12
+ };
13
+ /**
14
+ * Parses the X-FormSG-Signature header into its constituents
15
+ * @param header The X-FormSG-Signature header
16
+ * @returns The signature header constituents
17
+ */
18
+ export declare const parseSignatureHeader: (header: string) => HeaderSignature;
19
+ /**
20
+ * Parses the verification signature into its constituent
21
+ * @param signature The verification signature
22
+ * @returns The verification signature constituents
23
+ */
24
+ export declare const parseVerificationSignature: (signature: string) => VerificationSignature;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Helper function to retrieve keys-values in a signature.
3
+ * @param signature The signature to convert to a keymap
4
+ * @returns The key-value map of the signature
5
+ */
6
+ var signatureToKeyMap = function (signature) {
7
+ return signature
8
+ .split(',')
9
+ .map(function (kv) { return kv.split(/=(.*)/); })
10
+ .reduce(function (acc, _a) {
11
+ var k = _a[0], v = _a[1];
12
+ acc[k] = v;
13
+ return acc;
14
+ }, {});
15
+ };
16
+ /**
17
+ * Parses the X-FormSG-Signature header into its constituents
18
+ * @param header The X-FormSG-Signature header
19
+ * @returns The signature header constituents
20
+ */
21
+ export var parseSignatureHeader = function (header) {
22
+ var parsedSignature = signatureToKeyMap(header);
23
+ parsedSignature.t = Number(parsedSignature.t);
24
+ return parsedSignature;
25
+ };
26
+ /**
27
+ * Parses the verification signature into its constituent
28
+ * @param signature The verification signature
29
+ * @returns The verification signature constituents
30
+ */
31
+ export var parseVerificationSignature = function (signature) {
32
+ var parsedSignature = signatureToKeyMap(signature);
33
+ parsedSignature.t = Number(parsedSignature.t);
34
+ return parsedSignature;
35
+ };
@@ -0,0 +1,14 @@
1
+ import { PackageMode } from '../types';
2
+ /**
3
+ * Retrieves the appropriate signing public key.
4
+ * Defaults to production.
5
+ * @param mode The package mode to retrieve the public key for.
6
+ */
7
+ declare function getSigningPublicKey(mode?: PackageMode): string;
8
+ /**
9
+ * Retrieves the appropriate verification public key.
10
+ * Defaults to production.
11
+ * @param mode The package mode to retrieve the public key for.
12
+ */
13
+ declare function getVerificationPublicKey(mode?: PackageMode): string;
14
+ export { getSigningPublicKey, getVerificationPublicKey };
@@ -0,0 +1,38 @@
1
+ import { SIGNING_KEYS } from '../resource/signing-keys';
2
+ import { VERIFICATION_KEYS } from '../resource/verification-keys';
3
+ import STAGE from './stage';
4
+ /**
5
+ * Retrieves the appropriate signing public key.
6
+ * Defaults to production.
7
+ * @param mode The package mode to retrieve the public key for.
8
+ */
9
+ function getSigningPublicKey(mode) {
10
+ switch (mode) {
11
+ case STAGE.development:
12
+ return SIGNING_KEYS.development.publicKey;
13
+ case STAGE.staging:
14
+ return SIGNING_KEYS.staging.publicKey;
15
+ case STAGE.test:
16
+ return SIGNING_KEYS.test.publicKey;
17
+ default:
18
+ return SIGNING_KEYS.production.publicKey;
19
+ }
20
+ }
21
+ /**
22
+ * Retrieves the appropriate verification public key.
23
+ * Defaults to production.
24
+ * @param mode The package mode to retrieve the public key for.
25
+ */
26
+ function getVerificationPublicKey(mode) {
27
+ switch (mode) {
28
+ case STAGE.development:
29
+ return VERIFICATION_KEYS.development.publicKey;
30
+ case STAGE.staging:
31
+ return VERIFICATION_KEYS.staging.publicKey;
32
+ case STAGE.test:
33
+ return VERIFICATION_KEYS.test.publicKey;
34
+ default:
35
+ return VERIFICATION_KEYS.production.publicKey;
36
+ }
37
+ }
38
+ export { getSigningPublicKey, getVerificationPublicKey };
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Returns a signature from a basestring and secret key
3
+ * @param basestring The data you want to sign.
4
+ * @param secretKey 64-byte secret key in base64 encoding.
5
+ * @return base64 encoded signature
6
+ */
7
+ declare function sign(basestring: string, secretKey: string): string;
8
+ /**
9
+ * Verifies a signature against a message and public key
10
+ * @param message The message to verify
11
+ * @param signature The base64 encoded signature generated from sign()
12
+ * @param publicKey 32-byte public key in base64 encoding
13
+ * @return True if verification checks out, false otherwise
14
+ */
15
+ declare function verify(message: string, signature: string, publicKey: string): boolean;
16
+ export { sign, verify };
@@ -0,0 +1,22 @@
1
+ import * as tweetnacl from 'tweetnacl';
2
+ import { decodeBase64, decodeUTF8, encodeBase64 } from 'tweetnacl-util';
3
+ /**
4
+ * Returns a signature from a basestring and secret key
5
+ * @param basestring The data you want to sign.
6
+ * @param secretKey 64-byte secret key in base64 encoding.
7
+ * @return base64 encoded signature
8
+ */
9
+ function sign(basestring, secretKey) {
10
+ return encodeBase64(tweetnacl.sign.detached(decodeUTF8(basestring), decodeBase64(secretKey)));
11
+ }
12
+ /**
13
+ * Verifies a signature against a message and public key
14
+ * @param message The message to verify
15
+ * @param signature The base64 encoded signature generated from sign()
16
+ * @param publicKey 32-byte public key in base64 encoding
17
+ * @return True if verification checks out, false otherwise
18
+ */
19
+ function verify(message, signature, publicKey) {
20
+ return tweetnacl.sign.detached.verify(decodeUTF8(message), decodeBase64(signature), decodeBase64(publicKey));
21
+ }
22
+ export { sign, verify };
@@ -0,0 +1,5 @@
1
+ import { PackageMode } from '../types';
2
+ declare const STAGE: {
3
+ [stage in PackageMode]: stage;
4
+ };
5
+ export default STAGE;
@@ -0,0 +1,7 @@
1
+ var STAGE = {
2
+ staging: 'staging',
3
+ production: 'production',
4
+ development: 'development',
5
+ test: 'test',
6
+ };
7
+ export default STAGE;
@@ -0,0 +1,4 @@
1
+ import { FormField, FormFieldsV3 } from '../types';
2
+ declare function determineIsFormFields(tbd: any): tbd is FormField[];
3
+ declare function determineIsFormFieldsV3(tbd: any): tbd is FormFieldsV3;
4
+ export { determineIsFormFields, determineIsFormFieldsV3 };
@@ -0,0 +1,32 @@
1
+ function determineIsFormFields(tbd) {
2
+ if (!Array.isArray(tbd)) {
3
+ return false;
4
+ }
5
+ // If there exists even a single internal response that does not fit the
6
+ // shape, the object is not created properly.
7
+ var filter = tbd.filter(function (internal) {
8
+ // Have either answer or answerArray or is isHeader
9
+ // Since empty strings are allowed, check using typeof.
10
+ return (typeof internal.answer === 'string' ||
11
+ Array.isArray(internal.answerArray) ||
12
+ internal.isHeader) &&
13
+ internal._id &&
14
+ internal.fieldType &&
15
+ // The field is still valid even when the question title is empty string
16
+ // (even though it is not intended behavior).
17
+ typeof internal.question === 'string';
18
+ });
19
+ return filter.length === tbd.length;
20
+ }
21
+ // TODO(MRF): This is currently very rudimentary, we should look at making this more specific where required.
22
+ function determineIsFormFieldsV3(tbd) {
23
+ for (var _i = 0, _a = Object.keys(tbd); _i < _a.length; _i++) {
24
+ var id = _a[_i];
25
+ var value = tbd[id];
26
+ var hasCorrectShape = value.fieldType && value.answer !== undefined;
27
+ if (!hasCorrectShape)
28
+ return false;
29
+ }
30
+ return true;
31
+ }
32
+ export { determineIsFormFields, determineIsFormFieldsV3 };
@@ -0,0 +1,20 @@
1
+ import { HeaderSignature } from './parser';
2
+ /**
3
+ * Helper function to construct the basestring and verify the signature of an
4
+ * incoming request
5
+ * @param uri incoming request to verify
6
+ * @param signatureHeader the X-FormSG-Signature header to verify against
7
+ * @returns true if verification succeeds, false otherwise
8
+ * @throws {WebhookAuthenticateError} if given signature header is malformed.
9
+ */
10
+ declare const isSignatureHeaderValid: (uri: string, signatureHeader: HeaderSignature, publicKey: string) => boolean;
11
+ /**
12
+ * Helper function to verify that the epoch submitted is recent and valid.
13
+ * Prevents against replay attacks. Allows for negative time interval
14
+ * in case of clock drift between Form servers and recipient server.
15
+ * @param epoch The number of milliseconds since 1 Jan 1970 00:00:00 UTC.
16
+ * @param expiry Duration of expiry in milliseconds. The default is 5 minutes.
17
+ * @returns true if the epoch given has exceeded expiry duration calculated from current time.
18
+ */
19
+ declare const hasEpochExpired: (epoch: number, expiry?: number) => boolean;
20
+ export { isSignatureHeaderValid, hasEpochExpired };
@@ -0,0 +1,33 @@
1
+ import * as url from 'url';
2
+ import { WebhookAuthenticateError } from '../errors';
3
+ import { verify } from './signature';
4
+ /**
5
+ * Helper function to construct the basestring and verify the signature of an
6
+ * incoming request
7
+ * @param uri incoming request to verify
8
+ * @param signatureHeader the X-FormSG-Signature header to verify against
9
+ * @returns true if verification succeeds, false otherwise
10
+ * @throws {WebhookAuthenticateError} if given signature header is malformed.
11
+ */
12
+ var isSignatureHeaderValid = function (uri, signatureHeader, publicKey) {
13
+ var signature = signatureHeader.v1, epoch = signatureHeader.t, submissionId = signatureHeader.s, formId = signatureHeader.f;
14
+ if (!epoch || !signature || !submissionId || !formId) {
15
+ throw new WebhookAuthenticateError('X-FormSG-Signature header is invalid');
16
+ }
17
+ var baseString = "".concat(url.parse(uri).href, ".").concat(submissionId, ".").concat(formId, ".").concat(epoch);
18
+ return verify(baseString, signature, publicKey);
19
+ };
20
+ /**
21
+ * Helper function to verify that the epoch submitted is recent and valid.
22
+ * Prevents against replay attacks. Allows for negative time interval
23
+ * in case of clock drift between Form servers and recipient server.
24
+ * @param epoch The number of milliseconds since 1 Jan 1970 00:00:00 UTC.
25
+ * @param expiry Duration of expiry in milliseconds. The default is 5 minutes.
26
+ * @returns true if the epoch given has exceeded expiry duration calculated from current time.
27
+ */
28
+ var hasEpochExpired = function (epoch, expiry) {
29
+ if (expiry === void 0) { expiry = 300000; }
30
+ var difference = Math.abs(Date.now() - epoch);
31
+ return difference > expiry;
32
+ };
33
+ export { isSignatureHeaderValid, hasEpochExpired };
@@ -0,0 +1,18 @@
1
+ import { VerificationAuthenticateOptions, VerificationOptions, VerificationSignatureOptions } from '../types';
2
+ export default class Verification {
3
+ verificationPublicKey?: string;
4
+ verificationSecretKey?: string;
5
+ transactionExpiry?: number;
6
+ constructor(params?: VerificationOptions);
7
+ /**
8
+ * Verifies signature
9
+ * @param {object} data
10
+ * @param {string} data.signatureString
11
+ * @param {number} data.submissionCreatedAt date in milliseconds
12
+ * @param {string} data.fieldId
13
+ * @param {string} data.answer
14
+ * @param {string} data.publicKey
15
+ */
16
+ authenticate: ({ signatureString, submissionCreatedAt, fieldId, answer, }: VerificationAuthenticateOptions) => boolean;
17
+ generateSignature: ({ transactionId, formId, fieldId, answer, }: VerificationSignatureOptions) => string;
18
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * @file Manages verification of otp form fields (email, sms, whatsapp)
3
+ * @author Jean Tan
4
+ */
5
+ import nacl from 'tweetnacl';
6
+ import { decodeBase64, decodeUTF8, encodeBase64 } from 'tweetnacl-util';
7
+ import { MissingPublicKeyError, MissingSecretKeyError } from '../errors';
8
+ import { parseVerificationSignature } from '../util/parser';
9
+ import { formatToBaseString, isSignatureTimeValid } from './utils';
10
+ var Verification = /** @class */ (function () {
11
+ function Verification(params) {
12
+ var _this = this;
13
+ /**
14
+ * Verifies signature
15
+ * @param {object} data
16
+ * @param {string} data.signatureString
17
+ * @param {number} data.submissionCreatedAt date in milliseconds
18
+ * @param {string} data.fieldId
19
+ * @param {string} data.answer
20
+ * @param {string} data.publicKey
21
+ */
22
+ this.authenticate = function (_a) {
23
+ var signatureString = _a.signatureString, submissionCreatedAt = _a.submissionCreatedAt, fieldId = _a.fieldId, answer = _a.answer;
24
+ if (!_this.transactionExpiry) {
25
+ throw new Error('Provide a transaction expiry when when initializing the FormSG SDK to use this function.');
26
+ }
27
+ if (!_this.verificationPublicKey) {
28
+ throw new MissingPublicKeyError();
29
+ }
30
+ try {
31
+ var _b = parseVerificationSignature(signatureString), transactionId = _b.v, time = _b.t, formId = _b.f, signature = _b.s;
32
+ if (!time) {
33
+ throw new Error('Malformed signature string was passed into function');
34
+ }
35
+ if (isSignatureTimeValid(time, submissionCreatedAt, _this.transactionExpiry)) {
36
+ var data = formatToBaseString({
37
+ transactionId: transactionId,
38
+ formId: formId,
39
+ fieldId: fieldId,
40
+ answer: answer,
41
+ time: time,
42
+ });
43
+ return nacl.sign.detached.verify(decodeUTF8(data), decodeBase64(signature), decodeBase64(_this.verificationPublicKey));
44
+ }
45
+ else {
46
+ console.info("Signature was expired for signatureString=\"".concat(signatureString, "\" signatureDate=\"").concat(time, "\" submissionCreatedAt=\"").concat(submissionCreatedAt, "\""));
47
+ return false;
48
+ }
49
+ }
50
+ catch (error) {
51
+ console.error("An error occurred for signatureString=\"".concat(signatureString, "\" submissionCreatedAt=\"").concat(submissionCreatedAt, "\" fieldId=\"").concat(fieldId, "\" answer=\"").concat(answer, "\" error=\"").concat(error, "\""));
52
+ return false;
53
+ }
54
+ };
55
+ this.generateSignature = function (_a) {
56
+ var transactionId = _a.transactionId, formId = _a.formId, fieldId = _a.fieldId, answer = _a.answer;
57
+ if (!_this.verificationSecretKey) {
58
+ throw new MissingSecretKeyError('Provide a secret key when when initializing the Verification class to use this function.');
59
+ }
60
+ var time = Date.now();
61
+ var data = formatToBaseString({
62
+ transactionId: transactionId,
63
+ formId: formId,
64
+ fieldId: fieldId,
65
+ answer: answer,
66
+ time: time,
67
+ });
68
+ var signature = nacl.sign.detached(decodeUTF8(data), decodeBase64(_this.verificationSecretKey));
69
+ return "f=".concat(formId, ",v=").concat(transactionId, ",t=").concat(time, ",s=").concat(encodeBase64(signature));
70
+ };
71
+ this.verificationPublicKey = params === null || params === void 0 ? void 0 : params.publicKey;
72
+ this.verificationSecretKey = params === null || params === void 0 ? void 0 : params.secretKey;
73
+ this.transactionExpiry = params === null || params === void 0 ? void 0 : params.transactionExpiry;
74
+ }
75
+ return Verification;
76
+ }());
77
+ export default Verification;
@@ -0,0 +1,12 @@
1
+ import { VerificationBasestringOptions } from '../types';
2
+ /**
3
+ * Checks if signature was made within the given expiry range before
4
+ * submission was created.
5
+ * @param signatureTime ms
6
+ * @param submissionCreatedAt ms
7
+ */
8
+ export declare const isSignatureTimeValid: (signatureTime: number, submissionCreatedAt: number, transactionExpiry: number) => boolean;
9
+ /**
10
+ * Formats given data into a string for signing
11
+ */
12
+ export declare const formatToBaseString: ({ transactionId, formId, fieldId, answer, time, }: VerificationBasestringOptions) => string;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Checks if signature was made within the given expiry range before
3
+ * submission was created.
4
+ * @param signatureTime ms
5
+ * @param submissionCreatedAt ms
6
+ */
7
+ export var isSignatureTimeValid = function (signatureTime, submissionCreatedAt, transactionExpiry) {
8
+ var maxTime = submissionCreatedAt;
9
+ var minTime = maxTime - transactionExpiry * 1000;
10
+ return signatureTime > minTime && signatureTime < maxTime;
11
+ };
12
+ /**
13
+ * Formats given data into a string for signing
14
+ */
15
+ export var formatToBaseString = function (_a) {
16
+ var transactionId = _a.transactionId, formId = _a.formId, fieldId = _a.fieldId, answer = _a.answer, time = _a.time;
17
+ return "".concat(transactionId, ".").concat(formId, ".").concat(fieldId, ".").concat(answer, ".").concat(time);
18
+ };
@@ -0,0 +1,48 @@
1
+ export default class Webhooks {
2
+ publicKey: string;
3
+ secretKey?: string;
4
+ constructor({ publicKey, secretKey, }: {
5
+ publicKey: string;
6
+ secretKey?: string;
7
+ });
8
+ /**
9
+ * Injects the webhook public key for authentication
10
+ * @param header X-FormSG-Signature header
11
+ * @param uri The endpoint that FormSG is POSTing to
12
+ * @returns true if the header is verified
13
+ * @throws {WebhookAuthenticateError} If the signature or uri cannot be verified
14
+ */
15
+ authenticate: (header: string, uri: string) => boolean;
16
+ /**
17
+ * Generates a signature based on the URI, submission ID and epoch timestamp.
18
+ * @param params The parameters needed to generate the signature
19
+ * @param params.uri Full URL of the request
20
+ * @param params.submissionId Submission Mongo ObjectId saved to the database
21
+ * @param params.epoch Number of milliseconds since Jan 1, 1970
22
+ * @returns the generated signature
23
+ * @throws {MissingSecretKeyError} if a secret key is not provided when instantiating this class
24
+ * @throws {TypeError} if any parameters are undefined
25
+ */
26
+ generateSignature: ({ uri, submissionId, formId, epoch, }: {
27
+ uri: string;
28
+ submissionId: string;
29
+ formId: string;
30
+ epoch: number;
31
+ }) => string;
32
+ /**
33
+ * Constructs the `X-FormSG-Signature` header
34
+ * @param params The parameters needed to construct the header
35
+ * @param params.epoch Epoch timestamp
36
+ * @param params.submissionId Mongo ObjectId
37
+ * @param params.formId Mongo ObjectId
38
+ * @param params.signature A signature generated by the generateSignature() function
39
+ * @returns The `X-FormSG-Signature` header
40
+ * @throws {Error} if a secret key is not provided when instantiating this class
41
+ */
42
+ constructHeader: ({ epoch, submissionId, formId, signature, }: {
43
+ epoch: number;
44
+ submissionId: string;
45
+ formId: string;
46
+ signature: string;
47
+ }) => string;
48
+ }
@@ -0,0 +1,75 @@
1
+ import * as url from 'url';
2
+ import { parseSignatureHeader } from './util/parser';
3
+ import { sign } from './util/signature';
4
+ import { hasEpochExpired, isSignatureHeaderValid } from './util/webhooks';
5
+ import { MissingSecretKeyError, WebhookAuthenticateError } from './errors';
6
+ var Webhooks = /** @class */ (function () {
7
+ function Webhooks(_a) {
8
+ var publicKey = _a.publicKey, secretKey = _a.secretKey;
9
+ var _this = this;
10
+ /**
11
+ * Injects the webhook public key for authentication
12
+ * @param header X-FormSG-Signature header
13
+ * @param uri The endpoint that FormSG is POSTing to
14
+ * @returns true if the header is verified
15
+ * @throws {WebhookAuthenticateError} If the signature or uri cannot be verified
16
+ */
17
+ this.authenticate = function (header, uri) {
18
+ // Parse the header
19
+ var signatureHeader = parseSignatureHeader(header);
20
+ var signature = signatureHeader.v1, epoch = signatureHeader.t, submissionId = signatureHeader.s, formId = signatureHeader.f;
21
+ // Verify signature authenticity
22
+ if (!isSignatureHeaderValid(uri, signatureHeader, _this.publicKey)) {
23
+ throw new WebhookAuthenticateError("Signature could not be verified for uri=".concat(uri, " submissionId=").concat(submissionId, " formId=").concat(formId, " epoch=").concat(epoch, " signature=").concat(signature));
24
+ }
25
+ // Verify epoch recency
26
+ if (hasEpochExpired(epoch)) {
27
+ throw new WebhookAuthenticateError("Signature is not recent for uri=".concat(uri, " submissionId=").concat(submissionId, " formId=").concat(formId, " epoch=").concat(epoch, " signature=").concat(signature));
28
+ }
29
+ // All checks pass.
30
+ return true;
31
+ };
32
+ /**
33
+ * Generates a signature based on the URI, submission ID and epoch timestamp.
34
+ * @param params The parameters needed to generate the signature
35
+ * @param params.uri Full URL of the request
36
+ * @param params.submissionId Submission Mongo ObjectId saved to the database
37
+ * @param params.epoch Number of milliseconds since Jan 1, 1970
38
+ * @returns the generated signature
39
+ * @throws {MissingSecretKeyError} if a secret key is not provided when instantiating this class
40
+ * @throws {TypeError} if any parameters are undefined
41
+ */
42
+ this.generateSignature = function (_a) {
43
+ var uri = _a.uri, submissionId = _a.submissionId, formId = _a.formId, epoch = _a.epoch;
44
+ if (!_this.secretKey) {
45
+ throw new MissingSecretKeyError();
46
+ }
47
+ if (!submissionId || !uri || !formId || !epoch) {
48
+ throw new TypeError('submissionId, uri, formId, or epoch must be provided to generate a webhook signature');
49
+ }
50
+ var baseString = "".concat(url.parse(uri).href, ".").concat(submissionId, ".").concat(formId, ".").concat(epoch);
51
+ return sign(baseString, _this.secretKey);
52
+ };
53
+ /**
54
+ * Constructs the `X-FormSG-Signature` header
55
+ * @param params The parameters needed to construct the header
56
+ * @param params.epoch Epoch timestamp
57
+ * @param params.submissionId Mongo ObjectId
58
+ * @param params.formId Mongo ObjectId
59
+ * @param params.signature A signature generated by the generateSignature() function
60
+ * @returns The `X-FormSG-Signature` header
61
+ * @throws {Error} if a secret key is not provided when instantiating this class
62
+ */
63
+ this.constructHeader = function (_a) {
64
+ var epoch = _a.epoch, submissionId = _a.submissionId, formId = _a.formId, signature = _a.signature;
65
+ if (!_this.secretKey) {
66
+ throw new MissingSecretKeyError();
67
+ }
68
+ return "t=".concat(epoch, ",s=").concat(submissionId, ",f=").concat(formId, ",v1=").concat(signature);
69
+ };
70
+ this.publicKey = publicKey;
71
+ this.secretKey = secretKey;
72
+ }
73
+ return Webhooks;
74
+ }());
75
+ export default Webhooks;