-
-
Notifications
You must be signed in to change notification settings - Fork 67
/
signer.js
118 lines (97 loc) · 3.09 KB
/
signer.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
'use strict'
// Inspired by node-cookie-signature
// https://github.com/tj/node-cookie-signature
//
// The MIT License
// Copyright (c) 2012–2022 LearnBoost <[email protected]> and other contributors;
const crypto = require('node:crypto')
const base64PaddingRE = /=/gu
function Signer (secrets, algorithm = 'sha256') {
if (!(this instanceof Signer)) {
return new Signer(secrets, algorithm)
}
this.secrets = Array.isArray(secrets) ? secrets : [secrets]
this.signingKey = this.secrets[0]
this.algorithm = algorithm
validateSecrets(this.secrets)
validateAlgorithm(this.algorithm)
}
function validateSecrets (secrets) {
for (let i = 0; i < secrets.length; ++i) {
const secret = secrets[i]
if (typeof secret !== 'string' && Buffer.isBuffer(secret) === false) {
throw new TypeError('Secret key must be a string or Buffer.')
}
}
}
function validateAlgorithm (algorithm) {
// validate that the algorithm is supported by the node runtime
try {
crypto.createHmac(algorithm, crypto.randomBytes(16))
} catch (e) {
throw new TypeError(`Algorithm ${algorithm} not supported.`)
}
}
function _sign (value, secret, algorithm) {
if (typeof value !== 'string') {
throw new TypeError('Cookie value must be provided as a string.')
}
return value + '.' + crypto
.createHmac(algorithm, secret)
.update(value)
.digest('base64')
// remove base64 padding (=) as it has special meaning in cookies
.replace(base64PaddingRE, '')
}
function _unsign (signedValue, secrets, algorithm) {
if (typeof signedValue !== 'string') {
throw new TypeError('Signed cookie string must be provided.')
}
const value = signedValue.slice(0, signedValue.lastIndexOf('.'))
const actual = Buffer.from(signedValue.slice(signedValue.lastIndexOf('.') + 1))
for (let i = 0; i < secrets.length; ++i) {
const secret = secrets[i]
const expected = Buffer.from(crypto
.createHmac(algorithm, secret)
.update(value)
.digest('base64')
// remove base64 padding (=) as it has special meaning in cookies
.replace(base64PaddingRE, ''))
if (
expected.length === actual.length &&
crypto.timingSafeEqual(expected, actual)
) {
return {
valid: true,
renew: secret !== secrets[0],
value
}
}
}
return {
valid: false,
renew: false,
value: null
}
}
Signer.prototype.sign = function (value) {
return _sign(value, this.signingKey, this.algorithm)
}
Signer.prototype.unsign = function (signedValue) {
return _unsign(signedValue, this.secrets, this.algorithm)
}
function sign (value, secret, algorithm = 'sha256') {
const secrets = Array.isArray(secret) ? secret : [secret]
validateSecrets(secrets)
return _sign(value, secrets[0], algorithm)
}
function unsign (signedValue, secret, algorithm = 'sha256') {
const secrets = Array.isArray(secret) ? secret : [secret]
validateSecrets(secrets)
return _unsign(signedValue, secrets, algorithm)
}
module.exports = Signer
module.exports.signerFactory = Signer
module.exports.Signer = Signer
module.exports.sign = sign
module.exports.unsign = unsign