// Enhanced toast system
function showToast(message, type = 'info') {
// Remove any existing toast
const existingToast = document.querySelector('.toast-notification');
if (existingToast) {
existingToast.remove();
}
// Create new toast element
const toast = document.createElement('div');
toast.className = `toast-notification toast-${type}`;
// Add icon based on type
const icon = getToastIcon(type);
toast.innerHTML = `
${icon}
${message}
`;
// Add to document
document.body.appendChild(toast);
// Trigger entrance animation
setTimeout(() => toast.classList.add('toast-show'), 10);
// Auto-remove after 4 seconds
setTimeout(() => {
if (toast.parentElement) {
toast.classList.add('toast-hide');
setTimeout(() => toast.remove(), 300);
}
}, 4000);
}
function getToastIcon(type) {
switch (type) {
case 'success':
return ``;
case 'error':
return ``;
case 'info':
default:
return ``;
}
}
// Enhanced loading states for buttons
function setButtonLoading(button, loading = true) {
if (loading) {
button.dataset.originalText = button.textContent;
button.classList.add('loading');
button.disabled = true;
} else {
button.classList.remove('loading');
button.disabled = false;
if (button.dataset.originalText) {
button.textContent = button.dataset.originalText;
}
}
}
// Enhanced response error handling
function getResponseError(response, operation = 'operation') {
if (!response) {
return `Failed to ${operation}: No response received`;
}
if (response.success === false || response.error) {
const errorMsg = getErrorMessage(response.error, `${operation} failed`);
// Handle specific error types
if (errorMsg.includes('decryption error') || errorMsg.includes('aead::Error')) {
return 'Invalid password or corrupted keyspace data';
}
if (errorMsg.includes('Crypto error')) {
return 'Keyspace not found or corrupted. Try creating a new one.';
}
if (errorMsg.includes('not unlocked') || errorMsg.includes('session')) {
return 'Session expired. Please login again.';
}
return errorMsg;
}
return `Failed to ${operation}: Unknown error`;
}
function showSection(sectionId) {
document.querySelectorAll('.section').forEach(s => s.classList.add('hidden'));
document.getElementById(sectionId).classList.remove('hidden');
}
function setStatus(text, isConnected = false) {
document.getElementById('statusText').textContent = text;
const indicator = document.getElementById('statusIndicator');
indicator.classList.toggle('connected', isConnected);
// Show/hide lock button - only show when session is unlocked
const lockBtn = document.getElementById('lockBtn');
if (lockBtn) {
// Only show lock button when connected AND status indicates unlocked session
const isUnlocked = isConnected && text.toLowerCase().startsWith('connected to');
lockBtn.classList.toggle('hidden', !isUnlocked);
}
}
// Message handling
async function sendMessage(action, data = {}) {
return new Promise((resolve) => {
chrome.runtime.sendMessage({ action, ...data }, resolve);
});
}
// Copy to clipboard
async function copyToClipboard(text) {
try {
await navigator.clipboard.writeText(text);
showToast('Copied to clipboard!', 'success');
} catch (err) {
showToast('Failed to copy', 'error');
}
}
// Convert string to Uint8Array
function stringToUint8Array(str) {
return Array.from(new TextEncoder().encode(str));
}
// DOM Elements
const elements = {
keyspaceInput: document.getElementById('keyspaceInput'),
passwordInput: document.getElementById('passwordInput'),
createKeyspaceBtn: document.getElementById('createKeyspaceBtn'),
loginBtn: document.getElementById('loginBtn'),
lockBtn: document.getElementById('lockBtn'),
themeToggle: document.getElementById('themeToggle'),
toggleAddKeypairBtn: document.getElementById('toggleAddKeypairBtn'),
addKeypairCard: document.getElementById('addKeypairCard'),
keyTypeSelect: document.getElementById('keyTypeSelect'),
keyNameInput: document.getElementById('keyNameInput'),
addKeypairBtn: document.getElementById('addKeypairBtn'),
cancelAddKeypairBtn: document.getElementById('cancelAddKeypairBtn'),
keypairsList: document.getElementById('keypairsList'),
// Sign tab
messageInput: document.getElementById('messageInput'),
signBtn: document.getElementById('signBtn'),
signatureResult: document.getElementById('signatureResult'),
copySignatureBtn: document.getElementById('copySignatureBtn'),
// Encrypt tab
encryptMessageInput: document.getElementById('encryptMessageInput'),
encryptBtn: document.getElementById('encryptBtn'),
encryptResult: document.getElementById('encryptResult'),
// Decrypt tab
encryptedMessageInput: document.getElementById('encryptedMessageInput'),
decryptBtn: document.getElementById('decryptBtn'),
decryptResult: document.getElementById('decryptResult'),
// Verify tab
verifyMessageInput: document.getElementById('verifyMessageInput'),
signatureToVerifyInput: document.getElementById('signatureToVerifyInput'),
verifyBtn: document.getElementById('verifyBtn'),
verifyResult: document.getElementById('verifyResult'),
};
let currentKeyspace = null;
let selectedKeypairId = null;
let backgroundPort = null;
// Theme management
function initializeTheme() {
const savedTheme = localStorage.getItem('cryptovault-theme') || 'light';
document.documentElement.setAttribute('data-theme', savedTheme);
updateThemeIcon(savedTheme);
}
function toggleTheme() {
const currentTheme = document.documentElement.getAttribute('data-theme');
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', newTheme);
localStorage.setItem('cryptovault-theme', newTheme);
updateThemeIcon(newTheme);
}
function updateThemeIcon(theme) {
const themeToggle = elements.themeToggle;
if (!themeToggle) return;
if (theme === 'dark') {
themeToggle.innerHTML = '☀️';
themeToggle.title = 'Switch to light mode';
} else {
// Dark crescent moon SVG for better visibility
themeToggle.innerHTML = `
`;
themeToggle.title = 'Switch to dark mode';
}
}
// Establish connection to background script for keep-alive
function connectToBackground() {
try {
backgroundPort = chrome.runtime.connect({ name: 'popup' });
backgroundPort.onDisconnect.addListener(() => {
backgroundPort = null;
});
} catch (error) {
// Silently handle connection errors
}
}
// Initialize
document.addEventListener('DOMContentLoaded', async function() {
// Initialize theme first
initializeTheme();
// Ensure lock button starts hidden
const lockBtn = document.getElementById('lockBtn');
if (lockBtn) {
lockBtn.classList.add('hidden');
}
setStatus('Initializing...', false);
// Connect to background script for keep-alive
connectToBackground();
// Event listeners (with null checks)
if (elements.createKeyspaceBtn) {
elements.createKeyspaceBtn.addEventListener('click', createKeyspace);
}
if (elements.loginBtn) {
elements.loginBtn.addEventListener('click', login);
}
if (elements.lockBtn) {
elements.lockBtn.addEventListener('click', lockSession);
}
if (elements.themeToggle) {
elements.themeToggle.addEventListener('click', toggleTheme);
}
if (elements.toggleAddKeypairBtn) {
elements.toggleAddKeypairBtn.addEventListener('click', toggleAddKeypairForm);
}
if (elements.addKeypairBtn) {
elements.addKeypairBtn.addEventListener('click', addKeypair);
}
if (elements.cancelAddKeypairBtn) {
elements.cancelAddKeypairBtn.addEventListener('click', hideAddKeypairForm);
}
// Crypto operation buttons (with null checks)
if (elements.signBtn) {
elements.signBtn.addEventListener('click', signMessage);
}
if (elements.encryptBtn) {
elements.encryptBtn.addEventListener('click', encryptMessage);
}
if (elements.decryptBtn) {
elements.decryptBtn.addEventListener('click', decryptMessage);
}
if (elements.verifyBtn) {
elements.verifyBtn.addEventListener('click', verifySignature);
}
// Tab functionality
initializeTabs();
// Copy button event listeners (with null checks)
if (elements.copySignatureBtn) {
elements.copySignatureBtn.addEventListener('click', () => {
const signature = document.getElementById('signatureValue');
if (signature) {
copyToClipboard(signature.textContent);
}
});
}
// Enable sign button when message is entered (with null checks)
if (elements.messageInput && elements.signBtn) {
elements.messageInput.addEventListener('input', () => {
elements.signBtn.disabled = !elements.messageInput.value.trim() || !selectedKeypairId;
});
}
// Basic keyboard shortcuts
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && elements.addKeypairCard && !elements.addKeypairCard.classList.contains('hidden')) {
hideAddKeypairForm();
}
if (e.key === 'Enter' && e.target === elements.keyNameInput && elements.keyNameInput.value.trim()) {
e.preventDefault();
addKeypair();
}
});
// Check for existing session
await checkExistingSession();
});
async function checkExistingSession() {
try {
const response = await sendMessage('getStatus');
if (response && response.success && response.status && response.session) {
// Session is active
currentKeyspace = response.session.keyspace;
elements.keyspaceInput.value = currentKeyspace;
setStatus(`Connected to ${currentKeyspace}`, true);
showSection('vaultSection');
await loadKeypairs();
} else {
// No active session
setStatus('Ready', false);
showSection('authSection');
}
} catch (error) {
setStatus('Ready', false);
showSection('authSection');
}
}
// Toggle add keypair form
function toggleAddKeypairForm() {
const isHidden = elements.addKeypairCard.classList.contains('hidden');
if (isHidden) {
showAddKeypairForm();
} else {
hideAddKeypairForm();
}
}
function showAddKeypairForm() {
elements.addKeypairCard.classList.remove('hidden');
elements.keyNameInput.focus();
}
function hideAddKeypairForm() {
elements.addKeypairCard.classList.add('hidden');
// Clear the form
elements.keyNameInput.value = '';
elements.keyTypeSelect.selectedIndex = 0;
}
// Tab functionality
function initializeTabs() {
const tabButtons = document.querySelectorAll('.tab-btn');
const tabContents = document.querySelectorAll('.tab-content');
tabButtons.forEach(button => {
button.addEventListener('click', () => {
const targetTab = button.getAttribute('data-tab');
// Remove active class from all tabs and contents
tabButtons.forEach(btn => btn.classList.remove('active'));
tabContents.forEach(content => content.classList.remove('active'));
// Add active class to clicked tab and corresponding content
button.classList.add('active');
document.getElementById(`${targetTab}-tab`).classList.add('active');
// Clear results when switching tabs
clearTabResults();
// Update button states
updateButtonStates();
});
});
// Initialize input validation
initializeInputValidation();
}
function clearTabResults() {
// Hide all result sections (with null checks)
if (elements.signatureResult) {
elements.signatureResult.classList.add('hidden');
elements.signatureResult.innerHTML = '';
}
if (elements.encryptResult) {
elements.encryptResult.classList.add('hidden');
elements.encryptResult.innerHTML = '';
}
if (elements.decryptResult) {
elements.decryptResult.classList.add('hidden');
elements.decryptResult.innerHTML = '';
}
if (elements.verifyResult) {
elements.verifyResult.classList.add('hidden');
elements.verifyResult.innerHTML = '';
}
}
function initializeInputValidation() {
// Sign tab validation (with null checks)
if (elements.messageInput) {
elements.messageInput.addEventListener('input', updateButtonStates);
}
// Encrypt tab validation (with null checks)
if (elements.encryptMessageInput) {
elements.encryptMessageInput.addEventListener('input', updateButtonStates);
}
// Decrypt tab validation (with null checks)
if (elements.encryptedMessageInput) {
elements.encryptedMessageInput.addEventListener('input', updateButtonStates);
}
// Verify tab validation (with null checks)
if (elements.verifyMessageInput) {
elements.verifyMessageInput.addEventListener('input', updateButtonStates);
}
if (elements.signatureToVerifyInput) {
elements.signatureToVerifyInput.addEventListener('input', updateButtonStates);
}
}
function updateButtonStates() {
// Sign button (with null checks)
if (elements.signBtn && elements.messageInput) {
elements.signBtn.disabled = !elements.messageInput.value.trim() || !selectedKeypairId;
}
// Encrypt button (with null checks) - only needs message and keyspace session
if (elements.encryptBtn && elements.encryptMessageInput) {
elements.encryptBtn.disabled = !elements.encryptMessageInput.value.trim() || !currentKeyspace;
}
// Decrypt button (with null checks) - only needs encrypted message and keyspace session
if (elements.decryptBtn && elements.encryptedMessageInput) {
elements.decryptBtn.disabled = !elements.encryptedMessageInput.value.trim() || !currentKeyspace;
}
// Verify button (with null checks) - only needs message and signature
if (elements.verifyBtn && elements.verifyMessageInput && elements.signatureToVerifyInput) {
elements.verifyBtn.disabled = !elements.verifyMessageInput.value.trim() ||
!elements.signatureToVerifyInput.value.trim() ||
!selectedKeypairId;
}
}
// Clear all vault-related state and UI
function clearVaultState() {
// Clear all crypto operation inputs (with null checks)
if (elements.messageInput) elements.messageInput.value = '';
if (elements.encryptMessageInput) elements.encryptMessageInput.value = '';
if (elements.encryptedMessageInput) elements.encryptedMessageInput.value = '';
if (elements.verifyMessageInput) elements.verifyMessageInput.value = '';
if (elements.signatureToVerifyInput) elements.signatureToVerifyInput.value = '';
// Clear all result sections
clearTabResults();
// Clear signature value with null check
const signatureValue = document.getElementById('signatureValue');
if (signatureValue) signatureValue.textContent = '';
// Clear selected keypair state
selectedKeypairId = null;
updateButtonStates();
// Hide add keypair form if open
hideAddKeypairForm();
// Clear keypairs list
if (elements.keypairsList) {
elements.keypairsList.innerHTML = '
Loading keypairs...
';
}
}
async function createKeyspace() {
const keyspace = elements.keyspaceInput.value.trim();
const password = elements.passwordInput.value.trim();
if (!keyspace || !password) {
showToast('Please enter keyspace name and password', 'error');
return;
}
try {
await executeOperation(
async () => {
const response = await sendMessage('createKeyspace', { keyspace, password });
if (response && response.success) {
// Clear any existing state before auto-login
clearVaultState();
await login(); // Auto-login after creation
return response;
} else {
throw new Error(getResponseError(response, 'create keyspace'));
}
},
{
loadingElement: elements.createKeyspaceBtn,
successMessage: 'Keyspace created successfully!',
maxRetries: 1
}
);
} catch (error) {
console.error('Create keyspace error:', error);
}
}
async function login() {
const keyspace = elements.keyspaceInput.value.trim();
const password = elements.passwordInput.value.trim();
if (!keyspace || !password) {
showToast('Please enter keyspace name and password', 'error');
return;
}
try {
await executeOperation(
async () => {
const response = await sendMessage('initSession', { keyspace, password });
if (response && response.success) {
currentKeyspace = keyspace;
setStatus(`Connected to ${keyspace}`, true);
showSection('vaultSection');
// Clear any previous vault state before loading new keyspace
clearVaultState();
await loadKeypairs();
return response;
} else {
throw new Error(getResponseError(response, 'login'));
}
},
{
loadingElement: elements.loginBtn,
successMessage: 'Logged in successfully!',
maxRetries: 2
}
);
} catch (error) {
console.error('Login error:', error);
}
}
async function lockSession() {
try {
await sendMessage('lockSession');
currentKeyspace = null;
selectedKeypairId = null;
setStatus('Locked', false);
showSection('authSection');
// Clear all form inputs
elements.keyspaceInput.value = '';
elements.passwordInput.value = '';
clearVaultState();
showToast('Session locked', 'info');
} catch (error) {
showToast('Error: ' + error.message, 'error');
}
}
async function addKeypair() {
const keyType = elements.keyTypeSelect.value;
const keyName = elements.keyNameInput.value.trim();
if (!keyName) {
showToast('Please enter a name for the keypair', 'error');
return;
}
try {
await executeOperation(
async () => {
const metadata = JSON.stringify({ name: keyName });
const response = await sendMessage('addKeypair', { keyType, metadata });
if (response?.success) {
hideAddKeypairForm();
await loadKeypairs();
return response;
} else {
throw new Error(getResponseError(response, 'add keypair'));
}
},
{
loadingElement: elements.addKeypairBtn,
successMessage: 'Keypair added successfully!'
}
);
} catch (error) {
// Error already handled by executeOperation
}
}
async function loadKeypairs() {
try {
const response = await sendMessage('listKeypairs');
if (response && response.success) {
renderKeypairs(response.keypairs);
} else {
const errorMsg = getResponseError(response, 'load keypairs');
const container = elements.keypairsList;
container.innerHTML = '