Computing a MS-CHAPv2 Client Response

Objective

This document contains code sample that calculates a valid response to a MS-CHAPv2 challenge request from a given user name, password, and server challenge. It could be useful for applications requiring secure authentication against Windows-based user databases. It also can be used as a tiny tutorial to some of Microsoft CryptoAPI aspects: hashing, key generation, and encryption.

Background Information

Challenge-Handshake Authentication Protocol allows you to authenticate a peer over an untrusted channel without transmitting a cleartext password and with protection against replay attacks. It is described in RFC 1994. Microsoft extensions allow you to use the CHAP algorithm with an encrypted passwords database, such as NTDS or Active Directory. They are described at RFC 2759.

Brief Description of the Algorithm

  1. Peer receives a random challenge from a server, 'authenticator challenge,' and generates its own random 'peer challenge'.
  2. Peer computes a combined SHA-1 hash of peer challenge, authenticator challenge, and user name.
  3. Peer calculates an irreversible MD4 hash of user password in UTF-16 encoding.
  4. Peer returns a final response, calculated as DES-encrypted challenge hash, using hashed password as key.

Comments

Functions are named according to their RFC 2759-proposed equivalents. The code is proven to be compiled in Microsoft Visual C++ 2003. The only useful function, which makes everything work, is GenerateNTResponse. It accepts all neccessary data—authenticator challenge, peer challenge, user name, and password—and returns a calculated MS-CHAPv2 response ready to transmit to the authenticating server (for example, to RADIUS NAS). The calling code should allocate a 24-byte long buffer for the result.

The code contains a sample test with example values from RFC 2759, as a proof of correctness.

// (c) 2006, Eugene Prigorodov, for public domain

#include <stdio.h>
#include <tchar.h>
#include <windows.h>
#include <WinCrypt.h>

void print_array(const BYTE* array, DWORD length);

void GenerateNTResponse(const HCRYPTPROV hProv,
                        const BYTE* auth_challenge,
                        const BYTE* peer_challenge,
                        const char* user_name,
                        const wchar_t* password,
                        BYTE* response);


int _tmain(int argc, _TCHAR* argv[])
{
   const char* user_name = "User";
   const wchar_t* password = L"clientPass";
   const BYTE auth_challenge[] = {0x5B,0x5D,0x7C,0x7D,0x7B,0x3F,
                                  0x2F,0x3E,0x3C,0x2C,0x60,0x21,
                                  0x32,0x26,0x26,0x28};
   const BYTE peer_challenge[] = {0x21,0x40,0x23,0x24,0x25,0x5E,
                                  0x26,0x2A,0x28,0x29,0x5F,0x2B,
                                  0x3A,0x33,0x7C,0x7E};
   DWORD status = 0;
   HCRYPTPROV hProv = 0;

   try
   {
      if (!CryptAcquireContext(&hProv, NULL, MS_DEF_PROV,
          PROV_RSA_FULL, CRYPT_VERIFYCONTEXT))
          throw "CryptAcquireContext failed"; 

      printf("User name: %s\n", user_name);
      wprintf(L"User password: %s\n", password);
      printf("Authenticator challenge: ");
      print_array(auth_challenge, 16);
      printf("Peer challenge: "); print_array(peer_challenge, 16);

      BYTE nt_response[24];
      GenerateNTResponse(hProv, auth_challenge, peer_challenge,
                         user_name, password, nt_response);
      printf("NT Response: "); print_array(nt_response, 24);
   }
   catch (const char* msg)
   {
      status = GetLastError();
      printf("Error: %s, system code %d\n", msg, status);
   }

   if (hProv)
      CryptReleaseContext(hProv, 0);

   return status;
}


const CHAR hex_digits[] = "0123456789ABCDEF";

void print_array(const BYTE* array, DWORD length)
{
   printf("0x");
   for (DWORD i=0; i<length; i++)
      printf("%c%c", hex_digits[array[i] >> 4],
             hex_digits[array[i] & 0xf]);
   printf("\n");
}


