This commit is contained in:
despiegk 2025-04-21 13:02:32 +02:00
parent 2cf31905b0
commit c47f67b901
2 changed files with 758 additions and 70 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

@ -70,34 +70,181 @@ 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));
}
}
return spaces;
// 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();
};
});
}
// Remove space from localStorage
function removeSpaceFromStorage(spaceName) {
localStorage.removeItem(`${STORAGE_PREFIX}${spaceName}`);
// 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) => {
console.error('Error retrieving space:', event.target.error);
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) => {
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
@ -131,7 +278,7 @@ function updateLoginUI() {
}
// Update the spaces dropdown list
function updateSpacesList() {
async function updateSpacesList() {
const spacesList = document.getElementById('space-list');
// Clear existing options
@ -139,16 +286,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,8 +313,11 @@ 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;
@ -189,6 +343,7 @@ async function performLogin() {
document.getElementById('space-result').textContent = `Error logging in: ${result}`;
}
} catch (e) {
console.error('Login error:', e);
document.getElementById('space-result').textContent = `Error: ${e}`;
}
}
@ -203,19 +358,23 @@ async function performCreateSpace() {
return;
}
// Check if space already exists
if (getSpaceFromStorage(spaceName)) {
document.getElementById('space-result').textContent = `Space "${spaceName}" already exists`;
return;
}
try {
// 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;
}
// 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);
await saveSpaceToStorage(spaceName, encryptedSpace);
isLoggedIn = true;
currentSpace = spaceName;
@ -234,6 +393,7 @@ async function performCreateSpace() {
document.getElementById('space-result').textContent = `Error creating space: ${result}`;
}
} catch (e) {
console.error('Error creating space:', e);
document.getElementById('space-result').textContent = `Error: ${e}`;
}
}
@ -329,7 +489,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 +569,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 +584,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 +627,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 +652,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}`;
}
}
});