-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathindex.js
134 lines (117 loc) · 5.27 KB
/
index.js
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
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
import assert from 'node:assert';
import {randomBytes} from 'node:crypto';
import Keygrip from 'keygrip';
const DESTROYED_ERROR_MSG = 'KeygripAutorotate instance is already destroyed';
/**
* A key-signing and signature-verification helper based on Keygrip ({@link https://github.com/crypto-utils/keygrip}),
* extended with internal auto-rotation of secrets used for signing, so any previously used secret is discarded
* automatically after a maximum TTL.
*
* Signing is always done using the freshest of the secret only.
*
* @param {object} opts
* @param {number} opts.totalSecrets - the number of secrets to use for signing and verification
* @param {number} opts.ttlPerSecret - the maximum time to live per secret in millis, from its creation till being rotated out
* @param {function():(string|Buffer)} [opts.createSecret] - function returning a new secret.
* If omitted, secrets are auto-generated as 64 to 128 random bytes
* @param {string} [opts.hmacAlgorithm] - defaults to 'sha256' (alternative 'sha1')
* @param {string} [opts.encoding] - defaults to 'base64' (alternative 'hex', ... 'base64urlsafe')
*
* @constructor
*/
export function KeygripAutorotate(opts) {
if (!this instanceof KeygripAutorotate) {
throw new Error('KeygripAutorotate is a constructor');
}
assert(opts && typeof opts === 'object', 'invalid options');
const {totalSecrets, ttlPerSecret, hmacAlgorithm = 'sha256', createSecret = generateRandomBytes, encoding = 'base64url'} = opts;
assert(!isNaN(totalSecrets) && totalSecrets >= 2, 'totalSecrets should be > 2');
assert(!isNaN(ttlPerSecret) && ttlPerSecret >= 1000, 'ttlPerSecret should be greater than 1000 (millis)');
assert(hmacAlgorithm && typeof hmacAlgorithm === 'string', 'hmacAlgorithm must be a valid hmac algorithm or omitted');
assert(encoding && typeof encoding === 'string', 'Invalid encoding');
assert(typeof createSecret === 'function', 'createSecret must be a secret-generating function');
const rotationInterval = Math.round(ttlPerSecret / totalSecrets);
assert(rotationInterval < Math.pow(2,31), 'Secret rotation intervals over 24 days are not supported');
const secrets = Array(totalSecrets).fill(null).map(createSecret);
const keygrip = new Keygrip(secrets, hmacAlgorithm, encoding);
let isDestroyed = false;
/**
* @type {?number} - the interval timer id while {@link periodicSecretRotate} gets called periodically.
* A falsy value of `rotationTimer` means
* - secrets rotation is currently paused, and
* - none of the current secrets has been used for signing anything yet
*/
let rotationTimer;
let rotationsTillPause = 0;
const periodicSecretRotate = function() {
if (!rotationsTillPause) {
clearInterval(rotationTimer);
rotationTimer = null;
return;
}
secrets.pop();
secrets.unshift(createSecret());
--rotationsTillPause;
};
/**
* @param {string|Buffer} data
* @return {string} a signature for data, calculated using the freshest secret
*/
this.sign = function(data) {
if (isDestroyed) {
throw new Error(DESTROYED_ERROR_MSG);
}
rotationsTillPause = totalSecrets;
if (!rotationTimer) {
rotationTimer = setInterval(periodicSecretRotate, rotationInterval);
}
return keygrip.sign(data);
};
/**
* @see https://github.com/crypto-utils/keygrip
* @param {string|Buffer} data
* @param {string} digest
* @return {number} - the index of the matched secret
*/
this.index = (data, digest) => {
if (isDestroyed) {
throw new Error(DESTROYED_ERROR_MSG);
}
return keygrip.index(data, digest);
};
/**
* Verifies if the given digest was generated with any of the current secrets.
*
* (!) If {@link rotationTimer} is currently falsy, we know beforehand that *none* of the current secrets
* can have been used for signing. Let's call verify nonetheless, so the outside world can't determine
* the current status of rotation just through timings.
*
* @see https://github.com/crypto-utils/keygrip
* @param {string|Buffer} data
* @param {string} digest
* @return {boolean} - true if the digest was generated with of the secrets, otherwise false
*/
this.verify = (data, digest) => {
if (isDestroyed) {
throw new Error(DESTROYED_ERROR_MSG);
}
return keygrip.verify(data, digest) && !!rotationTimer; // ignore result if all secrets are unused
};
/**
* Marks this instances destroyed and stops any running interval timer, e.g. to let the process exit without delays.
* Subsequent calls of any other methods will throw an error.
*/
this.destroy = function() {
isDestroyed = true;
if (rotationTimer) {
clearInterval(rotationTimer)
rotationTimer = null;
}
};
}
/**
* @param {number} [minLen=64]
* @param {number} [maxLen=128]
* @return {Buffer}
*/
export const generateRandomBytes = (minLen = 64, maxLen = 128) => randomBytes(minLen + Math.round(Math.random() * (maxLen - minLen)));