;(function (exports) { 'use strict'; var OAUTH3 = exports.OAUTH3 = exports.OAUTH3 || require('./oauth3.core.js').OAUTH3; OAUTH3.crypto = {}; try { OAUTH3.crypto.core = require('./oauth3.node.crypto'); } catch (error) { OAUTH3.crypto.core = {}; OAUTH3.crypto.core.ready = false; var finishBeforeReady = []; var deferedCalls = []; // We don't currently have a fallback method for this function, so we assign // it directly to the core object instead of the webCrypto object. OAUTH3.crypto.core.randomBytes = function (size) { var buf = OAUTH3._browser.window.crypto.getRandomValues(new Uint8Array(size)); return OAUTH3.PromiseA.resolve(buf); }; var webCrypto = {}; var deferCryptoCall = function(name) { return function() { var args = arguments; return new OAUTH3.PromiseA(function(resolve, reject) { deferedCalls.push(function(){ try { webCrypto[name].apply(webCrypto, args) .then(function(result){ resolve(result); }); } catch(e) { reject(e); } }); }); }; }; OAUTH3.crypto.core.sha256 = deferCryptoCall("sha256"); webCrypto.sha256 = function (buf) { return OAUTH3._browser.window.crypto.subtle.digest({name: 'SHA-256'}, buf); }; OAUTH3.crypto.core.pbkdf2 = deferCryptoCall("pbkdf2"); webCrypto.pbkdf2 = function (password, salt) { return OAUTH3._browser.window.crypto.subtle.importKey('raw', OAUTH3._binStr.binStrToBuffer(password), {name: 'PBKDF2'}, false, ['deriveKey']) .then(function (key) { var opts = {name: 'PBKDF2', salt: salt, iterations: 8192, hash: {name: 'SHA-256'}}; return OAUTH3._browser.window.crypto.subtle.deriveKey(opts, key, {name: 'AES-GCM', length: 128}, true, ['encrypt', 'decrypt']); }) .then(function (key) { return OAUTH3._browser.window.crypto.subtle.exportKey('raw', key); }); }; OAUTH3.crypto.core.encrypt = deferCryptoCall("encrypt"); webCrypto.encrypt = function (rawKey, iv, data) { return OAUTH3._browser.window.crypto.subtle.importKey('raw', rawKey, {name: 'AES-GCM'}, false, ['encrypt']) .then(function (key) { return OAUTH3._browser.window.crypto.subtle.encrypt({name: 'AES-GCM', iv: iv}, key, data); }); }; OAUTH3.crypto.core.decrypt = deferCryptoCall("decrypt"); webCrypto.decrypt = function (rawKey, iv, data) { return OAUTH3._browser.window.crypto.subtle.importKey('raw', rawKey, {name: 'AES-GCM'}, false, ['decrypt']) .then(function (key) { return OAUTH3._browser.window.crypto.subtle.decrypt({name: 'AES-GCM', iv: iv}, key, data); }); }; OAUTH3.crypto.core.genEcdsaKeyPair = deferCryptoCall("genEcdsaKeyPair"); webCrypto.genEcdsaKeyPair = function () { return OAUTH3._browser.window.crypto.subtle.generateKey({name: 'ECDSA', namedCurve: 'P-256'}, true, ['sign', 'verify']) .then(function (keyPair) { return OAUTH3.PromiseA.all([ OAUTH3._browser.window.crypto.subtle.exportKey('jwk', keyPair.privateKey) , OAUTH3._browser.window.crypto.subtle.exportKey('jwk', keyPair.publicKey) ]); }).then(function (jwkPair) { return { privateKey: jwkPair[0], publicKey: jwkPair[1] }; }); }; OAUTH3.crypto.core.sign = deferCryptoCall("sign"); webCrypto.sign = function (jwk, msg) { return OAUTH3._browser.window.crypto.subtle.importKey('jwk', jwk, {name: 'ECDSA', namedCurve: jwk.crv}, false, ['sign']) .then(function (key) { return OAUTH3._browser.window.crypto.subtle.sign({name: 'ECDSA', hash: {name: 'SHA-256'}}, key, msg); }) .then(function (sig) { return new Uint8Array(sig); }); }; OAUTH3.crypto.core.verify = deferCryptoCall("verify"); webCrypto.verify = function (jwk, msg, signature) { // If the JWK has properties that should only exist on the private key or is missing // "verify" in the key_ops, importing in as a public key won't work. if (jwk.hasOwnProperty('d') || jwk.hasOwnProperty('key_ops')) { jwk = JSON.parse(JSON.stringify(jwk)); delete jwk.d; delete jwk.key_ops; } return OAUTH3._browser.window.crypto.subtle.importKey('jwk', jwk, {name: 'ECDSA', namedCurve: jwk.crv}, false, ['verify']) .then(function (key) { return OAUTH3._browser.window.crypto.subtle.verify({name: 'ECDSA', hash: {name: 'SHA-256'}}, key, signature, msg); }); }; function checkWebCrypto() { /* global OAUTH3_crypto_fallback */ var loadFallback = function() { var prom; loadFallback = function () { return prom; }; prom = new OAUTH3.PromiseA(function (resolve) { var body = document.getElementsByTagName('body')[0]; var script = document.createElement('script'); script.type = 'text/javascript'; script.onload = resolve; script.onreadystatechange = function () { if (this.readyState === 'complete' || this.readyState === 'loaded') { resolve(); } }; script.src = '/assets/oauth3.org/oauth3.crypto.fallback.js'; body.appendChild(script); }); return prom; }; function checkException(name, func) { return OAUTH3.PromiseA.resolve().then(func) .then(function () { OAUTH3.crypto.core[name] = webCrypto[name]; }, function (err) { console.warn('error with WebCrypto', name, '- using fallback', err); return loadFallback().then(function () { OAUTH3.crypto.core[name] = OAUTH3_crypto_fallback[name]; }); }); } function checkResult(name, expected, func) { finishBeforeReady.push(checkException(name, function () { return func() .then(function (result) { if (typeof expected === typeof result) { return result; } return OAUTH3._base64.bufferToUrlSafe(result); }) .then(function (result) { if (result !== expected) { throw new Error("result ("+result+") doesn't match expectation ("+expected+")"); } }); })); } var zeroBuf = new Uint8Array([0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]); var dataBuf = OAUTH3._base64.urlSafeToBuffer('1234567890abcdefghijklmn'); var keyBuf = OAUTH3._base64.urlSafeToBuffer('l_Aeoqk6ePjwjCYrlHrgrg'); var encBuf = OAUTH3._base64.urlSafeToBuffer('Ji_gEtcNElUONSR4Mf9S75davXjh_6-oQN9AgO5UF8rERw'); checkResult('sha256', 'BwMveUm2V1axuERvUoxM4dScgNl9yKhER9a6p80GXj4', function () { return webCrypto.sha256(dataBuf); }); checkResult('pbkdf2', OAUTH3._base64.bufferToUrlSafe(keyBuf), function () { return webCrypto.pbkdf2('password', zeroBuf); }); checkResult('encrypt', OAUTH3._base64.bufferToUrlSafe(encBuf), function () { return webCrypto.encrypt(keyBuf, zeroBuf.slice(0, 12), dataBuf); }); checkResult('decrypt', OAUTH3._base64.bufferToUrlSafe(dataBuf), function () { return webCrypto.decrypt(keyBuf, zeroBuf.slice(0, 12), encBuf); }); var jwk = { kty: "EC" , crv: "P-256" , d: "ChXx7ea5YtEltCufA8CVb0lQv3glcCfcSpEgdedgIP0" , x: "Akt5ZDbytcKS5UQMURvGb_UIMS4qFctDwrX8bX22ato" , y: "cV7nhpWNT1FeRIbdold4jLtgsEpZBFcNy3p2E5mqvto" }; var sig = OAUTH3._base64.urlSafeToBuffer('nc3F8qeP8OXpfqPD9tTcFQg0Wfp37RTAppLPIKE1ZupR_8Aba64hNExwd1dOk802OFQxaECPDZCkKe7WA9RXAg'); checkResult('verify', true, function() { return webCrypto.verify(jwk, dataBuf, sig); }); // The results of these functions are less predictable, so we can't check their return value. finishBeforeReady.push(checkException('genEcdsaKeyPair', function () { return webCrypto.genEcdsaKeyPair(); })); finishBeforeReady.push(checkException('sign', function () { return webCrypto.sign(jwk, dataBuf); })); OAUTH3.PromiseA.all(finishBeforeReady) .then(function(results) { OAUTH3.crypto.core.ready = true; deferedCalls.forEach(function(request) { request(); }); }); } checkWebCrypto(); } OAUTH3.crypto.thumbprintJwk = function (jwk) { var keys; if (jwk.kty === 'EC') { keys = ['crv', 'x', 'y']; } else if (jwk.kty === 'RSA') { keys = ['e', 'n']; } else if (jwk.kty === 'oct') { keys = ['k']; } else { return OAUTH3.PromiseA.reject(new Error('invalid JWK key type ' + jwk.kty)); } keys.push('kty'); keys.sort(); var missing = keys.filter(function (name) { return !jwk.hasOwnProperty(name); }); if (missing.length > 0) { return OAUTH3.PromiseA.reject(new Error('JWK of type '+jwk.kty+' missing fields ' + missing)); } // I'm not actually 100% sure this behavior is guaranteed, but when we use an array as the // replacer argument the keys are always in the order they appeared in the array. var jwkStr = JSON.stringify(jwk, keys); return OAUTH3.crypto.core.sha256(OAUTH3._binStr.binStrToBuffer(jwkStr)) .then(OAUTH3._base64.bufferToUrlSafe); }; OAUTH3.crypto.createKeyPair = function () { // TODO: maybe support other types of key pairs, not just ECDSA P-256 return OAUTH3.crypto.core.genEcdsaKeyPair().then(function (keyPair) { return OAUTH3.crypto.thumbprintJwk(keyPair.publicKey).then(function (kid) { keyPair.privateKey.alg = keyPair.publicKey.alg = 'ES256'; keyPair.privateKey.kid = keyPair.publicKey.kid = kid; return keyPair; }); }); }; OAUTH3.crypto.encryptKeyPair = function (keyPair, password) { var saltProm = OAUTH3.crypto.core.randomBytes(16); var kekProm = saltProm.then(function (salt) { return OAUTH3.crypto.core.pbkdf2(password, salt); }); return OAUTH3.PromiseA.all([ kekProm , saltProm , OAUTH3.crypto.core.randomBytes(12) , ]).then(function (results) { var kek = results[0]; var salt = results[1]; var ecdsaIv = results[2]; var privKeyBuf = OAUTH3._binStr.binStrToBuffer(JSON.stringify(keyPair.privateKey)); return OAUTH3.crypto.core.encrypt(kek, ecdsaIv, privKeyBuf).then(function (encrypted) { return { publicKey: keyPair.publicKey , privateKey: OAUTH3._base64.bufferToUrlSafe(encrypted) , salt: OAUTH3._base64.bufferToUrlSafe(salt) , ecdsaIv: OAUTH3._base64.bufferToUrlSafe(ecdsaIv) , }; }); }); }; OAUTH3.crypto.decryptKeyPair = function (storedObj, password) { var salt = OAUTH3._base64.urlSafeToBuffer(storedObj.salt); var encJwk = OAUTH3._base64.urlSafeToBuffer(storedObj.privateKey); var iv = OAUTH3._base64.urlSafeToBuffer(storedObj.ecdsaIv); return OAUTH3.crypto.core.pbkdf2(password, salt) .then(function (key) { return OAUTH3.crypto.core.decrypt(key, iv, encJwk); }) .then(OAUTH3._binStr.bufferToBinStr) .then(JSON.parse) .then(function (privateKey) { return { privateKey: privateKey , publicKey: storedObj.publicKey , }; }); }; }('undefined' !== typeof exports ? exports : window));