* Maintainer: ivan zlax <@zlax@ussr.win> */ define('ANONCAPTCHA_SECRET', 'heresecretsalt'); define('ANONCAPTCHA_LIFE', 900); function anoncaptcha_load() { register_hook('post_local_start', 'addon/anoncaptcha/anoncaptcha.php', 'anoncaptcha_post_local_start'); register_hook('page_end', 'addon/anoncaptcha/anoncaptcha.php', 'anoncaptcha_page_end'); set_config('anoncaptcha', 'enabled', 1); } function anoncaptcha_unload() { unregister_hook('post_local_start', 'addon/anoncaptcha/anoncaptcha.php', 'anoncaptcha_post_local_start'); unregister_hook('page_end', 'addon/anoncaptcha/anoncaptcha.php', 'anoncaptcha_page_end'); del_config('anoncaptcha', 'enabled'); } /** * Generate a PNG image with a number and return as base64 data URI. * Uses random background colors and noise to deter bot recognition. */ function anoncaptcha_generate_image($answer) { $width = 64; $height = 32; $img = imagecreatetruecolor($width, $height); // Random background $bgR = rand(180, 230); $bgG = rand(180, 230); $bgB = rand(180, 230); $bg = imagecolorallocate($img, $bgR, $bgG, $bgB); imagefill($img, 0, 0, $bg); // Noise lines for ($i = 0; $i < 3; $i++) { $color = imagecolorallocate($img, rand(100, 200), rand(100, 200), rand(100, 200)); imageline($img, rand(0, $width), rand(0, $height), rand(0, $width), rand(0, $height), $color); } // Noise dots for ($i = 0; $i < 10; $i++) { $color = imagecolorallocate($img, rand(100, 200), rand(100, 200), rand(100, 200)); imagesetpixel($img, rand(0, $width), rand(0, $height), $color); } // High contrast text (complementary to background) imagestring($img, 5, ($width - 40) / 2, 4, (string)$answer, imagecolorallocate($img, 255 - $bgR, 255 - $bgG, 255 - $bgB)); // Encode as base64 data URI ob_start(); imagepng($img); $data = ob_get_contents(); ob_end_clean(); imagedestroy($img); return 'data:image/png;base64,' . base64_encode($data); } /** * Generate a captcha token containing a random sum and an HMAC-signed payload. * If use_image is enabled, embeds base64 images for each operand. */ function anoncaptcha_generate_token() { $use_image = get_config('anoncaptcha', 'use_image', false); $a = rand(5, 49); $b = rand(5, 49); $answer = $a + $b; $expires = time() + ANONCAPTCHA_LIFE; $data = $answer . '|' . $expires; $hmac = hash_hmac('sha256', $data, ANONCAPTCHA_SECRET); $token = [ 'token' => base64_encode($hmac . '|' . $data), 'a' => $a, 'b' => $b, 'use_image' => $use_image, ]; if ($use_image) { $token['img_a'] = anoncaptcha_generate_image($a); $token['img_b'] = anoncaptcha_generate_image($b); } return $token; } /** * Validate the captcha answer submitted via POST. * Checks that both token and answer are present, the answer is numeric, * and that the HMAC token is valid and not expired. */ function anoncaptcha_validate() { $token = $_POST['anoncaptcha_token'] ?? ''; $submitted = trim($_POST['anoncaptcha_answer'] ?? ''); if ($token === '' || $submitted === '') { return false; } if (!is_numeric($submitted)) { return false; } $correct = anoncaptcha_validate_token($token); if ($correct === false) { return false; } return (int)$submitted === $correct; } /** * Decode and verify an HMAC captcha token. * Returns the correct answer on success, or false if the token is * malformed, expired, or has an invalid signature. */ function anoncaptcha_validate_token($token) { $decoded = @base64_decode($token); if (!$decoded || !str_contains($decoded, '|')) { return false; } $parts = explode('|', $decoded, 3); if (count($parts) !== 3) { return false; } $hmac = $parts[0]; $answer = $parts[1]; $expires = (int)$parts[2]; // Reject expired tokens if ($expires < time()) { return false; } // Verify HMAC signature (constant-time comparison) $data = $answer . '|' . $expires; $expected_hmac = hash_hmac('sha256', $data, ANONCAPTCHA_SECRET); if (!hash_equals($expected_hmac, $hmac)) { return false; } return (int)$answer; } /** * Render the captcha form as HTML. * Shows image-based operands when use_image is enabled, otherwise text. */ function anoncaptcha_render_captcha() { $gen = anoncaptcha_generate_token(); $use_image = $gen['use_image']; $question = sprintf(t('What is %d + %d?'), $gen['a'], $gen['b']); if ($use_image) { return '