tigerbot
7 years ago
5 changed files with 350 additions and 170 deletions
@ -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)); |
Loading…
Reference in new issue