I built my own captcha

Captcha, for the uninitiated with the term, is displayed on many forms when you register for a website, and it can have impossible-to-read text. It’s fairly easy to install and common across the web. Because I’m me, I don’t want to use it. Maybe it’s “sticking it against the man” or maybe it’s naivety, or a mixture of both, I decided to build my own.

I started to get spam emails from my contact page here. They were far and few between, but my simple text was easy to hack. All you needed to do was build a page scrapper, and then you can send me an email. Though, I’m not sure why anyone would put effort into spamming some random dude who happens to have his own personalized domain. Regardless, someone or some people did.

The other thing was that I didn’t want to use any pre-existing captcha simply because everyone else uses it, and I’m sure bots are continually updated to beat them. “If I built my own,” I thought to myself, “then maybe I’ll just beat those bots.” So, I created an encrypted image with the captcha “text” that you have to enter. It’s stored as an encrypted session code. Once the user enters the text it will decode it. If there’s a match then the email will be sent. We’ll see if it works. Anyways, here’s the code in PHP:

First, in the header PHP file I have a salt (what’s a salt?) stored:

$salt = '$5$RayIsPrettyCoolIGuess';

Obviously, I won’t share the real salt, but you get the idea. The “$5$” at the beginning is setting the salt up for AES-256 encryption (see link above for more details). You can use any encryption algorithm: https://www.php.net/manual/en/function.crypt.php

Next, in the form itself I generate a random string of text:

function generateRandomString($length = 5) {
    $characters = '[email protected]#$%^&*()';
    $charactersLength = strlen($characters);
    $randomString = '';
    for ($i = 0; $i < $length; $i++) {
        $randomString .= $characters[rand(0, $charactersLength - 1)];
    }
    return $randomString;
}

$thecode = generateRandomString(5);

Obviously, I took out some characters like “O” and “0” because they’re hard to tell apart unless I use a specific font, which I didn’t feel like doing. Anyways, $thecode creates the text the user needs to enter, but they don’t see it yet. Before that, I want to store the text in a user session (it’s all about cookies) by encrypting the generated text against with the salt:

$_SESSION['code'] = crypt($thecode, $salt);

This takes $thecode we generated and $salt from the header then encrypts them to some weird text like “VGhpcyBpcyBhbiBlbmNvZGVkIHN0cmluZw==”. Indecipherable for a normal human. Between the SSL encryption on my site plus the password hash, I have no idea how anyone could decode this. But, please have fun trying, and let me know if you figure it out!

Next, we generate the image with the text. It was interesting to discover that PHP natively supports creating an image on the web. First I create the image size (100px width and 30 px height), then I set a white background with blue text. The final piece of code is writing the generated text and applying it to the image.

// Create image size
$im = imagecreate(100, 30);

// White background and blue text
$bg = imagecolorallocate($im, 255, 255, 255);
$textcolor = imagecolorallocate($im, 0, 0, 255);

// Write the string at the top left
imagestring($im, 14, rand(1,10), rand(1,10), $thecode, $textcolor);

One thing I will mention with this image is that I randomize the location of the text between 1-10 pixels vertically and horizontally. This should throw off most bots that try to find text in the same location. But, AI is kind of ruining that. Anyways, the next thing I do is create a Base64 encoded object (the image itself) and generate a JPEG, then destroy the object.

ob_start();
imagejpeg($im);
imagedestroy($im);
$data = ob_get_contents();
ob_end_clean();

Then I display the image:

echo "<p>Please enter this code: <img src='data:image/jpeg;base64,".base64_encode ($data)."'></p>";

Finally, a basic HTML form where the user can enter text:

<form method="POST" action="mail.php">
<input id="captcha" name="captcha" type="text" required/>
</form>

This calls the mail PHP file which has all the validation checks and sends the email itself. Now I validate the text entered matches the stored session:

if(hash_equals(crypt($_POST['captcha'], $salt), $_SESSION['code'])) 
{
     // Do something here...
}

I use the hash_equals function because it’s more secure. That, and comparing the encrypted text against the encrypted session doesn’t work. Then it appears like this:

You can use the contact page to use it and I haven’t gotten a single spam email.