/** * Validation utilities for VM operations * * Provides validation for wallet indices, derivation paths, seeds, and other * cryptographic inputs to prevent security issues and crashes. */ /** * VM validation utilities */ export class VMValidation { /** * Validate wallet/account index * * @param index - Index to validate * @param label - Label for error message * @throws Error if index is invalid */ static validateIndex(index: number, label: string = 'Index'): void { if (!Number.isInteger(index)) { throw new Error(`${label} must be an integer, got: ${index}`); } if (index < 0) { throw new Error(`${label} must be non-negative, got: ${index}`); } if (index > 0x7FFFFFFF) { throw new Error(`${label} exceeds maximum (2147483647), got: ${index}`); } } /** * Validate hex seed string * * @param seed - Seed to validate * @throws Error if seed is invalid */ static validateSeed(seed: string): void { if (typeof seed !== 'string') { throw new Error(`Seed must be a string, got: ${typeof seed}`); } if (seed.length === 0) { throw new Error('Seed cannot be empty'); } if (!/^[0-9a-fA-F]+$/.test(seed)) { throw new Error('Seed must be a hex string'); } // Typical seed length is 64 hex chars (32 bytes) if (seed.length < 32) { throw new Error(`Seed too short: ${seed.length} chars (minimum 32)`); } } /** * Validate mnemonic phrase * * @param mnemonic - Mnemonic to validate * @throws Error if mnemonic is invalid */ static validateMnemonic(mnemonic: string): void { if (typeof mnemonic !== 'string') { throw new Error(`Mnemonic must be a string, got: ${typeof mnemonic}`); } const trimmed = mnemonic.trim(); if (trimmed.length === 0) { throw new Error('Mnemonic cannot be empty'); } const words = trimmed.split(/\s+/); const validWordCounts = [12, 15, 18, 21, 24]; if (!validWordCounts.includes(words.length)) { throw new Error( `Mnemonic must have 12, 15, 18, 21, or 24 words (BIP-39 standard), got: ${words.length} words` ); } } /** * Validate BIP-44 derivation path format * * @param path - Derivation path to validate * @param vmType - VM type ('EVM' or 'SVM') * @throws Error if path is invalid */ static validateDerivationPath(path: string, vmType?: 'EVM' | 'SVM'): void { if (typeof path !== 'string') { throw new Error(`Derivation path must be a string, got: ${typeof path}`); } // Expected format: m/44'/cointype'/account'/change'/addressindex' // Some paths may have all hardened segments (with ') const pathRegex = /^m(\/\d+')+$/; if (!pathRegex.test(path)) { throw new Error( `Invalid derivation path format: ${path}. ` + `Expected format: m/44'/cointype'/account'/... (all segments hardened)` ); } // Extract parts const parts = path.split('/'); if (parts.length < 3) { throw new Error(`Derivation path too short: ${path}`); } // Check purpose (should be 44' for BIP-44) const purpose = parseInt(parts[1].replace("'", "")); if (purpose !== 44) { console.warn(`Warning: Non-BIP-44 purpose value: ${purpose}`); } // Validate coin type if VM type specified if (vmType && parts.length >= 3) { const coinType = parseInt(parts[2].replace("'", "")); if (vmType === 'EVM' && coinType !== 60) { throw new Error( `Invalid coin type for EVM: ${coinType}. Expected 60 (Ethereum).` ); } if (vmType === 'SVM' && coinType !== 501) { throw new Error( `Invalid coin type for SVM: ${coinType}. Expected 501 (Solana).` ); } } // Validate all indices are within range for (let i = 1; i < parts.length; i++) { const indexStr = parts[i].replace("'", ""); const index = parseInt(indexStr); if (isNaN(index)) { throw new Error(`Invalid index in path segment ${i}: ${parts[i]}`); } // Check hardened index range (0x80000000 to 0xFFFFFFFF) // Non-hardened range (0 to 0x7FFFFFFF) if (parts[i].endsWith("'")) { if (index > 0x7FFFFFFF) { throw new Error(`Hardened index ${index} exceeds maximum`); } } else { if (index > 0x7FFFFFFF) { throw new Error(`Non-hardened index ${index} exceeds maximum`); } } } } /** * Validate password strength * * @param password - Password to validate * @param minLength - Minimum password length (default: 8) * @throws Error if password is weak */ // static validatePassword(password: string, minLength: number = 8): void { // if (typeof password !== 'string') { // throw new Error(`Password must be a string, got: ${typeof password}`); // } // if (password.length < minLength) { // throw new Error( // `Password too short. Minimum length: ${minLength}, got: ${password.length}` // ); // } // // Check for common weak passwords // const weakPasswords = ['password', '12345678', 'qwerty', 'abc123']; // if (weakPasswords.includes(password.toLowerCase())) { // throw new Error('Password is too weak. Choose a stronger password.'); // } // } /** * Validate amount (bigint) * * @param amount - Amount to validate * @param label - Label for error message * @throws Error if amount is invalid */ static validateAmount(amount: bigint, label: string = 'Amount'): void { if (typeof amount !== 'bigint') { throw new Error(`${label} must be a bigint, got: ${typeof amount}`); } if (amount <= 0n) { throw new Error(`${label} must be positive, got: ${amount}`); } } /** * Validate Ethereum address format * * @param address - Address to validate * @param label - Label for error message * @throws Error if address is invalid */ static validateEthereumAddress(address: string, label: string = 'Address'): void { if (typeof address !== 'string') { throw new Error(`${label} must be a string, got: ${typeof address}`); } if (!/^0x[a-fA-F0-9]{40}$/.test(address)) { throw new Error( `${label} has invalid format. Expected 0x followed by 40 hex characters, got: ${address}` ); } } } /** * Sanitize error messages to remove sensitive data * * @param error - Error to sanitize * @param additionalSensitiveFields - Additional field names to redact * @returns Sanitized error */ export function sanitizeError( error: any, additionalSensitiveFields: string[] = [] ): Error { const message = error.message || error.toString(); // Remove common sensitive patterns let sanitized = message .replace(/seed:\s*[0-9a-fA-F]{32,}/gi, 'seed: [REDACTED]') .replace(/password:\s*\S+/gi, 'password: [REDACTED]') .replace(/mnemonic:\s*.+/gi, 'mnemonic: [REDACTED]') .replace(/private\s*key:\s*[0-9a-fA-F]+/gi, 'privateKey: [REDACTED]') .replace(/0x[0-9a-fA-F]{64,}/g, '[PRIVATE_KEY_REDACTED]') .replace(/secret:\s*\S+/gi, 'secret: [REDACTED]'); // Remove custom sensitive fields additionalSensitiveFields.forEach(field => { const regex = new RegExp(`${field}:\\s*\\S+`, 'gi'); sanitized = sanitized.replace(regex, `${field}: [REDACTED]`); }); const sanitizedError = new Error(sanitized); sanitizedError.stack = error.stack; // Preserve stack trace return sanitizedError; } /** * Safe error logger that redacts sensitive information * * @param message - Error message * @param error - Error object * @param context - Additional context */ export function logSafeError( message: string, error: any, context?: Record ): void { const sanitized = sanitizeError(error); // Filter sensitive data from context const safeContext = context ? { ...context } : {}; const sensitiveKeys = ['seed', 'mnemonic', 'privateKey', 'password', 'secret']; sensitiveKeys.forEach(key => { if (safeContext[key]) { safeContext[key] = '[REDACTED]'; } }); console.error(message, { error: sanitized.message, stack: sanitized.stack, context: safeContext }); }