@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.
- package/README.md +8 -9
- package/cjs-entry.cjs +5 -0
- package/dist/{crypto-base.js → cjs/crypto-base.js} +7 -9
- package/dist/{crypto.js → cjs/crypto.js} +2 -2
- package/dist/{index.d.ts → cjs/index.d.ts} +2 -2
- package/dist/{index.js → cjs/index.js} +12 -2
- package/dist/{util → cjs/util}/publicKey.js +2 -3
- package/dist/{util → cjs/util}/signature.js +19 -10
- package/dist/{util → cjs/util}/validate.js +2 -3
- package/dist/{util → cjs/util}/webhooks.js +17 -7
- package/dist/{webhooks.js → cjs/webhooks.js} +17 -7
- package/dist/esm/crypto-base.d.ts +25 -0
- package/dist/esm/crypto-base.js +86 -0
- package/dist/esm/crypto-v3.d.ts +41 -0
- package/dist/esm/crypto-v3.js +168 -0
- package/dist/esm/crypto.d.ts +41 -0
- package/dist/esm/crypto.js +228 -0
- package/dist/esm/errors.d.ts +13 -0
- package/dist/esm/errors.js +64 -0
- package/dist/esm/index.d.ts +20 -0
- package/dist/esm/index.js +39 -0
- package/dist/esm/resource/signing-keys.d.ts +16 -0
- package/dist/esm/resource/signing-keys.js +19 -0
- package/dist/esm/resource/verification-keys.d.ts +16 -0
- package/dist/esm/resource/verification-keys.js +19 -0
- package/dist/esm/types.d.ts +101 -0
- package/dist/esm/types.js +1 -0
- package/dist/esm/util/crypto.d.ts +42 -0
- package/dist/esm/util/crypto.js +74 -0
- package/dist/esm/util/parser.d.ts +24 -0
- package/dist/esm/util/parser.js +35 -0
- package/dist/esm/util/publicKey.d.ts +14 -0
- package/dist/esm/util/publicKey.js +38 -0
- package/dist/esm/util/signature.d.ts +16 -0
- package/dist/esm/util/signature.js +22 -0
- package/dist/esm/util/stage.d.ts +5 -0
- package/dist/esm/util/stage.js +7 -0
- package/dist/esm/util/validate.d.ts +4 -0
- package/dist/esm/util/validate.js +32 -0
- package/dist/esm/util/webhooks.d.ts +20 -0
- package/dist/esm/util/webhooks.js +33 -0
- package/dist/esm/verification/index.d.ts +18 -0
- package/dist/esm/verification/index.js +77 -0
- package/dist/esm/verification/utils.d.ts +12 -0
- package/dist/esm/verification/utils.js +18 -0
- package/dist/esm/webhooks.d.ts +48 -0
- package/dist/esm/webhooks.js +75 -0
- package/package.json +48 -32
- package/.eslintrc +0 -75
- package/.github/workflows/ci.yml +0 -53
- package/CHANGELOG.md +0 -177
- /package/dist/{crypto-base.d.ts → cjs/crypto-base.d.ts} +0 -0
- /package/dist/{crypto-v3.d.ts → cjs/crypto-v3.d.ts} +0 -0
- /package/dist/{crypto-v3.js → cjs/crypto-v3.js} +0 -0
- /package/dist/{crypto.d.ts → cjs/crypto.d.ts} +0 -0
- /package/dist/{errors.d.ts → cjs/errors.d.ts} +0 -0
- /package/dist/{errors.js → cjs/errors.js} +0 -0
- /package/dist/{resource → cjs/resource}/signing-keys.d.ts +0 -0
- /package/dist/{resource → cjs/resource}/signing-keys.js +0 -0
- /package/dist/{resource → cjs/resource}/verification-keys.d.ts +0 -0
- /package/dist/{resource → cjs/resource}/verification-keys.js +0 -0
- /package/dist/{types.d.ts → cjs/types.d.ts} +0 -0
- /package/dist/{types.js → cjs/types.js} +0 -0
- /package/dist/{util → cjs/util}/crypto.d.ts +0 -0
- /package/dist/{util → cjs/util}/crypto.js +0 -0
- /package/dist/{util → cjs/util}/parser.d.ts +0 -0
- /package/dist/{util → cjs/util}/parser.js +0 -0
- /package/dist/{util → cjs/util}/publicKey.d.ts +0 -0
- /package/dist/{util → cjs/util}/signature.d.ts +0 -0
- /package/dist/{util → cjs/util}/stage.d.ts +0 -0
- /package/dist/{util → cjs/util}/stage.js +0 -0
- /package/dist/{util → cjs/util}/validate.d.ts +0 -0
- /package/dist/{util → cjs/util}/webhooks.d.ts +0 -0
- /package/dist/{verification → cjs/verification}/index.d.ts +0 -0
- /package/dist/{verification → cjs/verification}/index.js +0 -0
- /package/dist/{verification → cjs/verification}/utils.d.ts +0 -0
- /package/dist/{verification → cjs/verification}/utils.js +0 -0
- /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,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;
|