-
Notifications
You must be signed in to change notification settings - Fork 23
/
webhooks.ts
108 lines (92 loc) · 2.81 KB
/
webhooks.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
import crypto from 'crypto';
import { SignatureVerificationException } from '../common/exceptions';
import { deserializeEvent } from '../common/serializers';
import { Event, EventResponse } from '../common/interfaces';
export class Webhooks {
constructEvent({
payload,
sigHeader,
secret,
tolerance = 180000,
}: {
payload: unknown;
sigHeader: string;
secret: string;
tolerance?: number;
}): Event {
const options = { payload, sigHeader, secret, tolerance };
this.verifyHeader(options);
const webhookPayload = payload as EventResponse;
return deserializeEvent(webhookPayload);
}
verifyHeader({
payload,
sigHeader,
secret,
tolerance = 180000,
}: {
payload: any;
sigHeader: string;
secret: string;
tolerance?: number;
}): boolean {
const [timestamp, signatureHash] =
this.getTimestampAndSignatureHash(sigHeader);
if (!signatureHash || Object.keys(signatureHash).length === 0) {
throw new SignatureVerificationException(
'No signature hash found with expected scheme v1',
);
}
if (parseInt(timestamp, 10) < Date.now() - tolerance) {
throw new SignatureVerificationException(
'Timestamp outside the tolerance zone',
);
}
const expectedSig = this.computeSignature(timestamp, payload, secret);
if (this.secureCompare(expectedSig, signatureHash) === false) {
throw new SignatureVerificationException(
'Signature hash does not match the expected signature hash for payload',
);
}
return true;
}
getTimestampAndSignatureHash(sigHeader: string): string[] {
const signature = sigHeader;
const [t, v1] = signature.split(',');
if (typeof t === 'undefined' || typeof v1 === 'undefined') {
throw new SignatureVerificationException(
'Signature or timestamp missing',
);
}
const { 1: timestamp } = t.split('=');
const { 1: signatureHash } = v1.split('=');
return [timestamp, signatureHash];
}
computeSignature(timestamp: any, payload: any, secret: string): string {
payload = JSON.stringify(payload);
const signedPayload = `${timestamp}.${payload}`;
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest()
.toString('hex');
return expectedSignature;
}
secureCompare(stringA: string, stringB: string): boolean {
const strA = Buffer.from(stringA);
const strB = Buffer.from(stringB);
if (strA.length !== strB.length) {
return false;
}
if (crypto.timingSafeEqual) {
return crypto.timingSafeEqual(strA, strB);
}
const len = strA.length;
let result = 0;
for (let i = 0; i < len; ++i) {
// tslint:disable-next-line:no-bitwise
result |= strA[i] ^ strB[i];
}
return result === 0;
}
}