Merge remote-tracking branch 'origin/signing' into v1.0
This commit is contained in:
commit
5ed05f03cf
|
@ -0,0 +1 @@
|
||||||
|
node_modules/
|
10
README.md
10
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,
|
Instead of bloating your webapp and ruining the mobile experience,
|
||||||
you can use a single, small javascript file for all OAuth3 providers
|
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,
|
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)
|
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.info('Authentication was Successful:');
|
||||||
console.log(session);
|
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)
|
// (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
|
// 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
|
# clone the project as assets/org.oauth3
|
||||||
mkdir -p assets
|
mkdir -p assets
|
||||||
git clone git@git.daplie.com:Daplie/oauth3.js.git assets/org.oauth3
|
git clone git@git.daplie.com:Daplie/oauth3.js.git assets/org.oauth3
|
||||||
pushd assests/org.oauth3
|
pushd assets/org.oauth3
|
||||||
git checkout v1
|
git checkout v1
|
||||||
popd
|
popd
|
||||||
|
|
||||||
|
@ -232,7 +232,7 @@ function onClickLogin() {
|
||||||
console.info('Authentication was Successful:');
|
console.info('Authentication was Successful:');
|
||||||
console.log(session);
|
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)
|
// (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
|
// 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
|
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.
|
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
|
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
|
https://example.com/username/.well-known/oauth3/directives.json then /api/whatever would refer
|
||||||
to https://example.com/username/api/whatever.
|
to https://example.com/username/api/whatever.
|
||||||
|
|
|
@ -0,0 +1,97 @@
|
||||||
|
;(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var createHash = require('create-hash');
|
||||||
|
var pbkdf2 = require('pbkdf2');
|
||||||
|
var aes = require('browserify-aes');
|
||||||
|
var ec = require('elliptic/lib/elliptic/ec')('p256');
|
||||||
|
|
||||||
|
function sha256(buf) {
|
||||||
|
return createHash('sha256').update(buf).digest();
|
||||||
|
}
|
||||||
|
|
||||||
|
function runPbkdf2(password, salt) {
|
||||||
|
// Derived AES key is 128 bit, and the function takes a size in bytes.
|
||||||
|
return pbkdf2.pbkdf2Sync(password, Buffer(salt), 8192, 16, 'sha256');
|
||||||
|
}
|
||||||
|
|
||||||
|
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, iv, data) {
|
||||||
|
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 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;
|
||||||
|
// 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));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
}());
|
|
@ -0,0 +1,25 @@
|
||||||
|
;(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.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.fallback.min.js'))
|
||||||
|
.pipe(gulp.dest('./'))
|
||||||
|
;
|
||||||
|
});
|
||||||
|
|
||||||
|
gulp.task('watch', function () {
|
||||||
|
gulp.watch('browserify/*.js', [ 'default' ]);
|
||||||
|
});
|
||||||
|
}());
|
|
@ -14,6 +14,27 @@
|
||||||
return err;
|
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: {
|
, _base64: {
|
||||||
atob: function (base64) {
|
atob: function (base64) {
|
||||||
// atob must be called from the global context
|
// atob must be called from the global context
|
||||||
|
@ -43,6 +64,12 @@
|
||||||
b64 = b64.replace(/=+/g, '');
|
b64 = b64.replace(/=+/g, '');
|
||||||
return b64;
|
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: {
|
, uri: {
|
||||||
normalize: function (uri) {
|
normalize: function (uri) {
|
||||||
|
@ -181,15 +208,17 @@
|
||||||
// { header: {}, payload: {}, signature: '' }
|
// { header: {}, payload: {}, signature: '' }
|
||||||
var parts = str.split(/\./g);
|
var parts = str.split(/\./g);
|
||||||
var jsons = parts.slice(0, 2).map(function (urlsafe64) {
|
var jsons = parts.slice(0, 2).map(function (urlsafe64) {
|
||||||
var b64 = OAUTH3._base64.decodeUrlSafe(urlsafe64);
|
return JSON.parse(OAUTH3._base64.decodeUrlSafe(urlsafe64));
|
||||||
return b64;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return { header: jsons[0], payload: jsons[1] };
|
||||||
header: JSON.parse(jsons[0])
|
}
|
||||||
, payload: JSON.parse(jsons[1])
|
, verify: function (jwk, token) {
|
||||||
, signature: parts[2] // should remain url-safe base64
|
var parts = token.split(/\./g);
|
||||||
};
|
var data = OAUTH3._binStr.binStrToBuffer(parts.slice(0, 2).join('.'));
|
||||||
|
var signature = OAUTH3._base64.urlSafeToBuffer(parts[2]);
|
||||||
|
|
||||||
|
return OAUTH3.crypto.core.verify(jwk, data, signature);
|
||||||
}
|
}
|
||||||
, freshness: function (tokenMeta, staletime, _now) {
|
, freshness: function (tokenMeta, staletime, _now) {
|
||||||
staletime = staletime || (15 * 60);
|
staletime = staletime || (15 * 60);
|
||||||
|
@ -740,6 +769,7 @@
|
||||||
return OAUTH3.PromiseA.reject(OAUTH3.error.parse(directives.issuer /*providerUri*/, params));
|
return OAUTH3.PromiseA.reject(OAUTH3.error.parse(directives.issuer /*providerUri*/, params));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
OAUTH3.hooks.session._cache = {};
|
||||||
return params;
|
return params;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,293 @@
|
||||||
|
;(function (exports) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var OAUTH3 = exports.OAUTH3 = exports.OAUTH3 || require('./oauth3.core.js').OAUTH3;
|
||||||
|
|
||||||
|
OAUTH3.crypto = {};
|
||||||
|
try {
|
||||||
|
OAUTH3.crypto.core = require('./oauth3.node.crypto');
|
||||||
|
} catch (error) {
|
||||||
|
OAUTH3.crypto.core = {};
|
||||||
|
|
||||||
|
// We don't currently have a fallback method for this function, so we assign
|
||||||
|
// it directly to the core object instead of the webCrypto object.
|
||||||
|
OAUTH3.crypto.core.randomBytes = function (size) {
|
||||||
|
var buf = OAUTH3._browser.window.crypto.getRandomValues(new Uint8Array(size));
|
||||||
|
return OAUTH3.PromiseA.resolve(buf);
|
||||||
|
};
|
||||||
|
|
||||||
|
var webCrypto = {};
|
||||||
|
webCrypto.sha256 = function (buf) {
|
||||||
|
return OAUTH3._browser.window.crypto.subtle.digest({name: 'SHA-256'}, buf);
|
||||||
|
};
|
||||||
|
|
||||||
|
webCrypto.pbkdf2 = function (password, salt) {
|
||||||
|
return OAUTH3._browser.window.crypto.subtle.importKey('raw', OAUTH3._binStr.binStrToBuffer(password), {name: 'PBKDF2'}, false, ['deriveKey'])
|
||||||
|
.then(function (key) {
|
||||||
|
var opts = {name: 'PBKDF2', salt: salt, iterations: 8192, hash: {name: 'SHA-256'}};
|
||||||
|
return OAUTH3._browser.window.crypto.subtle.deriveKey(opts, key, {name: 'AES-GCM', length: 128}, true, ['encrypt', 'decrypt']);
|
||||||
|
})
|
||||||
|
.then(function (key) {
|
||||||
|
return OAUTH3._browser.window.crypto.subtle.exportKey('raw', key);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
webCrypto.encrypt = function (rawKey, iv, data) {
|
||||||
|
return OAUTH3._browser.window.crypto.subtle.importKey('raw', rawKey, {name: 'AES-GCM'}, false, ['encrypt'])
|
||||||
|
.then(function (key) {
|
||||||
|
return OAUTH3._browser.window.crypto.subtle.encrypt({name: 'AES-GCM', iv: iv}, key, data);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
webCrypto.decrypt = function (rawKey, iv, data) {
|
||||||
|
return OAUTH3._browser.window.crypto.subtle.importKey('raw', rawKey, {name: 'AES-GCM'}, false, ['decrypt'])
|
||||||
|
.then(function (key) {
|
||||||
|
return OAUTH3._browser.window.crypto.subtle.decrypt({name: 'AES-GCM', iv: iv}, key, data);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
webCrypto.genEcdsaKeyPair = function () {
|
||||||
|
return OAUTH3._browser.window.crypto.subtle.generateKey({name: 'ECDSA', namedCurve: 'P-256'}, true, ['sign', 'verify'])
|
||||||
|
.then(function (keyPair) {
|
||||||
|
return OAUTH3.PromiseA.all([
|
||||||
|
OAUTH3._browser.window.crypto.subtle.exportKey('jwk', keyPair.privateKey)
|
||||||
|
, OAUTH3._browser.window.crypto.subtle.exportKey('jwk', keyPair.publicKey)
|
||||||
|
]);
|
||||||
|
}).then(function (jwkPair) {
|
||||||
|
return { privateKey: jwkPair[0], publicKey: jwkPair[1] };
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
webCrypto.sign = function (jwk, msg) {
|
||||||
|
return OAUTH3._browser.window.crypto.subtle.importKey('jwk', jwk, {name: 'ECDSA', namedCurve: jwk.crv}, false, ['sign'])
|
||||||
|
.then(function (key) {
|
||||||
|
return OAUTH3._browser.window.crypto.subtle.sign({name: 'ECDSA', hash: {name: 'SHA-256'}}, key, msg);
|
||||||
|
})
|
||||||
|
.then(function (sig) {
|
||||||
|
return new Uint8Array(sig);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
webCrypto.verify = function (jwk, msg, signature) {
|
||||||
|
// If the JWK has properties that should only exist on the private key or is missing
|
||||||
|
// "verify" in the key_ops, importing in as a public key won't work.
|
||||||
|
if (jwk.hasOwnProperty('d') || jwk.hasOwnProperty('key_ops')) {
|
||||||
|
jwk = JSON.parse(JSON.stringify(jwk));
|
||||||
|
delete jwk.d;
|
||||||
|
delete jwk.key_ops;
|
||||||
|
}
|
||||||
|
|
||||||
|
return OAUTH3._browser.window.crypto.subtle.importKey('jwk', jwk, {name: 'ECDSA', namedCurve: jwk.crv}, false, ['verify'])
|
||||||
|
.then(function (key) {
|
||||||
|
return OAUTH3._browser.window.crypto.subtle.verify({name: 'ECDSA', hash: {name: 'SHA-256'}}, key, signature, msg);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function checkWebCrypto() {
|
||||||
|
var loadFallback = function() {
|
||||||
|
var prom;
|
||||||
|
loadFallback = function () { return prom; };
|
||||||
|
|
||||||
|
prom = new OAUTH3.PromiseA(function (resolve) {
|
||||||
|
var body = document.getElementsByTagName('body')[0];
|
||||||
|
var script = document.createElement('script');
|
||||||
|
script.type = 'text/javascript';
|
||||||
|
script.onload = resolve;
|
||||||
|
script.onreadystatechange = function () {
|
||||||
|
if (this.readyState === 'complete' || this.readyState === 'loaded') {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
script.src = '/assets/org.oauth3/oauth3.crypto.fallback.js';
|
||||||
|
body.appendChild(script);
|
||||||
|
});
|
||||||
|
return prom;
|
||||||
|
};
|
||||||
|
function checkException(name, func) {
|
||||||
|
new OAUTH3.PromiseA(function (resolve) { resolve(func()); })
|
||||||
|
.then(function () {
|
||||||
|
OAUTH3.crypto.core[name] = webCrypto[name];
|
||||||
|
})
|
||||||
|
.catch(function (err) {
|
||||||
|
console.warn('error with WebCrypto', name, '- using fallback', err);
|
||||||
|
loadFallback().then(function () {
|
||||||
|
OAUTH3.crypto.core[name] = OAUTH3_crypto_fallback[name];
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function checkResult(name, expected, func) {
|
||||||
|
checkException(name, function () {
|
||||||
|
return func()
|
||||||
|
.then(function (result) {
|
||||||
|
if (typeof expected === typeof result) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return OAUTH3._base64.bufferToUrlSafe(result);
|
||||||
|
})
|
||||||
|
.then(function (result) {
|
||||||
|
if (result !== expected) {
|
||||||
|
throw new Error("result ("+result+") doesn't match expectation ("+expected+")");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var zeroBuf = new Uint8Array([0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]);
|
||||||
|
var dataBuf = OAUTH3._base64.urlSafeToBuffer('1234567890abcdefghijklmn');
|
||||||
|
var keyBuf = OAUTH3._base64.urlSafeToBuffer('l_Aeoqk6ePjwjCYrlHrgrg');
|
||||||
|
var encBuf = OAUTH3._base64.urlSafeToBuffer('Ji_gEtcNElUONSR4Mf9S75davXjh_6-oQN9AgO5UF8rERw');
|
||||||
|
checkResult('sha256', 'BwMveUm2V1axuERvUoxM4dScgNl9yKhER9a6p80GXj4', function () {
|
||||||
|
return webCrypto.sha256(dataBuf);
|
||||||
|
});
|
||||||
|
checkResult('pbkdf2', OAUTH3._base64.bufferToUrlSafe(keyBuf), function () {
|
||||||
|
return webCrypto.pbkdf2('password', zeroBuf);
|
||||||
|
});
|
||||||
|
checkResult('encrypt', OAUTH3._base64.bufferToUrlSafe(encBuf), function () {
|
||||||
|
return webCrypto.encrypt(keyBuf, zeroBuf.slice(0, 12), dataBuf);
|
||||||
|
});
|
||||||
|
checkResult('decrypt', OAUTH3._base64.bufferToUrlSafe(dataBuf), function () {
|
||||||
|
return webCrypto.decrypt(keyBuf, zeroBuf.slice(0, 12), encBuf);
|
||||||
|
});
|
||||||
|
|
||||||
|
var jwk = {
|
||||||
|
kty: "EC"
|
||||||
|
, crv: "P-256"
|
||||||
|
, d: "ChXx7ea5YtEltCufA8CVb0lQv3glcCfcSpEgdedgIP0"
|
||||||
|
, x: "Akt5ZDbytcKS5UQMURvGb_UIMS4qFctDwrX8bX22ato"
|
||||||
|
, y: "cV7nhpWNT1FeRIbdold4jLtgsEpZBFcNy3p2E5mqvto"
|
||||||
|
};
|
||||||
|
var sig = OAUTH3._base64.urlSafeToBuffer('nc3F8qeP8OXpfqPD9tTcFQg0Wfp37RTAppLPIKE1ZupR_8Aba64hNExwd1dOk802OFQxaECPDZCkKe7WA9RXAg');
|
||||||
|
checkResult('verify', true, function() {
|
||||||
|
return webCrypto.verify(jwk, dataBuf, sig);
|
||||||
|
});
|
||||||
|
// The results of these functions are less predictable, so we can't check their return value.
|
||||||
|
checkException('genEcdsaKeyPair', function () {
|
||||||
|
return webCrypto.genEcdsaKeyPair();
|
||||||
|
});
|
||||||
|
checkException('sign', function () {
|
||||||
|
return webCrypto.sign(jwk, dataBuf);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
checkWebCrypto();
|
||||||
|
}
|
||||||
|
|
||||||
|
OAUTH3.crypto.thumbprintJwk = function (jwk) {
|
||||||
|
var keys;
|
||||||
|
if (jwk.kty === 'EC') {
|
||||||
|
keys = ['crv', 'x', 'y'];
|
||||||
|
} else if (jwk.kty === 'RSA') {
|
||||||
|
keys = ['e', 'n'];
|
||||||
|
} else if (jwk.kty === 'oct') {
|
||||||
|
keys = ['k'];
|
||||||
|
} else {
|
||||||
|
return OAUTH3.PromiseA.reject(new Error('invalid JWK key type ' + jwk.kty));
|
||||||
|
}
|
||||||
|
keys.push('kty');
|
||||||
|
keys.sort();
|
||||||
|
|
||||||
|
var missing = keys.filter(function (name) { return !jwk.hasOwnProperty(name); });
|
||||||
|
if (missing.length > 0) {
|
||||||
|
return OAUTH3.PromiseA.reject(new Error('JWK of type '+jwk.kty+' missing fields ' + missing));
|
||||||
|
}
|
||||||
|
|
||||||
|
var jwkStr = '{' + keys.map(function (name) { return name+':'+jwk[name]; }).join(',') + '}';
|
||||||
|
return OAUTH3.crypto.core.sha256(OAUTH3._binStr.binStrToBuffer(jwkStr))
|
||||||
|
.then(OAUTH3._base64.bufferToUrlSafe);
|
||||||
|
};
|
||||||
|
|
||||||
|
OAUTH3.crypto._createKey = function (ppid) {
|
||||||
|
var saltProm = OAUTH3.crypto.core.randomBytes(16);
|
||||||
|
var kekProm = saltProm.then(function (salt) {
|
||||||
|
return OAUTH3.crypto.core.pbkdf2(ppid, salt);
|
||||||
|
});
|
||||||
|
|
||||||
|
var ecdsaProm = OAUTH3.crypto.core.genEcdsaKeyPair()
|
||||||
|
.then(function (keyPair) {
|
||||||
|
return OAUTH3.crypto.thumbprintJwk(keyPair.publicKey).then(function (kid) {
|
||||||
|
keyPair.privateKey.alg = keyPair.publicKey.alg = 'ES256';
|
||||||
|
keyPair.privateKey.kid = keyPair.publicKey.kid = kid;
|
||||||
|
return keyPair;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return OAUTH3.PromiseA.all([
|
||||||
|
kekProm
|
||||||
|
, ecdsaProm
|
||||||
|
, saltProm
|
||||||
|
, OAUTH3.crypto.core.randomBytes(16)
|
||||||
|
, OAUTH3.crypto.core.randomBytes(12)
|
||||||
|
, OAUTH3.crypto.core.randomBytes(12)
|
||||||
|
]).then(function (results) {
|
||||||
|
var kek = results[0];
|
||||||
|
var keyPair = results[1];
|
||||||
|
var salt = results[2];
|
||||||
|
var userSecret = results[3];
|
||||||
|
var ecdsaIv = results[4];
|
||||||
|
var secretIv = results[5];
|
||||||
|
|
||||||
|
return OAUTH3.PromiseA.all([
|
||||||
|
OAUTH3.crypto.core.encrypt(kek, ecdsaIv, OAUTH3._binStr.binStrToBuffer(JSON.stringify(keyPair.privateKey)))
|
||||||
|
, OAUTH3.crypto.core.encrypt(kek, secretIv, userSecret)
|
||||||
|
])
|
||||||
|
.then(function (encrypted) {
|
||||||
|
return {
|
||||||
|
publicKey: keyPair.publicKey
|
||||||
|
, privateKey: OAUTH3._base64.bufferToUrlSafe(encrypted[0])
|
||||||
|
, userSecret: OAUTH3._base64.bufferToUrlSafe(encrypted[1])
|
||||||
|
, salt: OAUTH3._base64.bufferToUrlSafe(salt)
|
||||||
|
, ecdsaIv: OAUTH3._base64.bufferToUrlSafe(ecdsaIv)
|
||||||
|
, secretIv: OAUTH3._base64.bufferToUrlSafe(secretIv)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
OAUTH3.crypto._decryptKey = function (ppid, storedObj) {
|
||||||
|
var salt = OAUTH3._base64.urlSafeToBuffer(storedObj.salt);
|
||||||
|
var encJwk = OAUTH3._base64.urlSafeToBuffer(storedObj.privateKey);
|
||||||
|
var iv = OAUTH3._base64.urlSafeToBuffer(storedObj.ecdsaIv);
|
||||||
|
|
||||||
|
return OAUTH3.crypto.core.pbkdf2(ppid, salt)
|
||||||
|
.then(function (key) {
|
||||||
|
return OAUTH3.crypto.core.decrypt(key, iv, encJwk);
|
||||||
|
})
|
||||||
|
.then(OAUTH3._binStr.bufferToBinStr)
|
||||||
|
.then(JSON.parse);
|
||||||
|
};
|
||||||
|
|
||||||
|
OAUTH3.crypto._getKey = function (ppid) {
|
||||||
|
return OAUTH3.crypto.core.sha256(OAUTH3._binStr.binStrToBuffer(ppid))
|
||||||
|
.then(function (hash) {
|
||||||
|
var name = 'kek-' + OAUTH3._base64.bufferToUrlSafe(hash);
|
||||||
|
var promise;
|
||||||
|
|
||||||
|
if (window.localStorage.getItem(name) === null) {
|
||||||
|
promise = OAUTH3.crypto._createKey(ppid).then(function (key) {
|
||||||
|
window.localStorage.setItem(name, JSON.stringify(key));
|
||||||
|
return key;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
promise = OAUTH3.PromiseA.resolve(JSON.parse(window.localStorage.getItem(name)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return promise.then(function (storedObj) {
|
||||||
|
return OAUTH3.crypto._decryptKey(ppid, storedObj);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
OAUTH3.crypto._signPayload = function (payload) {
|
||||||
|
return OAUTH3.crypto._getKey('some PPID').then(function (key) {
|
||||||
|
var header = {type: 'JWT', alg: key.alg, kid: key.kid};
|
||||||
|
var input = [
|
||||||
|
OAUTH3._base64.encodeUrlSafe(JSON.stringify(header, null))
|
||||||
|
, OAUTH3._base64.encodeUrlSafe(JSON.stringify(payload, null))
|
||||||
|
].join('.');
|
||||||
|
|
||||||
|
return OAUTH3.crypto.core.sign(key, OAUTH3._binStr.binStrToBuffer(input))
|
||||||
|
.then(OAUTH3._base64.bufferToUrlSafe)
|
||||||
|
.then(function (signature) {
|
||||||
|
return input + '.' + signature;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
}('undefined' !== typeof exports ? exports : window));
|
|
@ -3,30 +3,6 @@
|
||||||
|
|
||||||
var OAUTH3 = exports.OAUTH3 = exports.OAUTH3 || require('./oauth3.core.js').OAUTH3;
|
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._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.jwt.encode = function (parts) {
|
|
||||||
parts.header = parts.header || { alg: 'none', typ: 'jwt' };
|
|
||||||
parts.signature = parts.signature || '';
|
|
||||||
|
|
||||||
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('.');
|
|
||||||
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
OAUTH3.authn.resourceOwnerPassword = OAUTH3.authz.resourceOwnerPassword = function (directive, opts) {
|
OAUTH3.authn.resourceOwnerPassword = OAUTH3.authz.resourceOwnerPassword = function (directive, opts) {
|
||||||
var providerUri = directive.issuer;
|
var providerUri = directive.issuer;
|
||||||
|
|
||||||
|
@ -50,12 +26,12 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
OAUTH3.authz.scopes = function () {
|
OAUTH3.authz.scopes = function () {
|
||||||
return {
|
return OAUTH3.PromiseA.resolve({
|
||||||
pending: ['oauth3_authn'] // not yet accepted
|
pending: ['oauth3_authn'] // not yet accepted
|
||||||
, granted: [] // all granted, ever
|
, granted: [] // all granted, ever
|
||||||
, requested: ['oauth3_authn'] // all requested, now
|
, requested: ['oauth3_authn'] // all requested, now
|
||||||
, accepted: [] // granted (ever) and requested (now)
|
, accepted: [] // granted (ever) and requested (now)
|
||||||
};
|
});
|
||||||
};
|
};
|
||||||
OAUTH3.authz.grants = function (providerUri, opts) {
|
OAUTH3.authz.grants = function (providerUri, opts) {
|
||||||
if ('POST' === opts.method) {
|
if ('POST' === opts.method) {
|
||||||
|
@ -82,12 +58,8 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
OAUTH3._mockToken = function (providerUri, opts) {
|
OAUTH3._mockToken = function (providerUri, opts) {
|
||||||
var accessToken = OAUTH3.jwt.encode({
|
var payload = { exp: Math.round(Date.now() / 1000) + 900, sub: 'fakeUserId', scp: opts.scope };
|
||||||
header: { alg: 'none' }
|
return OAUTH3.crypto._signPayload(payload).then(function (accessToken) {
|
||||||
, payload: { exp: Math.round(Date.now() / 1000) + 900, sub: 'fakeUserId', scp: opts.scope }
|
|
||||||
, signature: "fakeSig"
|
|
||||||
});
|
|
||||||
|
|
||||||
return OAUTH3.hooks.session.refresh(
|
return OAUTH3.hooks.session.refresh(
|
||||||
opts.session || {
|
opts.session || {
|
||||||
provider_uri: providerUri
|
provider_uri: providerUri
|
||||||
|
@ -100,6 +72,7 @@
|
||||||
, scope: opts.scope
|
, scope: opts.scope
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
}('undefined' !== typeof exports ? exports : window));
|
}('undefined' !== typeof exports ? exports : window));
|
||||||
|
|
|
@ -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;
|
||||||
|
}());
|
17
package.json
17
package.json
|
@ -4,6 +4,7 @@
|
||||||
"description": "The world's smallest, fastest, and most secure OAuth3 (and OAuth2) JavaScript implementation.",
|
"description": "The world's smallest, fastest, and most secure OAuth3 (and OAuth2) JavaScript implementation.",
|
||||||
"main": "oauth3.node.js",
|
"main": "oauth3.node.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"install": "./node_modules/.bin/gulp",
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -30,6 +31,22 @@
|
||||||
"log",
|
"log",
|
||||||
"sign"
|
"sign"
|
||||||
],
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"elliptic": "^6.4.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"browserify-aes": "^1.0.6",
|
||||||
|
"create-hash": "^1.1.2",
|
||||||
|
"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",
|
||||||
|
"vinyl-source-stream": "^1.1.0"
|
||||||
|
},
|
||||||
"author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)",
|
"author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)",
|
||||||
"license": "(MIT OR Apache-2.0)"
|
"license": "(MIT OR Apache-2.0)"
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue