From a6edcd626957ef4b2f00935614f142fd66a6e91d Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Mon, 2 Jun 2025 03:19:09 +0000 Subject: [PATCH] feat(db): add secure secret generation and initial data setup Add proper secret generation using Node's crypto module and initial data setup for the electron environment. This commit: - Implements secure random secret generation using crypto.randomBytes() - Adds initial data migrations (002) with: - Secret table with cryptographically secure random key - Settings table with default API server - Contacts, logs, and temp tables - Improves SQL parameter handling for migrations - Adds proper transaction safety and rollback support - Includes comprehensive logging and error handling Security: - Uses Node's crypto module for secure random generation - Implements proper base64 encoding for secrets - Maintains transaction safety for all operations Testing: - Verified database structure via sqlite3 CLI - Confirmed successful migration execution - Validated initial data insertion - Checked index creation and constraints Note: This is a temporary solution for secret storage until a more secure storage mechanism is implemented. --- electron/src/rt/sqlite-migrations.ts | 110 +++++++++++++++++++++------ 1 file changed, 87 insertions(+), 23 deletions(-) diff --git a/electron/src/rt/sqlite-migrations.ts b/electron/src/rt/sqlite-migrations.ts index 75962933..2aaa581e 100644 --- a/electron/src/rt/sqlite-migrations.ts +++ b/electron/src/rt/sqlite-migrations.ts @@ -62,6 +62,30 @@ import { CapacitorSQLite } from '@capacitor-community/sqlite/electron/dist/plugin.js'; import { logger } from './logger'; +import * as crypto from 'crypto'; + +// Constants +const DEFAULT_ENDORSER_API_SERVER = 'https://api.timesafari.app'; + +// Utility function to delay execution +const delay = (ms: number): Promise => { + return new Promise(resolve => setTimeout(resolve, ms)); +}; + +// Utility function to convert Buffer to base64 +const bufferToBase64 = (buffer: Buffer): string => { + return buffer.toString('base64'); +}; + +// Generate a random secret for the secret table +// Note: This is a temporary solution until better secure storage is implemented +const generateSecret = (): string => { + const randomBytes = crypto.randomBytes(32); + return bufferToBase64(randomBytes); +}; + +// Constants for initial data +const INITIAL_SECRET = generateSecret(); // Types for migration system interface Migration { @@ -116,18 +140,10 @@ const MAX_RETRY_ATTEMPTS = 3; const RETRY_DELAY_MS = 1000; const LOCK_TIMEOUT_MS = 10000; // 10 seconds total timeout for locks -/** - * Utility function to delay execution - * @param ms Milliseconds to delay - * @returns Promise that resolves after the delay - */ -const delay = (ms: number): Promise => { - return new Promise(resolve => setTimeout(resolve, ms)); -}; - // SQL Parsing Utilities interface ParsedSQL { statements: string[]; + parameters: any[][]; errors: string[]; warnings: string[]; } @@ -276,6 +292,7 @@ const validateSQLStatement = (statement: string): string[] => { const parseSQL = (sql: string): ParsedSQL => { const result: ParsedSQL = { statements: [], + parameters: [], errors: [], warnings: [] }; @@ -290,13 +307,16 @@ const parseSQL = (sql: string): ParsedSQL => { .map(s => formatSQLStatement(s)) .filter(s => s.length > 0); - // Validate each statement + // Process each statement for (const statement of rawStatements) { const errors = validateSQLStatement(statement); if (errors.length > 0) { result.errors.push(...errors.map(e => `${e} in statement: ${statement.substring(0, 50)}...`)); } else { - result.statements.push(statement); + // Extract any parameterized values (e.g., from INSERT statements) + const { processedStatement, params } = extractParameters(statement); + result.statements.push(processedStatement); + result.parameters.push(params); } } @@ -323,6 +343,35 @@ const parseSQL = (sql: string): ParsedSQL => { return result; }; +// Helper to extract parameters from SQL statements +const extractParameters = (statement: string): { processedStatement: string; params: any[] } => { + const params: any[] = []; + let processedStatement = statement; + + // Handle INSERT statements with VALUES + if (statement.toLowerCase().includes('insert into') && statement.includes('values')) { + const valuesMatch = statement.match(/values\s*\((.*?)\)/i); + if (valuesMatch) { + const values = valuesMatch[1].split(',').map(v => v.trim()); + const placeholders = values.map((_, i) => '?').join(', '); + processedStatement = statement.replace(/values\s*\(.*?\)/i, `VALUES (${placeholders})`); + + // Extract actual values + values.forEach(v => { + if (v.startsWith("'") && v.endsWith("'")) { + params.push(v.slice(1, -1)); // Remove quotes + } else if (!isNaN(Number(v))) { + params.push(Number(v)); + } else { + params.push(v); + } + }); + } + } + + return { processedStatement, params }; +}; + // Add version conflict detection const validateMigrationVersions = (migrations: Migration[]): void => { const versions = new Set(); @@ -384,7 +433,7 @@ const MIGRATIONS: Migration[] = [ { version: 2, name: '002_secret_and_settings', - description: 'Add secret, settings, contacts, logs, and temp tables', + description: 'Add secret, settings, contacts, logs, and temp tables with initial data', sql: ` -- Secret table for storing encryption keys -- Note: This is a temporary solution until better secure storage is implemented @@ -393,6 +442,9 @@ const MIGRATIONS: Migration[] = [ secretBase64 TEXT NOT NULL ); + -- Insert initial secret + INSERT INTO secret (id, secretBase64) VALUES (1, '${INITIAL_SECRET}'); + -- Settings table for user preferences and app state CREATE TABLE IF NOT EXISTS settings ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -426,6 +478,9 @@ const MIGRATIONS: Migration[] = [ webPushServer TEXT ); + -- Insert default API server setting + INSERT INTO settings (id, apiServer) VALUES (1, '${DEFAULT_ENDORSER_API_SERVER}'); + CREATE INDEX IF NOT EXISTS idx_settings_accountDid ON settings(accountDid); -- Contacts table for user connections @@ -458,11 +513,15 @@ const MIGRATIONS: Migration[] = [ ); `, rollback: ` - DROP TABLE IF EXISTS secret; - DROP TABLE IF EXISTS settings; - DROP TABLE IF EXISTS contacts; - DROP TABLE IF EXISTS logs; + -- Drop tables in reverse order to avoid foreign key issues DROP TABLE IF EXISTS temp; + DROP TABLE IF EXISTS logs; + DROP INDEX IF EXISTS idx_contacts_name; + DROP INDEX IF EXISTS idx_contacts_did; + DROP TABLE IF EXISTS contacts; + DROP INDEX IF EXISTS idx_settings_accountDid; + DROP TABLE IF EXISTS settings; + DROP TABLE IF EXISTS secret; ` } ]; @@ -705,8 +764,8 @@ const ensureMigrationsTable = async ( } }; -// Update the parseMigrationStatements function to use the new parser -const parseMigrationStatements = (sql: string): string[] => { +// Update parseMigrationStatements to return ParsedSQL type +const parseMigrationStatements = (sql: string): ParsedSQL => { const parsed = parseSQL(sql); if (parsed.errors.length > 0) { @@ -717,7 +776,7 @@ const parseMigrationStatements = (sql: string): string[] => { logger.warn('SQL parsing warnings:', parsed.warnings); } - return parsed.statements; + return parsed; }; // Add debug helper function @@ -780,7 +839,9 @@ const executeMigration = async ( migration: Migration ): Promise => { const startTime = Date.now(); - const statements = parseMigrationStatements(migration.sql); + + // Parse SQL and extract any parameterized values + const { statements, parameters } = parseMigrationStatements(migration.sql); let transactionStarted = false; logger.info(`Starting migration ${migration.version}: ${migration.name}`, { @@ -788,7 +849,8 @@ const executeMigration = async ( version: migration.version, name: migration.name, description: migration.description, - statementCount: statements.length + statementCount: statements.length, + hasParameters: parameters.length > 0 } }); @@ -822,13 +884,15 @@ const executeMigration = async ( ); try { - // Execute each statement with retry + // Execute each statement with retry and parameters if any for (let i = 0; i < statements.length; i++) { const statement = statements[i]; + const statementParams = parameters[i] || []; + await executeWithRetry( plugin, database, - () => executeSingleStatement(plugin, database, statement), + () => executeSingleStatement(plugin, database, statement, statementParams), `executeStatement_${i + 1}` ); }