934 lines
30 KiB
JavaScript
934 lines
30 KiB
JavaScript
// Consolidated toast system
|
|
function showToast(message, type = 'info') {
|
|
document.querySelector('.toast-notification')?.remove();
|
|
|
|
const icons = {
|
|
success: '<polyline points="20,6 9,17 4,12"></polyline>',
|
|
error: '<circle cx="12" cy="12" r="10"></circle><line x1="15" y1="9" x2="9" y2="15"></line><line x1="9" y1="9" x2="15" y2="15"></line>',
|
|
info: '<circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line>'
|
|
};
|
|
|
|
const toast = Object.assign(document.createElement('div'), {
|
|
className: `toast-notification toast-${type}`,
|
|
innerHTML: `
|
|
<div class="toast-icon">
|
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
${icons[type] || icons.info}
|
|
</svg>
|
|
</div>
|
|
<div class="toast-content"><div class="toast-message">${message}</div></div>
|
|
`
|
|
});
|
|
|
|
document.body.appendChild(toast);
|
|
setTimeout(() => toast.classList.add('toast-show'), 10);
|
|
setTimeout(() => {
|
|
if (toast.parentElement) {
|
|
toast.classList.add('toast-hide');
|
|
setTimeout(() => toast.remove(), 300);
|
|
}
|
|
}, 4000);
|
|
}
|
|
|
|
// 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) {
|
|
const statusText = document.getElementById('statusText');
|
|
const statusSection = document.getElementById('vaultStatus');
|
|
const lockBtn = document.getElementById('lockBtn');
|
|
|
|
if (isConnected && text) {
|
|
// Show keyspace name and status section
|
|
statusText.textContent = text;
|
|
statusSection.classList.remove('hidden');
|
|
if (lockBtn) {
|
|
lockBtn.classList.remove('hidden');
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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 = {
|
|
// Authentication elements
|
|
keyspaceInput: document.getElementById('keyspaceInput'),
|
|
passwordInput: document.getElementById('passwordInput'),
|
|
createKeyspaceBtn: document.getElementById('createKeyspaceBtn'),
|
|
loginBtn: document.getElementById('loginBtn'),
|
|
|
|
// Header elements
|
|
lockBtn: document.getElementById('lockBtn'),
|
|
themeToggle: document.getElementById('themeToggle'),
|
|
settingsToggle: document.getElementById('settingsToggle'),
|
|
settingsDropdown: document.getElementById('settingsDropdown'),
|
|
timeoutInput: document.getElementById('timeoutInput'),
|
|
|
|
// Keypair management elements
|
|
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'),
|
|
|
|
// Crypto operation elements - Sign tab
|
|
messageInput: document.getElementById('messageInput'),
|
|
signBtn: document.getElementById('signBtn'),
|
|
signatureResult: document.getElementById('signatureResult'),
|
|
copySignatureBtn: document.getElementById('copySignatureBtn'),
|
|
|
|
// Crypto operation elements - Encrypt tab
|
|
encryptMessageInput: document.getElementById('encryptMessageInput'),
|
|
encryptBtn: document.getElementById('encryptBtn'),
|
|
encryptResult: document.getElementById('encryptResult'),
|
|
|
|
// Crypto operation elements - Decrypt tab
|
|
encryptedMessageInput: document.getElementById('encryptedMessageInput'),
|
|
decryptBtn: document.getElementById('decryptBtn'),
|
|
decryptResult: document.getElementById('decryptResult'),
|
|
|
|
// Crypto operation elements - Verify tab
|
|
verifyMessageInput: document.getElementById('verifyMessageInput'),
|
|
signatureToVerifyInput: document.getElementById('signatureToVerifyInput'),
|
|
verifyBtn: document.getElementById('verifyBtn'),
|
|
verifyResult: document.getElementById('verifyResult'),
|
|
};
|
|
|
|
// Global state variables
|
|
let currentKeyspace = null;
|
|
let selectedKeypairId = null;
|
|
let backgroundPort = null;
|
|
let sessionTimeoutDuration = 15; // Default 15 seconds
|
|
|
|
// Session timeout management
|
|
function handleError(error, context, shouldShowToast = true) {
|
|
const errorMessage = error?.message || 'An unexpected error occurred';
|
|
|
|
if (shouldShowToast) {
|
|
showToast(`${context}: ${errorMessage}`, 'error');
|
|
}
|
|
}
|
|
|
|
function validateInput(value, fieldName, options = {}) {
|
|
const { minLength = 1, maxLength = 1000, required = true } = options;
|
|
|
|
if (required && (!value || !value.trim())) {
|
|
showToast(`${fieldName} is required`, 'error');
|
|
return false;
|
|
}
|
|
|
|
if (value && value.length < minLength) {
|
|
showToast(`${fieldName} must be at least ${minLength} characters`, 'error');
|
|
return false;
|
|
}
|
|
|
|
if (value && value.length > maxLength) {
|
|
showToast(`${fieldName} must be less than ${maxLength} characters`, 'error');
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
async function loadTimeoutSetting() {
|
|
const result = await chrome.storage.local.get(['sessionTimeout']);
|
|
sessionTimeoutDuration = result.sessionTimeout || 15;
|
|
if (elements.timeoutInput) {
|
|
elements.timeoutInput.value = sessionTimeoutDuration;
|
|
}
|
|
}
|
|
|
|
async function checkSessionTimeout() {
|
|
const result = await chrome.storage.local.get(['sessionTimedOut']);
|
|
if (result.sessionTimedOut) {
|
|
// Clear the flag
|
|
await chrome.storage.local.remove(['sessionTimedOut']);
|
|
// Show timeout notification
|
|
showToast('Session timed out due to inactivity', 'info');
|
|
}
|
|
}
|
|
async function saveTimeoutSetting(timeout) {
|
|
sessionTimeoutDuration = timeout;
|
|
await sendMessage('updateTimeout', { timeout });
|
|
}
|
|
|
|
async function resetSessionTimeout() {
|
|
if (currentKeyspace) {
|
|
await sendMessage('resetTimeout');
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
|
|
// Settings dropdown management
|
|
function toggleSettingsDropdown() {
|
|
const dropdown = elements.settingsDropdown;
|
|
if (dropdown) {
|
|
dropdown.classList.toggle('hidden');
|
|
}
|
|
}
|
|
|
|
function closeSettingsDropdown() {
|
|
const dropdown = elements.settingsDropdown;
|
|
if (dropdown) {
|
|
dropdown.classList.add('hidden');
|
|
}
|
|
}
|
|
|
|
function updateThemeIcon(theme) {
|
|
const themeToggle = elements.themeToggle;
|
|
if (!themeToggle) return;
|
|
|
|
if (theme === 'dark') {
|
|
// Bright sun SVG for dark theme
|
|
themeToggle.innerHTML = `
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<circle cx="12" cy="12" r="5"></circle>
|
|
<line x1="12" y1="1" x2="12" y2="3"></line>
|
|
<line x1="12" y1="21" x2="12" y2="23"></line>
|
|
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
|
|
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
|
|
<line x1="1" y1="12" x2="3" y2="12"></line>
|
|
<line x1="21" y1="12" x2="23" y2="12"></line>
|
|
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
|
|
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
|
|
</svg>
|
|
`;
|
|
themeToggle.title = 'Switch to light mode';
|
|
} else {
|
|
// Dark crescent moon SVG for light theme
|
|
themeToggle.innerHTML = `
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
|
|
</svg>
|
|
`;
|
|
themeToggle.title = 'Switch to dark mode';
|
|
}
|
|
}
|
|
|
|
// Establish connection to background script for keep-alive
|
|
function connectToBackground() {
|
|
backgroundPort = chrome.runtime.connect({ name: 'popup' });
|
|
|
|
// Listen for messages from background script
|
|
backgroundPort.onMessage.addListener((message) => {
|
|
if (message.type === 'sessionTimeout') {
|
|
// Update UI state to reflect locked session
|
|
currentKeyspace = null;
|
|
selectedKeypairId = null;
|
|
setStatus('', false);
|
|
showSection('authSection');
|
|
clearVaultState();
|
|
|
|
// Clear form inputs
|
|
if (elements.keyspaceInput) elements.keyspaceInput.value = '';
|
|
if (elements.passwordInput) elements.passwordInput.value = '';
|
|
|
|
// Show timeout notification
|
|
showToast(message.message, 'info');
|
|
}
|
|
});
|
|
|
|
backgroundPort.onDisconnect.addListener(() => {
|
|
backgroundPort = null;
|
|
});
|
|
}
|
|
|
|
// Initialize
|
|
document.addEventListener('DOMContentLoaded', async function() {
|
|
// Initialize theme first
|
|
initializeTheme();
|
|
|
|
// Load timeout setting
|
|
await loadTimeoutSetting();
|
|
|
|
// Ensure lock button starts hidden
|
|
const lockBtn = document.getElementById('lockBtn');
|
|
if (lockBtn) {
|
|
lockBtn.classList.add('hidden');
|
|
}
|
|
|
|
// Connect to background script for keep-alive
|
|
connectToBackground();
|
|
|
|
// Consolidated event listeners
|
|
const eventMap = {
|
|
createKeyspaceBtn: createKeyspace,
|
|
loginBtn: login,
|
|
lockBtn: lockSession,
|
|
themeToggle: toggleTheme,
|
|
settingsToggle: toggleSettingsDropdown,
|
|
toggleAddKeypairBtn: toggleAddKeypairForm,
|
|
addKeypairBtn: addKeypair,
|
|
cancelAddKeypairBtn: hideAddKeypairForm,
|
|
signBtn: signMessage,
|
|
encryptBtn: encryptMessage,
|
|
decryptBtn: decryptMessage,
|
|
verifyBtn: verifySignature
|
|
};
|
|
|
|
Object.entries(eventMap).forEach(([elementKey, handler]) => {
|
|
elements[elementKey]?.addEventListener('click', handler);
|
|
});
|
|
|
|
// Tab functionality
|
|
initializeTabs();
|
|
|
|
// Additional event listeners
|
|
elements.copySignatureBtn?.addEventListener('click', () => {
|
|
copyToClipboard(document.getElementById('signatureValue')?.textContent);
|
|
});
|
|
|
|
elements.messageInput?.addEventListener('input', () => {
|
|
if (elements.signBtn) {
|
|
elements.signBtn.disabled = !elements.messageInput.value.trim() || !selectedKeypairId;
|
|
}
|
|
});
|
|
|
|
// Timeout setting event listener
|
|
elements.timeoutInput?.addEventListener('change', async (e) => {
|
|
const timeout = parseInt(e.target.value);
|
|
if (timeout >= 3 && timeout <= 300) {
|
|
await saveTimeoutSetting(timeout);
|
|
} else {
|
|
e.target.value = sessionTimeoutDuration; // Reset to current value if invalid
|
|
}
|
|
});
|
|
|
|
// Activity detection - reset timeout on any interaction
|
|
document.addEventListener('click', (e) => {
|
|
resetSessionTimeout();
|
|
|
|
// Close settings dropdown if clicking outside
|
|
if (!elements.settingsToggle?.contains(e.target) &&
|
|
!elements.settingsDropdown?.contains(e.target)) {
|
|
closeSettingsDropdown();
|
|
}
|
|
});
|
|
document.addEventListener('keydown', resetSessionTimeout);
|
|
document.addEventListener('input', resetSessionTimeout);
|
|
|
|
// Keyboard shortcuts
|
|
document.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Escape' && !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(currentKeyspace, true);
|
|
showSection('vaultSection');
|
|
await loadKeypairs();
|
|
} else {
|
|
// No active session
|
|
setStatus('', false);
|
|
showSection('authSection');
|
|
}
|
|
} catch (error) {
|
|
setStatus('', 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 tabContainer = document.querySelector('.operation-tabs');
|
|
|
|
if (tabContainer) {
|
|
// Use event delegation for better performance
|
|
tabContainer.addEventListener('click', (e) => {
|
|
if (e.target.classList.contains('tab-btn')) {
|
|
handleTabSwitch(e.target);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Initialize input validation
|
|
initializeInputValidation();
|
|
}
|
|
|
|
function handleTabSwitch(clickedTab) {
|
|
const targetTab = clickedTab.getAttribute('data-tab');
|
|
const tabButtons = document.querySelectorAll('.tab-btn');
|
|
const tabContents = document.querySelectorAll('.tab-content');
|
|
|
|
// 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
|
|
clickedTab.classList.add('active');
|
|
const targetContent = document.getElementById(`${targetTab}-tab`);
|
|
if (targetContent) {
|
|
targetContent.classList.add('active');
|
|
}
|
|
|
|
// Clear results when switching tabs
|
|
clearTabResults();
|
|
|
|
// Update button states
|
|
updateButtonStates();
|
|
}
|
|
|
|
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 = '<div class="loading">Loading keypairs...</div>';
|
|
}
|
|
}
|
|
|
|
// Validation utilities
|
|
const validateAuth = () => {
|
|
const keyspace = elements.keyspaceInput.value.trim();
|
|
const password = elements.passwordInput.value.trim();
|
|
|
|
if (!validateInput(keyspace, 'Keyspace name', { minLength: 1, maxLength: 100 })) {
|
|
return null;
|
|
}
|
|
|
|
if (!validateInput(password, 'Password', { minLength: 1, maxLength: 1000 })) {
|
|
return null;
|
|
}
|
|
|
|
return { keyspace, password };
|
|
};
|
|
|
|
async function createKeyspace() {
|
|
const auth = validateAuth();
|
|
if (!auth) return;
|
|
|
|
try {
|
|
await executeOperation(
|
|
async () => {
|
|
const response = await sendMessage('createKeyspace', auth);
|
|
if (response?.success) {
|
|
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) {
|
|
handleError(error, 'Create keyspace');
|
|
}
|
|
}
|
|
|
|
async function login() {
|
|
const auth = validateAuth();
|
|
if (!auth) return;
|
|
|
|
try {
|
|
await executeOperation(
|
|
async () => {
|
|
const response = await sendMessage('initSession', auth);
|
|
if (response?.success) {
|
|
currentKeyspace = auth.keyspace;
|
|
setStatus(auth.keyspace, true);
|
|
showSection('vaultSection');
|
|
clearVaultState();
|
|
await loadKeypairs();
|
|
|
|
return response;
|
|
} else {
|
|
throw new Error(getResponseError(response, 'login'));
|
|
}
|
|
},
|
|
{
|
|
loadingElement: elements.loginBtn,
|
|
successMessage: 'Logged in successfully!',
|
|
maxRetries: 2
|
|
}
|
|
);
|
|
} catch (error) {
|
|
handleError(error, 'Create keyspace');
|
|
}
|
|
}
|
|
|
|
async function lockSession() {
|
|
try {
|
|
await sendMessage('lockSession');
|
|
currentKeyspace = null;
|
|
selectedKeypairId = null;
|
|
setStatus('', 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;
|
|
}
|
|
|
|
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!'
|
|
}
|
|
);
|
|
}
|
|
|
|
async function loadKeypairs() {
|
|
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 = '<div class="empty-state">Failed to load keypairs. Try refreshing.</div>';
|
|
showToast(errorMsg, 'error');
|
|
}
|
|
}
|
|
|
|
function renderKeypairs(keypairs) {
|
|
const container = elements.keypairsList;
|
|
|
|
// Simple array handling
|
|
const keypairArray = Array.isArray(keypairs) ? keypairs : [];
|
|
|
|
if (keypairArray.length === 0) {
|
|
container.innerHTML = '<div class="empty-state">No keypairs found. Add one above.</div>';
|
|
return;
|
|
}
|
|
|
|
container.innerHTML = keypairArray.map((keypair) => {
|
|
const metadata = typeof keypair.metadata === 'string'
|
|
? JSON.parse(keypair.metadata)
|
|
: keypair.metadata;
|
|
|
|
return `
|
|
<div class="keypair-item" data-keypair-id="${keypair.id}" role="button" tabindex="0">
|
|
<div class="keypair-info">
|
|
<div class="keypair-header">
|
|
<div class="keypair-name">${metadata.name || 'Unnamed'}</div>
|
|
<div class="keypair-type">${keypair.key_type}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
|
|
// Add event listeners to all keypair cards
|
|
container.querySelectorAll('.keypair-item').forEach(card => {
|
|
card.addEventListener('click', (e) => {
|
|
const keypairId = e.currentTarget.getAttribute('data-keypair-id');
|
|
selectKeypair(keypairId);
|
|
});
|
|
|
|
// Add keyboard support
|
|
card.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
e.preventDefault();
|
|
const keypairId = e.currentTarget.getAttribute('data-keypair-id');
|
|
selectKeypair(keypairId);
|
|
}
|
|
});
|
|
});
|
|
|
|
// Restore selection state if there was a previously selected keypair
|
|
if (selectedKeypairId) {
|
|
updateKeypairSelection(selectedKeypairId);
|
|
}
|
|
}
|
|
|
|
async function selectKeypair(keyId) {
|
|
// Don't show loading overlay for selection - it's too disruptive
|
|
try {
|
|
// Update visual state immediately for better UX
|
|
updateKeypairSelection(keyId);
|
|
|
|
await sendMessage('selectKeypair', { keyId });
|
|
selectedKeypairId = keyId;
|
|
|
|
// Get keypair details for internal use (but don't show the card)
|
|
const metadataResponse = await sendMessage('getCurrentKeypairMetadata');
|
|
const publicKeyResponse = await sendMessage('getCurrentKeypairPublicKey');
|
|
|
|
if (metadataResponse && metadataResponse.success && publicKeyResponse && publicKeyResponse.success) {
|
|
// Enable sign button if message is entered
|
|
updateButtonStates();
|
|
} else {
|
|
// Handle metadata or public key fetch failure
|
|
const metadataError = getResponseError(metadataResponse, 'get keypair metadata');
|
|
const publicKeyError = getResponseError(publicKeyResponse, 'get public key');
|
|
const errorMsg = metadataResponse && !metadataResponse.success ? metadataError : publicKeyError;
|
|
|
|
updateKeypairSelection(null);
|
|
showToast(errorMsg, 'error');
|
|
}
|
|
} catch (error) {
|
|
const errorMsg = getErrorMessage(error, 'Failed to select keypair');
|
|
// Revert visual state if there was an error
|
|
updateKeypairSelection(null);
|
|
showToast(errorMsg, 'error');
|
|
}
|
|
}
|
|
|
|
function updateKeypairSelection(selectedId) {
|
|
// Remove previous selection styling from all keypair items
|
|
const allKeypairs = document.querySelectorAll('.keypair-item');
|
|
allKeypairs.forEach(item => {
|
|
item.classList.remove('selected');
|
|
});
|
|
|
|
// Add selection styling to the selected keypair (if any)
|
|
if (selectedId) {
|
|
const selectedKeypair = document.querySelector(`[data-keypair-id="${selectedId}"]`);
|
|
if (selectedKeypair) {
|
|
selectedKeypair.classList.add('selected');
|
|
}
|
|
}
|
|
}
|
|
|
|
// Shared templates
|
|
const copyIcon = `<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>`;
|
|
|
|
const createResultContainer = (label, value, btnId) => `
|
|
<label>${label}:</label>
|
|
<div class="signature-container">
|
|
<code id="${value}Value">${value}</code>
|
|
<button id="${btnId}" class="btn-copy" title="Copy">${copyIcon}</button>
|
|
</div>
|
|
`;
|
|
|
|
// Unified crypto operation handler
|
|
async function performCryptoOperation(config) {
|
|
const { input, validation, action, resultElement, button, successMsg, resultProcessor } = config;
|
|
|
|
if (!validation()) {
|
|
showToast(config.errorMsg, 'error');
|
|
return;
|
|
}
|
|
|
|
await executeOperation(
|
|
async () => {
|
|
const response = await sendMessage(action, input());
|
|
if (response?.success) {
|
|
resultElement.classList.remove('hidden');
|
|
resultElement.innerHTML = resultProcessor(response);
|
|
|
|
// Add copy button listener if result has copy button
|
|
const copyBtn = resultElement.querySelector('.btn-copy');
|
|
if (copyBtn && config.copyValue) {
|
|
copyBtn.addEventListener('click', () => copyToClipboard(config.copyValue(response)));
|
|
}
|
|
|
|
return response;
|
|
} else {
|
|
throw new Error(getResponseError(response, action));
|
|
}
|
|
},
|
|
{ loadingElement: button, successMessage: successMsg }
|
|
);
|
|
}
|
|
|
|
// Crypto operation functions using shared templates
|
|
const signMessage = () => performCryptoOperation({
|
|
validation: () => elements.messageInput.value.trim() && selectedKeypairId,
|
|
errorMsg: 'Please enter a message and select a keypair',
|
|
action: 'sign',
|
|
input: () => ({ message: stringToUint8Array(elements.messageInput.value.trim()) }),
|
|
resultElement: elements.signatureResult,
|
|
button: elements.signBtn,
|
|
successMsg: 'Message signed successfully!',
|
|
copyValue: (response) => response.signature,
|
|
resultProcessor: (response) => createResultContainer('Signature', response.signature, 'copySignatureBtn')
|
|
});
|
|
|
|
const encryptMessage = () => performCryptoOperation({
|
|
validation: () => elements.encryptMessageInput.value.trim() && currentKeyspace,
|
|
errorMsg: 'Please enter a message and ensure you are connected to a keyspace',
|
|
action: 'encrypt',
|
|
input: () => ({ message: elements.encryptMessageInput.value.trim() }),
|
|
resultElement: elements.encryptResult,
|
|
button: elements.encryptBtn,
|
|
successMsg: 'Message encrypted successfully!',
|
|
copyValue: (response) => response.encryptedMessage,
|
|
resultProcessor: (response) => createResultContainer('Encrypted Message', response.encryptedMessage, 'copyEncryptedBtn')
|
|
});
|
|
|
|
const decryptMessage = () => performCryptoOperation({
|
|
validation: () => elements.encryptedMessageInput.value.trim() && currentKeyspace,
|
|
errorMsg: 'Please enter encrypted message and ensure you are connected to a keyspace',
|
|
action: 'decrypt',
|
|
input: () => ({ encryptedMessage: elements.encryptedMessageInput.value.trim() }),
|
|
resultElement: elements.decryptResult,
|
|
button: elements.decryptBtn,
|
|
successMsg: 'Message decrypted successfully!',
|
|
copyValue: (response) => response.decryptedMessage,
|
|
resultProcessor: (response) => createResultContainer('Decrypted Message', response.decryptedMessage, 'copyDecryptedBtn')
|
|
});
|
|
|
|
const verifySignature = () => performCryptoOperation({
|
|
validation: () => elements.verifyMessageInput.value.trim() && elements.signatureToVerifyInput.value.trim() && selectedKeypairId,
|
|
errorMsg: 'Please enter message, signature, and select a keypair',
|
|
action: 'verify',
|
|
input: () => ({
|
|
message: stringToUint8Array(elements.verifyMessageInput.value.trim()),
|
|
signature: elements.signatureToVerifyInput.value.trim()
|
|
}),
|
|
resultElement: elements.verifyResult,
|
|
button: elements.verifyBtn,
|
|
successMsg: null,
|
|
resultProcessor: (response) => {
|
|
const isValid = response.isValid;
|
|
const icon = isValid
|
|
? `<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="20,6 9,17 4,12"></polyline>
|
|
</svg>`
|
|
: `<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>
|
|
<line x1="15" y1="9" x2="9" y2="15"></line>
|
|
<line x1="9" y1="9" x2="15" y2="15"></line>
|
|
</svg>`;
|
|
const text = isValid ? 'Signature is valid' : 'Signature is invalid';
|
|
return `<div class="verification-status ${isValid ? 'valid' : 'invalid'}">
|
|
<span class="verification-icon">${icon}</span>
|
|
<span>${text}</span>
|
|
</div>`;
|
|
}
|
|
}); |