Calculating HMACSHA256 using c# to match payment provider example
Asked Answered
V

7

76

For a payment provider, I need to calculate a hash-based message authentication code, using HMAC-SHA256. That is causing me quite a bit of trouble.

The payment provider gives two examples of orrectly calculated authentication code in pseudo-code. All keys are in hex.

Method 1

key = 57617b5d2349434b34734345635073433835777e2d244c31715535255a366773755a4d70532a5879793238235f707c4f7865753f3f446e633a21575643303f66
message = "amount=100&currency=EUR"
MAC = HMAC-SHA256( hexDecode(key), message )
result = b436e3e86cb3800b3864aeecc8d06c126f005e7645803461717a8e4b2de3a905

Method 2

message = "amount=100&currency=EUR"
Ki = 61574d6b157f757d02457573556645750e0341481b127a07476303136c005145436c7b46651c6e4f4f040e1569464a794e534309097258550c17616075060950
Ko = 0b3d27017f151f17682f1f193f0c2f1f64692b227178106d2d096979066a3b2f2906112c0f760425256e647f032c2013243929636318323f667d0b0a1f6c633a
MAC = SHA256( hexDecode(Ko) + SHA256( hexDecode(Ki) + message ) )
result = b436e3e86cb3800b3864aeecc8d06c126f005e7645803461717a8e4b2de3a905

I tried to write the code to do this, after doing some research, but I keep coming up with different results.

private static void Main(string[] args)
    {
        var key = "57617b5d2349434b34734345635073433835777e2d244c31715535255a366773755a4d70532a5879793238235f707c4f7865753f3f446e633a21575643303f66";
        var ki = "61574d6b157f757d02457573556645750e0341481b127a07476303136c005145436c7b46651c6e4f4f040e1569464a794e534309097258550c17616075060950";
        var ko = "0b3d27017f151f17682f1f193f0c2f1f64692b227178106d2d096979066a3b2f2906112c0f760425256e647f032c2013243929636318323f667d0b0a1f6c633a";
        var mm = "amount=100&currency=EUR";

        var result1 = CalcHMACSHA256Hash(HexDecode(key), mm);

        var result2 = CalcSha256Hash(string.Format("{0}{1}", HexDecode(ko), CalcSha256Hash(HexDecode(ki) + mm)));

        Console.WriteLine("Expected: b436e3e86cb3800b3864aeecc8d06c126f005e7645803461717a8e4b2de3a905");
        Console.WriteLine("Actual 1: " + result1);
        Console.WriteLine("Actual 2: " + result2);

        Console.WriteLine("------------------------------");
        Console.ReadKey();

    }

    private static string HexDecode(string hex)
    {
        var sb = new StringBuilder();
        for (int i = 0; i <= hex.Length - 2; i += 2)
        {
            sb.Append(Convert.ToString(Convert.ToChar(Int32.Parse(hex.Substring(i, 2), System.Globalization.NumberStyles.HexNumber))));
        }
        return sb.ToString();
    }

    private static string CalcHMACSHA256Hash(string plaintext, string salt)
    {
        string result = "";
        var enc = Encoding.Default;
        byte[]
        baText2BeHashed = enc.GetBytes(plaintext),
        baSalt = enc.GetBytes(salt);
        System.Security.Cryptography.HMACSHA256 hasher = new HMACSHA256(baSalt);
        byte[] baHashedText = hasher.ComputeHash(baText2BeHashed);
        result = string.Join("", baHashedText.ToList().Select(b => b.ToString("x2")).ToArray());
        return result;
    }


    public static string CalcSha256Hash(string input)
    {
        SHA256 sha256 = new SHA256Managed();
        byte[] sha256Bytes = Encoding.Default.GetBytes(input);
        byte[] cryString = sha256.ComputeHash(sha256Bytes);
        string sha256Str = string.Empty;
        for (int i = 0; i < cryString.Length; i++)
        {
            sha256Str += cryString[i].ToString("x2");
        }
        return sha256Str;
    }

And this is the result I get:

Expected: b436e3e86cb3800b3864aeecc8d06c126f005e7645803461717a8e4b2de3a905
Actual 1: 421ce16f2036bb9f2a3770c16f01e9220f0232d45580584ca41768fd16c15fe6
Actual 2: 290f14398bf8c0959dfc963e2fd9c377534c6fec1983025d2ab192382f132b92

So with none of the two methods, I can get the result the provider example wants.

What am I missing here? Is it encoding? Is my hexDecode screwed up?

Test tool from payment provider: http://tech.dibs.dk/dibs_api/other_features/hmac_tool/

PHP sample code: http://tech.dibspayment.com/dibs_api/other_features/mac_calculation/