void _ChallengeHash(const HCRYPTPROV hProv,
                    const BYTE* peer_challenge,
                    const BYTE* auth_challenge,
                    const char* user_name,
                    BYTE* challenge) throw(...);
void _NtPasswordHash(const HCRYPTPROV hProv,
                     const wchar_t* password,
                     BYTE* password_hash) throw(...);
void _ChallengeResponse(const HCRYPTPROV hProv,
                        const BYTE* challenge,
                        const BYTE* password_hash,
                        BYTE* response) throw(...);

void GenerateNTResponse(const HCRYPTPROV hProv, 
                        const BYTE* auth_challenge,
                        const BYTE* peer_challenge,
                        const char* user_name,
                        const wchar_t* password,
                        BYTE* response) throw(...)
{
   BYTE challenge[8];
   BYTE password_hash[16];

   _ChallengeHash(hProv, peer_challenge, auth_challenge,
                  user_name, challenge);
   _NtPasswordHash(hProv, password, password_hash);
   _ChallengeResponse(hProv, challenge, password_hash, response);
}


#define SHA1LEN 20

void _ChallengeHash(const HCRYPTPROV hProv,
                    const BYTE* peer_challenge,
                    const BYTE* auth_challenge,
                    const char* user_name,
                    BYTE* challenge) throw(...)
{
   HCRYPTHASH hHash = 0;

   try 
   {
      if (!CryptCreateHash(hProv, CALG_SHA1, 0, 0, &hHash))
          throw "CryptCreateHash failed (SHA1)";
      if (!CryptHashData(hHash, peer_challenge, 16, 0))
          throw "CryptHashData failed (peer challenge)";
      if (!CryptHashData(hHash, auth_challenge, 16, 0))
          throw "CryptHashData failed (auth challenge)";
      if (!CryptHashData(hHash, (const BYTE*)user_name,
                         (DWORD)strlen(user_name), 0))
          throw "CryptHashData failed (user name)";
      DWORD hash_len = SHA1LEN;
      BYTE hash_buffer[SHA1LEN];
      if (!CryptGetHashParam(hHash, HP_HASHVAL, hash_buffer,
                             &hash_len, 0))
          throw "CryptGetHashParam failed (challenge hash)";
      memcpy(challenge, hash_buffer, 8);
   }
   catch (const char* msg)
   {
      if (hHash) 
            CryptDestroyHash(hHash);
      throw msg;
   }
   CryptDestroyHash(hHash);
}


#define MD4LEN 16

void _NtPasswordHash(const HCRYPTPROV hProv,
                     const wchar_t* password,
                     BYTE* password_hash) throw(...)
{
   HCRYPTHASH hHash = 0;

   try
   {
      if (!CryptCreateHash(hProv, CALG_MD4, 0, 0, &hHash))
          throw "CryptCreateHash failed (MD4)";
      // (lstrlenW << 1) -- size of Unicode password string in bytes
      if (!CryptHashData(hHash, (const BYTE*)password,
                         lstrlenW(password) << 1, 0))
          throw "CryptHashData failed (user password)";
      DWORD hash_len = MD4LEN;
      BYTE hash_buffer[MD4LEN];
      if (!CryptGetHashParam(hHash, HP_HASHVAL, hash_buffer,
                             &hash_len, 0))
          throw "CryptGetHashParam failed (NT password hash)";
      memcpy(password_hash, hash_buffer, 16);
   }
   catch (const char* msg) {
      if (hHash)
          CryptDestroyHash(hHash);
      throw msg;
   }
   CryptDestroyHash(hHash);
}


void _DesEncrypt(const HCRYPTPROV hProv,
                 const BYTE* data,
                 const BYTE* key,
                 BYTE* result) throw(...);

void _ChallengeResponse(const HCRYPTPROV hProv,
                        const BYTE* challenge,
                        const BYTE* password_hash,
                        BYTE* response) throw(...)
{
   BYTE z_password_hash[21];

   // Set ZPasswordHash to PasswordHash zero-padded to 21 octets
   memset(z_password_hash, 0, 21);
   memcpy(z_password_hash, password_hash, 16);

   _DesEncrypt(hProv, challenge, z_password_hash, response);
   _DesEncrypt(hProv, challenge, z_password_hash + 7, response + 8);
   _DesEncrypt(hProv, challenge, z_password_hash + 14, response + 16);
}


typedef struct {
   BLOBHEADER key_header;
   DWORD key_length;
   BYTE key_data[8];
} DESKey;

void _InsertParityBits(BYTE* key);

void _DesEncrypt(const HCRYPTPROV hProv,
                 const BYTE* data,
                 const BYTE* key,
                 BYTE* result) throw(...)
{
   // Fill CryptoAPI-required key structure
   DESKey des_key;
   des_key.key_header.bType    = PLAINTEXTKEYBLOB;
   des_key.key_header.bVersion = CUR_BLOB_VERSION;
   des_key.key_header.reserved = 0;
   des_key.key_header.aiKeyAlg = CALG_DES;
   des_key.key_length          = 8;
   memcpy(des_key.key_data, key, 7);
   _InsertParityBits(des_key.key_data);

   HCRYPTKEY hKey;

   try {
      // import key BLOB 
      if (!CryptImportKey(hProv, (BYTE*)&des_key,
                          sizeof(des_key),0,0,&hKey))
          throw "CryptImportKey failed";
      // set ECB mode required by RFC
      DWORD des_mode = CRYPT_MODE_ECB;
      if (!CryptSetKeyParam(hKey, KP_MODE, (BYTE*) &des_mode, 0))
          throw "CryptSetKeyParam failed (ECB mode)";
      // set initialization vector
      BYTE IV[8] = {0, 0, 0, 0, 0, 0, 0, 0};
      if (!CryptSetKeyParam(hKey, KP_IV, &IV[0], 0))
          throw "CryptSetKeyParam failed (init vector)";
      // encrypt
      DWORD data_len = 8;
      memcpy(result, data, 8);    // encrypt in-place
      if (!CryptEncrypt(hKey, 0, FALSE, 0, (BYTE*) &result[0],
          &data_len, 8))
          throw "CryptEncrypt failed (DES)";
   }
   catch (const char* msg) {
      if (hKey)
          CryptDestroyKey(hKey);
      throw msg;
   }
   CryptDestroyKey(hKey);
}


// insert in place dummy parity bits in 56-bit key
// to make valid 64-bit DES key
void _InsertParityBits(BYTE* key)
{
   BYTE left_octet, right_octet;
   BYTE new_key[8];
   // split original key into eight 7-bit packs:
   // 00000001 11111122 22222333 3333444 44455555 55666666 67777777
   for (int i=0; i<8; i++)
   { 
      // fetch two adjacent octets of key containing current 7 bits
      left_octet = key[i>0 ? i-1 : 0];
      right_octet = key[i<7 ? i : 6];
      // clear all bits except current 7 ones
      left_octet &= 0xFF >> (8-i);
      right_octet &= 0xFF << (i+1);
      // shift bits to their final position
      left_octet = left_octet << (8-i);
      right_octet = right_octet >> i;
      // combine into resulting octet
      new_key[i] = left_octet | right_octet;
   }
   memcpy(key, new_key, 8);
}


About the Author

Eugene Prigorodov

Experienced software developer, currently specialized in Python. Russia.

Downloads

Comments

  • Thank you!

    Posted by jahr on 06/14/2013 08:51pm

    Thank you very much for this code, it has really helped me in debugging my own implementation.)

    Reply
Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • Application and data integration doesn't have to be slow and expensive. Learn how cloud-based integration platforms dramatically speed results and lower costs in this white paper by Bloor Research.

  • This ESG study by Mark Peters evaluated a common industry-standard disk VTl deduplication system (with 15:1 reduction ratio) versus a tape library with LTO-5, drives with full nightly backups, over a five-year period.  The scenarios included replicated systems and offsite tape vaults.  In all circumstances, the TCO for VTL with deduplication ranged from about 2 to 4 times more expensive than the LTO-5 tape library TCO. The paper shares recent ESG research and lots more. 

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds