Skip to content

Commit 1354d0b

Browse files
authored
Merge pull request #30 from quirrel-dev/asymmetric-signatures
asymmetric-signatures
2 parents cbe59e0 + a09a15c commit 1354d0b

File tree

2 files changed

+173
-44
lines changed

2 files changed

+173
-44
lines changed

src/index.spec.ts

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

3-
test('the whole flow', () => {
3+
function testWithKeyPair(
4+
swh: SecureWebhooks,
5+
pub: string,
6+
priv: string,
7+
expextedResult: string
8+
) {
49
const input = 'hello world';
5-
const secret = 'iamverysecret';
610
const timestamp = 1600000000000;
7-
const signature = sign(input, secret, timestamp);
8-
expect(signature).toMatchInlineSnapshot(
9-
`"v=1600000000000,d=3bd0b48300a7a7afc491e90ecef6805cb46813ea047434eed9cccc567f7aeecb"`
10-
);
11+
const signature = swh.sign(input, priv, timestamp);
12+
expect(signature).toEqual(expextedResult);
1113

1214
// transmit input + signature over the wire
1315

1416
const receivingTimestamp = timestamp + 60 * 1000;
1517

16-
const isValidIfWithinTimeout = verify(input, secret, signature, {
18+
const isValidIfWithinTimeout = swh.verify(input, pub, signature, {
1719
timestamp: receivingTimestamp,
1820
});
1921
expect(isValidIfWithinTimeout).toBe(true);
2022

21-
const isValidIfOverTimeout = verify(input, secret, signature, {
23+
const isValidIfOverTimeout = swh.verify(input, pub, signature, {
2224
timestamp: receivingTimestamp,
2325
timeout: 0.5 * 60 * 1000,
2426
});
2527
expect(isValidIfOverTimeout).toBe(false);
28+
}
29+
30+
test('symmetric', () => {
31+
const secret = 'iamverysecret';
32+
testWithKeyPair(
33+
symmetric,
34+
secret,
35+
secret,
36+
`v=1600000000000,d=3bd0b48300a7a7afc491e90ecef6805cb46813ea047434eed9cccc567f7aeecb`
37+
);
38+
});
39+
40+
test('asymmetric', () => {
41+
const publicKey = `
42+
-----BEGIN RSA PUBLIC KEY-----
43+
MIICCgKCAgEAqmeVACnRiy7Em4TJ2hevBQJek3ryctBaMQZKULAUg/+3Gk3CNgY/
44+
ZoWpFFlwnlIb60KuNAIIev8PEd63F3Ut98UaiTiZnsqMN6j7SyPoSDMP5viFZN8p
45+
u+xwn6VoGS0y/7fGs8kV+4i96dh2ev0t704E8l8CApVi607IN9NhaAgZrYHW4kTZ
46+
b1UNcVSKYiG3U4J2e6HNeSIon4Ww7tdM/6JEJWmKYybRzlGVDjZ5NYqRE+kTK1Dv
47+
lj525aJEVPy8AZzH53AwfIpukoz571voMKWgSypiyKPsXxs6AtaZfpWxLfz4VF0U
48+
lPr3Shf1CUls4llr6bFBRWub7YhQgiwF14KkdmU/g5DnqiW8jG5P+D5suV8kLUaq
49+
tm47IfrGhb548/lrh6f9/fFgx+YXjweMdamh5jggjFPUGM3X7glIhl29v/js8HXy
50+
rFyuj97rS2G3ni36CAvTmZs4FCepmJAF8EWFSUo4193mrFjuOeaWfGxqwUQ14W1T
51+
sHVEDWgptpO8RU3BNtP3dItY1iTbmIFG2lisGb7ynitJ1vItme0Yd6MeelQjVRTi
52+
9vfHQawfm5D8kW6dT/ozKFMmffIphQkgwa4xcerb9UQRMMMzgOy3HvQO2b7u0Wt/
53+
sm4IJlmI1/lBDYlcm4jt/69CvFw1C+3Z2ud5X92lVG0AqFzycFCMyzUCAwEAAQ==
54+
-----END RSA PUBLIC KEY-----
55+
`.trim();
56+
const privateKey = `
57+
-----BEGIN RSA PRIVATE KEY-----
58+
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
107+
-----END RSA PRIVATE KEY-----
108+
`.trim();
109+
110+
testWithKeyPair(
111+
asymmetric,
112+
publicKey,
113+
privateKey,
114+
`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=`
115+
);
26116
});

src/index.ts

Lines changed: 74 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,86 @@
1-
import { createHmac } from 'crypto';
1+
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-
export function sign(
8-
input: string,
9-
secret: string,
10-
timestamp: number = Date.now()
5+
function makeSecureWebhooks(
6+
getSigner: (secretOrPrivateKey: string) => (input: string) => string,
7+
getVerifier: (
8+
secretOrPublicKey: string
9+
) => (input: string, digest: string) => boolean
1110
) {
12-
const hmac = getHmac(secret);
11+
return {
12+
sign(
13+
input: string,
14+
secretOrPrivateKey: string,
15+
timestamp: number = Date.now()
16+
): string {
17+
const signer = getSigner(secretOrPrivateKey);
1318

14-
hmac.update(input + timestamp);
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+
}
1531

16-
return `v=${timestamp},d=${hmac.digest('hex')}`;
17-
}
32+
const poststamp = Number(match[1]);
33+
const postDigest = match[2];
1834

19-
const FIVE_MINUTES = 5 * 60 * 1000;
35+
const timestamp = opts?.timestamp ?? Date.now();
36+
const timeout = opts?.timeout ?? FIVE_MINUTES;
2037

21-
export function verify(
22-
input: string,
23-
secret: string,
24-
signature: string,
25-
opts: { timeout?: number; timestamp?: number } = {}
26-
) {
27-
const match = /v=(\d+),d=([\da-f]+)/.exec(signature);
28-
if (!match) {
29-
return false;
30-
}
38+
const difference = Math.abs(timestamp - poststamp);
39+
if (difference > timeout) {
40+
return false;
41+
}
3142

32-
const poststamp = Number(match[1]);
33-
const postDigest = match[2];
43+
const verifier = getVerifier(secret);
3444

35-
const timestamp = opts?.timestamp ?? Date.now();
36-
const timeout = opts?.timeout ?? FIVE_MINUTES;
45+
return verifier(input + poststamp, postDigest);
46+
},
47+
};
48+
}
3749

38-
const difference = Math.abs(timestamp - poststamp);
39-
if (difference > timeout) {
40-
return false;
41-
}
50+
export type SecureWebhooks = ReturnType<typeof makeSecureWebhooks>;
4251

43-
const hmac = getHmac(secret);
44-
hmac.update(input + poststamp);
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)
59+
.update(input)
60+
.digest('hex') === digest
61+
);
4562

46-
return hmac.digest('hex') === postDigest;
47-
}
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)