import * as crypto from 'crypto'; import { getRealIP, type NetworkRequest } from './network.js'; import { parseDuration } from './time.js'; // Type definitions for secure proof operations export interface ChallengeData { readonly challenge: string; readonly salt: string; } export interface ChallengeParams { readonly Challenge: string; readonly Salt: string; readonly Difficulty: number; readonly ExpiresAt: number; readonly CreatedAt: number; readonly ClientIP: string; readonly PoSSeed: string; } export interface CheckpointConfig { readonly SaltLength: number; readonly Difficulty: number; readonly ChallengeExpiration: number; readonly CheckPoSTimes: boolean; readonly PoSTimeConsistencyRatio: number; } // Security constants - prevent DoS attacks while respecting user config const ABSOLUTE_MAX_SALT_LENGTH = 1024; // 1KB - prevents memory exhaustion const ABSOLUTE_MAX_DIFFICULTY = 64; // Reasonable upper bound for crypto safety const ABSOLUTE_MIN_DIFFICULTY = 1; // Must be at least 1 const ABSOLUTE_MAX_DURATION = parseDuration('365d'); // 1 year - prevents overflow const EXPECTED_POS_TIMES_LENGTH = 3; // Protocol requirement const EXPECTED_POS_HASHES_LENGTH = 3; // Protocol requirement const EXPECTED_HASH_LENGTH = 64; // SHA-256 hex length const ABSOLUTE_MAX_INPUT_LENGTH = 100000; // 100KB - prevents DoS const ABSOLUTE_MAX_REQUEST_ID_LENGTH = 64; // Reasonable hex string limit // Input validation functions - zero trust approach function validateHexString(value: unknown, paramName: string, maxLength: number): string { if (typeof value !== 'string') { throw new Error(`${paramName} must be a string`); } if (value.length === 0) { throw new Error(`${paramName} cannot be empty`); } if (value.length > maxLength) { throw new Error(`${paramName} exceeds maximum length of ${maxLength}`); } if (!/^[0-9a-fA-F]+$/.test(value)) { throw new Error(`${paramName} must be a valid hexadecimal string`); } return value.toLowerCase(); } function validatePositiveInteger(value: unknown, paramName: string, min: number, max: number): number { if (typeof value !== 'number' || !Number.isInteger(value)) { throw new Error(`${paramName} must be an integer`); } if (value < min || value > max) { throw new Error(`${paramName} must be between ${min} and ${max}`); } return value; } function validateTimesArray(value: unknown, paramName: string): number[] { if (!Array.isArray(value)) { throw new Error(`${paramName} must be an array`); } if (value.length !== EXPECTED_POS_TIMES_LENGTH) { throw new Error(`${paramName} must have exactly ${EXPECTED_POS_TIMES_LENGTH} elements`); } const validatedTimes: number[] = []; for (let i = 0; i < value.length; i++) { const time = value[i]; if (typeof time !== 'number' || !Number.isFinite(time) || time < 0) { throw new Error(`${paramName}[${i}] must be a non-negative finite number`); } if (time > 10000000) { // 10M ms = ~3 hours - generous but prevents DoS throw new Error(`${paramName}[${i}] exceeds maximum allowed value`); } validatedTimes.push(time); } return validatedTimes; } function validateHashesArray(value: unknown, paramName: string): string[] { if (!Array.isArray(value)) { throw new Error(`${paramName} must be an array`); } if (value.length !== EXPECTED_POS_HASHES_LENGTH) { throw new Error(`${paramName} must have exactly ${EXPECTED_POS_HASHES_LENGTH} elements`); } const validatedHashes: string[] = []; for (let i = 0; i < value.length; i++) { const hash = validateHexString(value[i], `${paramName}[${i}]`, EXPECTED_HASH_LENGTH); if (hash.length !== EXPECTED_HASH_LENGTH) { throw new Error(`${paramName}[${i}] must be exactly ${EXPECTED_HASH_LENGTH} characters`); } validatedHashes.push(hash); } return validatedHashes; } function validateCheckpointConfig(config: unknown): CheckpointConfig { if (!config || typeof config !== 'object') { throw new Error('CheckpointConfig must be an object'); } const cfg = config as Record; // Validate user's salt length - allow generous range but prevent memory exhaustion const saltLength = validatePositiveInteger(cfg.SaltLength, 'SaltLength', 1, ABSOLUTE_MAX_SALT_LENGTH); // Respect user's difficulty settings completely - they know their security needs const difficulty = validatePositiveInteger(cfg.Difficulty, 'Difficulty', ABSOLUTE_MIN_DIFFICULTY, ABSOLUTE_MAX_DIFFICULTY); // Respect user's expiration settings - they control their own security/usability balance const challengeExpiration = validatePositiveInteger(cfg.ChallengeExpiration, 'ChallengeExpiration', 1000, ABSOLUTE_MAX_DURATION); // Validate consistency ratio - prevent divide by zero but allow user control const consistencyRatio = typeof cfg.PoSTimeConsistencyRatio === 'number' && cfg.PoSTimeConsistencyRatio > 0 && cfg.PoSTimeConsistencyRatio <= 1000 ? cfg.PoSTimeConsistencyRatio : 2.0; return { SaltLength: saltLength, Difficulty: difficulty, ChallengeExpiration: challengeExpiration, CheckPoSTimes: typeof cfg.CheckPoSTimes === 'boolean' ? cfg.CheckPoSTimes : false, PoSTimeConsistencyRatio: consistencyRatio }; } function validateNetworkRequest(request: unknown): NetworkRequest { if (!request || typeof request !== 'object') { throw new Error('Request must be an object'); } const req = request as Record; // Validate headers object exists if (!req.headers || typeof req.headers !== 'object') { throw new Error('Request must have headers object'); } // Basic validation - ensure it has the minimal structure for a NetworkRequest return request as NetworkRequest; } function generateChallenge(checkpointConfig: unknown): ChallengeData { const validatedConfig = validateCheckpointConfig(checkpointConfig); const challenge = crypto.randomBytes(16).toString('hex'); const salt = crypto.randomBytes(validatedConfig.SaltLength).toString('hex'); return { challenge, salt }; } function calculateHash(input: unknown): string { if (typeof input !== 'string') { throw new Error('Hash input must be a string'); } if (input.length === 0) { throw new Error('Hash input cannot be empty'); } if (input.length > ABSOLUTE_MAX_INPUT_LENGTH) { // Prevent DoS via massive strings throw new Error(`Hash input exceeds maximum length of ${ABSOLUTE_MAX_INPUT_LENGTH}`); } return crypto.createHash('sha256').update(input).digest('hex'); } export function verifyPoW( challenge: unknown, salt: unknown, nonce: unknown, difficulty: unknown ): boolean { // Validate all user-provided inputs with zero trust const validatedChallenge = validateHexString(challenge, 'challenge', ABSOLUTE_MAX_INPUT_LENGTH); const validatedSalt = validateHexString(salt, 'salt', ABSOLUTE_MAX_INPUT_LENGTH); const validatedNonce = validateHexString(nonce, 'nonce', ABSOLUTE_MAX_INPUT_LENGTH); const validatedDifficulty = validatePositiveInteger(difficulty, 'difficulty', ABSOLUTE_MIN_DIFFICULTY, ABSOLUTE_MAX_DIFFICULTY); // Perform cryptographic operation with validated inputs const hash = calculateHash(validatedChallenge + validatedSalt + validatedNonce); const requiredPrefix = '0'.repeat(validatedDifficulty); return hash.startsWith(requiredPrefix); } export function checkPoSTimes(times: unknown, enableCheck: unknown, ratio: unknown): void { const validatedTimes = validateTimesArray(times, 'times'); const validatedEnableCheck = typeof enableCheck === 'boolean' ? enableCheck : false; const validatedRatio = typeof ratio === 'number' && ratio > 0 ? ratio : 2.0; if (!validatedEnableCheck) { return; // Skip check if disabled } const minTime = Math.min(...validatedTimes); const maxTime = Math.max(...validatedTimes); if (minTime === 0) { throw new Error('PoS run times cannot be zero'); } const actualRatio = maxTime / minTime; if (actualRatio > validatedRatio) { throw new Error(`PoS run times inconsistent (ratio ${actualRatio.toFixed(2)} > ${validatedRatio})`); } } // Secure in-memory storage with automatic cleanup export const challengeStore = new Map(); // Cleanup expired challenges to prevent memory exhaustion function cleanupExpiredChallenges(): void { const now = Date.now(); for (const [requestId, params] of Array.from(challengeStore.entries())) { if (params.ExpiresAt < now) { challengeStore.delete(requestId); } } } // Run cleanup every 5 minutes setInterval(cleanupExpiredChallenges, parseDuration('5m')); export function generateRequestID(request: unknown, checkpointConfig: unknown): string { const validatedConfig = validateCheckpointConfig(checkpointConfig); const validatedRequest = validateNetworkRequest(request); const { challenge, salt } = generateChallenge(validatedConfig); const posSeed = crypto.randomBytes(32).toString('hex'); const requestId = crypto.randomBytes(16).toString('hex'); const params: ChallengeParams = { Challenge: challenge, Salt: salt, Difficulty: validatedConfig.Difficulty, ExpiresAt: Date.now() + validatedConfig.ChallengeExpiration, CreatedAt: Date.now(), ClientIP: getRealIP(validatedRequest), PoSSeed: posSeed, }; challengeStore.set(requestId, params); return requestId; } export function getChallengeParams(requestId: unknown): ChallengeParams | undefined { if (typeof requestId !== 'string') { throw new Error('Request ID must be a string'); } if (requestId.length > ABSOLUTE_MAX_REQUEST_ID_LENGTH) { throw new Error(`Request ID exceeds maximum length of ${ABSOLUTE_MAX_REQUEST_ID_LENGTH}`); } if (requestId.length !== 32) { // Expected length for hex-encoded 16 bytes throw new Error('Invalid request ID format'); } if (!/^[0-9a-fA-F]+$/.test(requestId)) { throw new Error('Request ID must be hexadecimal'); } return challengeStore.get(requestId); } export function deleteChallenge(requestId: unknown): boolean { if (typeof requestId !== 'string') { throw new Error('Request ID must be a string'); } return challengeStore.delete(requestId); } export function verifyPoS( hashes: unknown, times: unknown, checkpointConfig: unknown ): void { // Validate all user inputs with zero trust const validatedHashes = validateHashesArray(hashes, 'hashes'); const validatedTimes = validateTimesArray(times, 'times'); const validatedConfig = validateCheckpointConfig(checkpointConfig); // Verify hash consistency - all must match const firstHash = validatedHashes[0]; for (let i = 1; i < validatedHashes.length; i++) { if (validatedHashes[i] !== firstHash) { throw new Error('PoS hashes do not match'); } } // Validate timing consistency checkPoSTimes(validatedTimes, validatedConfig.CheckPoSTimes, validatedConfig.PoSTimeConsistencyRatio); } // Export for testing export { calculateHash, generateChallenge };