UNPKG

captcha-canvas

Version:

A captcha generator by using skia-canvas module.

343 lines (342 loc) 13.6 kB
"use strict"; /** * Enhanced cryptographic utilities for secure CAPTCHA generation. * * This module provides cryptographically secure random number generation * and text generation functions that eliminate Math.random() fallbacks * and provide robust error handling for crypto module availability. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.CHARACTER_SETS = exports.CHARSET_UNICODE_BASIC = exports.CHARSET_ALPHANUMERIC_SYMBOLS = exports.CHARSET_ALPHANUMERIC_MIXED_CASE = exports.CHARSET_ALPHANUMERIC = exports.CHARSET_HEXADECIMAL = void 0; exports.validateCryptoAvailability = validateCryptoAvailability; exports.secureRandomInt = secureRandomInt; exports.secureRandomFloat = secureRandomFloat; exports.generateSecureText = generateSecureText; exports.calculateEntropy = calculateEntropy; exports.validateTextEntropy = validateTextEntropy; exports.getCryptographicStrength = getCryptographicStrength; const crypto_1 = require("crypto"); const errors_1 = require("../errors"); /** * Character set definitions for secure text generation. */ exports.CHARSET_HEXADECIMAL = '0123456789ABCDEF'; exports.CHARSET_ALPHANUMERIC = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'; exports.CHARSET_ALPHANUMERIC_MIXED_CASE = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; exports.CHARSET_ALPHANUMERIC_SYMBOLS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!@#$%^&*'; exports.CHARSET_UNICODE_BASIC = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!@#$%^&*()_+-=[]{}|;:,.<>?'; /** * Predefined character set configurations. */ exports.CHARACTER_SETS = { HEXADECIMAL: { charset: exports.CHARSET_HEXADECIMAL, name: 'Hexadecimal', minEntropyBits: 4 // log2(16) = 4 bits per character }, ALPHANUMERIC: { charset: exports.CHARSET_ALPHANUMERIC, name: 'Alphanumeric (Uppercase)', minEntropyBits: 5.17 // log2(36) ≈ 5.17 bits per character }, ALPHANUMERIC_MIXED_CASE: { charset: exports.CHARSET_ALPHANUMERIC_MIXED_CASE, name: 'Alphanumeric (Mixed Case)', minEntropyBits: 5.95 // log2(62) ≈ 5.95 bits per character }, ALPHANUMERIC_SYMBOLS: { charset: exports.CHARSET_ALPHANUMERIC_SYMBOLS, name: 'Alphanumeric with Symbols', minEntropyBits: 6.17 // log2(70) ≈ 6.17 bits per character }, UNICODE_BASIC: { charset: exports.CHARSET_UNICODE_BASIC, name: 'Unicode Basic', minEntropyBits: 6.58 // log2(95) ≈ 6.58 bits per character } }; /** * Validates that the crypto module is available and functional. * * @throws {CaptchaGenerationError} When crypto module is unavailable or non-functional * * @example * ```typescript * try { * validateCryptoAvailability(); * // Safe to use crypto functions * } catch (error) { * console.error('Crypto not available:', error.message); * } * ``` */ function validateCryptoAvailability() { try { // Test crypto.randomInt functionality (0, crypto_1.randomInt)(0, 2); // Test crypto.randomBytes functionality (0, crypto_1.randomBytes)(1); } catch (error) { throw new errors_1.CaptchaGenerationError('Cryptographic functions are not available. This environment does not support secure random number generation.', { param: 'crypto', expected: 'Node.js crypto module', actual: 'unavailable', suggestion: 'Ensure you are running in a Node.js environment with crypto support. Browser environments are not supported for secure CAPTCHA generation.' }); } } /** * Generates a cryptographically secure random integer within the specified range (inclusive). * * This function provides guaranteed cryptographically secure random number generation * without any Math.random() fallbacks. If crypto is unavailable, it throws an error * rather than degrading security. * * @param min - Minimum value (inclusive) * @param max - Maximum value (inclusive) * @returns Cryptographically secure random integer between min and max * @throws {CaptchaGenerationError} When crypto module is unavailable * * @example Basic range * ```typescript * const angle = secureRandomInt(-15, 15); // Random angle between -15 and 15 degrees * ``` * * @example Single parameter (0 to n) * ```typescript * const index = secureRandomInt(0, 5); // Random number from 0 to 5 * ``` */ function secureRandomInt(min = 0, max = 0) { // Validate crypto availability first validateCryptoAvailability(); const range = Math.abs(max - min); if (range === 0) return Math.min(min, max); try { // Use crypto.randomInt for cryptographically secure random numbers // randomInt is inclusive of min, exclusive of max, so we add 1 to make it inclusive return (0, crypto_1.randomInt)(0, range + 1) + Math.min(min, max); } catch (error) { throw new errors_1.CaptchaGenerationError(`Failed to generate secure random integer: ${error instanceof Error ? error.message : 'unknown error'}`, { param: 'secureRandomInt', expected: 'cryptographically secure integer', actual: 'crypto error', suggestion: 'Ensure the crypto module is properly initialized and the range is valid' }); } } /** * Generates a cryptographically secure random float between 0 and 1. * * Used for skewing transformations and other visual randomization that requires * cryptographic security. No Math.random() fallback is provided. * * @param min - Minimum value (default: 0) * @param max - Maximum value (default: 1) * @returns Cryptographically secure random float between min and max * @throws {CaptchaGenerationError} When crypto module is unavailable * * @example Basic usage * ```typescript * const skewFactor = secureRandomFloat(); // 0.0 to 1.0 * ``` * * @example Custom range * ```typescript * const opacity = secureRandomFloat(0.3, 0.8); // 0.3 to 0.8 * ``` */ function secureRandomFloat(min = 0, max = 1) { // Validate crypto availability first validateCryptoAvailability(); try { // Generate a random integer between 0 and 2^32-1, then divide by 2^32 to get 0-1 range const randomValue = (0, crypto_1.randomInt)(0, 0x100000000) / 0x100000000; // Scale to the desired range return min + (randomValue * (max - min)); } catch (error) { throw new errors_1.CaptchaGenerationError(`Failed to generate secure random float: ${error instanceof Error ? error.message : 'unknown error'}`, { param: 'secureRandomFloat', expected: 'cryptographically secure float', actual: 'crypto error', suggestion: 'Ensure the crypto module is properly initialized' }); } } /** * Generates cryptographically secure text using the specified character set. * * This function provides enhanced text generation with configurable character sets * and guaranteed cryptographic security. It replaces the limited hexadecimal-only * randomText function with flexible character set support. * * @param length - Number of characters to generate * @param config - Character set configuration (defaults to hexadecimal for backward compatibility) * @returns Cryptographically secure random text * @throws {CaptchaGenerationError} When crypto module is unavailable or parameters are invalid * * @example Hexadecimal (backward compatible) * ```typescript * const hex = generateSecureText(6); // e.g., "A3F7B2" * ``` * * @example Alphanumeric * ```typescript * const text = generateSecureText(8, CHARACTER_SETS.ALPHANUMERIC); // e.g., "K7M9P2X4" * ``` * * @example Custom character set * ```typescript * const custom = generateSecureText(6, { * charset: '0123456789', * name: 'Numeric Only' * }); // e.g., "847291" * ``` */ function generateSecureText(length, config = exports.CHARACTER_SETS.HEXADECIMAL) { if (length <= 0) return ''; // Validate crypto availability first validateCryptoAvailability(); // Validate character set if (!config.charset || config.charset.length === 0) { throw new errors_1.CaptchaGenerationError('Invalid character set: charset cannot be empty', { param: 'charset', expected: 'non-empty string', actual: config.charset, suggestion: 'Provide a valid character set string' }); } try { const charset = config.charset; const charsetLength = charset.length; let result = ''; // Generate random bytes and map to character set const randomBytesNeeded = Math.ceil(length * 1.5); // Generate extra bytes for better distribution const bytes = (0, crypto_1.randomBytes)(randomBytesNeeded); for (let i = 0, byteIndex = 0; i < length && byteIndex < bytes.length; byteIndex++) { // Use rejection sampling to ensure uniform distribution const byte = bytes[byteIndex]; const maxValidValue = Math.floor(256 / charsetLength) * charsetLength; if (byte < maxValidValue) { result += charset[byte % charsetLength]; i++; } // If byte >= maxValidValue, skip it to maintain uniform distribution } // If we didn't get enough characters due to rejection sampling, generate more while (result.length < length) { const additionalBytes = (0, crypto_1.randomBytes)(length - result.length); for (let i = 0; i < additionalBytes.length && result.length < length; i++) { const byte = additionalBytes[i]; const maxValidValue = Math.floor(256 / charsetLength) * charsetLength; if (byte < maxValidValue) { result += charset[byte % charsetLength]; } } } return result.substring(0, length); } catch (error) { throw new errors_1.CaptchaGenerationError(`Failed to generate secure text: ${error instanceof Error ? error.message : 'unknown error'}`, { param: 'generateSecureText', expected: 'cryptographically secure text', actual: 'crypto error', suggestion: 'Ensure the crypto module is properly initialized and parameters are valid' }); } } /** * Calculates the entropy (in bits) of text generated with the given character set and length. * * @param length - Length of the text * @param charsetSize - Size of the character set used * @returns Entropy in bits * * @example * ```typescript * const entropy = calculateEntropy(6, 16); // 24 bits for 6-char hex * const entropy2 = calculateEntropy(8, 62); // ~47.6 bits for 8-char mixed case alphanumeric * ``` */ function calculateEntropy(length, charsetSize) { if (length <= 0 || charsetSize <= 1) return 0; return length * Math.log2(charsetSize); } /** * Validates that the generated text meets minimum entropy requirements. * * @param text - The generated text to validate * @param config - Character set configuration used to generate the text * @param minEntropyBits - Minimum required entropy in bits (default: 32) * @returns True if entropy is sufficient * @throws {CaptchaGenerationError} When entropy is insufficient * * @example * ```typescript * const text = generateSecureText(6, CHARACTER_SETS.HEXADECIMAL); * validateTextEntropy(text, CHARACTER_SETS.HEXADECIMAL, 20); // Requires at least 20 bits * ``` */ function validateTextEntropy(text, config, minEntropyBits = 32) { const actualEntropy = calculateEntropy(text.length, config.charset.length); if (actualEntropy < minEntropyBits) { throw new errors_1.CaptchaGenerationError(`Insufficient entropy: ${actualEntropy.toFixed(2)} bits (minimum: ${minEntropyBits} bits)`, { param: 'entropy', expected: `>= ${minEntropyBits} bits`, actual: `${actualEntropy.toFixed(2)} bits`, suggestion: `Increase text length or use a larger character set. Current: ${text.length} chars from ${config.charset.length}-char set` }); } return true; } /** * Gets cryptographic strength assessment for the given configuration. * * @param length - Text length * @param config - Character set configuration * @returns Strength assessment object * * @example * ```typescript * const assessment = getCryptographicStrength(6, CHARACTER_SETS.HEXADECIMAL); * console.log(`Strength: ${assessment.level}, Entropy: ${assessment.entropyBits} bits`); * ``` */ function getCryptographicStrength(length, config) { const entropy = calculateEntropy(length, config.charset.length); let level; let timeToBreak; let recommendation; if (entropy < 20) { level = 'WEAK'; timeToBreak = 'Minutes to hours'; recommendation = 'Increase length or use larger character set'; } else if (entropy < 32) { level = 'MODERATE'; timeToBreak = 'Hours to days'; recommendation = 'Consider increasing length for better security'; } else if (entropy < 48) { level = 'STRONG'; timeToBreak = 'Years to decades'; recommendation = 'Good security for most applications'; } else { level = 'VERY_STRONG'; timeToBreak = 'Centuries or more'; recommendation = 'Excellent security'; } return { level, entropyBits: entropy, timeToBreak, recommendation }; }