implemented the core crypto functions for node

This commit is contained in:
tigerbot 2017-03-21 17:16:40 -06:00
parent 9cfd517880
commit ac89cb7904
4 changed files with 306 additions and 175 deletions

View File

@ -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));
});
};

View File

@ -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)

106
oauth3.node.crypto.js Normal file
View File

@ -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;
}());

View File

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