From 338f62439ab6011b4abce3ae2c27509ec083546b Mon Sep 17 00:00:00 2001 From: tigerbot Date: Mon, 6 Mar 2017 15:13:29 -0700 Subject: [PATCH 01/15] implemented ability to sign tokens with random key generated every time --- oauth3.issuer.mock.js | 89 ++++++++++++++++++++++++++----------------- 1 file changed, 55 insertions(+), 34 deletions(-) diff --git a/oauth3.issuer.mock.js b/oauth3.issuer.mock.js index 5f4966a..8208476 100644 --- a/oauth3.issuer.mock.js +++ b/oauth3.issuer.mock.js @@ -3,28 +3,52 @@ var OAUTH3 = exports.OAUTH3 = exports.OAUTH3 || require('./oauth3.core.js').OAUTH3; - OAUTH3._base64.btoa = function (b64) { - // http://stackoverflow.com/questions/9677985/uncaught-typeerror-illegal-invocation-in-chrome - return (exports.btoa || require('btoa'))(b64); + OAUTH3.utils.bufferToBinaryString = function (buf) { + return Array.prototype.map.call(new Uint8Array(buf), function(ch) { + return String.fromCharCode(ch); + }).join(''); }; - OAUTH3._base64.encodeUrlSafe = function (b64) { - // Base64 to URL-safe Base64 - b64 = b64.replace(/\+/g, '-').replace(/\//g, '_'); - b64 = b64.replace(/=+/g, ''); - return OAUTH3._base64.btoa(b64); + OAUTH3.utils.binaryStringToBuffer = function (str) { + var buf; + + if ('undefined' !== typeof Uint8Array) { + buf = new Uint8Array(str.length); + } else { + buf = []; + } + + Array.prototype.forEach.call(str, function (ch, ind) { + buf[ind] = ch.charCodeAt(0); + }); + return buf; }; - OAUTH3.jwt.encode = function (parts) { - parts.header = parts.header || { alg: 'none', typ: 'jwt' }; - parts.signature = parts.signature || ''; + 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); + }); + }; - var result = [ - OAUTH3._base64.encodeUrlSafe(JSON.stringify(parts.header, null)) - , OAUTH3._base64.encodeUrlSafe(JSON.stringify(parts.payload, null)) - , parts.signature // should already be url-safe base64 - ].join('.'); + OAUTH3.crypto._signPayload = function (payload) { + return OAUTH3.crypto._generateKey().then(function (key) { + var header = {type: 'jwt', alg: 'ES256'}; + var input = [ + OAUTH3._base64.encodeUrlSafe(JSON.stringify(header, null)) + , OAUTH3._base64.encodeUrlSafe(JSON.stringify(payload, null)) + ].join('.'); - return result; + return window.crypto.subtle.sign( + {name: 'ECDSA', hash: {name: 'SHA-256'}} + , key.privateKey + , OAUTH3.utils.binaryStringToBuffer(input) + ).then(function (signature) { + var base64Sig = OAUTH3._base64.encodeUrlSafe(OAUTH3.utils.bufferToBinaryString(signature)); + return OAUTH3.PromiseA.resolve(input + '.' + base64Sig); + }); + }); }; OAUTH3.authn.resourceOwnerPassword = OAUTH3.authz.resourceOwnerPassword = function (directive, opts) { @@ -82,24 +106,21 @@ }; OAUTH3._mockToken = function (providerUri, opts) { - var accessToken = OAUTH3.jwt.encode({ - header: { alg: 'none' } - , payload: { exp: Math.round(Date.now() / 1000) + 900, sub: 'fakeUserId', scp: opts.scope } - , signature: "fakeSig" + var payload = { exp: Math.round(Date.now() / 1000) + 900, sub: 'fakeUserId', scp: opts.scope }; + return OAUTH3.crypto._signPayload(payload).then(function (accessToken) { + return OAUTH3.hooks.session.refresh( + opts.session || { + provider_uri: providerUri + , client_id: opts.client_id + , client_uri: opts.client_uri || opts.clientUri + } + , { access_token: accessToken + , refresh_token: accessToken + , expires_in: "900" + , scope: opts.scope + } + ); }); - - return OAUTH3.hooks.session.refresh( - opts.session || { - provider_uri: providerUri - , client_id: opts.client_id - , client_uri: opts.client_uri || opts.clientUri - } - , { access_token: accessToken - , refresh_token: accessToken - , expires_in: "900" - , scope: opts.scope - } - ); }; }('undefined' !== typeof exports ? exports : window)); From 9ec66a10b22d1492fb9d06bbaddcd0e1cfd16416 Mon Sep 17 00:00:00 2001 From: tigerbot Date: Mon, 6 Mar 2017 18:39:46 -0700 Subject: [PATCH 02/15] implemented storage of the encrypted private ECDSA JWK --- oauth3.issuer.mock.js | 107 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 100 insertions(+), 7 deletions(-) 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)); From 68cecb7c966b3369ea1b2d23f04154d7a4cf3f26 Mon Sep 17 00:00:00 2001 From: tigerbot Date: Tue, 7 Mar 2017 14:42:02 -0700 Subject: [PATCH 03/15] added unencrypted public key to the localStorage --- oauth3.issuer.mock.js | 74 ++++++++++++++++++++++++------------------- 1 file changed, 42 insertions(+), 32 deletions(-) diff --git a/oauth3.issuer.mock.js b/oauth3.issuer.mock.js index 1358cd4..7add79e 100644 --- a/oauth3.issuer.mock.js +++ b/oauth3.issuer.mock.js @@ -49,36 +49,45 @@ }; OAUTH3.crypto._createKey = function (ppid) { - var ecdsaPromise, kekPromise; + var kekPromise, ecdsaPromise; 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) { + ecdsaPromise = window.crypto.subtle.generateKey({name: 'ECDSA', namedCurve: 'P-256'}, true, ['sign', 'verify']) + .then(function (keyPair) { + function tweakJWK(jwk) { + return OAUTH3.crypto.fingerprintJWK(jwk).then(function (kid) { + delete jwk.ext; + jwk.alg = 'ES256'; + jwk.kid = kid; + return jwk; + }); + } + return OAUTH3.PromiseA.all([ + window.crypto.subtle.exportKey('jwk', keyPair.privateKey).then(tweakJWK) + , window.crypto.subtle.exportKey('jwk', keyPair.publicKey).then(tweakJWK) + ]).then(function (jwkPair) { 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)) + privateKey: jwkPair[0] + , publicKey: jwkPair[1] + }; + }); + }); + + return OAUTH3.PromiseA.all([kekPromise, ecdsaPromise]).then(function (keys) { + var jwkBuf = OAUTH3.utils.binaryStringToBuffer(JSON.stringify(keys[1].privateKey)); + var iv = window.crypto.getRandomValues(new Uint8Array(12)); + return window.crypto.subtle.encrypt({name: 'AES-GCM', iv: iv}, keys[0], jwkBuf).then(function (encrypted) { + return { + publicKey: keys[1].publicKey + , privateKey: OAUTH3._base64.btoa(OAUTH3.utils.bufferToBinaryString(encrypted)) + , salt: OAUTH3._base64.btoa(OAUTH3.utils.bufferToBinaryString(salt)) + , iv: OAUTH3._base64.btoa(OAUTH3.utils.bufferToBinaryString(iv)) }; }); }); @@ -86,7 +95,7 @@ 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 encJwk = OAUTH3.utils.binaryStringToBuffer(OAUTH3._base64.atob(storedObj.privateKey)); var iv = OAUTH3.utils.binaryStringToBuffer(OAUTH3._base64.atob(storedObj.iv)); return window.crypto.subtle.importKey('raw', OAUTH3.utils.binaryStringToBuffer(ppid), {name: 'PBKDF2'}, false, ['deriveKey']) @@ -100,7 +109,12 @@ .then(OAUTH3.utils.bufferToBinaryString) .then(JSON.parse) .then(function (jwk) { - return window.crypto.subtle.importKey('jwk', jwk, {name: 'ECDSA', namedCurve: jwk.crv}, false, ['sign']); + return window.crypto.subtle.importKey('jwk', jwk, {name: 'ECDSA', namedCurve: jwk.crv}, false, ['sign']) + .then(function (key) { + key.kid = jwk.kid; + key.alg = jwk.alg; + return key; + }); }); }; @@ -113,7 +127,7 @@ 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); + return key; }); } else { promise = OAUTH3.PromiseA.resolve(JSON.parse(window.localStorage.getItem(name))); @@ -127,19 +141,15 @@ OAUTH3.crypto._signPayload = function (payload) { return OAUTH3.crypto._getKey('some PPID').then(function (key) { - var header = {type: 'jwt', alg: 'ES256'}; + 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 window.crypto.subtle.sign( - {name: 'ECDSA', hash: {name: 'SHA-256'}} - , key - , OAUTH3.utils.binaryStringToBuffer(input) - ).then(function (signature) { - var base64Sig = OAUTH3._base64.encodeUrlSafe(OAUTH3.utils.bufferToBinaryString(signature)); - return OAUTH3.PromiseA.resolve(input + '.' + base64Sig); + return window.crypto.subtle.sign({name: 'ECDSA', hash: {name: 'SHA-256'}}, key, OAUTH3.utils.binaryStringToBuffer(input)) + .then(function (signature) { + return input + '.' + OAUTH3._base64.encodeUrlSafe(OAUTH3.utils.bufferToBinaryString(signature)); }); }); }; From 29967cde19067df62dbe98e881ebb560740a2d39 Mon Sep 17 00:00:00 2001 From: tigerbot Date: Tue, 7 Mar 2017 14:54:08 -0700 Subject: [PATCH 04/15] fixed some spelling errors in the README --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index ec7cb33..1a40080 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ The world's smallest, fastest, and most secure OAuth3 (and OAuth2) JavaScript im Instead of bloating your webapp and ruining the mobile experience, you can use a single, small javascript file for all OAuth3 providers -(and almost all OAuth2 providers) with a seemless experience. +(and almost all OAuth2 providers) with a seamless experience. Also, instead of complicated (or worse - insecure) CLI and Desktop login methods, you can easily integrate an OAuth3 flow (or broker) into any node.js app (i.e. Electron, Node-Webkit) @@ -74,7 +74,7 @@ function onClickLogin() { console.info('Authentication was Successful:'); console.log(session); - // You can use the PPID (or preferrably a hash of it) as the login for your app + // You can use the PPID (or preferably a hash of it) as the login for your app // (it securely functions as both username and password which is known only by your app) // If you use a hash of it as an ID, you can also use the PPID itself as a decryption key // @@ -168,7 +168,7 @@ pushd /path/to/your/web/app # clone the project as assets/org.oauth3 mkdir -p assets git clone git@git.daplie.com:Daplie/oauth3.js.git assets/org.oauth3 -pushd assests/org.oauth3 +pushd assets/org.oauth3 git checkout v1 popd @@ -232,7 +232,7 @@ function onClickLogin() { console.info('Authentication was Successful:'); console.log(session); - // You can use the PPID (or preferrably a hash of it) as the login for your app + // You can use the PPID (or preferably a hash of it) as the login for your app // (it securely functions as both username and password which is known only by your app) // If you use a hash of it as an ID, you can also use the PPID itself as a decryption key // @@ -448,7 +448,7 @@ As a general rule I don't like rules that sometimes apply and sometimes don't, so I may need to rethink this. However, there are cases where including the protocol can be very ugly and confusing and we definitely need to allow relative paths. -A potential work-around would be to assume all paths are relative (elimitate #4 instead) +A potential work-around would be to assume all paths are relative (eliminate #4 instead) and have the path always key off of the base URL - if oauth3 directives are to be found at https://example.com/username/.well-known/oauth3/directives.json then /api/whatever would refer to https://example.com/username/api/whatever. From db9d8ff313544e33913fd0435732d978e3cae2f6 Mon Sep 17 00:00:00 2001 From: tigerbot Date: Tue, 7 Mar 2017 14:55:27 -0700 Subject: [PATCH 05/15] fixed a couple bugs found in the workflow examples --- oauth3.core.js | 1 + oauth3.issuer.mock.js | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/oauth3.core.js b/oauth3.core.js index dd3fea9..f36482b 100644 --- a/oauth3.core.js +++ b/oauth3.core.js @@ -708,6 +708,7 @@ return OAUTH3.PromiseA.reject(OAUTH3.error.parse(directives.issuer /*providerUri*/, params)); } + OAUTH3.hooks.session._cache = {}; return params; }); } diff --git a/oauth3.issuer.mock.js b/oauth3.issuer.mock.js index 7add79e..d59db8a 100644 --- a/oauth3.issuer.mock.js +++ b/oauth3.issuer.mock.js @@ -177,12 +177,12 @@ }; OAUTH3.authz.scopes = function () { - return { + return OAUTH3.PromiseA.resolve({ pending: ['oauth3_authn'] // not yet accepted , granted: [] // all granted, ever , requested: ['oauth3_authn'] // all requested, now , accepted: [] // granted (ever) and requested (now) - }; + }); }; OAUTH3.authz.grants = function (providerUri, opts) { if ('POST' === opts.method) { From bde3c2ca333c6b582c6275466d4f66cb6905a2a7 Mon Sep 17 00:00:00 2001 From: tigerbot Date: Mon, 13 Mar 2017 12:33:09 -0600 Subject: [PATCH 06/15] add an encrypted user secret key to the stored object --- oauth3.issuer.mock.js | 34 ++++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/oauth3.issuer.mock.js b/oauth3.issuer.mock.js index d59db8a..ea72fa7 100644 --- a/oauth3.issuer.mock.js +++ b/oauth3.issuer.mock.js @@ -49,13 +49,13 @@ }; OAUTH3.crypto._createKey = function (ppid) { - var kekPromise, ecdsaPromise; + var kekPromise, ecdsaPromise, secretPromise; var salt = window.crypto.getRandomValues(new Uint8Array(16)); 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 window.crypto.subtle.deriveKey(opts, key, {name: 'AES-GCM', length: 128}, false, ['encrypt']); }); ecdsaPromise = window.crypto.subtle.generateKey({name: 'ECDSA', namedCurve: 'P-256'}, true, ['sign', 'verify']) @@ -79,15 +79,29 @@ }); }); - return OAUTH3.PromiseA.all([kekPromise, ecdsaPromise]).then(function (keys) { - var jwkBuf = OAUTH3.utils.binaryStringToBuffer(JSON.stringify(keys[1].privateKey)); - var iv = window.crypto.getRandomValues(new Uint8Array(12)); - return window.crypto.subtle.encrypt({name: 'AES-GCM', iv: iv}, keys[0], jwkBuf).then(function (encrypted) { + secretPromise = window.crypto.subtle.generateKey({name: 'AES-GCM', length: 128}, true, ['encrypt', 'decrypt']) + .then(function (key) { + return window.crypto.subtle.exportKey('jwk', key); + }); + + return OAUTH3.PromiseA.all([kekPromise, ecdsaPromise, secretPromise]).then(function (keys) { + var ecdsaJwk = OAUTH3.utils.binaryStringToBuffer(JSON.stringify(keys[1].privateKey)); + var secretJwk = OAUTH3.utils.binaryStringToBuffer(JSON.stringify(keys[2])); + var ecdsaIv = window.crypto.getRandomValues(new Uint8Array(12)); + var secretIv = window.crypto.getRandomValues(new Uint8Array(12)); + + return OAUTH3.PromiseA.all([ + window.crypto.subtle.encrypt({name: 'AES-GCM', iv: ecdsaIv}, keys[0], ecdsaJwk) + , window.crypto.subtle.encrypt({name: 'AES-GCM', iv: secretIv}, keys[0], secretJwk) + ]) + .then(function (encrypted) { return { publicKey: keys[1].publicKey - , privateKey: OAUTH3._base64.btoa(OAUTH3.utils.bufferToBinaryString(encrypted)) + , privateKey: OAUTH3._base64.btoa(OAUTH3.utils.bufferToBinaryString(encrypted[0])) + , userSecret: OAUTH3._base64.btoa(OAUTH3.utils.bufferToBinaryString(encrypted[1])) , salt: OAUTH3._base64.btoa(OAUTH3.utils.bufferToBinaryString(salt)) - , iv: OAUTH3._base64.btoa(OAUTH3.utils.bufferToBinaryString(iv)) + , ecdsaIv: OAUTH3._base64.btoa(OAUTH3.utils.bufferToBinaryString(ecdsaIv)) + , secretIv: OAUTH3._base64.btoa(OAUTH3.utils.bufferToBinaryString(secretIv)) }; }); }); @@ -96,12 +110,12 @@ 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.privateKey)); - var iv = OAUTH3.utils.binaryStringToBuffer(OAUTH3._base64.atob(storedObj.iv)); + var iv = OAUTH3.utils.binaryStringToBuffer(OAUTH3._base64.atob(storedObj.ecdsaIv)); 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']); + return window.crypto.subtle.deriveKey(opts, key, {name: 'AES-GCM', length: 128}, false, ['decrypt']); }) .then(function (key) { return window.crypto.subtle.decrypt({name: 'AES-GCM', iv: iv}, key, encJwk); From 4b63e38c1f7d8f8aba8c0714a7b8cef84de38bbe Mon Sep 17 00:00:00 2001 From: tigerbot Date: Mon, 13 Mar 2017 12:35:25 -0600 Subject: [PATCH 07/15] changed all stored base64 strings to url safe --- oauth3.issuer.mock.js | 49 ++++++++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/oauth3.issuer.mock.js b/oauth3.issuer.mock.js index ea72fa7..d64122f 100644 --- a/oauth3.issuer.mock.js +++ b/oauth3.issuer.mock.js @@ -3,12 +3,12 @@ var OAUTH3 = exports.OAUTH3 = exports.OAUTH3 || require('./oauth3.core.js').OAUTH3; - OAUTH3.utils.bufferToBinaryString = function (buf) { + OAUTH3.utils.bufferToBinStr = function (buf) { return Array.prototype.map.call(new Uint8Array(buf), function(ch) { return String.fromCharCode(ch); }).join(''); }; - OAUTH3.utils.binaryStringToBuffer = function (str) { + OAUTH3.utils.binStrToBuffer = function (str) { var buf; if ('undefined' !== typeof Uint8Array) { @@ -23,6 +23,13 @@ return buf; }; + OAUTH3._base64.urlSafeToBuffer = function (str) { + return OAUTH3.utils.binStrToBuffer(OAUTH3._base64.decodeUrlSafe(str)); + }; + OAUTH3._base64.bufferToUrlSafe = function (buf) { + return OAUTH3._base64.encodeUrlSafe(OAUTH3.utils.bufferToBinStr(buf)); + }; + OAUTH3.crypto = {}; OAUTH3.crypto.fingerprintJWK = function (jwk) { var keys; @@ -44,15 +51,15 @@ } 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); + return window.crypto.subtle.digest({name: 'SHA-256'}, OAUTH3.utils.binStrToBuffer(jwkStr)) + .then(OAUTH3._base64.bufferToUrlSafe); }; OAUTH3.crypto._createKey = function (ppid) { var kekPromise, ecdsaPromise, secretPromise; var salt = window.crypto.getRandomValues(new Uint8Array(16)); - kekPromise = window.crypto.subtle.importKey('raw', OAUTH3.utils.binaryStringToBuffer(ppid), {name: 'PBKDF2'}, false, ['deriveKey']) + kekPromise = window.crypto.subtle.importKey('raw', OAUTH3.utils.binStrToBuffer(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: 128}, false, ['encrypt']); @@ -85,8 +92,8 @@ }); return OAUTH3.PromiseA.all([kekPromise, ecdsaPromise, secretPromise]).then(function (keys) { - var ecdsaJwk = OAUTH3.utils.binaryStringToBuffer(JSON.stringify(keys[1].privateKey)); - var secretJwk = OAUTH3.utils.binaryStringToBuffer(JSON.stringify(keys[2])); + var ecdsaJwk = OAUTH3.utils.binStrToBuffer(JSON.stringify(keys[1].privateKey)); + var secretJwk = OAUTH3.utils.binStrToBuffer(JSON.stringify(keys[2])); var ecdsaIv = window.crypto.getRandomValues(new Uint8Array(12)); var secretIv = window.crypto.getRandomValues(new Uint8Array(12)); @@ -97,22 +104,22 @@ .then(function (encrypted) { return { publicKey: keys[1].publicKey - , privateKey: OAUTH3._base64.btoa(OAUTH3.utils.bufferToBinaryString(encrypted[0])) - , userSecret: OAUTH3._base64.btoa(OAUTH3.utils.bufferToBinaryString(encrypted[1])) - , salt: OAUTH3._base64.btoa(OAUTH3.utils.bufferToBinaryString(salt)) - , ecdsaIv: OAUTH3._base64.btoa(OAUTH3.utils.bufferToBinaryString(ecdsaIv)) - , secretIv: OAUTH3._base64.btoa(OAUTH3.utils.bufferToBinaryString(secretIv)) + , 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.utils.binaryStringToBuffer(OAUTH3._base64.atob(storedObj.salt)); - var encJwk = OAUTH3.utils.binaryStringToBuffer(OAUTH3._base64.atob(storedObj.privateKey)); - var iv = OAUTH3.utils.binaryStringToBuffer(OAUTH3._base64.atob(storedObj.ecdsaIv)); + var salt = OAUTH3._base64.urlSafeToBuffer(storedObj.salt); + var encJwk = OAUTH3._base64.urlSafeToBuffer(storedObj.privateKey); + var iv = OAUTH3._base64.urlSafeToBuffer(storedObj.ecdsaIv); - return window.crypto.subtle.importKey('raw', OAUTH3.utils.binaryStringToBuffer(ppid), {name: 'PBKDF2'}, false, ['deriveKey']) + return window.crypto.subtle.importKey('raw', OAUTH3.utils.binStrToBuffer(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: 128}, false, ['decrypt']); @@ -120,7 +127,7 @@ .then(function (key) { return window.crypto.subtle.decrypt({name: 'AES-GCM', iv: iv}, key, encJwk); }) - .then(OAUTH3.utils.bufferToBinaryString) + .then(OAUTH3.utils.bufferToBinStr) .then(JSON.parse) .then(function (jwk) { return window.crypto.subtle.importKey('jwk', jwk, {name: 'ECDSA', namedCurve: jwk.crv}, false, ['sign']) @@ -133,9 +140,9 @@ }; OAUTH3.crypto._getKey = function (ppid) { - return window.crypto.subtle.digest({name: 'SHA-256'}, OAUTH3.utils.binaryStringToBuffer(ppid)) + return window.crypto.subtle.digest({name: 'SHA-256'}, OAUTH3.utils.binStrToBuffer(ppid)) .then(function (hash) { - var name = 'kek-' + OAUTH3._base64.btoa(OAUTH3.utils.bufferToBinaryString(hash)); + var name = 'kek-' + OAUTH3._base64.bufferToUrlSafe(hash); var promise; if (window.localStorage.getItem(name) === null) { @@ -161,9 +168,9 @@ , OAUTH3._base64.encodeUrlSafe(JSON.stringify(payload, null)) ].join('.'); - return window.crypto.subtle.sign({name: 'ECDSA', hash: {name: 'SHA-256'}}, key, OAUTH3.utils.binaryStringToBuffer(input)) + return window.crypto.subtle.sign({name: 'ECDSA', hash: {name: 'SHA-256'}}, key, OAUTH3.utils.binStrToBuffer(input)) .then(function (signature) { - return input + '.' + OAUTH3._base64.encodeUrlSafe(OAUTH3.utils.bufferToBinaryString(signature)); + return input + '.' + OAUTH3._base64.bufferToUrlSafe(signature); }); }); }; From 6ec723ec1fea148578c9ecc84c46462c7afbf1ff Mon Sep 17 00:00:00 2001 From: tigerbot Date: Mon, 13 Mar 2017 13:37:06 -0600 Subject: [PATCH 08/15] implemented verification of JWT signatures --- oauth3.core.js | 52 +++++++++++++++++++++++++++++++++++++------ oauth3.issuer.mock.js | 43 +++++++---------------------------- 2 files changed, 53 insertions(+), 42 deletions(-) diff --git a/oauth3.core.js b/oauth3.core.js index f36482b..97d6bb9 100644 --- a/oauth3.core.js +++ b/oauth3.core.js @@ -14,6 +14,27 @@ return err; } } + , _binStr: { + bufferToBinStr: function (buf) { + return Array.prototype.map.call(new Uint8Array(buf), function(ch) { + return String.fromCharCode(ch); + }).join(''); + } + , binStrToBuffer: function (str) { + var buf; + + if ('undefined' !== typeof Uint8Array) { + buf = new Uint8Array(str.length); + } else { + buf = []; + } + + Array.prototype.forEach.call(str, function (ch, ind) { + buf[ind] = ch.charCodeAt(0); + }); + return buf; + } + } , _base64: { atob: function (base64) { // atob must be called from the global context @@ -43,6 +64,12 @@ b64 = b64.replace(/=+/g, ''); return b64; } + , urlSafeToBuffer: function (str) { + return OAUTH3._binStr.binStrToBuffer(OAUTH3._base64.decodeUrlSafe(str)); + } + , bufferToUrlSafe: function (buf) { + return OAUTH3._base64.encodeUrlSafe(OAUTH3._binStr.bufferToBinStr(buf)); + } } , uri: { normalize: function (uri) { @@ -181,15 +208,26 @@ // { header: {}, payload: {}, signature: '' } var parts = str.split(/\./g); var jsons = parts.slice(0, 2).map(function (urlsafe64) { - var b64 = OAUTH3._base64.decodeUrlSafe(urlsafe64); - return b64; + return JSON.parse(OAUTH3._base64.decodeUrlSafe(urlsafe64)); }); - return { - header: JSON.parse(jsons[0]) - , payload: JSON.parse(jsons[1]) - , signature: parts[2] // should remain url-safe base64 - }; + return { header: jsons[0], payload: jsons[1] }; + } + , verify: function (str, pubKey) { + var parts = str.split(/\./g); + var data = OAUTH3._binStr.binStrToBuffer(parts.slice(0, 2).join('.')); + var signature = OAUTH3._base64.urlSafeToBuffer(parts[2]); + + var keyPromise; + if (pubKey instanceof OAUTH3._browser.window.CryptoKey) { + keyPromise = OAUTH3.PromiseA.resolve(pubKey); + } else { + keyPromise = OAUTH3._browser.window.crypto.subtle.importKey('jwk', pubKey, {name: 'ECDSA', namedCurve: pubKey.crv}, false, ['verify']); + } + + return keyPromise.then(function (key) { + return OAUTH3._browser.window.crypto.subtle.verify({name: 'ECDSA', hash: {name: 'SHA-256'}}, key, signature, data); + }); } , freshness: function (tokenMeta, staletime, _now) { staletime = staletime || (15 * 60); diff --git a/oauth3.issuer.mock.js b/oauth3.issuer.mock.js index d64122f..d51f4b6 100644 --- a/oauth3.issuer.mock.js +++ b/oauth3.issuer.mock.js @@ -3,33 +3,6 @@ var OAUTH3 = exports.OAUTH3 = exports.OAUTH3 || require('./oauth3.core.js').OAUTH3; - OAUTH3.utils.bufferToBinStr = function (buf) { - return Array.prototype.map.call(new Uint8Array(buf), function(ch) { - return String.fromCharCode(ch); - }).join(''); - }; - OAUTH3.utils.binStrToBuffer = function (str) { - var buf; - - if ('undefined' !== typeof Uint8Array) { - buf = new Uint8Array(str.length); - } else { - buf = []; - } - - Array.prototype.forEach.call(str, function (ch, ind) { - buf[ind] = ch.charCodeAt(0); - }); - return buf; - }; - - OAUTH3._base64.urlSafeToBuffer = function (str) { - return OAUTH3.utils.binStrToBuffer(OAUTH3._base64.decodeUrlSafe(str)); - }; - OAUTH3._base64.bufferToUrlSafe = function (buf) { - return OAUTH3._base64.encodeUrlSafe(OAUTH3.utils.bufferToBinStr(buf)); - }; - OAUTH3.crypto = {}; OAUTH3.crypto.fingerprintJWK = function (jwk) { var keys; @@ -51,7 +24,7 @@ } var jwkStr = '{' + keys.map(function (name) { return name+':'+jwk[name]; }).join(',') + '}'; - return window.crypto.subtle.digest({name: 'SHA-256'}, OAUTH3.utils.binStrToBuffer(jwkStr)) + return window.crypto.subtle.digest({name: 'SHA-256'}, OAUTH3._binStr.binStrToBuffer(jwkStr)) .then(OAUTH3._base64.bufferToUrlSafe); }; @@ -59,7 +32,7 @@ var kekPromise, ecdsaPromise, secretPromise; var salt = window.crypto.getRandomValues(new Uint8Array(16)); - kekPromise = window.crypto.subtle.importKey('raw', OAUTH3.utils.binStrToBuffer(ppid), {name: 'PBKDF2'}, false, ['deriveKey']) + kekPromise = window.crypto.subtle.importKey('raw', OAUTH3._binStr.binStrToBuffer(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: 128}, false, ['encrypt']); @@ -92,8 +65,8 @@ }); return OAUTH3.PromiseA.all([kekPromise, ecdsaPromise, secretPromise]).then(function (keys) { - var ecdsaJwk = OAUTH3.utils.binStrToBuffer(JSON.stringify(keys[1].privateKey)); - var secretJwk = OAUTH3.utils.binStrToBuffer(JSON.stringify(keys[2])); + var ecdsaJwk = OAUTH3._binStr.binStrToBuffer(JSON.stringify(keys[1].privateKey)); + var secretJwk = OAUTH3._binStr.binStrToBuffer(JSON.stringify(keys[2])); var ecdsaIv = window.crypto.getRandomValues(new Uint8Array(12)); var secretIv = window.crypto.getRandomValues(new Uint8Array(12)); @@ -119,7 +92,7 @@ var encJwk = OAUTH3._base64.urlSafeToBuffer(storedObj.privateKey); var iv = OAUTH3._base64.urlSafeToBuffer(storedObj.ecdsaIv); - return window.crypto.subtle.importKey('raw', OAUTH3.utils.binStrToBuffer(ppid), {name: 'PBKDF2'}, false, ['deriveKey']) + return window.crypto.subtle.importKey('raw', OAUTH3._binStr.binStrToBuffer(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: 128}, false, ['decrypt']); @@ -127,7 +100,7 @@ .then(function (key) { return window.crypto.subtle.decrypt({name: 'AES-GCM', iv: iv}, key, encJwk); }) - .then(OAUTH3.utils.bufferToBinStr) + .then(OAUTH3._binStr.bufferToBinStr) .then(JSON.parse) .then(function (jwk) { return window.crypto.subtle.importKey('jwk', jwk, {name: 'ECDSA', namedCurve: jwk.crv}, false, ['sign']) @@ -140,7 +113,7 @@ }; OAUTH3.crypto._getKey = function (ppid) { - return window.crypto.subtle.digest({name: 'SHA-256'}, OAUTH3.utils.binStrToBuffer(ppid)) + return window.crypto.subtle.digest({name: 'SHA-256'}, OAUTH3._binStr.binStrToBuffer(ppid)) .then(function (hash) { var name = 'kek-' + OAUTH3._base64.bufferToUrlSafe(hash); var promise; @@ -168,7 +141,7 @@ , OAUTH3._base64.encodeUrlSafe(JSON.stringify(payload, null)) ].join('.'); - return window.crypto.subtle.sign({name: 'ECDSA', hash: {name: 'SHA-256'}}, key, OAUTH3.utils.binStrToBuffer(input)) + return window.crypto.subtle.sign({name: 'ECDSA', hash: {name: 'SHA-256'}}, key, OAUTH3._binStr.binStrToBuffer(input)) .then(function (signature) { return input + '.' + OAUTH3._base64.bufferToUrlSafe(signature); }); From e8e9b961a42a57bef95715d0c7b39d7f34ff362f Mon Sep 17 00:00:00 2001 From: tigerbot Date: Tue, 14 Mar 2017 14:33:11 -0600 Subject: [PATCH 09/15] implemented first of the fallback crypto functions --- .gitignore | 1 + browserify/crypto-index.js | 32 ++++++++++++++++++++++++++++++++ gulpfile.js | 21 +++++++++++++++++++++ package.json | 23 +++++++++++++++++++++++ 4 files changed, 77 insertions(+) create mode 100644 .gitignore create mode 100644 browserify/crypto-index.js create mode 100644 gulpfile.js create mode 100644 package.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c2658d7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/browserify/crypto-index.js b/browserify/crypto-index.js new file mode 100644 index 0000000..9a4f4c9 --- /dev/null +++ b/browserify/crypto-index.js @@ -0,0 +1,32 @@ +;(function () { +'use strict'; + + var createHash = require('create-hash'); + var pbkdf2 = require('pbkdf2'); + var aes = require('browserify-aes'); + + exports.sha256 = function (buf) { + var hash = createHash('sha256'); + hash.update(buf); + hash.end(); + return Promise.resolve(hash.read()); + }; + + exports.encrypt = function (data, password, salt, iv) { + // Derived AES key is 128 bit, and the function takes a size in bytes. + var aesKey = pbkdf2.pbkdf2Sync(password, Buffer(salt), 8192, 16, 'sha256'); + var cipher = aes.createCipheriv('aes-128-gcm', aesKey, Buffer(iv)); + var result = Buffer.concat([cipher.update(Buffer(data)), cipher.final(), cipher.getAuthTag()]); + return Promise.resolve(result); + }; + + exports.decrypt = function (data, password, salt, iv) { + var aesKey = pbkdf2.pbkdf2Sync(password, Buffer(salt), 8192, 16, 'sha256'); + var decipher = aes.createDecipheriv('aes-128-gcm', aesKey, Buffer(iv)); + + decipher.setAuthTag(Buffer(data.slice(-16))); + var result = Buffer.concat([decipher.update(Buffer(data.slice(0, -16))), decipher.final()]); + return Promise.resolve(result); + }; + +}()); diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 0000000..5cdc695 --- /dev/null +++ b/gulpfile.js @@ -0,0 +1,21 @@ +;(function () { + 'use strict'; + + var gulp = require('gulp'); + var browserify = require('browserify'); + var source = require('vinyl-source-stream'); + var streamify = require('gulp-streamify'); + var uglify = require('gulp-uglify'); + var rename = require('gulp-rename'); + + gulp.task('default', function () { + return browserify('./browserify/crypto-index.js', {standalone: 'OAUTH3_crypto'}).bundle() + .pipe(source('browserify/crypto-index.js')) + .pipe(rename('oauth3.crypto.js')) + .pipe(gulp.dest('./')) + .pipe(streamify(uglify())) + .pipe(rename('oauth3.crypto.min.js')) + .pipe(gulp.dest('./')) + ; + }); +}()); diff --git a/package.json b/package.json new file mode 100644 index 0000000..6ae6ae3 --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "oauth3", + "respository": { + "type": "git", + "url": "git+ssh://git@git.daplie.com:Daplie/oauth3.js.git" + }, + "scripts": { + "install": "./node_modules/.bin/gulp" + }, + "devDependencies": { + "atob": "^2.0.3", + "browserify": "^14.1.0", + "browserify-aes": "^1.0.6", + "btoa": "^1.1.2", + "gulp": "^3.9.1", + "gulp-cli": "^1.2.2", + "gulp-rename": "^1.2.2", + "gulp-streamify": "^1.0.2", + "gulp-uglify": "^2.1.0", + "pbkdf2": "^3.0.9", + "vinyl-source-stream": "^1.1.0" + } +} From 740c973afc94ffc0c225e0a0bf76b06562323fc1 Mon Sep 17 00:00:00 2001 From: tigerbot Date: Fri, 17 Mar 2017 17:14:47 -0600 Subject: [PATCH 10/15] added ECDSA features to crypto fallback --- browserify/crypto-index.js | 70 ++++++++++++++++++++++++++++++-------- gulpfile.js | 4 +++ package.json | 2 ++ 3 files changed, 62 insertions(+), 14 deletions(-) diff --git a/browserify/crypto-index.js b/browserify/crypto-index.js index 9a4f4c9..652c2ee 100644 --- a/browserify/crypto-index.js +++ b/browserify/crypto-index.js @@ -4,29 +4,71 @@ var createHash = require('create-hash'); var pbkdf2 = require('pbkdf2'); var aes = require('browserify-aes'); + var ec = require('elliptic').ec('p256'); - exports.sha256 = function (buf) { - var hash = createHash('sha256'); - hash.update(buf); - hash.end(); - return Promise.resolve(hash.read()); - }; + function sha256(buf) { + return createHash('sha256').update(buf).digest(); + } - exports.encrypt = function (data, password, salt, iv) { + function encrypt(data, password, salt, iv) { // Derived AES key is 128 bit, and the function takes a size in bytes. var aesKey = pbkdf2.pbkdf2Sync(password, Buffer(salt), 8192, 16, 'sha256'); var cipher = aes.createCipheriv('aes-128-gcm', aesKey, Buffer(iv)); - var result = Buffer.concat([cipher.update(Buffer(data)), cipher.final(), cipher.getAuthTag()]); - return Promise.resolve(result); - }; - exports.decrypt = function (data, password, salt, iv) { + return Buffer.concat([cipher.update(Buffer(data)), cipher.final(), cipher.getAuthTag()]); + } + + function decrypt(data, password, salt, iv) { var aesKey = pbkdf2.pbkdf2Sync(password, Buffer(salt), 8192, 16, 'sha256'); var decipher = aes.createDecipheriv('aes-128-gcm', aesKey, Buffer(iv)); decipher.setAuthTag(Buffer(data.slice(-16))); - var result = Buffer.concat([decipher.update(Buffer(data.slice(0, -16))), decipher.final()]); - return Promise.resolve(result); - }; + return Buffer.concat([decipher.update(Buffer(data.slice(0, -16))), decipher.final()]); + } + function convertBN(bn) { + if (bn.red) { + bn = bn.fromRed(); + } + var b64 = bn.toArrayLike(Buffer).toString('base64'); + return b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=*$/, ''); + } + function genEcdsaKeyPair() { + var key = ec.genKeyPair(); + var pubJwk = { + key_ops: ['verify'] + , kty: 'EC' + , crv: 'P-256' + , x: convertBN(key.getPublic().x) + , y: convertBN(key.getPublic().y) + }; + + var privJwk = JSON.parse(JSON.stringify(pubJwk)); + privJwk.key_ops = ['sign']; + privJwk.d = convertBN(key.getPrivate()); + + return {privateKey: privJwk, publicKey: pubJwk}; + } + + function sign(jwk, msg) { + var key = ec.keyFromPrivate(Buffer(jwk.d, 'base64')); + var sig = key.sign(sha256(msg)); + return Buffer.concat([Buffer(sig.r, 'hex'), Buffer(sig.s, 'hex')]); + } + + function verify(jwk, msg, signature) { + var key = ec.keyFromPublic({x: Buffer(jwk.x, 'base64'), y: Buffer(jwk.y, 'base64')}); + var sig = { + r: Buffer(signature.slice(0, signature.length/2)) + , s: Buffer(signature.slice(signature.length/2)) + }; + return key.verify(sha256(msg), sig); + } + + exports.sha256 = function () { return Promise.resolve(sha256.apply(this, arguments)); }; + exports.encrypt = function () { return Promise.resolve(encrypt.apply(this, arguments)); }; + exports.decrypt = function () { return Promise.resolve(decrypt.apply(this, arguments)); }; + exports.sign = function () { return Promise.resolve(sign.apply(this, arguments)); }; + exports.verify = function () { return Promise.resolve(verify.apply(this, arguments)); }; + exports.genEcdsaKeyPair = function () { return Promise.resolve(genEcdsaKeyPair.apply(this, arguments)); }; }()); diff --git a/gulpfile.js b/gulpfile.js index 5cdc695..f5a8966 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -18,4 +18,8 @@ .pipe(gulp.dest('./')) ; }); + + gulp.task('watch', function () { + gulp.watch('browserify/*.js', [ 'default' ]); + }); }()); diff --git a/package.json b/package.json index 6ae6ae3..192f7b7 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,8 @@ "browserify": "^14.1.0", "browserify-aes": "^1.0.6", "btoa": "^1.1.2", + "create-hash": "^1.1.2", + "elliptic": "^6.4.0", "gulp": "^3.9.1", "gulp-cli": "^1.2.2", "gulp-rename": "^1.2.2", From 01580dd6b321b7e7b85137a581e2b736df2ac128 Mon Sep 17 00:00:00 2001 From: tigerbot Date: Mon, 20 Mar 2017 16:11:14 -0600 Subject: [PATCH 11/15] implemented dynamic loading of fallback crypto functions --- .../{crypto-index.js => crypto.fallback.js} | 55 +++- gulpfile.js | 8 +- oauth3.crypto.js | 305 ++++++++++++++++++ oauth3.issuer.mock.js | 145 --------- package.json | 7 +- 5 files changed, 350 insertions(+), 170 deletions(-) rename browserify/{crypto-index.js => crypto.fallback.js} (52%) create mode 100644 oauth3.crypto.js diff --git a/browserify/crypto-index.js b/browserify/crypto.fallback.js similarity index 52% rename from browserify/crypto-index.js rename to browserify/crypto.fallback.js index 652c2ee..311085d 100644 --- a/browserify/crypto-index.js +++ b/browserify/crypto.fallback.js @@ -10,23 +10,25 @@ return createHash('sha256').update(buf).digest(); } - function encrypt(data, password, salt, iv) { + function runPbkdf2(password, salt) { // Derived AES key is 128 bit, and the function takes a size in bytes. - var aesKey = pbkdf2.pbkdf2Sync(password, Buffer(salt), 8192, 16, 'sha256'); - var cipher = aes.createCipheriv('aes-128-gcm', aesKey, Buffer(iv)); + return pbkdf2.pbkdf2Sync(password, Buffer(salt), 8192, 16, 'sha256'); + } + + function encrypt(key, data, iv) { + var cipher = aes.createCipheriv('aes-128-gcm', Buffer(key), Buffer(iv)); return Buffer.concat([cipher.update(Buffer(data)), cipher.final(), cipher.getAuthTag()]); } - function decrypt(data, password, salt, iv) { - var aesKey = pbkdf2.pbkdf2Sync(password, Buffer(salt), 8192, 16, 'sha256'); - var decipher = aes.createDecipheriv('aes-128-gcm', aesKey, Buffer(iv)); + function decrypt(key, data, iv) { + var decipher = aes.createDecipheriv('aes-128-gcm', Buffer(key), Buffer(iv)); decipher.setAuthTag(Buffer(data.slice(-16))); return Buffer.concat([decipher.update(Buffer(data.slice(0, -16))), decipher.final()]); } - function convertBN(bn) { + function bnToB64(bn) { if (bn.red) { bn = bn.fromRed(); } @@ -39,21 +41,31 @@ key_ops: ['verify'] , kty: 'EC' , crv: 'P-256' - , x: convertBN(key.getPublic().x) - , y: convertBN(key.getPublic().y) + , x: bnToB64(key.getPublic().x) + , y: bnToB64(key.getPublic().y) }; var privJwk = JSON.parse(JSON.stringify(pubJwk)); privJwk.key_ops = ['sign']; - privJwk.d = convertBN(key.getPrivate()); + privJwk.d = bnToB64(key.getPrivate()); return {privateKey: privJwk, publicKey: pubJwk}; } + function bnToBuffer(bn, size) { + var buf = bn.toArrayLike(Buffer); + if (!size || buf.length === size) { + return buf; + } + if (buf > size) { + throw new Error("EC signature number bigger than expected"); + } + return Buffer.concat([Buffer(size-buf.length).fill(0), buf]); + } function sign(jwk, msg) { var key = ec.keyFromPrivate(Buffer(jwk.d, 'base64')); var sig = key.sign(sha256(msg)); - return Buffer.concat([Buffer(sig.r, 'hex'), Buffer(sig.s, 'hex')]); + return Buffer.concat([bnToBuffer(sig.r, 32), bnToBuffer(sig.s, 32)]); } function verify(jwk, msg, signature) { @@ -65,10 +77,19 @@ return key.verify(sha256(msg), sig); } - exports.sha256 = function () { return Promise.resolve(sha256.apply(this, arguments)); }; - exports.encrypt = function () { return Promise.resolve(encrypt.apply(this, arguments)); }; - exports.decrypt = function () { return Promise.resolve(decrypt.apply(this, arguments)); }; - exports.sign = function () { return Promise.resolve(sign.apply(this, arguments)); }; - exports.verify = function () { return Promise.resolve(verify.apply(this, arguments)); }; - exports.genEcdsaKeyPair = function () { return Promise.resolve(genEcdsaKeyPair.apply(this, arguments)); }; + function promiseWrap(func) { + return function() { + var args = arguments; + return new Promise(function (resolve) { + resolve(func.apply(null, args)); + }); + }; + } + exports.sha256 = promiseWrap(sha256); + exports.pbkdf2 = promiseWrap(runPbkdf2); + exports.encrypt = promiseWrap(encrypt); + exports.decrypt = promiseWrap(decrypt); + exports.sign = promiseWrap(sign); + exports.verify = promiseWrap(verify); + exports.genEcdsaKeyPair = promiseWrap(genEcdsaKeyPair); }()); diff --git a/gulpfile.js b/gulpfile.js index f5a8966..44f6e3c 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -9,12 +9,12 @@ var rename = require('gulp-rename'); gulp.task('default', function () { - return browserify('./browserify/crypto-index.js', {standalone: 'OAUTH3_crypto'}).bundle() - .pipe(source('browserify/crypto-index.js')) - .pipe(rename('oauth3.crypto.js')) + return browserify('./browserify/crypto.fallback.js', {standalone: 'OAUTH3_crypto_fallback'}).bundle() + .pipe(source('browserify/crypto.fallback.js')) + .pipe(rename('oauth3.crypto.fallback.js')) .pipe(gulp.dest('./')) .pipe(streamify(uglify())) - .pipe(rename('oauth3.crypto.min.js')) + .pipe(rename('oauth3.crypto.fallback.min.js')) .pipe(gulp.dest('./')) ; }); diff --git a/oauth3.crypto.js b/oauth3.crypto.js new file mode 100644 index 0000000..abbaee7 --- /dev/null +++ b/oauth3.crypto.js @@ -0,0 +1,305 @@ +;(function (exports) { +'use strict'; + + var OAUTH3 = exports.OAUTH3 = exports.OAUTH3 || require('./oauth3.core.js').OAUTH3; + + 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; + }; + + var webCrypto = {}; + webCrypto.sha256 = function (buf) { + return crypto.subtle.digest({name: 'SHA-256'}, buf); + }; + + webCrypto.pbkdf2 = function (password, salt) { + return 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 crypto.subtle.deriveKey(opts, key, {name: 'AES-GCM', length: 128}, true, ['encrypt', 'decrypt']); + }) + .then(function (key) { + return crypto.subtle.exportKey('raw', key); + }); + }; + + webCrypto.encrypt = function (rawKey, data, iv) { + return crypto.subtle.importKey('raw', rawKey, {name: 'AES-GCM'}, false, ['encrypt']) + .then(function (key) { + return crypto.subtle.encrypt({name: 'AES-GCM', iv: iv}, key, data); + }); + }; + webCrypto.decrypt = function (rawKey, data, iv) { + return crypto.subtle.importKey('raw', rawKey, {name: 'AES-GCM'}, false, ['decrypt']) + .then(function (key) { + return crypto.subtle.decrypt({name: 'AES-GCM', iv: iv}, key, data); + }); + }; + + webCrypto.genEcdsaKeyPair = function () { + return crypto.subtle.generateKey({name: 'ECDSA', namedCurve: 'P-256'}, true, ['sign', 'verify']) + .then(function (keyPair) { + return OAUTH3.PromiseA.all([ + crypto.subtle.exportKey('jwk', keyPair.privateKey) + , crypto.subtle.exportKey('jwk', keyPair.publicKey) + ]); + }).then(function (jwkPair) { + return { privateKey: jwkPair[0], publicKey: jwkPair[1] }; + }); + }; + + webCrypto.sign = function (jwk, msg) { + return crypto.subtle.importKey('jwk', jwk, {name: 'ECDSA', namedCurve: jwk.crv}, false, ['sign']) + .then(function (key) { + return 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 crypto.subtle.importKey('jwk', jwk, {name: 'ECDSA', namedCurve: jwk.crv}, false, ['verify']) + .then(function (key) { + return crypto.subtle.verify({name: 'ECDSA', hash: {name: 'SHA-256'}}, key, signature, msg); + }); + }; + + OAUTH3.crypto = {}; + OAUTH3.crypto.core = {}; + function checkWebCrypto() { + 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, dataBuf, zeroBuf.slice(0, 12)); + }); + checkResult('decrypt', OAUTH3._base64.bufferToUrlSafe(dataBuf), function () { + return webCrypto.decrypt(keyBuf, encBuf, zeroBuf.slice(0, 12)); + }); + + 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.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._binStr.binStrToBuffer(jwkStr)) + .then(OAUTH3._base64.bufferToUrlSafe); + }; + + OAUTH3.crypto._createKey = function (ppid) { + var kekPromise, ecdsaPromise, secretPromise; + var salt = window.crypto.getRandomValues(new Uint8Array(16)); + + kekPromise = window.crypto.subtle.importKey('raw', OAUTH3._binStr.binStrToBuffer(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: 128}, false, ['encrypt']); + }); + + ecdsaPromise = window.crypto.subtle.generateKey({name: 'ECDSA', namedCurve: 'P-256'}, true, ['sign', 'verify']) + .then(function (keyPair) { + function tweakJWK(jwk) { + return OAUTH3.crypto.fingerprintJWK(jwk).then(function (kid) { + delete jwk.ext; + jwk.alg = 'ES256'; + jwk.kid = kid; + return jwk; + }); + } + return OAUTH3.PromiseA.all([ + window.crypto.subtle.exportKey('jwk', keyPair.privateKey).then(tweakJWK) + , window.crypto.subtle.exportKey('jwk', keyPair.publicKey).then(tweakJWK) + ]).then(function (jwkPair) { + return { + privateKey: jwkPair[0] + , publicKey: jwkPair[1] + }; + }); + }); + + secretPromise = window.crypto.subtle.generateKey({name: 'AES-GCM', length: 128}, true, ['encrypt', 'decrypt']) + .then(function (key) { + return window.crypto.subtle.exportKey('jwk', key); + }); + + return OAUTH3.PromiseA.all([kekPromise, ecdsaPromise, secretPromise]).then(function (keys) { + var ecdsaJwk = OAUTH3._binStr.binStrToBuffer(JSON.stringify(keys[1].privateKey)); + var secretJwk = OAUTH3._binStr.binStrToBuffer(JSON.stringify(keys[2])); + var ecdsaIv = window.crypto.getRandomValues(new Uint8Array(12)); + var secretIv = window.crypto.getRandomValues(new Uint8Array(12)); + + return OAUTH3.PromiseA.all([ + window.crypto.subtle.encrypt({name: 'AES-GCM', iv: ecdsaIv}, keys[0], ecdsaJwk) + , window.crypto.subtle.encrypt({name: 'AES-GCM', iv: secretIv}, keys[0], secretJwk) + ]) + .then(function (encrypted) { + return { + publicKey: keys[1].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 window.crypto.subtle.importKey('raw', OAUTH3._binStr.binStrToBuffer(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: 128}, false, ['decrypt']); + }) + .then(function (key) { + return window.crypto.subtle.decrypt({name: 'AES-GCM', iv: iv}, key, encJwk); + }) + .then(OAUTH3._binStr.bufferToBinStr) + .then(JSON.parse) + .then(function (jwk) { + return window.crypto.subtle.importKey('jwk', jwk, {name: 'ECDSA', namedCurve: jwk.crv}, false, ['sign']) + .then(function (key) { + key.kid = jwk.kid; + key.alg = jwk.alg; + return key; + }); + }); + }; + + OAUTH3.crypto._getKey = function (ppid) { + return window.crypto.subtle.digest({name: 'SHA-256'}, 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 window.crypto.subtle.sign({name: 'ECDSA', hash: {name: 'SHA-256'}}, key, OAUTH3._binStr.binStrToBuffer(input)) + .then(function (signature) { + return input + '.' + OAUTH3._base64.bufferToUrlSafe(signature); + }); + }); + }; + +}('undefined' !== typeof exports ? exports : window)); diff --git a/oauth3.issuer.mock.js b/oauth3.issuer.mock.js index d51f4b6..e054edb 100644 --- a/oauth3.issuer.mock.js +++ b/oauth3.issuer.mock.js @@ -3,151 +3,6 @@ var OAUTH3 = exports.OAUTH3 = exports.OAUTH3 || require('./oauth3.core.js').OAUTH3; - OAUTH3.crypto = {}; - 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._binStr.binStrToBuffer(jwkStr)) - .then(OAUTH3._base64.bufferToUrlSafe); - }; - - OAUTH3.crypto._createKey = function (ppid) { - var kekPromise, ecdsaPromise, secretPromise; - var salt = window.crypto.getRandomValues(new Uint8Array(16)); - - kekPromise = window.crypto.subtle.importKey('raw', OAUTH3._binStr.binStrToBuffer(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: 128}, false, ['encrypt']); - }); - - ecdsaPromise = window.crypto.subtle.generateKey({name: 'ECDSA', namedCurve: 'P-256'}, true, ['sign', 'verify']) - .then(function (keyPair) { - function tweakJWK(jwk) { - return OAUTH3.crypto.fingerprintJWK(jwk).then(function (kid) { - delete jwk.ext; - jwk.alg = 'ES256'; - jwk.kid = kid; - return jwk; - }); - } - return OAUTH3.PromiseA.all([ - window.crypto.subtle.exportKey('jwk', keyPair.privateKey).then(tweakJWK) - , window.crypto.subtle.exportKey('jwk', keyPair.publicKey).then(tweakJWK) - ]).then(function (jwkPair) { - return { - privateKey: jwkPair[0] - , publicKey: jwkPair[1] - }; - }); - }); - - secretPromise = window.crypto.subtle.generateKey({name: 'AES-GCM', length: 128}, true, ['encrypt', 'decrypt']) - .then(function (key) { - return window.crypto.subtle.exportKey('jwk', key); - }); - - return OAUTH3.PromiseA.all([kekPromise, ecdsaPromise, secretPromise]).then(function (keys) { - var ecdsaJwk = OAUTH3._binStr.binStrToBuffer(JSON.stringify(keys[1].privateKey)); - var secretJwk = OAUTH3._binStr.binStrToBuffer(JSON.stringify(keys[2])); - var ecdsaIv = window.crypto.getRandomValues(new Uint8Array(12)); - var secretIv = window.crypto.getRandomValues(new Uint8Array(12)); - - return OAUTH3.PromiseA.all([ - window.crypto.subtle.encrypt({name: 'AES-GCM', iv: ecdsaIv}, keys[0], ecdsaJwk) - , window.crypto.subtle.encrypt({name: 'AES-GCM', iv: secretIv}, keys[0], secretJwk) - ]) - .then(function (encrypted) { - return { - publicKey: keys[1].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 window.crypto.subtle.importKey('raw', OAUTH3._binStr.binStrToBuffer(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: 128}, false, ['decrypt']); - }) - .then(function (key) { - return window.crypto.subtle.decrypt({name: 'AES-GCM', iv: iv}, key, encJwk); - }) - .then(OAUTH3._binStr.bufferToBinStr) - .then(JSON.parse) - .then(function (jwk) { - return window.crypto.subtle.importKey('jwk', jwk, {name: 'ECDSA', namedCurve: jwk.crv}, false, ['sign']) - .then(function (key) { - key.kid = jwk.kid; - key.alg = jwk.alg; - return key; - }); - }); - }; - - OAUTH3.crypto._getKey = function (ppid) { - return window.crypto.subtle.digest({name: 'SHA-256'}, 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 window.crypto.subtle.sign({name: 'ECDSA', hash: {name: 'SHA-256'}}, key, OAUTH3._binStr.binStrToBuffer(input)) - .then(function (signature) { - return input + '.' + OAUTH3._base64.bufferToUrlSafe(signature); - }); - }); - }; - OAUTH3.authn.resourceOwnerPassword = OAUTH3.authz.resourceOwnerPassword = function (directive, opts) { var providerUri = directive.issuer; diff --git a/package.json b/package.json index 192f7b7..cc8c040 100644 --- a/package.json +++ b/package.json @@ -8,18 +8,17 @@ "install": "./node_modules/.bin/gulp" }, "devDependencies": { - "atob": "^2.0.3", - "browserify": "^14.1.0", "browserify-aes": "^1.0.6", - "btoa": "^1.1.2", "create-hash": "^1.1.2", "elliptic": "^6.4.0", + "pbkdf2": "^3.0.9", + + "browserify": "^14.1.0", "gulp": "^3.9.1", "gulp-cli": "^1.2.2", "gulp-rename": "^1.2.2", "gulp-streamify": "^1.0.2", "gulp-uglify": "^2.1.0", - "pbkdf2": "^3.0.9", "vinyl-source-stream": "^1.1.0" } } From 695df45a1d7738944c440e03a49eae8c27d27d40 Mon Sep 17 00:00:00 2001 From: tigerbot Date: Mon, 20 Mar 2017 17:41:20 -0600 Subject: [PATCH 12/15] changed crypto functions to not directly use WebCrypto --- browserify/crypto.fallback.js | 4 +- oauth3.crypto.js | 91 +++++++++++------------------------ package.json | 2 +- 3 files changed, 32 insertions(+), 65 deletions(-) diff --git a/browserify/crypto.fallback.js b/browserify/crypto.fallback.js index 311085d..4c2c49e 100644 --- a/browserify/crypto.fallback.js +++ b/browserify/crypto.fallback.js @@ -15,13 +15,13 @@ return pbkdf2.pbkdf2Sync(password, Buffer(salt), 8192, 16, 'sha256'); } - function encrypt(key, data, iv) { + function encrypt(key, iv, data) { var cipher = aes.createCipheriv('aes-128-gcm', Buffer(key), Buffer(iv)); return Buffer.concat([cipher.update(Buffer(data)), cipher.final(), cipher.getAuthTag()]); } - function decrypt(key, data, iv) { + function decrypt(key, iv, data) { var decipher = aes.createDecipheriv('aes-128-gcm', Buffer(key), Buffer(iv)); decipher.setAuthTag(Buffer(data.slice(-16))); diff --git a/oauth3.crypto.js b/oauth3.crypto.js index abbaee7..0781545 100644 --- a/oauth3.crypto.js +++ b/oauth3.crypto.js @@ -39,13 +39,13 @@ }); }; - webCrypto.encrypt = function (rawKey, data, iv) { + webCrypto.encrypt = function (rawKey, iv, data) { return crypto.subtle.importKey('raw', rawKey, {name: 'AES-GCM'}, false, ['encrypt']) .then(function (key) { return crypto.subtle.encrypt({name: 'AES-GCM', iv: iv}, key, data); }); }; - webCrypto.decrypt = function (rawKey, data, iv) { + webCrypto.decrypt = function (rawKey, iv, data) { return crypto.subtle.importKey('raw', rawKey, {name: 'AES-GCM'}, false, ['decrypt']) .then(function (key) { return crypto.subtle.decrypt({name: 'AES-GCM', iv: iv}, key, data); @@ -131,10 +131,10 @@ return webCrypto.pbkdf2('password', zeroBuf); }); checkResult('encrypt', OAUTH3._base64.bufferToUrlSafe(encBuf), function () { - return webCrypto.encrypt(keyBuf, dataBuf, zeroBuf.slice(0, 12)); + return webCrypto.encrypt(keyBuf, zeroBuf.slice(0, 12), dataBuf); }); checkResult('decrypt', OAUTH3._base64.bufferToUrlSafe(dataBuf), function () { - return webCrypto.decrypt(keyBuf, encBuf, zeroBuf.slice(0, 12)); + return webCrypto.decrypt(keyBuf, zeroBuf.slice(0, 12), encBuf); }); var jwk = { @@ -158,7 +158,7 @@ } checkWebCrypto(); - OAUTH3.crypto.fingerprintJWK = function (jwk) { + OAUTH3.crypto.thumbprintJwk = function (jwk) { var keys; if (jwk.kty === 'EC') { keys = ['crv', 'x', 'y']; @@ -178,55 +178,33 @@ } var jwkStr = '{' + keys.map(function (name) { return name+':'+jwk[name]; }).join(',') + '}'; - return window.crypto.subtle.digest({name: 'SHA-256'}, OAUTH3._binStr.binStrToBuffer(jwkStr)) - .then(OAUTH3._base64.bufferToUrlSafe); + return OAUTH3.crypto.core.sha256(OAUTH3._binStr.binStrToBuffer(jwkStr)) + .then(OAUTH3._base64.bufferToUrlSafe); }; OAUTH3.crypto._createKey = function (ppid) { - var kekPromise, ecdsaPromise, secretPromise; + var kekPromise, ecdsaPromise; var salt = window.crypto.getRandomValues(new Uint8Array(16)); - kekPromise = window.crypto.subtle.importKey('raw', OAUTH3._binStr.binStrToBuffer(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: 128}, false, ['encrypt']); - }); + kekPromise = OAUTH3.crypto.core.pbkdf2(ppid, salt); - ecdsaPromise = window.crypto.subtle.generateKey({name: 'ECDSA', namedCurve: 'P-256'}, true, ['sign', 'verify']) + ecdsaPromise = OAUTH3.crypto.core.genEcdsaKeyPair() .then(function (keyPair) { - function tweakJWK(jwk) { - return OAUTH3.crypto.fingerprintJWK(jwk).then(function (kid) { - delete jwk.ext; - jwk.alg = 'ES256'; - jwk.kid = kid; - return jwk; - }); - } - return OAUTH3.PromiseA.all([ - window.crypto.subtle.exportKey('jwk', keyPair.privateKey).then(tweakJWK) - , window.crypto.subtle.exportKey('jwk', keyPair.publicKey).then(tweakJWK) - ]).then(function (jwkPair) { - return { - privateKey: jwkPair[0] - , publicKey: jwkPair[1] - }; + 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; }); }); - secretPromise = window.crypto.subtle.generateKey({name: 'AES-GCM', length: 128}, true, ['encrypt', 'decrypt']) - .then(function (key) { - return window.crypto.subtle.exportKey('jwk', key); - }); - - return OAUTH3.PromiseA.all([kekPromise, ecdsaPromise, secretPromise]).then(function (keys) { - var ecdsaJwk = OAUTH3._binStr.binStrToBuffer(JSON.stringify(keys[1].privateKey)); - var secretJwk = OAUTH3._binStr.binStrToBuffer(JSON.stringify(keys[2])); + return OAUTH3.PromiseA.all([kekPromise, ecdsaPromise]).then(function (keys) { var ecdsaIv = window.crypto.getRandomValues(new Uint8Array(12)); var secretIv = window.crypto.getRandomValues(new Uint8Array(12)); + var userSecret = window.crypto.getRandomValues(new Uint8Array(16)); return OAUTH3.PromiseA.all([ - window.crypto.subtle.encrypt({name: 'AES-GCM', iv: ecdsaIv}, keys[0], ecdsaJwk) - , window.crypto.subtle.encrypt({name: 'AES-GCM', iv: secretIv}, keys[0], secretJwk) + OAUTH3.crypto.core.encrypt(keys[0], ecdsaIv, OAUTH3._binStr.binStrToBuffer(JSON.stringify(keys[1].privateKey))) + , OAUTH3.crypto.core.encrypt(keys[0], secretIv, userSecret) ]) .then(function (encrypted) { return { @@ -246,28 +224,16 @@ var encJwk = OAUTH3._base64.urlSafeToBuffer(storedObj.privateKey); var iv = OAUTH3._base64.urlSafeToBuffer(storedObj.ecdsaIv); - return window.crypto.subtle.importKey('raw', OAUTH3._binStr.binStrToBuffer(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: 128}, false, ['decrypt']); - }) - .then(function (key) { - return window.crypto.subtle.decrypt({name: 'AES-GCM', iv: iv}, key, encJwk); - }) - .then(OAUTH3._binStr.bufferToBinStr) - .then(JSON.parse) - .then(function (jwk) { - return window.crypto.subtle.importKey('jwk', jwk, {name: 'ECDSA', namedCurve: jwk.crv}, false, ['sign']) + return OAUTH3.crypto.core.pbkdf2(ppid, salt) .then(function (key) { - key.kid = jwk.kid; - key.alg = jwk.alg; - return key; - }); - }); + return OAUTH3.crypto.core.decrypt(key, iv, encJwk); + }) + .then(OAUTH3._binStr.bufferToBinStr) + .then(JSON.parse); }; OAUTH3.crypto._getKey = function (ppid) { - return window.crypto.subtle.digest({name: 'SHA-256'}, OAUTH3._binStr.binStrToBuffer(ppid)) + return OAUTH3.crypto.core.sha256(OAUTH3._binStr.binStrToBuffer(ppid)) .then(function (hash) { var name = 'kek-' + OAUTH3._base64.bufferToUrlSafe(hash); var promise; @@ -295,10 +261,11 @@ , OAUTH3._base64.encodeUrlSafe(JSON.stringify(payload, null)) ].join('.'); - return window.crypto.subtle.sign({name: 'ECDSA', hash: {name: 'SHA-256'}}, key, OAUTH3._binStr.binStrToBuffer(input)) - .then(function (signature) { - return input + '.' + OAUTH3._base64.bufferToUrlSafe(signature); - }); + return OAUTH3.crypto.core.sign(key, OAUTH3._binStr.binStrToBuffer(input)) + .then(OAUTH3._base64.bufferToUrlSafe) + .then(function (signature) { + return input + '.' + signature; + }); }); }; diff --git a/package.json b/package.json index cc8c040..fa01b59 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "oauth3", "respository": { "type": "git", - "url": "git+ssh://git@git.daplie.com:Daplie/oauth3.js.git" + "url": "git+ssh://git@git.daplie.com:OAuth3/oauth3.js.git" }, "scripts": { "install": "./node_modules/.bin/gulp" From 06411918a7a557359526490687b2608ed3f3c450 Mon Sep 17 00:00:00 2001 From: tigerbot Date: Mon, 20 Mar 2017 18:18:47 -0600 Subject: [PATCH 13/15] changed jwt.verify to not directly use WebCrypto --- oauth3.core.js | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/oauth3.core.js b/oauth3.core.js index 97d6bb9..fd1a421 100644 --- a/oauth3.core.js +++ b/oauth3.core.js @@ -213,21 +213,12 @@ return { header: jsons[0], payload: jsons[1] }; } - , verify: function (str, pubKey) { - var parts = str.split(/\./g); + , verify: function (jwk, token) { + var parts = token.split(/\./g); var data = OAUTH3._binStr.binStrToBuffer(parts.slice(0, 2).join('.')); var signature = OAUTH3._base64.urlSafeToBuffer(parts[2]); - var keyPromise; - if (pubKey instanceof OAUTH3._browser.window.CryptoKey) { - keyPromise = OAUTH3.PromiseA.resolve(pubKey); - } else { - keyPromise = OAUTH3._browser.window.crypto.subtle.importKey('jwk', pubKey, {name: 'ECDSA', namedCurve: pubKey.crv}, false, ['verify']); - } - - return keyPromise.then(function (key) { - return OAUTH3._browser.window.crypto.subtle.verify({name: 'ECDSA', hash: {name: 'SHA-256'}}, key, signature, data); - }); + return OAUTH3.crypto.core.verify(jwk, data, signature); } , freshness: function (tokenMeta, staletime, _now) { staletime = staletime || (15 * 60); From ac89cb7904affee97ba9bd1bf69e10070c4c31fd Mon Sep 17 00:00:00 2001 From: tigerbot Date: Tue, 21 Mar 2017 17:16:40 -0600 Subject: [PATCH 14/15] implemented the core crypto functions for node --- browserify/crypto.fallback.js | 36 ++-- oauth3.crypto.js | 335 ++++++++++++++++++---------------- oauth3.node.crypto.js | 106 +++++++++++ package.json | 4 +- 4 files changed, 306 insertions(+), 175 deletions(-) create mode 100644 oauth3.node.crypto.js diff --git a/browserify/crypto.fallback.js b/browserify/crypto.fallback.js index 4c2c49e..aca2cb8 100644 --- a/browserify/crypto.fallback.js +++ b/browserify/crypto.fallback.js @@ -28,11 +28,20 @@ return Buffer.concat([decipher.update(Buffer(data.slice(0, -16))), decipher.final()]); } - function bnToB64(bn) { - if (bn.red) { - bn = bn.fromRed(); + function bnToBuffer(bn, size) { + var buf = bn.toArrayLike(Buffer); + + if (!size || buf.length === size) { + return buf; + } else if (buf.length < size) { + return Buffer.concat([Buffer(size-buf.length).fill(0), buf]); + } else if (buf.length > size) { + throw new Error('EC signature number bigger than expected'); } - var b64 = bn.toArrayLike(Buffer).toString('base64'); + throw new Error('invalid size "'+size+'" converting BigNumber to Buffer'); + } + function bnToB64(bn) { + var b64 = bnToBuffer(bn).toString('base64'); return b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=*$/, ''); } function genEcdsaKeyPair() { @@ -41,8 +50,8 @@ key_ops: ['verify'] , kty: 'EC' , crv: 'P-256' - , x: bnToB64(key.getPublic().x) - , y: bnToB64(key.getPublic().y) + , x: bnToB64(key.getPublic().getX()) + , y: bnToB64(key.getPublic().getY()) }; var privJwk = JSON.parse(JSON.stringify(pubJwk)); @@ -52,16 +61,6 @@ return {privateKey: privJwk, publicKey: pubJwk}; } - function bnToBuffer(bn, size) { - var buf = bn.toArrayLike(Buffer); - if (!size || buf.length === size) { - return buf; - } - if (buf > size) { - throw new Error("EC signature number bigger than expected"); - } - return Buffer.concat([Buffer(size-buf.length).fill(0), buf]); - } function sign(jwk, msg) { var key = ec.keyFromPrivate(Buffer(jwk.d, 'base64')); var sig = key.sign(sha256(msg)); @@ -80,7 +79,10 @@ function promiseWrap(func) { return function() { var args = arguments; - return new Promise(function (resolve) { + // This fallback file should only be used when the browser doesn't support everything we + // need with WebCrypto. Since it is only used in the browser we should be able to assume + // that OAUTH3 has been placed in the global scope and that we can access it here. + return new OAUTH3.PromiseA(function (resolve) { resolve(func.apply(null, args)); }); }; diff --git a/oauth3.crypto.js b/oauth3.crypto.js index 0781545..a6559ab 100644 --- a/oauth3.crypto.js +++ b/oauth3.crypto.js @@ -3,160 +3,171 @@ var OAUTH3 = exports.OAUTH3 = exports.OAUTH3 || require('./oauth3.core.js').OAUTH3; - 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; - }; - - var webCrypto = {}; - webCrypto.sha256 = function (buf) { - return crypto.subtle.digest({name: 'SHA-256'}, buf); - }; - - webCrypto.pbkdf2 = function (password, salt) { - return 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 crypto.subtle.deriveKey(opts, key, {name: 'AES-GCM', length: 128}, true, ['encrypt', 'decrypt']); - }) - .then(function (key) { - return crypto.subtle.exportKey('raw', key); - }); - }; - - webCrypto.encrypt = function (rawKey, iv, data) { - return crypto.subtle.importKey('raw', rawKey, {name: 'AES-GCM'}, false, ['encrypt']) - .then(function (key) { - return crypto.subtle.encrypt({name: 'AES-GCM', iv: iv}, key, data); - }); - }; - webCrypto.decrypt = function (rawKey, iv, data) { - return crypto.subtle.importKey('raw', rawKey, {name: 'AES-GCM'}, false, ['decrypt']) - .then(function (key) { - return crypto.subtle.decrypt({name: 'AES-GCM', iv: iv}, key, data); - }); - }; - - webCrypto.genEcdsaKeyPair = function () { - return crypto.subtle.generateKey({name: 'ECDSA', namedCurve: 'P-256'}, true, ['sign', 'verify']) - .then(function (keyPair) { - return OAUTH3.PromiseA.all([ - crypto.subtle.exportKey('jwk', keyPair.privateKey) - , crypto.subtle.exportKey('jwk', keyPair.publicKey) - ]); - }).then(function (jwkPair) { - return { privateKey: jwkPair[0], publicKey: jwkPair[1] }; - }); - }; - - webCrypto.sign = function (jwk, msg) { - return crypto.subtle.importKey('jwk', jwk, {name: 'ECDSA', namedCurve: jwk.crv}, false, ['sign']) - .then(function (key) { - return 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 crypto.subtle.importKey('jwk', jwk, {name: 'ECDSA', namedCurve: jwk.crv}, false, ['verify']) - .then(function (key) { - return crypto.subtle.verify({name: 'ECDSA', hash: {name: 'SHA-256'}}, key, signature, msg); - }); - }; - OAUTH3.crypto = {}; - OAUTH3.crypto.core = {}; - function checkWebCrypto() { - function checkException(name, func) { - new OAUTH3.PromiseA(function (resolve) { resolve(func()); }) - .then(function () { - OAUTH3.crypto.core[name] = webCrypto[name]; + 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']); }) - .catch(function (err) { - console.warn('error with WebCrypto', name, '- using fallback', err); - loadFallback().then(function () { - OAUTH3.crypto.core[name] = OAUTH3_crypto_fallback[name]; - }); + .then(function (key) { + return OAUTH3._browser.window.crypto.subtle.exportKey('raw', key); }); - } - function checkResult(name, expected, func) { - checkException(name, function () { - return func() - .then(function (result) { - if (typeof expected === typeof result) { - return result; + }; + + 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(); } - return OAUTH3._base64.bufferToUrlSafe(result); + }; + 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]; }) - .then(function (result) { - if (result !== expected) { - throw new Error("result ("+result+") doesn't match expectation ("+expected+")"); - } + .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); }); } - - 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(); } - checkWebCrypto(); OAUTH3.crypto.thumbprintJwk = function (jwk) { var keys; @@ -183,12 +194,12 @@ }; OAUTH3.crypto._createKey = function (ppid) { - var kekPromise, ecdsaPromise; - var salt = window.crypto.getRandomValues(new Uint8Array(16)); + var saltProm = OAUTH3.crypto.core.randomBytes(16); + var kekProm = saltProm.then(function (salt) { + return OAUTH3.crypto.core.pbkdf2(ppid, salt); + }); - kekPromise = OAUTH3.crypto.core.pbkdf2(ppid, salt); - - ecdsaPromise = OAUTH3.crypto.core.genEcdsaKeyPair() + 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'; @@ -197,18 +208,28 @@ }); }); - return OAUTH3.PromiseA.all([kekPromise, ecdsaPromise]).then(function (keys) { - var ecdsaIv = window.crypto.getRandomValues(new Uint8Array(12)); - var secretIv = window.crypto.getRandomValues(new Uint8Array(12)); - var userSecret = window.crypto.getRandomValues(new Uint8Array(16)); + 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(keys[0], ecdsaIv, OAUTH3._binStr.binStrToBuffer(JSON.stringify(keys[1].privateKey))) - , OAUTH3.crypto.core.encrypt(keys[0], secretIv, userSecret) + 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: keys[1].publicKey + publicKey: keyPair.publicKey , privateKey: OAUTH3._base64.bufferToUrlSafe(encrypted[0]) , userSecret: OAUTH3._base64.bufferToUrlSafe(encrypted[1]) , salt: OAUTH3._base64.bufferToUrlSafe(salt) diff --git a/oauth3.node.crypto.js b/oauth3.node.crypto.js new file mode 100644 index 0000000..96424f5 --- /dev/null +++ b/oauth3.node.crypto.js @@ -0,0 +1,106 @@ +;(function () { +'use strict'; + + var crypto = require('crypto'); + var OAUTH3 = require('./oauth3.core.js').OAUTH3; + var ec = require('elliptic').ec('p256'); + + function randomBytes(size) { + return new OAUTH3.PromiseA(function (resolve, reject) { + crypto.randomBytes(size, function (err, buf) { + if (err) { + reject(err); + } else { + resolve(buf); + } + }); + }); + } + + function sha256(buf) { + return crypto.createHash('sha256').update(buf).digest(); + } + + function pbkdf2(password, salt) { + // Derived AES key is 128 bit, and the function takes a size in bytes. + return crypto.pbkdf2Sync(password, Buffer(salt), 8192, 16, 'sha256'); + } + + function encrypt(key, iv, data) { + var cipher = crypto.createCipheriv('aes-128-gcm', Buffer(key), Buffer(iv)); + + return Buffer.concat([cipher.update(Buffer(data)), cipher.final(), cipher.getAuthTag()]); + } + + function decrypt(key, iv, data) { + var decipher = crypto.createDecipheriv('aes-128-gcm', Buffer(key), Buffer(iv)); + + decipher.setAuthTag(Buffer(data.slice(-16))); + return Buffer.concat([decipher.update(Buffer(data.slice(0, -16))), decipher.final()]); + } + + function bnToBuffer(bn, size) { + var buf = bn.toArrayLike(Buffer); + + if (!size || buf.length === size) { + return buf; + } else if (buf.length < size) { + return Buffer.concat([Buffer(size-buf.length).fill(0), buf]); + } else if (buf.length > size) { + throw new Error('EC signature number bigger than expected'); + } + throw new Error('invalid size "'+size+'" converting BigNumber to Buffer'); + } + function bnToB64(bn) { + var b64 = bnToBuffer(bn).toString('base64'); + return b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=*$/, ''); + } + function genEcdsaKeyPair() { + var key = ec.genKeyPair(); + var pubJwk = { + key_ops: ['verify'] + , kty: 'EC' + , crv: 'P-256' + , x: bnToB64(key.getPublic().getX()) + , y: bnToB64(key.getPublic().getY()) + }; + + var privJwk = JSON.parse(JSON.stringify(pubJwk)); + privJwk.key_ops = ['sign']; + privJwk.d = bnToB64(key.getPrivate()); + + return {privateKey: privJwk, publicKey: pubJwk}; + } + + function sign(jwk, msg) { + var key = ec.keyFromPrivate(Buffer(jwk.d, 'base64')); + var sig = key.sign(sha256(msg)); + return Buffer.concat([bnToBuffer(sig.r, 32), bnToBuffer(sig.s, 32)]); + } + + function verify(jwk, msg, signature) { + var key = ec.keyFromPublic({x: Buffer(jwk.x, 'base64'), y: Buffer(jwk.y, 'base64')}); + var sig = { + r: Buffer(signature.slice(0, signature.length/2)) + , s: Buffer(signature.slice(signature.length/2)) + }; + return key.verify(sha256(msg), sig); + } + + function promiseWrap(func) { + return function() { + var args = arguments; + return new OAUTH3.PromiseA(function (resolve) { + resolve(func.apply(null, args)); + }); + }; + } + exports.sha256 = promiseWrap(sha256); + exports.pbkdf2 = promiseWrap(pbkdf2); + exports.encrypt = promiseWrap(encrypt); + exports.decrypt = promiseWrap(decrypt); + exports.sign = promiseWrap(sign); + exports.verify = promiseWrap(verify); + exports.genEcdsaKeyPair = promiseWrap(genEcdsaKeyPair); + exports.randomBytes = randomBytes; +}()); diff --git a/package.json b/package.json index febe179..60abc9b 100644 --- a/package.json +++ b/package.json @@ -31,10 +31,12 @@ "log", "sign" ], + "dependencies": { + "elliptic": "^6.4.0" + }, "devDependencies": { "browserify-aes": "^1.0.6", "create-hash": "^1.1.2", - "elliptic": "^6.4.0", "pbkdf2": "^3.0.9", "browserify": "^14.1.0", From fe1b5126781e6067e441db418abcd310efceca80 Mon Sep 17 00:00:00 2001 From: tigerbot Date: Tue, 21 Mar 2017 17:27:52 -0600 Subject: [PATCH 15/15] reduced the size of the fallback file --- browserify/crypto.fallback.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/browserify/crypto.fallback.js b/browserify/crypto.fallback.js index aca2cb8..54e587f 100644 --- a/browserify/crypto.fallback.js +++ b/browserify/crypto.fallback.js @@ -4,7 +4,7 @@ var createHash = require('create-hash'); var pbkdf2 = require('pbkdf2'); var aes = require('browserify-aes'); - var ec = require('elliptic').ec('p256'); + var ec = require('elliptic/lib/elliptic/ec')('p256'); function sha256(buf) { return createHash('sha256').update(buf).digest();