/** * EncryptID Authentication for rWallet.online * * Adds optional passkey-based identity to the static wallet explorer. * When authenticated, the user gets a persistent identity and can * associate wallet addresses with their account. */ const EncryptID = (() => { const SERVER = 'https://encryptid.jeffemmett.com'; const STORAGE_KEY = 'rwallet_encryptid'; // ─── Helpers ───────────────────────────────────────────────── function toBase64url(buffer) { return btoa(String.fromCharCode(...new Uint8Array(buffer))) .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=+$/g, ''); } function fromBase64url(str) { return Uint8Array.from( atob(str.replace(/-/g, '+').replace(/_/g, '/')), c => c.charCodeAt(0) ); } function getStoredAuth() { try { const raw = localStorage.getItem(STORAGE_KEY); return raw ? JSON.parse(raw) : null; } catch { return null; } } function setStoredAuth(auth) { localStorage.setItem(STORAGE_KEY, JSON.stringify(auth)); } function clearStoredAuth() { localStorage.removeItem(STORAGE_KEY); } // ─── Authentication ────────────────────────────────────────── async function authenticate() { // Step 1: Get challenge const startRes = await fetch(`${SERVER}/api/auth/start`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}), }); const { options } = await startRes.json(); // Step 2: WebAuthn ceremony const assertion = await navigator.credentials.get({ publicKey: { challenge: fromBase64url(options.challenge), rpId: options.rpId, userVerification: options.userVerification, timeout: options.timeout, allowCredentials: options.allowCredentials?.map(c => ({ type: c.type, id: fromBase64url(c.id), transports: c.transports, })), }, }); const response = assertion.response; // Step 3: Complete const completeRes = await fetch(`${SERVER}/api/auth/complete`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ challenge: options.challenge, credential: { credentialId: assertion.id, authenticatorData: toBase64url(response.authenticatorData), clientDataJSON: toBase64url(response.clientDataJSON), signature: toBase64url(response.signature), userHandle: response.userHandle ? toBase64url(response.userHandle) : null, }, }), }); const result = await completeRes.json(); if (!result.success) throw new Error(result.error || 'Authentication failed'); const auth = { token: result.token, did: result.did, username: result.username }; setStoredAuth(auth); return auth; } async function register(username) { // Step 1: Get registration options const startRes = await fetch(`${SERVER}/api/register/start`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, displayName: username }), }); const { options, userId } = await startRes.json(); // Step 2: WebAuthn ceremony const credential = await navigator.credentials.create({ publicKey: { challenge: fromBase64url(options.challenge), rp: options.rp, user: { id: fromBase64url(options.user.id), name: options.user.name, displayName: options.user.displayName, }, pubKeyCredParams: options.pubKeyCredParams, authenticatorSelection: options.authenticatorSelection, timeout: options.timeout, attestation: options.attestation, }, }); const response = credential.response; // Step 3: Complete const completeRes = await fetch(`${SERVER}/api/register/complete`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ challenge: options.challenge, userId, username, credential: { credentialId: credential.id, publicKey: toBase64url(response.getPublicKey?.() || response.attestationObject), attestationObject: toBase64url(response.attestationObject), clientDataJSON: toBase64url(response.clientDataJSON), transports: response.getTransports?.() || [], }, }), }); const result = await completeRes.json(); if (!result.success) throw new Error(result.error || 'Registration failed'); const auth = { token: result.token, did: result.did, username }; setStoredAuth(auth); return auth; } function logout() { clearStoredAuth(); } function isAuthenticated() { return !!getStoredAuth(); } function getUser() { return getStoredAuth(); } // ─── UI Component ──────────────────────────────────────────── /** * Render a passkey auth button into the specified container. * Shows sign-in when anonymous, username + sign-out when authenticated. */ function renderAuthButton(containerId) { const container = document.getElementById(containerId); if (!container) return; function render() { const auth = getStoredAuth(); if (auth) { container.innerHTML = `