sal-modular/crypto_vault_extension/popup.js
2025-05-27 17:15:53 +03:00

648 lines
21 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Utility functions
function showToast(message, type = 'info') {
const toast = document.getElementById('toast');
toast.textContent = message;
toast.className = `toast ${type}`;
setTimeout(() => toast.classList.add('hidden'), 3000);
}
function showLoading(show = true) {
const overlay = document.getElementById('loadingOverlay');
overlay.classList.toggle('hidden', !show);
}
// Enhanced loading states for buttons
function setButtonLoading(button, loading = true, originalText = null) {
if (loading) {
button.dataset.originalText = button.textContent;
button.classList.add('loading');
button.disabled = true;
} else {
button.classList.remove('loading');
button.disabled = false;
if (originalText) {
button.textContent = originalText;
} else if (button.dataset.originalText) {
button.textContent = button.dataset.originalText;
}
}
}
// Show inline loading for specific operations
function showInlineLoading(element, message = 'Processing...') {
element.innerHTML = `
<div class="inline-loading">
<div class="inline-spinner"></div>
<span>${message}</span>
</div>
`;
}
// Enhanced error handling utility
function getErrorMessage(error, fallback = 'An unexpected error occurred') {
if (!error) return fallback;
// If it's a string, return it
if (typeof error === 'string') {
return error.trim() || fallback;
}
// If it's an Error object
if (error instanceof Error) {
return error.message || fallback;
}
// If it's an object with error property
if (error.error) {
return getErrorMessage(error.error, fallback);
}
// If it's an object with message property
if (error.message) {
return error.message || fallback;
}
// Try to stringify if it's an object
if (typeof error === 'object') {
try {
const stringified = JSON.stringify(error);
if (stringified && stringified !== '{}') {
return stringified;
}
} catch (e) {
// Ignore JSON stringify errors
}
}
return fallback;
}
// Enhanced response error handling
function getResponseError(response, operation = 'operation') {
if (!response) {
return `Failed to ${operation}: No response received`;
}
if (response.success === false || response.error) {
const errorMsg = getErrorMessage(response.error, `${operation} failed`);
// Handle specific error types
if (errorMsg.includes('decryption error') || errorMsg.includes('aead::Error')) {
return 'Invalid password or corrupted keyspace data';
}
if (errorMsg.includes('Crypto error')) {
return 'Keyspace not found or corrupted. Try creating a new one.';
}
if (errorMsg.includes('not unlocked') || errorMsg.includes('session')) {
return 'Session expired. Please login again.';
}
return errorMsg;
}
return `Failed to ${operation}: Unknown error`;
}
function showSection(sectionId) {
document.querySelectorAll('.section').forEach(s => s.classList.add('hidden'));
document.getElementById(sectionId).classList.remove('hidden');
}
function setStatus(text, isConnected = false) {
document.getElementById('statusText').textContent = text;
const indicator = document.getElementById('statusIndicator');
indicator.classList.toggle('connected', isConnected);
}
// Message handling
async function sendMessage(action, data = {}) {
return new Promise((resolve) => {
chrome.runtime.sendMessage({ action, ...data }, resolve);
});
}
// Copy to clipboard
async function copyToClipboard(text) {
try {
await navigator.clipboard.writeText(text);
showToast('Copied to clipboard!', 'success');
} catch (err) {
showToast('Failed to copy', 'error');
}
}
// Convert string to Uint8Array
function stringToUint8Array(str) {
if (str.match(/^[0-9a-fA-F]+$/)) {
// Hex string
const bytes = [];
for (let i = 0; i < str.length; i += 2) {
bytes.push(parseInt(str.substr(i, 2), 16));
}
return bytes;
} else {
// Regular string
return Array.from(new TextEncoder().encode(str));
}
}
// DOM Elements
const elements = {
keyspaceInput: document.getElementById('keyspaceInput'),
passwordInput: document.getElementById('passwordInput'),
createKeyspaceBtn: document.getElementById('createKeyspaceBtn'),
loginBtn: document.getElementById('loginBtn'),
lockBtn: document.getElementById('lockBtn'),
toggleAddKeypairBtn: document.getElementById('toggleAddKeypairBtn'),
addKeypairCard: document.getElementById('addKeypairCard'),
keyTypeSelect: document.getElementById('keyTypeSelect'),
keyNameInput: document.getElementById('keyNameInput'),
addKeypairBtn: document.getElementById('addKeypairBtn'),
cancelAddKeypairBtn: document.getElementById('cancelAddKeypairBtn'),
keypairsList: document.getElementById('keypairsList'),
selectedKeypairCard: document.getElementById('selectedKeypairCard'),
messageInput: document.getElementById('messageInput'),
signBtn: document.getElementById('signBtn'),
signatureResult: document.getElementById('signatureResult'),
copyPublicKeyBtn: document.getElementById('copyPublicKeyBtn'),
copySignatureBtn: document.getElementById('copySignatureBtn'),
};
let currentKeyspace = null;
let selectedKeypairId = null;
// Initialize
document.addEventListener('DOMContentLoaded', async function() {
setStatus('Initializing...', false);
// Event listeners
elements.createKeyspaceBtn.addEventListener('click', createKeyspace);
elements.loginBtn.addEventListener('click', login);
elements.lockBtn.addEventListener('click', lockSession);
elements.toggleAddKeypairBtn.addEventListener('click', toggleAddKeypairForm);
elements.addKeypairBtn.addEventListener('click', addKeypair);
elements.cancelAddKeypairBtn.addEventListener('click', hideAddKeypairForm);
elements.signBtn.addEventListener('click', signMessage);
elements.copyPublicKeyBtn.addEventListener('click', () => {
const publicKey = document.getElementById('selectedPublicKey').textContent;
copyToClipboard(publicKey);
});
elements.copySignatureBtn.addEventListener('click', () => {
const signature = document.getElementById('signatureValue').textContent;
copyToClipboard(signature);
});
// Enable sign button when message is entered
elements.messageInput.addEventListener('input', () => {
elements.signBtn.disabled = !elements.messageInput.value.trim() || !selectedKeypairId;
});
// Keyboard shortcuts
document.addEventListener('keydown', (e) => {
// Escape key closes the add keypair form
if (e.key === 'Escape' && !elements.addKeypairCard.classList.contains('hidden')) {
hideAddKeypairForm();
}
// Enter key in the name input submits the form
if (e.key === 'Enter' && e.target === elements.keyNameInput) {
e.preventDefault();
if (elements.keyNameInput.value.trim()) {
addKeypair();
}
}
});
// Check for existing session
await checkExistingSession();
});
async function checkExistingSession() {
try {
const response = await sendMessage('getStatus');
if (response && response.success && response.status && response.session) {
// Session is active
currentKeyspace = response.session.keyspace;
elements.keyspaceInput.value = currentKeyspace;
setStatus(`Connected to ${currentKeyspace}`, true);
showSection('vaultSection');
await loadKeypairs();
showToast('Session restored!', 'success');
} else {
// No active session
setStatus('Ready', true);
showSection('authSection');
}
} catch (error) {
console.error('Error checking session:', error);
setStatus('Ready', true);
showSection('authSection');
// Don't show toast for session check errors as it's not user-initiated
}
}
// Toggle add keypair form
function toggleAddKeypairForm() {
const isHidden = elements.addKeypairCard.classList.contains('hidden');
if (isHidden) {
showAddKeypairForm();
} else {
hideAddKeypairForm();
}
}
function showAddKeypairForm() {
elements.addKeypairCard.classList.remove('hidden');
// Rotate the + icon to × when form is open
const icon = elements.toggleAddKeypairBtn.querySelector('.btn-icon');
icon.style.transform = 'rotate(45deg)';
// Focus on the name input after animation
setTimeout(() => {
elements.keyNameInput.focus();
}, 300);
}
function hideAddKeypairForm() {
elements.addKeypairCard.classList.add('hidden');
// Rotate the icon back to +
const icon = elements.toggleAddKeypairBtn.querySelector('.btn-icon');
icon.style.transform = 'rotate(0deg)';
// Clear the form
elements.keyNameInput.value = '';
elements.keyTypeSelect.selectedIndex = 0;
}
// Clear all vault-related state and UI
function clearVaultState() {
// Clear message input and signature result
elements.messageInput.value = '';
elements.signatureResult.classList.add('hidden');
document.getElementById('signatureValue').textContent = '';
// Clear selected keypair state
selectedKeypairId = null;
elements.signBtn.disabled = true;
// Clear selected keypair info (hidden elements)
document.getElementById('selectedName').textContent = '-';
document.getElementById('selectedType').textContent = '-';
document.getElementById('selectedPublicKey').textContent = '-';
// Hide add keypair form if open
hideAddKeypairForm();
// Clear keypairs list
elements.keypairsList.innerHTML = '<div class="loading">Loading keypairs...</div>';
}
async function createKeyspace() {
const keyspace = elements.keyspaceInput.value.trim();
const password = elements.passwordInput.value.trim();
if (!keyspace || !password) {
showToast('Please enter keyspace name and password', 'error');
return;
}
setButtonLoading(elements.createKeyspaceBtn, true);
try {
const response = await sendMessage('createKeyspace', { keyspace, password });
if (response && response.success) {
showToast('Keyspace created successfully!', 'success');
// Clear any existing state before auto-login
clearVaultState();
await login(); // Auto-login after creation
} else {
const errorMsg = getResponseError(response, 'create keyspace');
showToast(errorMsg, 'error');
}
} catch (error) {
const errorMsg = getErrorMessage(error, 'Failed to create keyspace');
console.error('Create keyspace error:', error);
showToast(errorMsg, 'error');
} finally {
setButtonLoading(elements.createKeyspaceBtn, false);
}
}
async function login() {
const keyspace = elements.keyspaceInput.value.trim();
const password = elements.passwordInput.value.trim();
if (!keyspace || !password) {
showToast('Please enter keyspace name and password', 'error');
return;
}
setButtonLoading(elements.loginBtn, true);
try {
const response = await sendMessage('initSession', { keyspace, password });
if (response && response.success) {
currentKeyspace = keyspace;
setStatus(`Connected to ${keyspace}`, true);
showSection('vaultSection');
// Clear any previous vault state before loading new keyspace
clearVaultState();
await loadKeypairs();
showToast('Logged in successfully!', 'success');
} else {
const errorMsg = getResponseError(response, 'login');
showToast(errorMsg, 'error');
}
} catch (error) {
const errorMsg = getErrorMessage(error, 'Failed to login');
console.error('Login error:', error);
showToast(errorMsg, 'error');
} finally {
setButtonLoading(elements.loginBtn, false);
}
}
async function lockSession() {
showLoading(true);
try {
await sendMessage('lockSession');
currentKeyspace = null;
selectedKeypairId = null;
setStatus('Locked', false);
showSection('authSection');
// Clear all form inputs
elements.keyspaceInput.value = '';
elements.passwordInput.value = '';
clearVaultState();
showToast('Session locked', 'info');
} catch (error) {
showToast('Error: ' + error.message, 'error');
} finally {
showLoading(false);
}
}
async function addKeypair() {
const keyType = elements.keyTypeSelect.value;
const keyName = elements.keyNameInput.value.trim();
if (!keyName) {
showToast('Please enter a name for the keypair', 'error');
return;
}
// Use button loading instead of full overlay
setButtonLoading(elements.addKeypairBtn, true);
try {
console.log('Adding keypair:', { keyType, keyName });
const metadata = JSON.stringify({ name: keyName });
console.log('Metadata:', metadata);
const response = await sendMessage('addKeypair', { keyType, metadata });
console.log('Add keypair response:', response);
if (response && response.success) {
console.log('Keypair added successfully, clearing input and reloading list...');
hideAddKeypairForm(); // Hide the form after successful addition
// Show inline loading in keypairs list while reloading
showInlineLoading(elements.keypairsList, 'Adding keypair...');
await loadKeypairs();
showToast('Keypair added successfully!', 'success');
} else {
const errorMsg = getResponseError(response, 'add keypair');
console.error('Failed to add keypair:', response);
showToast(errorMsg, 'error');
}
} catch (error) {
const errorMsg = getErrorMessage(error, 'Failed to add keypair');
console.error('Error adding keypair:', error);
showToast(errorMsg, 'error');
} finally {
setButtonLoading(elements.addKeypairBtn, false);
}
}
async function loadKeypairs() {
try {
console.log('Loading keypairs...');
const response = await sendMessage('listKeypairs');
console.log('Keypairs response:', response);
if (response && response.success) {
console.log('Keypairs data:', response.keypairs);
console.log('Keypairs data type:', typeof response.keypairs);
renderKeypairs(response.keypairs);
} else {
const errorMsg = getResponseError(response, 'load keypairs');
console.error('Failed to load keypairs:', response);
const container = elements.keypairsList;
container.innerHTML = '<div class="empty-state">Failed to load keypairs. Try refreshing.</div>';
showToast(errorMsg, 'error');
}
} catch (error) {
const errorMsg = getErrorMessage(error, 'Failed to load keypairs');
console.error('Error loading keypairs:', error);
const container = elements.keypairsList;
container.innerHTML = '<div class="empty-state">Error loading keypairs. Try refreshing.</div>';
showToast(errorMsg, 'error');
}
}
function renderKeypairs(keypairs) {
console.log('Rendering keypairs:', keypairs);
console.log('Keypairs type:', typeof keypairs);
console.log('Keypairs is array:', Array.isArray(keypairs));
const container = elements.keypairsList;
// Handle different data types that might be returned
let keypairArray = [];
if (Array.isArray(keypairs)) {
keypairArray = keypairs;
} else if (keypairs && typeof keypairs === 'object') {
// If it's an object, try to extract array from common properties
if (keypairs.keypairs && Array.isArray(keypairs.keypairs)) {
keypairArray = keypairs.keypairs;
} else if (keypairs.data && Array.isArray(keypairs.data)) {
keypairArray = keypairs.data;
} else {
console.log('Keypairs object structure:', Object.keys(keypairs));
// Try to convert object to array if it has numeric keys
const keys = Object.keys(keypairs);
if (keys.length > 0 && keys.every(key => !isNaN(key))) {
keypairArray = Object.values(keypairs);
}
}
}
console.log('Final keypair array:', keypairArray);
console.log('Array length:', keypairArray.length);
if (!keypairArray || keypairArray.length === 0) {
console.log('No keypairs to render');
container.innerHTML = '<div class="empty-state">No keypairs found. Add one above.</div>';
return;
}
console.log('Rendering', keypairArray.length, 'keypairs');
container.innerHTML = keypairArray.map((keypair, index) => {
console.log('Processing keypair:', keypair);
const metadata = typeof keypair.metadata === 'string'
? JSON.parse(keypair.metadata)
: keypair.metadata;
return `
<div class="keypair-item" data-id="${keypair.id}">
<div class="keypair-info">
<div class="keypair-name">${metadata.name || 'Unnamed'}</div>
<div class="keypair-type">${keypair.key_type}</div>
</div>
<button class="btn btn-small select-btn" data-keypair-id="${keypair.id}" data-index="${index}">
Select
</button>
</div>
`;
}).join('');
// Add event listeners to all select buttons
const selectButtons = container.querySelectorAll('.select-btn');
selectButtons.forEach(button => {
button.addEventListener('click', (e) => {
e.preventDefault(); // Prevent any default button behavior
e.stopPropagation(); // Stop event bubbling
const keypairId = e.target.getAttribute('data-keypair-id');
console.log('Select button clicked for keypair:', keypairId);
selectKeypair(keypairId);
});
});
}
async function selectKeypair(keyId) {
console.log('Selecting keypair:', keyId);
// Don't show loading overlay for selection - it's too disruptive
try {
// Update visual state immediately for better UX
updateKeypairSelection(keyId);
await sendMessage('selectKeypair', { keyId });
selectedKeypairId = keyId;
// Get keypair details for internal use (but don't show the card)
const metadataResponse = await sendMessage('getCurrentKeypairMetadata');
const publicKeyResponse = await sendMessage('getCurrentKeypairPublicKey');
if (metadataResponse && metadataResponse.success && publicKeyResponse && publicKeyResponse.success) {
const metadata = metadataResponse.metadata;
// Store the details in hidden elements for internal use
document.getElementById('selectedName').textContent = metadata.name || 'Unnamed';
document.getElementById('selectedType').textContent = metadata.key_type;
document.getElementById('selectedPublicKey').textContent = publicKeyResponse.publicKey;
// Enable sign button if message is entered
elements.signBtn.disabled = !elements.messageInput.value.trim();
// Show a subtle success message without toast
console.log(`Keypair "${metadata.name}" selected successfully`);
} else {
// Handle metadata or public key fetch failure
const metadataError = getResponseError(metadataResponse, 'get keypair metadata');
const publicKeyError = getResponseError(publicKeyResponse, 'get public key');
const errorMsg = metadataResponse && !metadataResponse.success ? metadataError : publicKeyError;
console.error('Failed to get keypair details:', { metadataResponse, publicKeyResponse });
updateKeypairSelection(null);
showToast(errorMsg, 'error');
}
} catch (error) {
const errorMsg = getErrorMessage(error, 'Failed to select keypair');
console.error('Error selecting keypair:', error);
// Revert visual state if there was an error
updateKeypairSelection(null);
showToast(errorMsg, 'error');
}
}
function updateKeypairSelection(selectedId) {
// Remove previous selection styling
const allKeypairs = document.querySelectorAll('.keypair-item');
allKeypairs.forEach(item => {
item.classList.remove('selected');
const button = item.querySelector('.select-btn');
button.textContent = 'Select';
button.classList.remove('selected');
});
// Add selection styling to the selected keypair (if any)
if (selectedId) {
const selectedKeypair = document.querySelector(`[data-id="${selectedId}"]`);
if (selectedKeypair) {
selectedKeypair.classList.add('selected');
const button = selectedKeypair.querySelector('.select-btn');
button.textContent = 'Selected';
button.classList.add('selected');
}
}
}
async function signMessage() {
const messageText = elements.messageInput.value.trim();
if (!messageText || !selectedKeypairId) {
showToast('Please enter a message and select a keypair', 'error');
return;
}
// Use button loading and show inline loading in signature area
setButtonLoading(elements.signBtn, true);
// Show loading in signature result area
elements.signatureResult.classList.remove('hidden');
showInlineLoading(elements.signatureResult, 'Signing message...');
try {
const messageBytes = stringToUint8Array(messageText);
const response = await sendMessage('sign', { message: messageBytes });
if (response && response.success) {
// Restore signature result structure and show signature
elements.signatureResult.innerHTML = `
<label>Signature:</label>
<div class="signature-container">
<code id="signatureValue">${response.signature}</code>
<button id="copySignatureBtn" class="btn-copy" title="Copy to clipboard">📋</button>
</div>
`;
// Re-attach copy event listener
document.getElementById('copySignatureBtn').addEventListener('click', () => {
copyToClipboard(response.signature);
});
showToast('Message signed successfully!', 'success');
} else {
const errorMsg = getResponseError(response, 'sign message');
elements.signatureResult.classList.add('hidden');
showToast(errorMsg, 'error');
}
} catch (error) {
const errorMsg = getErrorMessage(error, 'Failed to sign message');
console.error('Sign message error:', error);
elements.signatureResult.classList.add('hidden');
showToast(errorMsg, 'error');
} finally {
setButtonLoading(elements.signBtn, false);
}
}
// selectKeypair is now handled via event listeners, no need for global access