Home > Back-end >  Creating a SHA256 Hash from Ascii Text in C# - equivalent of PHP bin2hex
Creating a SHA256 Hash from Ascii Text in C# - equivalent of PHP bin2hex

Time:11-23

I need to create a SHA-256 hash in C# from a string which will be passed to a payment service. I have some old sample code that has been provided in PHP and have written a C# version of this - unfortunately the generated hash is not being accepted by the service that requires it so it looks as though I have made a mistake somewhere in my C# code.

The steps the payment service requires to create the hash is:

  • Collect selected parameters and join into one string
  • Convert the created string to its ascii hexadecimal representation
  • Pass the ascii hex representation to the SHA-256 algorithm.

This is the sample PHP code:

$stringToHash = $storeName.$chargetotal.$currency.$sharedsecret;  // These are just supplied variables
$ascii = bin2hex($stringToHash);
return hash("sha256", $ascii);

This my C# code:

var hashString = new StringBuilder();

// Append the supplied  variables
hashString.Append(storeName);
hashString.Append(chargeTotal.ToString("f2"));
hashString.Append(currency);
hashString.Append(sharedSecret);

var bytes = Encoding.ASCII.GetBytes(hashString.ToString());

using (SHA256 shaM = new SHA256Managed())
{
    var hash = shaM.ComputeHash(bytes);
    return BitConverter.ToString(hash).Replace("-", "");
}

Specifically, I'm not sure if the method I've used to get the ascii bytes is the same as it what is being done in the PHP bin2hex method.

EDIT - Problem Solved!

Problem solved using Polynomial's solution.

For some background info, the general process this is used in is that payment information is POSTed off to a payment gateway and one part of this is the hash is sent alongside plain text versions of the other variables (apart from the shared secret). There are a lot more variables including a timestamp which would avoid the problem of duplicate hashes.

The gateway server then recomputes the hash to verify the request. For this reason the hashes have to match and I can't change the hashing algorithm or character set etc. For further info this service is from a major global bank...

Unfortunately I don't have any control over the PHP code; it isn't mine. The snippet was sent as part of some 'sample' code and I had to recreate this using C#. For any users struggling with this, the key part was using the string builder instead of the BitConverter.

CodePudding user response:

No, that isn't the same.

The PHP code takes the string, converts it to a hexadecimal representation, then hashes that string. This also involves some intermediate internal steps with conversion to bytes:

  1. Construct the string.
  2. Convert that string to a sequence of bytes, using whichever text encoding (e.g. UTF-8, Windows-1252) is configured to be the default.
  3. Convert that string to the hexadecimal representation of those bytes, as a string. The PHP documentation says "an ASCII string", but this refers only to the fact that basic 0-9 and a-f characters are used. The string's encoding is still whatever PHP's default is.
  4. Convert that string back to bytes, using the default text encoding.
  5. Hash those bytes with SHA256 and return them as a hexadecimal string.

What you've done is convert the input string to a sequence of bytes using ASCII encoding, hash that, then convert that back to a hexadecimal string. This skips the step where the string is first turned into hexadecimal, and also doesn't match the string encoding and will produce different hashes if the inputs contain non-ASCII characters (e.g. Unicode).

The PHP code itself is fragile, since its behaviour is dependent on the system configuration. Two systems with differing locale configurations may produce different hashes due to the inherent differences in underlying character representations. For example, the string "áéíóú€" encodes to c3a1c3a9c3adc3b3c3bae282ac in UTF-8, but e1e9edf3fa3f in ISO-8859-1, and e1e9edf3fa80 in Windows-1252.

If you have control over the PHP code, I strongly recommend altering it to use a single canonical encoding such as UTF-8. For example:

$token = $storeName . $chargetotal . $currency . $sharedsecret;
$utf8token = mb_convert_encoding($token, 'UTF-8');
$hextoken = bin2hex($utf8token);
return hash("sha256", $hextoken);

This removes the encoding ambiguity. Note that using ASCII is a poor idea here - if the store name or any other field included in the input may contain accented, Cyrillic, or CJK characters (you should support internationalisation!) then the hashes you generate will not be properly representative of that name, and may break or collide in unexpected ways.

Another bug is in your numeric conversion. You're telling C# to format the currency with two decimal places, but the PHP side just concatenates the number with the default conversion to string. You should ensure that the currency value on the PHP side is encoded as a number with two decimal places, e.g. 15.00 for 15.

You should also use decimal to store currency values in C#, not float. Floating point numbers are not guaranteed to store currency values accurately due to the way they internally represent numbers. Decimal guarantees that the integer portion of the number will be properly represented. The fractional portion is also stored with more than enough precision to represent currency.

