implemented PKCE auth (#921)

* implemented PKCE auth

* removed node-jose

* added PKCE tests
This commit is contained in:
Danny Coates 2018-09-14 08:00:33 -07:00 committed by Donovan Preston
parent 20528eb0d1
commit 7ccf462bf8
18 changed files with 331 additions and 263 deletions

View file

@ -1,21 +1,153 @@
import jose from 'node-jose';
import { arrayToB64, b64ToArray } from './utils';
const encoder = new TextEncoder();
const decoder = new TextDecoder();
export async function prepareWrapKey(storage) {
const keystore = jose.JWK.createKeyStore();
const keypair = await keystore.generate('EC', 'P-256');
storage.set('fxaWrapKey', JSON.stringify(keystore.toJSON(true)));
return jose.util.base64url.encode(JSON.stringify(keypair.toJSON()));
function getOtherInfo(enc) {
const name = encoder.encode(enc);
const length = 256;
const buffer = new ArrayBuffer(name.length + 16);
const dv = new DataView(buffer);
const result = new Uint8Array(buffer);
let i = 0;
dv.setUint32(i, name.length);
i += 4;
result.set(name, i);
i += name.length;
dv.setUint32(i, 0);
i += 4;
dv.setUint32(i, 0);
i += 4;
dv.setUint32(i, length);
return result;
}
function concat(b1, b2) {
const result = new Uint8Array(b1.length + b2.length);
result.set(b1, 0);
result.set(b2, b1.length);
return result;
}
async function concatKdf(key, enc) {
if (key.length !== 32) {
throw new Error('unsupported key length');
}
const otherInfo = getOtherInfo(enc);
const buffer = new ArrayBuffer(4 + key.length + otherInfo.length);
const dv = new DataView(buffer);
const concat = new Uint8Array(buffer);
dv.setUint32(0, 1);
concat.set(key, 4);
concat.set(otherInfo, key.length + 4);
const result = await crypto.subtle.digest('SHA-256', concat);
return new Uint8Array(result);
}
export async function prepareScopedBundleKey(storage) {
const keys = await crypto.subtle.generateKey(
{
name: 'ECDH',
namedCurve: 'P-256'
},
true,
['deriveBits']
);
const privateJwk = await crypto.subtle.exportKey('jwk', keys.privateKey);
const publicJwk = await crypto.subtle.exportKey('jwk', keys.publicKey);
const kid = await crypto.subtle.digest(
'SHA-256',
encoder.encode(JSON.stringify(publicJwk))
);
privateJwk.kid = kid;
publicJwk.kid = kid;
storage.set('scopedBundlePrivateKey', JSON.stringify(privateJwk));
return arrayToB64(encoder.encode(JSON.stringify(publicJwk)));
}
export async function decryptBundle(storage, bundle) {
const privateJwk = JSON.parse(storage.get('scopedBundlePrivateKey'));
storage.remove('scopedBundlePrivateKey');
const privateKey = await crypto.subtle.importKey(
'jwk',
privateJwk,
{
name: 'ECDH',
namedCurve: 'P-256'
},
false,
['deriveBits']
);
const jweParts = bundle.split('.');
if (jweParts.length !== 5) {
throw new Error('invalid jwe');
}
const header = JSON.parse(decoder.decode(b64ToArray(jweParts[0])));
const additionalData = encoder.encode(jweParts[0]);
const iv = b64ToArray(jweParts[2]);
const ciphertext = b64ToArray(jweParts[3]);
const tag = b64ToArray(jweParts[4]);
if (header.alg !== 'ECDH-ES' || header.enc !== 'A256GCM') {
throw new Error('unsupported jwe type');
}
const publicKey = await crypto.subtle.importKey(
'jwk',
header.epk,
{
name: 'ECDH',
namedCurve: 'P-256'
},
false,
[]
);
const sharedBits = await crypto.subtle.deriveBits(
{
name: 'ECDH',
public: publicKey
},
privateKey,
256
);
const rawSharedKey = await concatKdf(new Uint8Array(sharedBits), header.enc);
const sharedKey = await crypto.subtle.importKey(
'raw',
rawSharedKey,
{
name: 'AES-GCM'
},
false,
['decrypt']
);
const plaintext = await crypto.subtle.decrypt(
{
name: 'AES-GCM',
iv: iv,
additionalData: additionalData,
tagLength: tag.length * 8
},
sharedKey,
concat(ciphertext, tag)
);
return JSON.parse(decoder.decode(plaintext));
}
export async function preparePkce(storage) {
const verifier = arrayToB64(crypto.getRandomValues(new Uint8Array(64)));
storage.set('pkceVerifier', verifier);
const challenge = await crypto.subtle.digest(
'SHA-256',
encoder.encode(verifier)
);
return arrayToB64(new Uint8Array(challenge));
}
export async function getFileListKey(storage, bundle) {
const keystore = await jose.JWK.asKeyStore(
JSON.parse(storage.get('fxaWrapKey'))
);
const result = await jose.JWE.createDecrypt(keystore).decrypt(bundle);
const jwks = JSON.parse(jose.util.utf8.encode(result.plaintext));
const jwks = await decryptBundle(storage, bundle);
const jwk = jwks['https://identity.mozilla.com/apps/send'];
const baseKey = await crypto.subtle.importKey(
'raw',