From 2f8dce44d3f2be74b5c6ec0a2e7f4ceced715328 Mon Sep 17 00:00:00 2001 From: pennae Date: Wed, 13 Jul 2022 10:33:30 +0200 Subject: initial import --- web/js/crypto.js | 137 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 web/js/crypto.js (limited to 'web/js/crypto.js') diff --git a/web/js/crypto.js b/web/js/crypto.js new file mode 100644 index 0000000..5c12a2c --- /dev/null +++ b/web/js/crypto.js @@ -0,0 +1,137 @@ +'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))); +} -- cgit v1.2.3