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
- Peer receives a random challenge from a server, ‘authenticator challenge,’ and generates its own random ‘peer challenge’.
- Peer computes a combined SHA-1 hash of peer challenge, authenticator challenge, and user name.
- Peer calculates an irreversible MD4 hash of user password in UTF-16 encoding.
- 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: %sn", user_name); wprintf(L"User password: %sn", 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 %dn", 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); }