Click here to Skip to main content
15,867,308 members
Articles / Web Development / ASP.NET
Article

A CAPTCHA Control for ASP.NET 2

Rate me:
Please Sign up or sign in to vote.
4.86/5 (55 votes)
14 Mar 2008LGPL35 min read 357.6K   5.6K   174   67
A CAPTCHA control that is simple, secure, and easy to use.

Note: A new version of this control is available here. All further updates to the control are posted to the project's page on SourceForge.net.

Sample Image - CaptchaNET_2.gif

Introduction

CAPTCHA is short for "completely automated public Turing test to tell computers and humans apart", and is the most popular technique used to prevent computer programs from sending automated requests to Web servers. These can be for meta-searching search engines, doing dictionary attacks in login pages, or sending spam using mail servers. You might have seen CAPTCHA images in the Google register page when you do availability check on too many usernames, or in Yahoo!, PayPal, or other big Web sites.

Sources

The first CAPTCHA image generator I used was written using the CAPTCHA Image article by BrainJar. After that, I read the MSDN HIP challenge article and made many changes to my code. The code used in this control is based on the MSDN HIP article, but some parts are not changed.

How It Works

  • Captcha.ascx is the control file. When loaded, it calls the SetCaptcha() method. This method does everything needed, using other classes.
  • RandomText class generates cryptographically-strong random texts.
  • RNG class generates cryptographically-strong random numbers.
  • CaptchaImage class creates the image.
  • Encryptor class is used for encryption and decryption.
  • Captcha.ashx returns the image.

We will discuss some of them later in this article.

The Control

The main method in the control code is SetCaptcha() which is executed whenever we need to change the picture or load it.

C#
private void SetCaptcha()
{
    // Set image
    string s = RandomText.Generate();

    // Encrypt
    string ens = Encryptor.Encrypt(s, "srgerg$%^bg",
                 Convert.FromBase64String("srfjuoxp"));

    // Save to session
    Session["captcha"] = s.ToLower();

    // Set URL
    imgCaptcha.ImageUrl = "~/Captcha.ashx?w=305&h=92&c=" +
                          ens + "&bc=" + color;
}

This encrypts a random text using an encryption key which is hard-coded in this code. To prevent hard coding, you can store this information in the database and retrieve it when needed. This method also saves text to the session for comparison with user input.

To make the control style match with the page style, there are two properties used:

  • Style
  • Background color

The Style property sets the control style and the background color sets the background color for the generated image.

Two event handlers handle the Success and Failure events. We use a delegate for these handlers.

C#
public delegate void CaptchaEventHandler();

When the user submits the form, btnSubmit_Click() validates the user input.

C#
protected void btnSubmit_Click(object s, EventArgs e)
{
    if (Session["captcha"] != null && txtCaptcha.Text.ToLower() ==
    Session["captcha"].ToString())
    {
        if (success != null)
        {
            success();
        }
    }
    else
    {
        txtCaptcha.Text = "";
        SetCaptcha();

        if (failure != null)
        {
            failure();
        }
    }
}

RNG Class

The RNG class generates cryptographically-strong random numbers, using the RNGCryptoServiceProvider class.

C#
public static class RNG
{
    private static byte[] randb = new byte[4];
    private static RNGCryptoServiceProvider rand
                   = new RNGCryptoServiceProvider();

    public static int Next()
    {
        rand.GetBytes(randb);
        int value = BitConverter.ToInt32(randb, 0);
        if (value < 0) value = -value;
        return value;
    }
    public static int Next(int max)
    {
        // ...
    }
    public static int Next(int min, int max)
    {
        // ...
    }
}

RandomText Class

To create a cryptographically-strong random text, we use the RNG class to randomly select each character from the array of characters. This is a very useful technique I first saw in the CryptoPasswordGenerator.

C#
public static class RandomText
{
    public static string Generate()
    {
        // Generate random text
        string s = "";
        char[] chars = "abcdefghijklmnopqrstuvw".ToCharArray() +
                       "xyzABCDEFGHIJKLMNOPQRSTUV".ToCharArray() +
                       "WXYZ0123456789".ToCharArray();
        int index;
        int lenght = RNG.Next(4, 6);
        for (int i = 0; i < lenght; i++)
        {
            index = RNG.Next(chars.Length - 1);
            s += chars[index].ToString();
        }
        return s;
    }
}

CaptchaImage Class

This is the heart of our control. It gets the image text, dimensions, and background color, and generates the image.

The main method is GenerateImage() which generates the image using the information we provide.

C#
private void GenerateImage()
{
    // Create a new 32-bit bitmap image.
    Bitmap bitmap = new Bitmap(this.width, this.height,
        PixelFormat.Format32bppArgb);

    // Create a graphics object for drawing.
    Graphics g = Graphics.FromImage(bitmap);
    Rectangle rect = new Rectangle(0, 0, this.width, this.height);
    g.SmoothingMode = SmoothingMode.AntiAlias;

    // Fill background
    using (SolidBrush b = new SolidBrush(bc))
    {
        g.FillRectangle(b, rect);
    }

First, we declare Bitmap and Graphics objects, and a Rectangle whose dimensions are the same as the Bitmap object. Then, using a SolidBrush, we fill the background.

Now, we need to set the font size to fit within the image. The font family is chosen randomly from the fonts family collection.

C#
// Set up the text font.
int emSize = (int)(this.width * 2 / text.Length);
FontFamily family = fonts[RNG.Next(fonts.Length - 1)];
Font font = new Font(family, emSize);

// Adjust the font size until
// the text fits within the image.
SizeF measured = new SizeF(0, 0);
SizeF workingSize = new SizeF(this.width, this.height);

while (emSize > 2 &&
      (measured = g.MeasureString(text, font)).Width
       > workingSize.Width || measured.Height
       > workingSize.Height)
{
    font.Dispose();
    font = new Font(family, emSize -= 2);
}

We calculate a size for the font by multiplying the image width by 2 and then dividing it by the text length. It works well in most cases; for example, when the text length is 4 and the width is 8 pixels, the font size would be 4. But if the text length is 1, the font size would be 16. Also, when the image height is too short, the text will not fit within the image. When the calculated size is less than 2, we can be sure that it fits within the image except when the image height is very short, which we don't pay attention to. But when it is bigger than 2, we must make sure that the text fits within the image. We do that by getting the width and height the text needs for the selected font and size. If the width or height doesn't fit, then we reduce the size and check again and again till it fits.

The next step would be adding the text. It is done using a GraphicsPath object.

C#
GraphicsPath path = new GraphicsPath();
path.AddString(this.text, font.FontFamily,
        (int)font.Style, font.Size, rect, format);

The most important part is colorizing and distorting the text. We set the text color using RGB codes, each one using a random value between 0 and 255. A random color is then generated successfully. Now, we must check if the color is visible within the background color. It's done by calculating the difference between the text color R channel and the background color R channel. If it is less than 20, we regenerate the R channel value.

C#
// Set font color to a color that is visible within background color
int bcR = Convert.ToInt32(bc.R);
int red = random.Next(256), green = random.Next(256), blue =
    random.Next(256);
// This prevents font color from being near the bg color
while (red >= bcR && red - 20 < bcR ||
    red < bcR && red + 20 > bcR)
{
    red = random.Next(0, 255);
}
SolidBrush sBrush = new SolidBrush(Color.FromArgb(red, green, blue));
g.FillPath(sBrush, path);

Lastly, we distort the image by changing the pixel colors. For each pixel, we select a random picture from the original picture (the Bitmap object which we don't change) and set a random pixel color for it. Since distort is random, we see different distortions.

C#
// Iterate over every pixel
double distort = random.Next(5, 20) * (random.Next(10) == 1 ? 1 : -1);

// Copy the image so that we're always using the original for
// source color
using (Bitmap copy = (Bitmap)bitmap.Clone())
{
    for (int y = 0; y < height; y++)
    {
        for (int x = 0; x < width; x++)
        {
            // Adds a simple wave

            int newX =
              (int)(x + (distort * Math.Sin(Math.PI * y / 84.0)));
            int newY =
              (int)(y + (distort * Math.Cos(Math.PI * x / 44.0)));
            if (newX < 0 || newX >= width)
                newX = 0;
            if (newY < 0 || newY >= height)
                newY = 0;
            bitmap.SetPixel(x, y,
            copy.GetPixel(newX, newY));
        }
    }
}

Captcha.ashx

This HTTP handler gets the information needed to create a CAPTCHA image, and returns one. Note that this handler receives the encrypted text and has the key to decrypt it.

C#
public class Captcha : IHttpHandler
{
    public void ProcessRequest (HttpContext context) {
        context.Response.ContentType = "image/jpeg";
        context.Response.Cache.SetCacheability(HttpCacheability.NoCache);
        context.Response.BufferOutput = false;

        // Get text
        string s = "No Text";
        if (context.Request.QueryString["c"] != null &&
            context.Request.QueryString["c"] != "")
        {
            string enc = context.Request.QueryString["c"].ToString();

            // space was replaced with + to prevent error
            enc = enc.Replace(" ", "+");
            try
            {
                s = Encryptor.Decrypt(enc, "srgerg$%^bg",
            Convert.FromBase64String("srfjuoxp"));
            }
            catch { }
        }
        // Get dimensions
        int w = 120;
        int h = 50;
        // Width
        if (context.Request.QueryString["w"] != null &&

            context.Request.QueryString["w"] != "")
        {
            try
            {
                w = Convert.ToInt32(context.Request.QueryString["w"]);
            }
            catch { }
        }
        // Height
        if (context.Request.QueryString["h"] != null &&
            context.Request.QueryString["h"] != "")
        {
            try
            {
                h = Convert.ToInt32(context.Request.QueryString["h"]);
            }
            catch { }
        }
        // Color
        Color Bc = Color.White;
        if (context.Request.QueryString["bc"] != null &&

            context.Request.QueryString["bc"] != "")
        {
            try
            {
                string bc = context.Request.QueryString["bc"].
            ToString().Insert(0, "#");
                Bc = ColorTranslator.FromHtml(bc);
            }
            catch { }
        }
        // Generate image
        CaptchaImage ci = new CaptchaImage(s, Bc, w, h);

        // Return
        ci.Image.Save(context.Response.OutputStream, ImageFormat.Jpeg);
        // Dispose
        ci.Dispose();
    }

    public bool IsReusable
    {
        get
        {
            return true;
        }
    }
}

There are only two points to be noted about this class:

  1. Since in a URL, '+' means space, we replace a space with '+' (for encrypted text).
  2. Using # in the URL causes problems. We don't send # with the color value. For example, when color is #ffffff, we send ffffff and then add # in the handler.

Summary

When the control loads, it executes the SetCaptcha() method. The RandomText class generates a random text, and we save the text to the Session object and encrypt it. This method then generates the image URL using the dimensions, the encrypted text, and the background color information.

You may see an example of using this control in the source code.

License

This article, along with any associated source code and files, is licensed under The GNU Lesser General Public License (LGPLv3)


Written By
Australia Australia
I'm a Computer Science student at Monash University, Australia.

See my blog at http://blog.farshidzavareh.com

Comments and Discussions

 
GeneralQuestions and comments Pin
jmcunningham27-Nov-07 4:31
jmcunningham27-Nov-07 4:31 
GeneralRe: Questions and comments Pin
Farshid Zavareh13-Mar-08 22:13
Farshid Zavareh13-Mar-08 22:13 
QuestionDoes anyone have this in VB Pin
SirNathaniel1-Aug-07 1:38
SirNathaniel1-Aug-07 1:38 
GeneralDon’t put too much faith in CAPTCHAs in general Pin
JoelMMCC26-Jun-07 11:37
JoelMMCC26-Jun-07 11:37 
GeneralRe: Don&#8217;t put too much faith in CAPTCHAs in general Pin
Farshid Zavareh27-Jun-07 2:58
Farshid Zavareh27-Jun-07 2:58 
Generalrandom.Next(10) --> random.Next(2) Pin
Ilya Tretyakov it3xl7-Mar-07 4:03
Ilya Tretyakov it3xl7-Mar-07 4:03 
GeneralRe: random.Next(10) --> random.Next(2) Pin
Farshid Zavareh9-Mar-07 4:09
Farshid Zavareh9-Mar-07 4:09 
NewsNew Version Ready Pin
Farshid Zavareh18-Feb-07 5:56
Farshid Zavareh18-Feb-07 5:56 
Hi everyone.

I know that I had promised to send a new version of this control several months ago, but I didn't have opportunity to do so. Please accept my sincere apologizes.

I thought that now that I have been so late, let's publish something as bug free as possible. Therefore, I have provided the URL for compiled control in this message.

Please send me all the bugs you find, as well as everything you think I should add or modify. Since my source code may be a little hard to understand at the moment, I have not included it. But I will upload the source code as soon as I add necessary comments etc.

CAPTCHA.NET 2, ver. 2 : http://files.farshidh.com/Captcha.NET2/Captcha.NET2.zip

Cheers,
Farshid
QuestionRe: New Version Ready Pin
mityak22-Jun-07 4:25
mityak22-Jun-07 4:25 
AnswerRe: New Version Ready Pin
Jas T27-Jun-07 23:23
Jas T27-Jun-07 23:23 
GeneralRe: New Version Ready Pin
Farshid Zavareh27-Oct-07 8:25
Farshid Zavareh27-Oct-07 8:25 
GeneralVisual Flash CAPTCHA Pin
Searcherx9-Feb-07 9:17
Searcherx9-Feb-07 9:17 
GeneralRe: Visual Flash CAPTCHA Pin
Farshid Zavareh10-Feb-07 0:56
Farshid Zavareh10-Feb-07 0:56 
QuestionHow do you alter distortion? Pin
tedrog24-Jan-07 15:29
tedrog24-Jan-07 15:29 
AnswerRe: How do you alter distortion? Pin
tedrog25-Jan-07 16:43
tedrog25-Jan-07 16:43 
AnswerRe: How do you alter distortion? Pin
Farshid Zavareh10-Feb-07 0:51
Farshid Zavareh10-Feb-07 0:51 
GeneralRe: How do you alter distortion? Pin
breakpoint1-Jun-07 1:17
breakpoint1-Jun-07 1:17 
GeneralError and not work Pin
zsery224-Oct-06 3:10
zsery224-Oct-06 3:10 
GeneralRe: Error and not work Pin
Farshid Zavareh24-Oct-06 8:38
Farshid Zavareh24-Oct-06 8:38 
QuestionHow to integrate with existing forms? Pin
multiplex777711-Oct-06 21:29
multiplex777711-Oct-06 21:29 
AnswerRe: How to integrate with existing forms? Pin
Farshid Zavareh14-Oct-06 5:35
Farshid Zavareh14-Oct-06 5:35 
GeneralRe: How to integrate with existing forms? Pin
multiplex777716-Oct-06 17:12
multiplex777716-Oct-06 17:12 
QuestionWhere's the DLL? Pin
AdrOrg31-Aug-06 8:18
AdrOrg31-Aug-06 8:18 
AnswerRe: Where's the DLL? Pin
Farshid Zavareh21-Sep-06 9:24
Farshid Zavareh21-Sep-06 9:24 
GeneralRe: Where's the DLL? Pin
AdrOrg21-Sep-06 11:56
AdrOrg21-Sep-06 11:56 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.