import {
ALG,
base64decode,
getHeaders,
getSchema,
setRefresh,
setTokenSigningKey,
tryCompress
} from '/assets/{{ version }}/js/core.js';
const MAX_KEY_SYNC_DELAY_S = 60 * 30;
const MIN_KEY_SYNC_DELAY_S = 60 * 5;
const ORDINARY_DEVICE_ID = 'ORDINARY_DEVICE_ID';
async function registerKeysForAudience(audience, verifier, pub) {
return {
memberid: {
deviceid: [
new Uint8Array([]),
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) {
}
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);
}
}
}