Skip to content

Commit 1ef4de2

Browse files
committed
Add support for asymmetric signatures
1 parent cbe59e0 commit 1ef4de2

File tree

2 files changed

+154
-18
lines changed

2 files changed

+154
-18
lines changed

src/index.spec.ts

Lines changed: 91 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,109 @@
11
import { sign, verify } from './';
22

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

129
// transmit input + signature over the wire
1310

1411
const receivingTimestamp = timestamp + 60 * 1000;
1512

16-
const isValidIfWithinTimeout = verify(input, secret, signature, {
13+
const isValidIfWithinTimeout = verify(input, pub, signature, {
1714
timestamp: receivingTimestamp,
1815
});
1916
expect(isValidIfWithinTimeout).toBe(true);
2017

21-
const isValidIfOverTimeout = verify(input, secret, signature, {
18+
const isValidIfOverTimeout = verify(input, pub, signature, {
2219
timestamp: receivingTimestamp,
2320
timeout: 0.5 * 60 * 1000,
2421
});
2522
expect(isValidIfOverTimeout).toBe(false);
23+
}
24+
25+
test('symmetric', () => {
26+
const secret = 'iamverysecret';
27+
testWithKeyPair(
28+
secret,
29+
secret,
30+
`v=1600000000000,d=3bd0b48300a7a7afc491e90ecef6805cb46813ea047434eed9cccc567f7aeecb`
31+
);
32+
});
33+
34+
test('asymmetric', () => {
35+
const publicKey = `
36+
-----BEGIN RSA PUBLIC KEY-----
37+
MIICCgKCAgEAqmeVACnRiy7Em4TJ2hevBQJek3ryctBaMQZKULAUg/+3Gk3CNgY/
38+
ZoWpFFlwnlIb60KuNAIIev8PEd63F3Ut98UaiTiZnsqMN6j7SyPoSDMP5viFZN8p
39+
u+xwn6VoGS0y/7fGs8kV+4i96dh2ev0t704E8l8CApVi607IN9NhaAgZrYHW4kTZ
40+
b1UNcVSKYiG3U4J2e6HNeSIon4Ww7tdM/6JEJWmKYybRzlGVDjZ5NYqRE+kTK1Dv
41+
lj525aJEVPy8AZzH53AwfIpukoz571voMKWgSypiyKPsXxs6AtaZfpWxLfz4VF0U
42+
lPr3Shf1CUls4llr6bFBRWub7YhQgiwF14KkdmU/g5DnqiW8jG5P+D5suV8kLUaq
43+
tm47IfrGhb548/lrh6f9/fFgx+YXjweMdamh5jggjFPUGM3X7glIhl29v/js8HXy
44+
rFyuj97rS2G3ni36CAvTmZs4FCepmJAF8EWFSUo4193mrFjuOeaWfGxqwUQ14W1T
45+
sHVEDWgptpO8RU3BNtP3dItY1iTbmIFG2lisGb7ynitJ1vItme0Yd6MeelQjVRTi
46+
9vfHQawfm5D8kW6dT/ozKFMmffIphQkgwa4xcerb9UQRMMMzgOy3HvQO2b7u0Wt/
47+
sm4IJlmI1/lBDYlcm4jt/69CvFw1C+3Z2ud5X92lVG0AqFzycFCMyzUCAwEAAQ==
48+
-----END RSA PUBLIC KEY-----
49+
`.trim();
50+
const privateKey = `
51+
-----BEGIN RSA PRIVATE KEY-----
52+
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
101+
-----END RSA PRIVATE KEY-----
102+
`.trim();
103+
104+
testWithKeyPair(
105+
publicKey,
106+
privateKey,
107+
`v=1600000000000,d=3b01f67f6ccabbc0c7cc2ae514a5b7e95cce646ea19a1b4c08de6456a5f70504c59233e5edcae2e00afeca4cb49f23cd61f646f90d4a7f77e2b6157cb9fcbeab6e53642d885f284fa67cad6537caa6d9692d3dc1189d10d53331d60417ac414dda4ce5d691f97abb7137533062e527402c1fc228002ca79a4ad0034990095554b6e53a7ee8307c3ab369cb08deaee419e9f5085ad59c17dab63bdf3bce3fae52ac9ae06bd4b1ad4d23d3c521751bf367ba657a70fb47ea2cdb072d88fad18a9d09f7a2bf1b373be1c625e92fab026e8ff6cf09fb5b0c14f80a3e2ce6c8c5607b55dd17f6111ceed32708243e015c2de83b27a4d4f5f995b68844bec9c0ca535a2afdf058d611b49294d6cd791fc413356d286c2059c10094edf71f8b012a1735fce3ca700c8c53c345bae75e512ac648dce5f7ea8b209917fa817272de6292c64f6e2cb2a48d8db2b4d9718e16eb23a96f996af2098dbbe37619fc242b9ee82f42a3e2afb366e56b185450613ae05951db2613211699c82869c583bd682247eac639fa7567ac00355580ac8ba10d1913c13fda473c2adfa980f92e9ffcb031fc88b4e7fd032220ba5b46b0ac9080fa52dda222a092c4c6e89c36c722edbadb5c95cca3e82b71c57b1a18039db18b9f5b8baed134cc287d77f3a9d1ccd62916cb57308f340288616d7e864693351386b3f0aeeef84ecbb83beba62d117e845a9a`
108+
);
26109
});

src/index.ts

Lines changed: 63 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,80 @@
1-
import { createHmac } from 'crypto';
1+
import { createHmac, createSign, createVerify } from 'crypto';
22

33
function getHmac(secret: string) {
44
return createHmac('sha256', secret);
55
}
66

7+
function isPrivateKey(secretOrPrivateKey: string): boolean {
8+
return secretOrPrivateKey.includes('PRIVATE KEY');
9+
}
10+
11+
function isPublicKey(secretOrPrivateKey: string): boolean {
12+
return secretOrPrivateKey.includes('PUBLIC KEY');
13+
}
14+
15+
function verifyWithSecret(
16+
input: string,
17+
digest: string,
18+
secret: string
19+
): boolean {
20+
return (
21+
getHmac(secret)
22+
.update(input)
23+
.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, 'hex');
45+
}
46+
47+
function verifyWithPublic(input: string, digest: string, pub: string): boolean {
48+
return createVerify('sha256')
49+
.update(input)
50+
.verify(pub, digest, 'hex');
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+
}
62+
763
export function sign(
864
input: string,
9-
secret: string,
65+
secretOrPrivateKey: string,
1066
timestamp: number = Date.now()
1167
) {
12-
const hmac = getHmac(secret);
13-
14-
hmac.update(input + timestamp);
68+
const signer = getSigner(secretOrPrivateKey);
1569

16-
return `v=${timestamp},d=${hmac.digest('hex')}`;
70+
return `v=${timestamp},d=${signer(input + timestamp)}`;
1771
}
1872

1973
const FIVE_MINUTES = 5 * 60 * 1000;
2074

2175
export function verify(
2276
input: string,
23-
secret: string,
77+
secretOrPublicKey: string,
2478
signature: string,
2579
opts: { timeout?: number; timestamp?: number } = {}
2680
) {
@@ -40,8 +94,7 @@ export function verify(
4094
return false;
4195
}
4296

43-
const hmac = getHmac(secret);
44-
hmac.update(input + poststamp);
97+
const verifier = getVerifier(secretOrPublicKey);
4598

46-
return hmac.digest('hex') === postDigest;
99+
return verifier(input + poststamp, postDigest);
47100
}

0 commit comments

Comments
 (0)