Venereal answered 29/8, 2012 at 19:26 Comment(3)
You HexDecode is screwed up indeed. I'd have expected a function that returns a byte[]. You are returning a string that has be stuffed with binary data.Anticosti
To be very precise the provider of the example should also specify the character encoding of the message itself, although ASCII would be a pretty good bet. Don't forget, almost all modern cryptographic primitives expect bytes. One byte wrong = fail.Spellman
You don't have to .ToList() the baHashedText before calling .Select(b => b.ToString("x2")), its unnecessary. You also don't need to .ToArray() afterwards. e.g.: string.Join(string.Empty, baHashedText.Select(b => b.ToString("x2")))Terrapin
B
203

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 see A || 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):

  1. Create an empty buffer byte[] innerData the length of innerKey.Length + message.Length (again assuming byte arrays)
  2. Copy the innerKey and the message into the byte[] innerData
  3. Compute SHA256 of innerData and store it in byte[] innerHash
  4. Create an empty buffer byte[] data the length of outerKey.Length + innerHash.Length
  5. Copy the outerKey and innerHash (from step #3)
  6. Compute the final hash of data and store it in byte[] 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> with byte[]). 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&currency=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

Buettner answered 3/9, 2012 at 20:33 Comment(8)
Awesome answer. I am giving you a bounty for that (in 24 hours, there is some sort of delay in place).Venereal
@Jaxrtech can you attach a license to the code you published in pastebin? Say MIT or CC0?Ginnifer
@Ginnifer as always IANAL, and technically HexDecode(string hex) is from @Fromma being only 5 lines if you really want to be cutting hairs, so I don't really know if i can technically "re-license" the entire thing since its not entirely mine. But I'm sure plenty of people have already outrightly just copy and pasted the entire thing which is what I would imagine SO is partially for and what people intend for. Otherwise, I would feel just to stick the MIT license on top of thing.Buettner
@Ginnifer (cont'd) Of course at the bottom of this page, you can see that "user contributions licensed under cc by-sa 3.0 with attribution required" but of course this seems to be in some stupid horrendous mess (meta.stackexchange.com/questions/12527/…). In all honesty though, I seriously doubt someone on SO is going to want to pursue a lawsuit over 5 lines of code they posted on SO against who knows who in whatever jurisdiction of the world they may be situated in.Buettner
@JoshBowden Thanks for this solution. I am getting below error in hex string -> byte[] function: "Input string was not in a correct format." StackTrace: " at System.Number.ThrowOverflowOrFormatException(ParsingStatus status, TypeCode type)\r\n at System.Byte.Parse(String s, NumberStyles style)" The line byte.Parse(hex.Substring(i * 2, 2), NumberStyles.HexNumber) is giving this exception. For some hex substring values it is working fine but for some values it is failing.Lycopodium
In .Net 5 and .Net 6 there is available method for converting hex to byte array csharp public static byte[] FromHexString (string s); learn.microsoft.com/en-us/dotnet/api/…Maturate
@PrasadKaiche my guess is that your hex string either has illegal characters or the length of the hex was not of an even lengthBuettner
One example uses a single key, but the other example uses two keys. Both produce the same hash output, so how are the keys derived to produce the same result?Dreiser
U
67

Here's a string extension method for getting a fairly standard HMAC SHA 256 token for a given string:

usage:

myMessageString.HmacSha256Digest(mySecret)

string extension method:

public static string HmacSha256Digest(this string message, string secret)
{
    ASCIIEncoding encoding = new ASCIIEncoding();
    byte[] keyBytes = encoding.GetBytes(secret);
    byte[] messageBytes = encoding.GetBytes(message);
    System.Security.Cryptography.HMACSHA256 cryptographer = new System.Security.Cryptography.HMACSHA256(keyBytes);

    byte[] bytes = cryptographer.ComputeHash(messageBytes);

    return BitConverter.ToString(bytes).Replace("-", "").ToLower();
}
Underlayer answered 11/11, 2016 at 1:43 Comment(4)
The accepted answer is beautifully in-depth. But to be quite honest, I felt that it was a lot to digest. I upvoted this because it's: 1) correct, 2) easy to consume intellectually, 3) just so darn simple. Nicely put @Chris HalcrowUnsought
I tried using this, and while it is more concise, it doesn't work for the example 1 provided above. The byte[] keyBytes = encoding.GetBytes(secret); doesn't work for the HEX key provided in the example. It needs to be byte[] keyBytes = HexDecode(secret); where HexDecode is shown in the example answer, and originally, in the detailed answer provided by @bobince.Ezaria
Thank you Chris. I tried the most voted above solution but it is failing in hex string -> byte[] function. Tried your solution and got it working. This solution is short and concise. Thanks again.Lycopodium
One of the classic examples, why Microsoft's .NET documentation is hated so much. Couldn't understand a thing in their documentation. Your answer saved my day. Thank You.Shirtwaist
L
8

You can use this method for HMACSHA256.

string key = "your key";
string message = "your message";
System.Text.ASCIIEncoding encoding = new System.Text.ASCIIEncoding();
byte[] keyByte = encoding.GetBytes(key);

HMACSHA256 hmacsha256 = new HMACSHA256(keyByte);

byte[] messageBytes = encoding.GetBytes(message);
byte[] hashmessage = hmacsha256.ComputeHash(messageBytes);
return ByteToString(hashmessage);

Here is the ByteToString method:

public static string ByteToString(byte[] buff)
    {
        string sbinary = "";

        for (int i = 0; i < buff.Length; i++)
        {
            sbinary += buff[i].ToString("X2"); // hex format
        }
        return (sbinary);
    }
Lilongwe answered 28/5, 2015 at 11:4 Comment(0)
F
0

A SHA hash is calculated on a sequence of bytes. Bytes are a profoundly different datatype to characters. You should not use character Strings to store binary data such as hashes.

sb.Append(Convert.ToString(Convert.ToChar(Int32.Parse(hex.Substring(i, 2)...

This creates a character string by reading each encoded byte and turning into a character of the same Unicode code point number. This is equivalent to decoding the bytes 0-255 using the ISO-8859-1 (Latin1) encoding, due to that encoding's property of matching the first 256 code points in Unicode.

var enc = Encoding.Default; [...] baSalt = enc.GetBytes(salt);

byte[] sha256Bytes = Encoding.Default.GetBytes(input);

These both convert the characters back to bytes using the system default encoding. This encoding varies between installs, but it will never be ISO-8859-1 - even the similar Western European code page 1252 has different characters in the range 0x80-0x9F.

Consequently the byte array you are using doesn't contain the bytes implied by the example hex sequences. A cheap fix would be to use Encoding.GetEncoding("ISO-8859-1") instead of the default encoding, but really you should be using a bytes array to store data in the first place instead of a String, eg:

byte[] key= new byte[] { 0x57, 0x61, 0x7b, 0x5d, 0x23, 0x49, ... };

and pass that directly into ComputeHash.

If you must initialise data from a hex string, parse it directly into a byte array, eg:

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;
}
Fromma answered 29/8, 2012 at 23:1 Comment(0)
S
0

I realize the question is answered, but I am posting this in case others need it. Here is a snippet of code created by the payment provider (DIBS):

    /**
    * calculateMac
    * Calculates the MAC key from a Dictionary<string, string> and a secret key
    * @param params_dict The Dictionary<string, string> object containing all keys and their values for MAC calculation
    * @param K_hexEnc String containing the hex encoded secret key from DIBS Admin
    * @return String containig the hex encoded MAC key calculated
    **/
    public static string calculateMac(Dictionary<string, string> paramsDict, string kHexEnc)
    {
        //Create the message for MAC calculation sorted by the key
        var keys = paramsDict.Keys.ToList();
        keys.Sort();
        var msg = "";
        foreach (var key in keys)
        {
            if (key != keys[0]) msg += "&";
            msg += key + "=" + paramsDict[key];
        }

        //Decoding the secret Hex encoded key and getting the bytes for MAC calculation
        var kBytes = new byte[kHexEnc.Length / 2];
        for (var i = 0; i < kBytes.Length; i++)
        {
            kBytes[i] = byte.Parse(kHexEnc.Substring(i * 2, 2), NumberStyles.HexNumber);
        }

        //Getting bytes from message
        var msgBytes = Encoding.Default.GetBytes(msg);

        //Calculate MAC key
        var hash = new HMACSHA256(kBytes);
        var macBytes = hash.ComputeHash(msgBytes);
        var mac = BitConverter.ToString(macBytes).Replace("-", "").ToLower();

        return mac;
    }

http://tech.dibspayment.com/DX/Hosted/HMAC

Salimeter answered 15/7, 2015 at 7:39 Comment(2)
During the string conversion at the end, why do we need to remove "-" ?Urias
I am actually not sure. I got this piece of code from my payment gateway provider.Salimeter
B
0

Thanks you saved my time.

request.Method = "GET";
string signature = "";
string strtime = DateTime.UtcNow.ToString("yyyy-MM-ddTHH\\:mm\\:ssZ");

string secret = "xxxx";

string message = "sellerid:email:" + strtime; 

var encoding = new System.Text.ASCIIEncoding(); 

byte[] keyByte = encoding.GetBytes(secret);

byte[] messageBytes = encoding.GetBytes(message);
using (var hmacsha256 = new HMACSHA256(keyByte))
{
var hash = new HMACSHA256(keyByte);
byte[] signature1 = hash.ComputeHash(messageBytes);
signature = BitConverter.ToString(signature1).Replace("-", "").ToLower();
}

request.Headers.Add("authorization", "HMAC-SHA256" + " " + 
"[email protected],timestamp=" + strtime + ",signature=" + signature);
HttpWebResponse response = request.GetResponse() as HttpWebResponse;
Banbury answered 6/10, 2016 at 14:9 Comment(0)
S
-1
    private static string GenerateSignature(string data, string signatureKey)
    {
        var keyByte = Encoding.UTF8.GetBytes(signatureKey);
        using (var hmacsha256 = new HMACSHA256(keyByte))
        {
            hmacsha256.ComputeHash(Encoding.UTF8.GetBytes(data));
            return hmacsha256.Hash.Aggregate("", (current, t) => current + t.ToString("X2")).ToLower();
        }
    }
Snowonthemountain answered 23/2, 2021 at 18:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.