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.

363 lines
9.8 KiB

'use strict'
const asn = require('asn1.js')
const {
createHmac,
createVerify,
createSign,
timingSafeEqual,
createPublicKey,
constants: {
RSA_PKCS1_PSS_PADDING,
RSA_PSS_SALTLEN_DIGEST,
RSA_PKCS1_PADDING,
RSA_PSS_SALTLEN_MAX_SIGN,
RSA_PSS_SALTLEN_AUTO
}
} = require('crypto')
let { sign: directSign, verify: directVerify } = require('crypto')
const { joseToDer, derToJose } = require('ecdsa-sig-formatter')
const Cache = require('mnemonist/lru-cache')
const { TokenError } = require('./error')
const useNewCrypto = typeof directSign === 'function'
const base64UrlMatcher = /[=+/]/g
const encoderMap = { '=': '', '+': '-', '/': '_' }
const privateKeyPemMatcher = /^-----BEGIN(?: (RSA|EC|ENCRYPTED))? PRIVATE KEY-----/
const publicKeyPemMatcher = '-----BEGIN PUBLIC KEY-----'
const publicKeyX509CertMatcher = '-----BEGIN CERTIFICATE-----'
const privateKeysCache = new Cache(1000)
const publicKeysCache = new Cache(1000)
const hsAlgorithms = ['HS256', 'HS384', 'HS512']
const esAlgorithms = ['ES256', 'ES384', 'ES512']
const rsaAlgorithms = ['RS256', 'RS384', 'RS512', 'PS256', 'PS384', 'PS512']
const edAlgorithms = ['EdDSA']
const ecCurves = {
'1.2.840.10045.3.1.7': { bits: '256', names: ['P-256', 'prime256v1'] },
'1.3.132.0.10': { bits: '256', names: ['secp256k1'] },
'1.3.132.0.34': { bits: '384', names: ['P-384', 'secp384r1'] },
'1.3.132.0.35': { bits: '512', names: ['P-521', 'secp521r1'] }
}
/* istanbul ignore next */
if (!useNewCrypto) {
directSign = function(alg, data, options) {
if (typeof alg === 'undefined') {
throw new TokenError(TokenError.codes.signError, 'EdDSA algorithms are not supported by your Node.js version.')
}
return createSign(alg)
.update(data)
.sign(options)
}
}
const PrivateKey = asn.define('PrivateKey', function() {
this.seq().obj(
this.key('version').int(),
this.key('algorithm')
.seq()
.obj(
this.key('algorithm').objid(),
this.key('parameters')
.optional()
.objid()
)
)
})
const PublicKey = asn.define('PublicKey', function() {
this.seq().obj(
this.key('algorithm')
.seq()
.obj(
this.key('algorithm').objid(),
this.key('parameters')
.optional()
.objid()
)
)
})
const ECPrivateKey = asn.define('ECPrivateKey', function() {
this.seq().obj(
this.key('version').int(),
this.key('privateKey').octstr(),
this.key('parameters')
.explicit(0)
.optional()
.choice({ namedCurve: this.objid() })
)
})
function base64UrlReplacer(c) {
return encoderMap[c]
}
function cacheSet(cache, key, value, error) {
cache.set(key, [value, error])
return value || error
}
function performDetectPrivateKeyAlgorithm(key) {
if (key.includes(publicKeyPemMatcher) || key.includes(publicKeyX509CertMatcher)) {
throw new TokenError(TokenError.codes.invalidKey, 'Public keys are not supported for signing.')
}
const pemData = key.trim().match(privateKeyPemMatcher)
if (!pemData) {
return 'HS256'
}
let keyData
let oid
let curveId
switch (pemData[1]) {
case 'RSA': // pkcs1 format - Can only be RSA key
return 'RS256'
case 'EC': // sec1 format - Can only be a EC key
keyData = ECPrivateKey.decode(key, 'pem', { label: 'EC PRIVATE KEY' })
curveId = keyData.parameters.value.join('.')
break
case 'ENCRYPTED': // Can be either RSA or EC key - we'll used the supplied algorithm
return 'ENCRYPTED'
default:
// pkcs8
keyData = PrivateKey.decode(key, 'pem', { label: 'PRIVATE KEY' })
oid = keyData.algorithm.algorithm.join('.')
switch (oid) {
case '1.2.840.113549.1.1.1': // RSA
return 'RS256'
case '1.2.840.10045.2.1': // EC
curveId = keyData.algorithm.parameters.join('.')
break
case '1.3.101.112': // Ed25519
case '1.3.101.113': // Ed448
return 'EdDSA'
default:
throw new TokenError(TokenError.codes.invalidKey, `Unsupported PEM PCKS8 private key with OID ${oid}.`)
}
}
const curve = ecCurves[curveId]
if (!curve) {
throw new TokenError(TokenError.codes.invalidKey, `Unsupported EC private key with curve ${curveId}.`)
}
return `ES${curve.bits}`
}
function performDetectPublicKeyAlgorithms(key) {
if (key.match(privateKeyPemMatcher)) {
throw new TokenError(TokenError.codes.invalidKey, 'Private keys are not supported for verifying.')
} else if (!key.includes(publicKeyPemMatcher) && !key.includes(publicKeyX509CertMatcher)) {
// Not a PEM, assume a plain secret
return hsAlgorithms
}
// if the key is a X509 cert we need to convert it
if (key.includes(publicKeyX509CertMatcher)) {
key = createPublicKey(key).export({ type: 'spki', format: 'pem' })
}
const keyData = PublicKey.decode(key, 'pem', { label: 'PUBLIC KEY' })
const oid = keyData.algorithm.algorithm.join('.')
let curveId
switch (oid) {
case '1.2.840.113549.1.1.1': // RSA
return rsaAlgorithms
case '1.2.840.10045.2.1': // EC
curveId = keyData.algorithm.parameters.join('.')
break
case '1.3.101.112': // Ed25519
case '1.3.101.113': // Ed448
return ['EdDSA']
default:
throw new TokenError(TokenError.codes.invalidKey, `Unsupported PEM PCKS8 public key with OID ${oid}.`)
}
const curve = ecCurves[curveId]
if (!curve) {
throw new TokenError(TokenError.codes.invalidKey, `Unsupported EC public key with curve ${curveId}.`)
}
return [`ES${curve.bits}`]
}
function detectPrivateKeyAlgorithm(key, providedAlgorithm) {
if (key instanceof Buffer) {
key = key.toString('utf-8')
} else if (typeof key !== 'string') {
throw new TokenError(TokenError.codes.invalidKey, 'The private key must be a string or a buffer.')
}
// Check cache first
const [cached, error] = privateKeysCache.get(key) || []
if (cached) {
return cached
} else if (error) {
throw error
}
// Try detecting
try {
const detectedAlgorithm = performDetectPrivateKeyAlgorithm(key)
if (detectedAlgorithm === 'ENCRYPTED') {
return cacheSet(privateKeysCache, key, providedAlgorithm)
}
return cacheSet(privateKeysCache, key, detectedAlgorithm)
} catch (e) {
throw cacheSet(privateKeysCache, key, null, TokenError.wrap(e, TokenError.codes.invalidKey, 'Unsupported PEM private key.'))
}
}
function detectPublicKeyAlgorithms(key) {
if (!key) {
return 'none'
}
// Check cache first
const [cached, error] = publicKeysCache.get(key) || []
if (cached) {
return cached
} else if (error) {
throw error
}
// Try detecting
try {
if (key instanceof Buffer) {
key = key.toString('utf-8')
} else if (typeof key !== 'string') {
throw new TokenError(TokenError.codes.invalidKey, 'The public key must be a string or a buffer.')
}
return cacheSet(publicKeysCache, key, performDetectPublicKeyAlgorithms(key))
} catch (e) {
throw cacheSet(
publicKeysCache,
key,
null,
TokenError.wrap(e, TokenError.codes.invalidKey, 'Unsupported PEM public key.')
)
}
}
function createSignature(algorithm, key, input) {
try {
const type = algorithm.slice(0, 2)
const alg = `sha${algorithm.slice(2)}`
let raw
let options
switch (type) {
case 'HS':
raw = createHmac(alg, key)
.update(input)
.digest('base64')
break
case 'ES':
raw = derToJose(directSign(alg, Buffer.from(input, 'utf-8'), key), algorithm).toString('base64')
break
case 'RS':
case 'PS':
options = {
key,
padding: RSA_PKCS1_PADDING,
saltLength: RSA_PSS_SALTLEN_MAX_SIGN
}
if (type === 'PS') {
options.padding = RSA_PKCS1_PSS_PADDING
options.saltLength = RSA_PSS_SALTLEN_DIGEST
}
raw = createSign(alg)
.update(input)
.sign(options)
.toString('base64')
break
case 'Ed':
raw = directSign(undefined, Buffer.from(input, 'utf-8'), key).toString('base64')
}
return raw.replace(base64UrlMatcher, base64UrlReplacer)
} catch (e) {
/* istanbul ignore next */
throw new TokenError(TokenError.codes.signError, 'Cannot create the signature.', { originalError: e })
}
}
function verifySignature(algorithm, key, input, signature) {
try {
const type = algorithm.slice(0, 2)
const alg = `SHA${algorithm.slice(2)}`
signature = Buffer.from(signature, 'base64')
if (type === 'HS') {
try {
return timingSafeEqual(
createHmac(alg, key)
.update(input)
.digest(),
signature
)
} catch (e) {
return false
}
} else if (type === 'Ed') {
// Check if supported on Node 10
/* istanbul ignore next */
if (typeof directVerify === 'function') {
return directVerify(undefined, Buffer.from(input, 'utf-8'), key, signature)
} else {
throw new TokenError(TokenError.codes.signError, 'EdDSA algorithms are not supported by your Node.js version.')
}
}
const options = { key, padding: RSA_PKCS1_PADDING, saltLength: RSA_PSS_SALTLEN_AUTO }
if (type === 'PS') {
options.padding = RSA_PKCS1_PSS_PADDING
options.saltLength = RSA_PSS_SALTLEN_DIGEST
} else if (type === 'ES') {
signature = joseToDer(signature, algorithm)
}
return createVerify('RSA-' + alg)
.update(input)
.verify(options, signature)
} catch (e) {
/* istanbul ignore next */
throw new TokenError(TokenError.codes.verifyError, 'Cannot verify the signature.', { originalError: e })
}
}
module.exports = {
useNewCrypto,
base64UrlMatcher,
base64UrlReplacer,
hsAlgorithms,
rsaAlgorithms,
esAlgorithms,
edAlgorithms,
detectPrivateKeyAlgorithm,
detectPublicKeyAlgorithms,
createSignature,
verifySignature
}