Edit: You likely are looking for a quick and simple way to do HMAC-SHA256 and not get into the finer details. The original question asks of those finer details which are explained further below.
I want to perform a HMAC-SHA256 on a byte[]
message input
using System.Security.Cryptography;
...
private static byte[] HashHMAC(byte[] key, byte[] message)
{
var hash = new HMACSHA256(key);
return hash.ComputeHash(message);
}
I want to perform HMAC-SHA256 but I have a hex string input
In .NET 5 and above, use System.Convert.FromHexString
like so, (thanks @proximab). If you’re on pre-.NET 5, scroll to “Helper functions” which has alternative solutions.
using System;
using System.Security.Cryptography;
...
private static byte[] HashHMACHex(string keyHex, string messageHex)
{
var key = Convert.FromHexString(hexKey);
var message = Convert.FromHexString(messageHex);
var hash = new HMACSHA256(key);
return hash.ComputeHash(message);
}
I’m using a strange API service that sort of does HMAC, but it’s something custom
Continue reading. You likely want to use “Method 2” below as a reference point and adjust it to however your service wants you to implement HMAC for message anti-tampering.
How HMAC-SHA256 Works (should you need to know how…)
Here we will compute an HMAC-SHA256 manually (this answers “Method 2” from the original question).
Assume outerKey
, innerKey
, and message
are already byte arrays, we perform the following:
Notation: Assume
A + B
concatenates byte array A and B. You may
alternatively seeA || B
notation used in more academic settings.
HMAC = SHA256( outerKey + SHA256( innerKey + message ) )
. . `------------------´ . .
\ \ `innerData` / /
\ `------------------------´ /
\ `innerHash` /
`----------------------------------´
`data`
So the code can be broken down into these steps (using the above as a guide):
- Create an empty buffer
byte[] innerData
the length ofinnerKey.Length + message.Length
(again assuming byte arrays) - Copy the
innerKey
and themessage
into thebyte[] innerData
- Compute SHA256 of
innerData
and store it inbyte[] innerHash
- Create an empty buffer
byte[] data
the length ofouterKey.Length + innerHash.Length
- Copy the
outerKey
andinnerHash
(from step #3) - Compute the final hash of
data
and store it inbyte[] result
and return it.
To do the byte copying I’m using the Buffer.BlockCopy()
function since it apparently faster than some other ways (source).
n.b. There is likely (read: most certainly) a better way to do this using the the new
ReadOnlySpan<T>
API.
We can translate those steps into the following:
using System;
using System.Security.Cryptography;
...
private static byte[] HashSHA(byte[] innerKey, byte[] outerKey, byte[] message)
{
var hash = new SHA256Managed();
// Compute the hash for the inner data first
byte[] innerData = new byte[innerKey.Length + message.Length];
Buffer.BlockCopy(innerKey, 0, innerData, 0, innerKey.Length);
Buffer.BlockCopy(message, 0, innerData, innerKey.Length, message.Length);
byte[] innerHash = hash.ComputeHash(innerData);
// Compute the entire hash
byte[] data = new byte[outerKey.Length + innerHash.Length];
Buffer.BlockCopy(outerKey, 0, data, 0, outerKey.Length);
Buffer.BlockCopy(innerHash, 0, data, outerKey.Length, innerHash.Length);
byte[] result = hash.ComputeHash(data);
return result;
}
Helper functions
string
-> byte[]
You have plain ASCII or UTF8 text, but need it to be a byte[]
.
Use ASCIIEncoding
or UTF8Encoding
or whichever exotic encoding you’re using.
private static byte[] StringEncode(string text)
{
var encoding = new System.Text.ASCIIEncoding();
return encoding.GetBytes(text);
}
byte[]
-> hex string
You have a byte[]
, but you need it to be a hex string
.
private static string HashEncode(byte[] hash)
{
return BitConverter.ToString(hash).Replace("-", "").ToLower();
}
hex string
-> byte[]
You have a hex string, but you need it to be a
byte[]`.
.NET 5 and above
private static byte[] HexDecode(string hex) =>
System.Convert.FromHexString(hex);
Before .NET 5 (thanks @bobince)
private static byte[] HexDecode(string hex)
{
var bytes = new byte[hex.Length / 2];
for (int i = 0; i < bytes.Length; i++)
{
bytes[i] = byte.Parse(hex.Substring(i * 2, 2), NumberStyles.HexNumber);
}
return bytes;
}
n.b. If you need a performance tuned version on .NET Framework 4.x, you can alternatively backport the .NET 5+ version (by replacing
ReadOnlySpan<byte>
withbyte[]
). It uses proper lookup tables and conscious about hot-code paths. You can reference the .NET 5 (MIT licensed)System.Convert
code on Github.
For completeness, here are the final methods answering the question using both “Method 1” and “Method 2”
“Method 1” (using .NET libraries)
private static string HashHMACHex(string keyHex, string message)
{
byte[] hash = HashHMAC(HexDecode(keyHex), StringEncode(message));
return HashEncode(hash);
}
“Method 2” (manually computed)
private static string HashSHAHex(string innerKeyHex, string outerKeyHex, string message)
{
byte[] hash = HashSHA(HexDecode(innerKeyHex), HexDecode(outerKeyHex), StringEncode(message));
return HashEncode(hash);
}
We can perform a quick sanity check with a console app:
static void Main(string[] args)
{
string message = "amount=100¤cy=EUR";
string expectedHex = "b436e3e86cb3800b3864aeecc8d06c126f005e7645803461717a8e4b2de3a905";
Console.WriteLine("Expected: " + expectedHex);
// Test out the HMAC hash method
string key = "57617b5d2349434b34734345635073433835777e2d244c31715535255a366773755a4d70532a5879793238235f707c4f7865753f3f446e633a21575643303f66";
string hashHMACHex = HashHMACHex(key, message);
Console.WriteLine("Method 1: " + hashHMACHex);
// Test out the SHA hash method
string innerKey = "61574d6b157f757d02457573556645750e0341481b127a07476303136c005145436c7b46651c6e4f4f040e1569464a794e534309097258550c17616075060950";
string outerKey = "0b3d27017f151f17682f1f193f0c2f1f64692b227178106d2d096979066a3b2f2906112c0f760425256e647f032c2013243929636318323f667d0b0a1f6c633a";
string hashSHAHex = HashSHAHex(innerKey, outerKey, message);
Console.WriteLine("Method 2: " + hashSHAHex);
}
You should have all the hashes line up correctly:
Expected: b436e3e86cb3800b3864aeecc8d06c126f005e7645803461717a8e4b2de3a905
Method 1: b436e3e86cb3800b3864aeecc8d06c126f005e7645803461717a8e4b2de3a905
Method 2: b436e3e86cb3800b3864aeecc8d06c126f005e7645803461717a8e4b2de3a905
The original code for this answer can be accessed at:
http://pastebin.com/xAAuZrJX