fox-send/app/fxa.js

181 lines
4.4 KiB
JavaScript

import { arrayToB64, b64ToArray } from './utils';
const encoder = new TextEncoder();
const decoder = new TextDecoder();
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 deriveFileListKey(ikm) {
const baseKey = await crypto.subtle.importKey(
'raw',
b64ToArray(ikm),
{ name: 'HKDF' },
false,
['deriveKey']
);
const fileListKey = await crypto.subtle.deriveKey(
{
name: 'HKDF',
salt: new Uint8Array(),
info: encoder.encode('fileList'),
hash: 'SHA-256'
},
baseKey,
{
name: 'AES-GCM',
length: 128
},
true,
['encrypt', 'decrypt']
);
const rawFileListKey = await crypto.subtle.exportKey('raw', fileListKey);
return arrayToB64(new Uint8Array(rawFileListKey));
}
export async function getFileListKey(storage, bundle) {
const jwks = await decryptBundle(storage, bundle);
const jwk = jwks['https://identity.mozilla.com/apps/send'];
return deriveFileListKey(jwk.k);
}