// ============================================================================ // auth.jsx — Écrans d'authentification (login / register / unlock). // Toute la cryptographie est faite ici localement avant tout dialogue serveur. // ============================================================================ (function (global) { 'use strict'; const { useState, useRef, useEffect } = React; // ─── Helpers réseau + crypto ────────────────────────────────────────────── async function loginFlow(email, masterPassword) { const { kdf_salt, kdf_params } = await CPApi.salt(email); const { encKey, authKeyHex } = await CPCrypto.deriveKeys(masterPassword, kdf_salt, kdf_params); const r = await CPApi.login({ email, auth_key: authKeyHex }); let vault; try { vault = await CPCrypto.decryptJSON(encKey, r.vault_blob, r.vault_iv); } catch (e) { // Pas censé arriver après un /login OK, sauf coffre corrompu côté serveur. throw new Error('Le serveur a accepté l’authentification mais le coffre est illisible.'); } return { encKey, email: r.email, kdfSalt: r.kdf_salt, kdfParams: r.kdf_params, vault, vaultVersion: r.vault_version, }; } async function registerFlow(email, masterPassword) { const saltBytes = CPCrypto.randomBytes(16); const saltHex = CPCrypto.bytesToHex(saltBytes); const kdfParams = CPCrypto.DEFAULT_KDF_PARAMS; const { encKey, authKeyHex } = await CPCrypto.deriveKeys(masterPassword, saltHex, kdfParams); const vault = CPCrypto.makeEmptyVault(); const { blob, iv } = await CPCrypto.encryptJSON(encKey, vault); await CPApi.register({ email, kdf_salt: saltHex, kdf_params: kdfParams, auth_key: authKeyHex, vault_blob: blob, vault_iv: iv, }); return { encKey, email, kdfSalt: saltHex, kdfParams, vault, vaultVersion: 1 }; } // Déverrouillage avec session déjà valide : pas de /login, on dérive et on // tente de déchiffrer le coffre récupéré via /vault. async function unlockFlow(masterPassword, sessionInfo) { const { encKey } = await CPCrypto.deriveKeys( masterPassword, sessionInfo.kdfSalt, sessionInfo.kdfParams ); const v = await CPApi.vaultGet(); let vault; try { vault = await CPCrypto.decryptJSON(encKey, v.vault_blob, v.vault_iv); } catch { throw new Error('Mot de passe maître incorrect.'); } return { encKey, email: sessionInfo.email, kdfSalt: sessionInfo.kdfSalt, kdfParams: sessionInfo.kdfParams, vault, vaultVersion: v.vault_version, }; } // ─── Estimateur de force du mot de passe (simple, à l'inscription) ─────── function estimateStrength(pwd) { if (!pwd) return { score: 0, label: '—', color: '#666' }; let score = 0; if (pwd.length >= 8) score += 1; if (pwd.length >= 12) score += 1; if (pwd.length >= 16) score += 1; if (/[a-z]/.test(pwd) && /[A-Z]/.test(pwd)) score += 1; if (/\d/.test(pwd)) score += 1; if (/[^A-Za-z0-9]/.test(pwd)) score += 1; const labels = ['Très faible', 'Faible', 'Moyen', 'Bon', 'Très bon', 'Excellent', 'Excellent']; const colors = ['#EF4444', '#EF4444', '#F59E0B', '#F59E0B', '#10B981', '#10B981', '#10B981']; return { score, label: labels[score], color: colors[score] }; } // ─── Composant principal ────────────────────────────────────────────────── const AuthScreen = ({ initialMode = 'login', sessionInfo = null, onAuthenticated, onSwitchAccount }) => { const [mode, setMode] = useState(initialMode); // 'login' | 'register' | 'unlock' const [email, setEmail] = useState(sessionInfo?.email || ''); const [pwd, setPwd] = useState(''); const [pwd2, setPwd2] = useState(''); const [busy, setBusy] = useState(false); const [busyLabel, setBusyLabel] = useState(''); const [error, setError] = useState(''); const [showPwd, setShowPwd] = useState(false); const pwdRef = useRef(null); useEffect(() => { pwdRef.current?.focus(); }, [mode]); const strength = estimateStrength(pwd); async function submit(e) { e.preventDefault(); if (busy) return; setError(''); try { if (mode === 'register') { if (pwd.length < 12) return setError('Choisissez un mot de passe maître d’au moins 12 caractères.'); if (pwd !== pwd2) return setError('Les deux mots de passe ne correspondent pas.'); if (strength.score < 3) return setError('Mot de passe maître trop faible. Mélangez majuscules, chiffres et symboles.'); } if (!email && mode !== 'unlock') return setError('Email requis.'); if (!pwd) return setError('Mot de passe requis.'); setBusy(true); setBusyLabel('Dérivation Argon2id (peut prendre 1–2 secondes)…'); let result; if (mode === 'register') result = await registerFlow(email.trim(), pwd); else if (mode === 'unlock') result = await unlockFlow(pwd, sessionInfo); else result = await loginFlow(email.trim(), pwd); setBusyLabel(''); // On ne garde rien en clair dans le state une fois la clé dérivée. setPwd(''); setPwd2(''); onAuthenticated(result); } catch (err) { setError(err.message || 'Erreur inconnue'); } finally { setBusy(false); setBusyLabel(''); } } const titles = { login: { h: 'Bienvenue', s: 'Déverrouillez votre coffre.' }, register: { h: 'Créer un coffre', s: 'Choisissez un mot de passe maître. Il ne quittera jamais ce navigateur.' }, unlock: { h: 'Coffre verrouillé', s: 'Saisissez votre mot de passe maître pour ré-ouvrir le coffre.' }, }; const t = titles[mode]; return (
{t.s}
{mode !== 'unlock' && (