Remover bloqueio do cofre de senhas
This commit is contained in:
199
assets/app.js
199
assets/app.js
@@ -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 => ({"&":"&",
|
||||
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) {
|
||||
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("");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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); }
|
||||
|
||||
Reference in New Issue
Block a user