diff --git a/README.md b/README.md index f2ad462..7d7d827 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,10 @@ Instruções rápidas para clonar o repositório e subir o ambiente via Docker. - Adminer (banco): http://localhost:8081 — host `db`, usuário `telseg`, senha `telseg`, base `telseg` - Postgres: porta 5432 exposta localmente (opcional) +## Sobre o Cofre de Senhas (HTTPS) +- O cofre usa WebCrypto do navegador (PBKDF2 + AES-GCM). Para funcionar, o navegador exige contexto seguro (HTTPS ou localhost). +- Se acessar via IP/HTTP (ex.: `http://192.168.x.x:4242`), o botão de desbloquear pode não responder. Use HTTPS (mesmo com certificado self-signed) ou acesse via `https://` atrás de um proxy reverso. + ## Parar os serviços - `docker compose down` para parar - `docker compose down -v` se também quiser descartar os volumes do Postgres (dados serão perdidos) diff --git a/assets/app.js b/assets/app.js index 136ea53..a50bd74 100644 --- a/assets/app.js +++ b/assets/app.js @@ -263,6 +263,17 @@ function hideModal(id) { const el = document.getElementById(id); el?.classList.a function escapeHTML(s) { return (s ?? "").replace(/[&<>"]/g, c => ({"&":"&","<":"<",">":">","\"":"""}[c])); } function escapeAttr(s) { return escapeHTML(s).replace(/'/g, "'"); } function formatDate(s) { try { const [y,m,d] = (s||"").split("-"); return d && m && y ? `${d}/${m}/${y}` : s; } catch { return s; } } + +// WebCrypto guard (requer HTTPS/localhost) +const CRYPTO_REQUIRE_MSG = "O cofre precisa de HTTPS (ou localhost) porque o navegador bloqueia a criptografia em origens inseguras."; +let cryptoWarned = false; +function hasCrypto() { return !!(globalThis.crypto && globalThis.crypto.subtle); } +function ensureCrypto(msgTarget) { + if (hasCrypto()) return true; + if (msgTarget) msgTarget.textContent = CRYPTO_REQUIRE_MSG; + if (!cryptoWarned) { alert(CRYPTO_REQUIRE_MSG); cryptoWarned = true; } + return false; +} // ------- Cofre de Senhas (WebCrypto) ------- async function refreshCreds() { if (state.useApi) { @@ -421,6 +432,7 @@ function b642ab(b64) { } async function deriveKey(password, saltB64, iterations) { + if (!hasCrypto()) throw new Error('crypto_unavailable'); const salt = b642ab(saltB64); const keyMaterial = await crypto.subtle.importKey( "raw", textEnc.encode(password), { name: "PBKDF2" }, false, ["deriveKey"] @@ -456,6 +468,11 @@ function renderVaultGate() { const status = $("#vaultStatus"); $("#vaultLocked").classList.remove("hidden"); $("#vaultUnlocked").classList.add("hidden"); + if (!ensureCrypto(status)) { + setForm.classList.add("hidden"); + unlockForm.classList.add("hidden"); + return; + } if (!meta) { status.textContent = "Proteja suas senhas com uma senha mestra."; setForm.classList.remove("hidden"); @@ -468,6 +485,7 @@ function renderVaultGate() { } async function unlockVault(password) { + if (!ensureCrypto($("#vaultStatus"))) return; const meta = getVault(); if (!meta) throw new Error("Cofre não configurado"); const key = await deriveKey(password, meta.salt, meta.iterations || VAULT_ITERS); @@ -805,6 +823,7 @@ document.addEventListener("DOMContentLoaded", async () => { $("#setVaultForm").addEventListener("submit", async (ev) => { ev.preventDefault(); + if (!ensureCrypto($("#vaultStatus"))) return; const p1 = $("#vaultPass1").value; const p2 = $("#vaultPass2").value; if (!p1 || p1 !== p2) { alert("Senhas não conferem."); return; }