// Host-only ceremonies. No world scripts or external dependencies run here. const bytes = text => Uint8Array.from(atob(text.replace(/-/g, '+').replace(/_/g, '/')), c => c.charCodeAt(0)); const encoded = buffer => btoa(String.fromCharCode(...new Uint8Array(buffer))).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, ''); const post = async (path, body) => { const response = await fetch(new URL(path.replace(/^\//, ""), import.meta.url), {method: 'POST', headers: {'content-type': 'application/json'}, body: JSON.stringify(body)}); const result = await response.json(); if (!response.ok) throw new Error(result.message); return result; }; function options(challenge) { const value = challenge.options; value.publicKey.challenge = bytes(value.publicKey.challenge); if (value.publicKey.user) value.publicKey.user.id = bytes(value.publicKey.user.id); for (const key of ['allowCredentials', 'excludeCredentials']) { for (const credential of value.publicKey[key] || []) credential.id = bytes(credential.id); } return value; } function serialize(credential) { const response = {}; for (const key of ['clientDataJSON', 'attestationObject', 'authenticatorData', 'signature', 'userHandle']) { if (credential.response[key]) response[key] = encoded(credential.response[key]); } return {id: credential.id, rawId: encoded(credential.rawId), type: credential.type, response, clientExtensionResults: credential.getClientExtensionResults()}; } async function assertion(challenge) { return {challenge_id: challenge.id, assertion: serialize(await navigator.credentials.get(options(challenge)))}; } async function encrypt(publicKey, secret) { const host = await crypto.subtle.importKey('raw', new Uint8Array(publicKey), {name: 'ECDH', namedCurve: 'P-256'}, false, []); const pair = await crypto.subtle.generateKey({name: 'ECDH', namedCurve: 'P-256'}, true, ['deriveBits']); const shared = await crypto.subtle.deriveBits({name: 'ECDH', public: host}, pair.privateKey, 256); const material = await crypto.subtle.importKey('raw', shared, 'HKDF', false, ['deriveKey']); const key = await crypto.subtle.deriveKey({name: 'HKDF', hash: 'SHA-256', salt: new Uint8Array(), info: new TextEncoder().encode('sausage/secret-entry/v1')}, material, {name: 'AES-GCM', length: 256}, false, ['encrypt']); const iv = crypto.getRandomValues(new Uint8Array(12)); const ciphertext = await crypto.subtle.encrypt({name: 'AES-GCM', iv}, key, new TextEncoder().encode(secret)); const ephemeral_public_key = new Uint8Array(await crypto.subtle.exportKey('raw', pair.publicKey)); const envelope = {ephemeral_public_key: [...ephemeral_public_key], iv: [...iv], ciphertext: [...new Uint8Array(ciphertext)]}; const hash = new Uint8Array(await crypto.subtle.digest('SHA-256', new Uint8Array([...envelope.ephemeral_public_key, ...envelope.iv, ...envelope.ciphertext]))); return {envelope, ciphertext_sha256: [...hash].map(byte => byte.toString(16).padStart(2, '0')).join('')}; } const form = document.querySelector('form'); const status = document.querySelector('[role=status]'); form.addEventListener('submit', async event => { event.preventDefault(); const buttons = [...form.querySelectorAll('button')]; buttons.forEach(button => button.disabled = true); status.textContent = 'Complete the passkey prompt, if one appears.'; try { const data = new FormData(form); if (document.body.dataset.page === 'approve') { const path = `/cred/requests/${encodeURIComponent(document.body.dataset.request)}`; const decision = event.submitter.value; const overrides = decision === 'deny' || document.body.dataset.grant !== 'true' ? {} : {duration_secs: Number(data.get('duration')), max_uses: data.get('uses') ? Number(data.get('uses')) : null}; const challenge = await post(`${path}/challenge`, {decision, overrides}); await post(`${path}/decide`, challenge.unlocked ? {decision, overrides} : await assertion(challenge)); } else if (document.body.dataset.page === 'register') { const label = data.get('label'); const state = await (await fetch(new URL('cred/passkeys', import.meta.url))).json(); let gate; if (state.length) { const proof = await assertion(await post('/cred/passkeys/register/challenge', {label})); gate = {id: proof.challenge_id, response: proof.assertion}; } const challenge = await post('/cred/passkeys/register/start', {label, gate}); const credential = await navigator.credentials.create(options(challenge)); await post('/cred/passkeys/register/finish', {challenge_id: challenge.id, response: serialize(credential)}); } else { const path = `/cred/items/${encodeURIComponent(document.body.dataset.item)}/fields/${encodeURIComponent(document.body.dataset.field)}`; const encrypted = await encrypt(JSON.parse(document.body.dataset.publicKey), data.get('secret')); form.elements.secret.value = ''; const challenge = await post(`${path}/challenge`, {ciphertext_sha256: encrypted.ciphertext_sha256}); await post(path, {...await assertion(challenge), envelope: encrypted.envelope}); } status.textContent = 'Complete.'; } catch { status.textContent = 'The request failed. Review the page and try again.'; } finally { buttons.forEach(button => button.disabled = false); } });