38 KiB
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:
-
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
-
Key Features:
- Encrypted storage using SQLCipher
- Platform-specific security features
- Migration support from existing implementations
- Consistent API across platforms
Quick Start
1. Installation
# 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
// 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:
-
Database Access Pattern
// 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] );
-
Update Operations
// 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);
-
Insert Operations
// 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);
-
Delete Operations
// Before (Dexie) await db.table.where("id").equals(id).delete(); // After (SQL) await platform.dbExec("DELETE FROM table WHERE id = ?", [id]);
-
Result Processing
// 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:
- Use the
generateUpdateStatement
helper for update operations - Use the
mapColumnsToValues
helper for processing query results
Example Migration:
// 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.
-
For creates & updates & deletes, the duplicate code is fine.
-
For queries where we use the results, make the setting from SQL into a 'let' variable, then wrap the Dexie code in a check for USE_DEXIE_DB from app.ts and if it's true then use that result instead of the SQL code's result.
-
-
Test thoroughly after migration
-
Consider data migration needs, and warn if there are any potential migration problems
3. Platform Detection
// 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)
// 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)
// 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
// 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
// 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
// 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
-
Database Corruption Recovery
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); } }
-
Migration Recovery
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; } }
-
Performance Troubleshooting
- Monitor database size and growth
- Check query performance with EXPLAIN
- Review indexing strategy
- Monitor memory usage
- Check for connection leaks
Success Criteria
-
Functionality
- All CRUD operations work correctly
- Migration process completes successfully
- Error handling works as expected
- Platform-specific features function correctly
-
Performance
- Database operations complete within acceptable time
- Memory usage remains stable
- IndexedDB quota usage is monitored
- Concurrent operations work correctly
-
Security
- Data is properly encrypted
- Keys are securely stored
- Platform-specific security features work
- No sensitive data leaks
-
Testing
- All unit tests pass
- Integration tests complete successfully
- Edge cases are handled
- Error recovery works as expected
Appendix
A. Database Schema
-- 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)
// 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)
// 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:
-
Platform Awareness:
- Automatically detects platform capabilities
- Uses platform-specific features (biometrics, secure storage)
- Handles platform transitions gracefully
-
Enhanced Security:
- Encrypts sensitive data when platform supports it
- Verifies data integrity
- Uses platform-specific secure storage
-
Better Error Handling:
- Consistent error types and codes
- Platform-specific error recovery
- Detailed error messages
-
Migration Support:
- Automatic migration detection
- Data integrity verification
- Backup and recovery
-
Platform Integration:
- Background sync for contacts
- Biometric authentication
- Secure storage for sensitive data
-
Type Safety:
- Strong typing throughout
- Platform capability type checking
- Error type narrowing
-
Singleton Pattern:
- Single instance management
- Consistent state across the app
- Resource sharing
-
Extensibility:
- Easy to add new platform features
- Modular design
- Clear separation of concerns