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.
This commit is contained in:
@@ -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<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
|
||||
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<void> => {
|
||||
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<number>();
|
||||
@@ -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<MigrationResult> => {
|
||||
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}`
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user