You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

9.5 KiB

Secure Storage Implementation Guide for TimeSafari App

Overview

This document outlines the implementation of secure storage for the TimeSafari app. The implementation focuses on:

  1. Platform-Specific Storage Solutions:

    • Web: SQLite with IndexedDB backend (absurd-sql)
    • Electron: SQLite with Node.js backend
    • Native: (Planned) SQLCipher with platform-specific secure storage
  2. Key Features:

    • SQLite-based storage using absurd-sql for web
    • Platform-specific service factory pattern
    • Consistent API across platforms
    • Migration support from Dexie.js

Quick Start

1. Installation

# Core dependencies
npm install @jlongster/sql.js
npm install absurd-sql

# Platform-specific dependencies (for future native support)
npm install @capacitor/preferences
npm install @capacitor-community/biometric-auth

2. Basic Usage

// Using the platform service
import { PlatformServiceFactory } from '../services/PlatformServiceFactory';

// Get platform-specific service instance
const platformService = PlatformServiceFactory.getInstance();

// Example database operations
async function example() {
  try {
    // Query example
    const result = await platformService.dbQuery(
      "SELECT * FROM accounts WHERE did = ?",
      [did]
    );

    // Execute example
    await platformService.dbExec(
      "INSERT INTO accounts (did, public_key_hex) VALUES (?, ?)",
      [did, publicKeyHex]
    );

    } catch (error) {
    console.error('Database operation failed:', error);
  }
}

3. Platform Detection

// src/services/PlatformServiceFactory.ts
export class PlatformServiceFactory {
  static getInstance(): PlatformService {
    if (process.env.ELECTRON) {
      // Electron platform
      return new ElectronPlatformService();
    } else {
      // Web platform (default)
      return new AbsurdSqlDatabaseService();
    }
  }
}

4. Current Implementation Details

Web Platform (AbsurdSqlDatabaseService)

The web platform uses absurd-sql with IndexedDB backend:

// src/services/AbsurdSqlDatabaseService.ts
export class AbsurdSqlDatabaseService implements PlatformService {
  private static instance: AbsurdSqlDatabaseService | null = null;
  private db: AbsurdSqlDatabase | null = null;
  private initialized: boolean = false;
      
  // Singleton pattern
  static getInstance(): AbsurdSqlDatabaseService {
    if (!AbsurdSqlDatabaseService.instance) {
      AbsurdSqlDatabaseService.instance = new AbsurdSqlDatabaseService();
    }
    return AbsurdSqlDatabaseService.instance;
  }

  // Database operations
  async dbQuery(sql: string, params: unknown[] = []): Promise<QueryExecResult[]> {
    await this.waitForInitialization();
    return this.queueOperation<QueryExecResult[]>("query", sql, params);
    }

  async dbExec(sql: string, params: unknown[] = []): Promise<void> {
    await this.waitForInitialization();
    await this.queueOperation<void>("run", sql, params);
  }
}

Key features:

  • Uses absurd-sql for SQLite in the browser
  • Implements operation queuing for thread safety
  • Handles initialization and connection management
  • Provides consistent API across platforms

5. Migration from Dexie.js

The current implementation supports gradual migration from Dexie.js:

// Example of dual-storage pattern
async function getAccount(did: string): Promise<Account | undefined> {
  // Try SQLite first
  const platform = PlatformServiceFactory.getInstance();
  let account = await platform.dbQuery(
    "SELECT * FROM accounts WHERE did = ?",
    [did]
  );

  // Fallback to Dexie if needed
  if (USE_DEXIE_DB) {
    account = await db.accounts.get(did);
  }

  return account;
   }

A. Modifying Code

