Browse Source

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.
pull/136/head
Matthew Raymer 6 days ago
parent
commit
a6edcd6269
  1. 110
      electron/src/rt/sqlite-migrations.ts

110
electron/src/rt/sqlite-migrations.ts

@ -62,6 +62,30 @@
import { CapacitorSQLite } from '@capacitor-community/sqlite/electron/dist/plugin.js'; import { CapacitorSQLite } from '@capacitor-community/sqlite/electron/dist/plugin.js';
import { logger } from './logger'; 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<void> => {
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 // Types for migration system
interface Migration { interface Migration {
@ -116,18 +140,10 @@ const MAX_RETRY_ATTEMPTS = 3;
const RETRY_DELAY_MS = 1000; const RETRY_DELAY_MS = 1000;
const LOCK_TIMEOUT_MS = 10000; // 10 seconds total timeout for locks 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<void> => {
return new Promise(resolve => setTimeout(resolve, ms));
};
// SQL Parsing Utilities // SQL Parsing Utilities
interface ParsedSQL { interface ParsedSQL {
statements: string[]; statements: string[];
parameters: any[][];
errors: string[]; errors: string[];
warnings: string[]; warnings: string[];
} }
@ -276,6 +292,7 @@ const validateSQLStatement = (statement: string): string[] => {
const parseSQL = (sql: string): ParsedSQL => { const parseSQL = (sql: string): ParsedSQL => {
const result: ParsedSQL = { const result: ParsedSQL = {
statements: [], statements: [],
parameters: [],
errors: [], errors: [],
warnings: [] warnings: []
}; };
@ -290,13 +307,16 @@ const parseSQL = (sql: string): ParsedSQL => {
.map(s => formatSQLStatement(s)) .map(s => formatSQLStatement(s))
.filter(s => s.length > 0); .filter(s => s.length > 0);
// Validate each statement // Process each statement
for (const statement of rawStatements) { for (const statement of rawStatements) {
const errors = validateSQLStatement(statement); const errors = validateSQLStatement(statement);
if (errors.length > 0) { if (errors.length > 0) {
result.errors.push(...errors.map(e => `${e} in statement: ${statement.substring(0, 50)}...`)); result.errors.push(...errors.map(e => `${e} in statement: ${statement.substring(0, 50)}...`));
} else { } 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; 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 // Add version conflict detection
const validateMigrationVersions = (migrations: Migration[]): void => { const validateMigrationVersions = (migrations: Migration[]): void => {
const versions = new Set<number>(); const versions = new Set<number>();
@ -384,7 +433,7 @@ const MIGRATIONS: Migration[] = [
{ {
version: 2, version: 2,
name: '002_secret_and_settings', 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: ` sql: `
-- Secret table for storing encryption keys -- Secret table for storing encryption keys
-- Note: This is a temporary solution until better secure storage is implemented -- Note: This is a temporary solution until better secure storage is implemented
@ -393,6 +442,9 @@ const MIGRATIONS: Migration[] = [
secretBase64 TEXT NOT NULL secretBase64 TEXT NOT NULL
); );
-- Insert initial secret
INSERT INTO secret (id, secretBase64) VALUES (1, '${INITIAL_SECRET}');
-- Settings table for user preferences and app state -- Settings table for user preferences and app state
CREATE TABLE IF NOT EXISTS settings ( CREATE TABLE IF NOT EXISTS settings (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
@ -426,6 +478,9 @@ const MIGRATIONS: Migration[] = [
webPushServer TEXT 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); CREATE INDEX IF NOT EXISTS idx_settings_accountDid ON settings(accountDid);
-- Contacts table for user connections -- Contacts table for user connections
@ -458,11 +513,15 @@ const MIGRATIONS: Migration[] = [
); );
`, `,
rollback: ` rollback: `
DROP TABLE IF EXISTS secret; -- Drop tables in reverse order to avoid foreign key issues
DROP TABLE IF EXISTS settings;
DROP TABLE IF EXISTS contacts;
DROP TABLE IF EXISTS logs;
DROP TABLE IF EXISTS temp; 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 // Update parseMigrationStatements to return ParsedSQL type
const parseMigrationStatements = (sql: string): string[] => { const parseMigrationStatements = (sql: string): ParsedSQL => {
const parsed = parseSQL(sql); const parsed = parseSQL(sql);
if (parsed.errors.length > 0) { if (parsed.errors.length > 0) {
@ -717,7 +776,7 @@ const parseMigrationStatements = (sql: string): string[] => {
logger.warn('SQL parsing warnings:', parsed.warnings); logger.warn('SQL parsing warnings:', parsed.warnings);
} }
return parsed.statements; return parsed;
}; };
// Add debug helper function // Add debug helper function
@ -780,7 +839,9 @@ const executeMigration = async (
migration: Migration migration: Migration
): Promise<MigrationResult> => { ): Promise<MigrationResult> => {
const startTime = Date.now(); 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; let transactionStarted = false;
logger.info(`Starting migration ${migration.version}: ${migration.name}`, { logger.info(`Starting migration ${migration.version}: ${migration.name}`, {
@ -788,7 +849,8 @@ const executeMigration = async (
version: migration.version, version: migration.version,
name: migration.name, name: migration.name,
description: migration.description, description: migration.description,
statementCount: statements.length statementCount: statements.length,
hasParameters: parameters.length > 0
} }
}); });
@ -822,13 +884,15 @@ const executeMigration = async (
); );
try { try {
// Execute each statement with retry // Execute each statement with retry and parameters if any
for (let i = 0; i < statements.length; i++) { for (let i = 0; i < statements.length; i++) {
const statement = statements[i]; const statement = statements[i];
const statementParams = parameters[i] || [];
await executeWithRetry( await executeWithRetry(
plugin, plugin,
database, database,
() => executeSingleStatement(plugin, database, statement), () => executeSingleStatement(plugin, database, statement, statementParams),
`executeStatement_${i + 1}` `executeStatement_${i + 1}`
); );
} }

Loading…
Cancel
Save