Compare commits

...

2 Commits

Author SHA1 Message Date
6573a01d75 ... 2025-04-21 13:17:56 +02:00
c47f67b901 ... 2025-04-21 13:02:32 +02:00
3 changed files with 988 additions and 128 deletions

View File

@ -0,0 +1,507 @@
# Implementation Plan: Migrating from LocalStorage to IndexedDB
## Overview
This document outlines the plan for migrating the WebAssembly crypto example application from using `localStorage` to `IndexedDB` for persisting encrypted key spaces. The primary motivations for this migration are:
1. Transaction capabilities for better data integrity
2. Improved performance for larger data operations
3. More structured approach to data storage
## Current Implementation
The current implementation uses localStorage with the following key functions:
```javascript
// LocalStorage functions for key spaces
const STORAGE_PREFIX = 'crypto_space_';
// Save encrypted space to localStorage
function saveSpaceToStorage(spaceName, encryptedData) {
localStorage.setItem(`${STORAGE_PREFIX}${spaceName}`, encryptedData);
}
// Get encrypted space from localStorage
function getSpaceFromStorage(spaceName) {
return localStorage.getItem(`${STORAGE_PREFIX}${spaceName}`);
}
// List all spaces in localStorage
function listSpacesFromStorage() {
const spaces = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key.startsWith(STORAGE_PREFIX)) {
spaces.push(key.substring(STORAGE_PREFIX.length));
}
}
return spaces;
}
// Remove space from localStorage
function removeSpaceFromStorage(spaceName) {
localStorage.removeItem(`${STORAGE_PREFIX}${spaceName}`);
}
```
## Implementation Plan
### 1. Database Structure
- Create a database named 'CryptoSpaceDB'
- Create an object store named 'keySpaces' with 'name' as the key path
- Add indexes for efficient querying: 'name' (unique) and 'lastAccessed'
```mermaid
erDiagram
KeySpaces {
string name PK
string encryptedData
date created
date lastAccessed
}
```
### 2. Database Initialization
Create a module for initializing and managing the IndexedDB database:
```javascript
// Database constants
const DB_NAME = 'CryptoSpaceDB';
const DB_VERSION = 1;
const STORE_NAME = 'keySpaces';
// Initialize the database
function initDatabase() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(DB_NAME, DB_VERSION);
request.onerror = (event) => {
reject('Error opening database: ' + event.target.error);
};
request.onsuccess = (event) => {
const db = event.target.result;
resolve(db);
};
request.onupgradeneeded = (event) => {
const db = event.target.result;
// Create object store for key spaces if it doesn't exist
if (!db.objectStoreNames.contains(STORE_NAME)) {
const store = db.createObjectStore(STORE_NAME, { keyPath: 'name' });
store.createIndex('name', 'name', { unique: true });
store.createIndex('lastAccessed', 'lastAccessed', { unique: false });
}
};
});
}
// Get database connection
function getDB() {
return initDatabase();
}
```
### 3. Replace Storage Functions
Replace the localStorage functions with IndexedDB equivalents:
```javascript
// Save encrypted space to IndexedDB
async function saveSpaceToStorage(spaceName, encryptedData) {
const db = await getDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction([STORE_NAME], 'readwrite');
const store = transaction.objectStore(STORE_NAME);
const space = {
name: spaceName,
encryptedData: encryptedData,
created: new Date(),
lastAccessed: new Date()
};
const request = store.put(space);
request.onsuccess = () => {
resolve();
};
request.onerror = (event) => {
reject('Error saving space: ' + event.target.error);
};
transaction.oncomplete = () => {
db.close();
};
});
}
// Get encrypted space from IndexedDB
async function getSpaceFromStorage(spaceName) {
const db = await getDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction([STORE_NAME], 'readonly');
const store = transaction.objectStore(STORE_NAME);
const request = store.get(spaceName);
request.onsuccess = (event) => {
const space = event.target.result;
if (space) {
// Update last accessed timestamp
updateLastAccessed(spaceName).catch(console.error);
resolve(space.encryptedData);
} else {
resolve(null);
}
};
request.onerror = (event) => {
reject('Error retrieving space: ' + event.target.error);
};
transaction.oncomplete = () => {
db.close();
};
});
}
// Update last accessed timestamp
async function updateLastAccessed(spaceName) {
const db = await getDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction([STORE_NAME], 'readwrite');
const store = transaction.objectStore(STORE_NAME);
const request = store.get(spaceName);
request.onsuccess = (event) => {
const space = event.target.result;
if (space) {
space.lastAccessed = new Date();
store.put(space);
resolve();
} else {
resolve();
}
};
transaction.oncomplete = () => {
db.close();
};
});
}
// List all spaces in IndexedDB
async function listSpacesFromStorage() {
const db = await getDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction([STORE_NAME], 'readonly');
const store = transaction.objectStore(STORE_NAME);
const request = store.openCursor();
const spaces = [];
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
spaces.push(cursor.value.name);
cursor.continue();
} else {
resolve(spaces);
}
};
request.onerror = (event) => {
reject('Error listing spaces: ' + event.target.error);
};
transaction.oncomplete = () => {
db.close();
};
});
}
// Remove space from IndexedDB
async function removeSpaceFromStorage(spaceName) {
const db = await getDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction([STORE_NAME], 'readwrite');
const store = transaction.objectStore(STORE_NAME);
const request = store.delete(spaceName);
request.onsuccess = () => {
resolve();
};
request.onerror = (event) => {
reject('Error removing space: ' + event.target.error);
};
transaction.oncomplete = () => {
db.close();
};
});
}
```
### 4. Update Application Flow
Update the login, logout, and other functions to handle the asynchronous nature of IndexedDB:
```javascript
// Login to a space
async function performLogin() {
const spaceName = document.getElementById('space-name').value.trim();
const password = document.getElementById('space-password').value;
if (!spaceName || !password) {
document.getElementById('space-result').textContent = 'Please enter both space name and password';
return;
}
try {
// Get encrypted space from IndexedDB
const encryptedSpace = await getSpaceFromStorage(spaceName);
if (!encryptedSpace) {
document.getElementById('space-result').textContent = `Space "${spaceName}" not found`;
return;
}
// Decrypt the space
const result = decrypt_key_space(encryptedSpace, password);
if (result === 0) {
isLoggedIn = true;
currentSpace = spaceName;
updateLoginUI();
updateKeypairsList();
document.getElementById('space-result').textContent = `Successfully logged in to space "${spaceName}"`;
// Setup auto-logout
updateActivity();
setupAutoLogout();
// Add activity listeners
document.addEventListener('click', updateActivity);
document.addEventListener('keypress', updateActivity);
} else {
document.getElementById('space-result').textContent = `Error logging in: ${result}`;
}
} catch (e) {
document.getElementById('space-result').textContent = `Error: ${e}`;
}
}
// Create a new space
async function performCreateSpace() {
const spaceName = document.getElementById('space-name').value.trim();
const password = document.getElementById('space-password').value;
if (!spaceName || !password) {
document.getElementById('space-result').textContent = 'Please enter both space name and password';
return;
}
try {
// Check if space already exists
const existingSpace = await getSpaceFromStorage(spaceName);
if (existingSpace) {
document.getElementById('space-result').textContent = `Space "${spaceName}" already exists`;
return;
}
// Create new space
const result = create_key_space(spaceName);
if (result === 0) {
// Encrypt and save the space
const encryptedSpace = encrypt_key_space(password);
await saveSpaceToStorage(spaceName, encryptedSpace);
isLoggedIn = true;
currentSpace = spaceName;
updateLoginUI();
updateKeypairsList();
document.getElementById('space-result').textContent = `Successfully created space "${spaceName}"`;
// Setup auto-logout
updateActivity();
setupAutoLogout();
// Add activity listeners
document.addEventListener('click', updateActivity);
document.addEventListener('keypress', updateActivity);
} else {
document.getElementById('space-result').textContent = `Error creating space: ${result}`;
}
} catch (e) {
document.getElementById('space-result').textContent = `Error: ${e}`;
}
}
// Delete a space from storage
async function deleteSpace(spaceName) {
if (!spaceName) return false;
try {
// Check if space exists
const existingSpace = await getSpaceFromStorage(spaceName);
if (!existingSpace) {
return false;
}
// Remove from IndexedDB
await removeSpaceFromStorage(spaceName);
// If this was the current space, logout
if (isLoggedIn && currentSpace === spaceName) {
performLogout();
}
return true;
} catch (e) {
console.error('Error deleting space:', e);
return false;
}
}
// Update the spaces dropdown list
async function updateSpacesList() {
const spacesList = document.getElementById('space-list');
// Clear existing options
while (spacesList.options.length > 1) {
spacesList.remove(1);
}
try {
// Get spaces list
const spaces = await listSpacesFromStorage();
// Add options for each space
spaces.forEach(spaceName => {
const option = document.createElement('option');
option.value = spaceName;
option.textContent = spaceName;
spacesList.appendChild(option);
});
} catch (e) {
console.error('Error updating spaces list:', e);
}
}
// Save the current space to storage
async function saveCurrentSpace() {
if (!isLoggedIn || !currentSpace) return;
try {
// Store the password in a session variable when logging in
// and use it here to avoid issues when the password field is cleared
const password = document.getElementById('space-password').value;
if (!password) {
console.error('Password not available for saving space');
alert('Please re-enter your password to save changes');
return;
}
const encryptedSpace = encrypt_key_space(password);
await saveSpaceToStorage(currentSpace, encryptedSpace);
} catch (e) {
console.error('Error saving space:', e);
}
}
```
### 5. Update Event Handlers
Update the event handlers in the `run()` function to handle asynchronous operations:
```javascript
document.getElementById('delete-space-button').addEventListener('click', async () => {
if (confirm(`Are you sure you want to delete the space "${currentSpace}"? This action cannot be undone.`)) {
try {
if (await deleteSpace(currentSpace)) {
document.getElementById('space-result').textContent = `Space "${currentSpace}" deleted successfully`;
} else {
document.getElementById('space-result').textContent = `Error deleting space "${currentSpace}"`;
}
} catch (e) {
document.getElementById('space-result').textContent = `Error: ${e}`;
}
}
});
document.getElementById('delete-selected-space-button').addEventListener('click', async () => {
const selectedSpace = document.getElementById('space-list').value;
if (!selectedSpace) {
document.getElementById('space-result').textContent = 'Please select a space to delete';
return;
}
if (confirm(`Are you sure you want to delete the space "${selectedSpace}"? This action cannot be undone.`)) {
try {
if (await deleteSpace(selectedSpace)) {
document.getElementById('space-result').textContent = `Space "${selectedSpace}" deleted successfully`;
await updateSpacesList();
} else {
document.getElementById('space-result').textContent = `Error deleting space "${selectedSpace}"`;
}
} catch (e) {
document.getElementById('space-result').textContent = `Error: ${e}`;
}
}
});
```
## Testing Strategy
1. **Unit Tests**:
- Test individual IndexedDB functions
- Verify CRUD operations work correctly
2. **Integration Tests**:
- Test full application flow with IndexedDB
- Verify UI updates correctly
3. **Error Handling Tests**:
- Test database connection errors
- Test transaction rollbacks
4. **Performance Tests**:
- Compare performance with localStorage
- Verify improved performance for larger data sets
## Potential Challenges and Solutions
1. **Browser Compatibility**:
- IndexedDB is supported in all modern browsers, but older browsers might have compatibility issues
- Consider using a feature detection approach before initializing IndexedDB
- Provide a fallback mechanism for browsers that don't support IndexedDB
2. **Transaction Management**:
- Properly manage transactions to maintain data integrity
- Ensure all operations within a transaction are completed or rolled back
- Use appropriate transaction modes ('readonly' or 'readwrite')
3. **Error Handling**:
- Implement comprehensive error handling for all IndexedDB operations
- Provide user-friendly error messages
- Log detailed error information for debugging
4. **Asynchronous Operations**:
- Handle Promise rejections with try/catch blocks
- Provide loading indicators for operations that might take time
- Consider using async/await for cleaner code and better error handling
## Implementation Steps
1. Create the database initialization module
2. Implement the IndexedDB storage functions
3. Update the UI functions to handle asynchronous operations
4. Add comprehensive error handling
5. Test all functionality
6. Deploy the updated application
## Conclusion
Migrating from localStorage to IndexedDB will provide better performance, transaction capabilities, and a more structured approach to data storage. The asynchronous nature of IndexedDB requires updates to the application flow, but the benefits outweigh the implementation effort.

