Add HTTPS warning for vault and guard crypto availability

This commit is contained in:
Tiago
2025-11-29 21:04:27 -03:00
parent d3d89291a4
commit e06cb798c6
2 changed files with 23 additions and 0 deletions

View File

@@ -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` - Adminer (banco): http://localhost:8081 — host `db`, usuário `telseg`, senha `telseg`, base `telseg`
- Postgres: porta 5432 exposta localmente (opcional) - 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 ## Parar os serviços
- `docker compose down` para parar - `docker compose down` para parar
- `docker compose down -v` se também quiser descartar os volumes do Postgres (dados serão perdidos) - `docker compose down -v` se também quiser descartar os volumes do Postgres (dados serão perdidos)

View File

@@ -263,6 +263,17 @@ function hideModal(id) { const el = document.getElementById(id); el?.classList.a
function escapeHTML(s) { return (s ?? "").replace(/[&<>"]/g, c => ({"&":"&amp;","<":"&lt;",">":"&gt;","\"":"&quot;"}[c])); } function escapeHTML(s) { return (s ?? "").replace(/[&<>"]/g, c => ({"&":"&amp;","<":"&lt;",">":"&gt;","\"":"&quot;"}[c])); }
function escapeAttr(s) { return escapeHTML(s).replace(/'/g, "&#039;"); } function escapeAttr(s) { return escapeHTML(s).replace(/'/g, "&#039;"); }
function formatDate(s) { try { const [y,m,d] = (s||"").split("-"); return d && m && y ? `${d}/${m}/${y}` : s; } catch { return s; } } 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) ------- // ------- Cofre de Senhas (WebCrypto) -------
async function refreshCreds() { async function refreshCreds() {
if (state.useApi) { if (state.useApi) {
@@ -421,6 +432,7 @@ function b642ab(b64) {
} }
async function deriveKey(password, saltB64, iterations) { async function deriveKey(password, saltB64, iterations) {
if (!hasCrypto()) throw new Error('crypto_unavailable');
const salt = b642ab(saltB64); const salt = b642ab(saltB64);
const keyMaterial = await crypto.subtle.importKey( const keyMaterial = await crypto.subtle.importKey(
"raw", textEnc.encode(password), { name: "PBKDF2" }, false, ["deriveKey"] "raw", textEnc.encode(password), { name: "PBKDF2" }, false, ["deriveKey"]
@@ -456,6 +468,11 @@ function renderVaultGate() {
const status = $("#vaultStatus"); const status = $("#vaultStatus");
$("#vaultLocked").classList.remove("hidden"); $("#vaultLocked").classList.remove("hidden");
$("#vaultUnlocked").classList.add("hidden"); $("#vaultUnlocked").classList.add("hidden");
if (!ensureCrypto(status)) {
setForm.classList.add("hidden");
unlockForm.classList.add("hidden");
return;
}
if (!meta) { if (!meta) {
status.textContent = "Proteja suas senhas com uma senha mestra."; status.textContent = "Proteja suas senhas com uma senha mestra.";
setForm.classList.remove("hidden"); setForm.classList.remove("hidden");
@@ -468,6 +485,7 @@ function renderVaultGate() {
} }
async function unlockVault(password) { async function unlockVault(password) {
if (!ensureCrypto($("#vaultStatus"))) return;
const meta = getVault(); const meta = getVault();
if (!meta) throw new Error("Cofre não configurado"); if (!meta) throw new Error("Cofre não configurado");
const key = await deriveKey(password, meta.salt, meta.iterations || VAULT_ITERS); const key = await deriveKey(password, meta.salt, meta.iterations || VAULT_ITERS);
@@ -805,6 +823,7 @@ document.addEventListener("DOMContentLoaded", async () => {
$("#setVaultForm").addEventListener("submit", async (ev) => { $("#setVaultForm").addEventListener("submit", async (ev) => {
ev.preventDefault(); ev.preventDefault();
if (!ensureCrypto($("#vaultStatus"))) return;
const p1 = $("#vaultPass1").value; const p1 = $("#vaultPass1").value;
const p2 = $("#vaultPass2").value; const p2 = $("#vaultPass2").value;
if (!p1 || p1 !== p2) { alert("Senhas não conferem."); return; } if (!p1 || p1 !== p2) { alert("Senhas não conferem."); return; }