-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathscrypt.js
310 lines (265 loc) · 15.5 KB
/
scrypt.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
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
/* Scrypt password-based key derivation function. © 2018-2024 Chris Veness / Movable Type Ltd */
/* MIT Licence */
/* */
/* The function derives one or more secret keys from a secret string. It is based on memory-hard */
/* functions, which offer added protection against attacks using custom hardware. */
/* */
/* www.tarsnap.com/scrypt.html, tools.ietf.org/html/rfc7914 */
/* */
/* This implementation is a zero-dependency wrapper providing access to the OpenSSL scrypt */
/* function, returning a derived key with scrypt parameters and salt in Colin Percival's standard */
/* file header format, and a function for verifying that key against the original password. */
/* */
/* Runs on Node.js v18.0.0+ or Deno v2.0.1+. */
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
import nodeCrypto from 'node:crypto'; // for OpenSSL scrypt, timingSafeEqual
import { Buffer } from 'node:buffer'; // key is returned as Buffer (for better or for worse)
import os from 'node:os'; // for total amoung to system memory
import { TextEncoder } from 'node:util'; // TextEncoder should be a global in Node.js, but it's not
import { promisify } from 'node:util';
const opensslScrypt = promisify(nodeCrypto.scrypt); // OpenSSL scrypt; docs.openssl.org/1.1.1/man7/scrypt/
const opensslScryptSync = nodeCrypto.scryptSync;
class Scrypt {
/**
* Produce derived key using scrypt as a key derivation function.
*
* Recommended parameter values (2017) are logN:15, r:8, p:1; words.filippo.io/the-scrypt-parameters.
*
* @param {string|Uint8Array|Buffer} passphrase - Secret value such as a password from which key is to be derived.
* @param {Object} params - Scrypt parameters.
* @param {number} params.logN - CPU/memory cost parameter.
* @param {number=8} params.r - Block size parameter.
* @param {number=1} params.p - Parallelization parameter.
* @returns {Promise<Buffer>} Derived key.
*
* @example
* const key = (await Scrypt.kdf('my secret password', { logN: 15 })).toString('base64');
*/
static async kdf(passphrase, params) {
if (typeof passphrase!='string' && !ArrayBuffer.isView(passphrase)) throw new TypeError(`passphrase must be a string, TypedArray, or Buffer (received ${typeOf(passphrase)})`);
if (params === undefined) throw new TypeError('params must be supplied');
if (typeof params != 'object' || params == null) throw new TypeError(`params must be an object (received ${typeOf(params)})`);
const paramDefaults = { logN: undefined, r: 8, p: 1 };
params = Object.assign({}, paramDefaults, params);
// range-check logN, r, p
const logN = Math.round(params.logN);
const r = Math.round(params.r);
const p = Math.round(params.p);
if (isNaN(logN) || logN != params.logN) throw new RangeError(`parameter logN must be an integer; received ${params.logN}`);
if (logN < 1 || logN > 30) throw new RangeError(`parameter logN must be between 1 and 30; received ${params.logN}`);
if (isNaN(r) || r != params.r || r <= 0) throw new RangeError(`parameter r must be a positive integer; received ${params.r}`);
if (isNaN(p) || p != params.p || p <= 0) throw new RangeError(`parameter p must be a positive integer; received ${params.p}`);
if (p > (2**30-1)/r) throw new RangeError('parameters p*r must be <= 2^30-1');
// the derived key is 96 bytes: use an ArrayBuffer to view it in different formats
const keyBuff = new ArrayBuffer(96);
// a structured view of the derived key
const struct = {
scrypt: new Uint8Array(keyBuff, 0, 6),
params: {
v: new DataView(keyBuff, 6, 1),
logN: new DataView(keyBuff, 7, 1),
r: new DataView(keyBuff, 8, 4),
p: new DataView(keyBuff, 12, 4),
},
salt: new Uint8Array(keyBuff, 16, 32),
checksum: new Uint8Array(keyBuff, 48, 16),
hmachash: new Uint8Array(keyBuff, 64, 32),
};
// set params
struct.scrypt.set(new TextEncoder().encode('scrypt')); // convert string to Uint8Array
struct.params.logN.setUint8(0, logN);
struct.params.r.setUint32(0, r, false); // big-endian
struct.params.p.setUint32(0, p, false); // big-endian
// set salt
struct.salt.set(crypto.getRandomValues(new Uint8Array(32)));
// set checksum of params & salt
const prefix48 = new Uint8Array(keyBuff, 0, 48); // view onto struct.scrypt, struct.params, struct.salt
const prefix48hash = await crypto.subtle.digest('SHA-256', prefix48); // digest() returns ArrayBuffer...
struct.checksum.set(new Uint8Array(prefix48hash.slice(0, 16))); // note TypedArray.set() requires TypedArray arg, not ArrayBuffer
// set HMAC hash from scrypt-derived key
try {
params = {
N: 2**logN,
r: r,
p: p,
maxmem: 2**31-1, // 2GB is maximum maxmem allowed
};
// apply scrypt kdf to salt to derive hmac key
const hmacKey = await opensslScrypt(passphrase, struct.salt, 64, params);
// get hmachash of params, salt, & checksum, using 1st 32 bytes of scrypt hash as key
const prefix64 = new Uint8Array(keyBuff, 0, 64);
const algorithm = { name: 'HMAC', hash: 'SHA-256' };
const cryptoKey = await crypto.subtle.importKey('raw', hmacKey.slice(32), algorithm, false, [ 'sign' ]);
const hmacHash = await crypto.subtle.sign(algorithm.name, cryptoKey, prefix64); // sign() returns ArrayBuffer...
struct.hmachash.set(new Uint8Array(hmacHash)); // note TypedArray.set() requires TypedArray arg, not ArrayBuffer
return Buffer.from(keyBuff); // return ArrayBuffer as Buffer/Uint8Array
} catch (e) {
throw new Error(e.message); // e.g. memory limit exceeded; localise error to this function
}
}
/**
* Check whether key was generated from passphrase.
*
* @param {string|Uint8Array|Buffer} key - Derived key obtained from Scrypt.kdf().
* @param {string|Uint8Array|Buffer} passphrase - Passphrase originally used to generate key.
* @returns {Promise<boolean>} True if key was generated from passphrase.
*
* @example
* const key = (await Scrypt.kdf('my secret password', { logN: 15 })).toString('base64');
* const ok = await Scrypt.verify(Buffer.from(key, 'base64'), 'my secret password');
*/
static async verify(key, passphrase) {
const keyArr = typeof key == 'string' ? new Uint8Array([ ...atob(key) ].map(ch => ch.charCodeAt(0))) : key;
if (!(keyArr instanceof Uint8Array)) throw new TypeError(`key must be a Uint8Array/Buffer (received ${typeOf(keyArr)})`);
if (keyArr.length != 96) throw new RangeError('invalid key');
if (typeof passphrase!='string' && !ArrayBuffer.isView(passphrase)) throw new TypeError(`passphrase must be a string, TypedArray, or Buffer (received ${typeOf(passphrase)})`);
// use the underlying ArrayBuffer to view key in different formats
const keyBuff = keyArr.buffer.slice(keyArr.byteOffset, keyArr.byteOffset + keyArr.byteLength);
// a structured view of the derived key
const struct = {
scrypt: new Uint8Array(keyBuff, 0, 6),
params: {
v: new DataView(keyBuff, 6, 1),
logN: new DataView(keyBuff, 7, 1),
r: new DataView(keyBuff, 8, 4),
p: new DataView(keyBuff, 12, 4),
},
salt: new Uint8Array(keyBuff, 16, 32),
checksum: new Uint8Array(keyBuff, 48, 16),
hmachash: new Uint8Array(keyBuff, 64, 32),
};
// verify checksum of params & salt
const prefix48 = new Uint8Array(keyBuff, 0, 48); // view onto struct.scrypt, struct.params, struct.salt
const checksumRecalcd = await crypto.subtle.digest('SHA-256', prefix48);
if (!nodeCrypto.timingSafeEqual(struct.checksum, checksumRecalcd.slice(0, 16))) return false;
// rehash scrypt-derived key
try {
const params = {
N: 2**struct.params.logN.getUint8(0),
r: struct.params.r.getUint32(0, false), // big-endian
p: struct.params.p.getUint32(0, false), // big-endian
maxmem: 2**31-1, // 2GB is maximum allowed
};
// apply scrypt kdf to salt to derive hmac key
const hmacKey = await opensslScrypt(passphrase, struct.salt, 64, params);
// get hmachash of params, salt, & checksum, using 1st 32 bytes of scrypt hash as key
const prefix64 = new Uint8Array(keyBuff, 0, 64);
const algorithm = { name: 'HMAC', hash: 'SHA-256' };
const cryptoKey = await crypto.subtle.importKey('raw', hmacKey.slice(32), algorithm, false, [ 'sign' ]);
const hmacHash = await crypto.subtle.sign(algorithm.name, cryptoKey, prefix64);
// verify hash
return nodeCrypto.timingSafeEqual(hmacHash, struct.hmachash);
} catch (e) {
throw new Error(e.message); // localise error to this function [can't happen?]
}
}
/**
* View scrypt parameters which were used to derive key.
*
* @param {string|Uint8Array|Buffer} key - Derived base64 key obtained from Scrypt.kdf().
* @returns {Object} Scrypt parameters logN, r, p.
*
* @example
* const key = await Scrypt.kdf('my secret password', { logN: 15 } );
* const params = Scrypt.viewParams(key); // => { logN: 15, r: 8, p: 1 }
*/
static viewParams(key) {
const keyArr = typeof key == 'string' ? new Uint8Array([ ...atob(key) ].map(ch => ch.charCodeAt(0))) : key;
if (!(keyArr instanceof Uint8Array)) throw new TypeError(`key must be a Uint8Array/Buffer (received ${typeOf(keyArr)})`);
if (keyArr.length != 96) throw new RangeError('invalid key');
// use the underlying ArrayBuffer to view key in structured format
const keyBuff = keyArr.buffer.slice(keyArr.byteOffset, keyArr.byteOffset + keyArr.byteLength);
// a structured view of the derived key
const struct = {
scrypt: new Uint8Array(keyBuff, 0, 6),
params: {
v: new DataView(keyBuff, 6, 1),
logN: new DataView(keyBuff, 7, 1),
r: new DataView(keyBuff, 8, 4),
p: new DataView(keyBuff, 12, 4),
},
salt: new Uint8Array(keyBuff, 16, 32),
checksum: new Uint8Array(keyBuff, 48, 16),
hmachash: new Uint8Array(keyBuff, 64, 32),
};
const params = {
logN: struct.params.logN.getUint8(0),
r: struct.params.r.getUint32(0, false), // big-endian
p: struct.params.p.getUint32(0, false), // big-endian
};
return params;
}
/**
* Calculate scrypt parameters from maxtime, maxmem, maxmemfrac values.
*
* Adapted from Colin Percival's code: see github.com/Tarsnap/scrypt/tree/master/lib/scryptenc.
* Percival recommended an interactive login delay of "up to 100ms".
*
* NOTE: empirical tests in 2024 suggest this approach underestimates logN by one or two, and
* confirm Filippo Valsorda's recommendation for logN=15; ("the biggest power of two that
* will run in less than 100ms") - timing tests are the most reliable way to validate
* optimal parameters.
*
* Returned parameters may vary depending on computer specs & current loading.
*
* @param {number} maxtime - Maximum time in seconds scrypt will spend computing the derived key.
* @param {number=availMem} maxmem - Maximum bytes of RAM used when computing the derived encryption key.
* @param {number=0.5} maxmemfrac - Fraction of the available RAM used when computing the derived key.
* @returns {Object} Scrypt parameters logN, r, p.
*
* @example
* const params = Scrypt.pickParams(0.1); // => e.g. { logN: 15, r: 8, p: 1 }
*/
static pickParams(maxtime, maxmem=os.totalmem(), maxmemfrac=0.5) {
if (maxmem==0 || maxmem==null) maxmem = os.totalmem();
if (maxmemfrac==0 || maxmemfrac>0.5) maxmemfrac = 0.5;
// memory limit is memfrac · physical memory, no more than maxmem and no less than 1MiB
const physicalMemory = os.totalmem();
const memlimit = Math.max(Math.min(physicalMemory*maxmemfrac, maxmem), 1024*1024);
// Colin Percival measures how many scrypts can be done in one clock tick using C/POSIX
// clock_getres() / CLOCKS_PER_SEC (usually just one?); we will use performance.now() to get
// a DOMHighResTimeStamp. (Following meltdown/spectre timing attacks high-res timestamp
// resolution has been 'coarsened' to 100µs, so we'll be conservative and do a 100ms run -
// typically 100..1000 minimal scrypts, noting greater timing variability in JavaScript than
// in C).
let i = 0;
const start = performance.now();
while (performance.now()-start < 100) { // 100ms run
opensslScryptSync('', '', 64, { N: 128, r: 1, p: 1 });
i += 512; // we invoked the salsa20/8 core 512 times
}
const duration = (performance.now()-start) / 1000; // in seconds
const opps = i / duration;
// allow a minimum of 2^15 salsa20/8 cores
const opslimit = Math.max(opps * maxtime, 2**15);
const r = 8; // "fix r = 8 for now"
// memory limit requires that 128·N·r <= memlimit
// CPU limit requires that 4·N·r·p <= opslimit
// if opslimit < memlimit/32, opslimit imposes the stronger limit on N
let p = null;
let logN = 0;
if (opslimit < memlimit/32) {
// set p = 1 & choose N based on CPU limit
p = 1;
const maxN = opslimit / (r*4);
while (1<<logN <= maxN/2 && logN < 63) logN++;
} else {
// set N based on the memory limit
const maxN = memlimit / (r * 128);
while (1<<logN <= maxN/2 && logN < 63) logN++;
// choose p based on the CPU limit
const maxrp = Math.min((opslimit / 4) / (1<<logN), 0x3fffffff);
p = Math.round(maxrp / r);
}
return { logN, r, p };
}
}
/**
* Return more useful type description than 'typeof': javascriptweblog.wordpress.com/2011/08/08/
*/
function typeOf(obj) {
return ({}).toString.call(obj).match(/\s([a-zA-Z]+)/)[1].toLowerCase();
}
/* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
export default Scrypt;