forked from jsnbuchanan/crowd-funder-for-time-pwa
1408 lines
38 KiB
Markdown
1408 lines
38 KiB
Markdown
# Secure Storage Implementation Guide for TimeSafari App
|
|
|
|
## Overview
|
|
|
|
This document outlines the implementation of secure storage for the TimeSafari app using Capacitor solutions. The implementation focuses on:
|
|
|
|
1. **Platform-Specific Storage Solutions**:
|
|
- Web: wa-sqlite with IndexedDB backend
|
|
- iOS: SQLCipher with Keychain integration
|
|
- Android: SQLCipher with Keystore integration
|
|
- Electron: SQLite with secure storage
|
|
|
|
2. **Key Features**:
|
|
- Encrypted storage using SQLCipher
|
|
- Platform-specific security features
|
|
- Migration support from existing implementations
|
|
- Consistent API across platforms
|
|
|
|
## Quick Start
|
|
|
|
### 1. Installation
|
|
|
|
```bash
|
|
# Core dependencies
|
|
npm install @capacitor-community/sqlite@6.0.0
|
|
npm install @wa-sqlite/sql.js@0.8.12
|
|
npm install @wa-sqlite/sql.js-httpvfs@0.8.12
|
|
|
|
# Platform-specific dependencies
|
|
npm install @capacitor/preferences@6.0.2
|
|
npm install @capacitor-community/biometric-auth@5.0.0
|
|
```
|
|
|
|
### 2. Basic Usage
|
|
|
|
```typescript
|
|
// src/services/storage/StorageService.ts
|
|
import { PlatformServiceFactory } from '../PlatformServiceFactory';
|
|
import { StorageError, StorageErrorCodes } from './errors/StorageError';
|
|
|
|
export class StorageService {
|
|
private static instance: StorageService;
|
|
private platformService: PlatformService;
|
|
|
|
private constructor() {
|
|
this.platformService = PlatformServiceFactory.create();
|
|
}
|
|
|
|
static getInstance(): StorageService {
|
|
if (!StorageService.instance) {
|
|
StorageService.instance = new StorageService();
|
|
}
|
|
return StorageService.instance;
|
|
}
|
|
|
|
async initialize(): Promise<void> {
|
|
try {
|
|
// Initialize databases
|
|
await this.platformService.openSecretDatabase();
|
|
await this.platformService.openAccountsDatabase();
|
|
|
|
// Check for migration
|
|
if (await this.platformService.needsMigration()) {
|
|
await this.handleMigration();
|
|
}
|
|
} catch (error) {
|
|
throw new StorageError(
|
|
'Failed to initialize storage service',
|
|
StorageErrorCodes.INITIALIZATION_FAILED,
|
|
error
|
|
);
|
|
}
|
|
}
|
|
|
|
private async handleMigration(): Promise<void> {
|
|
try {
|
|
// Show migration UI
|
|
const shouldMigrate = await this.showMigrationPrompt();
|
|
if (!shouldMigrate) return;
|
|
|
|
// Perform migration
|
|
await this.platformService.performMigration();
|
|
|
|
// Verify migration
|
|
await this.verifyMigration();
|
|
} catch (error) {
|
|
// Handle migration failure
|
|
await this.handleMigrationError(error);
|
|
}
|
|
}
|
|
|
|
// Example: Adding an account
|
|
async addAccount(account: Account): Promise<void> {
|
|
try {
|
|
await this.platformService.addAccount(account);
|
|
} catch (error) {
|
|
if (error instanceof StorageError) {
|
|
throw error;
|
|
}
|
|
throw new StorageError(
|
|
'Failed to add account',
|
|
StorageErrorCodes.QUERY_FAILED,
|
|
error
|
|
);
|
|
}
|
|
}
|
|
|
|
// Example: Retrieving an account
|
|
async getAccountByDid(did: string): Promise<Account | undefined> {
|
|
try {
|
|
return await this.platformService.getAccountByDid(did);
|
|
} catch (error) {
|
|
if (error instanceof StorageError) {
|
|
throw error;
|
|
}
|
|
throw new StorageError(
|
|
'Failed to retrieve account',
|
|
StorageErrorCodes.QUERY_FAILED,
|
|
error
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Usage example:
|
|
const storageService = StorageService.getInstance();
|
|
await storageService.initialize();
|
|
|
|
try {
|
|
const account = await storageService.getAccountByDid('did:example:123');
|
|
if (!account) {
|
|
await storageService.addAccount({
|
|
did: 'did:example:123',
|
|
publicKeyHex: '0x123...',
|
|
// ... other account properties
|
|
});
|
|
}
|
|
} catch (error) {
|
|
if (error instanceof StorageError) {
|
|
console.error(`Storage error: ${error.code}`, error.message);
|
|
} else {
|
|
console.error('Unexpected error:', error);
|
|
}
|
|
}
|
|
```
|
|
|
|
#### A. Modifying Code
|
|
|
|
When converting from Dexie.js to SQL-based implementation, follow these patterns:
|
|
|
|
1. **Database Access Pattern**
|
|
```typescript
|
|
// Before (Dexie)
|
|
const result = await db.table.where("field").equals(value).first();
|
|
|
|
// After (SQL)
|
|
const platform = PlatformServiceFactory.getInstance();
|
|
const result = await platform.dbQuery(
|
|
"SELECT * FROM table WHERE field = ?",
|
|
[value]
|
|
);
|
|
```
|
|
|
|
2. **Update Operations**
|
|
```typescript
|
|
// Before (Dexie)
|
|
await db.table.where("id").equals(id).modify(changes);
|
|
|
|
// After (SQL)
|
|
const { sql, params } = generateUpdateStatement(
|
|
changes,
|
|
"table",
|
|
"id = ?",
|
|
[id]
|
|
);
|
|
await platform.dbExec(sql, params);
|
|
```
|
|
|
|
3. **Insert Operations**
|
|
```typescript
|
|
// Before (Dexie)
|
|
await db.table.add(item);
|
|
|
|
// After (SQL)
|
|
const columns = Object.keys(item);
|
|
const values = Object.values(item);
|
|
const placeholders = values.map(() => '?').join(', ');
|
|
const sql = `INSERT INTO table (${columns.join(', ')}) VALUES (${placeholders})`;
|
|
await platform.dbExec(sql, values);
|
|
```
|
|
|
|
4. **Delete Operations**
|
|
```typescript
|
|
// Before (Dexie)
|
|
await db.table.where("id").equals(id).delete();
|
|
|
|
// After (SQL)
|
|
await platform.dbExec("DELETE FROM table WHERE id = ?", [id]);
|
|
```
|
|
|
|
5. **Result Processing**
|
|
```typescript
|
|
// Before (Dexie)
|
|
const items = await db.table.toArray();
|
|
|
|
// After (SQL)
|
|
const result = await platform.dbQuery("SELECT * FROM table");
|
|
const items = mapColumnsToValues(result.columns, result.values);
|
|
```
|
|
|
|
Key Considerations:
|
|
1. Always use parameterized queries to prevent SQL injection
|
|
2. Use the `generateUpdateStatement` helper for update operations
|
|
3. Use the `mapColumnsToValues` helper for processing query results
|
|
4. Handle transactions explicitly for batch operations
|
|
5. Use appropriate error handling with the StorageError class
|
|
6. Consider platform-specific capabilities when implementing features
|
|
|
|
Example Migration:
|
|
```typescript
|
|
// Before (Dexie)
|
|
export async function updateSettings(settings: Settings): Promise<void> {
|
|
await db.settings.put(settings);
|
|
}
|
|
|
|
// After (SQL)
|
|
export async function updateSettings(settings: Settings): Promise<void> {
|
|
const platform = PlatformServiceFactory.getInstance();
|
|
const { sql, params } = generateUpdateStatement(
|
|
settings,
|
|
"settings",
|
|
"id = ?",
|
|
[settings.id]
|
|
);
|
|
await platform.dbExec(sql, params);
|
|
}
|
|
```
|
|
|
|
Remember to:
|
|
- Create database access code to use the platform service, putting it in front of the Dexie version
|
|
- Instead of removing Dexie-specific code, keep it; if we use the results of the query, then check the USE_DEXIE_DB from app.ts and if it's true then use that instead of the SQL code.
|
|
- Test thoroughly after migration
|
|
- Consider data migration needs, and warn if there are any potential migration problems
|
|
|
|
### 3. Platform Detection
|
|
|
|
```typescript
|
|
// src/services/storage/PlatformDetection.ts
|
|
import { Capacitor } from '@capacitor/core';
|
|
import { StorageError, StorageErrorCodes } from './errors/StorageError';
|
|
|
|
export class PlatformDetection {
|
|
static isNativePlatform(): boolean {
|
|
return Capacitor.isNativePlatform();
|
|
}
|
|
|
|
static getPlatform(): 'ios' | 'android' | 'web' | 'electron' {
|
|
if (Capacitor.isNativePlatform()) {
|
|
return Capacitor.getPlatform() as 'ios' | 'android';
|
|
}
|
|
return window.electron ? 'electron' : 'web';
|
|
}
|
|
|
|
static async getCapabilities(): Promise<PlatformCapabilities> {
|
|
try {
|
|
const platform = this.getPlatform();
|
|
|
|
return {
|
|
hasFileSystem: platform !== 'web',
|
|
hasSecureStorage: platform !== 'web',
|
|
hasBiometrics: await this.checkBiometrics(),
|
|
isIOS: platform === 'ios',
|
|
isAndroid: platform === 'android'
|
|
};
|
|
} catch (error) {
|
|
throw new StorageError(
|
|
'Failed to detect platform capabilities',
|
|
StorageErrorCodes.INITIALIZATION_FAILED,
|
|
error
|
|
);
|
|
}
|
|
}
|
|
|
|
private static async checkBiometrics(): Promise<boolean> {
|
|
if (!this.isNativePlatform()) return false;
|
|
|
|
try {
|
|
const { BiometricAuth } = await import('@capacitor-community/biometric-auth');
|
|
const available = await BiometricAuth.isAvailable();
|
|
return available.has;
|
|
} catch (error) {
|
|
console.warn('Biometric check failed:', error);
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Usage example:
|
|
try {
|
|
const capabilities = await PlatformDetection.getCapabilities();
|
|
if (capabilities.hasSecureStorage) {
|
|
// Use platform-specific secure storage
|
|
await initializeSecureStorage();
|
|
} else {
|
|
// Fall back to web storage
|
|
await initializeWebStorage();
|
|
}
|
|
} catch (error) {
|
|
if (error instanceof StorageError) {
|
|
console.error(`Platform detection error: ${error.code}`, error.message);
|
|
} else {
|
|
console.error('Unexpected error during platform detection:', error);
|
|
}
|
|
}
|
|
```
|
|
|
|
### 4. Platform-Specific Implementations
|
|
|
|
#### Web Platform (wa-sqlite)
|
|
|
|
```typescript
|
|
// src/services/platforms/web/WebSQLiteService.ts
|
|
export class WebSQLiteService implements PlatformService {
|
|
private db: SQLite.Database | null = null;
|
|
private vfs: IDBBatchAtomicVFS | null = null;
|
|
private initialized = false;
|
|
|
|
async initialize(): Promise<void> {
|
|
if (this.initialized) return;
|
|
|
|
try {
|
|
// 1. Initialize SQLite
|
|
const sqlite3 = await this.initializeSQLite();
|
|
|
|
// 2. Set up VFS
|
|
await this.setupVFS(sqlite3);
|
|
|
|
// 3. Open database
|
|
await this.openDatabase();
|
|
|
|
// 4. Set up schema
|
|
await this.setupSchema();
|
|
|
|
// 5. Optimize performance
|
|
await this.optimizePerformance();
|
|
|
|
this.initialized = true;
|
|
} catch (error) {
|
|
throw new StorageError(
|
|
'Failed to initialize web SQLite',
|
|
StorageErrorCodes.INITIALIZATION_FAILED,
|
|
error
|
|
);
|
|
}
|
|
}
|
|
|
|
private async initializeSQLite(): Promise<SQLite.SqlJsStatic> {
|
|
try {
|
|
return await SQLite.init({
|
|
locateFile: file => `https://cdn.jsdelivr.net/npm/@wa-sqlite/sql.js@0.8.12/dist/${file}`
|
|
});
|
|
} catch (error) {
|
|
throw new StorageError(
|
|
'Failed to load SQLite WebAssembly',
|
|
StorageErrorCodes.WASM_LOAD_FAILED,
|
|
error
|
|
);
|
|
}
|
|
}
|
|
|
|
private async setupVFS(sqlite3: SQLite.SqlJsStatic): Promise<void> {
|
|
try {
|
|
this.vfs = new IDBBatchAtomicVFS('timesafari');
|
|
await this.vfs.registerVFS(sqlite3);
|
|
} catch (error) {
|
|
throw new StorageError(
|
|
'Failed to set up IndexedDB VFS',
|
|
StorageErrorCodes.VFS_SETUP_FAILED,
|
|
error
|
|
);
|
|
}
|
|
}
|
|
|
|
async openDatabase(): Promise<void> {
|
|
if (!this.vfs) {
|
|
throw new StorageError(
|
|
'VFS not initialized',
|
|
StorageErrorCodes.INITIALIZATION_FAILED
|
|
);
|
|
}
|
|
|
|
try {
|
|
this.db = await this.vfs.openDatabase('timesafari.db');
|
|
await this.setupPragmas();
|
|
} catch (error) {
|
|
throw new StorageError(
|
|
'Failed to open database',
|
|
StorageErrorCodes.INITIALIZATION_FAILED,
|
|
error
|
|
);
|
|
}
|
|
}
|
|
|
|
private async setupPragmas(): Promise<void> {
|
|
if (!this.db) return;
|
|
|
|
try {
|
|
await this.db.exec(`
|
|
PRAGMA journal_mode = WAL;
|
|
PRAGMA synchronous = NORMAL;
|
|
PRAGMA foreign_keys = ON;
|
|
PRAGMA busy_timeout = 5000;
|
|
`);
|
|
} catch (error) {
|
|
throw new StorageError(
|
|
'Failed to set up database pragmas',
|
|
StorageErrorCodes.INITIALIZATION_FAILED,
|
|
error
|
|
);
|
|
}
|
|
}
|
|
|
|
async close(): Promise<void> {
|
|
if (this.db) {
|
|
await this.db.close();
|
|
this.db = null;
|
|
}
|
|
this.initialized = false;
|
|
}
|
|
}
|
|
|
|
// Migration strategy for web platform
|
|
export class WebMigrationService {
|
|
async migrate(): Promise<void> {
|
|
// 1. Check prerequisites
|
|
await this.checkPrerequisites();
|
|
|
|
// 2. Create backup
|
|
const backup = await this.createBackup();
|
|
|
|
// 3. Perform migration
|
|
try {
|
|
await this.performMigration(backup);
|
|
} catch (error) {
|
|
// 4. Handle failure
|
|
await this.handleMigrationFailure(error, backup);
|
|
}
|
|
|
|
// 5. Verify migration
|
|
await this.verifyMigration(backup);
|
|
}
|
|
|
|
private async checkPrerequisites(): Promise<void> {
|
|
// Check IndexedDB availability
|
|
if (!window.indexedDB) {
|
|
throw new StorageError(
|
|
'IndexedDB not available',
|
|
StorageErrorCodes.INITIALIZATION_FAILED
|
|
);
|
|
}
|
|
|
|
// Check storage quota
|
|
const quota = await navigator.storage.estimate();
|
|
if (quota.quota && quota.usage && quota.usage > quota.quota * 0.9) {
|
|
throw new StorageError(
|
|
'Insufficient storage space',
|
|
StorageErrorCodes.INITIALIZATION_FAILED
|
|
);
|
|
}
|
|
}
|
|
|
|
private async createBackup(): Promise<MigrationBackup> {
|
|
const backup = {
|
|
timestamp: Date.now(),
|
|
accounts: await this.dexieDB.accounts.toArray(),
|
|
settings: await this.dexieDB.settings.toArray(),
|
|
contacts: await this.dexieDB.contacts.toArray()
|
|
};
|
|
|
|
// Store backup in IndexedDB
|
|
await this.storeBackup(backup);
|
|
|
|
return backup;
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Native Platform (iOS/Android)
|
|
|
|
```typescript
|
|
// src/services/platforms/native/NativeSQLiteService.ts
|
|
export class NativeSQLiteService implements PlatformService {
|
|
private db: SQLiteConnection | null = null;
|
|
private initialized = false;
|
|
|
|
async initialize(): Promise<void> {
|
|
if (this.initialized) return;
|
|
|
|
try {
|
|
// 1. Check platform capabilities
|
|
await this.checkPlatformCapabilities();
|
|
|
|
// 2. Initialize SQLite with encryption
|
|
await this.initializeEncryptedDatabase();
|
|
|
|
// 3. Set up schema
|
|
await this.setupSchema();
|
|
|
|
this.initialized = true;
|
|
} catch (error) {
|
|
throw new StorageError(
|
|
'Failed to initialize native SQLite',
|
|
StorageErrorCodes.INITIALIZATION_FAILED,
|
|
error
|
|
);
|
|
}
|
|
}
|
|
|
|
private async checkPlatformCapabilities(): Promise<void> {
|
|
const { Capacitor } = await import('@capacitor/core');
|
|
if (!Capacitor.isNativePlatform()) {
|
|
throw new StorageError(
|
|
'Not running on native platform',
|
|
StorageErrorCodes.INITIALIZATION_FAILED
|
|
);
|
|
}
|
|
}
|
|
|
|
private async initializeEncryptedDatabase(): Promise<void> {
|
|
const { SQLite } = await import('@capacitor-community/sqlite');
|
|
this.db = await SQLite.createConnection(
|
|
'timesafari',
|
|
false,
|
|
'encryption',
|
|
1,
|
|
false
|
|
);
|
|
await this.db.open();
|
|
}
|
|
|
|
async setupSchema(): Promise<void> {
|
|
if (!this.db) {
|
|
throw new StorageError(
|
|
'Database not initialized',
|
|
StorageErrorCodes.INITIALIZATION_FAILED
|
|
);
|
|
}
|
|
|
|
try {
|
|
await this.db.execute(`
|
|
CREATE TABLE IF NOT EXISTS accounts (
|
|
did TEXT PRIMARY KEY,
|
|
public_key_hex TEXT NOT NULL,
|
|
created_at INTEGER NOT NULL,
|
|
updated_at INTEGER NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS settings (
|
|
key TEXT PRIMARY KEY,
|
|
value TEXT NOT NULL,
|
|
updated_at INTEGER NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS contacts (
|
|
did TEXT PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
public_key_hex TEXT NOT NULL,
|
|
created_at INTEGER NOT NULL,
|
|
updated_at INTEGER NOT NULL
|
|
);
|
|
`);
|
|
} catch (error) {
|
|
throw new StorageError(
|
|
'Failed to set up database schema',
|
|
StorageErrorCodes.INITIALIZATION_FAILED,
|
|
error
|
|
);
|
|
}
|
|
}
|
|
|
|
async close(): Promise<void> {
|
|
if (this.db) {
|
|
await this.db.close();
|
|
this.db = null;
|
|
}
|
|
this.initialized = false;
|
|
}
|
|
}
|
|
```
|
|
|
|
### 5. Error Handling
|
|
|
|
```typescript
|
|
// src/services/storage/errors/StorageError.ts
|
|
export enum StorageErrorCodes {
|
|
INITIALIZATION_FAILED = 'STORAGE_INIT_FAILED',
|
|
QUERY_FAILED = 'STORAGE_QUERY_FAILED',
|
|
MIGRATION_FAILED = 'STORAGE_MIGRATION_FAILED',
|
|
ENCRYPTION_FAILED = 'STORAGE_ENCRYPTION_FAILED',
|
|
DECRYPTION_FAILED = 'STORAGE_DECRYPTION_FAILED',
|
|
INVALID_DATA = 'STORAGE_INVALID_DATA',
|
|
DATABASE_CORRUPTED = 'STORAGE_DB_CORRUPTED',
|
|
INSUFFICIENT_PERMISSIONS = 'STORAGE_INSUFFICIENT_PERMISSIONS',
|
|
STORAGE_FULL = 'STORAGE_FULL',
|
|
CONCURRENT_ACCESS = 'STORAGE_CONCURRENT_ACCESS'
|
|
}
|
|
|
|
export class StorageError extends Error {
|
|
constructor(
|
|
message: string,
|
|
public code: StorageErrorCodes,
|
|
public originalError?: unknown
|
|
) {
|
|
super(message);
|
|
this.name = 'StorageError';
|
|
}
|
|
|
|
static isStorageError(error: unknown): error is StorageError {
|
|
return error instanceof StorageError;
|
|
}
|
|
|
|
static fromUnknown(error: unknown, context: string): StorageError {
|
|
if (this.isStorageError(error)) {
|
|
return error;
|
|
}
|
|
return new StorageError(
|
|
`${context}: ${error instanceof Error ? error.message : String(error)}`,
|
|
StorageErrorCodes.QUERY_FAILED,
|
|
error
|
|
);
|
|
}
|
|
}
|
|
|
|
// Error recovery strategies
|
|
export class StorageErrorRecovery {
|
|
static async handleError(error: StorageError): Promise<void> {
|
|
switch (error.code) {
|
|
case StorageErrorCodes.DATABASE_CORRUPTED:
|
|
await this.handleCorruptedDatabase();
|
|
break;
|
|
case StorageErrorCodes.STORAGE_FULL:
|
|
await this.handleStorageFull();
|
|
break;
|
|
case StorageErrorCodes.CONCURRENT_ACCESS:
|
|
await this.handleConcurrentAccess();
|
|
break;
|
|
default:
|
|
throw error; // Re-throw unhandled errors
|
|
}
|
|
}
|
|
|
|
private static async handleCorruptedDatabase(): Promise<void> {
|
|
// 1. Attempt to repair
|
|
try {
|
|
await this.repairDatabase();
|
|
} catch {
|
|
// 2. If repair fails, restore from backup
|
|
await this.restoreFromBackup();
|
|
}
|
|
}
|
|
|
|
private static async handleStorageFull(): Promise<void> {
|
|
// 1. Clean up temporary files
|
|
await this.cleanupTempFiles();
|
|
|
|
// 2. If still full, notify user
|
|
const isStillFull = await this.checkStorageFull();
|
|
if (isStillFull) {
|
|
throw new StorageError(
|
|
'Storage is full. Please free up space.',
|
|
StorageErrorCodes.STORAGE_FULL
|
|
);
|
|
}
|
|
}
|
|
|
|
private static async handleConcurrentAccess(): Promise<void> {
|
|
// Implement retry logic with exponential backoff
|
|
await this.retryWithBackoff(async () => {
|
|
// Attempt operation again
|
|
});
|
|
}
|
|
}
|
|
```
|
|
|
|
### 6. Testing Strategy
|
|
|
|
```typescript
|
|
// src/services/storage/__tests__/StorageService.test.ts
|
|
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
import { StorageService } from '../StorageService';
|
|
import { StorageError, StorageErrorCodes } from '../errors/StorageError';
|
|
import { PlatformDetection } from '../PlatformDetection';
|
|
|
|
describe('StorageService', () => {
|
|
let storageService: StorageService;
|
|
|
|
beforeEach(async () => {
|
|
storageService = StorageService.getInstance();
|
|
await storageService.initialize();
|
|
});
|
|
|
|
afterEach(async () => {
|
|
// Clean up test data
|
|
await cleanupTestData();
|
|
});
|
|
|
|
describe('Account Operations', () => {
|
|
it('should add and retrieve an account', async () => {
|
|
const account = {
|
|
did: 'did:test:123',
|
|
publicKeyHex: '0x123...',
|
|
// ... other properties
|
|
};
|
|
|
|
await storageService.addAccount(account);
|
|
const retrieved = await storageService.getAccountByDid(account.did);
|
|
|
|
expect(retrieved).toBeDefined();
|
|
expect(retrieved?.did).toBe(account.did);
|
|
});
|
|
|
|
it('should handle duplicate accounts', async () => {
|
|
const account = {
|
|
did: 'did:test:123',
|
|
publicKeyHex: '0x123...',
|
|
};
|
|
|
|
await storageService.addAccount(account);
|
|
|
|
await expect(
|
|
storageService.addAccount(account)
|
|
).rejects.toThrow(StorageError);
|
|
});
|
|
});
|
|
|
|
describe('Error Handling', () => {
|
|
it('should handle database corruption', async () => {
|
|
// Simulate database corruption
|
|
await simulateDatabaseCorruption();
|
|
|
|
await expect(
|
|
storageService.getAccountByDid('did:test:123')
|
|
).rejects.toThrow(StorageError);
|
|
|
|
// Verify recovery
|
|
const recovered = await storageService.getAccountByDid('did:test:123');
|
|
expect(recovered).toBeDefined();
|
|
});
|
|
|
|
it('should handle concurrent access', async () => {
|
|
const promises = Array(5).fill(null).map(() =>
|
|
storageService.addAccount({
|
|
did: `did:test:${Math.random()}`,
|
|
publicKeyHex: '0x123...',
|
|
})
|
|
);
|
|
|
|
const results = await Promise.allSettled(promises);
|
|
const errors = results.filter(r => r.status === 'rejected');
|
|
|
|
expect(errors.length).toBeLessThan(promises.length);
|
|
});
|
|
});
|
|
|
|
describe('Platform-Specific Tests', () => {
|
|
it('should use correct storage implementation', async () => {
|
|
const capabilities = await PlatformDetection.getCapabilities();
|
|
|
|
if (capabilities.hasSecureStorage) {
|
|
// Verify native storage implementation
|
|
expect(storageService.getImplementation()).toBe('native');
|
|
} else {
|
|
// Verify web storage implementation
|
|
expect(storageService.getImplementation()).toBe('web');
|
|
}
|
|
});
|
|
|
|
it('should handle platform transitions', async () => {
|
|
// Simulate platform change (e.g., web to native)
|
|
await simulatePlatformChange();
|
|
|
|
// Verify data persistence
|
|
const account = await storageService.getAccountByDid('did:test:123');
|
|
expect(account).toBeDefined();
|
|
});
|
|
});
|
|
});
|
|
|
|
// Helper functions for testing
|
|
async function cleanupTestData(): Promise<void> {
|
|
// Implementation
|
|
}
|
|
|
|
async function simulateDatabaseCorruption(): Promise<void> {
|
|
// Implementation
|
|
}
|
|
|
|
async function simulatePlatformChange(): Promise<void> {
|
|
// Implementation
|
|
}
|
|
```
|
|
|
|
#### Additional Platform-Specific Tests
|
|
|
|
```typescript
|
|
// src/services/storage/__tests__/WebSQLiteService.spec.ts
|
|
import { WebSQLiteService } from '../platforms/web/WebSQLiteService';
|
|
import { StorageError, StorageErrorCodes } from '../errors/StorageError';
|
|
|
|
describe('WebSQLiteService', () => {
|
|
let service: WebSQLiteService;
|
|
|
|
beforeEach(async () => {
|
|
service = new WebSQLiteService();
|
|
await service.initialize();
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await service.close();
|
|
});
|
|
|
|
it('should initialize successfully', async () => {
|
|
expect(service.isInitialized()).toBe(true);
|
|
});
|
|
|
|
it('should handle IndexedDB errors', async () => {
|
|
// Mock IndexedDB failure
|
|
const mockIndexedDB = jest.spyOn(window, 'indexedDB', 'get');
|
|
mockIndexedDB.mockImplementation(() => undefined);
|
|
|
|
await expect(service.initialize()).rejects.toThrow(
|
|
new StorageError(
|
|
'IndexedDB not available',
|
|
StorageErrorCodes.INITIALIZATION_FAILED
|
|
)
|
|
);
|
|
});
|
|
|
|
it('should migrate data correctly', async () => {
|
|
// Set up test data
|
|
const testAccount = createTestAccount();
|
|
await dexieDB.accounts.add(testAccount);
|
|
|
|
// Perform migration
|
|
await service.migrate();
|
|
|
|
// Verify migration
|
|
const migratedAccount = await service.getAccountByDid(testAccount.did);
|
|
expect(migratedAccount).toEqual(testAccount);
|
|
});
|
|
});
|
|
|
|
// Integration tests
|
|
describe('StorageService Integration', () => {
|
|
it('should handle concurrent access', async () => {
|
|
const service1 = StorageService.getInstance();
|
|
const service2 = StorageService.getInstance();
|
|
|
|
// Simulate concurrent access
|
|
const [result1, result2] = await Promise.all([
|
|
service1.addAccount(testAccount1),
|
|
service2.addAccount(testAccount2)
|
|
]);
|
|
|
|
// Verify both operations succeeded
|
|
expect(result1).toBeDefined();
|
|
expect(result2).toBeDefined();
|
|
});
|
|
|
|
it('should recover from errors', async () => {
|
|
const service = StorageService.getInstance();
|
|
|
|
// Simulate database corruption
|
|
await simulateDatabaseCorruption();
|
|
|
|
// Attempt recovery
|
|
await service.recover();
|
|
|
|
// Verify data integrity
|
|
const accounts = await service.getAllAccounts();
|
|
expect(accounts).toBeDefined();
|
|
});
|
|
});
|
|
```
|
|
|
|
### 7. Troubleshooting Guide
|
|
|
|
#### Detailed Recovery Procedures
|
|
|
|
1. **Database Corruption Recovery**
|
|
```typescript
|
|
async recoverFromCorruption(): Promise<void> {
|
|
// 1. Stop all database operations
|
|
await this.stopDatabaseOperations();
|
|
|
|
// 2. Create backup of corrupted database
|
|
const backup = await this.createEmergencyBackup();
|
|
|
|
// 3. Attempt repair
|
|
try {
|
|
await this.repairDatabase();
|
|
} catch (error) {
|
|
// 4. Restore from backup if repair fails
|
|
await this.restoreFromBackup(backup);
|
|
}
|
|
}
|
|
```
|
|
|
|
2. **Migration Recovery**
|
|
```typescript
|
|
async recoverFromFailedMigration(): Promise<void> {
|
|
// 1. Identify migration stage
|
|
const stage = await this.getMigrationStage();
|
|
|
|
// 2. Execute appropriate recovery
|
|
switch (stage) {
|
|
case 'backup':
|
|
await this.recoverFromBackupStage();
|
|
break;
|
|
case 'migration':
|
|
await this.recoverFromMigrationStage();
|
|
break;
|
|
case 'verification':
|
|
await this.recoverFromVerificationStage();
|
|
break;
|
|
}
|
|
}
|
|
```
|
|
|
|
3. **Performance Troubleshooting**
|
|
- Monitor database size and growth
|
|
- Check query performance with EXPLAIN
|
|
- Review indexing strategy
|
|
- Monitor memory usage
|
|
- Check for connection leaks
|
|
|
|
## Success Criteria
|
|
|
|
1. **Functionality**
|
|
- [ ] All CRUD operations work correctly
|
|
- [ ] Migration process completes successfully
|
|
- [ ] Error handling works as expected
|
|
- [ ] Platform-specific features function correctly
|
|
|
|
2. **Performance**
|
|
- [ ] Database operations complete within acceptable time
|
|
- [ ] Memory usage remains stable
|
|
- [ ] IndexedDB quota usage is monitored
|
|
- [ ] Concurrent operations work correctly
|
|
|
|
3. **Security**
|
|
- [ ] Data is properly encrypted
|
|
- [ ] Keys are securely stored
|
|
- [ ] Platform-specific security features work
|
|
- [ ] No sensitive data leaks
|
|
|
|
4. **Testing**
|
|
- [ ] All unit tests pass
|
|
- [ ] Integration tests complete successfully
|
|
- [ ] Edge cases are handled
|
|
- [ ] Error recovery works as expected
|
|
|
|
## Appendix
|
|
|
|
### A. Database Schema
|
|
|
|
```sql
|
|
-- Accounts Table
|
|
CREATE TABLE accounts (
|
|
did TEXT PRIMARY KEY,
|
|
public_key_hex TEXT NOT NULL,
|
|
created_at INTEGER NOT NULL,
|
|
updated_at INTEGER NOT NULL
|
|
);
|
|
|
|
-- Settings Table
|
|
CREATE TABLE settings (
|
|
key TEXT PRIMARY KEY,
|
|
value TEXT NOT NULL,
|
|
updated_at INTEGER NOT NULL
|
|
);
|
|
|
|
-- Contacts Table
|
|
CREATE TABLE contacts (
|
|
did TEXT PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
public_key_hex TEXT NOT NULL,
|
|
created_at INTEGER NOT NULL,
|
|
updated_at INTEGER NOT NULL
|
|
);
|
|
|
|
-- Indexes
|
|
CREATE INDEX idx_accounts_created_at ON accounts(created_at);
|
|
CREATE INDEX idx_contacts_created_at ON contacts(created_at);
|
|
CREATE INDEX idx_settings_updated_at ON settings(updated_at);
|
|
```
|
|
|
|
### B. Error Codes Reference
|
|
|
|
| Code | Description | Recovery Action |
|
|
|------|-------------|-----------------|
|
|
| `STORAGE_INIT_FAILED` | Database initialization failed | Check permissions, storage space |
|
|
| `STORAGE_QUERY_FAILED` | Database query failed | Verify query, check connection |
|
|
| `STORAGE_MIGRATION_FAILED` | Data migration failed | Use backup, manual migration |
|
|
| `STORAGE_ENCRYPTION_FAILED` | Data encryption failed | Check key management |
|
|
| `STORAGE_DECRYPTION_FAILED` | Data decryption failed | Verify encryption key |
|
|
| `STORAGE_INVALID_DATA` | Invalid data format | Validate input data |
|
|
| `STORAGE_DB_CORRUPTED` | Database corruption detected | Use backup, repair |
|
|
| `STORAGE_INSUFFICIENT_PERMISSIONS` | Missing required permissions | Request permissions |
|
|
| `STORAGE_FULL` | Storage quota exceeded | Clean up, increase quota |
|
|
| `STORAGE_CONCURRENT_ACCESS` | Concurrent access conflict | Implement retry logic |
|
|
|
|
### C. Platform Capabilities Matrix
|
|
|
|
| Feature | Web | iOS | Android | Electron |
|
|
|---------|-----|-----|---------|----------|
|
|
| SQLite | wa-sqlite | SQLCipher | SQLCipher | SQLite |
|
|
| Encryption | SQLCipher | SQLCipher | SQLCipher | SQLCipher |
|
|
| Secure Storage | IndexedDB | Keychain | Keystore | Secure Storage |
|
|
| Biometrics | No | Yes | Yes | No |
|
|
| File System | Limited | Full | Full | Full |
|
|
| Background Sync | No | Yes | Yes | Yes |
|
|
| Storage Quota | Yes | No | No | No |
|
|
| Multi-tab Support | Yes | N/A | N/A | Yes |
|
|
|
|
### D. Usage Examples
|
|
|
|
#### Before (Using Dexie.js)
|
|
|
|
```typescript
|
|
// src/services/storage/legacy/AccountService.ts
|
|
import Dexie from 'dexie';
|
|
|
|
class AccountDatabase extends Dexie {
|
|
accounts: Dexie.Table<Account, string>;
|
|
settings: Dexie.Table<Setting, string>;
|
|
contacts: Dexie.Table<Contact, string>;
|
|
|
|
constructor() {
|
|
super('TimeSafariDB');
|
|
this.version(1).stores({
|
|
accounts: 'did, publicKeyHex, createdAt, updatedAt',
|
|
settings: 'key, value, updatedAt',
|
|
contacts: 'did, name, publicKeyHex, createdAt, updatedAt'
|
|
});
|
|
}
|
|
}
|
|
|
|
export class AccountService {
|
|
private db: AccountDatabase;
|
|
|
|
constructor() {
|
|
this.db = new AccountDatabase();
|
|
}
|
|
|
|
// Account Management
|
|
async addAccount(account: Account): Promise<void> {
|
|
try {
|
|
await this.db.accounts.add(account);
|
|
} catch (error) {
|
|
if (error instanceof Dexie.ConstraintError) {
|
|
throw new Error('Account already exists');
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async getAccount(did: string): Promise<Account | undefined> {
|
|
return await this.db.accounts.get(did);
|
|
}
|
|
|
|
// Settings Management
|
|
async updateSetting(key: string, value: string): Promise<void> {
|
|
await this.db.settings.put({
|
|
key,
|
|
value,
|
|
updatedAt: Date.now()
|
|
});
|
|
}
|
|
|
|
async getSetting(key: string): Promise<string | undefined> {
|
|
const setting = await this.db.settings.get(key);
|
|
return setting?.value;
|
|
}
|
|
|
|
// Contact Management
|
|
async addContact(contact: Contact): Promise<void> {
|
|
await this.db.contacts.add(contact);
|
|
}
|
|
|
|
async getContacts(): Promise<Contact[]> {
|
|
return await this.db.contacts.toArray();
|
|
}
|
|
}
|
|
|
|
// Usage Example
|
|
const accountService = new AccountService();
|
|
|
|
// Add an account
|
|
await accountService.addAccount({
|
|
did: 'did:example:123',
|
|
publicKeyHex: '0x123...',
|
|
createdAt: Date.now(),
|
|
updatedAt: Date.now()
|
|
});
|
|
|
|
// Update settings
|
|
await accountService.updateSetting('theme', 'dark');
|
|
|
|
// Add a contact
|
|
await accountService.addContact({
|
|
did: 'did:example:456',
|
|
name: 'Alice',
|
|
publicKeyHex: '0x456...',
|
|
createdAt: Date.now(),
|
|
updatedAt: Date.now()
|
|
});
|
|
```
|
|
|
|
#### After (Using Platform Service)
|
|
|
|
```typescript
|
|
// src/services/storage/AccountService.ts
|
|
import { StorageService } from './StorageService';
|
|
import { StorageError, StorageErrorCodes } from './errors/StorageError';
|
|
import { PlatformDetection } from './PlatformDetection';
|
|
|
|
export class AccountService {
|
|
private static instance: AccountService;
|
|
private storageService: StorageService;
|
|
|
|
private constructor() {
|
|
this.storageService = StorageService.getInstance();
|
|
}
|
|
|
|
static getInstance(): AccountService {
|
|
if (!AccountService.instance) {
|
|
AccountService.instance = new AccountService();
|
|
}
|
|
return AccountService.instance;
|
|
}
|
|
|
|
async initialize(): Promise<void> {
|
|
try {
|
|
// Initialize storage with platform-specific implementation
|
|
await this.storageService.initialize();
|
|
|
|
// Check for migration if needed
|
|
if (await this.storageService.needsMigration()) {
|
|
await this.handleMigration();
|
|
}
|
|
} catch (error) {
|
|
throw StorageError.fromUnknown(error, 'Failed to initialize account service');
|
|
}
|
|
}
|
|
|
|
// Account Management with Platform-Specific Features
|
|
async addAccount(account: Account): Promise<void> {
|
|
try {
|
|
// Check platform capabilities
|
|
const capabilities = await PlatformDetection.getCapabilities();
|
|
|
|
// Add platform-specific metadata
|
|
const enhancedAccount = {
|
|
...account,
|
|
platform: capabilities.isIOS ? 'ios' :
|
|
capabilities.isAndroid ? 'android' :
|
|
capabilities.hasSecureStorage ? 'electron' : 'web',
|
|
secureStorage: capabilities.hasSecureStorage,
|
|
biometricsEnabled: capabilities.hasBiometrics
|
|
};
|
|
|
|
await this.storageService.addAccount(enhancedAccount);
|
|
|
|
// If platform supports biometrics, offer to enable it
|
|
if (capabilities.hasBiometrics) {
|
|
await this.offerBiometricSetup(account.did);
|
|
}
|
|
} catch (error) {
|
|
if (error instanceof StorageError) {
|
|
throw error;
|
|
}
|
|
throw new StorageError(
|
|
'Failed to add account',
|
|
StorageErrorCodes.QUERY_FAILED,
|
|
error
|
|
);
|
|
}
|
|
}
|
|
|
|
async getAccount(did: string): Promise<Account | undefined> {
|
|
try {
|
|
const account = await this.storageService.getAccountByDid(did);
|
|
|
|
// Verify account integrity
|
|
if (account) {
|
|
await this.verifyAccountIntegrity(account);
|
|
}
|
|
|
|
return account;
|
|
} catch (error) {
|
|
throw StorageError.fromUnknown(error, `Failed to get account ${did}`);
|
|
}
|
|
}
|
|
|
|
// Settings Management with Encryption
|
|
async updateSetting(key: string, value: string): Promise<void> {
|
|
try {
|
|
const capabilities = await PlatformDetection.getCapabilities();
|
|
|
|
// Encrypt sensitive settings if platform supports it
|
|
const processedValue = capabilities.hasSecureStorage ?
|
|
await this.encryptSetting(value) : value;
|
|
|
|
await this.storageService.updateSettings({
|
|
key,
|
|
value: processedValue,
|
|
updatedAt: Date.now()
|
|
});
|
|
} catch (error) {
|
|
throw StorageError.fromUnknown(error, `Failed to update setting ${key}`);
|
|
}
|
|
}
|
|
|
|
async getSetting(key: string): Promise<string | undefined> {
|
|
try {
|
|
const setting = await this.storageService.getAccountSettings(key);
|
|
|
|
if (setting?.value) {
|
|
const capabilities = await PlatformDetection.getCapabilities();
|
|
|
|
// Decrypt if the setting was encrypted
|
|
return capabilities.hasSecureStorage ?
|
|
await this.decryptSetting(setting.value) :
|
|
setting.value;
|
|
}
|
|
|
|
return undefined;
|
|
} catch (error) {
|
|
throw StorageError.fromUnknown(error, `Failed to get setting ${key}`);
|
|
}
|
|
}
|
|
|
|
// Contact Management with Platform Integration
|
|
async addContact(contact: Contact): Promise<void> {
|
|
try {
|
|
const capabilities = await PlatformDetection.getCapabilities();
|
|
|
|
// Add platform-specific features
|
|
const enhancedContact = {
|
|
...contact,
|
|
platform: capabilities.isIOS ? 'ios' :
|
|
capabilities.isAndroid ? 'android' :
|
|
capabilities.hasSecureStorage ? 'electron' : 'web',
|
|
syncEnabled: capabilities.hasBackgroundSync
|
|
};
|
|
|
|
await this.storageService.addContact(enhancedContact);
|
|
|
|
// If platform supports background sync, schedule contact sync
|
|
if (capabilities.hasBackgroundSync) {
|
|
await this.scheduleContactSync(contact.did);
|
|
}
|
|
} catch (error) {
|
|
throw StorageError.fromUnknown(error, 'Failed to add contact');
|
|
}
|
|
}
|
|
|
|
async getContacts(): Promise<Contact[]> {
|
|
try {
|
|
const contacts = await this.storageService.getAllContacts();
|
|
|
|
// Verify contact data integrity
|
|
await Promise.all(contacts.map(contact =>
|
|
this.verifyContactIntegrity(contact)
|
|
));
|
|
|
|
return contacts;
|
|
} catch (error) {
|
|
throw StorageError.fromUnknown(error, 'Failed to get contacts');
|
|
}
|
|
}
|
|
|
|
// Platform-Specific Helper Methods
|
|
private async offerBiometricSetup(did: string): Promise<void> {
|
|
const { BiometricAuth } = await import('@capacitor-community/biometric-auth');
|
|
const available = await BiometricAuth.isAvailable();
|
|
|
|
if (available.has) {
|
|
// Show biometric setup prompt
|
|
// Implementation depends on UI framework
|
|
}
|
|
}
|
|
|
|
private async verifyAccountIntegrity(account: Account): Promise<void> {
|
|
// Verify account data integrity
|
|
// Implementation depends on security requirements
|
|
}
|
|
|
|
private async verifyContactIntegrity(contact: Contact): Promise<void> {
|
|
// Verify contact data integrity
|
|
// Implementation depends on security requirements
|
|
}
|
|
|
|
private async encryptSetting(value: string): Promise<string> {
|
|
// Encrypt sensitive settings
|
|
// Implementation depends on encryption requirements
|
|
return value; // Placeholder
|
|
}
|
|
|
|
private async decryptSetting(value: string): Promise<string> {
|
|
// Decrypt sensitive settings
|
|
// Implementation depends on encryption requirements
|
|
return value; // Placeholder
|
|
}
|
|
|
|
private async scheduleContactSync(did: string): Promise<void> {
|
|
// Schedule background sync for contacts
|
|
// Implementation depends on platform capabilities
|
|
}
|
|
}
|
|
|
|
// Usage Example
|
|
const accountService = AccountService.getInstance();
|
|
|
|
// Initialize with platform detection
|
|
await accountService.initialize();
|
|
|
|
try {
|
|
// Add an account with platform-specific features
|
|
await accountService.addAccount({
|
|
did: 'did:example:123',
|
|
publicKeyHex: '0x123...',
|
|
createdAt: Date.now(),
|
|
updatedAt: Date.now()
|
|
});
|
|
|
|
// Update settings with encryption if available
|
|
await accountService.updateSetting('theme', 'dark');
|
|
await accountService.updateSetting('apiKey', 'sensitive-data');
|
|
|
|
// Add a contact with platform integration
|
|
await accountService.addContact({
|
|
did: 'did:example:456',
|
|
name: 'Alice',
|
|
publicKeyHex: '0x456...',
|
|
createdAt: Date.now(),
|
|
updatedAt: Date.now()
|
|
});
|
|
|
|
// Retrieve data with integrity verification
|
|
const account = await accountService.getAccount('did:example:123');
|
|
const contacts = await accountService.getContacts();
|
|
const theme = await accountService.getSetting('theme');
|
|
|
|
console.log('Account:', account);
|
|
console.log('Contacts:', contacts);
|
|
console.log('Theme:', theme);
|
|
} catch (error) {
|
|
if (error instanceof StorageError) {
|
|
console.error(`Storage error: ${error.code}`, error.message);
|
|
} else {
|
|
console.error('Unexpected error:', error);
|
|
}
|
|
}
|
|
```
|
|
|
|
Key improvements in the new implementation:
|
|
|
|
1. **Platform Awareness**:
|
|
- Automatically detects platform capabilities
|
|
- Uses platform-specific features (biometrics, secure storage)
|
|
- Handles platform transitions gracefully
|
|
|
|
2. **Enhanced Security**:
|
|
- Encrypts sensitive data when platform supports it
|
|
- Verifies data integrity
|
|
- Uses platform-specific secure storage
|
|
|
|
3. **Better Error Handling**:
|
|
- Consistent error types and codes
|
|
- Platform-specific error recovery
|
|
- Detailed error messages
|
|
|
|
4. **Migration Support**:
|
|
- Automatic migration detection
|
|
- Data integrity verification
|
|
- Backup and recovery
|
|
|
|
5. **Platform Integration**:
|
|
- Background sync for contacts
|
|
- Biometric authentication
|
|
- Secure storage for sensitive data
|
|
|
|
6. **Type Safety**:
|
|
- Strong typing throughout
|
|
- Platform capability type checking
|
|
- Error type narrowing
|
|
|
|
7. **Singleton Pattern**:
|
|
- Single instance management
|
|
- Consistent state across the app
|
|
- Resource sharing
|
|
|
|
8. **Extensibility**:
|
|
- Easy to add new platform features
|
|
- Modular design
|
|
- Clear separation of concerns |