/** * Ed25519 keypair: generate, store in localStorage, sign requests. * Public key exported as SPKI base64url for server verification. */ const STORAGE_KEY_PUBLIC = "builazoo_public_key"; const STORAGE_KEY_PRIVATE = "builazoo_private_key"; /** * @returns {Promise} */ function generateKeyPair() { return crypto.subtle.generateKey( { name: "Ed25519" }, true, ["sign", "verify"] ); } /** * @param {ArrayBuffer} buf * @returns {string} */ function base64url(buf) { const b64 = btoa(String.fromCharCode(...new Uint8Array(buf))); return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); } /** * @returns {Promise<{ publicKeyBase64: string, privateKey: CryptoKey } | null>} */ export async function getOrCreateKeyPair() { if (typeof crypto === "undefined" || !crypto.subtle) { throw new Error("Connexion sécurisée requise (HTTPS ou localhost) pour créer un compte."); } try { const pubRaw = localStorage.getItem(STORAGE_KEY_PUBLIC); const privRaw = localStorage.getItem(STORAGE_KEY_PRIVATE); if (pubRaw && privRaw) { const privateKey = await crypto.subtle.importKey( "pkcs8", base64urlDecodeToBuf(privRaw), { name: "Ed25519" }, true, ["sign"] ); await crypto.subtle.importKey( "spki", base64urlDecodeToBuf(pubRaw), { name: "Ed25519" }, true, ["verify"] ); return { publicKeyBase64: pubRaw, privateKey }; } const pair = await generateKeyPair(); const [pubExported, privExported] = await Promise.all([ crypto.subtle.exportKey("spki", pair.publicKey), crypto.subtle.exportKey("pkcs8", pair.privateKey), ]); localStorage.setItem(STORAGE_KEY_PUBLIC, base64url(pubExported)); localStorage.setItem(STORAGE_KEY_PRIVATE, base64url(privExported)); return { publicKeyBase64: base64url(pubExported), privateKey: pair.privateKey }; } catch (e) { console.warn("getOrCreateKeyPair failed", e); return null; } } /** * @param {string} str base64url * @returns {ArrayBuffer} */ function base64urlDecodeToBuf(str) { const pad = (4 - (str.length % 4)) % 4; const b64 = (str + "==".slice(0, pad)).replace(/-/g, "+").replace(/_/g, "/"); const binary = atob(b64); const buf = new Uint8Array(binary.length); for (let i = 0; i < binary.length; i++) buf[i] = binary.charCodeAt(i); return buf.buffer; } /** * @param {CryptoKey} privateKey * @param {string} message * @returns {Promise} base64url signature */ export async function signMessage(privateKey, message) { const enc = new TextEncoder().encode(message); const sig = await crypto.subtle.sign("Ed25519", privateKey, enc); return base64url(sig); } /** * Call after register to ensure we have keys stored (already done by getOrCreateKeyPair). * @returns {boolean} true if keys exist */ export function hasStoredKeys() { return Boolean(localStorage.getItem(STORAGE_KEY_PUBLIC) && localStorage.getItem(STORAGE_KEY_PRIVATE)); }