UNPKG

node-webtokens

Version:

Simple, opinionated implementation of JWS and JWE compact serialization

360 lines (340 loc) 13.4 kB
const crypto = require('crypto'); const { responder, buf2b64url, payloadVerifications } = require('./common.js'); const ALG_RE = /^(PBES2-HS(256|384|512)\053)?(RSA-OAEP|dir|A(128|192|256)KW)$/; const ENC_RE = /^A(128|192|256)(GCM|CBC-HS(256|384|512))$/; // ===== JWE GENERATION ======================================================= exports.generate = (alg, enc, payload, ...rest) => { // alg, enc, payload, key[, cb] or alg, enc, payload, keystore, kid[, cb] let key; let cb; let header = { alg: alg, enc: enc }; 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 aMatch = typeof alg === 'string' ? alg.match(ALG_RE) : null; if (!aMatch || (aMatch[2] && +aMatch[2] !== aMatch[4] * 2)) { let error = new TypeError('Unrecognized key management algorithm'); return responder(error, null, cb); } let eMatch = typeof enc === 'string' ? enc.match(ENC_RE) : null; if (!eMatch || (eMatch[3] && +eMatch[3] !== eMatch[1] * 2)) { let error = new TypeError('Unrecognized content encryption algorithm'); return responder(error, null, cb); } let salt; if (aMatch[2]) { let p2s = crypto.randomBytes(16); header.p2c = 1024; header.p2s = buf2b64url(p2s); salt = Buffer.concat([Buffer.from(alg), Buffer.from([0]), p2s]); if (!cb) { let bits = Number(aMatch[2]); key = crypto.pbkdf2Sync(key, salt, header.p2c, bits >> 4, `sha${bits}`); } } let aad = buf2b64url(Buffer.from(JSON.stringify(header))); if (!aMatch[2] || !cb) { return generateJwe(aMatch, eMatch, aad, payload, key, cb); } let bits = Number(aMatch[2]); crypto.pbkdf2(key, salt, header.p2c, bits >> 4, `sha${bits}`, (err, key) => { if (err) return cb(err); generateJwe(aMatch, eMatch, aad, payload, key, cb); }); } function generateJwe(aMatch, eMatch, aad, payload, key, cb) { let cekLen = eMatch[3] ? +eMatch[1] >> 2 : +eMatch[1] >> 3; let contEncr = eMatch[3] ? contentEncryptCbc : contentEncryptGcm; let cek; let cekEnc; if (aMatch[0] !== 'dir') { cek = crypto.randomBytes(cekLen); let keyEncr = aMatch[4] ? aesKeyWrap : rsaOaepEncrypt; try { cekEnc = keyEncr(cek, key, +aMatch[4]); } catch (error) { return responder(error, null, cb); } } else { // Key must be directly used for content encryption if (typeof key === 'string') { key = Buffer.from(key, 'base64'); } else if (!(key instanceof Buffer)) { let error = new TypeError('Key must be a buffer or a base64 string'); return responder(error, null, cb); } if (key.length < cekLen) { let error = new TypeError(`Key must be at least ${cekLen} bytes`); return responder(error, null, cb); } cek = key.slice(0, cekLen); cekEnc = ''; } payload.iat = Math.floor(Date.now() / 1000); let token = contEncr(aad, cek, cekEnc, JSON.stringify(payload), +eMatch[1]); return responder(null, token, cb); } // ===== JWE 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); } if (typeof parsed.header.enc !== 'string') { parsed.error = { message: 'Missing or invalid enc claim in header' }; return responder(null, parsed, cb); } let aMatch = parsed.header.alg.match(ALG_RE); if (!aMatch || (aMatch[2] && +aMatch[2] !== aMatch[4] * 2)) { parsed.error = { message: `Unrecognized key management algorithm ${parsed.header.alg}` }; return responder(null, parsed, cb); } if (parsed.algList && !parsed.algList.includes(parsed.header.alg)) { parsed.error = { message: `Unwanted key management algorithm ${parsed.header.alg}` }; return responder(null, parsed, cb); } let eMatch = parsed.header.enc.match(ENC_RE); if (!eMatch || (eMatch[3] && +eMatch[3] !== eMatch[1] * 2)) { parsed.error = { message: `Unrecognized content encryption algorithm ${parsed.header.enc}` }; return responder(null, parsed, cb); } if (parsed.encList && !parsed.encList.includes(parsed.header.enc)) { parsed.error = { message: `Unwanted content encryption algorithm ${parsed.header.enc}` }; return responder(null, parsed, cb); } let salt; let iter; if (aMatch[2]) { iter = parsed.header.p2c; if (!Number.isInteger(iter) || iter < 1 || iter > 16384) { parsed.error = { message: 'Missing or invalid p2c claim in header' }; return responder(null, parsed, cb); } else if (!cb && iter > 1024) { parsed.error = { message: 'p2c value too large for synchronous mode' }; return responder(null, parsed, cb); } if (typeof parsed.header.p2s !== 'string') { parsed.error = { message: 'Missing or invalid p2s claim in header' }; return responder(null, parsed, cb); } let p2s = Buffer.from(parsed.header.p2s, 'base64'); let alg = parsed.header.alg; salt = Buffer.concat([Buffer.from(alg), Buffer.from([0]), p2s]); if (!cb) { let bits = Number(aMatch[2]); key = crypto.pbkdf2Sync(key, salt, iter, bits >> 4, `sha${bits}`); } } if (!aMatch[2] || !cb) { return decryptJwe(parsed, aMatch, eMatch, key, cb); } let bits = Number(aMatch[2]); crypto.pbkdf2(key, salt, iter, bits >> 4, `sha${bits}`, (error, key) => { if (error) return cb(error); decryptJwe(parsed, aMatch, eMatch, key, cb); }); } function decryptJwe(parsed, aMatch, eMatch, key, cb) { let aad = Buffer.from(parsed.parts[0]); let cekEnc = Buffer.from(parsed.parts[1], 'base64'); let iv = Buffer.from(parsed.parts[2], 'base64'); let content = Buffer.from(parsed.parts[3], 'base64'); let tag = Buffer.from(parsed.parts[4], 'base64'); let cekLen = eMatch[3] ? +eMatch[1] >> 2 : +eMatch[1] >> 3; let contDecr = eMatch[3] ? contentDecryptCbc : contentDecryptGcm; let cek; if (aMatch[0] !== 'dir') { let keyDecr = aMatch[4] ? aesKeyUnwrap : rsaOaepDecrypt; try { cek = keyDecr(cekEnc, key, +aMatch[4]); } catch (error) { parsed.error = { message: `Could not decrypt token. ${error.message}` }; return responder(null, parsed, cb); } } else { // Key must be directly used for content decryption if (typeof key === 'string') { key = Buffer.from(key, 'base64'); } else if (!(key instanceof Buffer)) { parsed.error = { message: 'Invalid key' }; return responder(null, parsed, cb); } if (key.length < cekLen) { parsed.error = { message: `Invalid key length. Must be at least ${cekLen} bytes` }; return responder(null, parsed, cb); } cek = key.slice(0, cekLen); } let plain; try { plain = contDecr(content, aad, tag, cek, iv, +eMatch[1]); } catch (error) { parsed.error = { message: `Could not decrypt token. ${error.message}` }; return responder(null, parsed, cb); } try { parsed.payload = JSON.parse(plain); } catch (error) { parsed.error = { message: `Non parsable payload. ${error.message}` }; return responder(null, parsed, cb); } return payloadVerifications(parsed, cb); } // ===== A128GCM, A192GCM, A256GCM ============================================ function contentEncryptGcm(aad, cek, cekEnc, plain, bits) { let iv = crypto.randomBytes(12); let cipher = crypto.createCipheriv(`id-aes${bits}-GCM`, cek, iv); cipher.setAutoPadding(false); cipher.setAAD(Buffer.from(aad)); let enc = buf2b64url(Buffer.concat([cipher.update(plain), cipher.final()])); let tag = buf2b64url(cipher.getAuthTag()); return `${aad}.${cekEnc}.${buf2b64url(iv)}.${enc}.${tag}`; } function contentDecryptGcm(content, aad, tag, cek, iv, bits) { let decipher = crypto.createDecipheriv(`id-aes${bits}-GCM`, cek, iv); decipher.setAutoPadding(false); decipher.setAAD(aad); decipher.setAuthTag(tag); return Buffer.concat([decipher.update(content), decipher.final()]); } // ===== A128CBC-HS256, A192CBC-HS384, A256CBC-HS512 ========================== function contentEncryptCbc(aad, cek, cekEnc, plain, bits) { let iv = crypto.randomBytes(16); let bytes = bits >> 3; let cipher = crypto.createCipheriv(`AES-${bits}-CBC`, cek.slice(bytes), iv); let enc = Buffer.concat([cipher.update(plain), cipher.final()]); let len = aad.length << 3; let al = Buffer.from(`000000000000000${len.toString(16)}`.slice(-16), 'hex'); let hmac = crypto.createHmac(`SHA${bits << 1}`, cek.slice(0, bytes)); hmac.update(Buffer.from(aad)).update(iv).update(enc).update(al); let tag = buf2b64url(hmac.digest().slice(0, bytes)); return `${aad}.${cekEnc}.${buf2b64url(iv)}.${buf2b64url(enc)}.${tag}`; } function contentDecryptCbc(content, aad, tag, cek, iv, bits) { let bytes = bits >> 3; let len = aad.length << 3; let al = Buffer.from(`000000000000000${len.toString(16)}`.slice(-16), 'hex'); let hmac = crypto.createHmac(`SHA${bits << 1}`, cek.slice(0, bytes)); hmac.update(aad).update(iv).update(content).update(al); if (!crypto.timingSafeEqual(hmac.digest().slice(0, bytes), tag)) { throw new Error('Authentication of encrypted data failed'); } let encKey = cek.slice(bytes); let decipher = crypto.createDecipheriv(`AES-${bits}-CBC`, encKey, iv); return Buffer.concat([decipher.update(content), decipher.final()]); } // ===== RSA-OAEP ============================================================= function rsaOaepEncrypt(cek, key) { if (key instanceof Buffer) { key = key.toString(); } else if (typeof key !== 'string') { throw new TypeError('Key must be a buffer or a string'); } if (!key.includes('-----BEGIN') || !(key.includes('KEY-----') || key.includes('CERTIFICATE-----'))) { throw new TypeError('Key must be a PEM formatted RSA public key'); } let options = {key: key, padding: crypto.constants.RSA_PKCS1_OAEP_PADDING}; return buf2b64url(crypto.publicEncrypt(options, cek)); } function rsaOaepDecrypt(cekEnc, key) { if (key instanceof Buffer) { key = key.toString(); } if (typeof key !== 'string' || !key.includes('-----BEGIN') || !key.includes('KEY-----')) { throw new TypeError('Key must be a PEM formatted RSA private key'); } let options = {key: key, padding: crypto.constants.RSA_PKCS1_OAEP_PADDING}; return crypto.privateDecrypt(options, cekEnc); } // ===== A128KW, A192KW, A256KW =============================================== function aesKeyWrap(cek, key, bits) { let bytes = bits >> 3; 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-encoded string'); } if (key.length < bytes) { throw new TypeError(`Key length must be at least ${bytes} bytes`); } key = key.slice(0, bytes); let r = []; for (let i = 0; i < cek.length; i += 8) { r.push(cek.slice(i, i + 8)); } let iv = Buffer.alloc(16); let a = Buffer.from('A6A6A6A6A6A6A6A6', 'hex'); let count = 1; for (let j = 0; j < 6; j++) { for (let i = 0; i < r.length; i++) { let cipher = crypto.createCipheriv(`AES${bits}`, key, iv); let c = `000000000000000${count.toString(16)}`.slice(-16); let b = cipher.update(Buffer.concat([a, r[i]])); a = Buffer.from(c, 'hex'); for (let n = 0; n < 8; n++) { a[n] ^= b[n]; } r[i] = b.slice(8, 16); count++; } } return buf2b64url(Buffer.concat([a].concat(r))); } function aesKeyUnwrap(cekEnc, key, bits) { let bytes = bits >> 3; 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'); } if (key.length < bytes) { throw new TypeError(`Key must be at least ${bytes} bytes`); } key = key.slice(0, bytes); let a = cekEnc.slice(0, 8); let r = []; for (let i = 8; i < cekEnc.length; i += 8) { r.push(cekEnc.slice(i, i + 8)); } let z = Buffer.alloc(16); let count = 6 * r.length; for (let j = 5; j >= 0 ; j--) { for (let i = r.length - 1; i >= 0; i--) { let c = `000000000000000${count.toString(16)}`.slice(-16); c = Buffer.from(c, 'hex'); for (let n = 0; n < 8; n++) { a[n] ^= c[n]; } let decipher = crypto.createDecipheriv(`AES${bits}`, key, z); let b = decipher.update(Buffer.concat([a, r[i], z])); a = b.slice(0, 8); r[i] = b.slice(8, 16); count--; } } if (!a.equals(Buffer.from('A6A6A6A6A6A6A6A6', 'hex'))) { throw new Error('Key unwrapping failed'); } return Buffer.concat(r); }