Better CAPTCHA Through Encryption

I recently wrote about a lightweight invisible CAPTCHA validator control I built as a defensive measure against comment spam.  I wanted the control to work in as many situations as possible, so it doesn’t rely on ViewState nor Session since some users of the control may want to turn those things off.

Of course this begs the question, how do I know the answer submitted in the form is the answer to the question I asked?  Remember, never trust your inputs, even form submissions can easily be tampered with.

Well one way is to give the client the answer in some manner that it can’t be read and can’t be tampered with.  Encryption to the rescue!

Using a few new objects from the System.Security.Cryptography namespace in .NET 2.0, I quickly put together code that would encrypt the answer along with the current system time into a base 64 encoded string.  That string would then be placed in a hidden input field.

When the form is submitted, I made sure that the encrypted value contained the answer and that the date inside was not too old, thus defeating replay attacks.

The first change was to initialize the encryption algorithm via a static constructor.

The code can be hard to read in a browser, so I did include the source code in the download link at the end of this post.

static SymmetricAlgorithm encryptionAlgorithm 
    = InitializeEncryptionAlgorithm();

static SymmetricAlgorithm InitializeEncryptionAlgorithm()
{
  SymmetricAlgorithm rijaendel = RijndaelManaged.Create();
  rijaendel.GenerateKey();
  rijaendel.GenerateIV();
  return rijaendel;
}

With that in place, I added a couple static methods to the control.

static SymmetricAlgorithm InitializeEncryptionAlgorithm()
{
  SymmetricAlgorithm rijaendel = RijndaelManaged.Create();
  rijaendel.GenerateKey();
  rijaendel.GenerateIV();
  return rijaendel;
}

public static string EncryptString(string clearText)
{
  byte[] clearTextBytes = Encoding.UTF8.GetBytes(clearText);
  byte[] encrypted = encryptionAlgorithm.CreateEncryptor()
    .TransformFinalBlock(clearTextBytes, 0
    , clearTextBytes.Length);
  return Convert.ToBase64String(encrypted);
}

In the PreRender method I simply took the answer, appended the date using a pipe character as a separator, encrypted the whole stew, and the slapped it in a hidden form field.

//Inside of OnPreRender
Page.ClientScript.RegisterHiddenField
    (this.HiddenEncryptedAnswerFieldName
    , EncryptAnswer(answer));

string EncryptAnswer(string answer)
{
  return EncryptString(answer 
    + "|" 
    + DateTime.Now.ToString("yyyy/MM/dd HH:mm"));
}

Now with all that in place, when the user submits the form, I can determine if the answer is valid by grabbing the value from the form field, calling decrypt on it, splitting it using the pipe character as a delimiter, and examining the result.

protected override bool EvaluateIsValid()
{
  string answer = GetClientSpecifiedAnswer();
    
  string encryptedAnswerFromForm = 
    Page.Request.Form[this.HiddenEncryptedAnswerFieldName];
    
  if(String.IsNullOrEmpty(encryptedAnswerFromForm))
    return false;
    
  string decryptedAnswer = DecryptString(encryptedAnswerFromForm);
    
  string[] answerParts = decryptedAnswer.Split('|');
  if(answerParts.Length < 2)
    return false;
    
  string expectedAnswer = answerParts[0];
  DateTime date = DateTime.ParseExact(answerParts[1]
    , "yyyy/MM/dd HH:mm", CultureInfo.InvariantCulture);
  if ((DateTime.Now - date).Minutes > 30)
  {
    this.ErrorMessage = "Sorry, but this form has expired. 
      Please submit again.";
    return false;
  }

  return !String.IsNullOrEmpty(answer) 
    && answer == expectedAnswer;
}

// Gets the answer from the client, whether entered by 
// javascript or by the user.
private string GetClientSpecifiedAnswer()
{
  string answer = Page.Request.Form[this.HiddenAnswerFieldName];
  if(String.IsNullOrEmpty(answer))
    answer = Page.Request.Form[this.VisibleAnswerFieldName];
  return answer;
}

This technique could work particularly well for a visible CAPTCHA control as well. The request for a CAPTCHA image is an asynchronous request and the code that renders that image has to know which CAPTCHA image to render. Implementations I’ve seen simply store an image in the CACHE using a GUID as a key when rendering the control. Thus when the asynchronous request to grab the CAPTCHA image arrives, the CAPTCHA image rendering HttpHandler looks up the image using the GUID and renders that baby out.

Using encryption, the URL for the CAPTCHA image could embed the answer (aka the word to render).

If you are interested, you can download an updated binary and source code for the Invisible CAPTCHA control which now includes the symmetric encryption from here.

What others have said

