captcha-canvas
Version:
A captcha generator by using skia-canvas module.
343 lines (342 loc) • 13.6 kB
JavaScript
"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
};
}