Add session timeout, refactor session manager, reduce code duplication, update icons & styling

This commit is contained in:
zaelgohary 2025-05-29 08:57:25 +03:00
parent c2c5be3409
commit 31975aa9d3
11 changed files with 765 additions and 819 deletions

View File

@ -2,6 +2,9 @@ let vault = null;
let isInitialized = false;
let currentSession = null;
let keepAliveInterval = null;
let sessionTimeoutDuration = 15; // Default 15 seconds
let sessionTimeoutId = null; // Background timer
let popupPort = null; // Track popup connection
// Utility function to convert Uint8Array to hex
function toHex(uint8Array) {
@ -9,24 +12,56 @@ function toHex(uint8Array) {
.map(b => b.toString(16).padStart(2, '0'))
.join('');
}
// Background session timeout management
async function loadTimeoutSetting() {
const result = await chrome.storage.local.get(['sessionTimeout']);
sessionTimeoutDuration = result.sessionTimeout || 15;
}
function startSessionTimeout() {
clearSessionTimeout();
if (currentSession && sessionTimeoutDuration > 0) {
sessionTimeoutId = setTimeout(async () => {
if (vault && currentSession) {
// Lock the session
vault.lock_session();
await sessionManager.clear();
// Notify popup if it's open
if (popupPort) {
popupPort.postMessage({
type: 'sessionTimeout',
message: 'Session timed out due to inactivity'
});
}
}
}, sessionTimeoutDuration * 1000);
}
}
function clearSessionTimeout() {
if (sessionTimeoutId) {
clearTimeout(sessionTimeoutId);
sessionTimeoutId = null;
}
}
function resetSessionTimeout() {
if (currentSession) {
startSessionTimeout();
}
}
// Session persistence functions
async function saveSession(keyspace) {
currentSession = { keyspace, timestamp: Date.now() };
// Save to both session and local storage for better persistence
try {
await chrome.storage.session.set({ cryptoVaultSession: currentSession });
await chrome.storage.local.set({ cryptoVaultSessionBackup: currentSession });
} catch (error) {
console.error('Failed to save session:', error);
}
}
async function loadSession() {
try {
// Try session storage first
let result = await chrome.storage.session.get(['cryptoVaultSession']);
if (result.cryptoVaultSession) {
@ -42,20 +77,13 @@ async function loadSession() {
await chrome.storage.session.set({ cryptoVaultSession: currentSession });
return currentSession;
}
} catch (error) {
console.error('Failed to load session:', error);
}
return null;
}
async function clearSession() {
currentSession = null;
try {
await chrome.storage.session.remove(['cryptoVaultSession']);
await chrome.storage.local.remove(['cryptoVaultSessionBackup']);
} catch (error) {
console.error('Failed to clear session:', error);
}
}
// Keep service worker alive
@ -64,12 +92,8 @@ function startKeepAlive() {
clearInterval(keepAliveInterval);
}
// Ping every 20 seconds to keep service worker alive
keepAliveInterval = setInterval(() => {
// Simple operation to keep service worker active
chrome.storage.session.get(['keepAlive']).catch(() => {
// Ignore errors
});
chrome.storage.session.get(['keepAlive']).catch(() => {});
}, 20000);
}
@ -80,21 +104,24 @@ function stopKeepAlive() {
}
}
// Enhanced session management with keep-alive
async function saveSessionWithKeepAlive(keyspace) {
// Consolidated session management
const sessionManager = {
async save(keyspace) {
await saveSession(keyspace);
startKeepAlive();
}
async function clearSessionWithKeepAlive() {
await loadTimeoutSetting();
startSessionTimeout();
},
async clear() {
await clearSession();
stopKeepAlive();
}
clearSessionTimeout();
}
};
async function restoreSession() {
const session = await loadSession();
if (session && vault) {
try {
// Check if the session is still valid by testing if vault is unlocked
const isUnlocked = vault.is_unlocked();
if (isUnlocked) {
@ -102,32 +129,14 @@ async function restoreSession() {
startKeepAlive();
return session;
} else {
await clearSessionWithKeepAlive();
}
} catch (error) {
console.error('Error checking session validity:', error);
await clearSessionWithKeepAlive();
await sessionManager.clear();
}
}
return null;
}
// Import WASM module functions
import init, {
create_keyspace,
init_session,
is_unlocked,
add_keypair,
list_keypairs,
select_keypair,
current_keypair_metadata,
current_keypair_public_key,
sign,
verify,
encrypt_data,
decrypt_data,
lock_session
} from './wasm/wasm_app.js';
import init, * as wasmFunctions from './wasm/wasm_app.js';
// Initialize WASM module
async function initVault() {
@ -138,23 +147,8 @@ async function initVault() {
const wasmUrl = chrome.runtime.getURL('wasm/wasm_app_bg.wasm');
await init(wasmUrl);
// Create a vault object with all the imported functions
vault = {
create_keyspace,
init_session,
is_unlocked,
add_keypair,
list_keypairs,
select_keypair,
current_keypair_metadata,
current_keypair_public_key,
sign,
verify,
encrypt_data,
decrypt_data,
lock_session
};
// Use imported functions directly
vault = wasmFunctions;
isInitialized = true;
// Try to restore previous session
@ -167,20 +161,108 @@ async function initVault() {
}
}
// Handle popup connection/disconnection
chrome.runtime.onConnect.addListener((port) => {
if (port.name === 'popup') {
// If we have an active session, ensure keep-alive is running
if (currentSession) {
startKeepAlive();
}
port.onDisconnect.addListener(() => {
// Keep the keep-alive running even after popup disconnects
// This ensures session persistence across popup closes
});
// Consolidated message handlers
const messageHandlers = {
createKeyspace: async (request) => {
await vault.create_keyspace(request.keyspace, request.password);
return { success: true };
},
initSession: async (request) => {
await vault.init_session(request.keyspace, request.password);
await sessionManager.save(request.keyspace);
return { success: true };
},
isUnlocked: () => ({ success: true, unlocked: vault.is_unlocked() }),
addKeypair: async (request) => {
const result = await vault.add_keypair(request.keyType, request.metadata);
return { success: true, result };
},
listKeypairs: async () => {
if (!vault.is_unlocked()) {
return { success: false, error: 'Session is not unlocked' };
}
});
const keypairsRaw = await vault.list_keypairs();
const keypairs = typeof keypairsRaw === 'string' ? JSON.parse(keypairsRaw) : keypairsRaw;
return { success: true, keypairs };
},
selectKeypair: (request) => {
vault.select_keypair(request.keyId);
return { success: true };
},
getCurrentKeypairMetadata: () => ({ success: true, metadata: vault.current_keypair_metadata() }),
getCurrentKeypairPublicKey: () => ({ success: true, publicKey: toHex(vault.current_keypair_public_key()) }),
sign: async (request) => {
const signature = await vault.sign(new Uint8Array(request.message));
return { success: true, signature };
},
encrypt: async (request) => {
if (!vault.is_unlocked()) {
return { success: false, error: 'Session is not unlocked' };
}
const messageBytes = new TextEncoder().encode(request.message);
const encryptedData = await vault.encrypt_data(messageBytes);
const encryptedMessage = btoa(String.fromCharCode(...new Uint8Array(encryptedData)));
return { success: true, encryptedMessage };
},
decrypt: async (request) => {
if (!vault.is_unlocked()) {
return { success: false, error: 'Session is not unlocked' };
}
const encryptedBytes = new Uint8Array(atob(request.encryptedMessage).split('').map(c => c.charCodeAt(0)));
const decryptedData = await vault.decrypt_data(encryptedBytes);
const decryptedMessage = new TextDecoder().decode(new Uint8Array(decryptedData));
return { success: true, decryptedMessage };
},
verify: async (request) => {
const metadata = vault.current_keypair_metadata();
if (!metadata) {
return { success: false, error: 'No keypair selected' };
}
const isValid = await vault.verify(new Uint8Array(request.message), request.signature);
return { success: true, isValid };
},
lockSession: async () => {
vault.lock_session();
await sessionManager.clear();
return { success: true };
},
getStatus: async () => {
const status = vault ? vault.is_unlocked() : false;
const session = await loadSession();
return {
success: true,
status,
session: session ? { keyspace: session.keyspace } : null
};
},
// Timeout management handlers
resetTimeout: async () => {
resetSessionTimeout();
return { success: true };
},
updateTimeout: async (request) => {
sessionTimeoutDuration = request.timeout;
await chrome.storage.local.set({ sessionTimeout: request.timeout });
resetSessionTimeout(); // Restart with new duration
return { success: true };
}
};
// Handle messages from popup and content scripts
chrome.runtime.onMessage.addListener((request, _sender, sendResponse) => {
@ -190,143 +272,13 @@ chrome.runtime.onMessage.addListener((request, _sender, sendResponse) => {
await initVault();
}
switch (request.action) {
case 'createKeyspace':
await vault.create_keyspace(request.keyspace, request.password);
return { success: true };
case 'initSession':
await vault.init_session(request.keyspace, request.password);
await saveSessionWithKeepAlive(request.keyspace);
return { success: true };
case 'isUnlocked':
const unlocked = vault.is_unlocked();
return { success: true, unlocked };
case 'addKeypair':
const result = await vault.add_keypair(request.keyType, request.metadata);
return { success: true, result };
case 'listKeypairs':
// Check if session is unlocked first
const isUnlocked = vault.is_unlocked();
if (!isUnlocked) {
return { success: false, error: 'Session is not unlocked' };
}
try {
const keypairsRaw = await vault.list_keypairs();
// Parse JSON string if needed
let keypairs;
if (typeof keypairsRaw === 'string') {
keypairs = JSON.parse(keypairsRaw);
const handler = messageHandlers[request.action];
if (handler) {
return await handler(request);
} else {
keypairs = keypairsRaw;
}
return { success: true, keypairs };
} catch (listError) {
console.error('Background: Error calling list_keypairs:', listError);
throw listError;
}
case 'selectKeypair':
vault.select_keypair(request.keyId);
return { success: true };
case 'getCurrentKeypairMetadata':
const metadata = vault.current_keypair_metadata();
return { success: true, metadata };
case 'getCurrentKeypairPublicKey':
const publicKey = vault.current_keypair_public_key();
const hexKey = toHex(publicKey);
return { success: true, publicKey: hexKey };
case 'sign':
const signature = await vault.sign(new Uint8Array(request.message));
return { success: true, signature };
case 'encrypt':
// Check if session is unlocked
if (!vault.is_unlocked()) {
return { success: false, error: 'Session is not unlocked' };
}
try {
// Convert message to Uint8Array for WASM
const messageBytes = new TextEncoder().encode(request.message);
// Use WASM encrypt_data function with ChaCha20-Poly1305
const encryptedData = await vault.encrypt_data(messageBytes);
// Convert result to base64 for easy handling
const encryptedMessage = btoa(String.fromCharCode(...new Uint8Array(encryptedData)));
return { success: true, encryptedMessage };
} catch (error) {
console.error('Encryption error:', error);
return { success: false, error: error.message };
}
case 'decrypt':
// Check if session is unlocked
if (!vault.is_unlocked()) {
return { success: false, error: 'Session is not unlocked' };
}
try {
// Convert base64 back to Uint8Array
const encryptedBytes = new Uint8Array(atob(request.encryptedMessage).split('').map(c => c.charCodeAt(0)));
// Use WASM decrypt_data function with ChaCha20-Poly1305
const decryptedData = await vault.decrypt_data(encryptedBytes);
// Convert result back to string
const decryptedMessage = new TextDecoder().decode(new Uint8Array(decryptedData));
return { success: true, decryptedMessage };
} catch (error) {
console.error('Decryption error:', error);
return { success: false, error: error.message };
}
case 'verify':
// Check if a keypair is selected
try {
const metadata = vault.current_keypair_metadata();
if (!metadata) {
return { success: false, error: 'No keypair selected' };
}
// Use WASM verify function
const isValid = await vault.verify(new Uint8Array(request.message), request.signature);
return { success: true, isValid };
} catch (error) {
console.error('Verification error:', error);
return { success: false, error: error.message };
}
case 'lockSession':
vault.lock_session();
await clearSessionWithKeepAlive();
return { success: true };
case 'getStatus':
const status = vault ? vault.is_unlocked() : false;
const session = await loadSession();
return {
success: true,
status,
session: session ? { keyspace: session.keyspace } : null
};
default:
throw new Error('Unknown action: ' + request.action);
}
} catch (error) {
console.error('Background script error:', error);
return { success: false, error: error.message };
}
};
@ -343,3 +295,22 @@ chrome.runtime.onStartup.addListener(() => {
chrome.runtime.onInstalled.addListener(() => {
initVault();
});
// Handle popup connection for keep-alive and timeout notifications
chrome.runtime.onConnect.addListener((port) => {
if (port.name === 'popup') {
// Track popup connection
popupPort = port;
// If we have an active session, ensure keep-alive is running
if (currentSession) {
startKeepAlive();
}
port.onDisconnect.addListener(() => {
// Popup closed, clear reference and stop keep-alive
popupPort = null;
stopKeepAlive();
});
}
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 454 B

After

Width:  |  Height:  |  Size: 676 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -1,5 +1,3 @@
// Enhanced Error Handling System for CryptoVault Extension
class CryptoVaultError extends Error {
constructor(message, code, retryable = false, userMessage = null) {
super(message);
@ -11,35 +9,24 @@ class CryptoVaultError extends Error {
}
}
// Error codes for different types of errors
const ERROR_CODES = {
// Network/Connection errors (retryable)
NETWORK_ERROR: 'NETWORK_ERROR',
TIMEOUT_ERROR: 'TIMEOUT_ERROR',
SERVICE_UNAVAILABLE: 'SERVICE_UNAVAILABLE',
// Authentication errors (not retryable)
INVALID_PASSWORD: 'INVALID_PASSWORD',
SESSION_EXPIRED: 'SESSION_EXPIRED',
UNAUTHORIZED: 'UNAUTHORIZED',
// Crypto errors (not retryable)
CRYPTO_ERROR: 'CRYPTO_ERROR',
INVALID_SIGNATURE: 'INVALID_SIGNATURE',
ENCRYPTION_FAILED: 'ENCRYPTION_FAILED',
// Validation errors (not retryable)
INVALID_INPUT: 'INVALID_INPUT',
MISSING_KEYPAIR: 'MISSING_KEYPAIR',
INVALID_FORMAT: 'INVALID_FORMAT',
// System errors (sometimes retryable)
WASM_ERROR: 'WASM_ERROR',
STORAGE_ERROR: 'STORAGE_ERROR',
UNKNOWN_ERROR: 'UNKNOWN_ERROR'
};
// User-friendly error messages
const ERROR_MESSAGES = {
[ERROR_CODES.NETWORK_ERROR]: 'Connection failed. Please check your internet connection and try again.',
[ERROR_CODES.TIMEOUT_ERROR]: 'Operation timed out. Please try again.',
@ -62,7 +49,6 @@ const ERROR_MESSAGES = {
[ERROR_CODES.UNKNOWN_ERROR]: 'An unexpected error occurred. Please try again.'
};
// Determine if an error is retryable
const RETRYABLE_ERRORS = new Set([
ERROR_CODES.NETWORK_ERROR,
ERROR_CODES.TIMEOUT_ERROR,
@ -71,11 +57,9 @@ const RETRYABLE_ERRORS = new Set([
ERROR_CODES.STORAGE_ERROR
]);
// Enhanced error classification
function classifyError(error) {
const errorMessage = getErrorMessage(error);
// Network/Connection errors
if (errorMessage.includes('fetch') || errorMessage.includes('network') || errorMessage.includes('connection')) {
return new CryptoVaultError(
errorMessage,
@ -85,7 +69,6 @@ function classifyError(error) {
);
}
// Authentication errors
if (errorMessage.includes('password') || errorMessage.includes('Invalid password')) {
return new CryptoVaultError(
errorMessage,
@ -104,7 +87,6 @@ function classifyError(error) {
);
}
// Crypto errors
if (errorMessage.includes('decryption error') || errorMessage.includes('aead::Error')) {
return new CryptoVaultError(
errorMessage,
@ -123,7 +105,6 @@ function classifyError(error) {
);
}
// Validation errors
if (errorMessage.includes('No keypair selected')) {
return new CryptoVaultError(
errorMessage,
@ -133,7 +114,6 @@ function classifyError(error) {
);
}
// WASM errors
if (errorMessage.includes('wasm') || errorMessage.includes('WASM')) {
return new CryptoVaultError(
errorMessage,
@ -143,7 +123,6 @@ function classifyError(error) {
);
}
// Default to unknown error
return new CryptoVaultError(
errorMessage,
ERROR_CODES.UNKNOWN_ERROR,
@ -152,7 +131,6 @@ function classifyError(error) {
);
}
// Get error message from various error types
function getErrorMessage(error) {
if (!error) return 'Unknown error';
@ -179,14 +157,13 @@ function getErrorMessage(error) {
return stringified;
}
} catch (e) {
// Ignore JSON stringify errors
// Silently handle JSON stringify errors
}
}
return 'Unknown error';
}
// Retry logic with exponential backoff
async function withRetry(operation, options = {}) {
const {
maxRetries = 3,
@ -205,20 +182,16 @@ async function withRetry(operation, options = {}) {
const classifiedError = classifyError(error);
lastError = classifiedError;
// Don't retry if it's the last attempt or error is not retryable
if (attempt === maxRetries || !classifiedError.retryable) {
throw classifiedError;
}
// Calculate delay with exponential backoff
const delay = Math.min(baseDelay * Math.pow(backoffFactor, attempt), maxDelay);
// Call retry callback if provided
if (onRetry) {
onRetry(attempt + 1, delay, classifiedError);
}
// Wait before retrying
await new Promise(resolve => setTimeout(resolve, delay));
}
}
@ -226,7 +199,6 @@ async function withRetry(operation, options = {}) {
throw lastError;
}
// Enhanced operation wrapper with loading states
async function executeOperation(operation, options = {}) {
const {
loadingElement = null,
@ -235,7 +207,6 @@ async function executeOperation(operation, options = {}) {
onProgress = null
} = options;
// Show loading state
if (loadingElement) {
setButtonLoading(loadingElement, true);
}
@ -253,25 +224,21 @@ async function executeOperation(operation, options = {}) {
}
});
// Show success message if provided
if (successMessage) {
showToast(successMessage, 'success');
}
return result;
} catch (error) {
// Show user-friendly error message
showToast(error.userMessage || error.message, 'error');
throw error;
} finally {
// Hide loading state
if (loadingElement) {
setButtonLoading(loadingElement, false);
}
}
}
// Export for use in other modules
window.CryptoVaultError = CryptoVaultError;
window.ERROR_CODES = ERROR_CODES;
window.classifyError = classifyError;

View File

@ -9,17 +9,23 @@
"activeTab"
],
"icons": {
"16": "icons/icon16.png",
"32": "icons/icon32.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
},
"background": {
"service_worker": "background.js",
"type": "module"
},
"action": {
"default_popup": "popup.html",
"default_icon": {
"16": "icons/icon16.png",
"32": "icons/icon32.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
}

View File

@ -12,6 +12,23 @@
<h1>CryptoVault</h1>
</div>
<div class="header-actions">
<div class="settings-container">
<button id="settingsToggle" class="btn-icon-only" title="Settings">
<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="3"></circle>
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
</svg>
</button>
<div class="settings-dropdown hidden" id="settingsDropdown">
<div class="settings-item">
<label for="timeoutInput">Session Timeout</label>
<div class="timeout-input-group">
<input type="number" id="timeoutInput" min="3" max="300" value="15">
<span>seconds</span>
</div>
</div>
</div>
</div>
<button id="themeToggle" class="btn-icon-only" title="Switch to dark mode">
<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>
@ -44,10 +61,7 @@
<!-- Status Section -->
<div class="vault-status" id="vaultStatus">
<div class="status-indicator" id="statusIndicator">
<div class="status-content">
<div class="status-dot"></div>
<span id="statusText">Initializing...</span>
</div>
<span id="statusText"></span>
<button id="lockBtn" class="btn btn-ghost btn-small hidden">
<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="3" y="11" width="18" height="11" rx="2" ry="2"></rect>

View File

@ -1,33 +1,27 @@
// Enhanced toast system
// Consolidated toast system
function showToast(message, type = 'info') {
// Remove any existing toast
const existingToast = document.querySelector('.toast-notification');
if (existingToast) {
existingToast.remove();
}
document.querySelector('.toast-notification')?.remove();
// Create new toast element
const toast = document.createElement('div');
toast.className = `toast-notification toast-${type}`;
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>'
};
// 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>
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>
<button class="toast-close" onclick="this.parentElement.remove()">×</button>
`;
<div class="toast-content"><div class="toast-message">${message}</div></div>
`
});
// 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');
@ -36,30 +30,6 @@ function showToast(message, type = 'info') {
}, 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>`;
}
}
// Enhanced loading states for buttons
function setButtonLoading(button, loading = true) {
if (loading) {
@ -109,16 +79,17 @@ function showSection(sectionId) {
}
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 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) {
// Only show lock button when connected AND status indicates unlocked session
const isUnlocked = isConnected && text.toLowerCase().startsWith('connected to');
lockBtn.classList.toggle('hidden', !isUnlocked);
lockBtn.classList.remove('hidden');
}
}
}
@ -146,12 +117,20 @@ function stringToUint8Array(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'),
@ -160,34 +139,91 @@ const elements = {
cancelAddKeypairBtn: document.getElementById('cancelAddKeypairBtn'),
keypairsList: document.getElementById('keypairsList'),
// Sign tab
// Crypto operation elements - Sign tab
messageInput: document.getElementById('messageInput'),
signBtn: document.getElementById('signBtn'),
signatureResult: document.getElementById('signatureResult'),
copySignatureBtn: document.getElementById('copySignatureBtn'),
// Encrypt tab
// Crypto operation elements - Encrypt tab
encryptMessageInput: document.getElementById('encryptMessageInput'),
encryptBtn: document.getElementById('encryptBtn'),
encryptResult: document.getElementById('encryptResult'),
// Decrypt tab
// Crypto operation elements - Decrypt tab
encryptedMessageInput: document.getElementById('encryptedMessageInput'),
decryptBtn: document.getElementById('decryptBtn'),
decryptResult: document.getElementById('decryptResult'),
// Verify tab
// 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() {
@ -205,18 +241,46 @@ function toggleTheme() {
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') {
themeToggle.innerHTML = '☀️';
// 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 better visibility
// Dark crescent moon SVG for light theme
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 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';
@ -225,14 +289,30 @@ function updateThemeIcon(theme) {
// Establish connection to background script for keep-alive
function connectToBackground() {
try {
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;
});
} catch (error) {
// Silently handle connection errors
}
}
// Initialize
@ -240,78 +320,78 @@ 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');
}
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);
}
// 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
};
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);
}
Object.entries(eventMap).forEach(([elementKey, handler]) => {
elements[elementKey]?.addEventListener('click', handler);
});
// 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);
}
// Additional event listeners
elements.copySignatureBtn?.addEventListener('click', () => {
copyToClipboard(document.getElementById('signatureValue')?.textContent);
});
}
// Enable sign button when message is entered (with null checks)
if (elements.messageInput && elements.signBtn) {
elements.messageInput.addEventListener('input', () => {
elements.messageInput?.addEventListener('input', () => {
if (elements.signBtn) {
elements.signBtn.disabled = !elements.messageInput.value.trim() || !selectedKeypairId;
});
}
});
// Basic keyboard shortcuts
// 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 && !elements.addKeypairCard.classList.contains('hidden')) {
if (e.key === 'Escape' && !elements.addKeypairCard?.classList.contains('hidden')) {
hideAddKeypairForm();
}
if (e.key === 'Enter' && e.target === elements.keyNameInput && elements.keyNameInput.value.trim()) {
@ -331,16 +411,16 @@ async function checkExistingSession() {
// Session is active
currentKeyspace = response.session.keyspace;
elements.keyspaceInput.value = currentKeyspace;
setStatus(`Connected to ${currentKeyspace}`, true);
setStatus(currentKeyspace, true);
showSection('vaultSection');
await loadKeypairs();
} else {
// No active session
setStatus('Ready', false);
setStatus('', false);
showSection('authSection');
}
} catch (error) {
setStatus('Ready', false);
setStatus('', false);
showSection('authSection');
}
}
@ -370,35 +450,44 @@ function hideAddKeypairForm() {
// 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');
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');
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();
});
});
// Initialize input validation
initializeInputValidation();
}
function clearTabResults() {
// Hide all result sections (with null checks)
if (elements.signatureResult) {
@ -488,8 +577,6 @@ function clearVaultState() {
selectedKeypairId = null;
updateButtonStates();
// Hide add keypair form if open
hideAddKeypairForm();
@ -499,24 +586,31 @@ function clearVaultState() {
}
}
async function createKeyspace() {
// Validation utilities
const validateAuth = () => {
const keyspace = elements.keyspaceInput.value.trim();
const password = elements.passwordInput.value.trim();
if (!keyspace || !password) {
showToast('Please enter keyspace name and password', 'error');
return;
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', { keyspace, password });
if (response && response.success) {
// Clear any existing state before auto-login
const response = await sendMessage('createKeyspace', auth);
if (response?.success) {
clearVaultState();
await login(); // Auto-login after creation
return response;
@ -531,32 +625,25 @@ async function createKeyspace() {
}
);
} catch (error) {
console.error('Create keyspace error:', error);
handleError(error, 'Create keyspace');
}
}
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;
}
const auth = validateAuth();
if (!auth) return;
try {
await executeOperation(
async () => {
const response = await sendMessage('initSession', { keyspace, password });
if (response && response.success) {
currentKeyspace = keyspace;
setStatus(`Connected to ${keyspace}`, true);
const response = await sendMessage('initSession', auth);
if (response?.success) {
currentKeyspace = auth.keyspace;
setStatus(auth.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'));
@ -569,7 +656,7 @@ async function login() {
}
);
} catch (error) {
console.error('Login error:', error);
handleError(error, 'Create keyspace');
}
}
@ -578,7 +665,7 @@ async function lockSession() {
await sendMessage('lockSession');
currentKeyspace = null;
selectedKeypairId = null;
setStatus('Locked', false);
setStatus('', false);
showSection('authSection');
// Clear all form inputs
@ -601,7 +688,6 @@ async function addKeypair() {
return;
}
try {
await executeOperation(
async () => {
const metadata = JSON.stringify({ name: keyName });
@ -620,13 +706,9 @@ async function addKeypair() {
successMessage: 'Keypair added successfully!'
}
);
} catch (error) {
// Error already handled by executeOperation
}
}
async function loadKeypairs() {
try {
const response = await sendMessage('listKeypairs');
if (response && response.success) {
@ -637,13 +719,6 @@ async function loadKeypairs() {
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) {
@ -724,7 +799,6 @@ async function selectKeypair(keyId) {
}
} 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');
@ -747,183 +821,114 @@ function updateKeypairSelection(selectedId) {
}
}
async function signMessage() {
const messageText = elements.messageInput.value.trim();
if (!messageText || !selectedKeypairId) {
showToast('Please enter a message and select a keypair', 'error');
// 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;
}
try {
await executeOperation(
async () => {
const messageBytes = stringToUint8Array(messageText);
const response = await sendMessage('sign', { message: messageBytes });
const response = await sendMessage(action, input());
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>
`;
resultElement.classList.remove('hidden');
resultElement.innerHTML = resultProcessor(response);
document.getElementById('copySignatureBtn').addEventListener('click', () => {
copyToClipboard(response.signature);
});
// 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, 'sign message'));
throw new Error(getResponseError(response, action));
}
},
{
loadingElement: elements.signBtn,
successMessage: 'Message signed successfully!'
}
{ loadingElement: button, successMessage: successMsg }
);
} 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;
}
// 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')
});
try {
await executeOperation(
async () => {
const response = await sendMessage('encrypt', { message: messageText });
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')
});
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>
`;
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')
});
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 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 ? '✅' : '❌';
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';
elements.verifyResult.classList.remove('hidden');
elements.verifyResult.innerHTML = `
<div class="verification-status ${isValid ? 'valid' : 'invalid'}">
<span>${icon}</span>
return `<div class="verification-status ${isValid ? 'valid' : 'invalid'}">
<span class="verification-icon">${icon}</span>
<span>${text}</span>
</div>
`;
return response;
} else {
throw new Error(getResponseError(response, 'verify signature'));
</div>`;
}
},
{
loadingElement: elements.verifyBtn,
successMessage: null // No success message for verification
}
);
} catch (error) {
elements.verifyResult.classList.add('hidden');
}
}
});

View File

@ -34,7 +34,7 @@
--accent-success: hsl(var(--accent-hue), 65%, 45%);
--accent-error: hsl(0, 70%, 55%);
--accent-warning: hsl(35, 85%, 55%);
--accent-info: hsl(var(--secondary-hue), 70%, 55%);
--accent-info: hsl(var(--primary-hue), 35%, 60%);
/* Spacing system */
--spacing-xs: 4px;
@ -75,66 +75,63 @@
--accent-success: hsl(var(--accent-hue), 60%, 55%);
--accent-error: hsl(0, 65%, 60%);
--accent-warning: hsl(35, 80%, 60%);
--accent-info: hsl(var(--secondary-hue), 65%, 60%);
--accent-info: hsl(var(--primary-hue), 30%, 70%);
}
/* Harmonious button styling system */
.btn-primary {
background: var(--bg-button-primary);
color: var(--text-button-primary);
/* Consolidated button styling system */
.btn-primary, .btn-secondary, .btn-ghost {
border: none;
font-weight: 600;
box-shadow: var(--shadow-button);
font-weight: 500;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
.btn-primary:hover {
background: hsl(var(--primary-hue), var(--primary-saturation), 50%);
box-shadow: var(--shadow-button-hover);
transform: translateY(-1px);
}
.btn-primary:active {
transform: translateY(0);
.btn-primary {
background: var(--bg-button-primary);
color: var(--text-button-primary);
font-weight: 600;
box-shadow: var(--shadow-button);
}
/* Dark theme primary button adjustments */
[data-theme="dark"] .btn-primary:hover {
background: hsl(var(--primary-hue), var(--primary-saturation), 65%);
}
.btn-secondary {
background: var(--bg-button-secondary);
color: var(--text-button-secondary);
border: 1px solid var(--border-color);
font-weight: 500;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
.btn-secondary:hover {
background: var(--bg-input);
border-color: var(--border-focus);
color: var(--text-primary);
transform: translateY(-1px);
}
.btn-secondary:active {
transform: translateY(0);
}
.btn-ghost {
background: var(--bg-button-ghost);
color: var(--border-focus);
border: 1px solid transparent;
font-weight: 500;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
.btn-primary:hover, .btn-secondary:hover, .btn-ghost:hover {
transform: translateY(-1px);
}
.btn-primary:active, .btn-secondary:active, .btn-ghost:active {
transform: translateY(0);
}
.btn-primary:hover {
background: hsl(var(--primary-hue), var(--primary-saturation), 50%);
box-shadow: var(--shadow-button-hover);
}
.btn-secondary:hover, .btn-ghost:hover {
background: var(--bg-input);
color: var(--text-primary);
}
.btn-secondary:hover {
border-color: var(--border-focus);
}
.btn-ghost:hover {
background: var(--bg-input);
border-color: var(--border-color);
color: var(--text-primary);
}
[data-theme="dark"] .btn-primary:hover {
background: hsl(var(--primary-hue), var(--primary-saturation), 65%);
}
* {
@ -197,6 +194,73 @@ body {
gap: var(--spacing-md);
}
.settings-container {
position: relative;
}
.settings-dropdown {
position: absolute;
top: 100%;
right: 0;
margin-top: var(--spacing-sm);
background: var(--bg-card);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: var(--spacing-lg);
box-shadow: var(--shadow-card);
backdrop-filter: blur(20px);
min-width: 200px;
z-index: 1000;
}
.settings-dropdown.hidden {
display: none;
}
.settings-item {
margin-bottom: var(--spacing-md);
}
.settings-item:last-child {
margin-bottom: 0;
}
.settings-item label {
display: block;
font-size: 14px;
font-weight: 500;
color: var(--text-secondary);
margin-bottom: var(--spacing-xs);
}
.timeout-input-group {
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.timeout-input-group input {
width: 60px;
padding: var(--spacing-xs) var(--spacing-sm);
font-size: 14px;
border: 1px solid var(--border-color);
border-radius: 8px;
background: var(--bg-input);
color: var(--text-primary);
text-align: center;
}
.timeout-input-group input:focus {
outline: none;
border-color: var(--border-focus);
box-shadow: 0 0 0 2px hsla(var(--primary-hue), var(--primary-saturation), 55%, 0.15);
}
.timeout-input-group span {
font-size: 14px;
color: var(--text-muted);
}
.btn-icon-only {
background: var(--bg-button-ghost);
border: none;
@ -224,39 +288,15 @@ body {
gap: var(--spacing-md);
}
.status-indicator .status-content {
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.status-dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--accent-warning);
box-shadow: 0 0 0 2px rgba(245, 158, 11, 0.2);
}
.status-indicator.connected .status-dot {
background: var(--accent-success);
box-shadow: 0 0 0 2px rgba(16, 185, 129, 0.2);
}
#statusText {
font-size: 14px;
font-weight: 500;
font-size: 22px;
font-weight: 700;
color: var(--text-primary);
}
/* Vault status specific styling */
.vault-status .status-indicator {
color: var(--text-primary);
}
.vault-status #statusText {
color: var(--text-primary);
}
/* Enhanced lock button styling */
#lockBtn {
@ -515,7 +555,7 @@ input::placeholder, textarea::placeholder {
.vault-header h2 {
color: var(--text-primary);
font-size: 20px;
font-size: 18px;
font-weight: 600;
}
@ -749,31 +789,6 @@ input::placeholder, textarea::placeholder {
color: var(--text-primary);
}
/* Signature Result */
.signature-result {
margin-top: 16px;
padding: 16px;
background: hsla(var(--accent-hue), 60%, 95%, 0.8);
border-radius: 12px;
border: 1px solid hsla(var(--accent-hue), 50%, 70%, 0.3);
}
[data-theme="dark"] .signature-result {
background: hsla(var(--accent-hue), 40%, 15%, 0.6);
border-color: hsla(var(--accent-hue), 50%, 40%, 0.4);
}
.signature-result label {
color: var(--accent-success);
font-weight: 500;
margin-bottom: 8px;
display: block;
}
/* Enhanced Toast Notifications */
.toast-notification {
position: fixed;
@ -828,31 +843,7 @@ input::placeholder, textarea::placeholder {
word-wrap: break-word;
}
.toast-close {
background: none;
border: none;
color: inherit;
cursor: pointer;
font-size: 18px;
font-weight: bold;
line-height: 1;
opacity: 0.7;
padding: 0;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
transition: all 0.2s ease;
flex-shrink: 0;
margin-top: 2px;
}
.toast-close:hover {
opacity: 1;
background: rgba(255, 255, 255, 0.1);
}
/* Success Toast */
.toast-success {
@ -880,9 +871,9 @@ input::placeholder, textarea::placeholder {
/* Info Toast */
.toast-info {
background: linear-gradient(135deg, rgba(59, 130, 246, 0.95) 0%, rgba(37, 99, 235, 0.95) 100%);
background: var(--accent-info);
color: white;
border-color: rgba(59, 130, 246, 0.3);
border-color: hsla(var(--primary-hue), 35%, 60%, 0.3);
}
.toast-info .toast-icon {
@ -981,10 +972,7 @@ input::placeholder, textarea::placeholder {
color: var(--text-primary);
}
[data-theme="dark"] .tab-btn:hover {
color: var(--border-focus);
}
[data-theme="dark"] .tab-btn:hover,
[data-theme="dark"] .tab-btn.active {
color: var(--border-focus);
}
@ -997,8 +985,8 @@ input::placeholder, textarea::placeholder {
display: block;
}
/* Result Styling */
.encrypt-result, .decrypt-result, .verify-result {
/* Consolidated result styling */
.encrypt-result, .decrypt-result, .verify-result, .signature-result {
margin-top: 16px;
padding: 16px;
border-radius: 12px;
@ -1006,55 +994,55 @@ input::placeholder, textarea::placeholder {
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
.encrypt-result {
.encrypt-result, .signature-result {
background: hsla(var(--accent-hue), 60%, 95%, 0.8);
border-color: hsla(var(--accent-hue), 50%, 70%, 0.3);
box-shadow: 0 2px 12px hsla(var(--accent-hue), 50%, 50%, 0.1);
}
[data-theme="dark"] .encrypt-result {
background: hsla(var(--accent-hue), 40%, 15%, 0.6);
border-color: hsla(var(--accent-hue), 50%, 40%, 0.4);
}
.encrypt-result label {
color: var(--accent-success);
font-weight: 600;
margin-bottom: 12px;
display: block;
font-size: 14px;
}
.decrypt-result {
background: hsla(var(--secondary-hue), 60%, 95%, 0.8);
border-color: hsla(var(--secondary-hue), 50%, 70%, 0.3);
box-shadow: 0 2px 12px hsla(var(--secondary-hue), 50%, 50%, 0.1);
}
[data-theme="dark"] .decrypt-result {
background: hsla(var(--secondary-hue), 40%, 15%, 0.6);
border-color: hsla(var(--secondary-hue), 50%, 40%, 0.4);
}
.decrypt-result label {
color: var(--accent-info);
font-weight: 600;
margin-bottom: 12px;
display: block;
font-size: 14px;
}
.verify-result {
background: hsla(var(--primary-hue), 30%, 95%, 0.8);
border-color: hsla(var(--primary-hue), 40%, 70%, 0.3);
box-shadow: 0 2px 12px hsla(var(--primary-hue), 40%, 50%, 0.1);
}
[data-theme="dark"] .encrypt-result,
[data-theme="dark"] .signature-result {
background: hsla(var(--accent-hue), 40%, 15%, 0.6);
border-color: hsla(var(--accent-hue), 50%, 40%, 0.4);
}
[data-theme="dark"] .decrypt-result {
background: hsla(var(--secondary-hue), 40%, 15%, 0.6);
border-color: hsla(var(--secondary-hue), 50%, 40%, 0.4);
}
[data-theme="dark"] .verify-result {
background: hsla(var(--primary-hue), 20%, 15%, 0.6);
border-color: hsla(var(--primary-hue), 30%, 40%, 0.4);
}
.encrypt-result label, .decrypt-result label, .signature-result label {
font-weight: 600;
margin-bottom: 12px;
display: block;
font-size: 14px;
}
.encrypt-result label, .signature-result label {
color: var(--accent-success);
}
.decrypt-result label {
color: var(--accent-info);
}
.verification-status {
display: flex;
align-items: center;
@ -1071,19 +1059,14 @@ input::placeholder, textarea::placeholder {
color: var(--accent-error);
}
.verification-icon {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.verification-icon svg {
width: 20px;
height: 20px;
}