UNPKG

passport-azure-ad

Version:

OIDC and Bearer Passport strategies for Azure Active Directory

529 lines (453 loc) 18.9 kB
/** * Copyright (c) Microsoft Corporation * All Rights Reserved * MIT License * * Permission is hereby granted, free of charge, to any person obtaining a copy of this * software and associated documentation files (the "Software"), to deal in the Software * without restriction, including without limitation the rights to use, copy, modify, * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to * permit persons to whom the Software is furnished to do so, subject to the following * conditions: * * The above copyright notice and this permission notice shall be * included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS * OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT * OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /* eslint-disable no-new */ 'use strict'; var crypto = require('crypto'); var constants = require('constants'); const base64url = require('base64url'); var aadutils = require('./aadutils'); var jwkToPem = require('jwk-to-pem'); /****************************************************************************** * utility functions *****************************************************************************/ /** * Create a buffer depends on the version of node * * @param{Buffer/String/Number} data: buffer, string, or size * @param{String} encoding: ignored if data is a buffer * @return{Buffer} */ var createBuffer = (data, encoding) => { if (!Buffer.isBuffer(data) && typeof data !== 'string' && typeof data !== 'number') throw new Error('in createBuffer, data must be a buffer, string or number'); if (process.version >= 'v6') { if (typeof data === 'string') return Buffer.from(data, encoding); else if (typeof data === 'number') return Buffer.alloc(data); else return Buffer.from(data); } else { if (typeof data === 'string') return new Buffer(data, encoding); else return new Buffer(data); } }; /** * xor of two number in buffer format * * @param{Buffer} a: first number * @param{Buffer} b: second number * @return{Buffer} a^b */ var xor = (a, b) => { var c1, c2; if (a.length > b.length) { c1 = a; c2 = b; } else { c2 = a; c1 = b; } var c = createBuffer(c1); for (var i = 1; i <= c2.length; i++) c[c1.length-i] = c[c1.length-i] ^ c2[c2.length-i]; return c; }; /****************************************************************************** * AES key unwrap algorithms *****************************************************************************/ /** * Unwrap wrapped_cek using AES with kek * * @param{String} algorithm: algorithm * @param{Buffer} wrapped_cek: wrapped key * @param{Buffer} kek: key encryption key * @return{Buffer} cek, the unwrapped key */ var AESKeyUnWrap = (algorithm, wrapped_cek, kek) => { /**************************************************************************** * Inputs: CipherText, (n+1) 64-bit values {C0, C1, ..., Cn}, and * Key, K (the KEK) * Outputs: Plaintext, n 64-bit values {P0, P1, K, Pn} ***************************************************************************/ var C = wrapped_cek; var n = C.length/8-1; var K = kek; /**************************************************************************** * 1) Initialize variables * Set A = C[0] * For i = 1 to b * R[i] = C[i] ***************************************************************************/ var A = C.slice(0,8); var R = [createBuffer(1)]; for (var i = 1; i <= n; i++) R.push(C.slice(8*i, 8*i+8)); /**************************************************************************** * 2) compute intermediate values * For j = 5 to 0 * For i = n to 1 * B = AES-1(K, (A^t) | R[i]) where t = n*j+i * A = MSB(64, B) * R[i] = LSB(64, B) ***************************************************************************/ for(var j=5; j >= 0; j--) { for(var i=n; i >= 1; i--) { // turn t = n*j+i into buffer var str = (n*j+i).toString(16); if (str.length %2 !== 0) str = '0' + str; var t = createBuffer(str, 'hex'); // B = AES-1(K, (A^t) | R[i]) var aes = crypto.createDecipheriv(algorithm, K, ''); aes.setAutoPadding(false); var B = aes.update(Buffer.concat([xor(A, t), R[i]]), null, 'hex'); B += aes.final('hex'); B = createBuffer(B, 'hex'); // A = MSB(64, B) A = B.slice(0, 8); // R[i] = LSB(64, B) R[i] = B.slice(B.length-8); } } /**************************************************************************** * 3) Output results. * If A is an appropriate initial value * Then * For i = 1 to n * P[i] = R[i] * Else * Return an error ***************************************************************************/ // check A if (A.toString('hex').toUpperCase() === 'A6A6A6A6A6A6A6A6') { var result = R[1]; for (var i = 2; i <= n; i++) result = Buffer.concat([result, R[i]]); return result; } else { throw new Error('aes decryption failed: invalid key'); } }; /****************************************************************************** * AES-CBC-HMAC-SHA2 decryption *****************************************************************************/ /** * decrypt ciperText using aes-cbc-hmac-sha2 algorithm * * @param{String} algorithm: algorithm * @param{Buffer} key: encryption key * @param{Buffer} cipherText: encrypted content * @param{Buffer} iv: intialization vector * @param{Buffer} aad: additional authentication data * @param{Buffer} authTag: authentication tag * @return{Buffer} decrypted content */ var decrypt_AES_CBC_HMAC_SHA2 = (algorithm, key, cipherText, iv, aad, authTag) => { // algorithm information // note ENC_KEY_LEN = MAC_KEY_LEN = T_LEN = len var algInfo = { 'aes-128-cbc-hmac-sha-256': { 'cipher_algo': 'aes-128-cbc', 'hash_algo': 'sha256', 'len': 16 }, 'aes-192-cbc-hmac-sha-384': { 'cipher_algo': 'aes-192-cbc', 'hash_algo': 'sha384', 'len': 24 }, 'aes-256-cbc-hmac-sha-512': { 'cipher_algo': 'aes-256-cbc', 'hash_algo': 'sha512', 'len': 32 } }; // 1. Verify input // check if we have a supported algorithm if (!algorithm) throw new Error('In decrypt_AES_CBC_HMAC_SHA2: algorithm is not provided'); if (!algInfo[algorithm]) throw new Error('In decrypt_AES_CBC_HMAC_SHA2: unsupported algorithm: ' + algorithm); var algo = algInfo[algorithm]; // check the size of key if (!key) throw new Error('In decrypt_AES_CBC_HMAC_SHA2: key is not provided'); if (!(key instanceof Buffer)) throw new Error('In decrypt_AES_CBC_HMAC_SHA2: key must be a buffer'); if (key.length !== algInfo[algorithm].len * 2) throw new Error('In decrypt_AES_CBC_HMAC_SHA2: key has size ' + key.length + ', it must have size ' + algo.len * 2); // check the size of iv if (!iv) throw new Error('In decrypt_AES_CBC_HMAC_SHA2: iv is not provided'); if (!(iv instanceof Buffer)) throw new Error('In decrypt_AES_CBC_HMAC_SHA2: iv must be a buffer'); if (iv.length !== 16) throw new Error('In decrypt_AES_CBC_HMAC_SHA2: iv has size ' + iv.length + ', it must have size 16'); // check the existence of aad if (!aad) throw new Error('In decrypt_AES_CBC_HMAC_SHA2: aad is not provided'); if (!(aad instanceof Buffer)) throw new Error('In decrypt_AES_CBC_HMAC_SHA2: aad must be a buffer'); // 2. Split key // first half is mac_key and the second half is enc_key var mac_key = key.slice(0, algo.len); var enc_key = key.slice(algo.len); // 3. Verify tag // tag should be: hash(aad + iv + cipherText + aad_len) where aad_len is the // number of bits in aad expressed as a 64-bit unsigned big-endian integer // 3.1 compute aad_len var aad_len = createBuffer(8); aad_len.writeUInt32BE(aad.length * 8, 4); aad_len.writeUInt32BE(0, 0); // 3.2 create hmac algorithm var hmac = crypto.createHmac(algo.hash_algo, mac_key); hmac.write(aad); hmac.write(iv); hmac.update(cipherText); hmac.update(aad_len); var computed_authTag = hmac.digest().slice(0, algo.len); // 3.3 verify the tag if (!authTag || !computed_authTag || (authTag.toString('hex') !== computed_authTag.toString('hex'))) throw new Error('In decrypt_AES_CBC_HMAC_SHA2: invalid authentication tag'); // 4. Decrypt cipherText var decipher = crypto.createDecipheriv(algo.cipher_algo, enc_key, iv); var plainText = decipher.update(cipherText); plainText = Buffer.concat([plainText, decipher.final()]); return plainText; }; /****************************************************************************** * JWE decryption *****************************************************************************/ /** * decrypt cek helper function * * @param{String} alg: algorithm * @param{Buffer} encrypted_cek: encrypted cek * @param{Buffer} key: encryption key * @return{Function} log: logging function */ var decryptCEKHelper = (alg, encrypted_cek, key, log) => { var error = null; var cek = null; try { var key_to_use; if (alg === 'RSA1_5' || alg === 'RSA-OAEP') { if (key['privatePemKey']) { log.info('using RSA privatePemKey to decrypt cek'); key_to_use = key['privatePemKey']; } else { log.info('converting jwk to RSA privatePemKey to decrypt cek'); key_to_use = jwkToPem(key, {private: true}); } } else if (alg === 'A128KW' || alg === 'A256KW') { log.info('using symmetric key to decrypt cek'); key_to_use = base64url.toBuffer(key.k); } else { log.info('unsupported alg: ' + alg); return {'error': error, 'cek': null}; } if (alg === 'RSA1_5') cek = crypto.privateDecrypt({ key: key_to_use, padding: constants.RSA_PKCS1_PADDING }, encrypted_cek); else if (alg === 'RSA-OAEP') cek = crypto.privateDecrypt({ key: key_to_use, padding: constants.RSA_PKCS1_OAEP_PADDING }, encrypted_cek); else if (alg === 'A128KW') cek = AESKeyUnWrap('aes-128-ecb', encrypted_cek, key_to_use); else if (alg === 'A256KW') cek = AESKeyUnWrap('aes-256-ecb', encrypted_cek, key_to_use); else cek = key_to_use; // dir } catch (ex) { error = ex; } return {'error': error, 'cek': cek}; }; /** * decrypt cek function (except header.alg == dir) * * @param{Object} header: header * @param{Buffer} encrypted_cek: encrypted cek * @param{Object} jweKeyStore: list of keys * @return{Function} log: logging function */ var decryptCEK = (header, encrypted_cek, jweKeyStore, log) => { var algKtyMapper = { 'RSA1_5': 'RSA', 'RSA-OAEP': 'RSA', 'A128KW': 'oct', 'A256KW': 'oct'}; if (!header.alg) return { 'error': new Error('alg is missing in JWE header'), 'cek': null }; if(['RSA1_5', 'RSA-OAEP', 'dir', 'A128KW', 'A256KW'].indexOf(header.alg) === -1) return { 'error' : new Error('Unsupported alg in JWE header: ' + header.alg), 'cek': null }; var key = null; if (header.kid) { for (var i = 0; i < jweKeyStore.length; i++) { if (header.kid === jweKeyStore[i].kid && algKtyMapper[header.alg] === jweKeyStore[i].kty) { key = jweKeyStore[i]; log.info('found a key matching kid: ' + header.kid); return decryptCEKHelper(header.alg, encrypted_cek, key, log); } } return { 'error': new Error('cannot find a key matching kid: ' + header.kid), 'cek': null }; } log.info('kid is not provided in JWE header, now we try all the possible keys'); // kid matching failed, now we try every possible key for(var i = 0; i < jweKeyStore.length; i++) { if (jweKeyStore[i].kty === algKtyMapper[header.alg]) { var result = decryptCEKHelper(header.alg, encrypted_cek, jweKeyStore[i], log); if (!result.error && result.cek) return result; } } return { 'error': new Error('tried all keys to decrypt cek but none of them works'), 'cek': null }; }; /** * decrypt content function with provided cek * * @param{Object} header: header * @param{Buffer} cek: cek * @param{Buffer} cipherText: encrypted content * @param{Buffer} iv: initialization vector * @param{Buffer} authTag: authentication tag * @param{Buffer} aad: additional authentication information * @return{Function} log: logging function */ var decryptContent = (header, cek, cipherText, iv, authTag, aad, log) => { if (!header.enc) return { 'error': new Error('enc is missing in JWE header'), 'content': null }; if (['A128GCM', 'A256GCM', 'A128CBC-HS256', 'A192CBC-HS384', 'A256CBC-HS512'].indexOf(header.enc) === -1) return { 'error' : new Error('Unsupported enc in JWE header: ' + header.enc), 'content': null }; var mapper = { 'A128GCM': 'aes-128-gcm', 'A256GCM': 'aes-256-gcm', 'A128CBC-HS256': 'aes-128-cbc-hmac-sha-256', 'A192CBC-HS384': 'aes-192-cbc-hmac-sha-384', 'A256CBC-HS512': 'aes-256-cbc-hmac-sha-512' }; try { var content = null; if (header.enc === 'A128GCM' || header.enc === 'A256GCM') { var decipher = crypto.createDecipheriv(mapper[header.enc], cek, iv); decipher.setAAD(aad); decipher.setAuthTag(authTag); content = decipher.update(cipherText); content = Buffer.concat([content, decipher.final()]); } else { content = decrypt_AES_CBC_HMAC_SHA2(mapper[header.enc], cek, cipherText, iv, aad, authTag); } return { 'error': null, 'content': content.toString() }; } catch (ex) { return { 'error': ex, 'content': null }; } }; /** * decrypt content function for the case where alg is dir * * @param{Object} header: header * @param{Buffer} jweKeyStore: list of keys * @param{Buffer} cipherText: encrypted content * @param{Buffer} iv: initialization vector * @param{Buffer} authTag: authentication tag * @param{Buffer} aad: additional authentication information * @return{Function} log: logging function */ var decryptContentForDir = (header, jweKeyStore, cipherText, iv, authTag, aad, log) => { var key = null; // try the key with the corresponding kid if (header.kid) { for (var i = 0; i < jweKeyStore.length; i++) { if (header.kid === jweKeyStore[i].kid && jweKeyStore[i].kty === 'oct') { key = jweKeyStore[i]; log.info('Decrypting JWE content, header.alg == dir, found a key matching kid: ' + header.kid); break; } } if (key) return decryptContent(header, base64url.toBuffer(key.k), cipherText, iv, authTag, aad, log); else return { 'error': new Error('cannot find a key matching kid: ' + header.kid), 'cek': null }; } log.info('In decryptContentForDir: kid is not provided in JWE header, now we try all the possible keys'); // kid matching failed, now we try every possible key for(var i = 0; i < jweKeyStore.length; i++) { if (jweKeyStore[i].kty === 'oct') { var result = decryptContent(header, base64url.toBuffer(jweKeyStore[i].k), cipherText, iv, authTag, aad, log); if (!result.error) return result; } } log.info('In decryptContentForDir: tried all keys to decrypt the content but all failed'); return { error: new Error('In decryptContentForDir: tried all keys to decrypt the content but all failed'), content_result: null }; }; /** * The main decryption function * * @param{String} jweString: JWE id_token * @param{Object} jweKeyStore: list of keys * @return{Function} log: logging function * @return{Function} callback: callback function */ exports.decrypt = (jweString, jweKeyStore, log, callback) => { /**************************************************************************** * JWE compact format structure **************************************************************************** * BASE64URL(UTF8(JWE Protected Header)) || '.' || * BASE64URL(JWE Encrypted Key) || '.' || * BASE64URL(JWE Initialization Vector) || '.' || * BASE64URL(JWE Ciphertext) || '.' || * BASE64URL(JWE Authentication Tag) ***************************************************************************/ var parts = jweString.split('.'); if (parts.length !== 5) return callback(new Error('In jwe.decrypt: invalid JWE string, it has ' + parts.length + ' parts instead of 5'), null); var header; try { header = JSON.parse(base64url.decode(parts[0], 'binary')); } catch (ex) { return callback(new Error('In jwe.decrypt: failed to parse JWE header'), null); } var aad = createBuffer(parts[0]); var encrypted_cek = base64url.toBuffer(parts[1]); var iv = base64url.toBuffer(parts[2]); var cipherText = base64url.toBuffer(parts[3]); var authTag = base64url.toBuffer(parts[4]); log.info('In jwe.decrypt: the header is ' + JSON.stringify(header)); var cek_result; var content_result; // special treatment of 'dir' if (header.alg === 'dir') { content_result = decryptContentForDir(header, jweKeyStore, cipherText, iv, authTag, aad, log); } else { /**************************************************************************** * cek decryption ***************************************************************************/ var cek_result = decryptCEK(header, encrypted_cek, jweKeyStore, log); if (cek_result.error) return callback(cek_result.error); /**************************************************************************** * content decryption ***************************************************************************/ var content_result = decryptContent(header, cek_result.cek, cipherText, iv, authTag, aad, log); } if (!content_result.error) log.info('In jwe.decrypt: successfully decrypted id_token'); return callback(content_result.error, content_result.content); }; exports.createBuffer = createBuffer;