Skip to content

Commit a09a15c

Browse files
committed
export "symmetric", "asymmetric" and "combined"
1 parent a43b0d9 commit a09a15c

File tree

2 files changed

+89
-101
lines changed

2 files changed

+89
-101
lines changed

src/index.spec.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,26 @@
1-
import { sign, verify } from './';
1+
import { SecureWebhooks, symmetric, asymmetric } from './';
22

3-
function testWithKeyPair(pub: string, priv: string, expextedResult: string) {
3+
function testWithKeyPair(
4+
swh: SecureWebhooks,
5+
pub: string,
6+
priv: string,
7+
expextedResult: string
8+
) {
49
const input = 'hello world';
510
const timestamp = 1600000000000;
6-
const signature = sign(input, priv, timestamp);
11+
const signature = swh.sign(input, priv, timestamp);
712
expect(signature).toEqual(expextedResult);
813

914
// transmit input + signature over the wire
1015

1116
const receivingTimestamp = timestamp + 60 * 1000;
1217

13-
const isValidIfWithinTimeout = verify(input, pub, signature, {
18+
const isValidIfWithinTimeout = swh.verify(input, pub, signature, {
1419
timestamp: receivingTimestamp,
1520
});
1621
expect(isValidIfWithinTimeout).toBe(true);
1722

18-
const isValidIfOverTimeout = verify(input, pub, signature, {
23+
const isValidIfOverTimeout = swh.verify(input, pub, signature, {
1924
timestamp: receivingTimestamp,
2025
timeout: 0.5 * 60 * 1000,
2126
});
@@ -25,6 +30,7 @@ function testWithKeyPair(pub: string, priv: string, expextedResult: string) {
2530
test('symmetric', () => {
2631
const secret = 'iamverysecret';
2732
testWithKeyPair(
33+
symmetric,
2834
secret,
2935
secret,
3036
`v=1600000000000,d=3bd0b48300a7a7afc491e90ecef6805cb46813ea047434eed9cccc567f7aeecb`
@@ -102,6 +108,7 @@ yS6CCva4O1bQailhOWHMCXusEMvoSMCg7Xfs8bw1zEqkEGY6yvD3yDoHAOrkfJun
102108
`.trim();
103109

104110
testWithKeyPair(
111+
asymmetric,
105112
publicKey,
106113
privateKey,
107114
`v=1600000000000,d=OwH2f2zKu8DHzCrlFKW36VzOZG6hmhtMCN5kVqX3BQTFkjPl7cri4Ar+yky0nyPNYfZG+Q1Kf3fithV8ufy+q25TZC2IXyhPpnytZTfKptlpLT3BGJ0Q1TMx1gQXrEFN2kzl1pH5ertxN1MwYuUnQCwfwigALKeaStADSZAJVVS25Tp+6DB8OrNpywjeruQZ6fUIWtWcF9q2O987zj+uUqya4GvUsa1NI9PFIXUb82e6ZXpw+0fqLNsHLYj60YqdCfeivxs3O+HGJekvqwJuj/bPCftbDBT4Cj4s5sjFYHtV3Rf2ERzu0ycIJD4BXC3oOyek1PX5lbaIRL7JwMpTWir98FjWEbSSlNbNeR/EEzVtKGwgWcEAlO33H4sBKhc1/OPKcAyMU8NFuudeUSrGSNzl9+qLIJkX+oFyct5iksZPbiyypI2NsrTZcY4W6yOpb5lq8gmNu+N2GfwkK57oL0Kj4q+zZuVrGFRQYTrgWVHbJhMhFpnIKGnFg71oIkfqxjn6dWesADVVgKyLoQ0ZE8E/2kc8Kt+pgPkun/ywMfyItOf9AyIgultGsKyQgPpS3aIioJLExuicNsci7brbXJXMo+grccV7GhgDnbGLn1uLrtE0zCh9d/Op0czWKRbLVzCPNAKIYW1+hkaTNROGs/Cu7vhOy7g766YtEX6EWpo=`

src/index.ts

Lines changed: 77 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -1,105 +1,86 @@
11
import { createHmac, createSign, createVerify } from 'crypto';
22

3-
function getHmac(secret: string) {
4-
return createHmac('sha256', secret);
5-
}
3+
const FIVE_MINUTES = 5 * 60 * 1000;
64

7-
function isPrivateKey(secretOrPrivateKey: string): boolean {
8-
return secretOrPrivateKey.includes('PRIVATE KEY');
5+
function makeSecureWebhooks(
6+
getSigner: (secretOrPrivateKey: string) => (input: string) => string,
7+
getVerifier: (
8+
secretOrPublicKey: string
9+
) => (input: string, digest: string) => boolean
10+
) {
11+
return {
12+
sign(
13+
input: string,
14+
secretOrPrivateKey: string,
15+
timestamp: number = Date.now()
16+
): string {
17+
const signer = getSigner(secretOrPrivateKey);
18+
19+
return `v=${timestamp},d=${signer(input + timestamp)}`;
20+
},
21+
verify(
22+
input: string,
23+
secret: string,
24+
signature: string,
25+
opts: { timeout?: number; timestamp?: number } = {}
26+
): boolean {
27+
const match = /v=(\d+),d=(.*)/.exec(signature);
28+
if (!match) {
29+
return false;
30+
}
31+
32+
const poststamp = Number(match[1]);
33+
const postDigest = match[2];
34+
35+
const timestamp = opts?.timestamp ?? Date.now();
36+
const timeout = opts?.timeout ?? FIVE_MINUTES;
37+
38+
const difference = Math.abs(timestamp - poststamp);
39+
if (difference > timeout) {
40+
return false;
41+
}
42+
43+
const verifier = getVerifier(secret);
44+
45+
return verifier(input + poststamp, postDigest);
46+
},
47+
};
948
}
1049

11-
function isPublicKey(secretOrPrivateKey: string): boolean {
12-
return secretOrPrivateKey.includes('PUBLIC KEY');
13-
}
50+
export type SecureWebhooks = ReturnType<typeof makeSecureWebhooks>;
1451

15-
function verifyWithSecret(
16-
input: string,
17-
digest: string,
18-
secret: string
19-
): boolean {
20-
return (
21-
getHmac(secret)
52+
export const symmetric = makeSecureWebhooks(
53+
secret => input =>
54+
createHmac('sha256', secret)
55+
.update(input)
56+
.digest('hex'),
57+
secret => (input, digest) =>
58+
createHmac('sha256', secret)
2259
.update(input)
2360
.digest('hex') === digest
24-
);
25-
}
26-
27-
function signWithSecret(input: string, secret: string) {
28-
return getHmac(secret)
29-
.update(input)
30-
.digest('hex');
31-
}
32-
33-
function getSigner(secretOrPrivateKey: string): (input: string) => string {
34-
if (isPrivateKey(secretOrPrivateKey)) {
35-
return v => signWithPrivate(v, secretOrPrivateKey);
36-
} else {
37-
return v => signWithSecret(v, secretOrPrivateKey);
38-
}
39-
}
40-
41-
function signWithPrivate(input: string, priv: string): string {
42-
return createSign('sha256')
43-
.update(input)
44-
.sign(priv, 'base64');
45-
}
46-
47-
function verifyWithPublic(input: string, digest: string, pub: string): boolean {
48-
return createVerify('sha256')
49-
.update(input)
50-
.verify(pub, digest, 'base64');
51-
}
52-
53-
function getVerifier(
54-
secretOrPublicKey: string
55-
): (input: string, digest: string) => boolean {
56-
if (isPublicKey(secretOrPublicKey)) {
57-
return (i, d) => verifyWithPublic(i, d, secretOrPublicKey);
58-
} else {
59-
return (i, d) => verifyWithSecret(i, d, secretOrPublicKey);
60-
}
61-
}
61+
);
6262

63-
export function sign(
64-
input: string,
65-
secretOrPrivateKey: string,
66-
timestamp: number = Date.now()
67-
) {
68-
const signer = getSigner(secretOrPrivateKey);
69-
70-
return `v=${timestamp},d=${signer(input + timestamp)}`;
71-
}
72-
73-
const FIVE_MINUTES = 5 * 60 * 1000;
74-
75-
export function verify(
76-
input: string,
77-
secretOrPublicKey: string,
78-
signature: string,
79-
opts: { timeout?: number; timestamp?: number } = {}
80-
) {
81-
const useBase64 = isPublicKey(secretOrPublicKey);
82-
const regex = useBase64
83-
? /v=(\d+),d=((?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?)/
84-
: /v=(\d+),d=([\da-f]+)/;
85-
const match = regex.exec(signature);
86-
87-
if (!match) {
88-
return false;
89-
}
90-
91-
const poststamp = Number(match[1]);
92-
const postDigest = match[2];
93-
94-
const timestamp = opts?.timestamp ?? Date.now();
95-
const timeout = opts?.timeout ?? FIVE_MINUTES;
96-
97-
const difference = Math.abs(timestamp - poststamp);
98-
if (difference > timeout) {
99-
return false;
100-
}
101-
102-
const verifier = getVerifier(secretOrPublicKey);
103-
104-
return verifier(input + poststamp, postDigest);
105-
}
63+
export const asymmetric = makeSecureWebhooks(
64+
priv => input =>
65+
createSign('sha256')
66+
.update(input)
67+
.sign(priv, 'base64'),
68+
pub => (input, digest) =>
69+
createVerify('sha256')
70+
.update(input)
71+
.verify(pub, digest, 'base64')
72+
);
73+
74+
export const combined: SecureWebhooks = {
75+
sign: (input, secretOrPrivateKey, timestamp) =>
76+
secretOrPrivateKey.includes('PRIVATE KEY')
77+
? asymmetric.sign(input, secretOrPrivateKey, timestamp)
78+
: symmetric.sign(input, secretOrPrivateKey, timestamp),
79+
verify: (input, secretOrPublicKey, signature, opts) =>
80+
secretOrPublicKey.includes('PUBLIC KEY')
81+
? asymmetric.verify(input, secretOrPublicKey, signature, opts)
82+
: symmetric.verify(input, secretOrPublicKey, signature, opts),
83+
};
84+
85+
export const sign = symmetric.sign;
86+
export const verify = symmetric.verify;

0 commit comments

Comments
 (0)