oauth3.js/oauth3.crypto.js

306 lines
12 KiB
JavaScript

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