hammertime 0.5.49

Build & codegen tool for Ordinary
Documentation
import {
    ALG,
    base64decode,
    getHeaders,
    getSchema,
    setRefresh,
    setTokenSigningKey,
    tryCompress
} from '/assets/{{ version }}/js/core.js';

// ?? key poll rate should also be configurable
const MAX_KEY_SYNC_DELAY_S = 60 * 30;
const MIN_KEY_SYNC_DELAY_S = 60 * 5;

// !! should never be included in events
const ORDINARY_DEVICE_ID = 'ORDINARY_DEVICE_ID';

// audience > channel > member > device

async function registerKeysForAudience(audience, verifier, pub) {
    // todo: post keys up to app
    // !! receiving back the public keys for this audience

    return {
        // uuid for the member in this audience
        memberid: {
            // uuid for device it was sent from
            deviceid: [
                // verifier
                new Uint8Array([]),
                // pub 
                new Uint8Array([]),
            ],
        },
    };
}

function extractFromMembers(pubKey, members) {
    const memberIds = Object.keys(members);

    let ids;
    let keys = [];

    for (let i = 0; i < memberIds.length; i += 1) {
        const deviceIds = Object.keys(members[memberIds[i]]);

        for (let j = 0; j < deviceIds.length; j += 1) {
            keys = [...keys, ...deviceIds[i][1]];

            if (deviceIds[i][1] === pubKey) {
                ids = {
                    member: memberIds[i],
                    device: deviceIds[j],
                }
            }
        }
    }

    return {ids, keys}
}

function getAudienceSendKeys(audience) {
    return new Promise((res) => {
        const keysDB = indexedDB.open("OrdinaryKeys", 1);

        keysDB.onerror = (evt) => {
            console.error(evt);
        };

        keysDB.onupgradeneeded = (evt) => {
            const db = evt.target.result;
            db.createObjectStore("audiences", {keyPath: "id"});

            eventsStore.createIndex("updated_at", "updated_at", {unique: false});
        };

        keysDB.onsuccess = (evt) => {
            const db = evt.target.result;

            const audienceStore = db
                .transaction("audiences", "readwrite")
                .objectStore("audiences");

            audienceStore.get(audience);

            audienceStore.onerror = async (evt) => {
                console.error(evt);

                const {fingerprint, verifier} = generate_fingerprint();
                const {private: priv, public: pub} = generate_dh_keys();

                const members = await registerKeysForAudience(audience, verifier, pub);

                audienceStore.put(audience, {
                    members,
                    device: [fingerprint, verifier, priv, pub],
                    updated_at: new Date().valueOf(),
                });

                const {ids, keys: pubKeys} = extractFromMembers(pub, members);

                res({
                    ids,
                    audiencePubKeys: pubKeys,
                    devicePrivKeys: [fingerprint, priv],
                });
            }

            audienceStore.onsuccess = (evt) => {
                const {members, device} = evt.result.value;
                const {ids, keys: pubKeys} = extractFromMembers(pub, members);


                res({
                    ids,
                    audiencePubKeys: pubKeys,
                    devicePrivKeys: [device[0], device[2]],
                });
            }
        }
    });
}

export async function obfuscate(value) {
    await __wbg_init();

    let kind = 0;
    let bytes = value;

    if (typeof value == 'string') {
        const encoder = new TextEncoder();
        kind = 1;
        bytes = encoder.encode(value);
    }

    const {
        ids: {member, device},
        devicePrivKeys: [fingerprint, privKey],
        audiencePubKeys
    } = await getAudienceSendKeys();

    const obfuscated = encrypt_dh(
        fingerprint,
        bytes,
        privKey,
        new Uint8Array([...audiencePubKeys.flat()])
    );

    return new Uint8Array([...member, ...device, kind, ...obfuscated])
}

export async function reveal(value) {
    // todo

    // ?? channel, member and device keys can/should be ratcheted
}

