Support encrypt, decrypt & verify + add dark theme + better error handling & UI enhancements
This commit is contained in:
parent
5bc205b2f7
commit
37764e3861
@ -1,6 +1,7 @@
|
|||||||
let vault = null;
|
let vault = null;
|
||||||
let isInitialized = false;
|
let isInitialized = false;
|
||||||
let currentSession = null;
|
let currentSession = null;
|
||||||
|
let keepAliveInterval = null;
|
||||||
|
|
||||||
// Utility function to convert Uint8Array to hex
|
// Utility function to convert Uint8Array to hex
|
||||||
function toHex(uint8Array) {
|
function toHex(uint8Array) {
|
||||||
@ -12,16 +13,31 @@ function toHex(uint8Array) {
|
|||||||
// Session persistence functions
|
// Session persistence functions
|
||||||
async function saveSession(keyspace) {
|
async function saveSession(keyspace) {
|
||||||
currentSession = { keyspace, timestamp: Date.now() };
|
currentSession = { keyspace, timestamp: Date.now() };
|
||||||
await chrome.storage.session.set({ cryptoVaultSession: currentSession });
|
|
||||||
console.log('Session saved:', currentSession);
|
// Save to both session and local storage for better persistence
|
||||||
|
try {
|
||||||
|
await chrome.storage.session.set({ cryptoVaultSession: currentSession });
|
||||||
|
await chrome.storage.local.set({ cryptoVaultSessionBackup: currentSession });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to save session:', error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadSession() {
|
async function loadSession() {
|
||||||
try {
|
try {
|
||||||
const result = await chrome.storage.session.get(['cryptoVaultSession']);
|
// Try session storage first
|
||||||
|
let result = await chrome.storage.session.get(['cryptoVaultSession']);
|
||||||
if (result.cryptoVaultSession) {
|
if (result.cryptoVaultSession) {
|
||||||
currentSession = result.cryptoVaultSession;
|
currentSession = result.cryptoVaultSession;
|
||||||
console.log('Session loaded:', currentSession);
|
return currentSession;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to local storage
|
||||||
|
result = await chrome.storage.local.get(['cryptoVaultSessionBackup']);
|
||||||
|
if (result.cryptoVaultSessionBackup) {
|
||||||
|
currentSession = result.cryptoVaultSessionBackup;
|
||||||
|
// Restore to session storage
|
||||||
|
await chrome.storage.session.set({ cryptoVaultSession: currentSession });
|
||||||
return currentSession;
|
return currentSession;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -32,8 +48,45 @@ async function loadSession() {
|
|||||||
|
|
||||||
async function clearSession() {
|
async function clearSession() {
|
||||||
currentSession = null;
|
currentSession = null;
|
||||||
await chrome.storage.session.remove(['cryptoVaultSession']);
|
try {
|
||||||
console.log('Session cleared');
|
await chrome.storage.session.remove(['cryptoVaultSession']);
|
||||||
|
await chrome.storage.local.remove(['cryptoVaultSessionBackup']);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to clear session:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep service worker alive
|
||||||
|
function startKeepAlive() {
|
||||||
|
if (keepAliveInterval) {
|
||||||
|
clearInterval(keepAliveInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ping every 20 seconds to keep service worker alive
|
||||||
|
keepAliveInterval = setInterval(() => {
|
||||||
|
// Simple operation to keep service worker active
|
||||||
|
chrome.storage.session.get(['keepAlive']).catch(() => {
|
||||||
|
// Ignore errors
|
||||||
|
});
|
||||||
|
}, 20000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopKeepAlive() {
|
||||||
|
if (keepAliveInterval) {
|
||||||
|
clearInterval(keepAliveInterval);
|
||||||
|
keepAliveInterval = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enhanced session management with keep-alive
|
||||||
|
async function saveSessionWithKeepAlive(keyspace) {
|
||||||
|
await saveSession(keyspace);
|
||||||
|
startKeepAlive();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clearSessionWithKeepAlive() {
|
||||||
|
await clearSession();
|
||||||
|
stopKeepAlive();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function restoreSession() {
|
async function restoreSession() {
|
||||||
@ -43,15 +96,15 @@ async function restoreSession() {
|
|||||||
// 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) {
|
||||||
console.log('Session restored successfully for keyspace:', session.keyspace);
|
// Restart keep-alive for restored session
|
||||||
|
startKeepAlive();
|
||||||
return session;
|
return session;
|
||||||
} else {
|
} else {
|
||||||
console.log('Session expired, clearing...');
|
await clearSessionWithKeepAlive();
|
||||||
await clearSession();
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error checking session validity:', error);
|
console.error('Error checking session validity:', error);
|
||||||
await clearSession();
|
await clearSessionWithKeepAlive();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@ -68,6 +121,9 @@ import init, {
|
|||||||
current_keypair_metadata,
|
current_keypair_metadata,
|
||||||
current_keypair_public_key,
|
current_keypair_public_key,
|
||||||
sign,
|
sign,
|
||||||
|
verify,
|
||||||
|
encrypt_data,
|
||||||
|
decrypt_data,
|
||||||
lock_session
|
lock_session
|
||||||
} from './wasm/wasm_app.js';
|
} from './wasm/wasm_app.js';
|
||||||
|
|
||||||
@ -91,11 +147,13 @@ async function initVault() {
|
|||||||
current_keypair_metadata,
|
current_keypair_metadata,
|
||||||
current_keypair_public_key,
|
current_keypair_public_key,
|
||||||
sign,
|
sign,
|
||||||
|
verify,
|
||||||
|
encrypt_data,
|
||||||
|
decrypt_data,
|
||||||
lock_session
|
lock_session
|
||||||
};
|
};
|
||||||
|
|
||||||
isInitialized = true;
|
isInitialized = true;
|
||||||
console.log('CryptoVault initialized successfully');
|
|
||||||
|
|
||||||
// Try to restore previous session
|
// Try to restore previous session
|
||||||
await restoreSession();
|
await restoreSession();
|
||||||
@ -107,8 +165,23 @@ async function initVault() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle popup connection/disconnection
|
||||||
|
chrome.runtime.onConnect.addListener((port) => {
|
||||||
|
if (port.name === 'popup') {
|
||||||
|
// If we have an active session, ensure keep-alive is running
|
||||||
|
if (currentSession) {
|
||||||
|
startKeepAlive();
|
||||||
|
}
|
||||||
|
|
||||||
|
port.onDisconnect.addListener(() => {
|
||||||
|
// Keep the keep-alive running even after popup disconnects
|
||||||
|
// This ensures session persistence across popup closes
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// 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) => {
|
||||||
const handleRequest = async () => {
|
const handleRequest = async () => {
|
||||||
try {
|
try {
|
||||||
if (!vault) {
|
if (!vault) {
|
||||||
@ -122,7 +195,7 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
|||||||
|
|
||||||
case 'initSession':
|
case 'initSession':
|
||||||
await vault.init_session(request.keyspace, request.password);
|
await vault.init_session(request.keyspace, request.password);
|
||||||
await saveSession(request.keyspace);
|
await saveSessionWithKeepAlive(request.keyspace);
|
||||||
return { success: true };
|
return { success: true };
|
||||||
|
|
||||||
case 'isUnlocked':
|
case 'isUnlocked':
|
||||||
@ -134,37 +207,24 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
|||||||
return { success: true, result };
|
return { success: true, result };
|
||||||
|
|
||||||
case 'listKeypairs':
|
case 'listKeypairs':
|
||||||
console.log('Background: listing keypairs...');
|
|
||||||
console.log('Background: vault object:', vault);
|
|
||||||
console.log('Background: vault.list_keypairs function:', vault.list_keypairs);
|
|
||||||
|
|
||||||
// Check if session is unlocked first
|
// Check if session is unlocked first
|
||||||
const isUnlocked = vault.is_unlocked();
|
const isUnlocked = vault.is_unlocked();
|
||||||
console.log('Background: is session unlocked?', isUnlocked);
|
|
||||||
|
|
||||||
if (!isUnlocked) {
|
if (!isUnlocked) {
|
||||||
console.log('Background: Session is not unlocked, cannot list keypairs');
|
|
||||||
return { success: false, error: 'Session is not unlocked' };
|
return { success: false, error: 'Session is not unlocked' };
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const keypairsRaw = await vault.list_keypairs();
|
const keypairsRaw = await vault.list_keypairs();
|
||||||
console.log('Background: keypairs raw result:', keypairsRaw);
|
|
||||||
console.log('Background: keypairs type:', typeof keypairsRaw);
|
|
||||||
|
|
||||||
// Parse JSON string if needed
|
// Parse JSON string if needed
|
||||||
let keypairs;
|
let keypairs;
|
||||||
if (typeof keypairsRaw === 'string') {
|
if (typeof keypairsRaw === 'string') {
|
||||||
console.log('Background: Parsing JSON string...');
|
|
||||||
keypairs = JSON.parse(keypairsRaw);
|
keypairs = JSON.parse(keypairsRaw);
|
||||||
} else {
|
} else {
|
||||||
keypairs = keypairsRaw;
|
keypairs = keypairsRaw;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Background: parsed keypairs:', keypairs);
|
|
||||||
console.log('Background: parsed keypairs type:', typeof keypairs);
|
|
||||||
console.log('Background: keypairs array length:', Array.isArray(keypairs) ? keypairs.length : 'not an array');
|
|
||||||
|
|
||||||
return { success: true, keypairs };
|
return { success: true, keypairs };
|
||||||
} catch (listError) {
|
} catch (listError) {
|
||||||
console.error('Background: Error calling list_keypairs:', listError);
|
console.error('Background: Error calling list_keypairs:', listError);
|
||||||
@ -188,9 +248,67 @@ chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
|
|||||||
const signature = await vault.sign(new Uint8Array(request.message));
|
const signature = await vault.sign(new Uint8Array(request.message));
|
||||||
return { success: true, signature };
|
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':
|
case 'lockSession':
|
||||||
vault.lock_session();
|
vault.lock_session();
|
||||||
await clearSession();
|
await clearSessionWithKeepAlive();
|
||||||
return { success: true };
|
return { success: true };
|
||||||
|
|
||||||
case 'getStatus':
|
case 'getStatus':
|
||||||
|
280
crypto_vault_extension/js/errorHandler.js
Normal file
280
crypto_vault_extension/js/errorHandler.js
Normal file
@ -0,0 +1,280 @@
|
|||||||
|
// Enhanced Error Handling System for CryptoVault Extension
|
||||||
|
|
||||||
|
class CryptoVaultError extends Error {
|
||||||
|
constructor(message, code, retryable = false, userMessage = null) {
|
||||||
|
super(message);
|
||||||
|
this.name = 'CryptoVaultError';
|
||||||
|
this.code = code;
|
||||||
|
this.retryable = retryable;
|
||||||
|
this.userMessage = userMessage || message;
|
||||||
|
this.timestamp = Date.now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error codes for different types of errors
|
||||||
|
const ERROR_CODES = {
|
||||||
|
// Network/Connection errors (retryable)
|
||||||
|
NETWORK_ERROR: 'NETWORK_ERROR',
|
||||||
|
TIMEOUT_ERROR: 'TIMEOUT_ERROR',
|
||||||
|
SERVICE_UNAVAILABLE: 'SERVICE_UNAVAILABLE',
|
||||||
|
|
||||||
|
// Authentication errors (not retryable)
|
||||||
|
INVALID_PASSWORD: 'INVALID_PASSWORD',
|
||||||
|
SESSION_EXPIRED: 'SESSION_EXPIRED',
|
||||||
|
UNAUTHORIZED: 'UNAUTHORIZED',
|
||||||
|
|
||||||
|
// Crypto errors (not retryable)
|
||||||
|
CRYPTO_ERROR: 'CRYPTO_ERROR',
|
||||||
|
INVALID_SIGNATURE: 'INVALID_SIGNATURE',
|
||||||
|
ENCRYPTION_FAILED: 'ENCRYPTION_FAILED',
|
||||||
|
|
||||||
|
// Validation errors (not retryable)
|
||||||
|
INVALID_INPUT: 'INVALID_INPUT',
|
||||||
|
MISSING_KEYPAIR: 'MISSING_KEYPAIR',
|
||||||
|
INVALID_FORMAT: 'INVALID_FORMAT',
|
||||||
|
|
||||||
|
// System errors (sometimes retryable)
|
||||||
|
WASM_ERROR: 'WASM_ERROR',
|
||||||
|
STORAGE_ERROR: 'STORAGE_ERROR',
|
||||||
|
UNKNOWN_ERROR: 'UNKNOWN_ERROR'
|
||||||
|
};
|
||||||
|
|
||||||
|
// User-friendly error messages
|
||||||
|
const ERROR_MESSAGES = {
|
||||||
|
[ERROR_CODES.NETWORK_ERROR]: 'Connection failed. Please check your internet connection and try again.',
|
||||||
|
[ERROR_CODES.TIMEOUT_ERROR]: 'Operation timed out. Please try again.',
|
||||||
|
[ERROR_CODES.SERVICE_UNAVAILABLE]: 'Service is temporarily unavailable. Please try again later.',
|
||||||
|
|
||||||
|
[ERROR_CODES.INVALID_PASSWORD]: 'Invalid password. Please check your password and try again.',
|
||||||
|
[ERROR_CODES.SESSION_EXPIRED]: 'Your session has expired. Please log in again.',
|
||||||
|
[ERROR_CODES.UNAUTHORIZED]: 'You are not authorized to perform this action.',
|
||||||
|
|
||||||
|
[ERROR_CODES.CRYPTO_ERROR]: 'Cryptographic operation failed. Please try again.',
|
||||||
|
[ERROR_CODES.INVALID_SIGNATURE]: 'Invalid signature. Please verify your input.',
|
||||||
|
[ERROR_CODES.ENCRYPTION_FAILED]: 'Encryption failed. Please try again.',
|
||||||
|
|
||||||
|
[ERROR_CODES.INVALID_INPUT]: 'Invalid input. Please check your data and try again.',
|
||||||
|
[ERROR_CODES.MISSING_KEYPAIR]: 'No keypair selected. Please select a keypair first.',
|
||||||
|
[ERROR_CODES.INVALID_FORMAT]: 'Invalid data format. Please check your input.',
|
||||||
|
|
||||||
|
[ERROR_CODES.WASM_ERROR]: 'System error occurred. Please refresh and try again.',
|
||||||
|
[ERROR_CODES.STORAGE_ERROR]: 'Storage 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([
|
||||||
|
ERROR_CODES.NETWORK_ERROR,
|
||||||
|
ERROR_CODES.TIMEOUT_ERROR,
|
||||||
|
ERROR_CODES.SERVICE_UNAVAILABLE,
|
||||||
|
ERROR_CODES.WASM_ERROR,
|
||||||
|
ERROR_CODES.STORAGE_ERROR
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Enhanced error classification
|
||||||
|
function classifyError(error) {
|
||||||
|
const errorMessage = getErrorMessage(error);
|
||||||
|
|
||||||
|
// Network/Connection errors
|
||||||
|
if (errorMessage.includes('fetch') || errorMessage.includes('network') || errorMessage.includes('connection')) {
|
||||||
|
return new CryptoVaultError(
|
||||||
|
errorMessage,
|
||||||
|
ERROR_CODES.NETWORK_ERROR,
|
||||||
|
true,
|
||||||
|
ERROR_MESSAGES[ERROR_CODES.NETWORK_ERROR]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authentication errors
|
||||||
|
if (errorMessage.includes('password') || errorMessage.includes('Invalid password')) {
|
||||||
|
return new CryptoVaultError(
|
||||||
|
errorMessage,
|
||||||
|
ERROR_CODES.INVALID_PASSWORD,
|
||||||
|
false,
|
||||||
|
ERROR_MESSAGES[ERROR_CODES.INVALID_PASSWORD]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errorMessage.includes('session') || errorMessage.includes('not unlocked') || errorMessage.includes('expired')) {
|
||||||
|
return new CryptoVaultError(
|
||||||
|
errorMessage,
|
||||||
|
ERROR_CODES.SESSION_EXPIRED,
|
||||||
|
false,
|
||||||
|
ERROR_MESSAGES[ERROR_CODES.SESSION_EXPIRED]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Crypto errors
|
||||||
|
if (errorMessage.includes('decryption error') || errorMessage.includes('aead::Error')) {
|
||||||
|
return new CryptoVaultError(
|
||||||
|
errorMessage,
|
||||||
|
ERROR_CODES.CRYPTO_ERROR,
|
||||||
|
false,
|
||||||
|
'Invalid password or corrupted data. Please check your password.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errorMessage.includes('Crypto error') || errorMessage.includes('encryption')) {
|
||||||
|
return new CryptoVaultError(
|
||||||
|
errorMessage,
|
||||||
|
ERROR_CODES.CRYPTO_ERROR,
|
||||||
|
false,
|
||||||
|
ERROR_MESSAGES[ERROR_CODES.CRYPTO_ERROR]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validation errors
|
||||||
|
if (errorMessage.includes('No keypair selected')) {
|
||||||
|
return new CryptoVaultError(
|
||||||
|
errorMessage,
|
||||||
|
ERROR_CODES.MISSING_KEYPAIR,
|
||||||
|
false,
|
||||||
|
ERROR_MESSAGES[ERROR_CODES.MISSING_KEYPAIR]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// WASM errors
|
||||||
|
if (errorMessage.includes('wasm') || errorMessage.includes('WASM')) {
|
||||||
|
return new CryptoVaultError(
|
||||||
|
errorMessage,
|
||||||
|
ERROR_CODES.WASM_ERROR,
|
||||||
|
true,
|
||||||
|
ERROR_MESSAGES[ERROR_CODES.WASM_ERROR]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default to unknown error
|
||||||
|
return new CryptoVaultError(
|
||||||
|
errorMessage,
|
||||||
|
ERROR_CODES.UNKNOWN_ERROR,
|
||||||
|
false,
|
||||||
|
ERROR_MESSAGES[ERROR_CODES.UNKNOWN_ERROR]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get error message from various error types
|
||||||
|
function getErrorMessage(error) {
|
||||||
|
if (!error) return 'Unknown error';
|
||||||
|
|
||||||
|
if (typeof error === 'string') {
|
||||||
|
return error.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error instanceof Error) {
|
||||||
|
return error.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error.error) {
|
||||||
|
return getErrorMessage(error.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error.message) {
|
||||||
|
return error.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof error === 'object') {
|
||||||
|
try {
|
||||||
|
const stringified = JSON.stringify(error);
|
||||||
|
if (stringified && stringified !== '{}') {
|
||||||
|
return stringified;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore JSON stringify errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Unknown error';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retry logic with exponential backoff
|
||||||
|
async function withRetry(operation, options = {}) {
|
||||||
|
const {
|
||||||
|
maxRetries = 3,
|
||||||
|
baseDelay = 1000,
|
||||||
|
maxDelay = 10000,
|
||||||
|
backoffFactor = 2,
|
||||||
|
onRetry = null
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
let lastError;
|
||||||
|
|
||||||
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
||||||
|
try {
|
||||||
|
return await operation();
|
||||||
|
} catch (error) {
|
||||||
|
const classifiedError = classifyError(error);
|
||||||
|
lastError = classifiedError;
|
||||||
|
|
||||||
|
// Don't retry if it's the last attempt or error is not retryable
|
||||||
|
if (attempt === maxRetries || !classifiedError.retryable) {
|
||||||
|
throw classifiedError;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate delay with exponential backoff
|
||||||
|
const delay = Math.min(baseDelay * Math.pow(backoffFactor, attempt), maxDelay);
|
||||||
|
|
||||||
|
// Call retry callback if provided
|
||||||
|
if (onRetry) {
|
||||||
|
onRetry(attempt + 1, delay, classifiedError);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait before retrying
|
||||||
|
await new Promise(resolve => setTimeout(resolve, delay));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw lastError;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enhanced operation wrapper with loading states
|
||||||
|
async function executeOperation(operation, options = {}) {
|
||||||
|
const {
|
||||||
|
loadingElement = null,
|
||||||
|
successMessage = null,
|
||||||
|
showRetryProgress = false,
|
||||||
|
onProgress = null
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
// Show loading state
|
||||||
|
if (loadingElement) {
|
||||||
|
setButtonLoading(loadingElement, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await withRetry(operation, {
|
||||||
|
...options,
|
||||||
|
onRetry: (attempt, delay, error) => {
|
||||||
|
if (showRetryProgress && onProgress) {
|
||||||
|
onProgress(`Retrying... (${attempt}/${options.maxRetries || 3})`);
|
||||||
|
}
|
||||||
|
if (options.onRetry) {
|
||||||
|
options.onRetry(attempt, delay, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show success message if provided
|
||||||
|
if (successMessage) {
|
||||||
|
showToast(successMessage, 'success');
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
// Show user-friendly error message
|
||||||
|
showToast(error.userMessage || error.message, 'error');
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
// Hide loading state
|
||||||
|
if (loadingElement) {
|
||||||
|
setButtonLoading(loadingElement, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export for use in other modules
|
||||||
|
window.CryptoVaultError = CryptoVaultError;
|
||||||
|
window.ERROR_CODES = ERROR_CODES;
|
||||||
|
window.classifyError = classifyError;
|
||||||
|
window.getErrorMessage = getErrorMessage;
|
||||||
|
window.withRetry = withRetry;
|
||||||
|
window.executeOperation = executeOperation;
|
@ -11,9 +11,12 @@
|
|||||||
<div class="logo-icon">🔐</div>
|
<div class="logo-icon">🔐</div>
|
||||||
<h1>CryptoVault</h1>
|
<h1>CryptoVault</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="status-indicator" id="statusIndicator">
|
<div class="header-actions">
|
||||||
<div class="status-dot"></div>
|
<button id="themeToggle" class="btn-icon-only" title="Switch to dark mode">
|
||||||
<span id="statusText">Initializing...</span>
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@ -38,13 +41,26 @@
|
|||||||
|
|
||||||
<!-- Main Vault Section -->
|
<!-- Main Vault Section -->
|
||||||
<section class="section hidden" id="vaultSection">
|
<section class="section hidden" id="vaultSection">
|
||||||
<div class="vault-header">
|
<!-- Status Section -->
|
||||||
<h2>Your Keypairs</h2>
|
<div class="vault-status" id="vaultStatus">
|
||||||
<button id="lockBtn" class="btn btn-ghost">🔒 Lock</button>
|
<div class="status-indicator" id="statusIndicator">
|
||||||
|
<div class="status-content">
|
||||||
|
<div class="status-dot"></div>
|
||||||
|
<span id="statusText">Initializing...</span>
|
||||||
|
</div>
|
||||||
|
<button id="lockBtn" class="btn btn-ghost btn-small hidden">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
|
||||||
|
<circle cx="12" cy="16" r="1"></circle>
|
||||||
|
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
|
||||||
|
</svg>
|
||||||
|
Lock
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Add Keypair Toggle Button -->
|
<div class="vault-header">
|
||||||
<div class="add-keypair-toggle">
|
<h2>Your Keypairs</h2>
|
||||||
<button id="toggleAddKeypairBtn" class="btn btn-primary">
|
<button id="toggleAddKeypairBtn" class="btn btn-primary">
|
||||||
<span class="btn-icon">+</span>
|
<span class="btn-icon">+</span>
|
||||||
Add Keypair
|
Add Keypair
|
||||||
@ -99,25 +115,88 @@
|
|||||||
<label>Public Key:</label>
|
<label>Public Key:</label>
|
||||||
<div class="public-key-container">
|
<div class="public-key-container">
|
||||||
<code id="selectedPublicKey">-</code>
|
<code id="selectedPublicKey">-</code>
|
||||||
<button id="copyPublicKeyBtn" class="btn-copy" title="Copy to clipboard">📋</button>
|
<button id="copyPublicKeyBtn" class="btn-copy" title="Copy to clipboard">
|
||||||
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Sign Message -->
|
<!-- Crypto Operations -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h3>Sign Message</h3>
|
<h3>Crypto Operations</h3>
|
||||||
<div class="form-group">
|
|
||||||
<label for="messageInput">Message (hex or text)</label>
|
<!-- Operation Tabs -->
|
||||||
<textarea id="messageInput" placeholder="Enter message to sign" rows="3"></textarea>
|
<div class="operation-tabs">
|
||||||
|
<button class="tab-btn active" data-tab="encrypt">Encrypt</button>
|
||||||
|
<button class="tab-btn" data-tab="decrypt">Decrypt</button>
|
||||||
|
<button class="tab-btn" data-tab="sign">Sign</button>
|
||||||
|
<button class="tab-btn" data-tab="verify">Verify</button>
|
||||||
</div>
|
</div>
|
||||||
<button id="signBtn" class="btn btn-primary" disabled>Sign Message</button>
|
|
||||||
<div id="signatureResult" class="signature-result hidden">
|
<!-- Encrypt Tab -->
|
||||||
<label>Signature:</label>
|
<div class="tab-content active" id="encrypt-tab">
|
||||||
<div class="signature-container">
|
<div class="form-group">
|
||||||
<code id="signatureValue"></code>
|
<label for="encryptMessageInput">Message to Encrypt</label>
|
||||||
<button id="copySignatureBtn" class="btn-copy" title="Copy to clipboard">📋</button>
|
<textarea id="encryptMessageInput" placeholder="Enter message to encrypt..." rows="3"></textarea>
|
||||||
|
</div>
|
||||||
|
<button id="encryptBtn" class="btn btn-primary" disabled>Encrypt Message</button>
|
||||||
|
|
||||||
|
<div class="encrypt-result hidden" id="encryptResult">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Decrypt Tab -->
|
||||||
|
<div class="tab-content" id="decrypt-tab">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="encryptedMessageInput">Encrypted Message</label>
|
||||||
|
<textarea id="encryptedMessageInput" placeholder="Enter encrypted message..." rows="3"></textarea>
|
||||||
|
</div>
|
||||||
|
<button id="decryptBtn" class="btn btn-primary" disabled>Decrypt Message</button>
|
||||||
|
|
||||||
|
<div class="decrypt-result hidden" id="decryptResult">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sign Tab -->
|
||||||
|
<div class="tab-content" id="sign-tab">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="messageInput">Message to Sign</label>
|
||||||
|
<textarea id="messageInput" placeholder="Enter your message here..." rows="3"></textarea>
|
||||||
|
</div>
|
||||||
|
<button id="signBtn" class="btn btn-primary" disabled>Sign Message</button>
|
||||||
|
|
||||||
|
<div class="signature-result hidden" id="signatureResult">
|
||||||
|
<label>Signature:</label>
|
||||||
|
<div class="signature-container">
|
||||||
|
<code id="signatureValue">-</code>
|
||||||
|
<button id="copySignatureBtn" class="btn-copy" title="Copy to clipboard">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Verify Tab -->
|
||||||
|
<div class="tab-content" id="verify-tab">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="verifyMessageInput">Original Message</label>
|
||||||
|
<textarea id="verifyMessageInput" placeholder="Enter the original message..." rows="3"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="signatureToVerifyInput">Signature</label>
|
||||||
|
<input type="text" id="signatureToVerifyInput" placeholder="Enter signature to verify...">
|
||||||
|
</div>
|
||||||
|
<button id="verifyBtn" class="btn btn-primary" disabled>Verify Signature</button>
|
||||||
|
|
||||||
|
<div class="verify-result hidden" id="verifyResult">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -129,10 +208,11 @@
|
|||||||
<p>Processing...</p>
|
<p>Processing...</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Toast Notifications -->
|
|
||||||
<div id="toast" class="toast hidden"></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Enhanced JavaScript modules -->
|
||||||
|
<script src="js/errorHandler.js"></script>
|
||||||
<script src="popup.js"></script>
|
<script src="popup.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -202,33 +202,6 @@ function debugString(val) {
|
|||||||
// TODO we could test for more things here, like `Set`s and `Map`s.
|
// TODO we could test for more things here, like `Set`s and `Map`s.
|
||||||
return className;
|
return className;
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
* Initialize the scripting environment (must be called before run_rhai)
|
|
||||||
*/
|
|
||||||
export function init_rhai_env() {
|
|
||||||
wasm.init_rhai_env();
|
|
||||||
}
|
|
||||||
|
|
||||||
function takeFromExternrefTable0(idx) {
|
|
||||||
const value = wasm.__wbindgen_export_2.get(idx);
|
|
||||||
wasm.__externref_table_dealloc(idx);
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Securely run a Rhai script in the extension context (must be called only after user approval)
|
|
||||||
* @param {string} script
|
|
||||||
* @returns {any}
|
|
||||||
*/
|
|
||||||
export function run_rhai(script) {
|
|
||||||
const ptr0 = passStringToWasm0(script, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
|
||||||
const len0 = WASM_VECTOR_LEN;
|
|
||||||
const ret = wasm.run_rhai(ptr0, len0);
|
|
||||||
if (ret[2]) {
|
|
||||||
throw takeFromExternrefTable0(ret[1]);
|
|
||||||
}
|
|
||||||
return takeFromExternrefTable0(ret[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create and unlock a new keyspace with the given name and password
|
* Create and unlock a new keyspace with the given name and password
|
||||||
* @param {string} keyspace
|
* @param {string} keyspace
|
||||||
@ -266,6 +239,11 @@ export function lock_session() {
|
|||||||
wasm.lock_session();
|
wasm.lock_session();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function takeFromExternrefTable0(idx) {
|
||||||
|
const value = wasm.__wbindgen_export_2.get(idx);
|
||||||
|
wasm.__externref_table_dealloc(idx);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Get metadata of the currently selected keypair
|
* Get metadata of the currently selected keypair
|
||||||
* @returns {any}
|
* @returns {any}
|
||||||
@ -356,20 +334,81 @@ export function sign(message) {
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify a signature with the current session's selected keypair
|
||||||
|
* @param {Uint8Array} message
|
||||||
|
* @param {string} signature
|
||||||
|
* @returns {Promise<any>}
|
||||||
|
*/
|
||||||
|
export function verify(message, signature) {
|
||||||
|
const ptr0 = passArray8ToWasm0(message, wasm.__wbindgen_malloc);
|
||||||
|
const len0 = WASM_VECTOR_LEN;
|
||||||
|
const ptr1 = passStringToWasm0(signature, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||||
|
const len1 = WASM_VECTOR_LEN;
|
||||||
|
const ret = wasm.verify(ptr0, len0, ptr1, len1);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encrypt data using the current session's keyspace symmetric cipher
|
||||||
|
* @param {Uint8Array} data
|
||||||
|
* @returns {Promise<any>}
|
||||||
|
*/
|
||||||
|
export function encrypt_data(data) {
|
||||||
|
const ptr0 = passArray8ToWasm0(data, wasm.__wbindgen_malloc);
|
||||||
|
const len0 = WASM_VECTOR_LEN;
|
||||||
|
const ret = wasm.encrypt_data(ptr0, len0);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decrypt data using the current session's keyspace symmetric cipher
|
||||||
|
* @param {Uint8Array} encrypted
|
||||||
|
* @returns {Promise<any>}
|
||||||
|
*/
|
||||||
|
export function decrypt_data(encrypted) {
|
||||||
|
const ptr0 = passArray8ToWasm0(encrypted, wasm.__wbindgen_malloc);
|
||||||
|
const len0 = WASM_VECTOR_LEN;
|
||||||
|
const ret = wasm.decrypt_data(ptr0, len0);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the scripting environment (must be called before run_rhai)
|
||||||
|
*/
|
||||||
|
export function init_rhai_env() {
|
||||||
|
wasm.init_rhai_env();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Securely run a Rhai script in the extension context (must be called only after user approval)
|
||||||
|
* @param {string} script
|
||||||
|
* @returns {any}
|
||||||
|
*/
|
||||||
|
export function run_rhai(script) {
|
||||||
|
const ptr0 = passStringToWasm0(script, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||||
|
const len0 = WASM_VECTOR_LEN;
|
||||||
|
const ret = wasm.run_rhai(ptr0, len0);
|
||||||
|
if (ret[2]) {
|
||||||
|
throw takeFromExternrefTable0(ret[1]);
|
||||||
|
}
|
||||||
|
return takeFromExternrefTable0(ret[0]);
|
||||||
|
}
|
||||||
|
|
||||||
function __wbg_adapter_32(arg0, arg1, arg2) {
|
function __wbg_adapter_32(arg0, arg1, arg2) {
|
||||||
wasm.closure89_externref_shim(arg0, arg1, arg2);
|
wasm.closure121_externref_shim(arg0, arg1, arg2);
|
||||||
}
|
}
|
||||||
|
|
||||||
function __wbg_adapter_35(arg0, arg1, arg2) {
|
function __wbg_adapter_35(arg0, arg1, arg2) {
|
||||||
wasm.closure133_externref_shim(arg0, arg1, arg2);
|
wasm.closure150_externref_shim(arg0, arg1, arg2);
|
||||||
}
|
}
|
||||||
|
|
||||||
function __wbg_adapter_38(arg0, arg1, arg2) {
|
function __wbg_adapter_38(arg0, arg1, arg2) {
|
||||||
wasm.closure188_externref_shim(arg0, arg1, arg2);
|
wasm.closure227_externref_shim(arg0, arg1, arg2);
|
||||||
}
|
}
|
||||||
|
|
||||||
function __wbg_adapter_135(arg0, arg1, arg2, arg3) {
|
function __wbg_adapter_138(arg0, arg1, arg2, arg3) {
|
||||||
wasm.closure1847_externref_shim(arg0, arg1, arg2, arg3);
|
wasm.closure1879_externref_shim(arg0, arg1, arg2, arg3);
|
||||||
}
|
}
|
||||||
|
|
||||||
const __wbindgen_enum_IdbTransactionMode = ["readonly", "readwrite", "versionchange", "readwriteflush", "cleanup"];
|
const __wbindgen_enum_IdbTransactionMode = ["readonly", "readwrite", "versionchange", "readwriteflush", "cleanup"];
|
||||||
@ -519,7 +558,7 @@ function __wbg_get_imports() {
|
|||||||
const a = state0.a;
|
const a = state0.a;
|
||||||
state0.a = 0;
|
state0.a = 0;
|
||||||
try {
|
try {
|
||||||
return __wbg_adapter_135(a, state0.b, arg0, arg1);
|
return __wbg_adapter_138(a, state0.b, arg0, arg1);
|
||||||
} finally {
|
} finally {
|
||||||
state0.a = a;
|
state0.a = a;
|
||||||
}
|
}
|
||||||
@ -673,16 +712,16 @@ function __wbg_get_imports() {
|
|||||||
const ret = false;
|
const ret = false;
|
||||||
return ret;
|
return ret;
|
||||||
};
|
};
|
||||||
imports.wbg.__wbindgen_closure_wrapper288 = function(arg0, arg1, arg2) {
|
imports.wbg.__wbindgen_closure_wrapper378 = function(arg0, arg1, arg2) {
|
||||||
const ret = makeMutClosure(arg0, arg1, 90, __wbg_adapter_32);
|
const ret = makeMutClosure(arg0, arg1, 122, __wbg_adapter_32);
|
||||||
return ret;
|
return ret;
|
||||||
};
|
};
|
||||||
imports.wbg.__wbindgen_closure_wrapper518 = function(arg0, arg1, arg2) {
|
imports.wbg.__wbindgen_closure_wrapper549 = function(arg0, arg1, arg2) {
|
||||||
const ret = makeMutClosure(arg0, arg1, 134, __wbg_adapter_35);
|
const ret = makeMutClosure(arg0, arg1, 151, __wbg_adapter_35);
|
||||||
return ret;
|
return ret;
|
||||||
};
|
};
|
||||||
imports.wbg.__wbindgen_closure_wrapper776 = function(arg0, arg1, arg2) {
|
imports.wbg.__wbindgen_closure_wrapper857 = function(arg0, arg1, arg2) {
|
||||||
const ret = makeMutClosure(arg0, arg1, 189, __wbg_adapter_38);
|
const ret = makeMutClosure(arg0, arg1, 228, __wbg_adapter_38);
|
||||||
return ret;
|
return ret;
|
||||||
};
|
};
|
||||||
imports.wbg.__wbindgen_debug_string = function(arg0, arg1) {
|
imports.wbg.__wbindgen_debug_string = function(arg0, arg1) {
|
||||||
|
Binary file not shown.
Loading…
Reference in New Issue
Block a user