507 lines
15 KiB
Markdown
507 lines
15 KiB
Markdown
# 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. |