Initial commit of massive v2 rewrite
This commit is contained in:
parent
1025f3b523
commit
dc120fe78a
55 changed files with 21733 additions and 0 deletions
306
src/utils/proof.ts
Normal file
306
src/utils/proof.ts
Normal file
|
|
@ -0,0 +1,306 @@
|
|||
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<string, unknown>;
|
||||
|
||||
// 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<string, unknown>;
|
||||
|
||||
// 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<string, ChallengeParams>();
|
||||
|
||||
// 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
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue