diff --git a/oauth3.issuer.mock.js b/oauth3.issuer.mock.js index 8208476..1358cd4 100644 --- a/oauth3.issuer.mock.js +++ b/oauth3.issuer.mock.js @@ -24,16 +24,109 @@ }; OAUTH3.crypto = {}; - OAUTH3.crypto._generateKey = function () { - return window.crypto.subtle.generateKey({name: 'ECDSA', namedCurve: 'P-256'}, true, ['sign', 'verify']) - .catch(function (err) { - console.error('failed to generate ECDSA key', err); - return OAUTH3.PromiseA.reject(err); + OAUTH3.crypto.fingerprintJWK = 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)); + } + + var jwkStr = '{' + keys.map(function (name) { return name+':'+jwk[name]; }).join(',') + '}'; + return window.crypto.subtle.digest({name: 'SHA-256'}, OAUTH3.utils.binaryStringToBuffer(jwkStr)) + .then(OAUTH3.utils.bufferToBinaryString).then(OAUTH3._base64.btoa); + }; + + OAUTH3.crypto._createKey = function (ppid) { + var ecdsaPromise, kekPromise; + var salt = window.crypto.getRandomValues(new Uint8Array(16)); + + ecdsaPromise = window.crypto.subtle.generateKey({name: 'ECDSA', namedCurve: 'P-256'}, true, ['sign', 'verify']) + .then(function (key) { return window.crypto.subtle.exportKey('jwk', key.privateKey); }) + .then(function (jwk) { + return OAUTH3.crypto.fingerprintJWK(jwk).then(function (kid) { + delete jwk.ext; + delete jwk.key_ops; + jwk.alg = 'ES256'; + jwk.kid = kid; + return OAUTH3.PromiseA.resolve(jwk); + }); + }); + + kekPromise = window.crypto.subtle.importKey('raw', OAUTH3.utils.binaryStringToBuffer(ppid), {name: 'PBKDF2'}, false, ['deriveKey']) + .then(function (key) { + var opts = {name: 'PBKDF2', salt: salt, iterations: 8192, hash: {name: 'SHA-256'}}; + return window.crypto.subtle.deriveKey(opts, key, {name: 'AES-GCM', length: 256}, false, ['encrypt']); + }); + + return OAUTH3.PromiseA.all([ecdsaPromise, kekPromise]).then(function (keys) { + var jwkBuf = OAUTH3.utils.binaryStringToBuffer(JSON.stringify(keys[0])); + var iv = window.crypto.getRandomValues(new Uint8Array(12)); + return window.crypto.subtle.encrypt({name: 'AES-GCM', iv: iv}, keys[1], jwkBuf).then(function (encrypted) { + return { + kid: keys[0].kid + , key: OAUTH3._base64.btoa(OAUTH3.utils.bufferToBinaryString(encrypted)) + , salt: OAUTH3._base64.btoa(OAUTH3.utils.bufferToBinaryString(salt)) + , iv: OAUTH3._base64.btoa(OAUTH3.utils.bufferToBinaryString(iv)) + }; + }); + }); + }; + + OAUTH3.crypto._decryptKey = function (ppid, storedObj) { + var salt = OAUTH3.utils.binaryStringToBuffer(OAUTH3._base64.atob(storedObj.salt)); + var encJwk = OAUTH3.utils.binaryStringToBuffer(OAUTH3._base64.atob(storedObj.key)); + var iv = OAUTH3.utils.binaryStringToBuffer(OAUTH3._base64.atob(storedObj.iv)); + + return window.crypto.subtle.importKey('raw', OAUTH3.utils.binaryStringToBuffer(ppid), {name: 'PBKDF2'}, false, ['deriveKey']) + .then(function (key) { + var opts = {name: 'PBKDF2', salt: salt, iterations: 8192, hash: {name: 'SHA-256'}}; + return window.crypto.subtle.deriveKey(opts, key, {name: 'AES-GCM', length: 256}, false, ['decrypt']); + }) + .then(function (key) { + return window.crypto.subtle.decrypt({name: 'AES-GCM', iv: iv}, key, encJwk); + }) + .then(OAUTH3.utils.bufferToBinaryString) + .then(JSON.parse) + .then(function (jwk) { + return window.crypto.subtle.importKey('jwk', jwk, {name: 'ECDSA', namedCurve: jwk.crv}, false, ['sign']); + }); + }; + + OAUTH3.crypto._getKey = function (ppid) { + return window.crypto.subtle.digest({name: 'SHA-256'}, OAUTH3.utils.binaryStringToBuffer(ppid)) + .then(function (hash) { + var name = 'kek-' + OAUTH3._base64.btoa(OAUTH3.utils.bufferToBinaryString(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 OAUTH3.resolve(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._generateKey().then(function (key) { + return OAUTH3.crypto._getKey('some PPID').then(function (key) { var header = {type: 'jwt', alg: 'ES256'}; var input = [ OAUTH3._base64.encodeUrlSafe(JSON.stringify(header, null)) @@ -42,7 +135,7 @@ return window.crypto.subtle.sign( {name: 'ECDSA', hash: {name: 'SHA-256'}} - , key.privateKey + , key , OAUTH3.utils.binaryStringToBuffer(input) ).then(function (signature) { var base64Sig = OAUTH3._base64.encodeUrlSafe(OAUTH3.utils.bufferToBinaryString(signature));