-
Notifications
You must be signed in to change notification settings - Fork 25
/
Copy pathindex.js
430 lines (388 loc) · 13.9 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
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
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
// @flow
'use strict'
const assert = require('assert')
const nacl = require('tweetnacl')
// Lazy-load wordlists to save memory when using bip39
let niceware = null
let bip39 = null
/**
* Default seed size in bytes.
* @const
* @type {number}
* @default
*/
module.exports.DEFAULT_SEED_SIZE = 32
/**
* Implementation of HMAC SHA512 from https://github.com/dchest/tweetnacl-auth-js
* @param {Uint8Array} message message to HMAC
* @param {Uint8Array} key the HMAC key
* @returns {Uint8Array}
*/
module.exports.hmac = function (message/* : Uint8Array */, key/* : Uint8Array */) {
if (!(message instanceof Uint8Array) || !(key instanceof Uint8Array)) {
throw new Error('Inputs must be Uint8Arrays.')
}
const BLOCK_SIZE = 128
const HASH_SIZE = 64
const buf = new Uint8Array(BLOCK_SIZE + Math.max(HASH_SIZE, message.length))
let i
if (key.length > BLOCK_SIZE) {
key = nacl.hash(key)
}
for (i = 0; i < BLOCK_SIZE; i++) buf[i] = 0x36
for (i = 0; i < key.length; i++) buf[i] ^= key[i]
buf.set(message, BLOCK_SIZE)
const innerHash = nacl.hash(buf.subarray(0, BLOCK_SIZE + message.length))
for (i = 0; i < BLOCK_SIZE; i++) buf[i] = 0x5c
for (i = 0; i < key.length; i++) buf[i] ^= key[i]
buf.set(innerHash, BLOCK_SIZE)
return nacl.hash(buf.subarray(0, BLOCK_SIZE + innerHash.length))
}
/**
* Returns HKDF output according to rfc5869 using sha512
* @param {Uint8Array} ikm input keying material
* @param {Uint8Array} info context-specific info
* @param {number} extractLen length of extracted output keying material in
* octets
* @param {Uint8Array=} salt optional salt
* @returns {Uint8Array}
*/
module.exports.getHKDF = function (ikm/* : Uint8Array */, info/* : Uint8Array */,
extractLen, salt/* : Uint8Array */) {
const hashLength = 512 / 8
if (typeof extractLen !== 'number' || extractLen < 0 ||
extractLen > hashLength * 255) {
throw Error('Invalid extract length.')
}
// Extract
if (!(salt instanceof Uint8Array) || salt.length === 0) {
salt = new Uint8Array(hashLength)
}
const prk = module.exports.hmac(ikm, salt) // Pseudorandom Key
// Expand
const n = Math.ceil(extractLen / hashLength)
const t = []
t[0] = new Uint8Array()
info = info || new Uint8Array()
const okm = new Uint8Array(extractLen)
let filled = 0
for (let i = 1; i <= n; i++) {
const prev = t[i - 1]
const input = new Uint8Array(info.length + prev.length + 1)
input.set(prev)
input.set(info, prev.length)
input.set(new Uint8Array([i]), prev.length + info.length)
const output = module.exports.hmac(input, prk)
t[i] = output
const remaining = extractLen - filled
assert(remaining > 0)
if (output.length <= remaining) {
okm.set(output, filled)
filled = filled + output.length
} else {
okm.set(output.slice(0, remaining), filled)
return okm
}
}
return okm
}
/**
* Generates a random seed.
* @param {number=} size seed size in bytes; defaults to 32
* @returns {Uint8Array}
*/
module.exports.getSeed = function (size/* : number */ = module.exports.DEFAULT_SEED_SIZE) {
return nacl.randomBytes(size)
}
/**
* Derives an Ed25519 keypair given a random seed and an optional HKDF salt.
* Returns a nacl.sign keypair object:
* https://github.com/dchest/tweetnacl-js#naclsignkeypair
* @param {Uint8Array} seed random seed, recommended length 32
* @param {Uint8Array=} salt random salt, recommended length 64
* @returns {{secretKey: Uint8Array, publicKey: Uint8Array}}
*/
module.exports.deriveSigningKeysFromSeed = function (seed/* : Uint8Array */, salt/* : Uint8Array */) {
if (!(seed instanceof Uint8Array)) {
throw new Error('Seed must be Uint8Array.')
}
// Derive the Ed25519 signing keypair
const output = module.exports.getHKDF(seed, new Uint8Array([0]),
nacl.sign.seedLength, salt)
return nacl.sign.keyPair.fromSeed(output)
}
/**
* Converts Uint8Array or Buffer to a hex string.
* @param {Uint8Array|Buffer} arr Uint8Array / Buffer to convert
* @returns {string}
*/
module.exports.uint8ToHex = function (arr/* : Uint8Array | Buffer */) {
if (!(arr instanceof Uint8Array)) {
throw new Error('Input must be a Buffer or Uint8Array')
}
let buffer = arr
if (!(arr instanceof Buffer)) {
// Convert Uint8Array to Buffer
buffer = Buffer.from(arr.buffer)
// From https://github.com/feross/typedarray-to-buffer/blob/master/index.js
if (arr.byteLength !== arr.buffer.byteLength) {
buffer = buffer.slice(arr.byteOffset, arr.byteOffset + arr.byteLength)
}
}
return buffer.toString('hex')
}
/**
* Converts hex string to a Uint8Array.
* @param {string=} hex Hex string to convert; defaults to ''.
* @returns {Uint8Array}
*/
module.exports.hexToUint8 = function (hex/* : string */ = '') {
if (typeof hex !== 'string') {
throw new Error('Input must be a string')
}
if (!/^[0-9A-Fa-f]*$/.test(hex)) {
throw new Error('Input must be hex without the 0x prefix')
}
if (hex.length % 2 !== 0) {
hex = '0' + hex
}
const arr = new Uint8Array(hex.length / 2)
for (let i = 0; i < hex.length / 2; i++) {
arr[i] = Number('0x' + hex[2 * i] + hex[2 * i + 1])
}
return arr
}
// For browserify
/* istanbul ignore if */
if (typeof window === 'object') {
window.module = module
}
/**
* Utilities for converting keys to passphrases using bip39 or niceware
*/
module.exports.passphrase = {
/* @exports passphrase */
/**
* Converts bytes to passphrase using bip39 (default) or niceware
* @method
* @param {Uint8Array|Buffer|string} bytes Uint8Array / Buffer / hex string to convert; hex should not contain 0x prefix.
* @param {boolean=} useNiceware Whether to use Niceware; defaults to false
* @returns {string}
*/
fromBytesOrHex: function (bytes/* : Uint8Array | string */, useNiceware/* : boolean */ = false) {
if (useNiceware) {
niceware = niceware || require('niceware')
if (typeof bytes === 'string') {
bytes = module.exports.hexToUint8(bytes)
}
return niceware.bytesToPassphrase(Buffer.from(bytes)).join(' ')
} else {
bip39 = bip39 || require('bip39')
if (typeof bytes !== 'string') {
bytes = module.exports.uint8ToHex(bytes)
}
return bip39.entropyToMnemonic(bytes)
}
},
/**
* Converts a 32-byte passphrase to uint8array bytes. Infers whether the
* passphrase is bip39 or niceware based on length.
* @method
* @param {string} passphrase bip39/niceware phrase to convert
* @returns {Uint8Array}
*/
toBytes32: function (passphrase/* : string */) {
passphrase = passphrase.trim().replace(/\s+/gi, ' ')
const words = passphrase.split(' ')
if (words.length === module.exports.passphrase.NICEWARE_32_BYTE_WORD_COUNT) {
niceware = niceware || require('niceware')
return new Uint8Array(niceware.passphraseToBytes(words))
} else if (words.length === module.exports.passphrase.BIP39_32_BYTE_WORD_COUNT) {
bip39 = bip39 || require('bip39')
return module.exports.hexToUint8(bip39.mnemonicToEntropy(passphrase))
} else {
throw new Error(`Input words length ${words.length} is not 24 or 16.`)
}
},
/**
* Converts a 32-byte passphrase to hex. Infers whether the
* passphrase is bip39 or niceware based on length.
* @method
* @param {string} passphrase bip39/niceware phrase to convert
* @returns {string}
*/
toHex32: function (passphrase/* : string */) {
passphrase = passphrase.trim().replace(/\s+/gi, ' ')
const words = passphrase.split(' ')
if (words.length === module.exports.passphrase.NICEWARE_32_BYTE_WORD_COUNT) {
niceware = niceware || require('niceware')
const bytes = niceware.passphraseToBytes(words)
return module.exports.uint8ToHex(bytes)
} else if (words.length === module.exports.passphrase.BIP39_32_BYTE_WORD_COUNT) {
bip39 = bip39 || require('bip39')
return bip39.mnemonicToEntropy(passphrase)
} else {
throw new Error(`Input word length ${words.length} is not 24 or 16.`)
}
},
/**
* Number of niceware words corresponding to 32 bytes
* @const
* @type {number}
* @default
*/
NICEWARE_32_BYTE_WORD_COUNT: 16,
/**
* Number of niceware words corresponding to 32 bytes
* @const
* @type {number}
* @default
*/
BIP39_32_BYTE_WORD_COUNT: 24
}
/**
* Random samplers.
*/
module.exports.random = {
/**
* Sample uniformly at random from nonnegative integers below a
* specified bound.
*
* @method
* @param {number} n - exclusive upper bound, positive integer at most 2^53
* @returns {number}
*/
uniform: function (n/* : number */) {
if (typeof n !== 'number' || n % 1 !== 0 || n <= 0 || n > Math.pow(2, 53)) {
throw new Error('Bound must be positive integer at most 2^53.')
}
const min = Math.pow(2, 53) % n
let x
do {
const b = nacl.randomBytes(7)
const l32 = b[0] | (b[1] << 8) | (b[2] << 16) | (b[3] << 24)
const h21 = b[4] | (b[5] << 8) | ((b[6] & 0x1f) << 16)
x = Math.pow(2, 32) * h21 + l32
} while (x < min)
return x % n
},
/**
* Sample uniformly at random from floating-point numbers in [0, 1].
*
* @method
* @returns {number}
*/
uniform_01: function () {
function uniform32 () {
const b = nacl.randomBytes(4)
return (b[0] | (b[1] << 8) | (b[2] << 16) | (b[3] << 24)) >>> 0
}
// Draw an exponent with geometric distribution.
let e = 0
let x
// One in four billion chance that uniform32() is zero.
/* istanbul ignore if */
if ((x = uniform32()) === 0) {
do {
// emin = -1022; emin - 53 = -1054; emin - 64 = -1088 provides a
// hedge of paranoia in case I made a fencepost here.
/* istanbul ignore if */
if (e >= 1088) {
// You're struck by lightning, and you win the lottery...
// or your PRNG is broken.
return 0
}
e += 32
} while ((x = uniform32()) === 0)
}
e += Math.clz32(x)
// Draw normal odd 64-bit significand with uniform distribution.
const hi = (uniform32() | 0x80000000) >>> 0
const lo = (uniform32() | 0x00000001) >>> 0
// Assemble parts into [2^63, 2^64] with uniform distribution.
// Using an odd low part breaks ties in the rounding, which should
// occur only in a set of measure zero.
const s = hi * Math.pow(2, 32) + lo
// Scale into [1/2, 1] and apply the exponent.
return s * Math.pow(2, (-64 - e))
}
}
/**
* A dictionary of values, commonly used for objects i.e '{ 'header-name': 'header-value' }'
* @typedef {Object.<string, (string | string[] | undefined)>} Dictionary
*/
/**
* Uses Ed25519, a public-key signature system: {@link https://ed25519.cr.yp.to/}
*
* Spec: {@link https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-12}
*
* Signs the message using the secret key and returns a signature.
* @see {nacl.sign.detached}
*
* @method
* @param {string} keyId - an opaque string that the server can use to look up the component they need to validate the signature.
* @param {string} secretKey - hex encoded secret key to sign the message.
* @param {Dictionary} headers - headers containing the properties to sign.
* @returns {string}
*/
module.exports.ed25519HttpSign = function (keyId /* string */, secretKey /* string */, headers = {} /* dictionary */) {
if (!secretKey) throw new Error('secret key is required')
if (!keyId) throw new Error('key id is required')
if (Object.keys(headers).length === 0) throw new Error('headers are required')
const headerKeys = []
const message = Object.entries(headers)
.map(([key, value]) => {
headerKeys.push(key)
return `${key}: ${value}`
}).join('\n')
// Generate signature using the ed25519 algorithm.
const sign = nacl.sign.detached(
Uint8Array.from(Buffer.from(message)),
Uint8Array.from(Buffer.from(secretKey, 'hex'))
)
const signature = Buffer.from(sign).toString('base64')
return `keyId="${keyId}",algorithm="ed25519",headers="${headerKeys.join(' ')}",signature="${signature}"`
}
/**
* Uses Ed25519, a public-key signature system: {@link https://ed25519.cr.yp.to/}
*
* Spec: {@link https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-12}
*
* Verifies the signature for the message and returns parsed fields from the signature.
* @see {nacl.sign.detached.verify}
*
* @method
* @param {string} publicKey - hex encoded public key to verify the signature.
* @param {Dictionary} headers - headers containing the signature for verification.
* @returns {{ algorithm: string, headers: string[], keyId: string, signature: string, verified: boolean }}
*/
module.exports.ed25519HttpVerify = function (publicKey /* string */, headers = {} /* dictionary */) {
if (!publicKey) throw new Error('public key is required')
if (!headers.signature) throw new Error('header signature is required')
const signedRequest = headers.signature.split(',').reduce((result, part) => {
const eq = part.indexOf('=')
const key = part.substring(0, eq)
const value = part.substring(eq + 1, part.length)
if (value) result[key] = value.replace(/"/g, '') // remove quotes
return result
}, {})
if (!signedRequest.algorithm) throw new Error('no algorithm was parsed')
if (signedRequest.algorithm !== 'ed25519') throw new Error('unsupported algorithm, use ed25519')
if (!signedRequest.signature) throw new Error('no signature was parsed')
if (!signedRequest.keyId) throw new Error('no keyId was parsed')
if (!signedRequest.headers) throw new Error('no headers were parsed')
const signedRequestHeaders = signedRequest.headers.split(' ')
const message = signedRequestHeaders.map(key => `${key}: ${headers[key]}`).join('\n')
const verified = nacl.sign.detached.verify(
Uint8Array.from(Buffer.from(message)),
Uint8Array.from(Buffer.from(signedRequest.signature, 'base64')),
Uint8Array.from(Buffer.from(publicKey, 'hex'))
)
return {
algorithm: signedRequest.algorithm,
headers: signedRequestHeaders,
keyId: signedRequest.keyId,
signature: signedRequest.signature,
verified
}
}