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",