Password Vault
<!-- Unlock Screen -->
<div id="unlock-screen" class="flex items-center justify-center min-h-screen">
<div class="w-full max-w-sm bg-gray-800 rounded-xl shadow-2xl p-6 md:p-8">
<h1 class="text-3xl font-bold text-white text-center mb-2">Unlock Vault</h1>
<div class="flex justify-center items-center my-4 h-[100px] w-[100px] mx-auto">
<svg id="password-key" xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round" class="text-gray-600">
<path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"></path>
</svg>
<canvas id="password-identicon" width="100" height="100" class="bg-gray-700 rounded-lg hidden"></canvas>
</div>
<form id="unlock-form" class="space-y-4">
<div>
<label for="unlock-master-password" class="block text-sm font-medium text-gray-300 mb-1">Master Password</label>
<input type="password" id="unlock-master-password" required class="form-control bg-gray-700 border border-gray-600 text-white w-full rounded-lg px-4 py-2 focus:outline-none transition">
</div>
<p id="unlock-error" class="text-red-400 text-sm text-center hidden">Invalid Master Password.</p>
<button type="submit" class="w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 px-4 rounded-lg transition">Unlock</button>
</form>
</div>
</div>
<!-- Main App Screen -->
<div id="main-app" class="hidden container mx-auto max-w-6xl">
<header class="flex justify-between items-center mb-6">
<div class="flex items-center gap-2">
<h1 class="text-2xl md:text-3xl font-bold text-white">Password Vault</h1>
<button id="info-icon" class="text-gray-400 hover:text-white transition" aria-label="About this generator">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>
</button>
</div>
<button id="lock-btn" class="bg-red-600 hover:bg-red-700 text-white font-bold py-2 px-4 rounded-lg transition">Lock</button>
</header>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
<!-- Password Generator Section -->
<section id="password-generator-section" class="bg-gray-800 rounded-xl shadow-2xl p-6 md:p-8">
<h2 class="text-2xl font-bold text-white mb-6">Password Generator</h2>
<form id="password-form" class="space-y-6">
<div>
<label for="domain" class="block text-sm font-medium text-gray-300 mb-1">Domain or Service</label>
<input type="text" id="domain" required placeholder="e.g., google.com" class="form-control bg-gray-700 border border-gray-600 text-white w-full rounded-lg px-4 py-2 focus:outline-none transition">
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<label for="length" class="block text-sm font-medium text-gray-300 mb-1">Length</label>
<input type="number" id="length" value="10" min="8" max="64" class="form-control bg-gray-700 border border-gray-600 text-white w-full rounded-lg px-4 py-2 focus:outline-none transition">
</div>
<div>
<label for="algorithm" class="block text-sm font-medium text-gray-300 mb-1">Algorithm</label>
<select id="algorithm" class="form-control bg-gray-700 border border-gray-600 text-white w-full rounded-lg px-4 py-2 focus:outline-none transition appearance-none" style="background-image: url('data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fs vg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%239CA3AF%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E'); background-repeat: no-repeat; background-position: right .7rem center; background-size: .65em auto;">
<option value="md5" selected>MD5</option>
<option value="sha512">SHA-512</option>
</select>
</div>
</div>
<button type="submit" id="generate-btn" class="w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 px-4 rounded-lg transition">Generate Password</button>
</form>
<div id="result-container" class="mt-6 hidden">
<label class="block text-sm font-medium text-gray-300 mb-1">Generated Password</label>
<div class="relative"><input readonly id="result" class="w-full bg-gray-900 text-white font-mono rounded-lg pl-4 pr-12 py-3 text-center tracking-wider"><button id="copy-btn" class="absolute inset-y-0 right-0 flex items-center px-4 text-gray-400 hover:text-white transition group"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg><span id="copy-tooltip" class="absolute -top-10 right-0 bg-gray-900 text-white text-xs rounded-md px-2 py-1 opacity-0 group-hover:opacity-100 transition-opacity">Copy</span></button></div>
</div>
</section>
<!-- Right side with Tabs -->
<section class="bg-gray-800 rounded-xl shadow-2xl p-6 md:p-8">
<div class="flex border-b border-gray-700 mb-4">
<button id="show-totp-tab" class="tab-btn active font-semibold py-2 px-4 border-b-2 border-transparent text-gray-400 hover:text-white transition">Authenticator</button>
<button id="show-logins-tab" class="tab-btn font-semibold py-2 px-4 border-b-2 border-transparent text-gray-400 hover:text-white transition">Logins</button>
</div>
<!-- TOTP Section -->
<div id="totp-section">
<div class="flex justify-between items-center mb-6">
<h2 class="text-2xl font-bold text-white">Authenticator (TOTP)</h2>
<button id="add-totp-btn" class="bg-green-600 hover:bg-green-700 text-white font-bold py-2 px-4 rounded-lg transition">+ Add</button>
</div>
<div id="totp-list" class="space-y-4">
<!-- TOTP items will be injected here -->
</div>
<p id="no-totp-message" class="text-center text-gray-400 py-4">No TOTP secrets saved yet.</p>
</div>
<!-- Logins Section -->
<div id="logins-section" class="hidden">
<div class="flex justify-between items-center mb-6">
<h2 class="text-2xl font-bold text-white">Saved Logins</h2>
<button id="add-login-btn" class="bg-green-600 hover:bg-green-700 text-white font-bold py-2 px-4 rounded-lg transition">+ Add</button>
</div>
<div id="logins-list" class="space-y-4">
<!-- Login items will be injected here -->
</div>
<p id="no-logins-message" class="text-center text-gray-400 py-4">No logins saved yet.</p>
</div>
</section>
</div>
</div>
<!-- FAB for Settings -->
<button id="fab" class="fab bg-blue-600 hover:bg-blue-700 text-white rounded-full p-4 shadow-lg focus:outline-none hidden">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>
</button>
<!-- Modals -->
<div id="settings-modal" class="modal-overlay hidden">
<div class="bg-gray-800 w-full max-w-sm rounded-xl p-6 relative">
<button id="close-settings-modal" class="absolute top-4 right-4 text-gray-400 hover:text-white text-2xl">×</button>
<h2 class="text-xl font-bold mb-6">Settings</h2>
<div class="space-y-4">
<div>
<label for="secret-password" class="block text-sm font-medium mb-1">Secret Password</label>
<input type="password" id="secret-password" class="form-control bg-gray-700 w-full rounded-lg px-4 py-2">
<p class="text-xs text-gray-500 mt-1">Encrypted and appended to your master password.</p>
</div>
<div class="pt-4 border-t border-gray-700">
<h3 class="text-lg font-semibold mb-2">Data Management</h3>
<div class="flex gap-4">
<button id="import-btn" class="flex-1 bg-gray-600 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded-lg">Import</button>
<button id="export-btn" class="flex-1 bg-gray-600 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded-lg">Export</button>
</div>
<input type="file" id="import-file-input" class="hidden" accept=".txt,application/json">
</div>
</div>
<div class="mt-8 flex justify-end">
<button id="save-settings" class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded-lg">Save & Close</button>
</div>
</div>
</div>
<div id="add-totp-modal" class="modal-overlay hidden"><div class="bg-gray-800 w-full max-w-sm rounded-xl p-6 relative"><button id="close-totp-modal" class="absolute top-4 right-4 text-gray-400 hover:text-white text-2xl">×</button><h2 class="text-xl font-bold mb-4">Add Authenticator</h2><form id="add-totp-form" class="space-y-4"><div><label for="totp-name" class="block text-sm font-medium mb-1">Service Name</label><input type="text" id="totp-name" placeholder="e.g., Google" class="form-control bg-gray-700 w-full rounded-lg px-4 py-2"></div><div><label for="totp-secret" class="block text-sm font-medium mb-1">Secret Key or URI</label><input type="text" id="totp-secret" required class="form-control bg-gray-700 w-full rounded-lg px-4 py-2"></div><div class="flex justify-end"><button type="submit" class="bg-green-600 hover:bg-green-700 text-white font-bold py-2 px-4 rounded-lg">Save</button></div></form></div></div>
<div id="add-login-modal" class="modal-overlay hidden"><div class="bg-gray-800 w-full max-w-md rounded-xl p-6 relative"><button id="close-login-modal" class="absolute top-4 right-4 text-gray-400 hover:text-white text-2xl">×</button><h2 class="text-xl font-bold mb-4">Add Login</h2><form id="add-login-form" class="space-y-4"><div><label for="login-label" class="block text-sm font-medium mb-1">Label</label><input type="text" id="login-label" required placeholder="e.g., Personal Email" class="form-control bg-gray-700 w-full rounded-lg px-4 py-2"></div><div class="border-t border-gray-700 pt-4"><label for="login-website" class="block text-sm font-medium mb-1">Website or App</label><input type="text" id="login-website" class="form-control bg-gray-700 w-full rounded-lg px-4 py-2"></div><div><label for="login-username" class="block text-sm font-medium mb-1">Username</label><input type="text" id="login-username" class="form-control bg-gray-700 w-full rounded-lg px-4 py-2"></div><div><label for="login-password" class="block text-sm font-medium mb-1">Password</label><input type="password" id="login-password" class="form-control bg-gray-700 w-full rounded-lg px-4 py-2"></div><div class="border-t border-gray-700 pt-4"><label for="login-notes" class="block text-sm font-medium mb-1">Additional Notes</label><textarea id="login-notes" rows="3" class="form-control bg-gray-700 w-full rounded-lg px-4 py-2"></textarea></div><div class="flex justify-end"><button type="submit" class="bg-green-600 hover:bg-green-700 text-white font-bold py-2 px-4 rounded-lg">Save</button></div></form></div></div>
<div id="info-modal" class="modal-overlay hidden"><div class="bg-gray-800 w-full max-w-lg rounded-xl p-6 relative"><button id="close-info-modal" class="absolute top-4 right-4 text-gray-400 hover:text-white text-2xl">×</button><h2 class="text-xl font-bold mb-4">About This Vault</h2><div class="space-y-4 text-gray-300"><div><h3 class="font-semibold text-white mb-1">Your Privacy</h3><p class="text-sm">Your privacy is paramount. Each Master Password you use creates a separate, secure vault. All data for that vault (including the Secret Password and Authenticator keys) is encrypted as a single package before being saved in your browser. This data can only be decrypted with your exact Master Password. No information is ever sent to any server.</p></div><div><h3 class="font-semibold text-white mb-1">How It Works</h3><p class="text-sm">This tool combines a web-based re-implementation of the SuperGenPass (SGP) password generation philosophy with a standard TOTP authenticator, all within a secure, multi-user vault.</p></div><div><h3 class="font-semibold text-white mb-1">Learn More</h3><ul class="list-disc list-inside text-sm mt-2 space-y-1"><li><a href="https://chriszarate.github.io/supergenpass/" target="_blank" rel="noopener" class="text-blue-400 hover:underline">Original SuperGenPass Project</a></li><li><a href="https://github.com/chriszarate/supergenpass/wiki/FAQ" target="_blank" rel="noopener" class="text-blue-400 hover:underline">SuperGenPass FAQ</a></li></ul></div></div></div></div>
<script>
// --- Constants ---
const VAULT_STORAGE_KEY = 'passwordVault';
// --- Global State ---
let sessionMasterPassword = null;
let sessionUserHash = null;
let sessionUserData = {}; // In-memory store of the current user's decrypted data
let totpUpdateInterval = null;
// --- DOM Elements ---
const unlockScreen = document.getElementById('unlock-screen');
const mainApp = document.getElementById('main-app');
const unlockForm = document.getElementById('unlock-form');
const unlockMasterPasswordInput = document.getElementById('unlock-master-password');
const unlockError = document.getElementById('unlock-error');
const passwordIdenticon = document.getElementById('password-identicon');
const passwordKey = document.getElementById('password-key');
const lockBtn = document.getElementById('lock-btn');
const passwordForm = document.getElementById('password-form');
const domainInput = document.getElementById('domain');
const lengthInput = document.getElementById('length');
const algorithmSelect = document.getElementById('algorithm');
const resultContainer = document.getElementById('result-container');
const resultInput = document.getElementById('result');
const copyBtn = document.getElementById('copy-btn');
const fab = document.getElementById('fab');
// Tabs
const showTotpTab = document.getElementById('show-totp-tab');
const showLoginsTab = document.getElementById('show-logins-tab');
const totpSection = document.getElementById('totp-section');
const loginsSection = document.getElementById('logins-section');
// Modals
const settingsModal = document.getElementById('settings-modal');
const closeSettingsModalBtn = document.getElementById('close-settings-modal');
const saveSettingsBtn = document.getElementById('save-settings');
const secretPasswordInput = document.getElementById('secret-password');
const addTotpModal = document.getElementById('add-totp-modal');
const closeTotpModalBtn = document.getElementById('close-totp-modal');
const addLoginModal = document.getElementById('add-login-modal');
const closeLoginModalBtn = document.getElementById('close-login-modal');
const infoModal = document.getElementById('info-modal');
const infoIcon = document.getElementById('info-icon');
const closeInfoModalBtn = document.getElementById('close-info-modal');
const exportBtn = document.getElementById('export-btn');
const importBtn = document.getElementById('import-btn');
const importFileInput = document.getElementById('import-file-input');
// --- Core Logic (Storage, Encryption, Hashing) ---
const getUserHash = (password) => CryptoJS.SHA256(password).toString();
const getMasterVault = () => { try { return JSON.parse(localStorage.getItem(VAULT_STORAGE_KEY) || '{}'); } catch (e) { return {}; } };
const saveMasterVault = (vault) => localStorage.setItem(VAULT_STORAGE_KEY, JSON.stringify(vault));
const saveCurrentUserData = () => {
if (!sessionMasterPassword || !sessionUserHash) return;
const masterVault = getMasterVault();
masterVault[sessionUserHash] = CryptoJS.AES.encrypt(JSON.stringify(sessionUserData), sessionMasterPassword).toString();
saveMasterVault(masterVault);
};
// --- Password Generation Logic ---
const bufferToBase64 = (b) => window.btoa(String.fromCharCode(...new Uint8Array(b)));
const wordToBuffer = (wa) => { const b = new ArrayBuffer(wa.sigBytes); const v = new Uint8Array(b); for (let i = 0; i < wa.sigBytes; i++) { v[i] = (wa.words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff; } return b; };
async function getNextHash(str, algo) { let hb; if (algo === 'sha512') { hb = await crypto.subtle.digest('SHA-512', new TextEncoder().encode(str)); } else { hb = wordToBuffer(CryptoJS.MD5(str)); } return bufferToBase64(hb).replaceAll('+', '9').replaceAll('/', '8').replaceAll('=', 'A'); }
const validatePassword = (p) => p && (p[0] >= 'a' && p[0] <= 'z') && /[A-Z]/.test(p) && /\d/.test(p);
// --- App Flow (Lock/Unlock) ---
function lockApp() {
sessionMasterPassword = null;
sessionUserHash = null;
sessionUserData = {};
clearInterval(totpUpdateInterval);
mainApp.classList.add('hidden');
fab.classList.add('hidden');
unlockScreen.classList.remove('hidden');
unlockMasterPasswordInput.value = '';
unlockError.classList.add('hidden');
passwordKey.classList.remove('hidden');
passwordIdenticon.classList.add('hidden');
}
function unlockApp(event) {
event.preventDefault();
const password = unlockMasterPasswordInput.value;
if (!password) return;
const userHash = getUserHash(password);
const masterVault = getMasterVault();
const encryptedUserData = masterVault[userHash];
let decryptedData = null;
if (encryptedUserData) {
try {
const bytes = CryptoJS.AES.decrypt(encryptedUserData, password);
const decryptedString = bytes.toString(CryptoJS.enc.Utf8);
if (decryptedString) decryptedData = JSON.parse(decryptedString);
} catch (e) { /* Decryption failed */ }
} else {
decryptedData = { secret: '', totps: [], logins: [] };
}
if (decryptedData) {
sessionMasterPassword = password;
sessionUserHash = userHash;
sessionUserData = decryptedData;
unlockScreen.classList.add('hidden');
mainApp.classList.remove('hidden');
fab.classList.remove('hidden');
loadAndRenderAllData();
} else {
unlockError.classList.remove('hidden');
}
}
function updatePasswordIdenticon() {
const password = unlockMasterPasswordInput.value;
if (password) {
passwordKey.classList.add('hidden');
passwordIdenticon.classList.remove('hidden');
jdenticon.update(passwordIdenticon, password);
} else {
passwordKey.classList.remove('hidden');
passwordIdenticon.classList.add('hidden');
}
}
function loadAndRenderAllData() {
renderTotpList();
renderLoginsList();
startTotpUpdater();
}
// --- Tab Switching ---
function switchTab(tabToShow) {
if (tabToShow === 'totp') {
totpSection.classList.remove('hidden');
loginsSection.classList.add('hidden');
showTotpTab.classList.add('active');
showLoginsTab.classList.remove('active');
} else {
totpSection.classList.add('hidden');
loginsSection.classList.remove('hidden');
showTotpTab.classList.remove('active');
showLoginsTab.classList.add('active');
}
}
// --- TOTP Logic ---
function renderTotpList() {
const totpListEl = document.getElementById('totp-list');
const noTotpMessageEl = document.getElementById('no-totp-message');
totpListEl.innerHTML = '';
const totps = sessionUserData.totps || [];
if (noTotpMessageEl) noTotpMessageEl.classList.toggle('hidden', totps.length > 0);
totps.forEach((item, index) => {
const totpItem = document.createElement('div');
totpItem.className = 'bg-gray-700 p-4 rounded-lg flex items-center gap-4';
totpItem.innerHTML = `<div class="flex-grow"><p class="font-semibold text-white">${item.name}</p><p class="text-2xl font-mono tracking-wider text-cyan-400" data-totp-code="${index}">000000</p><div class="w-full bg-gray-600 rounded-full h-1.5 mt-2"><div class="bg-cyan-500 h-1.5 rounded-full progress-bar" data-totp-progress="${index}" style="width: 100%"></div></div></div><button data-copy-totp="${index}" class="text-gray-400 hover:text-white p-2" title="Copy Code"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg></button><button data-delete-totp="${index}" class="text-gray-400 hover:text-red-500 p-2" title="Delete"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg></button>`;
totpListEl.appendChild(totpItem);
});
}
function startTotpUpdater() {
clearInterval(totpUpdateInterval);
const updateCodes = () => {
const epoch = Math.floor(Date.now() / 1000);
const countdown = 30 - (epoch % 30);
const progress = (countdown / 30) * 100;
(sessionUserData.totps || []).forEach((item, index) => {
const progressEl = document.querySelector(`[data-totp-progress="${index}"]`);
if(progressEl) progressEl.style.width = `${progress}%`;
try {
const totp = new OTPAuth.TOTP({ secret: OTPAuth.Secret.fromBase32(item.secret) });
const codeEl = document.querySelector(`[data-totp-code="${index}"]`);
if(codeEl) codeEl.textContent = totp.generate();
} catch(e) {
console.error(`Failed to generate TOTP for "${item.name}" with secret "${item.secret}":`, e);
const codeEl = document.querySelector(`[data-totp-code="${index}"]`);
if(codeEl) codeEl.textContent = "Invalid";
}
});
};
updateCodes();
totpUpdateInterval = setInterval(updateCodes, 1000);
}
document.getElementById('add-totp-form').addEventListener('submit', (event) => {
event.preventDefault();
const nameInput = document.getElementById('totp-name');
const secretInput = document.getElementById('totp-secret');
let name = nameInput.value.trim();
let secret = secretInput.value.trim();
if (!secret) return;
if (secret.toLowerCase().startsWith('otpauth://')) {
try {
const parsedUri = OTPAuth.URI.parse(secret);
if (!name) name = parsedUri.issuer ? `${parsedUri.issuer} (${parsedUri.label})` : parsedUri.label;
secret = parsedUri.secret.base32;
} catch (e) {
alert('The provided text looks like an otpauth URI, but it could not be parsed.');
console.error("Failed to parse otpauth URI:", e);
return;
}
} else {
secret = secret.toUpperCase().replace(/[^A-Z2-7=]/g, '');
}
if (!name) name = "Untitled";
if (!name || !secret) { alert("Could not process the authenticator information."); return; }
sessionUserData.totps = sessionUserData.totps || [];
sessionUserData.totps.push({ name, secret });
saveCurrentUserData();
renderTotpList();
startTotpUpdater();
nameInput.value = ''; secretInput.value = '';
document.getElementById('add-totp-modal').classList.add('hidden');
});
document.getElementById('totp-list').addEventListener('click', (e) => {
const copyBtn = e.target.closest('[data-copy-totp]');
if (copyBtn) {
const code = copyBtn.parentElement.querySelector('[data-totp-code]').textContent;
navigator.clipboard.writeText(code);
}
const deleteBtn = e.target.closest('[data-delete-totp]');
if (deleteBtn) {
const index = parseInt(deleteBtn.dataset.deleteTotp, 10);
if (confirm(`Are you sure you want to delete "${sessionUserData.totps[index].name}"?`)) {
sessionUserData.totps.splice(index, 1);
saveCurrentUserData();
renderTotpList();
}
}
});
// --- Logins Logic ---
function renderLoginsList() {
const loginsListEl = document.getElementById('logins-list');
const noLoginsMessageEl = document.getElementById('no-logins-message');
loginsListEl.innerHTML = '';
const logins = sessionUserData.logins || [];
if(noLoginsMessageEl) noLoginsMessageEl.classList.toggle('hidden', logins.length > 0);
logins.forEach((item, index) => {
const loginItem = document.createElement('div');
loginItem.className = 'bg-gray-700 p-4 rounded-lg';
loginItem.innerHTML = `
<div class="flex justify-between items-start">
<div>
<p class="font-semibold text-white text-lg">${item.label}</p>
${item.website ? `<p class="text-sm text-gray-400">${item.website}</p>` : ''}
</div>
<button data-delete-login="${index}" class="text-gray-400 hover:text-red-500 p-1" title="Delete">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg>
</button>
</div>
<div class="mt-4 space-y-2">
${item.username ? `<div class="flex items-center justify-between"><span class="text-sm text-gray-300">Username:</span><div class="flex items-center gap-2"><span class="font-mono text-sm">${item.username}</span><button data-copy-login-user="${index}" class="text-gray-400 hover:text-white" title="Copy Username"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg></button></div></div>` : ''}
${item.password ? `<div class="flex items-center justify-between"><span class="text-sm text-gray-300">Password:</span><div class="flex items-center gap-2"><input type="password" value="${item.password}" readonly class="bg-transparent font-mono text-sm w-24 text-right" data-login-password-field="${index}"><button data-toggle-login-pass="${index}" class="text-gray-400 hover:text-white" title="Show/Hide"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg></button><button data-copy-login-pass="${index}" class="text-gray-400 hover:text-white" title="Copy Password"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg></button></div></div>` : ''}
${item.notes ? `<div class="pt-2 border-t border-gray-600"><p class="text-sm text-gray-300 whitespace-pre-wrap">${item.notes}</p></div>` : ''}
</div>
`;
loginsListEl.appendChild(loginItem);
});
}
document.getElementById('add-login-form').addEventListener('submit', (event) => {
event.preventDefault();
const label = document.getElementById('login-label').value.trim();
const website = document.getElementById('login-website').value.trim();
const username = document.getElementById('login-username').value.trim();
const password = document.getElementById('login-password').value;
const notes = document.getElementById('login-notes').value.trim();
if (!label) { alert("Label is required."); return; }
if (!website && !username && !password && !notes) { alert("Please fill in either the website/credentials or the notes field."); return; }
const newLogin = { label, website, username, password, notes };
sessionUserData.logins = sessionUserData.logins || [];
sessionUserData.logins.push(newLogin);
saveCurrentUserData();
renderLoginsList();
document.getElementById('add-login-form').reset();
addLoginModal.classList.add('hidden');
});
document.getElementById('logins-list').addEventListener('click', e => {
const deleteBtn = e.target.closest('[data-delete-login]');
if (deleteBtn) {
const index = parseInt(deleteBtn.dataset.deleteLogin, 10);
if(confirm(`Are you sure you want to delete "${sessionUserData.logins[index].label}"?`)) {
sessionUserData.logins.splice(index, 1);
saveCurrentUserData();
renderLoginsList();
}
}
const copyUserBtn = e.target.closest('[data-copy-login-user]');
if (copyUserBtn) {
const index = parseInt(copyUserBtn.dataset.copyLoginUser, 10);
navigator.clipboard.writeText(sessionUserData.logins[index].username);
}
const copyPassBtn = e.target.closest('[data-copy-login-pass]');
if (copyPassBtn) {
const index = parseInt(copyPassBtn.dataset.copyLoginPass, 10);
navigator.clipboard.writeText(sessionUserData.logins[index].password);
}
const togglePassBtn = e.target.closest('[data-toggle-login-pass]');
if (togglePassBtn) {
const index = parseInt(togglePassBtn.dataset.toggleLoginPass, 10);
const passField = document.querySelector(`[data-login-password-field="${index}"]`);
if (passField) passField.type = passField.type === 'password' ? 'text' : 'password';
}
});
// --- Password Generator Logic ---
passwordForm.addEventListener('submit', (event) => {
event.preventDefault();
const masterPassword = sessionMasterPassword + (sessionUserData.secret || '');
const domain = domainInput.value.toLowerCase();
const length = parseInt(lengthInput.value, 10);
const algorithm = algorithmSelect.value;
setTimeout(async () => {
let hash = `${masterPassword}:${domain}`;
for (let i = 0; i < 5000; i++) {
hash = await getNextHash(hash, algorithm);
if (i >= 9) {
const candidate = hash.substring(0, length);
if (validatePassword(candidate)) {
resultInput.value = candidate;
resultContainer.classList.remove('hidden');
return;
}
}
}
resultInput.value = "Failed to generate valid password.";
resultContainer.classList.remove('hidden');
}, 50);
});
// --- Data Management (Import/Export) ---
exportBtn.addEventListener('click', () => {
const encryptedData = CryptoJS.AES.encrypt(JSON.stringify(sessionUserData), sessionMasterPassword).toString();
const blob = new Blob([encryptedData], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `vault-backup-${new Date().toISOString().slice(0,10)}.txt`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
});
importBtn.addEventListener('click', () => importFileInput.click());
importFileInput.addEventListener('change', (event) => {
const file = event.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (e) => {
const encryptedData = e.target.result;
const importPassword = prompt("Please enter the Master Password for the backup file:");
if (!importPassword) return;
try {
const bytes = CryptoJS.AES.decrypt(encryptedData, importPassword);
const decryptedString = bytes.toString(CryptoJS.enc.Utf8);
const importedData = JSON.parse(decryptedString);
if (importedData && typeof importedData === 'object') {
if (confirm("Backup decrypted. Merge with current vault? (Duplicates will be ignored).")) {
let totpAdded = 0;
let loginsAdded = 0;
if(importedData.secret) sessionUserData.secret = importedData.secret;
// Merge TOTPs
sessionUserData.totps = sessionUserData.totps || [];
const existingTotpSecrets = new Set(sessionUserData.totps.map(item => item.secret));
if(importedData.totps && Array.isArray(importedData.totps)) {
importedData.totps.forEach(item => {
if(item.secret && !existingTotpSecrets.has(item.secret)) {
sessionUserData.totps.push(item);
totpAdded++;
}
});
}
// Merge Logins
sessionUserData.logins = sessionUserData.logins || [];
const existingLogins = new Set(sessionUserData.logins.map(l => `${l.label}|${l.website}|${l.username}`));
if (importedData.logins && Array.isArray(importedData.logins)) {
importedData.logins.forEach(item => {
const uniqueKey = `${item.label}|${item.website}|${item.username}`;
if (item.label && !existingLogins.has(uniqueKey)) {
sessionUserData.logins.push(item);
loginsAdded++;
}
});
}
saveCurrentUserData();
loadAndRenderAllData();
settingsModal.classList.add('hidden');
alert(`Merge complete. Added ${totpAdded} TOTP secret(s) and ${loginsAdded} login(s).`);
}
} else { throw new Error("Invalid data format."); }
} catch (err) {
alert("Decryption failed. Incorrect password or corrupt file.");
} finally {
importFileInput.value = ''; // Reset file input
}
};
reader.readAsText(file);
});
// --- Modal Logic & Event Listeners ---
function openSettingsModal() { secretPasswordInput.value = sessionUserData.secret || ''; settingsModal.classList.remove('hidden'); }
function saveAndCloseSettings() { sessionUserData.secret = secretPasswordInput.value; saveCurrentUserData(); settingsModal.classList.add('hidden'); }
unlockForm.addEventListener('submit', unlockApp);
unlockMasterPasswordInput.addEventListener('input', updatePasswordIdenticon);
lockBtn.addEventListener('click', lockApp);
copyBtn.addEventListener('click', () => navigator.clipboard.writeText(resultInput.value));
fab.addEventListener('click', openSettingsModal);
closeSettingsModalBtn.addEventListener('click', () => settingsModal.classList.add('hidden'));
saveSettingsBtn.addEventListener('click', saveAndCloseSettings);
// Modal Open/Close
document.getElementById('add-totp-btn').addEventListener('click', () => addTotpModal.classList.remove('hidden'));
closeTotpModalBtn.addEventListener('click', () => addTotpModal.classList.add('hidden'));
document.getElementById('add-login-btn').addEventListener('click', () => addLoginModal.classList.remove('hidden'));
closeLoginModalBtn.addEventListener('click', () => addLoginModal.classList.add('hidden'));
infoIcon.addEventListener('click', () => infoModal.classList.remove('hidden'));
closeInfoModalBtn.addEventListener('click', () => infoModal.classList.add('hidden'));
[settingsModal, addTotpModal, addLoginModal, infoModal].forEach(m => m.addEventListener('click', (e) => e.target === m && m.classList.add('hidden')));
// Tab Listeners
showTotpTab.addEventListener('click', () => switchTab('totp'));
showLoginsTab.addEventListener('click', () => switchTab('logins'));
</script>