Requesting Gravatar... Marcelo Calbucci Oct 02, 2006 10:03 PM
# re: Better CAPTCHA Through Encryption

So simple, yet so powerful idea. I've been using Session state and so far have not had any problem, but I can see the value of not depending on session.

Requesting Gravatar... DotNetKicks.com Oct 03, 2006 5:03 PM
# Better CAPTCHA Through Encryption
You've been kicked (a good thing) - Trackback from DotNetKicks.com
Requesting Gravatar... Simeon Oct 03, 2006 5:58 PM
# re: Better CAPTCHA Through Encryption
As RijndaelManaged is a symmetric algorithm this will fail, when the comment spammer knows your encryption/decryption method. They can decrypt the answer and send it straight back, or they can insert there own answer into the message.

What you wan to use is PKI. Encrypt the answer with your public key A, so only you with your private key A can unlock the answer. Then to stop insertion of "new" answers, sign the answer with your private key B, so you can check that your answer is not altered, by unlocking it with public key B.
Requesting Gravatar... Haacked Oct 03, 2006 6:02 PM
# re: Better CAPTCHA Through Encryption
How can they decrypt the answer if they don't have the encryption key I am using.

Remember, I'm not giving them the key.
Requesting Gravatar... Haacked Oct 03, 2006 6:10 PM
# re: Better CAPTCHA Through Encryption
Besides, if the client can decrypt RijndaelManaged, then it can certainly answer interpret and execute the javascript question, which would be much easier.

I think most spam bots don't have even this level of sophistication. Yet.
Requesting Gravatar... Simeon Oct 03, 2006 6:26 PM
# re: Better CAPTCHA Through Encryption
"How can they decrypt the answer if they don't have the encryption key I am using. Remember, I'm not giving them the key."

Ah good points, I missed that bit, was thinking of statelessness, but forgot the key could be site configured.... carry on...
Requesting Gravatar... Haacked Oct 03, 2006 7:34 PM
# re: Better CAPTCHA Through Encryption
Ah, I see. Yes, the key is the one piece of state I do need to keep around in the server, but that's easy enough to do, generating it once in a static constructor. Each time the AppDomain recycles, that key would get regenerated, which is not a problem (and actually desirable).
Requesting Gravatar... wife Oct 03, 2006 11:06 PM
# re: Better CAPTCHA Through Encryption
your loyal readers (your brother and I) over at the non-tech blog are feeling neglected.

just thought you should know.
Requesting Gravatar... Stefan Dobrev Oct 07, 2006 2:57 AM
# re: Better CAPTCHA Through Encryption
Hi.
It's good that your CAPTCHA is getting better and better.
One minor bug in the code that you posted, when you are checking if date is not too old:
if ((DateTime.Now - date).Minutes > 30)
You should use TimeSpan.TotalMinutes not TimeSpan.Minutes, because Minutes returns the minutes of the TimeSpan and TotalMinutes returns the total number of minutes in the TimeSpan.
Requesting Gravatar... Haacked Oct 07, 2006 2:13 PM
# re: Better CAPTCHA Through Encryption
Great catch! Thanks! I'll apply the fix immediately.
Requesting Gravatar... Captcha Oct 08, 2006 2:52 PM
# re: Not safe
Nice try. But:

I will solve captcha once and remember the plain answer and the encrypted answer.

In the next 30 minutes I will replace every encrypted answer you generate and send to my client.
Then I'll submit your web-forms with remembered answer, replaced encrypted answer, and some extra spam back to you.

=> I don't need your encryption key to break the system. And 30 minutes is lots of time. ;)

Regards,
MP
Requesting Gravatar... Haacked Oct 08, 2006 3:46 PM
# re: Better CAPTCHA Through Encryption
I assume you mean solving it manually and then feeding it into your script. But you're going to do this for every site you attempt to crawl?

The point of this CAPTCHA is not that it's going to stop all comment spam. The point is that I'm betting that most comment spam bots are dumb bots that don't evaluate javascript when posting to comment forms.

This would stop those. And yes, you could manually solve it, then feed it into your script. But that would really slow you down if you're trying to spam every site out there.
Requesting Gravatar... Jeferson Hultmann Oct 09, 2006 6:08 PM
# re: Better CAPTCHA Through Encryption
hmmm... Is RijndaelManaged.CreateEncryptor thread safe?
Requesting Gravatar... Haacked Oct 09, 2006 6:20 PM
# re: Better CAPTCHA Through Encryption
Good question. Perhaps a more precise question is if I am using it in a thread-safe manner since the instance of RijndaelManaged, in the var encryptionAlgorithm, is a static variable, which could bring up contention issues.