View File

@ -33,17 +33,107 @@ function hexToBuffer(hex) {
return bytes;
}
// LocalStorage functions for Ethereum wallets
const ETH_WALLET_PREFIX = 'eth_wallet_';
// IndexedDB setup for Ethereum wallets
const DB_NAME = 'EthWalletDB';
const DB_VERSION = 1;
const STORE_NAME = 'ethWallets';
// Save Ethereum wallet to localStorage
function saveEthWalletToStorage(address, privateKey) {
localStorage.setItem(`${ETH_WALLET_PREFIX}${address}`, privateKey);
// Initialize the database
function initDatabase() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(DB_NAME, DB_VERSION);
request.onerror = (event) => {
console.error('Error opening Ethereum wallet database:', event.target.error);
reject('Error opening database: ' + event.target.error);
};
request.onsuccess = (event) => {
const db = event.target.result;
resolve(db);
};
request.onupgradeneeded = (event) => {
const db = event.target.result;
// Create object store for Ethereum wallets if it doesn't exist
if (!db.objectStoreNames.contains(STORE_NAME)) {
const store = db.createObjectStore(STORE_NAME, { keyPath: 'address' });
store.createIndex('address', 'address', { unique: true });
}
};
});
}
// Get Ethereum wallet from localStorage
function getEthWalletFromStorage(address) {
return localStorage.getItem(`${ETH_WALLET_PREFIX}${address}`);
// Get database connection
function getDB() {
return initDatabase();
}
// Save Ethereum wallet to IndexedDB
async function saveEthWalletToStorage(address, privateKey) {
try {
const db = await getDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction([STORE_NAME], 'readwrite');
const store = transaction.objectStore(STORE_NAME);
const wallet = {
address: address,
privateKey: privateKey,
created: new Date()
};
const request = store.put(wallet);
request.onsuccess = () => {
resolve();
};
request.onerror = (event) => {
console.error('Error saving Ethereum wallet:', event.target.error);
reject('Error saving wallet: ' + event.target.error);
};
transaction.oncomplete = () => {
db.close();
};
});
} catch (error) {
console.error('Database error in saveEthWalletToStorage:', error);
}
}
// Get Ethereum wallet from IndexedDB
async function getEthWalletFromStorage(address) {
try {
const db = await getDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction([STORE_NAME], 'readonly');
const store = transaction.objectStore(STORE_NAME);
const request = store.get(address);
request.onsuccess = (event) => {
const wallet = event.target.result;
if (wallet) {
resolve(wallet.privateKey);
} else {
resolve(null);
}
};
request.onerror = (event) => {
console.error('Error retrieving Ethereum wallet:', event.target.error);
reject('Error retrieving wallet: ' + event.target.error);
};
transaction.oncomplete = () => {
db.close();
};
});
} catch (error) {
console.error('Database error in getEthWalletFromStorage:', error);
return null;
}
}
// Session state
@ -147,13 +237,20 @@ async function performCreateEthereumWallet() {
}
try {
// Show loading state
document.getElementById('ethereum-wallet-result').textContent = 'Creating wallet...';
// Create Ethereum wallet
console.log('Creating Ethereum wallet from keypair:', selectedKeypair);
const result = create_ethereum_wallet();
console.log('Create Ethereum wallet result:', result);
if (result === 0) {
hasEthereumWallet = true;
// Get and display Ethereum address
const address = get_ethereum_address();
console.log('Generated Ethereum address:', address);
document.getElementById('ethereum-address-value').textContent = address;
// Get and display private key
@ -163,14 +260,22 @@ async function performCreateEthereumWallet() {
// Show the wallet info
document.getElementById('ethereum-wallet-info').classList.remove('hidden');
// Save the wallet to localStorage
saveEthWalletToStorage(address, privateKey);
document.getElementById('ethereum-wallet-result').textContent = 'Successfully created Ethereum wallet';
try {
// Save the wallet to IndexedDB
console.log('Saving wallet to IndexedDB:', address);
await saveEthWalletToStorage(address, privateKey);
console.log('Wallet saved successfully');
document.getElementById('ethereum-wallet-result').textContent = 'Successfully created Ethereum wallet';
} catch (saveError) {
console.error('Error saving wallet to IndexedDB:', saveError);
document.getElementById('ethereum-wallet-result').textContent = 'Wallet created but failed to save to storage';
}
} else {
document.getElementById('ethereum-wallet-result').textContent = `Error creating Ethereum wallet: ${result}`;
}
} catch (e) {
console.error('Error in performCreateEthereumWallet:', e);
document.getElementById('ethereum-wallet-result').textContent = `Error: ${e}`;
}
}
@ -190,13 +295,20 @@ async function performCreateEthereumWalletFromName() {
}
try {
// Show loading state
document.getElementById('ethereum-wallet-result').textContent = 'Creating wallet...';
// Create Ethereum wallet from name
console.log('Creating Ethereum wallet from name:', name);
const result = create_ethereum_wallet_from_name(name);
console.log('Create Ethereum wallet from name result:', result);
if (result === 0) {
hasEthereumWallet = true;
// Get and display Ethereum address
const address = get_ethereum_address();
console.log('Generated Ethereum address:', address);
document.getElementById('ethereum-address-value').textContent = address;
// Get and display private key
@ -206,14 +318,22 @@ async function performCreateEthereumWalletFromName() {
// Show the wallet info
document.getElementById('ethereum-wallet-info').classList.remove('hidden');
// Save the wallet to localStorage
saveEthWalletToStorage(address, privateKey);
document.getElementById('ethereum-wallet-result').textContent = `Successfully created Ethereum wallet from name "${name}"`;
try {
// Save the wallet to IndexedDB
console.log('Saving wallet to IndexedDB:', address);
await saveEthWalletToStorage(address, privateKey);
console.log('Wallet saved successfully');
document.getElementById('ethereum-wallet-result').textContent = `Successfully created Ethereum wallet from name "${name}"`;
} catch (saveError) {
console.error('Error saving wallet to IndexedDB:', saveError);
document.getElementById('ethereum-wallet-result').textContent = 'Wallet created but failed to save to storage';
}
} else {
document.getElementById('ethereum-wallet-result').textContent = `Error creating Ethereum wallet: ${result}`;
}
} catch (e) {
console.error('Error in performCreateEthereumWalletFromName:', e);
document.getElementById('ethereum-wallet-result').textContent = `Error: ${e}`;
}
}
@ -228,13 +348,20 @@ async function performCreateEthereumWalletFromPrivateKey() {
}
try {
// Show loading state
document.getElementById('ethereum-wallet-result').textContent = 'Creating wallet...';
// Create Ethereum wallet from private key
console.log('Creating Ethereum wallet from private key');
const result = create_ethereum_wallet_from_private_key(privateKey);
console.log('Create Ethereum wallet from private key result:', result);
if (result === 0) {
hasEthereumWallet = true;
// Get and display Ethereum address
const address = get_ethereum_address();
console.log('Generated Ethereum address:', address);
document.getElementById('ethereum-address-value').textContent = address;
// Get and display private key
@ -244,14 +371,22 @@ async function performCreateEthereumWalletFromPrivateKey() {
// Show the wallet info
document.getElementById('ethereum-wallet-info').classList.remove('hidden');
// Save the wallet to localStorage
saveEthWalletToStorage(address, displayPrivateKey);
document.getElementById('ethereum-wallet-result').textContent = 'Successfully imported Ethereum wallet from private key';
try {
// Save the wallet to IndexedDB
console.log('Saving wallet to IndexedDB:', address);
await saveEthWalletToStorage(address, displayPrivateKey);
console.log('Wallet saved successfully');
document.getElementById('ethereum-wallet-result').textContent = 'Successfully imported Ethereum wallet from private key';
} catch (saveError) {
console.error('Error saving wallet to IndexedDB:', saveError);
document.getElementById('ethereum-wallet-result').textContent = 'Wallet imported but failed to save to storage';
}
} else {
document.getElementById('ethereum-wallet-result').textContent = `Error importing Ethereum wallet: ${result}`;
}
} catch (e) {
console.error('Error in performCreateEthereumWalletFromPrivateKey:', e);
document.getElementById('ethereum-wallet-result').textContent = `Error: ${e}`;
}
}

