'use strict'; // taken from fxa function hexToUint8(str) { const HEX_STRING = /^(?:[a-fA-F0-9]{2})+$/; if (!HEX_STRING.test(str)) { throw new Error(`invalid hex string: ${str}`); } const bytes = str.match(/[a-fA-F0-9]{2}/g); return new Uint8Array(bytes.map((byte) => parseInt(byte, 16))); } function uint8ToHex(array) { return array.reduce((str, byte) => str + ('00' + byte.toString(16)).slice(-2), ''); } function uint8ToBase64(array) { return btoa(String.fromCharCode(...array)); } function uint8ToBase64Url(array) { return uint8ToBase64(array).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_'); } export function urlatob(s) { return atob(s.replace(/-/g, '+').replace(/_/g, "/")); } export async function deriveScopedKey(kB, uid, identifier, key_rotation_secret, key_rotation_timestamp) { async function importKey(k) { return await crypto.subtle.importKey('raw', hexToUint8(k), 'HKDF', false, ['deriveBits']); } async function derive(salt, k, info, len) { let params = { name: 'HKDF', salt, info: new TextEncoder().encode(info), hash: 'SHA-256', }; return new Uint8Array(await crypto.subtle.deriveBits(params, await importKey(k), len * 8)); } if (identifier == "https://identity.mozilla.com/apps/oldsync") { const bits = await derive( new Uint8Array(), kB, "identity.mozilla.com/picl/v1/oldsync", 64); const kHash = await crypto.subtle.digest("SHA-256", hexToUint8(kB)); const kid = key_rotation_timestamp.toString() + "-" + uint8ToBase64Url(new Uint8Array(kHash.slice(0, 16))); return { k: uint8ToBase64Url(bits), kty: "oct", kid, scope: identifier, }; } else { const bits = await derive( hexToUint8(uid), kB + key_rotation_secret, "identity.mozilla.com/picl/v1/scoped_key\n" + identifier, 16 + 32); const fp = new Uint8Array(bits.slice(0, 16)); const key = new Uint8Array(bits.slice(16)); const kid = key_rotation_timestamp.toString() + "-" + uint8ToBase64Url(fp); return { k: uint8ToBase64Url(key), kty: "oct", kid, scope: identifier, }; } } export async function encryptScopedKeys(bundle, to, local_key, iv) { let encode = s => new TextEncoder().encode(s); let peer_key = await crypto.subtle.importKey( "jwk", JSON.parse(urlatob(to)), { name: "ECDH", namedCurve: "P-256" }, false, ['deriveKey']); local_key = local_key || await crypto.subtle.generateKey( { name: "ECDH", namedCurve: "P-256" }, true, ['deriveKey']); iv = iv || new Uint8Array(await crypto.subtle.exportKey( "raw", await crypto.subtle.generateKey({ name: "AES-CBC", length: 128 }, true, ['encrypt']) )).slice(0, 12); let key = await crypto.subtle.deriveKey( { name: "ECDH", public: peer_key }, local_key.privateKey, { name: "AES-GCM", length: 256 }, true, ['encrypt']); key = new Uint8Array(await crypto.subtle.exportKey("raw", key)); key = await crypto.subtle.digest( "SHA-256", new Uint8Array([ 0, 0, 0, 1, // rounds ...key, 0, 0, 0, 7, ...encode("A256GCM"), 0, 0, 0, 0, // apu 0, 0, 0, 0, // apv 0, 0, 1, 0, // key size ])); key = await crypto.subtle.importKey("raw", key, { name: "AES-GCM" }, false, ['encrypt']); let exported_key = await crypto.subtle.exportKey("jwk", local_key.publicKey); let header = { "alg": "ECDH-ES", "enc": "A256GCM", "epk": { crv: "P-256", kty: "EC", x: exported_key.x, y: exported_key.y } }; let ciphered = await crypto.subtle.encrypt( { name: "AES-GCM", iv, additionalData: encode(uint8ToBase64Url(encode(JSON.stringify(header)))) }, key, encode(JSON.stringify(bundle))); let tag = ciphered.slice(-16); ciphered = ciphered.slice(0, -16); return (uint8ToBase64Url(encode(JSON.stringify(header))) + ".." + uint8ToBase64Url(new Uint8Array(iv)) + "." + uint8ToBase64Url(new Uint8Array(ciphered)) + "." + uint8ToBase64Url(new Uint8Array(tag))); }