Created
November 17, 2020 17:21
-
-
Save teeks99/28d7872e5448be3f52736167dc16833f to your computer and use it in GitHub Desktop.
HMAC_MD5 and MD5 Digest on Windows
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// An attempt to make a HMAC_MD5 method on window that matches the RFC 2104 test vectors | |
// To do this, I also needed to make a MD5 Digest method that matches the test vectors at the end of (A.5) RFC 1321 | |
// https://tools.ietf.org/html/rfc2202 | |
// https://tools.ietf.org/html/rfc1321 | |
// Note: MD5 is not secure if you are using this, consider updating to a modern algorithm for HMAC such as: | |
// SHA-256, 384, 512 | |
// SHA3-* | |
// Based on the examples here: | |
// https://docs.microsoft.com/en-us/windows/win32/seccrypto/example-c-program--creating-an-hmac | |
// https://docs.microsoft.com/en-us/windows/win32/seccrypto/example-c-program--creating-an-md-5-hash-from-file-content | |
#include <windows.h> | |
#include <wincrypt.h> | |
#include <vector> | |
#include <string> | |
#include <stdexcept> | |
#include <iostream> | |
#define MD5LEN 16 | |
#define MAX_HMAC_KEY_LEN 64 | |
std::vector<unsigned char> RFC_MD5_Digest(std::vector<unsigned char> input) | |
{ | |
HCRYPTPROV hProv = 0; | |
HCRYPTHASH hHash = 0; | |
std::vector<unsigned char> output(MD5LEN, 0); | |
if (!CryptAcquireContext(&hProv, | |
NULL, | |
NULL, | |
PROV_RSA_FULL, | |
CRYPT_VERIFYCONTEXT)) | |
{ | |
unsigned int error = GetLastError(); | |
throw std::runtime_error("CryptAcquireContext failed: " + std::to_string(error)); | |
} | |
if (!CryptCreateHash(hProv, CALG_MD5, 0, 0, &hHash)) | |
{ | |
CryptReleaseContext(hProv, 0); | |
unsigned int error = GetLastError(); | |
throw std::runtime_error("CryptAcquireContext failed: " + std::to_string(error)); | |
} | |
if (!CryptHashData(hHash, input.data(), input.size(), 0)) | |
{ | |
CryptReleaseContext(hProv, 0); | |
CryptDestroyHash(hHash); | |
unsigned int error = GetLastError(); | |
throw std::runtime_error("CryptHashData failed: " + std::to_string(error)); | |
} | |
unsigned long outlen = MD5LEN; | |
if (!CryptGetHashParam(hHash, HP_HASHVAL, output.data(), &outlen, 0)) | |
{ | |
CryptDestroyHash(hHash); | |
CryptReleaseContext(hProv, 0); | |
unsigned int error = GetLastError(); | |
throw std::runtime_error("CryptGetHashParam failed: " + std::to_string(error)); | |
} | |
if (outlen != MD5LEN) | |
{ | |
throw std::runtime_error("The MD5 Digest returned was not MD5LEN bytes, it was: " + std::to_string(outlen)); | |
} | |
CryptDestroyHash(hHash); | |
CryptReleaseContext(hProv, 0); | |
return output; | |
} | |
std::vector<unsigned char> RFC_HMAC_MD5(std::vector<unsigned char> input, std::vector<unsigned char> key) | |
{ | |
std::vector<unsigned char> output; | |
HCRYPTPROV hProv = NULL; | |
HCRYPTHASH hHmacHash = NULL; | |
HCRYPTKEY keyHandle = NULL; | |
HMAC_INFO HmacInfo; | |
if (key.size() > MAX_HMAC_KEY_LEN) | |
{ | |
key = RFC_MD5_Digest(key); | |
} | |
ZeroMemory(&HmacInfo, sizeof(HmacInfo)); | |
HmacInfo.HashAlgid = CALG_MD5; | |
if (!CryptAcquireContext(&hProv, | |
NULL, | |
NULL, | |
PROV_RSA_FULL, | |
CRYPT_VERIFYCONTEXT)) | |
{ | |
unsigned int error = GetLastError(); | |
throw std::runtime_error("CryptAcquireContext failed: " + std::to_string(error)); | |
} | |
size_t blobSize = sizeof(BLOBHEADER) + 4 + key.size(); | |
unsigned char* keyBlob = new unsigned char[blobSize]; | |
BLOBHEADER* bh = reinterpret_cast<BLOBHEADER*>(keyBlob); | |
unsigned int* keySize = reinterpret_cast<unsigned int*>(keyBlob + sizeof(BLOBHEADER)); | |
bh->bType = PLAINTEXTKEYBLOB; | |
bh->bVersion = CUR_BLOB_VERSION; | |
bh->reserved = 0; | |
// These two values come from Google's chrome HMAC implementation, no RC2 is actually used here | |
// https://chromium.googlesource.com/chromium/src/+/32352ad08ee673a4d43e8593ce988b224f6482d3/crypto/symmetric_key_win.cc#78 | |
bh->aiKeyAlg = CALG_RC2; | |
unsigned int flags = CRYPT_EXPORTABLE | CRYPT_IPSEC_HMAC_KEY; | |
*keySize = key.size(); | |
memcpy(keyBlob + 4 + sizeof(BLOBHEADER), key.data(), key.size()); | |
if (!CryptImportKey( | |
hProv, | |
keyBlob, | |
blobSize, | |
0, | |
flags, | |
&keyHandle)) | |
{ | |
CryptReleaseContext(hProv, 0); | |
delete[] keyBlob; | |
unsigned int error = GetLastError(); | |
throw std::runtime_error("CryptImportKey failed: " + std::to_string(error)); | |
} | |
if (!CryptCreateHash( | |
hProv, // handle of the CSP. | |
CALG_HMAC, // HMAC hash algorithm ID | |
keyHandle, // key for the hash (see above) | |
0, // reserved | |
&hHmacHash)) // address of the hash handle | |
{ | |
CryptReleaseContext(hProv, 0); | |
delete[] keyBlob; | |
unsigned int error = GetLastError(); | |
throw std::runtime_error("CryptAcquireContext failed: " + std::to_string(error)); | |
} | |
if (!CryptSetHashParam( | |
hHmacHash, // handle of the HMAC hash object | |
HP_HMAC_INFO, // setting an HMAC_INFO object | |
(BYTE*)&HmacInfo, // the HMAC_INFO object | |
0)) // reserved | |
{ | |
CryptReleaseContext(hProv, 0); | |
delete[] keyBlob; | |
unsigned int error = GetLastError(); | |
throw std::runtime_error("Error in CryptSetHashParam: " + std::to_string(error)); | |
} | |
if (!CryptHashData( | |
hHmacHash, // handle of the HMAC hash object | |
input.data(), // message to hash | |
input.size(), // number of bytes of data to add | |
0)) // flags | |
{ | |
CryptReleaseContext(hProv, 0); | |
CryptDestroyHash(hHmacHash); | |
delete[] keyBlob; | |
unsigned int error = GetLastError(); | |
throw std::runtime_error("Error in CryptHashData: " + std::to_string(error)); | |
} | |
unsigned long hashLength = 0; | |
if (!CryptGetHashParam( | |
hHmacHash, // handle of the HMAC hash object | |
HP_HASHVAL, // query on the hash value | |
0, // pointer to the HMAC hash value | |
&hashLength, // length, in bytes, of the hash | |
0)) | |
{ | |
CryptReleaseContext(hProv, 0); | |
CryptDestroyHash(hHmacHash); | |
delete[] keyBlob; | |
unsigned int error = GetLastError(); | |
throw std::runtime_error("Error in CryptGetHashParam: " + std::to_string(error)); | |
} | |
output.resize(hashLength, 0); | |
if (!CryptGetHashParam( | |
hHmacHash, // handle of the HMAC hash object | |
HP_HASHVAL, // query on the hash value | |
output.data(), // pointer to the HMAC hash value | |
&hashLength, // length, in bytes, of the hash | |
0)) | |
{ | |
CryptReleaseContext(hProv, 0); | |
CryptDestroyHash(hHmacHash); | |
delete[] keyBlob; | |
unsigned int error = GetLastError(); | |
throw std::runtime_error("Error in CryptGetHashParam: " + std::to_string(error)); | |
} | |
CryptReleaseContext(hProv, 0); | |
CryptDestroyHash(hHmacHash); | |
delete[] keyBlob; | |
return output; | |
} | |
unsigned char char2int(char input) | |
{ | |
if (input >= '0' && input <= '9') | |
return input - '0'; | |
if (input >= 'A' && input <= 'F') | |
return input - 'A' + 10; | |
if (input >= 'a' && input <= 'f') | |
return input - 'a' + 10; | |
throw std::invalid_argument("Invalid input string"); | |
} | |
// This function assumes src to be a zero terminated sanitized string with | |
// an even number of [0-9a-f] characters, and target to be sufficiently large | |
std::vector<unsigned char> HexStringToBytes(std::string input) | |
{ | |
std::vector<unsigned char> output; | |
output.reserve(input.size() / 2); | |
if (input.rfind("0x", 0) == 0) | |
{ | |
input.erase(0, 2); | |
} | |
for(unsigned int byte = 0; byte < input.size(); byte += 2) | |
{ | |
unsigned char high = char2int(input[byte]); | |
unsigned char low = char2int(input[byte+1]); | |
output.push_back(high * 16 + low); | |
} | |
return output; | |
} | |
void PrintHexVector(std::vector<unsigned char> data) | |
{ | |
std::cout << "0x"; | |
for (unsigned char val : data) | |
{ | |
std::cout << std::hex << val; | |
} | |
} | |
void TestHMAC(std::string key, std::string data, std::string expectedDigestHex) | |
{ | |
std::vector<unsigned char> keyBytes; | |
if (key.rfind("0x", 0) == 0) | |
{ | |
keyBytes = HexStringToBytes(key); | |
} | |
else | |
{ | |
std::copy(key.begin(), key.end(), std::back_inserter(keyBytes)); | |
} | |
std::vector<unsigned char> dataBytes; | |
if (data.rfind("0x", 0) == 0) | |
{ | |
dataBytes = HexStringToBytes(data); | |
} | |
else | |
{ | |
std::copy(data.begin(), data.end(), std::back_inserter(dataBytes)); | |
} | |
std::vector<unsigned char> result = RFC_HMAC_MD5(dataBytes, keyBytes); | |
std::vector<unsigned char> expected = HexStringToBytes(expectedDigestHex); | |
bool same = true; | |
if (result.size() != expected.size()) | |
{ | |
same = false; | |
} | |
else | |
{ | |
for (unsigned int byte = 0; byte < result.size(); byte++) | |
{ | |
if (result[byte] != expected[byte]) | |
{ | |
same = false; | |
break; | |
} | |
} | |
} | |
if (same) | |
{ | |
std::cout << "Test Passed" << std::endl; | |
std::cout << "\tKey: " << key << std::endl; | |
std::cout << "\tData: " << data << std::endl; | |
std::cout << "\tDigest: " << expectedDigestHex << std::endl; | |
} | |
else | |
{ | |
std::cout << "ERROR Test Failed" << std::endl; | |
std::cout << "\tKey: " << key << std::endl; | |
std::cout << "\tData: " << data << std::endl; | |
std::cout << "\tExpected Digest: " << expectedDigestHex << std::endl; | |
std::cout << "\tActual Digest: "; | |
PrintHexVector(result); | |
std::cout << std::endl; | |
} | |
} | |
void HMAC_RFC_TestCases() | |
{ | |
std::cout << "------------------------------------------------------------" << std::endl; | |
std::cout << "Test Cases from RFC-2202 https://tools.ietf.org/html/rfc2202" << std::endl; | |
std::cout << "------------------------------------------------------------" << std::endl; | |
//test_case = 1 | |
//key = 0x0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b | |
//key_len = 16 | |
//data = "Hi There" | |
//data_len = 8 | |
//digest = 0x9294727a3638bb1c13f48ef8158bfc9d | |
TestHMAC( | |
"0x0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b", | |
"Hi There", | |
"0x9294727a3638bb1c13f48ef8158bfc9d"); | |
//test_case = 2 | |
//key = "Jefe" | |
//key_len = 4 | |
//data = "what do ya want for nothing?" | |
//data_len = 28 | |
//digest = 0x750c783e6ab0b503eaa86e310a5db738 | |
TestHMAC( | |
"Jefe", | |
"what do ya want for nothing?", | |
"0x750c783e6ab0b503eaa86e310a5db738"); | |
//test_case = 3 | |
//key = 0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa | |
//key_len 16 | |
//data = 0xdd repeated 50 times | |
//data_len = 50 | |
//digest = 0x56be34521d144c88dbb8c733f0e8b3f6 | |
TestHMAC( | |
"0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", | |
"0xDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD", | |
"0x56be34521d144c88dbb8c733f0e8b3f6"); | |
//test_case = 4 | |
//key = 0x0102030405060708090a0b0c0d0e0f10111213141516171819 | |
//key_len 25 | |
//data = 0xcd repeated 50 times | |
//data_len = 50 | |
//digest = 0x697eaf0aca3a3aea3a75164746ffaa79 | |
TestHMAC( | |
"0x0102030405060708090a0b0c0d0e0f10111213141516171819", | |
"0xCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCD", | |
"0x697eaf0aca3a3aea3a75164746ffaa79"); | |
//test_case = 5 | |
//key = 0x0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c | |
//key_len = 16 | |
//data = "Test With Truncation" | |
//data_len = 20 | |
//digest = 0x56461ef2342edc00f9bab995690efd4c | |
//digest-96 0x56461ef2342edc00f9bab995 | |
TestHMAC( | |
"0x0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c", | |
"Test With Truncation", | |
"0x56461ef2342edc00f9bab995690efd4c"); | |
//test_case = 6 | |
//key = 0xaa repeated 80 times | |
//key_len = 80 | |
//data = "Test Using Larger Than Block-Size Key - Hash Key First" | |
//data_len = 54 | |
//digest = 0x6b1ab7fe4bd7bf8f0b62e6ce61b9d0cd | |
TestHMAC( | |
"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", | |
"Test Using Larger Than Block-Size Key - Hash Key First", | |
"0x6b1ab7fe4bd7bf8f0b62e6ce61b9d0cd"); | |
//test_case = 7 | |
//key = 0xaa repeated 80 times | |
//key_len = 80 | |
//data = "Test Using Larger Than Block-Size Key and Larger | |
// Than One Block-Size Data" | |
//data_len = 73 | |
//digest = 0x6f630fad67cda0ee1fb1f562db3aa53e | |
TestHMAC( | |
"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", | |
"Test Using Larger Than Block-Size Key and Larger Than One Block-Size Data", | |
"0x6f630fad67cda0ee1fb1f562db3aa53e"); | |
std::cout << "------------------------------------------------------------" << std::endl; | |
std::cout << "Test Cases complete for RFC-2202" << std::endl; | |
std::cout << "------------------------------------------------------------" << std::endl; | |
} | |
bool TestMD5(std::string data, std::string expectedDigestHex, bool showDataOutput=true) | |
{ | |
std::vector<unsigned char> dataBytes; | |
if (data.rfind("0x", 0) == 0) | |
{ | |
dataBytes = HexStringToBytes(data); | |
} | |
else | |
{ | |
std::copy(data.begin(), data.end(), std::back_inserter(dataBytes)); | |
} | |
std::vector<unsigned char> result = RFC_MD5_Digest(dataBytes); | |
std::vector<unsigned char> expected = HexStringToBytes(expectedDigestHex); | |
bool same = true; | |
if (result.size() != expected.size()) | |
{ | |
same = false; | |
} | |
else | |
{ | |
for (unsigned int byte = 0; byte < result.size(); byte++) | |
{ | |
if (result[byte] != expected[byte]) | |
{ | |
same = false; | |
break; | |
} | |
} | |
} | |
if (same) | |
{ | |
std::cout << "Test Passed" << std::endl; | |
if (showDataOutput) { std::cout << "\tData: " << data << std::endl; } | |
std::cout << "\tDigest: " << expectedDigestHex << std::endl; | |
} | |
else | |
{ | |
std::cout << "ERROR Test Failed" << std::endl; | |
if (showDataOutput) { std::cout << "\tData: " << data << std::endl; } | |
std::cout << "\tExpected Digest: " << expectedDigestHex << std::endl; | |
std::cout << "\tActual Digest: "; | |
PrintHexVector(result); | |
std::cout << std::endl; | |
} | |
return same; | |
} | |
void MD5_RFC_TestCases() | |
{ | |
std::cout << "------------------------------------------------------------" << std::endl; | |
std::cout << "Test Cases from RFC-1321 https://tools.ietf.org/html/rfc1321" << std::endl; | |
std::cout << "------------------------------------------------------------" << std::endl; | |
//MD5("") = d41d8cd98f00b204e9800998ecf8427e | |
TestMD5( | |
"", | |
"0xd41d8cd98f00b204e9800998ecf8427e"); | |
//MD5("a") = 0cc175b9c0f1b6a831c399e269772661 | |
TestMD5( | |
"a", | |
"0x0cc175b9c0f1b6a831c399e269772661"); | |
//MD5("abc") = 900150983cd24fb0d6963f7d28e17f72 | |
TestMD5( | |
"abc", | |
"0x900150983cd24fb0d6963f7d28e17f72"); | |
//MD5("message digest") = f96b697d7cb7938d525a2f31aaf161d0 | |
TestMD5( | |
"message digest", | |
"0xf96b697d7cb7938d525a2f31aaf161d0"); | |
//MD5("abcdefghijklmnopqrstuvwxyz") = c3fcd3d76192e4007dfb496cca67e13b | |
TestMD5( | |
"abcdefghijklmnopqrstuvwxyz", | |
"0xc3fcd3d76192e4007dfb496cca67e13b"); | |
//MD5("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789") = | |
//d174ab98d277d9f5a5611c2c9f419d9f | |
TestMD5( | |
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", | |
"0xd174ab98d277d9f5a5611c2c9f419d9f"); | |
//MD5("123456789012345678901234567890123456789012345678901234567890123456 | |
// 78901234567890") = 57edf4a22be3c955ac49da2e2107b67a | |
TestMD5( | |
"12345678901234567890123456789012345678901234567890123456789012345678901234567890", | |
"0x57edf4a22be3c955ac49da2e2107b67a"); | |
std::cout << "------------------------------------------------------------" << std::endl; | |
std::cout << "Test Cases complete for RFC-1321" << std::endl; | |
std::cout << "------------------------------------------------------------" << std::endl; | |
} | |
void MD5_NIST_TestCases() | |
{ | |
std::cout << "------------------------------------------------------------" << std::endl; | |
std::cout << "Test Cases from NIST" << std::endl; | |
std::cout << "https://www.nist.gov/itl/ssd/software-quality-group/nsrl-test-data" << std::endl; | |
std::cout << "------------------------------------------------------------" << std::endl; | |
//A file containing the ASCII string "abc" results in a 128 - bit message digest of 90015098 3CD24FB0 D6963F7D 28E17F72. | |
TestMD5( | |
"abc", | |
"0x900150983CD24FB0D6963F7D28E17F72" | |
); | |
//A file containing the ASCII string "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq" results in a 128 - bit message digest of 8215EF07 96A20BCA AAE116D3 876C664A. | |
TestMD5( | |
"abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq", | |
"0x8215EF0796A20BCAAAE116D3876C664A" | |
); | |
//A file containing the binary - coded form of the ASCII string which consists of 1, 000, 000 repetitions of the character "a" results in a MD5 message digest of 7707D6AE 4E027C70 EEA2A935 C2296F21. | |
TestMD5( | |
std::string(1000000, 'a'), | |
"0x7707D6AE4E027C70EEA2A935C2296F21", | |
false | |
); | |
std::cout << "------------------------------------------------------------" << std::endl; | |
std::cout << "Test Cases from NIST complete" << std::endl; | |
std::cout << "------------------------------------------------------------" << std::endl; | |
} | |
int main() | |
{ | |
HMAC_RFC_TestCases(); | |
MD5_RFC_TestCases(); | |
MD5_NIST_TestCases(); | |
return 0; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
The biggest challenge in figuring this out was how to create a
HCRYPTKEY
for use withCryptCreateHash
from an input key's bytes. This required making a blob with aBLOBHEADER
, size, and key bytes. Further, theCALG_RC2
algorithm type has to be input, even though that isn't actually being used anywhere. Expected inputs such asCALG_HMAC
andCALG_MD5
can't be used for key inputs.