// ========== CREDENTIAL MANAGEMENT ========== (function () { // Inject the token-management modal HTML injectModal('token-management-modal', `

🔑 DNS Credentials

Enter Technitium DNS login credentials. Read-only accounts are used for logs; admin accounts for restarts, records, and updates.

DNS1 (Windows)

DNS2 (Linux)

DNS3 (AlmaLinux)

`); // Credential encryption — key stored in sessionStorage (not localStorage) so encrypted // values in localStorage can't be decrypted without the current session key. // On session close, key is lost; credentials are re-synced from backend on next save. function getEncryptionKey() { // 1. Check sessionStorage first (current session) let key = safeSessionGet('dashcaddy-encryption-key'); if (key) return key; // 2. Migrate from localStorage if old key exists (one-time upgrade) const oldKey = safeGet('dashcaddy-encryption-key'); if (oldKey) { safeSessionSet('dashcaddy-encryption-key', oldKey); safeRemove('dashcaddy-encryption-key'); // Remove from localStorage return oldKey; } // 3. Generate new key for this session const array = new Uint8Array(32); crypto.getRandomValues(array); key = Array.from(array, b => b.toString(16).padStart(2, '0')).join(''); safeSessionSet('dashcaddy-encryption-key', key); return key; } const ENCRYPTION_KEY = getEncryptionKey(); // AES-like multi-round encryption with per-value IV (stronger than single-pass XOR) function credentialEncrypt(text, key) { if (!text) return ''; // Generate random IV (8 bytes) const iv = crypto.getRandomValues(new Uint8Array(8)); const ivHex = Array.from(iv, b => b.toString(16).padStart(2, '0')).join(''); // Derive round key from key + IV for uniqueness per value const keyBytes = new TextEncoder().encode(key + ivHex); let result = ''; for (let i = 0; i < text.length; i++) { // Multi-source XOR: key byte + IV byte + position-dependent mixing const charCode = text.charCodeAt(i) ^ keyBytes[i % keyBytes.length] ^ iv[i % iv.length] ^ ((i * 31 + 17) & 0xFF); result += String.fromCharCode(charCode); } // Prepend IV to ciphertext so we can decrypt later return ivHex + ':' + btoa(result); } function credentialDecrypt(encryptedText, key) { if (!encryptedText) return ''; try { // Check for IV prefix (new format: "ivhex:base64") const colonIdx = encryptedText.indexOf(':'); if (colonIdx === 16) { // New format with IV const ivHex = encryptedText.substring(0, 16); const iv = new Uint8Array(ivHex.match(/.{2}/g).map(h => parseInt(h, 16))); const decoded = atob(encryptedText.substring(17)); const keyBytes = new TextEncoder().encode(key + ivHex); let result = ''; for (let i = 0; i < decoded.length; i++) { const charCode = decoded.charCodeAt(i) ^ keyBytes[i % keyBytes.length] ^ iv[i % iv.length] ^ ((i * 31 + 17) & 0xFF); result += String.fromCharCode(charCode); } return result; } // Legacy format (plain XOR base64) — for migration const decoded = atob(encryptedText); let result = ''; for (let i = 0; i < decoded.length; i++) { const charCode = decoded.charCodeAt(i) ^ key.charCodeAt(i % key.length); result += String.fromCharCode(charCode); } return result; } catch (e) { return ''; } } // Credential storage functions function getCredential(dnsId, tokenType, credType) { const encrypted = safeGet(`${dnsId}-${tokenType}-${credType}-enc`); return credentialDecrypt(encrypted, ENCRYPTION_KEY); } function setCredential(dnsId, tokenType, credType, value) { const key = `${dnsId}-${tokenType}-${credType}-enc`; if (value) { safeSet(key, credentialEncrypt(value, ENCRYPTION_KEY)); } else { safeRemove(key); } } function getToken(dnsId, tokenType) { return getCredential(dnsId, tokenType, 'token'); } function getUsername(dnsId, tokenType) { return getCredential(dnsId, tokenType, 'username'); } function setToken(dnsId, tokenType, token) { setCredential(dnsId, tokenType, 'token', token); } function setUsername(dnsId, tokenType, username) { setCredential(dnsId, tokenType, 'username', username); } function getAllCredentials() { return { dns1: { readonly: { username: getUsername('dns1', 'readonly'), token: getToken('dns1', 'readonly') }, admin: { username: getUsername('dns1', 'admin'), token: getToken('dns1', 'admin') } }, dns2: { readonly: { username: getUsername('dns2', 'readonly'), token: getToken('dns2', 'readonly') }, admin: { username: getUsername('dns2', 'admin'), token: getToken('dns2', 'admin') } }, dns3: { readonly: { username: getUsername('dns3', 'readonly'), token: getToken('dns3', 'readonly') }, admin: { username: getUsername('dns3', 'admin'), token: getToken('dns3', 'admin') } } }; } function clearAllCredentials() { ['dns1', 'dns2', 'dns3'].forEach(dnsId => { ['readonly', 'admin'].forEach(tokenType => { ['token', 'username'].forEach(credType => { safeRemove(`${dnsId}-${tokenType}-${credType}-enc`); }); }); safeRemove(`${dnsId}-token-enc`); safeRemove(`${dnsId}-username-enc`); }); } function getStoredCredentials(dnsId) { const readonlyToken = getToken(dnsId, 'readonly'); const readonlyUsername = getUsername(dnsId, 'readonly'); const adminToken = getToken(dnsId, 'admin'); const adminUsername = getUsername(dnsId, 'admin'); const oldToken = credentialDecrypt(safeGet(`${dnsId}-token-enc`), ENCRYPTION_KEY); const oldUsername = credentialDecrypt(safeGet(`${dnsId}-username-enc`), ENCRYPTION_KEY); return { username: adminUsername || readonlyUsername || oldUsername, token: adminToken || readonlyToken || oldToken, readonlyToken: readonlyToken || oldToken, readonlyUsername: readonlyUsername || oldUsername, adminToken: adminToken || oldToken, adminUsername: adminUsername || oldUsername }; } // Token Management Modal handlers document.getElementById('manage-tokens')?.addEventListener('click', () => { const modal = document.getElementById('token-management-modal'); const creds = getAllCredentials(); // Populate fields with existing credentials ['dns1', 'dns2', 'dns3'].forEach(dnsId => { document.getElementById(`${dnsId}-readonly-username`).value = creds[dnsId].readonly.username; document.getElementById(`${dnsId}-readonly-token`).value = creds[dnsId].readonly.token; document.getElementById(`${dnsId}-admin-username`).value = creds[dnsId].admin.username; document.getElementById(`${dnsId}-admin-token`).value = creds[dnsId].admin.token; document.getElementById(`${dnsId}-token-status`).textContent = ''; }); modal.classList.add('show'); }); // Toggle password visibility document.querySelectorAll('.token-toggle').forEach(btn => { btn.addEventListener('click', () => { const targetId = btn.dataset.target; const input = document.getElementById(targetId); if (input.type === 'password') { input.type = 'text'; btn.textContent = '\u{1F648}'; } else { input.type = 'password'; btn.textContent = '\u{1F441}'; } }); }); document.getElementById('token-save')?.addEventListener('click', async () => { // Save all credentials to localStorage ['dns1', 'dns2', 'dns3'].forEach(dnsId => { setUsername(dnsId, 'readonly', document.getElementById(`${dnsId}-readonly-username`).value.trim()); setToken(dnsId, 'readonly', document.getElementById(`${dnsId}-readonly-token`).value.trim()); setUsername(dnsId, 'admin', document.getElementById(`${dnsId}-admin-username`).value.trim()); setToken(dnsId, 'admin', document.getElementById(`${dnsId}-admin-token`).value.trim()); }); // Build per-server credentials payload for backend sync const servers = {}; let hasAnyCreds = false; ['dns1', 'dns2', 'dns3'].forEach(dnsId => { const entry = {}; const roUser = document.getElementById(`${dnsId}-readonly-username`).value.trim(); const roPass = document.getElementById(`${dnsId}-readonly-token`).value.trim(); const adminUser = document.getElementById(`${dnsId}-admin-username`).value.trim(); const adminPass = document.getElementById(`${dnsId}-admin-token`).value.trim(); if (roUser && roPass) { entry.readonly = { username: roUser, password: roPass }; hasAnyCreds = true; } if (adminUser && adminPass) { entry.admin = { username: adminUser, password: adminPass }; hasAnyCreds = true; } if (Object.keys(entry).length > 0) { servers[dnsId] = entry; } }); if (hasAnyCreds) { // Show syncing status ['dns1', 'dns2', 'dns3'].forEach(dnsId => { if (servers[dnsId]) { document.getElementById(`${dnsId}-token-status`).textContent = 'Verifying...'; document.getElementById(`${dnsId}-token-status`).className = 'token-status'; } }); try { const res = await secureFetch('/api/v1/dns/credentials', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ servers }) }); const data = await res.json(); if (data.results) { ['dns1', 'dns2', 'dns3'].forEach(dnsId => { const statusEl = document.getElementById(`${dnsId}-token-status`); if (!servers[dnsId]) { statusEl.textContent = ''; return; } const result = data.results[dnsId]; if (result?.success) { statusEl.textContent = '\u2713 Verified & saved'; statusEl.className = 'token-status success'; } else if (result?.partial) { statusEl.textContent = '\u2713 ' + result.partial; statusEl.className = 'token-status success'; } else { statusEl.textContent = '\u2717 ' + (result?.error || 'Login failed'); statusEl.className = 'token-status error'; } }); } else if (data.success) { ['dns1', 'dns2', 'dns3'].forEach(dnsId => { if (servers[dnsId]) { document.getElementById(`${dnsId}-token-status`).textContent = '\u2713 Saved'; document.getElementById(`${dnsId}-token-status`).className = 'token-status success'; } }); } else { ['dns1', 'dns2', 'dns3'].forEach(dnsId => { if (servers[dnsId]) { document.getElementById(`${dnsId}-token-status`).textContent = '\u2717 ' + (data.error || 'Failed'); document.getElementById(`${dnsId}-token-status`).className = 'token-status error'; } }); } } catch (e) { console.error('Failed to sync DNS credentials to backend:', e); ['dns1', 'dns2', 'dns3'].forEach(dnsId => { if (servers[dnsId]) { document.getElementById(`${dnsId}-token-status`).textContent = '\u2713 Saved locally (sync failed)'; document.getElementById(`${dnsId}-token-status`).className = 'token-status'; } }); } } else { ['dns1', 'dns2', 'dns3'].forEach(dnsId => { document.getElementById(`${dnsId}-token-status`).textContent = ''; }); } // Auto-close after delay if all succeeded setTimeout(() => { const allGood = ['dns1', 'dns2', 'dns3'].every(dnsId => { const status = document.getElementById(`${dnsId}-token-status`).textContent; return !status || status.includes('\u2713'); }); if (allGood) closeModal('token-management-modal'); }, 1500); }); document.getElementById('token-cancel')?.addEventListener('click', () => { closeModal('token-management-modal'); }); document.getElementById('token-clear-all')?.addEventListener('click', async () => { if (confirm('Clear all stored DNS credentials? This cannot be undone.')) { clearAllCredentials(); ['dns1', 'dns2', 'dns3'].forEach(dnsId => { document.getElementById(`${dnsId}-readonly-username`).value = ''; document.getElementById(`${dnsId}-readonly-token`).value = ''; document.getElementById(`${dnsId}-admin-username`).value = ''; document.getElementById(`${dnsId}-admin-token`).value = ''; document.getElementById(`${dnsId}-token-status`).textContent = '\u2713 Cleared'; document.getElementById(`${dnsId}-token-status`).className = 'token-status success'; }); try { await secureFetch('/api/v1/dns/credentials', { method: 'DELETE' }); } catch (_) {} } }); // Close modal on backdrop click document.getElementById('token-management-modal')?.addEventListener('click', (e) => { if (e.target.id === 'token-management-modal') { e.target.classList.remove('show'); } }); // Window exports window.getToken = getToken; window.getUsername = getUsername; window.setToken = setToken; window.setUsername = setUsername; window.getAllCredentials = getAllCredentials; window.getCredential = getCredential; window.setCredential = setCredential; window.getEncryptionKey = getEncryptionKey; })();