|
1 | 1 | /**
|
2 | 2 | * Module dependencies.
|
3 | 3 | */
|
| 4 | +const crypto = require('node:crypto'); |
4 | 5 |
|
5 |
| -var crypto = require('crypto'); |
| 6 | +const encoder = new TextEncoder(); |
6 | 7 |
|
7 | 8 | /**
|
8 | 9 | * Sign the given `val` with `secret`.
|
9 | 10 | *
|
10 |
| - * @param {String} val |
11 |
| - * @param {String|NodeJS.ArrayBufferView|crypto.KeyObject} secret |
| 11 | + * @param {String} value |
| 12 | + * @param {String} secret |
12 | 13 | * @return {String}
|
13 |
| - * @api private |
| 14 | + * @api public |
14 | 15 | */
|
| 16 | +exports.sign = async (value, secret) => { |
| 17 | + if (typeof value !== "string") { |
| 18 | + throw new TypeError("Cookie value must be provided as a string."); |
| 19 | + } |
| 20 | + if (typeof secret !== "string") { |
| 21 | + throw new TypeError("Secret key must be provided as a string."); |
| 22 | + } |
| 23 | + |
| 24 | + const data = encoder.encode(value); |
| 25 | + const key = await createKey(secret, ["sign"]); |
| 26 | + const signature = await crypto.webcrypto.subtle.sign("HMAC", key, data); |
| 27 | + const hash = btoa(String.fromCharCode(...new Uint8Array(signature))).replace( |
| 28 | + /=+$/, |
| 29 | + "" |
| 30 | + ); |
15 | 31 |
|
16 |
| -exports.sign = function(val, secret){ |
17 |
| - if ('string' != typeof val) throw new TypeError("Cookie value must be provided as a string."); |
18 |
| - if (null == secret) throw new TypeError("Secret key must be provided."); |
19 |
| - return val + '.' + crypto |
20 |
| - .createHmac('sha256', secret) |
21 |
| - .update(val) |
22 |
| - .digest('base64') |
23 |
| - .replace(/\=+$/, ''); |
| 32 | + return `${value}.${hash}`; |
24 | 33 | };
|
25 | 34 |
|
26 | 35 | /**
|
27 | 36 | * Unsign and decode the given `input` with `secret`,
|
28 | 37 | * returning `false` if the signature is invalid.
|
29 | 38 | *
|
30 |
| - * @param {String} input |
31 |
| - * @param {String|NodeJS.ArrayBufferView|crypto.KeyObject} secret |
32 |
| - * @return {String|Boolean} |
| 39 | + * @param {String} cookie |
| 40 | + * @param {String} secret |
| 41 | + * @return {Promise<String|false>} |
| 42 | + * @api public |
| 43 | + */ |
| 44 | +exports.unsign = async (cookie, secret) => { |
| 45 | + if (typeof cookie !== "string") { |
| 46 | + throw new TypeError("Signed cookie string must be provided."); |
| 47 | + } |
| 48 | + if (typeof secret !== "string") { |
| 49 | + throw new TypeError("Secret key must be provided."); |
| 50 | + } |
| 51 | + |
| 52 | + const value = cookie.slice(0, cookie.lastIndexOf(".")); |
| 53 | + const hash = cookie.slice(cookie.lastIndexOf(".") + 1); |
| 54 | + |
| 55 | + const data = encoder.encode(value); |
| 56 | + const key = await createKey(secret, ["verify"]); |
| 57 | + const signature = byteStringToUint8Array(atob(hash)); |
| 58 | + const valid = await crypto.webcrypto.subtle.verify("HMAC", key, signature, data); |
| 59 | + |
| 60 | + return valid ? value : false; |
| 61 | +}; |
| 62 | + |
| 63 | +/** |
| 64 | + * @param {String} secret |
| 65 | + * @param {ReadonlyArray<KeyUsage>} usages |
| 66 | + * @return {Promise<CryptoKey>} |
33 | 67 | * @api private
|
34 | 68 | */
|
| 69 | +const createKey = async (secret, usages) => |
| 70 | + crypto.webcrypto.subtle.importKey( |
| 71 | + "raw", |
| 72 | + encoder.encode(secret), |
| 73 | + { name: "HMAC", hash: "SHA-256" }, |
| 74 | + false, |
| 75 | + usages |
| 76 | + ); |
| 77 | + |
| 78 | +/** |
| 79 | + * @param {String} byteString |
| 80 | + * @return {Uint8Array} |
| 81 | + * @api private |
| 82 | + */ |
| 83 | +const byteStringToUint8Array = (byteString) => { |
| 84 | + const array = new Uint8Array(byteString.length); |
| 85 | + |
| 86 | + for (let i = 0; i < byteString.length; i++) { |
| 87 | + array[i] = byteString.charCodeAt(i); |
| 88 | + } |
35 | 89 |
|
36 |
| -exports.unsign = function(input, secret){ |
37 |
| - if ('string' != typeof input) throw new TypeError("Signed cookie string must be provided."); |
38 |
| - if (null == secret) throw new TypeError("Secret key must be provided."); |
39 |
| - var tentativeValue = input.slice(0, input.lastIndexOf('.')), |
40 |
| - expectedInput = exports.sign(tentativeValue, secret), |
41 |
| - expectedBuffer = Buffer.from(expectedInput), |
42 |
| - inputBuffer = Buffer.from(input); |
43 |
| - return ( |
44 |
| - expectedBuffer.length === inputBuffer.length && |
45 |
| - crypto.timingSafeEqual(expectedBuffer, inputBuffer) |
46 |
| - ) ? tentativeValue : false; |
| 90 | + return array; |
47 | 91 | };
|
0 commit comments