Remover bloqueio do cofre de senhas

This commit is contained in:
Tiago
2025-12-17 16:47:15 -03:00
parent 31d0b30365
commit 813f986344
3 changed files with 32 additions and 212 deletions

View File

@@ -4,7 +4,6 @@
const STORAGE = {
UNITS: "telseg_units",
CREDS: "telseg_passwords",
VAULT: "telseg_vault",
THEME: "telseg_theme",
NOTES: "telseg_notes",
AUTH: "telseg_auth",
@@ -14,7 +13,7 @@ const $ = (sel, el = document) => el.querySelector(sel);
const $$ = (sel, el = document) => Array.from(el.querySelectorAll(sel));
// Detecção de API
const state = { useApi: false, units: [], creds: [], vault: null, notes: [], auth: null };
const state = { useApi: false, units: [], creds: [], notes: [], auth: null };
async function probeApi() {
try {
@@ -264,17 +263,6 @@ function escapeHTML(s) { return (s ?? "").replace(/[&<>"]/g, c => ({"&":"&amp;",
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; } }
// 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) {
const r = await apiFetch('/api/creds');
@@ -286,18 +274,7 @@ async function refreshCreds() {
}
function loadCreds() { return state.creds || []; }
function saveCreds(list) { saveJSON(STORAGE.CREDS, list); state.creds = list; }
async function refreshVault() {
if (state.useApi) {
const r = await apiFetch('/api/vault');
state.vault = r.ok ? await r.json() : null;
} else {
state.vault = loadJSON(STORAGE.VAULT, null);
}
return state.vault;
}
function getVault() { return state.vault; }
function setVault(v) { saveJSON(STORAGE.VAULT, v); state.vault = v; }
function getCredPassword(cred) { return cred?.passwordEnc ?? ""; }
// ------- Notas -------
async function refreshNotes() {
@@ -417,97 +394,6 @@ function openNoteView(n) {
showModal('noteViewModal');
}
const textEnc = new TextEncoder();
const textDec = new TextDecoder();
function ab2b64(ab) {
const bytes = new Uint8Array(ab);
let bin = ""; for (let i=0; i<bytes.length; i++) bin += String.fromCharCode(bytes[i]);
return btoa(bin);
}
function b642ab(b64) {
const bin = atob(b64); const len = bin.length; const bytes = new Uint8Array(len);
for (let i=0; i<len; i++) bytes[i] = bin.charCodeAt(i);
return bytes.buffer;
}
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"]
);
return crypto.subtle.deriveKey(
{ name: "PBKDF2", salt, iterations, hash: "SHA-256" },
keyMaterial,
{ name: "AES-GCM", length: 256 },
false,
["encrypt", "decrypt"]
);
}
async function encryptString(key, plain) {
const iv = crypto.getRandomValues(new Uint8Array(12));
const ct = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, key, textEnc.encode(plain));
return { iv: ab2b64(iv), data: ab2b64(ct) };
}
async function decryptString(key, enc) {
const iv = new Uint8Array(b642ab(enc.iv));
const data = b642ab(enc.data);
const pt = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, key, data);
return textDec.decode(pt);
}
let vaultKey = null;
const VAULT_ITERS = 200_000;
function renderVaultGate() {
const meta = getVault();
const setForm = $("#setVaultForm");
const unlockForm = $("#unlockVaultForm");
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");
unlockForm.classList.add("hidden");
} else {
status.textContent = "Informe sua senha mestra para desbloquear.";
setForm.classList.add("hidden");
unlockForm.classList.remove("hidden");
}
}
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);
try {
const check = await decryptString(key, meta.verification);
if (check !== "ok") throw new Error("Senha inválida");
vaultKey = key; // mantido em memória
$("#vaultLocked").classList.add("hidden");
$("#vaultUnlocked").classList.remove("hidden");
renderCreds($("#searchCreds").value);
} catch {
alert("Senha mestra incorreta.");
}
}
function lockVault() {
vaultKey = null;
$("#vaultUnlockPass").value = "";
$("#credsList").innerHTML = "";
renderVaultGate();
}
async function renderCreds(filter = "") {
if (state.useApi) await refreshCreds();
const listEl = $("#credsList");
@@ -544,17 +430,19 @@ async function renderCreds(filter = "") {
const c = items2[idx];
if (!c) return;
if (btn) {
// Mantido por compatibilidade caso algum botão exista
if (!vaultKey) { alert("Cofre bloqueado."); return; }
const act = btn.dataset.act;
if (act === "open" && c.url) {
window.open(c.url, "_blank", "noopener");
} else if (act === "copyuser" && c.usuario) {
copyText(c.usuario);
} else if (act === "copypass") {
try { const pass = await decryptString(vaultKey, c.passwordEnc); copyText(pass); } catch { alert("Falha ao descriptografar."); }
const pass = getCredPassword(c);
if (!pass) { alert("Senha indisponível."); return; }
copyText(pass);
} else if (act === "reveal") {
try { const pass = await decryptString(vaultKey, c.passwordEnc); alert(`Senha: ${pass}`); } catch { alert("Falha ao descriptografar."); }
const pass = getCredPassword(c);
if (!pass) { alert("Senha indisponível."); return; }
alert(`Senha: ${pass}`);
} else if (act === "edit") {
openCredModal(c);
} else if (act === "del") {
@@ -569,8 +457,6 @@ async function renderCreds(filter = "") {
}
}
} else {
// Clique no card abre visualização
if (!vaultKey) { alert("Cofre bloqueado."); return; }
openCredView(c, canWrite);
}
};
@@ -580,7 +466,7 @@ function openCredModal(c) {
$("#credId").value = c?.id || "";
$("#credNome").value = c?.nome || "";
$("#credUsuario").value = c?.usuario || "";
$("#credSenha").value = ""; // nunca preencher
$("#credSenha").value = getCredPassword(c);
$("#credUrl").value = c?.url || "";
$("#credNotas").value = c?.notas || "";
showModal("credModal");
@@ -616,11 +502,15 @@ function openCredView(c, canWrite) {
btnOpen.onclick = () => { if (c.url) window.open(c.url, '_blank', 'noopener'); };
btnCopyUser.onclick = () => { if (c.usuario) copyText(c.usuario); };
btnCopyPass.onclick = async () => {
try { const pass = await decryptString(vaultKey, c.passwordEnc); copyText(pass); } catch { alert('Falha ao descriptografar.'); }
btnCopyPass.onclick = () => {
const pass = getCredPassword(c);
if (!pass) { alert('Senha indisponível.'); return; }
copyText(pass);
};
btnReveal.onclick = async () => {
try { const pass = await decryptString(vaultKey, c.passwordEnc); alert(`Senha: ${pass}`); } catch { alert('Falha ao descriptografar.'); }
btnReveal.onclick = () => {
const pass = getCredPassword(c);
if (!pass) { alert('Senha indisponível.'); return; }
alert(`Senha: ${pass}`);
};
btnEdit.onclick = () => { hideModal('credViewModal'); openCredModal(c); };
btnDelete.onclick = async () => {
@@ -647,7 +537,6 @@ function exportAll() {
exportedAt: new Date().toISOString(),
theme: document.documentElement.getAttribute("data-theme"),
units: loadUnits(),
vault: getVault(),
creds: loadCreds(),
notes: loadNotes(),
};
@@ -672,12 +561,10 @@ function importAll(file) {
if (data.theme) { localStorage.setItem(STORAGE.THEME, data.theme); document.documentElement.setAttribute("data-theme", data.theme); }
if (Array.isArray(data.units)) saveUnits(data.units);
if (Array.isArray(data.creds)) saveCreds(data.creds);
if (data.vault && data.vault.salt && data.vault.verification) setVault(data.vault);
if (Array.isArray(data.notes)) saveNotes(data.notes);
alert("Importação concluída.");
renderUnits($("#searchUnidades").value);
renderNotes($("#searchNotes").value);
lockVault(); // força rechecagem do cofre
} catch (e) {
alert("Arquivo inválido.");
}
@@ -706,7 +593,7 @@ document.addEventListener("DOMContentLoaded", async () => {
}
}
if (!state.useApi || state.auth?.user) {
await Promise.all([refreshUnits(), refreshCreds(), refreshVault(), refreshNotes()]);
await Promise.all([refreshUnits(), refreshCreds(), refreshNotes()]);
}
updateStorageInfo();
applyPermissionsUI();
@@ -757,14 +644,9 @@ document.addEventListener("DOMContentLoaded", async () => {
if (btn) hideModal(btn.getAttribute("data-close-modal"));
});
// Senhas/Cofre
renderVaultGate();
$("#lockVaultBtn").addEventListener("click", lockVault);
// Senhas
$("#searchCreds").addEventListener("input", ev => renderCreds(ev.target.value));
$("#addCredBtn").addEventListener("click", () => {
if (!vaultKey) { alert("Desbloqueie o cofre primeiro."); return; }
openCredModal();
});
$("#addCredBtn").addEventListener("click", () => openCredModal());
// Notas
$("#addNoteBtn").addEventListener("click", () => openNoteModal());
@@ -821,50 +703,19 @@ document.addEventListener("DOMContentLoaded", async () => {
renderNotes($("#searchNotes").value);
});
$("#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; }
const salt = ab2b64(crypto.getRandomValues(new Uint8Array(16)).buffer);
const key = await deriveKey(p1, salt, VAULT_ITERS);
const verification = await encryptString(key, "ok");
const meta = { salt, verification, iterations: VAULT_ITERS, v: 1 };
if (state.useApi) {
await apiFetch('/api/vault', { method: 'PUT', body: JSON.stringify(meta) });
await refreshVault();
} else {
setVault(meta);
}
alert("Cofre configurado. Guarde sua senha mestra!");
$("#vaultPass1").value = ""; $("#vaultPass2").value = "";
unlockVault(p1);
});
$("#unlockVaultForm").addEventListener("submit", async (ev) => {
ev.preventDefault();
const p = $("#vaultUnlockPass").value;
await unlockVault(p);
});
$("#credForm").addEventListener("submit", async (ev) => {
ev.preventDefault();
if (!vaultKey) { alert("Cofre bloqueado."); return; }
const id = $("#credId").value || uid();
const list = loadCreds();
const idx = list.findIndex(x => x.id === id);
const existing = idx >= 0 ? list[idx] : null;
const passwordPlain = $("#credSenha").value; // pode estar vazio ao editar
let passwordEnc = existing?.passwordEnc;
if (passwordPlain) passwordEnc = await encryptString(vaultKey, passwordPlain);
const passwordPlain = $("#credSenha").value.trim();
const item = {
id,
nome: $("#credNome").value.trim(),
usuario: $("#credUsuario").value.trim(),
url: $("#credUrl").value.trim(),
notas: $("#credNotas").value.trim(),
passwordEnc,
passwordEnc: passwordPlain,
updatedAt: Date.now(),
};
if (!item.nome) { alert("Informe o nome."); return; }
@@ -901,8 +752,7 @@ document.addEventListener("DOMContentLoaded", async () => {
const data = JSON.parse(text);
if (!confirm("Importar backup e substituir dados atuais no servidor?")) return;
await apiFetch('/api/import', { method: 'POST', body: JSON.stringify(data) });
await Promise.all([refreshUnits(), refreshCreds(), refreshVault(), refreshNotes()]);
lockVault();
await Promise.all([refreshUnits(), refreshCreds(), refreshNotes()]);
renderUnits($("#searchUnidades").value); // re-render
renderNotes($("#searchNotes").value);
} else {
@@ -926,7 +776,7 @@ document.addEventListener("DOMContentLoaded", async () => {
state.auth = { user: data.user };
hideLogin();
applyPermissionsUI();
await Promise.all([refreshUnits(), refreshCreds(), refreshVault(), refreshNotes()]);
await Promise.all([refreshUnits(), refreshCreds(), refreshNotes()]);
if (canMaster()) await renderUsers();
renderUnits(document.getElementById('searchUnidades').value);
renderNotes(document.getElementById('searchNotes').value);
@@ -940,6 +790,7 @@ document.addEventListener("DOMContentLoaded", async () => {
if (!state.useApi || state.auth?.user) {
renderUnits("");
renderNotes("");
renderCreds("");
}
});

View File

@@ -135,8 +135,6 @@ label span { display: block; font-size: 13px; color: var(--muted); margin-bottom
.modal-body { padding: 14px; display: grid; gap: 12px; }
.modal-foot { display: flex; justify-content: flex-end; gap: 8px; margin-top: 6px; }
.vault-lock { background: var(--panel); border: 1px solid var(--border); border-radius: 12px; padding: 16px; display: grid; gap: 10px; max-width: 720px; margin: 18px auto; }
.vault-form { display: grid; gap: 12px; }
.hidden { display: none !important; }
.app-footer { padding: 18px; text-align: center; color: var(--muted); }