Add session timeout, refactor session manager, reduce code duplication, update icons & styling
@ -2,6 +2,9 @@ let vault = null;
|
|||||||
let isInitialized = false;
|
let isInitialized = false;
|
||||||
let currentSession = null;
|
let currentSession = null;
|
||||||
let keepAliveInterval = 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
|
// Utility function to convert Uint8Array to hex
|
||||||
function toHex(uint8Array) {
|
function toHex(uint8Array) {
|
||||||
@ -9,53 +12,78 @@ function toHex(uint8Array) {
|
|||||||
.map(b => b.toString(16).padStart(2, '0'))
|
.map(b => b.toString(16).padStart(2, '0'))
|
||||||
.join('');
|
.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
|
// Session persistence functions
|
||||||
async function saveSession(keyspace) {
|
async function saveSession(keyspace) {
|
||||||
currentSession = { keyspace, timestamp: Date.now() };
|
currentSession = { keyspace, timestamp: Date.now() };
|
||||||
|
|
||||||
// Save to both session and local storage for better persistence
|
// Save to both session and local storage for better persistence
|
||||||
try {
|
await chrome.storage.session.set({ cryptoVaultSession: currentSession });
|
||||||
await chrome.storage.session.set({ cryptoVaultSession: currentSession });
|
await chrome.storage.local.set({ cryptoVaultSessionBackup: currentSession });
|
||||||
await chrome.storage.local.set({ cryptoVaultSessionBackup: currentSession });
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to save session:', error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadSession() {
|
async function loadSession() {
|
||||||
try {
|
// Try session storage first
|
||||||
// Try session storage first
|
let result = await chrome.storage.session.get(['cryptoVaultSession']);
|
||||||
let result = await chrome.storage.session.get(['cryptoVaultSession']);
|
if (result.cryptoVaultSession) {
|
||||||
if (result.cryptoVaultSession) {
|
currentSession = result.cryptoVaultSession;
|
||||||
currentSession = result.cryptoVaultSession;
|
return currentSession;
|
||||||
return currentSession;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback to local storage
|
// Fallback to local storage
|
||||||
result = await chrome.storage.local.get(['cryptoVaultSessionBackup']);
|
result = await chrome.storage.local.get(['cryptoVaultSessionBackup']);
|
||||||
if (result.cryptoVaultSessionBackup) {
|
if (result.cryptoVaultSessionBackup) {
|
||||||
currentSession = result.cryptoVaultSessionBackup;
|
currentSession = result.cryptoVaultSessionBackup;
|
||||||
// Restore to session storage
|
// Restore to session storage
|
||||||
await chrome.storage.session.set({ cryptoVaultSession: currentSession });
|
await chrome.storage.session.set({ cryptoVaultSession: currentSession });
|
||||||
return currentSession;
|
return currentSession;
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to load session:', error);
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function clearSession() {
|
async function clearSession() {
|
||||||
currentSession = null;
|
currentSession = null;
|
||||||
try {
|
await chrome.storage.session.remove(['cryptoVaultSession']);
|
||||||
await chrome.storage.session.remove(['cryptoVaultSession']);
|
await chrome.storage.local.remove(['cryptoVaultSessionBackup']);
|
||||||
await chrome.storage.local.remove(['cryptoVaultSessionBackup']);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to clear session:', error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keep service worker alive
|
// Keep service worker alive
|
||||||
@ -64,12 +92,8 @@ function startKeepAlive() {
|
|||||||
clearInterval(keepAliveInterval);
|
clearInterval(keepAliveInterval);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ping every 20 seconds to keep service worker alive
|
|
||||||
keepAliveInterval = setInterval(() => {
|
keepAliveInterval = setInterval(() => {
|
||||||
// Simple operation to keep service worker active
|
chrome.storage.session.get(['keepAlive']).catch(() => {});
|
||||||
chrome.storage.session.get(['keepAlive']).catch(() => {
|
|
||||||
// Ignore errors
|
|
||||||
});
|
|
||||||
}, 20000);
|
}, 20000);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,54 +104,39 @@ function stopKeepAlive() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enhanced session management with keep-alive
|
// Consolidated session management
|
||||||
async function saveSessionWithKeepAlive(keyspace) {
|
const sessionManager = {
|
||||||
await saveSession(keyspace);
|
async save(keyspace) {
|
||||||
startKeepAlive();
|
await saveSession(keyspace);
|
||||||
}
|
startKeepAlive();
|
||||||
|
await loadTimeoutSetting();
|
||||||
async function clearSessionWithKeepAlive() {
|
startSessionTimeout();
|
||||||
await clearSession();
|
},
|
||||||
stopKeepAlive();
|
async clear() {
|
||||||
}
|
await clearSession();
|
||||||
|
stopKeepAlive();
|
||||||
|
clearSessionTimeout();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
async function restoreSession() {
|
async function restoreSession() {
|
||||||
const session = await loadSession();
|
const session = await loadSession();
|
||||||
if (session && vault) {
|
if (session && vault) {
|
||||||
try {
|
// Check if the session is still valid by testing if vault is unlocked
|
||||||
// Check if the session is still valid by testing if vault is unlocked
|
const isUnlocked = vault.is_unlocked();
|
||||||
const isUnlocked = vault.is_unlocked();
|
if (isUnlocked) {
|
||||||
if (isUnlocked) {
|
// Restart keep-alive for restored session
|
||||||
// Restart keep-alive for restored session
|
startKeepAlive();
|
||||||
startKeepAlive();
|
return session;
|
||||||
return session;
|
} else {
|
||||||
} else {
|
await sessionManager.clear();
|
||||||
await clearSessionWithKeepAlive();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error checking session validity:', error);
|
|
||||||
await clearSessionWithKeepAlive();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Import WASM module functions
|
// Import WASM module functions
|
||||||
import init, {
|
import init, * as wasmFunctions from './wasm/wasm_app.js';
|
||||||
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';
|
|
||||||
|
|
||||||
// Initialize WASM module
|
// Initialize WASM module
|
||||||
async function initVault() {
|
async function initVault() {
|
||||||
@ -138,23 +147,8 @@ async function initVault() {
|
|||||||
const wasmUrl = chrome.runtime.getURL('wasm/wasm_app_bg.wasm');
|
const wasmUrl = chrome.runtime.getURL('wasm/wasm_app_bg.wasm');
|
||||||
await init(wasmUrl);
|
await init(wasmUrl);
|
||||||
|
|
||||||
// Create a vault object with all the imported functions
|
// Use imported functions directly
|
||||||
vault = {
|
vault = wasmFunctions;
|
||||||
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
|
|
||||||
};
|
|
||||||
|
|
||||||
isInitialized = true;
|
isInitialized = true;
|
||||||
|
|
||||||
// Try to restore previous session
|
// 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(() => {
|
// Consolidated message handlers
|
||||||
// Keep the keep-alive running even after popup disconnects
|
const messageHandlers = {
|
||||||
// This ensures session persistence across popup closes
|
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
|
// Handle messages from popup and content scripts
|
||||||
chrome.runtime.onMessage.addListener((request, _sender, sendResponse) => {
|
chrome.runtime.onMessage.addListener((request, _sender, sendResponse) => {
|
||||||
@ -190,143 +272,13 @@ chrome.runtime.onMessage.addListener((request, _sender, sendResponse) => {
|
|||||||
await initVault();
|
await initVault();
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (request.action) {
|
const handler = messageHandlers[request.action];
|
||||||
case 'createKeyspace':
|
if (handler) {
|
||||||
await vault.create_keyspace(request.keyspace, request.password);
|
return await handler(request);
|
||||||
return { success: true };
|
} else {
|
||||||
|
throw new Error('Unknown action: ' + request.action);
|
||||||
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);
|
|
||||||
} 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) {
|
} catch (error) {
|
||||||
console.error('Background script error:', error);
|
|
||||||
return { success: false, error: error.message };
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -342,4 +294,23 @@ chrome.runtime.onStartup.addListener(() => {
|
|||||||
|
|
||||||
chrome.runtime.onInstalled.addListener(() => {
|
chrome.runtime.onInstalled.addListener(() => {
|
||||||
initVault();
|
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();
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 7.5 KiB |
Before Width: | Height: | Size: 454 B After Width: | Height: | Size: 676 B |
BIN
crypto_vault_extension/icons/icon32.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
crypto_vault_extension/icons/icon36.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 2.1 KiB |
@ -1,5 +1,3 @@
|
|||||||
// Enhanced Error Handling System for CryptoVault Extension
|
|
||||||
|
|
||||||
class CryptoVaultError extends Error {
|
class CryptoVaultError extends Error {
|
||||||
constructor(message, code, retryable = false, userMessage = null) {
|
constructor(message, code, retryable = false, userMessage = null) {
|
||||||
super(message);
|
super(message);
|
||||||
@ -11,35 +9,24 @@ class CryptoVaultError extends Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Error codes for different types of errors
|
|
||||||
const ERROR_CODES = {
|
const ERROR_CODES = {
|
||||||
// Network/Connection errors (retryable)
|
|
||||||
NETWORK_ERROR: 'NETWORK_ERROR',
|
NETWORK_ERROR: 'NETWORK_ERROR',
|
||||||
TIMEOUT_ERROR: 'TIMEOUT_ERROR',
|
TIMEOUT_ERROR: 'TIMEOUT_ERROR',
|
||||||
SERVICE_UNAVAILABLE: 'SERVICE_UNAVAILABLE',
|
SERVICE_UNAVAILABLE: 'SERVICE_UNAVAILABLE',
|
||||||
|
|
||||||
// Authentication errors (not retryable)
|
|
||||||
INVALID_PASSWORD: 'INVALID_PASSWORD',
|
INVALID_PASSWORD: 'INVALID_PASSWORD',
|
||||||
SESSION_EXPIRED: 'SESSION_EXPIRED',
|
SESSION_EXPIRED: 'SESSION_EXPIRED',
|
||||||
UNAUTHORIZED: 'UNAUTHORIZED',
|
UNAUTHORIZED: 'UNAUTHORIZED',
|
||||||
|
|
||||||
// Crypto errors (not retryable)
|
|
||||||
CRYPTO_ERROR: 'CRYPTO_ERROR',
|
CRYPTO_ERROR: 'CRYPTO_ERROR',
|
||||||
INVALID_SIGNATURE: 'INVALID_SIGNATURE',
|
INVALID_SIGNATURE: 'INVALID_SIGNATURE',
|
||||||
ENCRYPTION_FAILED: 'ENCRYPTION_FAILED',
|
ENCRYPTION_FAILED: 'ENCRYPTION_FAILED',
|
||||||
|
|
||||||
// Validation errors (not retryable)
|
|
||||||
INVALID_INPUT: 'INVALID_INPUT',
|
INVALID_INPUT: 'INVALID_INPUT',
|
||||||
MISSING_KEYPAIR: 'MISSING_KEYPAIR',
|
MISSING_KEYPAIR: 'MISSING_KEYPAIR',
|
||||||
INVALID_FORMAT: 'INVALID_FORMAT',
|
INVALID_FORMAT: 'INVALID_FORMAT',
|
||||||
|
|
||||||
// System errors (sometimes retryable)
|
|
||||||
WASM_ERROR: 'WASM_ERROR',
|
WASM_ERROR: 'WASM_ERROR',
|
||||||
STORAGE_ERROR: 'STORAGE_ERROR',
|
STORAGE_ERROR: 'STORAGE_ERROR',
|
||||||
UNKNOWN_ERROR: 'UNKNOWN_ERROR'
|
UNKNOWN_ERROR: 'UNKNOWN_ERROR'
|
||||||
};
|
};
|
||||||
|
|
||||||
// User-friendly error messages
|
|
||||||
const ERROR_MESSAGES = {
|
const ERROR_MESSAGES = {
|
||||||
[ERROR_CODES.NETWORK_ERROR]: 'Connection failed. Please check your internet connection and try again.',
|
[ERROR_CODES.NETWORK_ERROR]: 'Connection failed. Please check your internet connection and try again.',
|
||||||
[ERROR_CODES.TIMEOUT_ERROR]: 'Operation timed out. Please 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.'
|
[ERROR_CODES.UNKNOWN_ERROR]: 'An unexpected error occurred. Please try again.'
|
||||||
};
|
};
|
||||||
|
|
||||||
// Determine if an error is retryable
|
|
||||||
const RETRYABLE_ERRORS = new Set([
|
const RETRYABLE_ERRORS = new Set([
|
||||||
ERROR_CODES.NETWORK_ERROR,
|
ERROR_CODES.NETWORK_ERROR,
|
||||||
ERROR_CODES.TIMEOUT_ERROR,
|
ERROR_CODES.TIMEOUT_ERROR,
|
||||||
@ -71,11 +57,9 @@ const RETRYABLE_ERRORS = new Set([
|
|||||||
ERROR_CODES.STORAGE_ERROR
|
ERROR_CODES.STORAGE_ERROR
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Enhanced error classification
|
|
||||||
function classifyError(error) {
|
function classifyError(error) {
|
||||||
const errorMessage = getErrorMessage(error);
|
const errorMessage = getErrorMessage(error);
|
||||||
|
|
||||||
// Network/Connection errors
|
|
||||||
if (errorMessage.includes('fetch') || errorMessage.includes('network') || errorMessage.includes('connection')) {
|
if (errorMessage.includes('fetch') || errorMessage.includes('network') || errorMessage.includes('connection')) {
|
||||||
return new CryptoVaultError(
|
return new CryptoVaultError(
|
||||||
errorMessage,
|
errorMessage,
|
||||||
@ -85,7 +69,6 @@ function classifyError(error) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Authentication errors
|
|
||||||
if (errorMessage.includes('password') || errorMessage.includes('Invalid password')) {
|
if (errorMessage.includes('password') || errorMessage.includes('Invalid password')) {
|
||||||
return new CryptoVaultError(
|
return new CryptoVaultError(
|
||||||
errorMessage,
|
errorMessage,
|
||||||
@ -104,7 +87,6 @@ function classifyError(error) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Crypto errors
|
|
||||||
if (errorMessage.includes('decryption error') || errorMessage.includes('aead::Error')) {
|
if (errorMessage.includes('decryption error') || errorMessage.includes('aead::Error')) {
|
||||||
return new CryptoVaultError(
|
return new CryptoVaultError(
|
||||||
errorMessage,
|
errorMessage,
|
||||||
@ -123,7 +105,6 @@ function classifyError(error) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validation errors
|
|
||||||
if (errorMessage.includes('No keypair selected')) {
|
if (errorMessage.includes('No keypair selected')) {
|
||||||
return new CryptoVaultError(
|
return new CryptoVaultError(
|
||||||
errorMessage,
|
errorMessage,
|
||||||
@ -133,7 +114,6 @@ function classifyError(error) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// WASM errors
|
|
||||||
if (errorMessage.includes('wasm') || errorMessage.includes('WASM')) {
|
if (errorMessage.includes('wasm') || errorMessage.includes('WASM')) {
|
||||||
return new CryptoVaultError(
|
return new CryptoVaultError(
|
||||||
errorMessage,
|
errorMessage,
|
||||||
@ -143,7 +123,6 @@ function classifyError(error) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default to unknown error
|
|
||||||
return new CryptoVaultError(
|
return new CryptoVaultError(
|
||||||
errorMessage,
|
errorMessage,
|
||||||
ERROR_CODES.UNKNOWN_ERROR,
|
ERROR_CODES.UNKNOWN_ERROR,
|
||||||
@ -152,7 +131,6 @@ function classifyError(error) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get error message from various error types
|
|
||||||
function getErrorMessage(error) {
|
function getErrorMessage(error) {
|
||||||
if (!error) return 'Unknown error';
|
if (!error) return 'Unknown error';
|
||||||
|
|
||||||
@ -179,14 +157,13 @@ function getErrorMessage(error) {
|
|||||||
return stringified;
|
return stringified;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Ignore JSON stringify errors
|
// Silently handle JSON stringify errors
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'Unknown error';
|
return 'Unknown error';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retry logic with exponential backoff
|
|
||||||
async function withRetry(operation, options = {}) {
|
async function withRetry(operation, options = {}) {
|
||||||
const {
|
const {
|
||||||
maxRetries = 3,
|
maxRetries = 3,
|
||||||
@ -205,20 +182,16 @@ async function withRetry(operation, options = {}) {
|
|||||||
const classifiedError = classifyError(error);
|
const classifiedError = classifyError(error);
|
||||||
lastError = classifiedError;
|
lastError = classifiedError;
|
||||||
|
|
||||||
// Don't retry if it's the last attempt or error is not retryable
|
|
||||||
if (attempt === maxRetries || !classifiedError.retryable) {
|
if (attempt === maxRetries || !classifiedError.retryable) {
|
||||||
throw classifiedError;
|
throw classifiedError;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate delay with exponential backoff
|
|
||||||
const delay = Math.min(baseDelay * Math.pow(backoffFactor, attempt), maxDelay);
|
const delay = Math.min(baseDelay * Math.pow(backoffFactor, attempt), maxDelay);
|
||||||
|
|
||||||
// Call retry callback if provided
|
|
||||||
if (onRetry) {
|
if (onRetry) {
|
||||||
onRetry(attempt + 1, delay, classifiedError);
|
onRetry(attempt + 1, delay, classifiedError);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait before retrying
|
|
||||||
await new Promise(resolve => setTimeout(resolve, delay));
|
await new Promise(resolve => setTimeout(resolve, delay));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -226,7 +199,6 @@ async function withRetry(operation, options = {}) {
|
|||||||
throw lastError;
|
throw lastError;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enhanced operation wrapper with loading states
|
|
||||||
async function executeOperation(operation, options = {}) {
|
async function executeOperation(operation, options = {}) {
|
||||||
const {
|
const {
|
||||||
loadingElement = null,
|
loadingElement = null,
|
||||||
@ -235,7 +207,6 @@ async function executeOperation(operation, options = {}) {
|
|||||||
onProgress = null
|
onProgress = null
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
// Show loading state
|
|
||||||
if (loadingElement) {
|
if (loadingElement) {
|
||||||
setButtonLoading(loadingElement, true);
|
setButtonLoading(loadingElement, true);
|
||||||
}
|
}
|
||||||
@ -253,25 +224,21 @@ async function executeOperation(operation, options = {}) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Show success message if provided
|
|
||||||
if (successMessage) {
|
if (successMessage) {
|
||||||
showToast(successMessage, 'success');
|
showToast(successMessage, 'success');
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Show user-friendly error message
|
|
||||||
showToast(error.userMessage || error.message, 'error');
|
showToast(error.userMessage || error.message, 'error');
|
||||||
throw error;
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
// Hide loading state
|
|
||||||
if (loadingElement) {
|
if (loadingElement) {
|
||||||
setButtonLoading(loadingElement, false);
|
setButtonLoading(loadingElement, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Export for use in other modules
|
|
||||||
window.CryptoVaultError = CryptoVaultError;
|
window.CryptoVaultError = CryptoVaultError;
|
||||||
window.ERROR_CODES = ERROR_CODES;
|
window.ERROR_CODES = ERROR_CODES;
|
||||||
window.classifyError = classifyError;
|
window.classifyError = classifyError;
|
||||||
|
@ -9,17 +9,23 @@
|
|||||||
"activeTab"
|
"activeTab"
|
||||||
],
|
],
|
||||||
|
|
||||||
|
"icons": {
|
||||||
|
"16": "icons/icon16.png",
|
||||||
|
"32": "icons/icon32.png",
|
||||||
|
"48": "icons/icon48.png",
|
||||||
|
"128": "icons/icon128.png"
|
||||||
|
},
|
||||||
|
|
||||||
"background": {
|
"background": {
|
||||||
"service_worker": "background.js",
|
"service_worker": "background.js",
|
||||||
"type": "module"
|
"type": "module"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
"action": {
|
"action": {
|
||||||
"default_popup": "popup.html",
|
"default_popup": "popup.html",
|
||||||
"default_icon": {
|
"default_icon": {
|
||||||
"16": "icons/icon16.png",
|
"16": "icons/icon16.png",
|
||||||
|
"32": "icons/icon32.png",
|
||||||
"48": "icons/icon48.png",
|
"48": "icons/icon48.png",
|
||||||
"128": "icons/icon128.png"
|
"128": "icons/icon128.png"
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,23 @@
|
|||||||
<h1>CryptoVault</h1>
|
<h1>CryptoVault</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-actions">
|
<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">
|
<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">
|
<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>
|
<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 -->
|
<!-- Status Section -->
|
||||||
<div class="vault-status" id="vaultStatus">
|
<div class="vault-status" id="vaultStatus">
|
||||||
<div class="status-indicator" id="statusIndicator">
|
<div class="status-indicator" id="statusIndicator">
|
||||||
<div class="status-content">
|
<span id="statusText"></span>
|
||||||
<div class="status-dot"></div>
|
|
||||||
<span id="statusText">Initializing...</span>
|
|
||||||
</div>
|
|
||||||
<button id="lockBtn" class="btn btn-ghost btn-small hidden">
|
<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">
|
<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>
|
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
|
||||||
|
@ -1,33 +1,27 @@
|
|||||||
// Enhanced toast system
|
// Consolidated toast system
|
||||||
function showToast(message, type = 'info') {
|
function showToast(message, type = 'info') {
|
||||||
// Remove any existing toast
|
document.querySelector('.toast-notification')?.remove();
|
||||||
const existingToast = document.querySelector('.toast-notification');
|
|
||||||
if (existingToast) {
|
|
||||||
existingToast.remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create new toast element
|
const icons = {
|
||||||
const toast = document.createElement('div');
|
success: '<polyline points="20,6 9,17 4,12"></polyline>',
|
||||||
toast.className = `toast-notification toast-${type}`;
|
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 toast = Object.assign(document.createElement('div'), {
|
||||||
const icon = getToastIcon(type);
|
className: `toast-notification toast-${type}`,
|
||||||
|
innerHTML: `
|
||||||
|
<div class="toast-icon">
|
||||||
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
${icons[type] || icons.info}
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="toast-content"><div class="toast-message">${message}</div></div>
|
||||||
|
`
|
||||||
|
});
|
||||||
|
|
||||||
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);
|
document.body.appendChild(toast);
|
||||||
|
|
||||||
// Trigger entrance animation
|
|
||||||
setTimeout(() => toast.classList.add('toast-show'), 10);
|
setTimeout(() => toast.classList.add('toast-show'), 10);
|
||||||
|
|
||||||
// Auto-remove after 4 seconds
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (toast.parentElement) {
|
if (toast.parentElement) {
|
||||||
toast.classList.add('toast-hide');
|
toast.classList.add('toast-hide');
|
||||||
@ -36,30 +30,6 @@ function showToast(message, type = 'info') {
|
|||||||
}, 4000);
|
}, 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
|
// Enhanced loading states for buttons
|
||||||
function setButtonLoading(button, loading = true) {
|
function setButtonLoading(button, loading = true) {
|
||||||
if (loading) {
|
if (loading) {
|
||||||
@ -109,16 +79,17 @@ function showSection(sectionId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setStatus(text, isConnected = false) {
|
function setStatus(text, isConnected = false) {
|
||||||
document.getElementById('statusText').textContent = text;
|
const statusText = document.getElementById('statusText');
|
||||||
const indicator = document.getElementById('statusIndicator');
|
const statusSection = document.getElementById('vaultStatus');
|
||||||
indicator.classList.toggle('connected', isConnected);
|
|
||||||
|
|
||||||
// Show/hide lock button - only show when session is unlocked
|
|
||||||
const lockBtn = document.getElementById('lockBtn');
|
const lockBtn = document.getElementById('lockBtn');
|
||||||
if (lockBtn) {
|
|
||||||
// Only show lock button when connected AND status indicates unlocked session
|
if (isConnected && text) {
|
||||||
const isUnlocked = isConnected && text.toLowerCase().startsWith('connected to');
|
// Show keyspace name and status section
|
||||||
lockBtn.classList.toggle('hidden', !isUnlocked);
|
statusText.textContent = text;
|
||||||
|
statusSection.classList.remove('hidden');
|
||||||
|
if (lockBtn) {
|
||||||
|
lockBtn.classList.remove('hidden');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -146,12 +117,20 @@ function stringToUint8Array(str) {
|
|||||||
|
|
||||||
// DOM Elements
|
// DOM Elements
|
||||||
const elements = {
|
const elements = {
|
||||||
|
// Authentication elements
|
||||||
keyspaceInput: document.getElementById('keyspaceInput'),
|
keyspaceInput: document.getElementById('keyspaceInput'),
|
||||||
passwordInput: document.getElementById('passwordInput'),
|
passwordInput: document.getElementById('passwordInput'),
|
||||||
createKeyspaceBtn: document.getElementById('createKeyspaceBtn'),
|
createKeyspaceBtn: document.getElementById('createKeyspaceBtn'),
|
||||||
loginBtn: document.getElementById('loginBtn'),
|
loginBtn: document.getElementById('loginBtn'),
|
||||||
|
|
||||||
|
// Header elements
|
||||||
lockBtn: document.getElementById('lockBtn'),
|
lockBtn: document.getElementById('lockBtn'),
|
||||||
themeToggle: document.getElementById('themeToggle'),
|
themeToggle: document.getElementById('themeToggle'),
|
||||||
|
settingsToggle: document.getElementById('settingsToggle'),
|
||||||
|
settingsDropdown: document.getElementById('settingsDropdown'),
|
||||||
|
timeoutInput: document.getElementById('timeoutInput'),
|
||||||
|
|
||||||
|
// Keypair management elements
|
||||||
toggleAddKeypairBtn: document.getElementById('toggleAddKeypairBtn'),
|
toggleAddKeypairBtn: document.getElementById('toggleAddKeypairBtn'),
|
||||||
addKeypairCard: document.getElementById('addKeypairCard'),
|
addKeypairCard: document.getElementById('addKeypairCard'),
|
||||||
keyTypeSelect: document.getElementById('keyTypeSelect'),
|
keyTypeSelect: document.getElementById('keyTypeSelect'),
|
||||||
@ -160,34 +139,91 @@ const elements = {
|
|||||||
cancelAddKeypairBtn: document.getElementById('cancelAddKeypairBtn'),
|
cancelAddKeypairBtn: document.getElementById('cancelAddKeypairBtn'),
|
||||||
keypairsList: document.getElementById('keypairsList'),
|
keypairsList: document.getElementById('keypairsList'),
|
||||||
|
|
||||||
// Sign tab
|
// Crypto operation elements - Sign tab
|
||||||
messageInput: document.getElementById('messageInput'),
|
messageInput: document.getElementById('messageInput'),
|
||||||
signBtn: document.getElementById('signBtn'),
|
signBtn: document.getElementById('signBtn'),
|
||||||
signatureResult: document.getElementById('signatureResult'),
|
signatureResult: document.getElementById('signatureResult'),
|
||||||
copySignatureBtn: document.getElementById('copySignatureBtn'),
|
copySignatureBtn: document.getElementById('copySignatureBtn'),
|
||||||
|
|
||||||
// Encrypt tab
|
// Crypto operation elements - Encrypt tab
|
||||||
encryptMessageInput: document.getElementById('encryptMessageInput'),
|
encryptMessageInput: document.getElementById('encryptMessageInput'),
|
||||||
encryptBtn: document.getElementById('encryptBtn'),
|
encryptBtn: document.getElementById('encryptBtn'),
|
||||||
encryptResult: document.getElementById('encryptResult'),
|
encryptResult: document.getElementById('encryptResult'),
|
||||||
|
|
||||||
// Decrypt tab
|
// Crypto operation elements - Decrypt tab
|
||||||
encryptedMessageInput: document.getElementById('encryptedMessageInput'),
|
encryptedMessageInput: document.getElementById('encryptedMessageInput'),
|
||||||
decryptBtn: document.getElementById('decryptBtn'),
|
decryptBtn: document.getElementById('decryptBtn'),
|
||||||
decryptResult: document.getElementById('decryptResult'),
|
decryptResult: document.getElementById('decryptResult'),
|
||||||
|
|
||||||
// Verify tab
|
// Crypto operation elements - Verify tab
|
||||||
verifyMessageInput: document.getElementById('verifyMessageInput'),
|
verifyMessageInput: document.getElementById('verifyMessageInput'),
|
||||||
signatureToVerifyInput: document.getElementById('signatureToVerifyInput'),
|
signatureToVerifyInput: document.getElementById('signatureToVerifyInput'),
|
||||||
verifyBtn: document.getElementById('verifyBtn'),
|
verifyBtn: document.getElementById('verifyBtn'),
|
||||||
verifyResult: document.getElementById('verifyResult'),
|
verifyResult: document.getElementById('verifyResult'),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Global state variables
|
||||||
let currentKeyspace = null;
|
let currentKeyspace = null;
|
||||||
let selectedKeypairId = null;
|
let selectedKeypairId = null;
|
||||||
let backgroundPort = 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
|
// Theme management
|
||||||
function initializeTheme() {
|
function initializeTheme() {
|
||||||
@ -205,18 +241,46 @@ function toggleTheme() {
|
|||||||
updateThemeIcon(newTheme);
|
updateThemeIcon(newTheme);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Settings dropdown management
|
||||||
|
function toggleSettingsDropdown() {
|
||||||
|
const dropdown = elements.settingsDropdown;
|
||||||
|
if (dropdown) {
|
||||||
|
dropdown.classList.toggle('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeSettingsDropdown() {
|
||||||
|
const dropdown = elements.settingsDropdown;
|
||||||
|
if (dropdown) {
|
||||||
|
dropdown.classList.add('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function updateThemeIcon(theme) {
|
function updateThemeIcon(theme) {
|
||||||
const themeToggle = elements.themeToggle;
|
const themeToggle = elements.themeToggle;
|
||||||
if (!themeToggle) return;
|
if (!themeToggle) return;
|
||||||
|
|
||||||
if (theme === 'dark') {
|
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';
|
themeToggle.title = 'Switch to light mode';
|
||||||
} else {
|
} else {
|
||||||
// Dark crescent moon SVG for better visibility
|
// Dark crescent moon SVG for light theme
|
||||||
themeToggle.innerHTML = `
|
themeToggle.innerHTML = `
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" stroke="none">
|
<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" fill="#333"/>
|
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
|
||||||
</svg>
|
</svg>
|
||||||
`;
|
`;
|
||||||
themeToggle.title = 'Switch to dark mode';
|
themeToggle.title = 'Switch to dark mode';
|
||||||
@ -225,14 +289,30 @@ function updateThemeIcon(theme) {
|
|||||||
|
|
||||||
// Establish connection to background script for keep-alive
|
// Establish connection to background script for keep-alive
|
||||||
function connectToBackground() {
|
function connectToBackground() {
|
||||||
try {
|
backgroundPort = chrome.runtime.connect({ name: 'popup' });
|
||||||
backgroundPort = chrome.runtime.connect({ name: 'popup' });
|
|
||||||
backgroundPort.onDisconnect.addListener(() => {
|
// Listen for messages from background script
|
||||||
backgroundPort = null;
|
backgroundPort.onMessage.addListener((message) => {
|
||||||
});
|
if (message.type === 'sessionTimeout') {
|
||||||
} catch (error) {
|
// Update UI state to reflect locked session
|
||||||
// Silently handle connection errors
|
currentKeyspace = null;
|
||||||
}
|
selectedKeypairId = null;
|
||||||
|
setStatus('', false);
|
||||||
|
showSection('authSection');
|
||||||
|
clearVaultState();
|
||||||
|
|
||||||
|
// Clear form inputs
|
||||||
|
if (elements.keyspaceInput) elements.keyspaceInput.value = '';
|
||||||
|
if (elements.passwordInput) elements.passwordInput.value = '';
|
||||||
|
|
||||||
|
// Show timeout notification
|
||||||
|
showToast(message.message, 'info');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
backgroundPort.onDisconnect.addListener(() => {
|
||||||
|
backgroundPort = null;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize
|
// Initialize
|
||||||
@ -240,78 +320,78 @@ document.addEventListener('DOMContentLoaded', async function() {
|
|||||||
// Initialize theme first
|
// Initialize theme first
|
||||||
initializeTheme();
|
initializeTheme();
|
||||||
|
|
||||||
|
// Load timeout setting
|
||||||
|
await loadTimeoutSetting();
|
||||||
|
|
||||||
// Ensure lock button starts hidden
|
// Ensure lock button starts hidden
|
||||||
const lockBtn = document.getElementById('lockBtn');
|
const lockBtn = document.getElementById('lockBtn');
|
||||||
if (lockBtn) {
|
if (lockBtn) {
|
||||||
lockBtn.classList.add('hidden');
|
lockBtn.classList.add('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
setStatus('Initializing...', false);
|
|
||||||
|
|
||||||
// Connect to background script for keep-alive
|
// Connect to background script for keep-alive
|
||||||
connectToBackground();
|
connectToBackground();
|
||||||
|
|
||||||
// Event listeners (with null checks)
|
// Consolidated event listeners
|
||||||
if (elements.createKeyspaceBtn) {
|
const eventMap = {
|
||||||
elements.createKeyspaceBtn.addEventListener('click', createKeyspace);
|
createKeyspaceBtn: createKeyspace,
|
||||||
}
|
loginBtn: login,
|
||||||
if (elements.loginBtn) {
|
lockBtn: lockSession,
|
||||||
elements.loginBtn.addEventListener('click', login);
|
themeToggle: toggleTheme,
|
||||||
}
|
settingsToggle: toggleSettingsDropdown,
|
||||||
if (elements.lockBtn) {
|
toggleAddKeypairBtn: toggleAddKeypairForm,
|
||||||
elements.lockBtn.addEventListener('click', lockSession);
|
addKeypairBtn: addKeypair,
|
||||||
}
|
cancelAddKeypairBtn: hideAddKeypairForm,
|
||||||
if (elements.themeToggle) {
|
signBtn: signMessage,
|
||||||
elements.themeToggle.addEventListener('click', toggleTheme);
|
encryptBtn: encryptMessage,
|
||||||
}
|
decryptBtn: decryptMessage,
|
||||||
|
verifyBtn: verifySignature
|
||||||
|
};
|
||||||
|
|
||||||
if (elements.toggleAddKeypairBtn) {
|
Object.entries(eventMap).forEach(([elementKey, handler]) => {
|
||||||
elements.toggleAddKeypairBtn.addEventListener('click', toggleAddKeypairForm);
|
elements[elementKey]?.addEventListener('click', handler);
|
||||||
}
|
});
|
||||||
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
|
// Tab functionality
|
||||||
initializeTabs();
|
initializeTabs();
|
||||||
|
|
||||||
// Copy button event listeners (with null checks)
|
// Additional event listeners
|
||||||
if (elements.copySignatureBtn) {
|
elements.copySignatureBtn?.addEventListener('click', () => {
|
||||||
elements.copySignatureBtn.addEventListener('click', () => {
|
copyToClipboard(document.getElementById('signatureValue')?.textContent);
|
||||||
const signature = document.getElementById('signatureValue');
|
});
|
||||||
if (signature) {
|
|
||||||
copyToClipboard(signature.textContent);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enable sign button when message is entered (with null checks)
|
elements.messageInput?.addEventListener('input', () => {
|
||||||
if (elements.messageInput && elements.signBtn) {
|
if (elements.signBtn) {
|
||||||
elements.messageInput.addEventListener('input', () => {
|
|
||||||
elements.signBtn.disabled = !elements.messageInput.value.trim() || !selectedKeypairId;
|
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) => {
|
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();
|
hideAddKeypairForm();
|
||||||
}
|
}
|
||||||
if (e.key === 'Enter' && e.target === elements.keyNameInput && elements.keyNameInput.value.trim()) {
|
if (e.key === 'Enter' && e.target === elements.keyNameInput && elements.keyNameInput.value.trim()) {
|
||||||
@ -331,16 +411,16 @@ async function checkExistingSession() {
|
|||||||
// Session is active
|
// Session is active
|
||||||
currentKeyspace = response.session.keyspace;
|
currentKeyspace = response.session.keyspace;
|
||||||
elements.keyspaceInput.value = currentKeyspace;
|
elements.keyspaceInput.value = currentKeyspace;
|
||||||
setStatus(`Connected to ${currentKeyspace}`, true);
|
setStatus(currentKeyspace, true);
|
||||||
showSection('vaultSection');
|
showSection('vaultSection');
|
||||||
await loadKeypairs();
|
await loadKeypairs();
|
||||||
} else {
|
} else {
|
||||||
// No active session
|
// No active session
|
||||||
setStatus('Ready', false);
|
setStatus('', false);
|
||||||
showSection('authSection');
|
showSection('authSection');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setStatus('Ready', false);
|
setStatus('', false);
|
||||||
showSection('authSection');
|
showSection('authSection');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -370,34 +450,43 @@ function hideAddKeypairForm() {
|
|||||||
|
|
||||||
// Tab functionality
|
// Tab functionality
|
||||||
function initializeTabs() {
|
function initializeTabs() {
|
||||||
const tabButtons = document.querySelectorAll('.tab-btn');
|
const tabContainer = document.querySelector('.operation-tabs');
|
||||||
const tabContents = document.querySelectorAll('.tab-content');
|
|
||||||
|
|
||||||
tabButtons.forEach(button => {
|
if (tabContainer) {
|
||||||
button.addEventListener('click', () => {
|
// Use event delegation for better performance
|
||||||
const targetTab = button.getAttribute('data-tab');
|
tabContainer.addEventListener('click', (e) => {
|
||||||
|
if (e.target.classList.contains('tab-btn')) {
|
||||||
// Remove active class from all tabs and contents
|
handleTabSwitch(e.target);
|
||||||
tabButtons.forEach(btn => btn.classList.remove('active'));
|
}
|
||||||
tabContents.forEach(content => content.classList.remove('active'));
|
|
||||||
|
|
||||||
// Add active class to clicked tab and corresponding content
|
|
||||||
button.classList.add('active');
|
|
||||||
document.getElementById(`${targetTab}-tab`).classList.add('active');
|
|
||||||
|
|
||||||
// Clear results when switching tabs
|
|
||||||
clearTabResults();
|
|
||||||
|
|
||||||
// Update button states
|
|
||||||
updateButtonStates();
|
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
// Initialize input validation
|
// Initialize input validation
|
||||||
initializeInputValidation();
|
initializeInputValidation();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleTabSwitch(clickedTab) {
|
||||||
|
const targetTab = clickedTab.getAttribute('data-tab');
|
||||||
|
const tabButtons = document.querySelectorAll('.tab-btn');
|
||||||
|
const tabContents = document.querySelectorAll('.tab-content');
|
||||||
|
|
||||||
|
// Remove active class from all tabs and contents
|
||||||
|
tabButtons.forEach(btn => btn.classList.remove('active'));
|
||||||
|
tabContents.forEach(content => content.classList.remove('active'));
|
||||||
|
|
||||||
|
// Add active class to clicked tab and corresponding content
|
||||||
|
clickedTab.classList.add('active');
|
||||||
|
const targetContent = document.getElementById(`${targetTab}-tab`);
|
||||||
|
if (targetContent) {
|
||||||
|
targetContent.classList.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear results when switching tabs
|
||||||
|
clearTabResults();
|
||||||
|
|
||||||
|
// Update button states
|
||||||
|
updateButtonStates();
|
||||||
|
}
|
||||||
|
|
||||||
function clearTabResults() {
|
function clearTabResults() {
|
||||||
// Hide all result sections (with null checks)
|
// Hide all result sections (with null checks)
|
||||||
@ -488,8 +577,6 @@ function clearVaultState() {
|
|||||||
selectedKeypairId = null;
|
selectedKeypairId = null;
|
||||||
updateButtonStates();
|
updateButtonStates();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Hide add keypair form if open
|
// Hide add keypair form if open
|
||||||
hideAddKeypairForm();
|
hideAddKeypairForm();
|
||||||
|
|
||||||
@ -499,24 +586,31 @@ function clearVaultState() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createKeyspace() {
|
// Validation utilities
|
||||||
|
const validateAuth = () => {
|
||||||
const keyspace = elements.keyspaceInput.value.trim();
|
const keyspace = elements.keyspaceInput.value.trim();
|
||||||
const password = elements.passwordInput.value.trim();
|
const password = elements.passwordInput.value.trim();
|
||||||
|
|
||||||
if (!keyspace || !password) {
|
if (!validateInput(keyspace, 'Keyspace name', { minLength: 1, maxLength: 100 })) {
|
||||||
showToast('Please enter keyspace name and password', 'error');
|
return null;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!validateInput(password, 'Password', { minLength: 1, maxLength: 1000 })) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { keyspace, password };
|
||||||
|
};
|
||||||
|
|
||||||
|
async function createKeyspace() {
|
||||||
|
const auth = validateAuth();
|
||||||
|
if (!auth) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await executeOperation(
|
await executeOperation(
|
||||||
async () => {
|
async () => {
|
||||||
const response = await sendMessage('createKeyspace', { keyspace, password });
|
const response = await sendMessage('createKeyspace', auth);
|
||||||
|
if (response?.success) {
|
||||||
if (response && response.success) {
|
|
||||||
// Clear any existing state before auto-login
|
|
||||||
clearVaultState();
|
clearVaultState();
|
||||||
await login(); // Auto-login after creation
|
await login(); // Auto-login after creation
|
||||||
return response;
|
return response;
|
||||||
@ -531,32 +625,25 @@ async function createKeyspace() {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Create keyspace error:', error);
|
handleError(error, 'Create keyspace');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function login() {
|
async function login() {
|
||||||
const keyspace = elements.keyspaceInput.value.trim();
|
const auth = validateAuth();
|
||||||
const password = elements.passwordInput.value.trim();
|
if (!auth) return;
|
||||||
|
|
||||||
if (!keyspace || !password) {
|
|
||||||
showToast('Please enter keyspace name and password', 'error');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await executeOperation(
|
await executeOperation(
|
||||||
async () => {
|
async () => {
|
||||||
const response = await sendMessage('initSession', { keyspace, password });
|
const response = await sendMessage('initSession', auth);
|
||||||
|
if (response?.success) {
|
||||||
if (response && response.success) {
|
currentKeyspace = auth.keyspace;
|
||||||
currentKeyspace = keyspace;
|
setStatus(auth.keyspace, true);
|
||||||
setStatus(`Connected to ${keyspace}`, true);
|
|
||||||
showSection('vaultSection');
|
showSection('vaultSection');
|
||||||
|
|
||||||
// Clear any previous vault state before loading new keyspace
|
|
||||||
clearVaultState();
|
clearVaultState();
|
||||||
await loadKeypairs();
|
await loadKeypairs();
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
} else {
|
} else {
|
||||||
throw new Error(getResponseError(response, 'login'));
|
throw new Error(getResponseError(response, 'login'));
|
||||||
@ -569,7 +656,7 @@ async function login() {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Login error:', error);
|
handleError(error, 'Create keyspace');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -578,7 +665,7 @@ async function lockSession() {
|
|||||||
await sendMessage('lockSession');
|
await sendMessage('lockSession');
|
||||||
currentKeyspace = null;
|
currentKeyspace = null;
|
||||||
selectedKeypairId = null;
|
selectedKeypairId = null;
|
||||||
setStatus('Locked', false);
|
setStatus('', false);
|
||||||
showSection('authSection');
|
showSection('authSection');
|
||||||
|
|
||||||
// Clear all form inputs
|
// Clear all form inputs
|
||||||
@ -601,47 +688,35 @@ async function addKeypair() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
await executeOperation(
|
||||||
await executeOperation(
|
async () => {
|
||||||
async () => {
|
const metadata = JSON.stringify({ name: keyName });
|
||||||
const metadata = JSON.stringify({ name: keyName });
|
const response = await sendMessage('addKeypair', { keyType, metadata });
|
||||||
const response = await sendMessage('addKeypair', { keyType, metadata });
|
|
||||||
|
|
||||||
if (response?.success) {
|
if (response?.success) {
|
||||||
hideAddKeypairForm();
|
hideAddKeypairForm();
|
||||||
await loadKeypairs();
|
await loadKeypairs();
|
||||||
return response;
|
return response;
|
||||||
} else {
|
} else {
|
||||||
throw new Error(getResponseError(response, 'add keypair'));
|
throw new Error(getResponseError(response, 'add keypair'));
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
loadingElement: elements.addKeypairBtn,
|
|
||||||
successMessage: 'Keypair added successfully!'
|
|
||||||
}
|
}
|
||||||
);
|
},
|
||||||
} catch (error) {
|
{
|
||||||
// Error already handled by executeOperation
|
loadingElement: elements.addKeypairBtn,
|
||||||
}
|
successMessage: 'Keypair added successfully!'
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadKeypairs() {
|
async function loadKeypairs() {
|
||||||
try {
|
const response = await sendMessage('listKeypairs');
|
||||||
const response = await sendMessage('listKeypairs');
|
|
||||||
|
|
||||||
if (response && response.success) {
|
if (response && response.success) {
|
||||||
renderKeypairs(response.keypairs);
|
renderKeypairs(response.keypairs);
|
||||||
} else {
|
} else {
|
||||||
const errorMsg = getResponseError(response, 'load keypairs');
|
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;
|
const container = elements.keypairsList;
|
||||||
container.innerHTML = '<div class="empty-state">Error loading keypairs. Try refreshing.</div>';
|
container.innerHTML = '<div class="empty-state">Failed to load keypairs. Try refreshing.</div>';
|
||||||
showToast(errorMsg, 'error');
|
showToast(errorMsg, 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -724,7 +799,6 @@ async function selectKeypair(keyId) {
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMsg = getErrorMessage(error, 'Failed to select keypair');
|
const errorMsg = getErrorMessage(error, 'Failed to select keypair');
|
||||||
console.error('Error selecting keypair:', error);
|
|
||||||
// Revert visual state if there was an error
|
// Revert visual state if there was an error
|
||||||
updateKeypairSelection(null);
|
updateKeypairSelection(null);
|
||||||
showToast(errorMsg, 'error');
|
showToast(errorMsg, 'error');
|
||||||
@ -747,183 +821,114 @@ function updateKeypairSelection(selectedId) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function signMessage() {
|
// Shared templates
|
||||||
const messageText = elements.messageInput.value.trim();
|
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">
|
||||||
if (!messageText || !selectedKeypairId) {
|
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
||||||
showToast('Please enter a message and select a keypair', 'error');
|
<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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
await executeOperation(
|
||||||
await executeOperation(
|
async () => {
|
||||||
async () => {
|
const response = await sendMessage(action, input());
|
||||||
const messageBytes = stringToUint8Array(messageText);
|
if (response?.success) {
|
||||||
const response = await sendMessage('sign', { message: messageBytes });
|
resultElement.classList.remove('hidden');
|
||||||
|
resultElement.innerHTML = resultProcessor(response);
|
||||||
|
|
||||||
if (response?.success) {
|
// Add copy button listener if result has copy button
|
||||||
elements.signatureResult.classList.remove('hidden');
|
const copyBtn = resultElement.querySelector('.btn-copy');
|
||||||
elements.signatureResult.innerHTML = `
|
if (copyBtn && config.copyValue) {
|
||||||
<label>Signature:</label>
|
copyBtn.addEventListener('click', () => copyToClipboard(config.copyValue(response)));
|
||||||
<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'));
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{
|
return response;
|
||||||
loadingElement: elements.signBtn,
|
} else {
|
||||||
successMessage: 'Message signed successfully!'
|
throw new Error(getResponseError(response, action));
|
||||||
}
|
}
|
||||||
);
|
},
|
||||||
} catch (error) {
|
{ loadingElement: button, successMessage: successMsg }
|
||||||
elements.signatureResult.classList.add('hidden');
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function encryptMessage() {
|
// Crypto operation functions using shared templates
|
||||||
const messageText = elements.encryptMessageInput.value.trim();
|
const signMessage = () => performCryptoOperation({
|
||||||
if (!messageText || !currentKeyspace) {
|
validation: () => elements.messageInput.value.trim() && selectedKeypairId,
|
||||||
showToast('Please enter a message and ensure you are connected to a keyspace', 'error');
|
errorMsg: 'Please enter a message and select a keypair',
|
||||||
return;
|
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 {
|
const encryptMessage = () => performCryptoOperation({
|
||||||
await executeOperation(
|
validation: () => elements.encryptMessageInput.value.trim() && currentKeyspace,
|
||||||
async () => {
|
errorMsg: 'Please enter a message and ensure you are connected to a keyspace',
|
||||||
const response = await sendMessage('encrypt', { message: messageText });
|
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) {
|
const decryptMessage = () => performCryptoOperation({
|
||||||
elements.encryptResult.classList.remove('hidden');
|
validation: () => elements.encryptedMessageInput.value.trim() && currentKeyspace,
|
||||||
elements.encryptResult.innerHTML = `
|
errorMsg: 'Please enter encrypted message and ensure you are connected to a keyspace',
|
||||||
<label>Encrypted Message:</label>
|
action: 'decrypt',
|
||||||
<div class="signature-container">
|
input: () => ({ encryptedMessage: elements.encryptedMessageInput.value.trim() }),
|
||||||
<code id="encryptedValue">${response.encryptedMessage}</code>
|
resultElement: elements.decryptResult,
|
||||||
<button id="copyEncryptedBtn" class="btn-copy" title="Copy">
|
button: elements.decryptBtn,
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
successMsg: 'Message decrypted successfully!',
|
||||||
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
copyValue: (response) => response.decryptedMessage,
|
||||||
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
resultProcessor: (response) => createResultContainer('Decrypted Message', response.decryptedMessage, 'copyDecryptedBtn')
|
||||||
</svg>
|
});
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
document.getElementById('copyEncryptedBtn').addEventListener('click', () => {
|
const verifySignature = () => performCryptoOperation({
|
||||||
copyToClipboard(response.encryptedMessage);
|
validation: () => elements.verifyMessageInput.value.trim() && elements.signatureToVerifyInput.value.trim() && selectedKeypairId,
|
||||||
});
|
errorMsg: 'Please enter message, signature, and select a keypair',
|
||||||
|
action: 'verify',
|
||||||
return response;
|
input: () => ({
|
||||||
} else {
|
message: stringToUint8Array(elements.verifyMessageInput.value.trim()),
|
||||||
throw new Error(getResponseError(response, 'encrypt message'));
|
signature: elements.signatureToVerifyInput.value.trim()
|
||||||
}
|
}),
|
||||||
},
|
resultElement: elements.verifyResult,
|
||||||
{
|
button: elements.verifyBtn,
|
||||||
loadingElement: elements.encryptBtn,
|
successMsg: null,
|
||||||
successMessage: 'Message encrypted successfully!'
|
resultProcessor: (response) => {
|
||||||
}
|
const isValid = response.isValid;
|
||||||
);
|
const icon = isValid
|
||||||
} catch (error) {
|
? `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
elements.encryptResult.classList.add('hidden');
|
<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>
|
||||||
async function decryptMessage() {
|
<line x1="15" y1="9" x2="9" y2="15"></line>
|
||||||
const encryptedText = elements.encryptedMessageInput.value.trim();
|
<line x1="9" y1="9" x2="15" y2="15"></line>
|
||||||
if (!encryptedText || !currentKeyspace) {
|
</svg>`;
|
||||||
showToast('Please enter encrypted message and ensure you are connected to a keyspace', 'error');
|
const text = isValid ? 'Signature is valid' : 'Signature is invalid';
|
||||||
return;
|
return `<div class="verification-status ${isValid ? 'valid' : 'invalid'}">
|
||||||
}
|
<span class="verification-icon">${icon}</span>
|
||||||
|
|
||||||
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>
|
<span>${text}</span>
|
||||||
</div>
|
</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');
|
|
||||||
}
|
}
|
||||||
}
|
});
|
@ -34,7 +34,7 @@
|
|||||||
--accent-success: hsl(var(--accent-hue), 65%, 45%);
|
--accent-success: hsl(var(--accent-hue), 65%, 45%);
|
||||||
--accent-error: hsl(0, 70%, 55%);
|
--accent-error: hsl(0, 70%, 55%);
|
||||||
--accent-warning: hsl(35, 85%, 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 system */
|
||||||
--spacing-xs: 4px;
|
--spacing-xs: 4px;
|
||||||
@ -75,66 +75,63 @@
|
|||||||
--accent-success: hsl(var(--accent-hue), 60%, 55%);
|
--accent-success: hsl(var(--accent-hue), 60%, 55%);
|
||||||
--accent-error: hsl(0, 65%, 60%);
|
--accent-error: hsl(0, 65%, 60%);
|
||||||
--accent-warning: hsl(35, 80%, 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 */
|
/* Consolidated button styling system */
|
||||||
.btn-primary {
|
.btn-primary, .btn-secondary, .btn-ghost {
|
||||||
background: var(--bg-button-primary);
|
|
||||||
color: var(--text-button-primary);
|
|
||||||
border: none;
|
border: none;
|
||||||
font-weight: 600;
|
font-weight: 500;
|
||||||
box-shadow: var(--shadow-button);
|
|
||||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary:hover {
|
.btn-primary {
|
||||||
background: hsl(var(--primary-hue), var(--primary-saturation), 50%);
|
background: var(--bg-button-primary);
|
||||||
box-shadow: var(--shadow-button-hover);
|
color: var(--text-button-primary);
|
||||||
transform: translateY(-1px);
|
font-weight: 600;
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary:active {
|
|
||||||
transform: translateY(0);
|
|
||||||
box-shadow: var(--shadow-button);
|
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 {
|
.btn-secondary {
|
||||||
background: var(--bg-button-secondary);
|
background: var(--bg-button-secondary);
|
||||||
color: var(--text-button-secondary);
|
color: var(--text-button-secondary);
|
||||||
border: 1px solid var(--border-color);
|
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 {
|
.btn-ghost {
|
||||||
background: var(--bg-button-ghost);
|
background: var(--bg-button-ghost);
|
||||||
color: var(--border-focus);
|
color: var(--border-focus);
|
||||||
border: 1px solid transparent;
|
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 {
|
.btn-ghost:hover {
|
||||||
background: var(--bg-input);
|
|
||||||
border-color: var(--border-color);
|
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);
|
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 {
|
.btn-icon-only {
|
||||||
background: var(--bg-button-ghost);
|
background: var(--bg-button-ghost);
|
||||||
border: none;
|
border: none;
|
||||||
@ -224,39 +288,15 @@ body {
|
|||||||
gap: var(--spacing-md);
|
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 {
|
#statusText {
|
||||||
font-size: 14px;
|
font-size: 22px;
|
||||||
font-weight: 500;
|
font-weight: 700;
|
||||||
color: var(--text-primary);
|
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 */
|
/* Enhanced lock button styling */
|
||||||
#lockBtn {
|
#lockBtn {
|
||||||
@ -515,7 +555,7 @@ input::placeholder, textarea::placeholder {
|
|||||||
|
|
||||||
.vault-header h2 {
|
.vault-header h2 {
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
font-size: 20px;
|
font-size: 18px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -749,31 +789,6 @@ input::placeholder, textarea::placeholder {
|
|||||||
color: var(--text-primary);
|
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 */
|
/* Enhanced Toast Notifications */
|
||||||
.toast-notification {
|
.toast-notification {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@ -828,31 +843,7 @@ input::placeholder, textarea::placeholder {
|
|||||||
word-wrap: break-word;
|
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 */
|
/* Success Toast */
|
||||||
.toast-success {
|
.toast-success {
|
||||||
@ -880,9 +871,9 @@ input::placeholder, textarea::placeholder {
|
|||||||
|
|
||||||
/* Info Toast */
|
/* Info Toast */
|
||||||
.toast-info {
|
.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;
|
color: white;
|
||||||
border-color: rgba(59, 130, 246, 0.3);
|
border-color: hsla(var(--primary-hue), 35%, 60%, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.toast-info .toast-icon {
|
.toast-info .toast-icon {
|
||||||
@ -981,10 +972,7 @@ input::placeholder, textarea::placeholder {
|
|||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-theme="dark"] .tab-btn:hover {
|
[data-theme="dark"] .tab-btn:hover,
|
||||||
color: var(--border-focus);
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-theme="dark"] .tab-btn.active {
|
[data-theme="dark"] .tab-btn.active {
|
||||||
color: var(--border-focus);
|
color: var(--border-focus);
|
||||||
}
|
}
|
||||||
@ -997,8 +985,8 @@ input::placeholder, textarea::placeholder {
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Result Styling */
|
/* Consolidated result styling */
|
||||||
.encrypt-result, .decrypt-result, .verify-result {
|
.encrypt-result, .decrypt-result, .verify-result, .signature-result {
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
@ -1006,55 +994,55 @@ input::placeholder, textarea::placeholder {
|
|||||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
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);
|
background: hsla(var(--accent-hue), 60%, 95%, 0.8);
|
||||||
border-color: hsla(var(--accent-hue), 50%, 70%, 0.3);
|
border-color: hsla(var(--accent-hue), 50%, 70%, 0.3);
|
||||||
box-shadow: 0 2px 12px hsla(var(--accent-hue), 50%, 50%, 0.1);
|
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 {
|
.decrypt-result {
|
||||||
background: hsla(var(--secondary-hue), 60%, 95%, 0.8);
|
background: hsla(var(--secondary-hue), 60%, 95%, 0.8);
|
||||||
border-color: hsla(var(--secondary-hue), 50%, 70%, 0.3);
|
border-color: hsla(var(--secondary-hue), 50%, 70%, 0.3);
|
||||||
box-shadow: 0 2px 12px hsla(var(--secondary-hue), 50%, 50%, 0.1);
|
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 {
|
.verify-result {
|
||||||
background: hsla(var(--primary-hue), 30%, 95%, 0.8);
|
background: hsla(var(--primary-hue), 30%, 95%, 0.8);
|
||||||
border-color: hsla(var(--primary-hue), 40%, 70%, 0.3);
|
border-color: hsla(var(--primary-hue), 40%, 70%, 0.3);
|
||||||
box-shadow: 0 2px 12px hsla(var(--primary-hue), 40%, 50%, 0.1);
|
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 {
|
[data-theme="dark"] .verify-result {
|
||||||
background: hsla(var(--primary-hue), 20%, 15%, 0.6);
|
background: hsla(var(--primary-hue), 20%, 15%, 0.6);
|
||||||
border-color: hsla(var(--primary-hue), 30%, 40%, 0.4);
|
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 {
|
.verification-status {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -1071,19 +1059,14 @@ input::placeholder, textarea::placeholder {
|
|||||||
color: var(--accent-error);
|
color: var(--accent-error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.verification-icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.verification-icon svg {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|