When converting from Dexie.js to SQL-based implementation, follow these patterns:

  1. Database Access Pattern

    // Before (Dexie)
    const result = await db.table.where("field").equals(value).first();
    
    // After (SQL)
    const platform = PlatformServiceFactory.getInstance();
    let result = await platform.dbQuery(
      "SELECT * FROM table WHERE field = ?",
      [value]
    );
    result = databaseUtil.mapQueryResultToValues(result);
    
    // Fallback to Dexie if needed
    if (USE_DEXIE_DB) {
      result = await db.table.where("field").equals(value).first();
    }
    
  2. Update Operations

    // Before (Dexie)
    await db.table.where("id").equals(id).modify(changes);
    
    // After (SQL)
    // For settings updates, use the utility methods:
    await databaseUtil.updateDefaultSettings(changes);
    // OR
    await databaseUtil.updateAccountSettings(did, changes);
    
    // For other tables, use direct SQL:
    const platform = PlatformServiceFactory.getInstance();
    await platform.dbExec(
      "UPDATE table SET field1 = ?, field2 = ? WHERE id = ?",
      [changes.field1, changes.field2, id]
    );
    
    // Fallback to Dexie if needed
    if (USE_DEXIE_DB) {
      await db.table.where("id").equals(id).modify(changes);
    }
    
  3. Insert Operations

    // Before (Dexie)
    await db.table.add(item);
    
    // After (SQL)
    const platform = PlatformServiceFactory.getInstance();
    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);
    
    // Fallback to Dexie if needed
    if (USE_DEXIE_DB) {
      await db.table.add(item);
    }
    
  4. Delete Operations

    // Before (Dexie)
    await db.table.where("id").equals(id).delete();
    
    // After (SQL)
    const platform = PlatformServiceFactory.getInstance();
    await platform.dbExec("DELETE FROM table WHERE id = ?", [id]);
    
    // Fallback to Dexie if needed
    if (USE_DEXIE_DB) {
      await db.table.where("id").equals(id).delete();
    }
    
  5. Result Processing

    // Before (Dexie)
    const items = await db.table.toArray();
    
    // After (SQL)
    const platform = PlatformServiceFactory.getInstance();
    let items = await platform.dbQuery("SELECT * FROM table");
    items = databaseUtil.mapQueryResultToValues(items);
    
    // Fallback to Dexie if needed
    if (USE_DEXIE_DB) {
      items = await db.table.toArray();
    }
    
  6. Using Utility Methods

When working with settings or other common operations, use the utility methods in db/index.ts:

// Settings operations
await databaseUtil.updateDefaultSettings(settings);
await databaseUtil.updateAccountSettings(did, settings);
const settings = await databaseUtil.retrieveSettingsForDefaultAccount();
const settings = await databaseUtil.retrieveSettingsForActiveAccount();

// Logging operations
await databaseUtil.logToDb(message);
await databaseUtil.logConsoleAndDb(message, showInConsole);

Key Considerations:

  • Always use databaseUtil.mapQueryResultToValues() to process SQL query results
  • Use utility methods from db/index.ts when available instead of direct SQL
  • Keep Dexie fallbacks wrapped in if (USE_DEXIE_DB) checks
  • For queries that return results, use let variables to allow Dexie fallback to override
  • For updates/inserts/deletes, execute both SQL and Dexie operations when USE_DEXIE_DB is true

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.

  • Consider data migration needs, and warn if there are any potential migration problems

Success Criteria

  1. Functionality

    • Basic CRUD operations work correctly
    • Platform service factory pattern implemented
    • Error handling in place
    • Native platform support (planned)
  2. Performance

    • Database operations complete within acceptable time
    • Operation queuing for thread safety
    • Proper initialization handling
    • Performance monitoring (planned)
  3. Security

    • Basic data integrity
    • Encryption (planned for native platforms)
    • Secure key storage (planned)
    • Platform-specific security features (planned)
  4. Testing

    • Basic unit tests
    • Comprehensive integration tests (planned)
    • Platform-specific tests (planned)
    • Migration tests (planned)

Next Steps

  1. Native Platform Support

    • Implement SQLCipher for iOS/Android
    • Add platform-specific secure storage
    • Implement biometric authentication
  2. Enhanced Security

    • Add encryption for sensitive data
    • Implement secure key storage
    • Add platform-specific security features
  3. Testing and Monitoring

    • Add comprehensive test coverage
    • Implement performance monitoring
    • Add error tracking and analytics
  4. Documentation

    • Add API documentation
    • Create migration guides
    • Document security measures