View File

@ -70,34 +70,186 @@ function clearAutoLogout() {
}
}
// LocalStorage functions for key spaces
const STORAGE_PREFIX = 'crypto_space_';
// IndexedDB setup and functions
const DB_NAME = 'CryptoSpaceDB';
const DB_VERSION = 1;
const STORE_NAME = 'keySpaces';
// Save encrypted space to localStorage
function saveSpaceToStorage(spaceName, encryptedData) {
localStorage.setItem(`${STORAGE_PREFIX}${spaceName}`, encryptedData);
// Initialize the database
function initDatabase() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(DB_NAME, DB_VERSION);
request.onerror = (event) => {
console.error('Error opening database:', event.target.error);
reject('Error opening database: ' + event.target.error);
};
request.onsuccess = (event) => {
const db = event.target.result;
resolve(db);
};
request.onupgradeneeded = (event) => {
const db = event.target.result;
// Create object store for key spaces if it doesn't exist
if (!db.objectStoreNames.contains(STORE_NAME)) {
const store = db.createObjectStore(STORE_NAME, { keyPath: 'name' });
store.createIndex('name', 'name', { unique: true });
store.createIndex('lastAccessed', 'lastAccessed', { unique: false });
}
};
});
}
// Get encrypted space from localStorage
function getSpaceFromStorage(spaceName) {
return localStorage.getItem(`${STORAGE_PREFIX}${spaceName}`);
// Get database connection
function getDB() {
return initDatabase();
}
// List all spaces in localStorage
function listSpacesFromStorage() {
const spaces = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key.startsWith(STORAGE_PREFIX)) {
spaces.push(key.substring(STORAGE_PREFIX.length));
}
// Save encrypted space to IndexedDB
async function saveSpaceToStorage(spaceName, encryptedData) {
const db = await getDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction([STORE_NAME], 'readwrite');
const store = transaction.objectStore(STORE_NAME);
const space = {
name: spaceName,
encryptedData: encryptedData,
created: new Date(),
lastAccessed: new Date()
};
const request = store.put(space);
request.onsuccess = () => {
resolve();
};
request.onerror = (event) => {
console.error('Error saving space:', event.target.error);
reject('Error saving space: ' + event.target.error);
};
transaction.oncomplete = () => {
db.close();
};
});
}
// Get encrypted space from IndexedDB
async function getSpaceFromStorage(spaceName) {
try {
const db = await getDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction([STORE_NAME], 'readonly');
const store = transaction.objectStore(STORE_NAME);
const request = store.get(spaceName);
request.onsuccess = (event) => {
const space = event.target.result;
if (space) {
// Update last accessed timestamp
updateLastAccessed(spaceName).catch(console.error);
resolve(space.encryptedData);
} else {
resolve(null);
}
};
request.onerror = (event) => {
console.error('Error retrieving space:', event.target.error);
reject('Error retrieving space: ' + event.target.error);
};
transaction.oncomplete = () => {
db.close();
};
});
} catch (error) {
console.error('Database error in getSpaceFromStorage:', error);
return null;
}
return spaces;
}
// Remove space from localStorage
function removeSpaceFromStorage(spaceName) {
localStorage.removeItem(`${STORAGE_PREFIX}${spaceName}`);
// Update last accessed timestamp
async function updateLastAccessed(spaceName) {
const db = await getDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction([STORE_NAME], 'readwrite');
const store = transaction.objectStore(STORE_NAME);
const request = store.get(spaceName);
request.onsuccess = (event) => {
const space = event.target.result;
if (space) {
space.lastAccessed = new Date();
store.put(space);
resolve();
} else {
resolve();
}
};
transaction.oncomplete = () => {
db.close();
};
});
}
// List all spaces in IndexedDB
async function listSpacesFromStorage() {
const db = await getDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction([STORE_NAME], 'readonly');
const store = transaction.objectStore(STORE_NAME);
const request = store.openCursor();
const spaces = [];
request.onsuccess = (event) => {
const cursor = event.target.result;
if (cursor) {
spaces.push(cursor.value.name);
cursor.continue();
} else {
resolve(spaces);
}
};
request.onerror = (event) => {
console.error('Error listing spaces:', event.target.error);
reject('Error listing spaces: ' + event.target.error);
};
transaction.oncomplete = () => {
db.close();
};
});
}
// Remove space from IndexedDB
async function removeSpaceFromStorage(spaceName) {
const db = await getDB();
return new Promise((resolve, reject) => {
const transaction = db.transaction([STORE_NAME], 'readwrite');
const store = transaction.objectStore(STORE_NAME);
const request = store.delete(spaceName);
request.onsuccess = () => {
resolve();
};
request.onerror = (event) => {
console.error('Error removing space:', event.target.error);
reject('Error removing space: ' + event.target.error);
};
transaction.oncomplete = () => {
db.close();
};
});
}
// Session state
@ -106,7 +258,7 @@ let currentSpace = null;
let selectedKeypair = null;
// Update UI based on login state
function updateLoginUI() {
async function updateLoginUI() {
const loginForm = document.getElementById('login-form');
const logoutForm = document.getElementById('logout-form');
const loginStatus = document.getElementById('login-status');
@ -127,11 +279,15 @@ function updateLoginUI() {
}
// Update the spaces list
updateSpacesList();
try {
await updateSpacesList();
} catch (e) {
console.error('Error updating spaces list in UI:', e);
}
}
// Update the spaces dropdown list
function updateSpacesList() {
async function updateSpacesList() {
const spacesList = document.getElementById('space-list');
// Clear existing options
@ -139,16 +295,20 @@ function updateSpacesList() {
spacesList.remove(1);
}
// Get spaces list
const spaces = listSpacesFromStorage();
// Add options for each space
spaces.forEach(spaceName => {
const option = document.createElement('option');
option.value = spaceName;
option.textContent = spaceName;
spacesList.appendChild(option);
});
try {
// Get spaces list
const spaces = await listSpacesFromStorage();
// Add options for each space
spaces.forEach(spaceName => {
const option = document.createElement('option');
option.value = spaceName;
option.textContent = spaceName;
spacesList.appendChild(option);
});
} catch (e) {
console.error('Error updating spaces list:', e);
}
}
// Login to a space
@ -162,33 +322,46 @@ async function performLogin() {
}
try {
// Get encrypted space from localStorage
const encryptedSpace = getSpaceFromStorage(spaceName);
// Show loading state
document.getElementById('space-result').textContent = 'Loading...';
// Get encrypted space from IndexedDB
const encryptedSpace = await getSpaceFromStorage(spaceName);
if (!encryptedSpace) {
document.getElementById('space-result').textContent = `Space "${spaceName}" not found`;
return;
}
// Decrypt the space
const result = decrypt_key_space(encryptedSpace, password);
if (result === 0) {
isLoggedIn = true;
currentSpace = spaceName;
updateLoginUI();
updateKeypairsList();
document.getElementById('space-result').textContent = `Successfully logged in to space "${spaceName}"`;
console.log('Retrieved space from IndexedDB:', { spaceName, encryptedDataLength: encryptedSpace.length });
try {
// Decrypt the space - this is a synchronous WebAssembly function
const result = decrypt_key_space(encryptedSpace, password);
console.log('Decrypt result:', result);
// Setup auto-logout
updateActivity();
setupAutoLogout();
// Add activity listeners
document.addEventListener('click', updateActivity);
document.addEventListener('keypress', updateActivity);
} else {
document.getElementById('space-result').textContent = `Error logging in: ${result}`;
if (result === 0) {
isLoggedIn = true;
currentSpace = spaceName;
await updateLoginUI();
updateKeypairsList();
document.getElementById('space-result').textContent = `Successfully logged in to space "${spaceName}"`;
// Setup auto-logout
updateActivity();
setupAutoLogout();
// Add activity listeners
document.addEventListener('click', updateActivity);
document.addEventListener('keypress', updateActivity);
} else {
document.getElementById('space-result').textContent = `Error logging in: ${result}`;
}
} catch (decryptErr) {
console.error('Decryption error:', decryptErr);
document.getElementById('space-result').textContent = `Decryption error: ${decryptErr}`;
}
} catch (e) {
console.error('Login error:', e);
document.getElementById('space-result').textContent = `Error: ${e}`;
}
}
@ -203,37 +376,61 @@ async function performCreateSpace() {
return;
}
// Check if space already exists
if (getSpaceFromStorage(spaceName)) {
document.getElementById('space-result').textContent = `Space "${spaceName}" already exists`;
return;
}
try {
// Create new space
const result = create_key_space(spaceName);
if (result === 0) {
// Encrypt and save the space
const encryptedSpace = encrypt_key_space(password);
saveSpaceToStorage(spaceName, encryptedSpace);
// Show loading state
document.getElementById('space-result').textContent = 'Loading...';
// Check if space already exists
const existingSpace = await getSpaceFromStorage(spaceName);
if (existingSpace) {
document.getElementById('space-result').textContent = `Space "${spaceName}" already exists`;
return;
}
try {
// Create new space
console.log('Creating new space:', spaceName);
const result = create_key_space(spaceName);
console.log('Create space result:', result);
isLoggedIn = true;
currentSpace = spaceName;
updateLoginUI();
updateKeypairsList();
document.getElementById('space-result').textContent = `Successfully created space "${spaceName}"`;
// Setup auto-logout
updateActivity();
setupAutoLogout();
// Add activity listeners
document.addEventListener('click', updateActivity);
document.addEventListener('keypress', updateActivity);
} else {
document.getElementById('space-result').textContent = `Error creating space: ${result}`;
if (result === 0) {
try {
// Encrypt and save the space
console.log('Encrypting space with password');
const encryptedSpace = encrypt_key_space(password);
console.log('Encrypted space length:', encryptedSpace.length);
// Save to IndexedDB
console.log('Saving to IndexedDB');
await saveSpaceToStorage(spaceName, encryptedSpace);
console.log('Save completed');
isLoggedIn = true;
currentSpace = spaceName;
await updateLoginUI();
updateKeypairsList();
document.getElementById('space-result').textContent = `Successfully created space "${spaceName}"`;
// Setup auto-logout
updateActivity();
setupAutoLogout();
// Add activity listeners
document.addEventListener('click', updateActivity);
document.addEventListener('keypress', updateActivity);
} catch (encryptError) {
console.error('Error encrypting or saving space:', encryptError);
document.getElementById('space-result').textContent = `Error saving space: ${encryptError}`;
}
} else {
document.getElementById('space-result').textContent = `Error creating space: ${result}`;
}
} catch (createError) {
console.error('Error in WebAssembly create_key_space:', createError);
document.getElementById('space-result').textContent = `Error creating key space: ${createError}`;
}
} catch (e) {
console.error('Error checking existing space:', e);
document.getElementById('space-result').textContent = `Error: ${e}`;
}
}
@ -329,7 +526,7 @@ async function performCreateKeypair() {
// Display public key
displaySelectedKeypairPublicKey();
// Save the updated space to localStorage
// Save the updated space to IndexedDB
saveCurrentSpace();
} else {
document.getElementById('keypair-management-result').textContent = `Error creating keypair: ${result}`;
@ -409,8 +606,8 @@ function displaySelectedKeypairPublicKey() {
}
}
// Save the current space to localStorage
function saveCurrentSpace() {
// Save the current space to IndexedDB
async function saveCurrentSpace() {
if (!isLoggedIn || !currentSpace) return;
try {
@ -424,30 +621,37 @@ function saveCurrentSpace() {
}
const encryptedSpace = encrypt_key_space(password);
saveSpaceToStorage(currentSpace, encryptedSpace);
await saveSpaceToStorage(currentSpace, encryptedSpace);
} catch (e) {
console.error('Error saving space:', e);
alert('Error saving space: ' + e);
}
}
// Delete a space from localStorage
function deleteSpace(spaceName) {
// Delete a space from IndexedDB
async function deleteSpace(spaceName) {
if (!spaceName) return false;
// Check if space exists
if (!getSpaceFromStorage(spaceName)) {
try {
// Check if space exists
const existingSpace = await getSpaceFromStorage(spaceName);
if (!existingSpace) {
return false;
}
// Remove from IndexedDB
await removeSpaceFromStorage(spaceName);
// If this was the current space, logout
if (isLoggedIn && currentSpace === spaceName) {
performLogout();
}
return true;
} catch (e) {
console.error('Error deleting space:', e);
return false;
}
// Remove from localStorage
removeSpaceFromStorage(spaceName);
// If this was the current space, logout
if (isLoggedIn && currentSpace === spaceName) {
performLogout();
}
return true;
}
async function run() {
@ -460,17 +664,24 @@ async function run() {
document.getElementById('login-button').addEventListener('click', performLogin);
document.getElementById('create-space-button').addEventListener('click', performCreateSpace);
document.getElementById('logout-button').addEventListener('click', performLogout);
document.getElementById('delete-space-button').addEventListener('click', () => {
document.getElementById('delete-space-button').addEventListener('click', async () => {
if (confirm(`Are you sure you want to delete the space "${currentSpace}"? This action cannot be undone.`)) {
if (deleteSpace(currentSpace)) {
document.getElementById('space-result').textContent = `Space "${currentSpace}" deleted successfully`;
} else {
document.getElementById('space-result').textContent = `Error deleting space "${currentSpace}"`;
document.getElementById('space-result').textContent = 'Deleting...';
try {
const result = await deleteSpace(currentSpace);
if (result) {
document.getElementById('space-result').textContent = `Space "${currentSpace}" deleted successfully`;
} else {
document.getElementById('space-result').textContent = `Error deleting space "${currentSpace}"`;
}
} catch (e) {
console.error('Error during space deletion:', e);
document.getElementById('space-result').textContent = `Error: ${e}`;
}
}
});
document.getElementById('delete-selected-space-button').addEventListener('click', () => {
document.getElementById('delete-selected-space-button').addEventListener('click', async () => {
const selectedSpace = document.getElementById('space-list').value;
if (!selectedSpace) {
document.getElementById('space-result').textContent = 'Please select a space to delete';
@ -478,11 +689,18 @@ async function run() {
}
if (confirm(`Are you sure you want to delete the space "${selectedSpace}"? This action cannot be undone.`)) {
if (deleteSpace(selectedSpace)) {
document.getElementById('space-result').textContent = `Space "${selectedSpace}" deleted successfully`;
updateSpacesList();
} else {
document.getElementById('space-result').textContent = `Error deleting space "${selectedSpace}"`;
document.getElementById('space-result').textContent = 'Deleting...';
try {
const result = await deleteSpace(selectedSpace);
if (result) {
document.getElementById('space-result').textContent = `Space "${selectedSpace}" deleted successfully`;
await updateSpacesList();
} else {
document.getElementById('space-result').textContent = `Error deleting space "${selectedSpace}"`;
}
} catch (e) {
console.error('Error during space deletion:', e);
document.getElementById('space-result').textContent = `Error: ${e}`;
}
}
});