Skip to content

Instantly share code, notes, and snippets.

@teeks99
Created November 17, 2020 17:21
Show Gist options
  • Save teeks99/28d7872e5448be3f52736167dc16833f to your computer and use it in GitHub Desktop.
Save teeks99/28d7872e5448be3f52736167dc16833f to your computer and use it in GitHub Desktop.
HMAC_MD5 and MD5 Digest on Windows
// 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;
}
@teeks99
Copy link
Author

teeks99 commented Nov 17, 2020

The biggest challenge in figuring this out was how to create a HCRYPTKEY for use with CryptCreateHash from an input key's bytes. This required making a blob with a BLOBHEADER, size, and key bytes. Further, the CALG_RC2 algorithm type has to be input, even though that isn't actually being used anywhere. Expected inputs such as CALG_HMAC and CALG_MD5 can't be used for key inputs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment