PasswordTextBox

Security Briefs

Syndication

Chris Sells used to poke fun at me when we worked together in my former life. He used to call my security class, "Essential Access Denied". His point was a good one: when they aren't applied carefully, security countermeasures often just get in the way of getting work done. I don't know about you, but password-mode text boxes in web forms have always been one of those annoyances.

I'm not complaining about the fact that I can't see what I'm typing. I understand and laud that feature, because I don't want someone looking over my shoulder at the password I'm typing, and this even applies when I'm at home. I love my children, but I certainly don't want them knowing the password to my bank account!

No, what I'm bothered by is how a typical password text box behaves on a form that may incur multiple post-backs before it's finally submitted. If you use the built in ASP.NET TextBox control, it purposely does not repopulate the password text, which means if you press a button on the form that performs a post-back, or if you have a multi-page form that posts back on every step, that password disappears, and the user typically has to re-enter it. You could solve this with liberal use of ASP.NET Ajax UpdatePanels, but that adds its own complexities. I wanted a simpler solution.

So I did a little research to see what others had discovered about this problem, and I ended up deriving my own custom control from TextBox to make a much more user-friendly (and developer-friendly) TextBox control. I called it PasswordTextBox, and it acts just like a TextBox in password mode, but it retains the password while still giving the user the same level of protection the standard TextBox supplies.

My PasswordTextBox operates very simply: it stores the password in control state, and renders a series of fixed characters (with the same length as the actual password) into the text box so that it "looks" like the user's password has been rendered. Since control state is part of view state, and since view state is stored in a hidden field on the form, I encrypt the password before putting it into control state.

The result is quite nice - the user can post your form back as many times as she needs to, perhaps moving back and forth across wizard steps or tabs, and when she finally presses the "Finish" button (or whatever you call the last step of your input form), your code will be able to read the password by simply accessing the Text property on the PasswordTextBox. The user will believe that her password is sitting there on the form while she's working, as the same number of obfuscated characters will show up in the field as she typed in originally (what she doesn't know is that those characters aren't her real password anymore, but what she doesn't know won't hurt her!)

Note that to keep this simple, I used DPAPI to encrypt the password, which suited my purposes. But if you have a web farm, that won't work well at all if you don't know which machine the user's going to post back to, so you'll want to replace that with something more robust. I could see looking up the <machineKey> for entropy, as that tends to be sync'd already across the farm, but I've not yet spent the cycles to go down that road, since unfortunately all of the code for generating keys based on that config section are off limits in ASP.NET (most of the useful stuff is marked internal). I don't think it'd be that hard to do though.

Anyway, without further ado, here's the code, which you'll see is quite simple. I'd love feedback, especially if you see any glaring problems with the idea or the implementation!

public class PasswordTextBox : TextBox
{
    // unlikely that a string of these would be used for a password
    const char PasswordPlaceholderChar = '}';

    string password; // stored encrypted in control state

    protected override void OnInit(EventArgs e)
    {
        base.OnInit(e);
        Page.RegisterRequiresControlState(this);
    }

    protected override object SaveControlState()
    {
        byte[] encryptedPassword = ProtectPassword(password);

        object baseControlState = base.SaveControlState();
        if (null == baseControlState)
            return encryptedPassword;
        else return new Pair(baseControlState, encryptedPassword);
    }

    protected override void LoadControlState(object savedState)
    {
        byte[] encryptedPassword;

        Pair pair = savedState as Pair;
        if (null != pair)
        {
            base.LoadControlState(pair.First);
            encryptedPassword = pair.Second as byte[];
        }
        else encryptedPassword = savedState as byte[];

        password = UnprotectPassword(encryptedPassword);
    }

    /// <summary>
    /// This control always uses TextMode=Password
    /// </summary>
    public override TextBoxMode TextMode
    {
        get
        {
            return TextBoxMode.Password;
        }
        set { }
    }

    /// <summary>
    /// TextBox doesn't render value attribute for TextMode=Password
    /// So we add code that renders a placeholder text instead
    /// </summary>
    /// <param name="writer"></param>
    protected override void AddAttributesToRender(HtmlTextWriter writer)
    {
        base.AddAttributesToRender(writer);

        string text = Text;
        if (text.Length > 0)
            writer.AddAttribute(HtmlTextWriterAttribute.Value,
                GetPlaceholderPassword(text));
    }

    /// <summary>
    /// TextBox doesn't save the "Text" viewstate in
    /// TextMode=Password and we don't want our behavior to break
    /// if ViewState is turned off so we store the password in
    /// Control State, encrypted with MachineKey
    /// </summary>
    public override string Text
    {
        get
        {
            return password ?? string.Empty;
        }
        set
        {
            // this prevents us overwriting the actual
            // password with a placeholder
            if (!string.IsNullOrEmpty(password) &&
                value.Equals(GetPlaceholderPassword(password)))
                return;

            password = value;
        }
    }

    private string GetPlaceholderPassword(string realPassword)
    {
        int length = 12;
        if (!string.IsNullOrEmpty(realPassword))
            length = realPassword.Length;

        StringBuilder sb = new StringBuilder();
        sb.Append(PasswordPlaceholderChar, length);

        return sb.ToString();
    }

    public byte[] ProtectPassword(string password)
    {
        if (string.IsNullOrEmpty(password))
            return null;
        byte[] cleartext = Encoding.UTF8.GetBytes(password);
        return ProtectedData.Protect(cleartext, null,
            DataProtectionScope.LocalMachine);
    }

    public string UnprotectPassword(byte[] ciphertext)
    {
        if (null == ciphertext)
            return null;
        byte[] cleartext = ProtectedData.Unprotect(ciphertext, null,
            DataProtectionScope.LocalMachine);
        return Encoding.UTF8.GetString(cleartext);
    }
}

Posted Oct 29 2008, 12:49 PM by keith-brown
Filed under: , ,

Comments

Nuno Agapito wrote re: PasswordTextBox
on 10-29-2008 6:00 PM

Nice!!!

Thank you!

Uwe wrote re: PasswordTextBox
on 12-26-2008 2:24 AM

Works great, thank you very much!

Add a Comment

(required)  
(optional)
(required)  
Remember Me?