62 KiB
Secure Storage Implementation Guide for TimeSafari App
Overview
This document outlines the implementation of secure storage for the TimeSafari app using Capacitor solutions. Two primary storage options are provided:
-
SQLite with SQLCipher Encryption (Primary Solution):
- Utilizes
@capacitor-community/sqlite
plugin with SQLCipher for 256-bit AES encryption. - Supports web and native platforms (iOS, Android).
- Ideal for complex queries and relational data.
- Platform-specific SQLCipher implementations:
- Android: Native SQLCipher library.
- iOS: SQLCipher as a drop-in SQLite replacement.
- Web: WebAssembly-based SQLCipher.
- Utilizes
-
Capacitor Preferences API (For Small Data):
- Built-in Capacitor solution for lightweight key-value storage.
- Uses platform-specific secure storage (e.g., Keychain on iOS, EncryptedSharedPreferences on Android).
- Limited to small datasets; no query capabilities.
Architecture
Directory Structure
src/services/
├── storage/ # Storage services
│ ├── SQLiteService.ts # Core SQLite service
│ ├── EncryptionService.ts # SQLCipher encryption handling
│ ├── KeyManagementService.ts # Secure key management
│ ├── platforms/ # Platform-specific implementations
│ │ ├── WebStorageService.ts
│ │ ├── CapacitorStorageService.ts
│ │ └── ElectronStorageService.ts
│ ├── migrations/ # Database migration scripts
│ │ ├── 001_initial.ts # Initial schema setup
│ │ └── 002_encryption.ts # Encryption configuration
│ └── types/
│ └── storage.types.ts # TypeScript type definitions
└── PlatformService.ts # Existing platform service interface
Platform Service Integration
The storage implementation integrates with the existing PlatformService
interface to provide platform-specific storage operations. This allows for consistent API usage across platforms while maintaining platform-specific optimizations.
Storage Service Interface
// src/services/PlatformService.ts
import { Account } from '../db/tables/accounts';
import { Contact } from '../db/tables/contacts';
import { Settings } from '../db/tables/settings';
import { Secret } from '../db/tables/secret';
export interface PlatformService {
// ... existing platform methods ...
// Storage Operations
// Secret Database
openSecretDatabase(): Promise<void>;
getMasterSecret(): Promise<Secret | undefined>;
setMasterSecret(secret: Secret): Promise<void>;
// Accounts Database
openAccountsDatabase(): Promise<void>;
getAccountsCount(): Promise<number>;
getAllAccounts(): Promise<Account[]>;
getAccountByDid(did: string): Promise<Account | undefined>;
addAccount(account: Account): Promise<void>;
updateAccountSettings(did: string, settings: Partial<Settings>): Promise<void>;
// Settings Operations
getDefaultSettings(): Promise<Settings | undefined>;
getAccountSettings(did: string): Promise<Settings | undefined>;
updateSettings(key: string, changes: Partial<Settings>): Promise<void>;
addSettings(settings: Settings): Promise<void>;
getSettingsCount(): Promise<number>;
// Contacts Operations
getAllContacts(): Promise<Contact[]>;
addContact(contact: Contact): Promise<void>;
// Database Management
deleteDatabase(): Promise<void>;
importDatabase(data: Blob): Promise<void>;
exportDatabase(): Promise<Blob>;
// Migration Support
isFirstInstall(): Promise<boolean>;
needsMigration(): Promise<boolean>;
performMigration(): Promise<void>;
// Platform Detection
isCapacitor(): boolean;
isElectron(): boolean;
isWeb(): boolean;
getCapabilities(): {
hasFileSystem: boolean;
hasSecureStorage: boolean;
hasBiometrics: boolean;
isIOS: boolean;
isAndroid: boolean;
};
}
Platform-Specific Implementations
-
Web Platform (Dexie)
// src/services/platforms/WebPlatformService.ts export class WebPlatformService implements PlatformService { // ... existing web platform methods ... // Secret Database async openSecretDatabase(): Promise<void> { await secretDB.open(); } async getMasterSecret(): Promise<Secret | undefined> { return await secretDB.secret.get(MASTER_SECRET_KEY); } async setMasterSecret(secret: Secret): Promise<void> { await secretDB.secret.put(secret); } // Accounts Database async openAccountsDatabase(): Promise<void> { const accountsDB = await accountsDBPromise; await accountsDB.open(); } async getAccountsCount(): Promise<number> { const accountsDB = await accountsDBPromise; return await accountsDB.accounts.count(); } // ... implement other storage methods using Dexie ... }
-
Capacitor Platform (SQLite)
// src/services/platforms/CapacitorPlatformService.ts export class CapacitorPlatformService implements PlatformService { private sqliteService: SQLiteService; private keyManagement: KeyManagementService; constructor() { this.sqliteService = SQLiteService.getInstance(); this.keyManagement = KeyManagementService.getInstance(); } // Secret Database async openSecretDatabase(): Promise<void> { await this.sqliteService.initialize({ database: 'timesafari_secret.db', encrypted: true, version: 1 }); } async getMasterSecret(): Promise<Secret | undefined> { const result = await this.sqliteService.query<Secret>( 'SELECT * FROM secret WHERE id = ?', [MASTER_SECRET_KEY] ); return result.value?.[0]; } async setMasterSecret(secret: Secret): Promise<void> { await this.sqliteService.query( 'INSERT OR REPLACE INTO secret (id, secret) VALUES (?, ?)', [secret.id, secret.secret] ); } // Accounts Database async openAccountsDatabase(): Promise<void> { await this.sqliteService.initialize({ database: 'timesafari_accounts.db', encrypted: true, version: 1, key: await this.keyManagement.getEncryptionKey() }); } async getAccountsCount(): Promise<number> { const result = await this.sqliteService.query<{ count: number }>( 'SELECT COUNT(*) as count FROM accounts' ); return result.value?.[0]?.count ?? 0; } // ... implement other storage methods using SQLite ... }
Usage Example
// src/services/storage/StorageService.ts
import { PlatformServiceFactory } from '../PlatformServiceFactory';
export class StorageService {
private static instance: StorageService | null = null;
private platformService: PlatformService;
private constructor() {
this.platformService = PlatformServiceFactory.getInstance();
}
static getInstance(): StorageService {
if (!StorageService.instance) {
StorageService.instance = new StorageService();
}
return StorageService.instance;
}
async initialize(): Promise<void> {
// Initialize secret database
await this.platformService.openSecretDatabase();
// Initialize accounts database
await this.platformService.openAccountsDatabase();
// Check if migration is needed
if (await this.platformService.needsMigration()) {
await this.platformService.performMigration();
}
}
async getAccountByDid(did: string): Promise<Account | undefined> {
return await this.platformService.getAccountByDid(did);
}
async updateAccountSettings(did: string, settings: Partial<Settings>): Promise<void> {
await this.platformService.updateAccountSettings(did, settings);
}
// ... implement other storage methods delegating to platformService ...
}
Migration Strategy
The platform service interface supports a smooth migration from Dexie to SQLite:
-
Web Platform:
- Continues using Dexie implementation
- No changes to existing code
- Maintains backward compatibility
-
Capacitor Platform:
- Uses SQLite with SQLCipher
- Implements platform-specific security
- Handles migration from Dexie if needed
-
Migration Process:
// src/services/storage/migration/MigrationService.ts export class MigrationService { async migrateFromDexieToSQLite(): Promise<void> { // 1. Export data from Dexie const accounts = await this.platformService.getAllAccounts(); const settings = await this.platformService.getAllSettings(); const contacts = await this.platformService.getAllContacts(); // 2. Initialize SQLite await this.platformService.openAccountsDatabase(); // 3. Import data to SQLite for (const account of accounts) { await this.platformService.addAccount(account); } // 4. Import settings and contacts for (const setting of settings) { await this.platformService.addSettings(setting); } for (const contact of contacts) { await this.platformService.addContact(contact); } // 5. Verify migration await this.verifyMigration(); } }
Dependencies
Verified and updated dependencies for the implementation:
-
Node.js Dependencies:
npm install @capacitor-community/sqlite@6.0.0 npm install @capacitor/core@6.1.2 npm install @capacitor/preferences@6.0.2 npm install @jlongster/sql.js@1.10.3 --save-dev # For web SQLCipher
-
Android (build.gradle):
implementation 'net.zetetic:android-database-sqlcipher:4.6.1'
-
iOS (Podfile):
pod 'SQLCipher', '~> 4.6.0'
Run npx cap sync
after installing dependencies to update native projects.
Type Definitions
// src/services/storage/types/storage.types.ts
export interface StorageOptions {
encrypted?: boolean; // Whether to use encryption (default: true)
database: string; // Database name
version: number; // Migration version
key?: string; // Encryption key (optional)
}
export interface StorageResult<T> {
success: boolean; // Operation success status
error?: string; // Error message if operation failed
value?: T; // Stored/retrieved value
}
export interface StorageService {
initialize(options: StorageOptions): Promise<void>;
setItem<T>(key: string, value: T): Promise<StorageResult<T>>;
getItem<T>(key: string): Promise<StorageResult<T>>;
removeItem(key: string): Promise<StorageResult<void>>;
query<T>(sql: string, params?: any[]): Promise<StorageResult<T[]>>;
}
Implementation Details
1. SQLite Service
// src/services/storage/SQLiteService.ts
import { CapacitorSQLite, SQLiteConnection, SQLiteDBConnection } from '@capacitor-community/sqlite';
import { EncryptionService } from './EncryptionService';
export class SQLiteService implements StorageService {
private static instance: SQLiteService;
private connection: SQLiteConnection;
private db?: SQLiteDBConnection;
private encryptionService: EncryptionService;
private constructor() {
this.connection = new SQLiteConnection(CapacitorSQLite);
this.encryptionService = new EncryptionService();
}
static getInstance(): SQLiteService {
if (!SQLiteService.instance) {
SQLiteService.instance = new SQLiteService();
}
return SQLiteService.instance;
}
async initialize(options: StorageOptions): Promise<void> {
try {
this.db = await this.connection.createConnection(
options.database,
options.encrypted ?? true,
options.encrypted ? 'encryption' : 'no-encryption',
options.version,
false
);
await this.db.open();
if (options.encrypted) {
const encryptionKey = options.key ?? await this.encryptionService.getEncryptionKey();
await this.db.execute(`PRAGMA key = '${encryptionKey}'`);
}
await this.runMigrations();
} catch (error) {
throw new Error(`Database initialization failed: ${(error as Error).message}`);
}
}
async setItem<T>(key: string, value: T): Promise<StorageResult<T>> {
if (!this.db) throw new Error('Database not initialized');
try {
const encryptedValue = await this.encryptionService.encrypt(JSON.stringify(value));
await this.db.run(
'INSERT OR REPLACE INTO storage (key, value) VALUES (?, ?)',
[key, encryptedValue]
);
return { success: true, value };
} catch (error) {
return { success: false, error: (error as Error).message };
}
}
async getItem<T>(key: string): Promise<StorageResult<T>> {
if (!this.db) throw new Error('Database not initialized');
try {
const result = await this.db.query('SELECT value FROM storage WHERE key = ?', [key]);
if (!result.values?.length) {
return { success: false, error: 'Key not found' };
}
const decryptedValue = await this.encryptionService.decrypt(result.values[0].value);
return { success: true, value: JSON.parse(decryptedValue) as T };
} catch (error) {
return { success: false, error: (error as Error).message };
}
}
async removeItem(key: string): Promise<StorageResult<void>> {
if (!this.db) throw new Error('Database not initialized');
try {
await this.db.run('DELETE FROM storage WHERE key = ?', [key]);
return { success: true };
} catch (error) {
return { success: false, error: (error as Error).message };
}
}
async query<T>(sql: string, params: any[] = []): Promise<StorageResult<T[]>> {
if (!this.db) throw new Error('Database not initialized');
try {
const result = await this.db.query(sql, params);
return { success: true, value: (result.values || []) as T[] };
} catch (error) {
return { success: false, error: (error as Error).message };
}
}
private async runMigrations(): Promise<void> {
if (!this.db) throw new Error('Database not initialized');
await this.db.execute(`
CREATE TABLE IF NOT EXISTS storage (
key TEXT PRIMARY KEY,
value TEXT
)
`);
// Add additional migration scripts from migrations/ folder as needed
}
}
2. Encryption Service
// src/services/storage/EncryptionService.ts
import { SQLiteDBConnection } from '@capacitor-community/sqlite';
import { Capacitor } from '@capacitor/core';
export class EncryptionService {
private encryptionKey: string | null = null;
async getEncryptionKey(): Promise<string> {
if (!this.encryptionKey) {
// In a real implementation, use platform-specific secure storage
// This is a simplified example
this.encryptionKey = await this.generateKey();
}
return this.encryptionKey;
}
async initialize(db: SQLiteDBConnection): Promise<void> {
const key = await this.getEncryptionKey();
await db.execute(`PRAGMA key = '${key}'`);
await db.execute('PRAGMA cipher_default_kdf_iter = 64000');
await db.execute('PRAGMA cipher_page_size = 4096');
}
async encrypt(value: string): Promise<string> {
if (Capacitor.isNativePlatform()) {
// Use platform-specific encryption (e.g., iOS Keychain, Android Keystore)
return value; // Placeholder for native encryption
}
// Web Crypto API for web platform
const encoder = new TextEncoder();
const key = await this.getWebCryptoKey();
const iv = crypto.getRandomValues(new Uint8Array(12));
const encrypted = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv },
key,
encoder.encode(value)
);
return btoa(String.fromCharCode(...new Uint8Array(iv), ...new Uint8Array(encrypted)));
}
async decrypt(value: string): Promise<string> {
if (Capacitor.isNativePlatform()) {
// Use platform-specific decryption
return value; // Placeholder for native decryption
}
// Web Crypto API for web platform
const data = Uint8Array.from(atob(value), c => c.charCodeAt(0));
const iv = data.slice(0, 12);
const encrypted = data.slice(12);
const key = await this.getWebCryptoKey();
const decrypted = await crypto.subtle.decrypt(
{ name: 'AES-GCM', iv },
key,
encrypted
);
return new TextDecoder().decode(decrypted);
}
private async generateKey(): Promise<string> {
const key = crypto.getRandomValues(new Uint8Array(32));
return btoa(String.fromCharCode(...key));
}
private async getWebCryptoKey(): Promise<CryptoKey> {
const key = await this.getEncryptionKey();
return crypto.subtle.importKey(
'raw',
Uint8Array.from(atob(key), c => c.charCodeAt(0)),
{ name: 'AES-GCM' },
false,
['encrypt', 'decrypt']
);
}
}
3. Preferences Service
// src/services/storage/PreferencesService.ts
import { Preferences } from '@capacitor/preferences';
import { StorageService, StorageResult } from './types/storage.types';
export class PreferencesService implements StorageService {
async initialize(): Promise<void> {
// No initialization needed for Preferences API
}
async setItem<T>(key: string, value: T): Promise<StorageResult<T>> {
try {
await Preferences.set({ key, value: JSON.stringify(value) });
return { success: true, value };
} catch (error) {
return { success: false, error: (error as Error).message };
}
}
async getItem<T>(key: string): Promise<StorageResult<T>> {
try {
const { value } = await Preferences.get({ key });
if (!value) {
return { success: false, error: 'Key not found' };
}
return { success: true, value: JSON.parse(value) as T };
} catch (error) {
return { success: false, error: (error as Error).message };
}
}
async removeItem(key: string): Promise<StorageResult<void>> {
try {
await Preferences.remove({ key });
return { success: true };
} catch (error) {
return { success: false, error: (error as Error).message };
}
}
async query<T>(): Promise<StorageResult<T[]>> {
return { success: false, error: 'Query not supported in Preferences API' };
}
}
4. Key Management Service
// src/services/storage/KeyManagementService.ts
import { Capacitor } from '@capacitor/core';
interface SecureKeyOptions {
useBiometrics: boolean;
keySize: number;
keyAlgorithm: 'AES-GCM' | 'AES-CBC';
}
export class KeyManagementService {
private static instance: KeyManagementService;
private constructor() {}
static getInstance(): KeyManagementService {
if (!KeyManagementService.instance) {
KeyManagementService.instance = new KeyManagementService();
}
return KeyManagementService.instance;
}
async generateSecureKey(options: SecureKeyOptions): Promise<string> {
const key = await this.generateRandomKey(options.keySize);
if (Capacitor.isNativePlatform()) {
return Capacitor.getPlatform() === 'ios'
? this.encryptWithSecureEnclave(key, options)
: this.encryptWithAndroidKeystore(key, options);
}
return this.encryptWithWebCrypto(key, options);
}
private async generateRandomKey(keySize: number): Promise<string> {
const key = crypto.getRandomValues(new Uint8Array(keySize / 8));
return btoa(String.fromCharCode(...key));
}
private async encryptWithSecureEnclave(key: string, options: SecureKeyOptions): Promise<string> {
// iOS Secure Enclave implementation (placeholder)
// Use Keychain with biometric protection
return key; // Implement platform-specific code
}
private async encryptWithAndroidKeystore(key: string, options: SecureKeyOptions): Promise<string> {
// Android Keystore implementation (placeholder)
// Use EncryptedSharedPreferences with biometric protection
return key; // Implement platform-specific code
}
private async encryptWithWebCrypto(key: string, options: SecureKeyOptions): Promise<string> {
// Web Crypto API implementation
const encoder = new TextEncoder();
const cryptoKey = await crypto.subtle.generateKey(
{ name: options.keyAlgorithm, length: options.keySize },
true,
['encrypt', 'decrypt']
);
const exportedKey = await crypto.subtle.exportKey('raw', cryptoKey);
return btoa(String.fromCharCode(...new Uint8Array(exportedKey)));
}
}
5. Biometric Service
// src/services/storage/BiometricService.ts
import { Capacitor } from '@capacitor/core';
export class BiometricService {
async authenticate(): Promise<boolean> {
if (!Capacitor.isNativePlatform()) {
return true; // Web fallback (no biometric auth)
}
try {
// Use Capacitor Biometric plugin (e.g., @capacitor-community/biometric-auth)
// Placeholder for actual implementation
return true;
} catch (error) {
console.error('Biometric authentication failed:', error);
return false;
}
}
}
Migration Strategy
Two-Day Implementation Timeline (Revised)
Day 1: Core Implementation and Basic Security
Morning (4 hours):
-
Essential Setup (1 hour)
# Install core dependencies npm install @capacitor-community/sqlite@6.0.0 @capacitor/core@6.1.2 npx cap sync
-
Core Services Implementation (3 hours)
- Implement basic
SQLiteService
with encryption support - Create simplified
KeyManagementService
for platform-specific key storage - Set up initial database schema
// Priority 1: Core tables export const initialSchema = ` CREATE TABLE IF NOT EXISTS accounts ( id INTEGER PRIMARY KEY AUTOINCREMENT, did TEXT UNIQUE, publicKeyHex TEXT, identity TEXT ); CREATE TABLE IF NOT EXISTS secret ( id INTEGER PRIMARY KEY, secret TEXT ); `;
- Implement basic
Afternoon (4 hours):
-
Platform-Specific Security (2 hours)
- Implement basic platform detection
- Set up platform-specific key storage:
- iOS: Basic Keychain integration
- Android: Basic Keystore integration
- Web: Web Crypto API with IndexedDB
- Defer advanced features (Secure Enclave, StrongBox) to post-migration
-
Migration Utilities (2 hours)
- Create basic data export from Dexie
- Implement simple data import to SQLite
- Add basic validation
// Simplified migration utility export class MigrationUtils { async migrateData(): Promise<void> { // 1. Export from Dexie const data = await this.exportFromDexie(); // 2. Initialize SQLite await this.initializeSQLite(); // 3. Import data await this.importToSQLite(data); // 4. Basic validation await this.validateMigration(); } }
Day 2: Migration and Testing
Morning (4 hours):
-
Migration Implementation (2 hours)
- Implement migration script
- Add basic error handling
- Create rollback capability
export class DatabaseMigration { async migrate(): Promise<void> { try { // 1. Backup existing data await this.backupExistingData(); // 2. Perform migration await this.migrationUtils.migrateData(); // 3. Verify migration const isValid = await this.verifyMigration(); if (!isValid) { await this.rollback(); throw new Error('Migration validation failed'); } } catch (error) { await this.rollback(); throw error; } } }
-
Basic Security Integration (2 hours)
- Implement basic biometric check
- Add simple key derivation
- Set up basic encryption
export class SecurityService { async initialize(): Promise<void> { // Basic security setup await this.setupEncryption(); await this.setupBiometrics(); } }
Afternoon (4 hours):
-
Testing and Validation (2 hours)
- Unit tests for core functionality
- Basic integration tests
- Platform-specific tests
describe('Database Migration', () => { it('should migrate accounts successfully', async () => { // Basic migration test }); it('should handle encryption correctly', async () => { // Basic encryption test }); });
-
Documentation and Cleanup (2 hours)
- Update documentation
- Clean up code
- Create basic backup/restore functionality
Post-Migration Features (To Be Implemented Later)
-
Enhanced Security
- Advanced platform-specific features (Secure Enclave, StrongBox)
- Sophisticated key rotation
- Advanced biometric integration
-
Advanced Backup
- Encrypted backup system
- Recovery key management
- Cross-device backup
-
Performance Optimization
- Query optimization
- Index management
- Caching strategies
Critical Path Items (Detailed Breakdown)
Build System Integration (Day 1 Morning)
-
Vite Configuration for Capacitor
// vite.config.ts import { defineConfig } from 'vite'; import { Capacitor } from '@capacitor/core'; export default defineConfig({ // ... existing config ... build: { // Ensure proper bundling for Capacitor target: 'es2015', rollupOptions: { output: { // Handle platform-specific code format: 'es', manualChunks: { 'capacitor-core': ['@capacitor/core'], 'sqlite': ['@capacitor-community/sqlite'] } } } }, plugins: [ // ... existing plugins ... { name: 'capacitor-platform', config(config, { command }) { // Add platform-specific environment variables return { define: { 'process.env.CAPACITOR_PLATFORM': JSON.stringify(Capacitor.getPlatform()) } }; } } ] });
-
Platform Detection and Conditional Imports
// src/services/storage/platform-detection.ts import { Capacitor } from '@capacitor/core'; export const isNativePlatform = Capacitor.isNativePlatform(); export const platform = Capacitor.getPlatform(); // Conditional imports for platform-specific code export const getPlatformService = async () => { if (!isNativePlatform) { return null; // Web platform uses existing implementation } switch (platform) { case 'ios': return (await import('./platforms/ios')).IOSStorageService; case 'android': return (await import('./platforms/android')).AndroidStorageService; default: throw new Error(`Unsupported platform: ${platform}`); } };
Day 1 Critical Path
-
Core Database Setup (Morning)
- SQLite plugin installation and configuration
- Basic database schema implementation
- Platform detection integration
- Vite build configuration for Capacitor
- Basic encryption setup
-
Platform-Specific Implementation (Afternoon)
- iOS Keychain integration
- Basic key storage
- Error handling
- Platform detection
- Android Keystore integration
- Basic key storage
- Error handling
- Platform detection
- Web platform detection
- Skip implementation for web
- Maintain existing Dexie implementation
- iOS Keychain integration
-
Migration Utilities (Afternoon)
- Data export from Dexie
- Account data
- Secret data
- Basic validation
- SQLite import
- Table creation
- Data import
- Basic error handling
- Data export from Dexie
Day 2 Critical Path
-
Migration Implementation (Morning)
- Migration script
// src/services/storage/migration/MigrationScript.ts export class MigrationScript { async execute(): Promise<void> { // 1. Platform check if (!isNativePlatform) { console.log('Skipping migration on web platform'); return; } // 2. Backup await this.backupExistingData(); // 3. Migration await this.performMigration(); // 4. Verification await this.verifyMigration(); } }
- Rollback mechanism
- Error handling
- Platform-specific validation
- Migration script
-
Security Integration (Morning)
- Basic biometric check
- Key derivation
- Platform-specific encryption
- Error handling for security features
-
Testing and Validation (Afternoon)
- Unit tests
// src/services/storage/__tests__/StorageService.spec.ts describe('StorageService', () => { it('should use SQLite on native platforms', async () => { if (isNativePlatform) { const service = await getPlatformService(); expect(service).toBeDefined(); // ... more tests } }); it('should skip migration on web', async () => { if (!isNativePlatform) { const migration = new MigrationScript(); await migration.execute(); // Verify web implementation unchanged } }); });
- Integration tests
- Platform-specific tests
- Migration validation
- Unit tests
-
Documentation and Cleanup (Afternoon)
- Update documentation
- Code cleanup
- Basic backup/restore
- Build system verification
Build System Integration Details
-
Vite Configuration for Capacitor
// vite.config.ts import { defineConfig } from 'vite'; import { Capacitor } from '@capacitor/core'; const capacitorConfig = { // Platform-specific entry points input: { main: 'src/main.ts', capacitor: 'src/capacitor.ts' }, // Platform-specific output output: { format: 'es', dir: 'dist', entryFileNames: (chunkInfo) => { return chunkInfo.name === 'capacitor' ? 'capacitor/[name].[hash].js' : '[name].[hash].js'; } } }; export default defineConfig({ // ... existing config ... build: { ...capacitorConfig, rollupOptions: { external: [ // External dependencies for Capacitor '@capacitor/core', '@capacitor-community/sqlite' ], output: { globals: { '@capacitor/core': 'Capacitor', '@capacitor-community/sqlite': 'CapacitorSQLite' } } } } });
-
Platform-Specific Entry Point
// src/capacitor.ts import { Capacitor } from '@capacitor/core'; import { SQLiteService } from './services/storage/SQLiteService'; // Only initialize Capacitor-specific services on native platforms if (Capacitor.isNativePlatform()) { const initializeCapacitor = async () => { const sqliteService = SQLiteService.getInstance(); await sqliteService.initialize({ database: 'timesafari.db', encrypted: true, version: 1 }); }; initializeCapacitor().catch(console.error); }
-
Build Scripts
// package.json { "scripts": { "build": "vite build", "build:capacitor": "vite build --mode capacitor", "build:ios": "vite build --mode capacitor --platform ios", "build:android": "vite build --mode capacitor --platform android", "cap:sync": "npm run build:capacitor && npx cap sync", "cap:ios": "npm run build:ios && npx cap sync ios", "cap:android": "npm run build:android && npx cap sync android" } }
-
Environment Configuration
// src/config/environment.ts import { Capacitor } from '@capacitor/core'; export const environment = { isNative: Capacitor.isNativePlatform(), platform: Capacitor.getPlatform(), storage: { type: Capacitor.isNativePlatform() ? 'sqlite' : 'dexie', // Platform-specific configuration sqlite: { database: 'timesafari.db', encrypted: true, version: 1 } } };
Success Criteria (Updated)
-
Build System
- Vite configuration properly handles Capacitor builds
- Platform-specific code is correctly bundled
- Web implementation remains unchanged
- Build scripts work for all platforms
-
Day 1
- SQLite database operational on native platforms
- Basic encryption working
- Migration utilities ready
- Platform detection working
- Build system integration complete
-
Day 2
- Successful migration on native platforms
- Basic security implemented
- Core functionality tested
- Rollback capability verified
- Web implementation unaffected
Security Considerations
- Key Management: Keys stored in iOS Keychain, Android Keystore, or Web Crypto API. Never stored in plaintext.
- Data Protection: 256-bit AES-GCM encryption via SQLCipher. Data bound to device and user authentication.
- Platform-Specific:
- iOS: Secure Enclave and Keychain with Face ID/Touch ID.
- Android: Android Keystore with BiometricPrompt.
- Web: Web Crypto API with secure storage fallback.
Performance Considerations
- Use transactions for batch operations.
- Implement proper indexing for query performance.
- Cache frequently accessed data.
- Monitor memory usage and optimize large datasets.
Future Improvements
- Key rotation support.
- Backup and restore capabilities.
- Enhanced biometric options.
- Cross-device synchronization.
Maintenance
- Regular security audits.
- Platform-specific updates.
- Dependency management with semantic versioning.
- Performance monitoring.
Platform-Specific Implementation Details
iOS Implementation
-
Secure Enclave Integration
// src/services/storage/platforms/ios/SecureEnclaveService.ts import { Capacitor } from '@capacitor/core'; import { Keychain } from '@capacitor-community/native-keychain'; export class SecureEnclaveService { private static readonly KEYCHAIN_SERVICE = 'com.timesafari.securestorage'; private static readonly KEYCHAIN_ACCESS_GROUP = 'group.com.timesafari.securestorage'; async storeKey(key: string, options: SecureKeyOptions): Promise<void> { const accessControl = { // Require device to be unlocked accessible: Keychain.Accessible.WHEN_UNLOCKED, // Use Secure Enclave accessControl: Keychain.AccessControl.BIOMETRY_ANY, // Require user presence authenticationType: Keychain.AuthenticationType.BIOMETRICS, // Keychain access group for app extension sharing accessGroup: this.KEYCHAIN_ACCESS_GROUP }; await Keychain.set({ key: 'sqlite_encryption_key', value: key, service: this.KEYCHAIN_SERVICE, ...accessControl }); } async retrieveKey(): Promise<string> { const result = await Keychain.get({ key: 'sqlite_encryption_key', service: this.KEYCHAIN_SERVICE }); if (!result.value) { throw new Error('Encryption key not found in Keychain'); } return result.value; } }
-
Face ID/Touch ID Integration
// src/services/storage/platforms/ios/BiometricService.ts import { BiometricAuth } from '@capacitor-community/biometric-auth'; export class IOSBiometricService { async authenticate(): Promise<boolean> { const available = await BiometricAuth.isAvailable(); if (!available.has) { return false; } try { const result = await BiometricAuth.verify({ reason: 'Authenticate to access secure data', title: 'TimeSafari Authentication', subtitle: 'Verify your identity', description: 'Use Face ID or Touch ID to access your secure data', negativeButtonText: 'Cancel' }); return result.verified; } catch (error) { console.error('Biometric authentication failed:', error); return false; } } }
Android Implementation
-
Android Keystore Integration
// src/services/storage/platforms/android/KeystoreService.ts import { AndroidKeystore } from '@capacitor-community/android-keystore'; export class AndroidKeystoreService { private static readonly KEY_ALIAS = 'timesafari_sqlite_key'; async storeKey(key: string, options: SecureKeyOptions): Promise<void> { const keyGenParameterSpec = { keyAlias: this.KEY_ALIAS, purposes: ['ENCRYPT', 'DECRYPT'], blockModes: ['GCM'], encryptionPaddings: ['NoPadding'], keySize: 256, userAuthenticationRequired: true, userAuthenticationValidityDurationSeconds: -1, // Use StrongBox if available isStrongBoxBacked: true }; await AndroidKeystore.generateKey(keyGenParameterSpec); await AndroidKeystore.encrypt({ keyAlias: this.KEY_ALIAS, data: key }); } async retrieveKey(): Promise<string> { const result = await AndroidKeystore.decrypt({ keyAlias: this.KEY_ALIAS }); return result.decryptedData; } }
-
Biometric Integration
// src/services/storage/platforms/android/BiometricService.ts import { BiometricAuth } from '@capacitor-community/biometric-auth'; export class AndroidBiometricService { async authenticate(): Promise<boolean> { const available = await BiometricAuth.isAvailable(); if (!available.has) { return false; } try { const result = await BiometricAuth.verify({ reason: 'Authenticate to access secure data', title: 'TimeSafari Authentication', subtitle: 'Verify your identity', description: 'Use biometric authentication to access your secure data', negativeButtonText: 'Cancel', // Android-specific options allowDeviceCredential: true }); return result.verified; } catch (error) { console.error('Biometric authentication failed:', error); return false; } } }
Web Implementation
- Web Crypto API Integration
// src/services/storage/platforms/web/WebCryptoService.ts export class WebCryptoService { private static readonly KEY_NAME = 'timesafari_sqlite_key'; async storeKey(key: string): Promise<void> { // Generate a master key for encrypting the SQLite key const masterKey = await crypto.subtle.generateKey( { name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt'] ); // Encrypt the SQLite key const iv = crypto.getRandomValues(new Uint8Array(12)); const encryptedKey = await crypto.subtle.encrypt( { name: 'AES-GCM', iv }, masterKey, new TextEncoder().encode(key) ); // Store encrypted key in IndexedDB const db = await this.getSecureDB(); await db.put('keys', { name: this.KEY_NAME, iv, data: encryptedKey }); // Export and store master key in secure storage const exportedKey = await crypto.subtle.exportKey('raw', masterKey); await this.storeMasterKey(exportedKey); } private async getSecureDB(): Promise<IDBDatabase> { // Implementation using IndexedDB with encryption } private async storeMasterKey(key: ArrayBuffer): Promise<void> { // Store in secure storage (e.g., localStorage with encryption) } }
Key Rotation and Backup Process
Key Rotation
-
Automatic Key Rotation
// src/services/storage/KeyRotationService.ts export class KeyRotationService { private static readonly ROTATION_INTERVAL = 30 * 24 * 60 * 60 * 1000; // 30 days async checkAndRotateKey(): Promise<void> { const lastRotation = await this.getLastRotationDate(); if (Date.now() - lastRotation > this.ROTATION_INTERVAL) { await this.rotateKey(); } } private async rotateKey(): Promise<void> { // 1. Generate new key const newKey = await this.generateNewKey(); // 2. Re-encrypt database with new key await this.reencryptDatabase(newKey); // 3. Store new key securely await this.storeNewKey(newKey); // 4. Update rotation timestamp await this.updateRotationDate(); } private async reencryptDatabase(newKey: string): Promise<void> { const sqliteService = SQLiteService.getInstance(); // Export all data const data = await sqliteService.exportAllData(); // Create new database with new key await sqliteService.initialize({ database: 'timesafari_new.db', encrypted: true, version: 1, key: newKey }); // Import data to new database await sqliteService.importData(data); // Verify data integrity await this.verifyDataIntegrity(); // Replace old database with new one await sqliteService.replaceDatabase('timesafari_new.db', 'timesafari.db'); } }
-
Manual Key Rotation
// src/services/storage/KeyRotationService.ts export class KeyRotationService { async manualRotateKey(): Promise<void> { // Require user authentication const biometrics = new BiometricService(); const authenticated = await biometrics.authenticate(); if (!authenticated) { throw new Error('Authentication required for key rotation'); } await this.rotateKey(); } }
Backup Process
-
Secure Backup
// src/services/storage/BackupService.ts export class BackupService { async createBackup(): Promise<BackupResult> { // 1. Export database const data = await this.exportDatabase(); // 2. Export encryption keys const keys = await this.exportKeys(); // 3. Create encrypted backup const backup = await this.encryptBackup(data, keys); // 4. Store backup securely return this.storeBackup(backup); } private async exportDatabase(): Promise<DatabaseExport> { const sqliteService = SQLiteService.getInstance(); return { version: await sqliteService.getVersion(), tables: await sqliteService.exportTables(), metadata: await sqliteService.getMetadata() }; } private async exportKeys(): Promise<KeyExport> { const keyManagement = KeyManagementService.getInstance(); return { sqliteKey: await keyManagement.exportKey(), backupKey: await this.generateBackupKey() }; } private async encryptBackup( data: DatabaseExport, keys: KeyExport ): Promise<EncryptedBackup> { // Encrypt data with backup key const encryptedData = await this.encryptData(data, keys.backupKey); // Encrypt backup key with user's recovery key const encryptedBackupKey = await this.encryptBackupKey( keys.backupKey, await this.getRecoveryKey() ); return { data: encryptedData, backupKey: encryptedBackupKey, timestamp: Date.now(), version: '1.0' }; } }
-
Restore Process
// src/services/storage/BackupService.ts export class BackupService { async restoreBackup(backup: EncryptedBackup): Promise<void> { // 1. Verify backup integrity await this.verifyBackup(backup); // 2. Decrypt backup key const backupKey = await this.decryptBackupKey( backup.backupKey, await this.getRecoveryKey() ); // 3. Decrypt data const data = await this.decryptData(backup.data, backupKey); // 4. Restore database await this.restoreDatabase(data); // 5. Verify restoration await this.verifyRestoration(); } }
Integration with Existing Security Model
-
Account Security Integration
// src/services/security/AccountSecurityService.ts export class AccountSecurityService { private storageService: StorageService; private keyManagement: KeyManagementService; async initialize(): Promise<void> { this.storageService = await StorageServiceFactory.create(); this.keyManagement = KeyManagementService.getInstance(); // Link SQLite encryption to account security await this.linkAccountSecurity(); } private async linkAccountSecurity(): Promise<void> { const account = await this.storageService.getActiveAccount(); if (account) { // Derive SQLite key from account credentials const sqliteKey = await this.deriveSQLiteKey(account); // Store derived key securely await this.keyManagement.storeKey(sqliteKey, { useBiometrics: true, keySize: 256, keyAlgorithm: 'AES-GCM' }); } } private async deriveSQLiteKey(account: Account): Promise<string> { // Use account credentials to derive SQLite key const input = `${account.did}:${account.publicKeyHex}`; return this.keyManagement.deriveKey(input); } }
-
Biometric Integration
// src/services/security/BiometricSecurityService.ts export class BiometricSecurityService { async setupBiometricProtection(): Promise<void> { const biometrics = new BiometricService(); const available = await biometrics.isAvailable(); if (available) { // Enable biometric protection for SQLite await this.keyManagement.updateKeyProtection({ useBiometrics: true, requireAuthentication: true }); // Update app settings await this.storageService.updateSettings({ biometricProtection: true, lastBiometricSetup: Date.now() }); } } }
-
Migration from Existing Security
// src/services/security/SecurityMigrationService.ts export class SecurityMigrationService { async migrateToNewSecurity(): Promise<void> { // 1. Export existing secure data const existingData = await this.exportExistingData(); // 2. Initialize new security model await this.initializeNewSecurity(); // 3. Import data with new security await this.importWithNewSecurity(existingData); // 4. Verify migration await this.verifySecurityMigration(); } private async exportExistingData(): Promise<SecureData> { // Export data from existing secure storage const accounts = await this.exportAccounts(); const secrets = await this.exportSecrets(); const settings = await this.exportSettings(); return { accounts, secrets, settings }; } private async initializeNewSecurity(): Promise<void> { // Set up new security infrastructure await this.keyManagement.initialize(); await this.storageService.initialize({ database: 'timesafari.db', encrypted: true, version: 1 }); } }
Detailed Platform Implementations
Web Platform (Dexie)
-
Database Initialization
// src/services/platforms/WebPlatformService.ts export class WebPlatformService implements PlatformService { private secretDB: Dexie; private accountsDB: Dexie | null = null; constructor() { // Initialize secret database this.secretDB = new Dexie('TimeSafariSecret'); this.secretDB.version(1).stores({ secret: 'id, secret' }); } async openAccountsDatabase(): Promise<void> { if (!this.accountsDB) { this.accountsDB = new Dexie('TimeSafariAccounts'); this.accountsDB.version(1).stores({ accounts: '++id, dateCreated, derivationPath, did, $identity, $mnemonic, publicKeyHex', settings: '++id, accountDid, *keys', contacts: '++id, did, name, *keys' }); // Apply encryption using master secret const secret = await this.getMasterSecret(); if (secret) { encrypted(this.accountsDB, { secretKey: secret.secret }); } } await this.accountsDB.open(); } async getAccountByDid(did: string): Promise<Account | undefined> { if (!this.accountsDB) { throw new Error('Accounts database not initialized'); } return await this.accountsDB.accounts .where('did') .equals(did) .first(); } // ... other methods ... }
-
Error Handling
// src/services/platforms/WebPlatformService.ts export class WebPlatformService implements PlatformService { private async handleDatabaseError(operation: string, error: unknown): Promise<never> { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; logger.error(`Database error during ${operation}:`, error); if (error instanceof DexieError) { switch (error.name) { case 'QuotaExceededError': throw new Error('Storage quota exceeded. Please clear some space and try again.'); case 'InvalidTableError': throw new Error('Database schema mismatch. Please try clearing your browser data.'); case 'ConstraintError': throw new Error('Operation would violate database constraints.'); default: throw new Error(`Database error: ${errorMessage}`); } } throw new Error(`Failed to ${operation}: ${errorMessage}`); } async addAccount(account: Account): Promise<void> { try { if (!this.accountsDB) { throw new Error('Accounts database not initialized'); } await this.accountsDB.accounts.add(account); } catch (error) { await this.handleDatabaseError('add account', error); } } }
Capacitor Platform (SQLite)
-
Database Initialization with Security
// src/services/platforms/CapacitorPlatformService.ts export class CapacitorPlatformService implements PlatformService { private sqliteService: SQLiteService; private keyManagement: KeyManagementService; private biometricService: BiometricService; constructor() { this.sqliteService = SQLiteService.getInstance(); this.keyManagement = KeyManagementService.getInstance(); this.biometricService = BiometricService.getInstance(); } async openAccountsDatabase(): Promise<void> { try { // Check biometric authentication if required if (await this.shouldRequireBiometrics()) { const authenticated = await this.biometricService.authenticate(); if (!authenticated) { throw new Error('Biometric authentication required'); } } // Get encryption key from secure storage const key = await this.keyManagement.getEncryptionKey(); // Initialize SQLite with encryption await this.sqliteService.initialize({ database: 'timesafari_accounts.db', encrypted: true, version: 1, key }); // Set up database schema await this.setupDatabaseSchema(); } catch (error) { await this.handleDatabaseError('open accounts database', error); } } private async setupDatabaseSchema(): Promise<void> { const schema = ` CREATE TABLE IF NOT EXISTS accounts ( id INTEGER PRIMARY KEY AUTOINCREMENT, dateCreated TEXT NOT NULL, derivationPath TEXT, did TEXT UNIQUE NOT NULL, identity TEXT, mnemonic TEXT, publicKeyHex TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS settings ( id INTEGER PRIMARY KEY AUTOINCREMENT, accountDid TEXT NOT NULL, key TEXT NOT NULL, value TEXT, FOREIGN KEY (accountDid) REFERENCES accounts(did) ); CREATE TABLE IF NOT EXISTS contacts ( id INTEGER PRIMARY KEY AUTOINCREMENT, did TEXT UNIQUE NOT NULL, name TEXT, keys TEXT ); `; await this.sqliteService.execute(schema); } // ... other methods ... }
-
Platform-Specific Security
// src/services/platforms/CapacitorPlatformService.ts export class CapacitorPlatformService implements PlatformService { private async shouldRequireBiometrics(): Promise<boolean> { const capabilities = this.getCapabilities(); if (!capabilities.hasBiometrics) { return false; } // Check user preferences const settings = await this.getDefaultSettings(); return settings?.requireBiometrics ?? false; } private async getPlatformKey(): Promise<string> { const capabilities = this.getCapabilities(); if (capabilities.isIOS) { // Use iOS Keychain with Secure Enclave return await this.keyManagement.getIOSKey({ useSecureEnclave: true, requireBiometrics: await this.shouldRequireBiometrics() }); } else if (capabilities.isAndroid) { // Use Android Keystore return await this.keyManagement.getAndroidKey({ useStrongBox: true, requireBiometrics: await this.shouldRequireBiometrics() }); } throw new Error('Unsupported platform for secure key storage'); } }
Usage Examples
-
Account Management
// src/services/AccountService.ts export class AccountService { constructor(private platformService: PlatformService) {} async createNewAccount(mnemonic: string): Promise<Account> { try { // Generate account details const [address, privateHex, publicHex, derivationPath] = deriveAddress(mnemonic); const account: Account = { dateCreated: new Date().toISOString(), derivationPath, did: `did:eth:${address}`, identity: JSON.stringify({ address, privateHex, publicHex }), mnemonic, publicKeyHex: publicHex }; // Save account await this.platformService.addAccount(account); // Set as active account await this.platformService.updateSettings('activeDid', { value: account.did }); return account; } catch (error) { if (error instanceof Error) { throw new Error(`Failed to create account: ${error.message}`); } throw error; } } async getActiveAccount(): Promise<Account | undefined> { const settings = await this.platformService.getDefaultSettings(); if (!settings?.activeDid) { return undefined; } return await this.platformService.getAccountByDid(settings.activeDid); } }
-
Settings Management
// src/services/SettingsService.ts export class SettingsService { constructor(private platformService: PlatformService) {} async updateAccountSettings(did: string, changes: Partial<Settings>): Promise<void> { try { // Verify account exists const account = await this.platformService.getAccountByDid(did); if (!account) { throw new Error(`Account ${did} not found`); } // Update settings await this.platformService.updateAccountSettings(did, changes); // Log changes for audit await this.logSettingsChange(did, changes); } catch (error) { await this.handleSettingsError('update account settings', error); } } private async logSettingsChange(did: string, changes: Partial<Settings>): Promise<void> { // Implementation for audit logging } }
Error Handling and Edge Cases
-
Common Error Scenarios
// src/services/storage/errors/StorageError.ts export class StorageError extends Error { constructor( message: string, public readonly code: string, public readonly originalError?: unknown ) { super(message); this.name = 'StorageError'; } } export const StorageErrorCodes = { DATABASE_NOT_INITIALIZED: 'DB_NOT_INIT', ENCRYPTION_FAILED: 'ENC_FAILED', DECRYPTION_FAILED: 'DEC_FAILED', QUOTA_EXCEEDED: 'QUOTA_EXCEEDED', BIOMETRIC_REQUIRED: 'BIOMETRIC_REQUIRED', MIGRATION_FAILED: 'MIGRATION_FAILED', INVALID_DATA: 'INVALID_DATA' } as const;
-
Error Recovery Strategies
// src/services/storage/error-recovery/ErrorRecoveryService.ts export class ErrorRecoveryService { constructor(private platformService: PlatformService) {} async handleStorageError(error: StorageError): Promise<void> { switch (error.code) { case StorageErrorCodes.DATABASE_NOT_INITIALIZED: await this.reinitializeDatabase(); break; case StorageErrorCodes.ENCRYPTION_FAILED: case StorageErrorCodes.DECRYPTION_FAILED: await this.handleEncryptionError(); break; case StorageErrorCodes.QUOTA_EXCEEDED: await this.handleQuotaExceeded(); break; case StorageErrorCodes.BIOMETRIC_REQUIRED: await this.handleBiometricRequired(); break; case StorageErrorCodes.MIGRATION_FAILED: await this.handleMigrationFailure(); break; default: throw error; // Re-throw unknown errors } } private async reinitializeDatabase(): Promise<void> { try { // Attempt to reinitialize with backup const backup = await this.platformService.exportDatabase(); await this.platformService.deleteDatabase(); await this.platformService.importDatabase(backup); } catch (error) { // If reinitialization fails, try clean initialization await this.platformService.deleteDatabase(); await this.platformService.openAccountsDatabase(); } } private async handleEncryptionError(): Promise<void> { // Attempt to recover encryption key const newKey = await this.keyManagement.regenerateKey(); await this.platformService.setMasterSecret({ id: MASTER_SECRET_KEY, secret: newKey }); } private async handleQuotaExceeded(): Promise<void> { // Implement cleanup strategy const accounts = await this.platformService.getAllAccounts(); const oldAccounts = accounts.filter(acc => new Date(acc.dateCreated).getTime() < Date.now() - 30 * 24 * 60 * 60 * 1000 ); for (const account of oldAccounts) { await this.platformService.deleteAccount(account.did); } } }
-
Edge Cases and Mitigations
a. Concurrent Access
// src/services/storage/ConcurrencyManager.ts export class ConcurrencyManager { private locks: Map<string, Promise<void>> = new Map(); async withLock<T>(key: string, operation: () => Promise<T>): Promise<T> { const existingLock = this.locks.get(key); if (existingLock) { await existingLock; } const lock = new Promise<void>((resolve) => { this.locks.set(key, lock); }); try { return await operation(); } finally { this.locks.delete(key); lock.resolve(); } } }
b. Data Integrity
// src/services/storage/DataIntegrityService.ts export class DataIntegrityService { async verifyDataIntegrity(): Promise<boolean> { const accounts = await this.platformService.getAllAccounts(); for (const account of accounts) { // Verify account data structure if (!this.isValidAccount(account)) { await this.repairAccount(account); continue; } // Verify settings exist const settings = await this.platformService.getAccountSettings(account.did); if (!settings) { await this.createDefaultSettings(account.did); } // Verify contacts const contacts = await this.platformService.getAllContacts(); const invalidContacts = contacts.filter(c => !this.isValidContact(c)); for (const contact of invalidContacts) { await this.repairContact(contact); } } return true; } private isValidAccount(account: Account): boolean { return ( account.did && account.publicKeyHex && account.dateCreated && (!account.identity || this.isValidJSON(account.identity)) ); } private async repairAccount(account: Account): Promise<void> { // Implementation for account repair } }
c. Platform Transitions
// src/services/storage/PlatformTransitionService.ts export class PlatformTransitionService { async handlePlatformTransition(): Promise<void> { const capabilities = this.platformService.getCapabilities(); if (capabilities.isIOS) { await this.handleIOSTransition(); } else if (capabilities.isAndroid) { await this.handleAndroidTransition(); } } private async handleIOSTransition(): Promise<void> { // Handle iOS-specific transitions // e.g., moving from Keychain to Secure Enclave } private async handleAndroidTransition(): Promise<void> { // Handle Android-specific transitions // e.g., upgrading to StrongBox } }