You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

117 lines
2.9 KiB

3 years ago
'use strict'
// Inspired by node-cookie-signature
// https://github.com/tj/node-cookie-signature
//
// The MIT License
// Copyright (c) 20122022 LearnBoost <tj@learnboost.com> and other contributors;
const crypto = require('crypto')
const base64PaddingRE = /=/g
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 (const secret of secrets) {
if (typeof secret !== 'string') {
throw new TypeError('Secret key must be a string.')
}
}
}
function validateAlgorithm (algorithm) {
// validate that the algorithm is supported by the node runtime
try {
crypto.createHmac(algorithm, 'dummyHmac')
} 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 (const secret of secrets) {
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