diff --git a/src/lib/security/captcha.test.ts b/src/lib/security/captcha.test.ts new file mode 100644 index 0000000..db47560 --- /dev/null +++ b/src/lib/security/captcha.test.ts @@ -0,0 +1,46 @@ +import { generateCaptcha, validateCaptcha, CaptchaResult } from './captcha'; + +describe('Enhanced Captcha System', () => { + test('should generate captcha with medium complexity', () => { + const result = generateCaptcha('medium'); + expect(result).toBeDefined(); + expect(result.question).toBeDefined(); + expect(result.answer).toBeDefined(); + expect(result.hash).toBeDefined(); + expect(result.timestamp).toBeDefined(); + expect(typeof result.question).toBe('string'); + expect(typeof result.answer).toBe('number'); + }); + + test('should generate different captcha each time', () => { + const result1 = generateCaptcha('medium'); + const result2 = generateCaptcha('medium'); + expect(result1.hash).not.toBe(result2.hash); + }); + + test('should validate correct captcha answer', () => { + const captcha = generateCaptcha('medium'); + const isValid = validateCaptcha(captcha.hash, captcha.answer, captcha.timestamp); + expect(isValid).toBe(true); + }); + + test('should reject incorrect captcha answer', () => { + const captcha = generateCaptcha('medium'); + const isValid = validateCaptcha(captcha.hash, captcha.answer + 1, captcha.timestamp); + expect(isValid).toBe(false); + }); + + test('should reject expired captcha', () => { + const captcha = generateCaptcha('medium'); + const expiredTimestamp = Date.now() - (6 * 60 * 1000); // 6 minutes ago + const isValid = validateCaptcha(captcha.hash, captcha.answer, expiredTimestamp); + expect(isValid).toBe(false); + }); + + test('should generate complex captcha', () => { + const result = generateCaptcha('complex'); + expect(result).toBeDefined(); + const numbers = result.question.match(/\d+/g); + expect(numbers?.length).toBeGreaterThan(2); + }); +}); diff --git a/src/lib/security/captcha.ts b/src/lib/security/captcha.ts new file mode 100644 index 0000000..ba103d0 --- /dev/null +++ b/src/lib/security/captcha.ts @@ -0,0 +1,121 @@ +import { getSecurityConfig } from './config'; + +export interface CaptchaResult { + question: string; + answer: number; + hash: string; + timestamp: number; +} + +function generateSimpleCaptcha(): CaptchaResult { + const num1 = Math.floor(Math.random() * 10) + 1; + const num2 = Math.floor(Math.random() * 10) + 1; + const answer = num1 + num2; + const question = `${num1} + ${num2} = ?`; + + return { + question, + answer, + hash: '', + timestamp: Date.now(), + }; +} + +function generateMediumCaptcha(): CaptchaResult { + const operations = ['+', '-', '*']; + const operation = operations[Math.floor(Math.random() * operations.length)]; + const num1 = Math.floor(Math.random() * 20) + 1; + const num2 = Math.floor(Math.random() * 10) + 1; + + let answer: number; + let question: string; + + switch (operation) { + case '+': + answer = num1 + num2; + question = `${num1} + ${num2} = ?`; + break; + case '-': + answer = num1 - num2; + question = `${num1} - ${num2} = ?`; + break; + case '*': + answer = num1 * num2; + question = `${num1} * ${num2} = ?`; + break; + default: + answer = num1 + num2; + question = `${num1} + ${num2} = ?`; + } + + return { + question, + answer, + hash: '', + timestamp: Date.now(), + }; +} + +function generateComplexCaptcha(): CaptchaResult { + const num1 = Math.floor(Math.random() * 15) + 1; + const num2 = Math.floor(Math.random() * 10) + 1; + const num3 = Math.floor(Math.random() * 5) + 1; + + const operations = ['+', '-', '*']; + const op1 = operations[Math.floor(Math.random() * operations.length)]; + const op2 = operations[Math.floor(Math.random() * operations.length)]; + + let answer: number; + let question: string; + + const temp1 = op1 === '+' ? num1 + num2 : op1 === '-' ? num1 - num2 : num1 * num2; + answer = op2 === '+' ? temp1 + num3 : op2 === '-' ? temp1 - num3 : temp1 * num3; + + question = `(${num1} ${op1} ${num2}) ${op2} ${num3} = ?`; + + return { + question, + answer, + hash: '', + timestamp: Date.now(), + }; +} + +function generateHash(answer: number, timestamp: number): string { + const secret = process.env.CAPTCHA_SECRET || 'default-secret-key'; + const data = `${answer}-${timestamp}-${secret}`; + return Buffer.from(data).toString('base64'); +} + +export function generateCaptcha(complexity: 'simple' | 'medium' | 'complex' = 'medium'): CaptchaResult { + let captcha: CaptchaResult; + + switch (complexity) { + case 'simple': + captcha = generateSimpleCaptcha(); + break; + case 'complex': + captcha = generateComplexCaptcha(); + break; + case 'medium': + default: + captcha = generateMediumCaptcha(); + break; + } + + captcha.hash = generateHash(captcha.answer, captcha.timestamp); + return captcha; +} + +export function validateCaptcha(hash: string, answer: number, timestamp: number): boolean { + const config = getSecurityConfig(); + const now = Date.now(); + const ageMinutes = (now - timestamp) / (1000 * 60); + + if (ageMinutes > config.captcha.expiryMinutes) { + return false; + } + + const expectedHash = generateHash(answer, timestamp); + return hash === expectedHash; +}