296 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			296 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| ;(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 = {};
 | |
| 
 | |
|     // 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 = {};
 | |
|     webCrypto.sha256 = function (buf) {
 | |
|       return OAUTH3._browser.window.crypto.subtle.digest({name: 'SHA-256'}, buf);
 | |
|     };
 | |
| 
 | |
|     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);
 | |
|         });
 | |
|     };
 | |
| 
 | |
|     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);
 | |
|         });
 | |
|     };
 | |
|     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);
 | |
|         });
 | |
|     };
 | |
| 
 | |
|     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] };
 | |
|         });
 | |
|     };
 | |
| 
 | |
|     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);
 | |
|         });
 | |
|     };
 | |
|     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() {
 | |
|       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/org.oauth3/oauth3.crypto.fallback.js';
 | |
|           body.appendChild(script);
 | |
|         });
 | |
|         return prom;
 | |
|       };
 | |
|       function checkException(name, func) {
 | |
|         new OAUTH3.PromiseA(function (resolve) { resolve(func()); })
 | |
|           .then(function () {
 | |
|             OAUTH3.crypto.core[name] = webCrypto[name];
 | |
|           })
 | |
|           .catch(function (err) {
 | |
|             console.warn('error with WebCrypto', name, '- using fallback', err);
 | |
|             loadFallback().then(function () {
 | |
|               OAUTH3.crypto.core[name] = OAUTH3_crypto_fallback[name];
 | |
|             });
 | |
|           });
 | |
|       }
 | |
|       function checkResult(name, expected, func) {
 | |
|         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.
 | |
|       checkException('genEcdsaKeyPair', function () {
 | |
|         return webCrypto.genEcdsaKeyPair();
 | |
|       });
 | |
|       checkException('sign', function () {
 | |
|         return webCrypto.sign(jwk, dataBuf);
 | |
|       });
 | |
|     }
 | |
|     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._createKey = function (ppid) {
 | |
|     var saltProm = OAUTH3.crypto.core.randomBytes(16);
 | |
|     var kekProm = saltProm.then(function (salt) {
 | |
|       return OAUTH3.crypto.core.pbkdf2(ppid, salt);
 | |
|     });
 | |
| 
 | |
|     var ecdsaProm = 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;
 | |
|       });
 | |
|     });
 | |
| 
 | |
|     return OAUTH3.PromiseA.all([
 | |
|       kekProm
 | |
|     , ecdsaProm
 | |
|     , saltProm
 | |
|     , OAUTH3.crypto.core.randomBytes(16)
 | |
|     , OAUTH3.crypto.core.randomBytes(12)
 | |
|     , OAUTH3.crypto.core.randomBytes(12)
 | |
|     ]).then(function (results) {
 | |
|       var kek        = results[0];
 | |
|       var keyPair    = results[1];
 | |
|       var salt       = results[2];
 | |
|       var userSecret = results[3];
 | |
|       var ecdsaIv    = results[4];
 | |
|       var secretIv   = results[5];
 | |
| 
 | |
|       return OAUTH3.PromiseA.all([
 | |
|         OAUTH3.crypto.core.encrypt(kek, ecdsaIv, OAUTH3._binStr.binStrToBuffer(JSON.stringify(keyPair.privateKey)))
 | |
|       , OAUTH3.crypto.core.encrypt(kek, secretIv, userSecret)
 | |
|       ])
 | |
|       .then(function (encrypted) {
 | |
|         return {
 | |
|           publicKey:  keyPair.publicKey
 | |
|         , privateKey: OAUTH3._base64.bufferToUrlSafe(encrypted[0])
 | |
|         , userSecret: OAUTH3._base64.bufferToUrlSafe(encrypted[1])
 | |
|         , salt:       OAUTH3._base64.bufferToUrlSafe(salt)
 | |
|         , ecdsaIv:    OAUTH3._base64.bufferToUrlSafe(ecdsaIv)
 | |
|         , secretIv:   OAUTH3._base64.bufferToUrlSafe(secretIv)
 | |
|         };
 | |
|       });
 | |
|     });
 | |
|   };
 | |
| 
 | |
|   OAUTH3.crypto._decryptKey = function (ppid, storedObj) {
 | |
|     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(ppid, salt)
 | |
|       .then(function (key) {
 | |
|         return OAUTH3.crypto.core.decrypt(key, iv, encJwk);
 | |
|       })
 | |
|       .then(OAUTH3._binStr.bufferToBinStr)
 | |
|       .then(JSON.parse);
 | |
|   };
 | |
| 
 | |
|   OAUTH3.crypto._getKey = function (ppid) {
 | |
|     return OAUTH3.crypto.core.sha256(OAUTH3._binStr.binStrToBuffer(ppid))
 | |
|     .then(function (hash) {
 | |
|       var name = 'kek-' + OAUTH3._base64.bufferToUrlSafe(hash);
 | |
|       var promise;
 | |
| 
 | |
|       if (window.localStorage.getItem(name) === null) {
 | |
|         promise = OAUTH3.crypto._createKey(ppid).then(function (key) {
 | |
|           window.localStorage.setItem(name, JSON.stringify(key));
 | |
|           return key;
 | |
|         });
 | |
|       } else {
 | |
|         promise = OAUTH3.PromiseA.resolve(JSON.parse(window.localStorage.getItem(name)));
 | |
|       }
 | |
| 
 | |
|       return promise.then(function (storedObj) {
 | |
|         return OAUTH3.crypto._decryptKey(ppid, storedObj);
 | |
|       });
 | |
|     });
 | |
|   };
 | |
| 
 | |
|   OAUTH3.crypto._signPayload = function (payload) {
 | |
|     return OAUTH3.crypto._getKey('some PPID').then(function (key) {
 | |
|       var header = {type: 'JWT', alg: key.alg, kid: key.kid};
 | |
|       var input = [
 | |
|         OAUTH3._base64.encodeUrlSafe(JSON.stringify(header, null))
 | |
|       , OAUTH3._base64.encodeUrlSafe(JSON.stringify(payload, null))
 | |
|       ].join('.');
 | |
| 
 | |
|       return OAUTH3.crypto.core.sign(key, OAUTH3._binStr.binStrToBuffer(input))
 | |
|         .then(OAUTH3._base64.bufferToUrlSafe)
 | |
|         .then(function (signature) {
 | |
|           return input + '.' + signature;
 | |
|         });
 | |
|     });
 | |
|   };
 | |
| 
 | |
| }('undefined' !== typeof exports ? exports : window));
 |