Managed C++: Authenticating Users via Hash Codes

A previous article covered how to use the .NET encryption classes to compare string values using hash codes. Another usage of hash codes is in situations where a system needs to authenticate a user without storing the user’s password. This is especially useful in situations where a company would be held legally liable should the passwords file be stolen or hacked. As I wrote in my book, Extending MFC Applications with the .NET Framework, this can be accomplished by storing the hash code of the password in a file and then—when the user attempts to log in—taking the user’s entered password, hashing it, and comparing it to the hash code that was saved. If you’ve ever used a system that warned you that it could not recover your password should you lose it, now you know one technique it could’ve used to authenticate your password without storing it. This article walks you though this technique using the .NET encryption and XML classes.

Figure 1: The .NET Encryption classes are used to compare the MD5 hash of the entered password against the stored value for that user.

This article uses the Visual C++ 2005 syntax.
  1. Creating the user/passwords file
    Because .NET has wonderful support for XML, I use the XML format to store the users and passwords. The following sample file has a single user and an MD5 hash value for that user’s password:

    <?xml version="1.0" encoding="utf-8" ?>
    <!DOCTYPE root [
      <!ELEMENT User ANY>
      <!ATTLIST User Name ID #REQUIRED>
    ]>
    <Users>
      <User Name="tom">97-FB-DF-62-D7-39-18-BD-AF-5A-CB-8A-AF-
                       4A-82-3C</User>
    </Users>
    
  2. Defining the Name attribute as an ID makes each user record unique based on name. It also allows you to programmatically search for users using that value and the XmlDocument::GetElementById method (which you’ll see shortly).

  3. Loading an XML file
    The demo application does this in the form load method. As you can see, the application closes if the file cannot be loaded (in the catch block):

    using namespace System::Xml;
    
    ...
    
    protected: XmlDocument^ passwordFile;
    
    ...
    
    private: System::Void Form1_Load(System::Object^ sender,
                                     System::EventArgs^  e)
    {
      try
      {
        // load passwords
        passwordFile = gcnew XmlDocument();
        passwordFile->Load("users.xml");
      }
      catch(Exception^ ex)
      {
        MessageBox::Show(ex->Message, "ERROR");
        Close();
      }
    }
    
  4. Creating and updating user records
    This method (from the article’s demo) first calculates the MD5 hash code from the user’s entered password by copying the password string into a byte array (via the Encoding::GetBytes method). It then uses a MD5CryptoServiceProvider object to compute the hash from that byte array. It converts the resulting hash code back into a String object (via BitConverter::ToString). When that is done, it searches the XML document using the XmlDocument::GetElementById method. If it finds the element (representing the user), it updates its “inner text” to the newly created hash value. If it doesn’t find the element—which indicates a new user—it creates a new node, sets its inner-text to the hashed password, creates an attribute object with its value set to the user’s name, and adds these objects to the XML document. Finally, it saves the document (file) with a call to XmlDocument::Save:

    private: System::Void btnSave_Click(System::Object^ sender,
                                        System::EventArgs^  e)
    {
      try
      {
        // Get the user and password values.
        String^ user = editUserName->Text;
        String^ password = editPassword->Text;
    
        // Calculate hash code for user's entered password
        array<Byte>^ baPassword = Encoding::ASCII->GetBytes(password);
        MD5CryptoServiceProvider^ md5csp = gcnew
            MD5CryptoServiceProvider();
        array<Byte>^ baHash = md5csp->ComputeHash(baPassword);
        String^ passwordHashString = BitConverter::ToString(baHash);
    
        // Search for specified user
        XmlElement^ userNode = passwordFile->GetElementById(user);
        if (userNode)    // if found, update user's password
        {
          // Update user's hash code representation of password
          userNode->InnerText = passwordHashString;
        }
        else    // Create new user
        {
          // Create XML node.
          XmlNode^ userNode = passwordFile->
                   CreateNode( XmlNodeType::Element, "User", nullptr );
          userNode->InnerText = passwordHashString;
    
          // Create attribute and add to node
          XmlAttribute^ attr = passwordFile->CreateAttribute("Name");
          attr->Value = user;
          userNode->Attributes->Append(attr);
    
          // Add node to document
          passwordFile->DocumentElement->AppendChild( userNode);
        }
    
        // Save the XML document
        passwordFile->Save("users.xml");
        MessageBox::Show("Successfully saved", "Success");
      }
      catch(Exception^ ex)
      {
        MessageBox::Show(ex->Message, "ERROR");
      }
    }
    
  5. Validating user information
    User authentication is just as easy. The click event for the demo application’s “Validate” button calls the Login method, which does the following: The Login method first attempts to locate the user via the XmlDocument::GetElementById method using the specified user name. If that is successful, the entered password is hashed (just as in the btnSave_Click method). The hashed value is then converted into a string and compared against the string representing the user’s hashed password in the XML file. If they’re equal, the user is authenticated:

    private: System::Void btnValidate_Click(System::Object^ sender,
                                            System::EventArgs^  e)
    {
      if (Login(editUserName->Text, editPassword->Text))
        MessageBox::Show("Successfully logged in", "Successful Login");
      else
        MessageBox::Show("The user name or password are incorrect.
                          Please try again.", "INVALID LOGIN ATTEMPT");
    }
    
    private: bool Login(String^ userName, String^ password)
    {
      bool loginIsValid = false;
    
      try
      {
        // Search for user
        XmlElement^ userNode = passwordFile->GetElementById(userName);
    
        if (userNode)    // If found, get password's hash value
        {
          String^ actualHashValue = userNode->InnerText;
    
          // Hash the user's entered password
          array<Byte>^ baPassword = Encoding::ASCII->GetBytes(password);
          MD5CryptoServiceProvider^ md5csp = gcnew
              MD5CryptoServiceProvider();
          array<Byte>^ baHash = md5csp->ComputeHash(baPassword);
    
          // Convert the hash value of the user's entered password
          // into a string
          String^ userEnteredPasswordHashValue =
              BitConverter::ToString(baHash);
    
          // Compare the two values (the one entered and the correct one
          // in the xml file)
          if (0 == String::Compare(userEnteredPasswordHashValue,
                                   actualHashValue))
            loginIsValid = true;
        }
      }
      catch(Exception^ ex)
      {
        MessageBox::Show(ex->Message, "ERROR");
      }
    
      return loginIsValid;
    }
    

Adding “Salt” to the Mix

Because a password is such a small size—generally 6-10 characters in length—someone that gains unauthorized access to your passwords file could try a brute-force method for determining the passwords. While unlikely, that threat still exists. Therefore, to give yourself an even greater level of security, you could simply add a “salt” to the password.

A salt is a static string that is added to a password before hashing. The addition of the salt greatly increases the difficulty in cracking the codes, because now if the file is hacked the unauthorized person must also determine the salt that gets added to the input as well as how it’s added to the input before even attempting to generate the passwords.

The salt itself and the ways to incorporate it into the value being hashed are unlimited. For example, the salt could be a sequence of letters such as *#(^ that you could add to the beginning or end of the password before hashing it. (Don’t forget to add the salt during validation.) You could also get a little more exotic and add each letter of the salt at certain positions of the password—such as every second position. You could also replace certain letters with your salt. As you can see, you have limitless possibilities that will aid you in protecting your user’s passwords and—by extension—the integrity of your system.

More by Author

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Must Read