forked from justlep/keygrip-autorotate
-
Notifications
You must be signed in to change notification settings - Fork 0
/
index.js
145 lines (123 loc) · 5.23 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
135
136
137
138
139
140
141
142
143
144
145
/*!
* keygrip-autorotate
* Copyright(c) 2018 Lennart Pegel
* MIT Licensed
*/
'use strict';
const assert = require('assert');
const crypto = require('crypto');
const Keygrip = require('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} [opts.createSecret] - a function returning a new secret.
* If omitted, secrets are auto-generated as 32 random byte hex strings
* @param {string} [opts.hmacAlgorithm] - defaults to 'sha256' (alternative 'sha1')
* @param {string} [opts.encoding] - defaults to 'base64' (alternative 'hex', ...)
*
* @constructor
*/
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 = _defaultCreateSecret, encoding = 'base64'} = 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 {Object} - the interval Timeout object 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) {
rotationTimer = void(clearInterval(rotationTimer));
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) {
rotationTimer = void(clearInterval(rotationTimer));
}
};
}
/**
* @return {string}
* @private
*/
function _defaultCreateSecret() {
return crypto.randomBytes(32).toString('hex');
}
module.exports = KeygripAutorotate;