|
|
'use strict'
|
|
|
|
|
|
// Inspired by node-cookie-signature
|
|
|
// https://github.com/tj/node-cookie-signature
|
|
|
//
|
|
|
// The MIT License
|
|
|
// Copyright (c) 2012–2022 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
|