/** * 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 = `
Signed in as ${auth.username || auth.did?.slice(0, 16) + '...'}
`; document.getElementById('eid-signout').addEventListener('click', () => { logout(); render(); }); } else { container.innerHTML = `
`; const btn = document.getElementById('eid-signin'); btn.addEventListener('mouseenter', () => { btn.style.borderColor = 'var(--primary)'; btn.style.color = 'var(--primary)'; }); btn.addEventListener('mouseleave', () => { btn.style.borderColor = 'var(--border)'; btn.style.color = 'var(--text-dim)'; }); btn.addEventListener('click', async () => { btn.textContent = 'Authenticating...'; btn.disabled = true; try { await authenticate(); render(); } catch (e) { if (e.name === 'NotAllowedError') { // No passkey found — prompt to register const name = prompt('No passkey found. Create one?\nEnter a username:'); if (name) { try { await register(name.trim()); render(); } catch (re) { alert('Registration failed: ' + re.message); render(); } } else { render(); } } else { alert('Sign in failed: ' + e.message); render(); } } }); } } render(); } // ─── Public API ────────────────────────────────────────────── return { authenticate, register, logout, isAuthenticated, getUser, renderAuthButton, }; })();