Another thing I'd recommend is using SHA256.Create() instead of explicitly constructing a SHA256Managed object. This will ensure that you utilise the native cryptographic implementation on the platform if it is available, rather than using the slower managed implementation every time.

On the C# side the equivalent is then:

// build the string
var tokenString = new StringBuilder();
tokenString.Append(storeName);
tokenString.Append(chargeTotal.ToString("0.00"));
tokenString.Append(currency);
tokenString.Append(sharedSecret);

// convert to bytes using UTF-8 encoding
var tokenBytes = Encoding.UTF8.GetBytes(tokenString);

// convert those bytes to a hexadecimal string
var tokenBytesHex = BitConverter.ToString(tokenBytes).Replace("-", "");

// convert that string back to bytes (UTF-8 used here since that is the default on PHP, but ASCII will work too)
var tokenBytesHexBytes = Encoding.UTF8.GetBytes(tokenString);

// hash those bytes
using (SHA256 sha256 = SHA256.Create())
{
    var hash = sha256.ComputeHash(tokenBytesHexBytes);
    return BitConverter.ToString(hash).Replace("-", "");
}

However, this is still broken. When you use BitConverter.ToString to get a hex string from the bytes, the output uses capital letters (e.g. FF for 255). In PHP, bin2hex uses lowercase letters. This matters because it produces a different input to the hash function.

A better solution is to replace those BitConverter calls to more direct hex conversions so you have direct control over the format:

var sb = new StringBuilder();
foreach (byte b in bytes)
    sb.AppendFormat("{0:x2}", b);
var hex = sb.ToString();

This should then match on both the PHP and C# side.

As an aside, I strongly recommend rethinking this hashing scheme from a security standpoint.

The first obvious vulnerability is that a store with the same name but with a numeric suffix can produce transaction hash collisions, e.g. a store called Shoe making a $54.00 transaction has the same hash as a store called Shoe5 making a $4.00 transaction, since both will produce Shoe54.00usd. You need to separate the fields using a character that cannot be present in the input strings in order to avoid this. Ideally this involves encoding the fields in a canonical structured format such as BSON or Bencode, but a crude approach here could just be to separate fields with a tab character.

Additionally, building message authentication codes by concatenating secret and non-secret information together is problematic for Merkle-Damgård construction hash functions like MD5, SHA1, and SHA256. The security properties of these hash functions are not tuned for this use-case, and you may fall victim to length extension attacks. Instead, you should consider using a HMAC, which is specifically designed for this use case. You can think of a HMAC like a keyed hash. The only difference is that the shared secret is used as a key, rather than concatenated to the data being hashed.

In PHP you can use hash_hmac for this. In C# you can use HMACSHA256.

Putting that all together, you get:

$token = $storeName . "\t" . $chargetotal . "\t" . $currency;
$utf8token = mb_convert_encoding($token, 'UTF-8');
$hextoken = bin2hex($utf8token);
return hash_hmac("sha256", $hextoken, $sharedsecret);

and

// build the string
var tokenString = new StringBuilder();
tokenString.Append(storeName);
tokenString.Append("\t");
tokenString.Append(chargeTotal.ToString("0.00"));
tokenString.Append("\t");
tokenString.Append(currency);

// convert to bytes using UTF-8 encoding
var tokenBytes = Encoding.UTF8.GetBytes(tokenString);

// convert those bytes to a hexadecimal string
var sb = new StringBuilder();
for (byte b in tokenBytes)
    sb.AppendFormat("{0:x2}", b);
var tokenBytesHex = sb.ToString();

// convert that string back to bytes (UTF-8 used here since that is the default on PHP, but ASCII will work too)
var tokenBytesHexBytes = Encoding.UTF8.GetBytes(tokenString);

// hash those bytes using HMAC-SHA256
byte[] sharedSecretBytes = Encoding.UTF8.GetBytes(sharedsecret);
using (HMACSHA256 hmac_sha256 = new HMACSHA256(sharedSecretBytes))
{
    var hashBytes = hmac_sha256.ComputeHash(tokenBytesHexBytes);
    sb = new StringBuilder();
    for (byte b in hashBytes)
        sb.AppendFormat("{0:x2}", b);
    var hashString = sb.ToString();
    return hashString;
}

This still isn't 100% ideal from a security perspective, since two transactions made at different times for the same amount will have the same hash, but you said the fields were examples so I won't demonstrate further there. Suffice to say you should have some unique transaction identifier in there. Another potential issue is that you're leaving strings containing hashes on the heap on the C# side, and those strings are sensitive, but there's not much you can do about that without using SecureString very carefully, and that's quite an involved topic.

  • Related