node-webtokens
Version:
Simple, opinionated implementation of JWS and JWE compact serialization
199 lines (184 loc) • 6.75 kB
JavaScript
const crypto = require('crypto');
const ecdsa = require('./ecdsa.js');
const { responder, buf2b64url, payloadVerifications } = require('./common.js');
const ALG_RE = /^(HS|RS|ES)(256|384|512)$/;
// ===== JWS GENERATION =======================================================
exports.generate = (alg, payload, ...rest) => {
// alg, payload, key[, cb] or alg, payload, keystore, kid[, cb]
let key;
let cb;
let header = { alg: alg };
if (rest[0].constructor !== Object) {
key = rest[0];
cb = typeof rest[1] === 'function' ? rest[1] : undefined;
} else {
header.kid = rest[1];
key = rest[0][rest[1]];
cb = typeof rest[2] === 'function' ? rest[2] : undefined;
if (!key) {
return responder(new TypeError('Invalid key identifier'), null, cb);
}
}
let match = typeof alg === 'string' ? alg.match(ALG_RE) : null;
if (!match) {
return responder(new TypeError('Unrecognized algorithm'), null, cb);
}
let generateJws;
if (match[1] === 'HS') {
generateJws = generateHsJws;
} else if (match[1] === 'RS') {
generateJws = generateRsJws;
} else {
generateJws = generateEsJws;
}
payload.iat = Math.floor(Date.now() / 1000);
let h = buf2b64url(Buffer.from(JSON.stringify(header)));
let p = buf2b64url(Buffer.from(JSON.stringify(payload)));
let token;
try {
token = generateJws(`${h}.${p}`, +match[2], key);
} catch (error) {
return responder(error, null, cb);
}
return responder(null, token, cb);
}
// ===== JWS VERIFICATION =====================================================
exports.verify = (parsed, key, cb) => {
if (parsed.error) return responder(null, parsed, cb);
if (typeof parsed.header.alg !== 'string') {
parsed.error = { message: 'Missing or invalid alg claim in header' };
return responder(null, parsed, cb);
}
let match = parsed.header.alg.match(ALG_RE);
if (!match) {
parsed.error = { message: `Unrecognized algorithm ${parsed.header.alg}` };
return responder(null, parsed, cb);
}
if (parsed.algList && !parsed.algList.includes(parsed.header.alg)) {
parsed.error = { message: `Unwanted algorithm ${parsed.header.alg}` };
return responder(null, parsed, cb);
}
let protect = `${parsed.parts[0]}.${parsed.parts[1]}`;
let verifyJws;
if (match[1] === 'HS') {
verifyJws = verifyHsJws;
} else if (match[1] === 'RS') {
verifyJws = verifyRsJws;
} else {
verifyJws = verifyEsJws;
}
let integrity;
try {
integrity = verifyJws(protect, parsed.parts[2], +match[2], key);
} catch (error) {
parsed.error = { message: `Could not verify integrity. ${error.message}` };
return responder(null, parsed, cb);
}
if (!integrity) {
parsed.error = { message: 'Integrity check failed' };
return responder(null, parsed, cb);
}
return payloadVerifications(parsed, cb);
}
// ===== HS256, HS384, HS512 ==================================================
function generateHsJws(protect, bits, key) {
if (typeof key === 'string') {
key = Buffer.from(key, 'base64');
} else if (!(key instanceof Buffer)) {
throw new TypeError('Key must be a buffer or a base64 string');
}
let bytes = bits >> 3;
if (key.length < bytes) {
throw new TypeError(`Key length must be at least ${bytes} bytes`);
}
let hmac = crypto.createHmac(`SHA${bits}`, key);
let mac = buf2b64url(hmac.update(protect).digest());
return `${protect}.${mac}`;
}
function verifyHsJws(protect, mac, bits, key) {
if (typeof key === 'string') {
key = Buffer.from(key, 'base64');
} else if (!(key instanceof Buffer)) {
throw new TypeError('Key must be a buffer or a base64 string');
}
let bytes = bits >> 3;
if (key.length < bytes) {
throw new TypeError(`Key length must be at least ${bytes} bytes`);
}
let hmac = crypto.createHmac(`SHA${bits}`, key);
hmac.update(protect);
return crypto.timingSafeEqual(hmac.digest(), Buffer.from(mac, 'base64'));
}
// ===== RS256, RS384, RS512 ==================================================
function generateRsJws(protect, bits, key) {
if (key instanceof Buffer) {
key = key.toString();
} else if (typeof key !== 'string') {
throw new TypeError('Key must be a buffer or a UTF-8 string');
}
if (!key.includes('-----BEGIN') || !key.includes('KEY-----')) {
throw new TypeError('Key must be a PEM formatted RSA private key');
}
let signer = crypto.createSign(`SHA${bits}`);
let signature = buf2b64url(signer.update(protect).sign(key));
return `${protect}.${signature}`;
}
function verifyRsJws(protect, signature, bits, key) {
if (key instanceof Buffer) {
key = key.toString();
} else if (typeof key !== 'string') {
throw new TypeError('Key must be a buffer or a UTF-8 string');
}
if (!key.includes('-----BEGIN') ||
!(key.includes('KEY-----') || key.includes('CERTIFICATE-----'))) {
throw new TypeError('Key must be a PEM formatted RSA public key');
}
let verifier = crypto.createVerify(`SHA${bits}`);
verifier.update(protect);
return verifier.verify(key, signature, 'base64');
}
// ===== ES256, ES384, ES512 ==================================================
function generateEsJws(protect, bits, key) {
if (key instanceof Buffer) {
key = key.toString();
} else if (typeof key !== 'string') {
throw new TypeError('Key must be a buffer or a UTF-8 string');
}
if (!key.includes('-----BEGIN') || !key.includes('KEY-----')) {
throw new TypeError('Key must be a PEM formatted EC private key');
}
let signer = crypto.createSign(`SHA${bits}`);
signer.update(protect);
let size = 32;
if (bits === 384) {
size = 48;
} else if (bits === 512) {
size = 66;
}
let signature = buf2b64url(ecdsa.derToConcat(signer.sign(key), size));
return `${protect}.${signature}`;
}
function verifyEsJws(protect, signature, bits, key) {
if (key instanceof Buffer) {
key = key.toString();
} else if (typeof key !== 'string') {
throw new TypeError('Key must be a buffer or a UTF-8 string');
}
if (!key.includes('-----BEGIN') ||
!(key.includes('KEY-----') || key.includes('CERTIFICATE-----'))) {
throw new TypeError('Key must be a PEM formatted EC public key');
}
signature = Buffer.from(signature, 'base64');
let size = 32;
if (bits === 384) {
size = 48;
} else if (bits === 512) {
size = 66;
}
if (signature.length !== size << 1) {
return false;
}
let verifier = crypto.createVerify(`SHA${bits}`);
verifier.update(protect);
return verifier.verify(key, ecdsa.concatToDer(signature, size));
}