I believe so since I am not changing the value of any of the properties of encryptionAlgorithm after it is created. Also, I create the instance in a static constructor which is guaranteed to be thread safe.

So the only contention would be caused by another thread changing the value of a property in the midst of that method call, which won't happen in this code.
Requesting Gravatar... Jeferson Hultmann Oct 09, 2006 8:50 PM
# re: Better CAPTCHA Through Encryption
I asked it because I remember an issue I had.

As RijndaelManaged internals are unknown, it would be safer to lock encryptionAlgorithm before using it. :/
Requesting Gravatar... Haacked Oct 09, 2006 11:25 PM
# re: Better CAPTCHA Through Encryption
Thanks for the heads up. I looked at the source in Reflector and didn't notice anything untoward. However no harm in being safe about it now and trying to create some tests to see if any race conditions happen.

Like I said, unless CreateEncryptor() changes the state of the encryption algorithm, I can't imagine any problems. But then again, that is no assurance.
Requesting Gravatar... CB Oct 13, 2006 5:54 PM
# re: Better CAPTCHA Through Encryption
Excellent!

I am a little confused with this line:

Page.ClientScript.RegisterHiddenField
(this.HiddenEncryptedAnswerFieldName
, EncryptAnswer(answer));

How is this being done this.HiddenEncryptedAnswerFieldName?
Requesting Gravatar... Haacked Oct 14, 2006 7:23 AM
# re: Better CAPTCHA Through Encryption
If you download the code, you'll see that's just a property. It's the name of the form field. I believe the code is this:


public string HiddenEncryptedAnswerFieldName
{
get
{
return this.ClientID + "_encrypted";
}
}
Requesting Gravatar... CB Oct 16, 2006 8:52 PM
# re: Better CAPTCHA Through Encryption
Pardon my ignorance, but I am new to .net. I didn't examine the source code closely enough to see it was a property.

I don't think I understand how I should be using this. I add the dll as a reference to my web site, then I import Subtext.Web.Controls.InvisibleCaptcha, then I am at a loss. I was able to use the encrypt and decrypt methods, but I am missing a great deal of the functionality of this great tool.
Requesting Gravatar... Haacked Oct 16, 2006 9:03 PM
# re: Better CAPTCHA Through Encryption
Once you add a reference to your web project, you need to add the control to your web page. It's a validator control, so make sure that when the user clicks yoru submit button, you check that Page.IsValid is true.
Requesting Gravatar... CB Oct 16, 2006 9:13 PM
# re: Better CAPTCHA Through Encryption
Thank you. I was thrown off because it was telling me there was an Error Rendering Control.

To test if it is valid I simply do

Dim test As New Subtext.Web.Controls.InvisibleCaptcha
Dim valid As Boolean
valid = test.IsValid

This has been a great learning tool for me.
Requesting Gravatar... nstlgc Oct 27, 2006 3:00 AM
# re: Better CAPTCHA Through Encryption
Doesn't this open a window of opportunity for a spammer? By solving a CAPTCHA challenge once, he has a certain window (in your case 30 minutes) in which he can spam your site as much as he wants by reusing the same CAPTCHA hidden value.

The workaround? Don't ask the client to actually remember the solution for you. Keep it stored serverside in a database, send an identification value back to the client so you know what entry to check, and remove the entry once the CAPTCHA has been approved. This way a correct solution can only be entered once.
Requesting Gravatar... Haacked Oct 27, 2006 8:58 AM
# re: Better CAPTCHA Through Encryption
In theory, spammers can already solve CAPTCHA. However theory and practice are two different things. Once you we see it in practice, then we can change it.
Requesting Gravatar... Sandra James Feb 27, 2007 7:51 AM
# re: Better CAPTCHA Through Encryption
the download link is broken
Requesting Gravatar... Sameer Jul 10, 2007 11:44 AM
# re: Better CAPTCHA Through Encryption
Here is captcha breaking in action
Requesting Gravatar... Spambot Oct 06, 2008 12:30 PM
# re: Better CAPTCHA Through Encryption
Small comment on the reaction of nstlgc:

What happens if a user keeps refreshing the web browser? create new database records?

Maybe it's better to keep a "blacklist" of answered security keys in the passed 30min? That way you won't be able to reuse the same security key over and over again and it won't kill your db server. If you combine this with the embedded time stamp in the security key you've got a solution that's pretty good!
Requesting Gravatar... Waleed Eissa Nov 06, 2008 2:19 PM
# re: Better CAPTCHA Through Encryption
Hi, I want to download the code but I get prompted to enter a username and password.

What do you have to say?

(will show your gravatar)
Please add 2 and 5 and type the answer here: