953 lines
31 KiB
JavaScript
953 lines
31 KiB
JavaScript
// 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 = `
|
||
<div class="toast-icon">${icon}</div>
|
||
<div class="toast-content">
|
||
<div class="toast-message">${message}</div>
|
||
</div>
|
||
<button class="toast-close" onclick="this.parentElement.remove()">×</button>
|
||
`;
|
||
|
||
// 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 `<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>`;
|
||
case 'error':
|
||
return `<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>`;
|
||
case 'info':
|
||
default:
|
||
return `<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="12" y1="16" x2="12" y2="12"></line>
|
||
<line x1="12" y1="8" x2="12.01" y2="8"></line>
|
||
</svg>`;
|
||
}
|
||
}
|
||
|
||
function showLoading(show = true) {
|
||
const overlay = document.getElementById('loadingOverlay');
|
||
overlay.classList.toggle('hidden', !show);
|
||
}
|
||
|
||
// 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'),
|
||
selectedKeypairCard: document.getElementById('selectedKeypairCard'),
|
||
|
||
// 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 = `
|
||
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" stroke="none">
|
||
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" fill="#333"/>
|
||
</svg>
|
||
`;
|
||
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');
|
||
|
||
// Scroll the selected tab into view
|
||
scrollTabIntoView(button);
|
||
|
||
// Clear results when switching tabs
|
||
clearTabResults();
|
||
|
||
// Update button states
|
||
updateButtonStates();
|
||
});
|
||
});
|
||
|
||
// Initialize input validation
|
||
initializeInputValidation();
|
||
}
|
||
|
||
function scrollTabIntoView(selectedTab) {
|
||
// Simple scroll into view
|
||
if (selectedTab) {
|
||
selectedTab.scrollIntoView({ behavior: 'smooth', inline: 'center' });
|
||
}
|
||
}
|
||
|
||
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();
|
||
|
||
// Clear selected keypair info (hidden elements) with null checks
|
||
const selectedName = document.getElementById('selectedName');
|
||
const selectedType = document.getElementById('selectedType');
|
||
const selectedPublicKey = document.getElementById('selectedPublicKey');
|
||
|
||
if (selectedName) selectedName.textContent = '-';
|
||
if (selectedType) selectedType.textContent = '-';
|
||
if (selectedPublicKey) selectedPublicKey.textContent = '-';
|
||
|
||
// Hide add keypair form if open
|
||
hideAddKeypairForm();
|
||
|
||
// Clear keypairs list
|
||
if (elements.keypairsList) {
|
||
elements.keypairsList.innerHTML = '<div class="loading">Loading keypairs...</div>';
|
||
}
|
||
}
|
||
|
||
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() {
|
||
showLoading(true);
|
||
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');
|
||
} finally {
|
||
showLoading(false);
|
||
}
|
||
}
|
||
|
||
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 = '<div class="empty-state">Failed to load keypairs. Try refreshing.</div>';
|
||
showToast(errorMsg, 'error');
|
||
}
|
||
} catch (error) {
|
||
const errorMsg = getErrorMessage(error, 'Failed to load keypairs');
|
||
console.error('Error loading keypairs:', error);
|
||
const container = elements.keypairsList;
|
||
container.innerHTML = '<div class="empty-state">Error loading 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-id="${keypair.id}">
|
||
<div class="keypair-info">
|
||
<div class="keypair-name">${metadata.name || 'Unnamed'}</div>
|
||
<div class="keypair-type">${keypair.key_type}</div>
|
||
</div>
|
||
<button class="btn btn-small select-btn" data-keypair-id="${keypair.id}">
|
||
Select
|
||
</button>
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
|
||
// Add event listeners to all select buttons
|
||
container.querySelectorAll('.select-btn').forEach(button => {
|
||
button.addEventListener('click', (e) => {
|
||
const keypairId = e.target.getAttribute('data-keypair-id');
|
||
selectKeypair(keypairId);
|
||
});
|
||
});
|
||
}
|
||
|
||
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) {
|
||
const metadata = metadataResponse.metadata;
|
||
|
||
// Store the details in hidden elements for internal use
|
||
document.getElementById('selectedName').textContent = metadata.name || 'Unnamed';
|
||
document.getElementById('selectedType').textContent = metadata.key_type;
|
||
document.getElementById('selectedPublicKey').textContent = publicKeyResponse.publicKey;
|
||
|
||
// 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');
|
||
console.error('Error selecting keypair:', error);
|
||
// Revert visual state if there was an error
|
||
updateKeypairSelection(null);
|
||
showToast(errorMsg, 'error');
|
||
}
|
||
}
|
||
|
||
function updateKeypairSelection(selectedId) {
|
||
// Remove previous selection styling
|
||
const allKeypairs = document.querySelectorAll('.keypair-item');
|
||
allKeypairs.forEach(item => {
|
||
item.classList.remove('selected');
|
||
const button = item.querySelector('.select-btn');
|
||
button.textContent = 'Select';
|
||
button.classList.remove('selected');
|
||
});
|
||
|
||
// Add selection styling to the selected keypair (if any)
|
||
if (selectedId) {
|
||
const selectedKeypair = document.querySelector(`[data-id="${selectedId}"]`);
|
||
if (selectedKeypair) {
|
||
selectedKeypair.classList.add('selected');
|
||
const button = selectedKeypair.querySelector('.select-btn');
|
||
button.textContent = 'Selected';
|
||
button.classList.add('selected');
|
||
}
|
||
}
|
||
}
|
||
|
||
async function signMessage() {
|
||
const messageText = elements.messageInput.value.trim();
|
||
if (!messageText || !selectedKeypairId) {
|
||
showToast('Please enter a message and select a keypair', 'error');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
await executeOperation(
|
||
async () => {
|
||
const messageBytes = stringToUint8Array(messageText);
|
||
const response = await sendMessage('sign', { message: messageBytes });
|
||
|
||
if (response?.success) {
|
||
elements.signatureResult.classList.remove('hidden');
|
||
elements.signatureResult.innerHTML = `
|
||
<label>Signature:</label>
|
||
<div class="signature-container">
|
||
<code id="signatureValue">${response.signature}</code>
|
||
<button id="copySignatureBtn" class="btn-copy" title="Copy">
|
||
<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>
|
||
`;
|
||
|
||
document.getElementById('copySignatureBtn').addEventListener('click', () => {
|
||
copyToClipboard(response.signature);
|
||
});
|
||
|
||
return response;
|
||
} else {
|
||
throw new Error(getResponseError(response, 'sign message'));
|
||
}
|
||
},
|
||
{
|
||
loadingElement: elements.signBtn,
|
||
successMessage: 'Message signed successfully!'
|
||
}
|
||
);
|
||
} catch (error) {
|
||
elements.signatureResult.classList.add('hidden');
|
||
}
|
||
}
|
||
|
||
async function encryptMessage() {
|
||
const messageText = elements.encryptMessageInput.value.trim();
|
||
if (!messageText || !currentKeyspace) {
|
||
showToast('Please enter a message and ensure you are connected to a keyspace', 'error');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
await executeOperation(
|
||
async () => {
|
||
const response = await sendMessage('encrypt', { message: messageText });
|
||
|
||
if (response?.success) {
|
||
elements.encryptResult.classList.remove('hidden');
|
||
elements.encryptResult.innerHTML = `
|
||
<label>Encrypted Message:</label>
|
||
<div class="signature-container">
|
||
<code id="encryptedValue">${response.encryptedMessage}</code>
|
||
<button id="copyEncryptedBtn" class="btn-copy" title="Copy">
|
||
<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>
|
||
`;
|
||
|
||
document.getElementById('copyEncryptedBtn').addEventListener('click', () => {
|
||
copyToClipboard(response.encryptedMessage);
|
||
});
|
||
|
||
return response;
|
||
} else {
|
||
throw new Error(getResponseError(response, 'encrypt message'));
|
||
}
|
||
},
|
||
{
|
||
loadingElement: elements.encryptBtn,
|
||
successMessage: 'Message encrypted successfully!'
|
||
}
|
||
);
|
||
} catch (error) {
|
||
elements.encryptResult.classList.add('hidden');
|
||
}
|
||
}
|
||
|
||
async function decryptMessage() {
|
||
const encryptedText = elements.encryptedMessageInput.value.trim();
|
||
if (!encryptedText || !currentKeyspace) {
|
||
showToast('Please enter encrypted message and ensure you are connected to a keyspace', 'error');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
await executeOperation(
|
||
async () => {
|
||
const response = await sendMessage('decrypt', { encryptedMessage: encryptedText });
|
||
|
||
if (response?.success) {
|
||
elements.decryptResult.classList.remove('hidden');
|
||
elements.decryptResult.innerHTML = `
|
||
<label>Decrypted Message:</label>
|
||
<div class="signature-container">
|
||
<code id="decryptedValue">${response.decryptedMessage}</code>
|
||
<button id="copyDecryptedBtn" class="btn-copy" title="Copy">
|
||
<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>
|
||
`;
|
||
|
||
document.getElementById('copyDecryptedBtn').addEventListener('click', () => {
|
||
copyToClipboard(response.decryptedMessage);
|
||
});
|
||
|
||
return response;
|
||
} else {
|
||
throw new Error(getResponseError(response, 'decrypt message'));
|
||
}
|
||
},
|
||
{
|
||
loadingElement: elements.decryptBtn,
|
||
successMessage: 'Message decrypted successfully!'
|
||
}
|
||
);
|
||
} catch (error) {
|
||
elements.decryptResult.classList.add('hidden');
|
||
}
|
||
}
|
||
|
||
async function verifySignature() {
|
||
const messageText = elements.verifyMessageInput.value.trim();
|
||
const signature = elements.signatureToVerifyInput.value.trim();
|
||
if (!messageText || !signature || !selectedKeypairId) {
|
||
showToast('Please enter message, signature, and select a keypair', 'error');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
await executeOperation(
|
||
async () => {
|
||
const messageBytes = stringToUint8Array(messageText);
|
||
const response = await sendMessage('verify', { message: messageBytes, signature });
|
||
|
||
if (response?.success) {
|
||
const isValid = response.isValid;
|
||
const icon = isValid ? '✅' : '❌';
|
||
const text = isValid ? 'Signature is valid' : 'Signature is invalid';
|
||
|
||
elements.verifyResult.classList.remove('hidden');
|
||
elements.verifyResult.innerHTML = `
|
||
<div class="verification-status ${isValid ? 'valid' : 'invalid'}">
|
||
<span>${icon}</span>
|
||
<span>${text}</span>
|
||
</div>
|
||
`;
|
||
|
||
return response;
|
||
} else {
|
||
throw new Error(getResponseError(response, 'verify signature'));
|
||
}
|
||
},
|
||
{
|
||
loadingElement: elements.verifyBtn,
|
||
successMessage: null // No success message for verification
|
||
}
|
||
);
|
||
} catch (error) {
|
||
elements.verifyResult.classList.add('hidden');
|
||
}
|
||
}
|
||
|
||
// selectKeypair is now handled via event listeners, no need for global access
|