在 C# 中从 Ascii 文本创建 SHA256 哈希 - 相当于 PHP bin2hex

Creating a SHA256 Hash from Ascii Text in C# - equivalent of PHP bin2hex

我需要在 C# 中根据将传递给支付服务的字符串创建 SHA-256 哈希。我在 PHP 中提供了一些旧的示例代码,并为此编写了 C# 版本 - 不幸的是,生成的散列未被需要它的服务接受,所以看起来好像我在某处犯了一个错误在我的 C# 代码中。

支付服务创建哈希所需的步骤是:

这是示例 PHP 代码:

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

这是我的 C# 代码:

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("-", "");
}

具体来说,我不确定我用来获取 ascii 字节的方法是否与 PHP bin2hex 方法中所做的相同。

编辑 - 问题已解决!

问题已使用多项式解法解决。

对于一些背景信息,这个过程中使用的一般过程是将支付信息发布到支付网关,其中一部分是散列与其他变量的纯文本版本一起发送(除了共享秘密)。还有更多的变量,包括时间戳,可以避免重复哈希的问题。

然后网关服务器重新计算哈希以验证请求。由于这个原因,哈希值必须匹配,我无法更改哈希算法或字符集等。有关此服务的更多信息,该服务来自一家主要的全球银行...

不幸的是,我无法控制 PHP 代码;这不是我的。该片段作为某些 'sample' 代码的一部分发送,我不得不使用 C# 重新创建它。对于为此苦苦挣扎的任何用户,关键部分是使用字符串生成器而不是 BitConverter。

不,那不一样。

PHP 代码获取字符串,将其转换为十六进制表示形式,然后对该字符串进行哈希处理。这还涉及一些转换为字节的中间内部步骤:

  1. 构造字符串。
  2. 将该字符串转换为字节序列,使用配置为默认的任何文本编码(例如 UTF-8、Windows-1252)。
  3. 将该字符串转换为这些字节的十六进制表示形式,作为字符串。 PHP 文档说“一个 ASCII 字符串”,但这仅指使用基本 0-9a-f 字符这一事实。字符串的编码仍然是 PHP 的默认值。
  4. 使用默认文本编码将该字符串转换回字节。
  5. 使用 SHA256 对这些字节进行哈希处理,return将它们作为十六进制字符串。

您所做的是使用 ASCII 编码将输入字符串转换为字节序列,对其进行哈希处理,然后将其转换回十六进制字符串。这会跳过字符串首先转换为十六进制的步骤,并且与字符串编码不匹配,如果输入包含非 ASCII 字符(例如 Unicode),则会产生不同的哈希值。

PHP 代码本身很脆弱,因为它的行为取决于系统配置。由于底层字符表示的固有差异,具有不同语言环境配置的两个系统可能会产生不同的哈希值。例如,字符串“áéíóú€”在 UTF-8 中编码为 c3a1c3a9c3adc3b3c3bae282ac,但在 ISO-8859-1 中编码为 e1e9edf3fa3f,在 Windows-1252 中编码为 e1e9edf3fa80

如果您可以控制 PHP 代码,我强烈建议将其更改为使用单一规范编码,例如 UTF-8。例如:

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

这消除了编码歧义。请注意,在这里使用 ASCII 不是一个好主意——如果商店名称或输入中包含的任何其他字段可能包含重音字符、西里尔字符或 CJK 字符(您应该支持国际化!),那么您生成的哈希值将无法正确代表该字符名称,并可能以意想不到的方式破裂或碰撞。

另一个错误是在您的数字转换中。您告诉 C# 将货币格式化为两位小数,但 PHP 端只是将数字与默认转换为字符串的方式连接起来。您应该确保 PHP 一侧的货币值被编码为具有两位小数的数字,例如15.00 15.

您还应该使用 decimal 在 C# 中存储货币值,而不是 float。由于浮点数在内部表示数字的方式,不能保证准确地存储货币值。 Decimal 保证数字的整数部分将被正确表示。小数部分的存储精度也足以表示货币。

我推荐的另一件事是使用 SHA256.Create() 而不是显式构建 SHA256Managed 对象。这将确保您在平台上使用本机加密实现(如果可用),而不是每次都使用较慢的托管实现。

在 C# 方面,等效项是:

// 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("-", "");
}

然而,这还是坏了。当您使用 BitConverter.ToString 从字节中获取十六进制字符串时,输出使用大写字母(例如 FF 表示 255)。在 PHP 中,bin2hex 使用小写字母。这很重要,因为它会为哈希函数产生不同的输入。

更好的解决方案是将那些 BitConverter 调用替换为更直接的十六进制转换,以便您可以直接控制格式:

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

这应该在 PHP 和 C# 端匹配。

顺便说一句,我强烈建议从安全的角度重新考虑这种哈希方案。

第一个明显的漏洞是具有相同名称但具有数字后缀的商店可能会产生事务哈希冲突,例如名为 Shoe 的商店进行 54.00 美元的交易与名为 Shoe5 的商店进行 4.00 美元的交易具有相同的哈希值,因为两者都会产生 Shoe54.00usd。您需要使用不能出现在输入字符串中的字符来分隔字段,以避免出现这种情况。理想情况下,这涉及以规范的结构化格式(例如 BSON 或 Bencode)对字段进行编码,但这里的粗略方法可能只是用制表符分隔字段。

此外,通过将秘密信息和非秘密信息连接在一起来构建消息身份验证代码对于 Merkle-Damgård 构造哈希函数(如 MD5、SHA1 和 SHA256)来说是有问题的。这些哈希函数的安全属性未针对此用例进行调整,您可能会成为长度扩展攻击的受害者。相反,您应该考虑使用专为此用例设计的 HMAC。您可以将 HMAC 想象成键控哈希。唯一的区别是共享秘密被用作密钥,而不是连接到被散列的数据。

在 PHP 中你可以使用 hash_hmac for this. In C# you can use HMACSHA256.

将所有这些放在一起,您会得到:

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

// 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;
}

从安全的角度来看,这仍然不是 100% 理想的,因为在不同时间进行的相同金额的两笔交易将具有相同的哈希值,但你说这些字段是示例,所以我不会在那里进一步演示.可以说你应该在那里有一些唯一的交易标识符。另一个潜在的问题是,您将在 C# 端的堆上留下包含散列的字符串,并且这些字符串是敏感的,但是如果不非常小心地使用 SecureString ,您将无能为力,这非常复杂主题。