2021-12-07 13:18:08 -05:00

107 lines
3.2 KiB
JavaScript

const { strict: assert } = require('assert');
const { createHash } = require('crypto');
const { format } = require('util');
const shake256 = require('./shake256');
let encode;
if (Buffer.isEncoding('base64url')) {
encode = (input) => input.toString('base64url');
} else {
const fromBase64 = (base64) => base64.replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
encode = (input) => fromBase64(input.toString('base64'));
}
/** SPECIFICATION
* Its (_hash) value is the base64url encoding of the left-most half of the hash of the octets of
* the ASCII representation of the token value, where the hash algorithm used is the hash algorithm
* used in the alg Header Parameter of the ID Token's JOSE Header. For instance, if the alg is
* RS256, hash the token value with SHA-256, then take the left-most 128 bits and base64url encode
* them. The _hash value is a case sensitive string.
*/
/**
* @name getHash
* @api private
*
* returns the sha length based off the JOSE alg heade value, defaults to sha256
*
* @param token {String} token value to generate the hash from
* @param alg {String} ID Token JOSE header alg value (i.e. RS256, HS384, ES512, PS256)
* @param [crv] {String} For EdDSA the curve decides what hash algorithm is used. Required for EdDSA
*/
function getHash(alg, crv) {
switch (alg) {
case 'HS256':
case 'RS256':
case 'PS256':
case 'ES256':
case 'ES256K':
return createHash('sha256');
case 'HS384':
case 'RS384':
case 'PS384':
case 'ES384':
return createHash('sha384');
case 'HS512':
case 'RS512':
case 'PS512':
case 'ES512':
return createHash('sha512');
case 'EdDSA':
switch (crv) {
case 'Ed25519':
return createHash('sha512');
case 'Ed448':
if (!shake256) {
throw new TypeError('Ed448 *_hash calculation is not supported in your Node.js runtime version');
}
return createHash('shake256', { outputLength: 114 });
default:
throw new TypeError('unrecognized or invalid EdDSA curve provided');
}
default:
throw new TypeError('unrecognized or invalid JWS algorithm provided');
}
}
function generate(token, alg, crv) {
const digest = getHash(alg, crv).update(token).digest();
return encode(digest.slice(0, digest.length / 2));
}
function validate(names, actual, source, alg, crv) {
if (typeof names.claim !== 'string' || !names.claim) {
throw new TypeError('names.claim must be a non-empty string');
}
if (typeof names.source !== 'string' || !names.source) {
throw new TypeError('names.source must be a non-empty string');
}
assert(typeof actual === 'string' && actual, `${names.claim} must be a non-empty string`);
assert(typeof source === 'string' && source, `${names.source} must be a non-empty string`);
let expected;
let msg;
try {
expected = generate(source, alg, crv);
} catch (err) {
msg = format('%s could not be validated (%s)', names.claim, err.message);
}
msg = msg || format('%s mismatch, expected %s, got: %s', names.claim, expected, actual);
assert.equal(expected, actual, msg);
}
module.exports = {
validate,
generate,
};