async function hash(
    domain,
    account,
    password,
    mfa_code,
) {
    const encoder = new TextEncoder();

    const encoded_domain = encoder.encode(domain);
    const encoded_account = encoder.encode(account);
    const encoded_password = encoder.encode(password);

    const all = new Uint8Array([...encoded_domain, ...encoded_account, ...encoded_password]);

    const schema = await getSchema();

    const alg = ALG[schema.auth.client_hash]
    const password_hash = await crypto.subtle.digest(alg, all);

    if (mfa_code) {
        const encoded_mfa_code = encoder.encode(mfa_code);
        const mfa_all = new Uint8Array([...encoded_domain, ...encoded_account, ...encoded_mfa_code]);

        const mfa_hash = await crypto.subtle.digest(alg, mfa_all);

        return [new Uint8Array(password_hash), new Uint8Array(mfa_hash)];
    } else {
        return new Uint8Array(password_hash);
    }
}

export async function register(
    account,
    password,
    domain,
    invite_token = null,
) {
    const schema = await getSchema();
    if (schema.auth.invite && !invite_token) throw new Error('invite token must be included');

    await __wbg_init();

    const password_hash = await hash(domain, account, password);
    const start_req = registration_start_req(
        account,
        password_hash,
        invite_token ? base64decode(invite_token) : invite_token,
    );

    const start_res = await fetch('/accounts/registration/start', {
        method: 'PUT',
        body: start_req.request,
        headers: await getHeaders({cookies: false}),
    });
    const server_message = await start_res.bytes();

    const finish_req = registration_finish_req(
        account, password_hash, start_req.client_state, server_message
    );

    const finish_res = await fetch('/accounts/registration/finish', {
        method: 'PUT',
        body: finish_req.request,
        headers: await getHeaders({cookies: false}),
    });
    const encrypted_totp_mfa = await finish_res.bytes();

    const decrypted = decrypt_totp_mfa(encrypted_totp_mfa, finish_req.private_key, domain, account);

    const [recovery_codes_str, mfa_svg] = decrypted.split('__');
    const recovery_codes = recovery_codes_str.match(/.{1,11}/g);

    return [mfa_svg, recovery_codes];
}

export async function login(
    account,
    password,
    mfa_code,
    domain,
) {
    await __wbg_init();

    const [password_hash, mfa_hash] = await hash(domain, account, password, mfa_code);
    const start_req = login_start_req(account, password_hash);

    const start_res = await fetch('/accounts/login/start', {
        method: 'PUT',
        body: start_req.request,
        headers: await getHeaders({cookies: false}),
    });
    const server_message = await start_res.bytes();

    const keypair = await crypto.subtle.generateKey({name: "Ed25519"}, true, ["sign", "verify"]);

    const pubKeyBytes = new Uint8Array(
        await crypto.subtle.exportKey("raw", keypair.publicKey),
    );

    const privKeyBytes = new Uint8Array(
        await crypto.subtle.exportKey("pkcs8", keypair.privateKey),
    );

    setTokenSigningKey(privKeyBytes);

    const finish_req = login_finish_req(
        account, password_hash, mfa_hash, start_req.client_state, server_message, pubKeyBytes
    );

    const finish_res = await fetch('/accounts/login/finish', {
        method: 'PUT',
        body: finish_req.request,
        credentials: 'omit',
        headers: await getHeaders({cookies: false}),
    });
    const encrypted_token = await finish_res.bytes();

    const refresh_token = decrypt_token(encrypted_token, finish_req.session_key);

    setRefresh(refresh_token);
}

export async function invoke(actionId, payload) {
    await __wbg_init();

    const schema = await getSchema();

    for (let i = 0; i < schema.actions.length; i += 1) {
        const action = schema.actions[i];

        if ((action.name === actionId || action.idx === actionId) && action.triggered_by.includes("Ordinary")) {
            const req = invoke_req(
                JSON.stringify(payload),
                JSON.stringify(action.accepts),
            );

            const send = await tryCompress(req, 'application/octet-stream');

            const res = await fetch(`/action/invoke/${action.idx}`, {
                method: 'PUT',
                body: send.body,
                headers: {
                    ...(await getHeaders({cookies: false, authorization: action.protected})),
                    ...send.headers,
                },
            });

            const res_bytes = await res.bytes();

            const res_json = invoke_res(JSON.stringify(action.returns), res_bytes);
            return JSON.parse(res_json);
        }
    }
}