Java Mac HMAC 与 C++ OpenSSL hmac

Java Mac HMAC vs C++ OpenSSL hmac

这将是一个很长的问题,但我有一个非常奇怪的错误。我在 C++ 中使用 OpenSSL 来计算 HMAC,并将它们与使用 javax.crypto.Mac 的模拟实现进行比较。对于某些密钥,HMAC 计算是正确的,而对于其他密钥,HMAC 存在差异。我相信当键变大时会出现问题。详情如下。

这是最重要的 C++ 代码:

void computeHMAC(std::string message, std::string key){
    unsigned int digestLength = 20;
    HMAC_CTX hmac_ctx_;
    BIGNUM* key_ = BN_new();;

    BN_hex2bn(&key_, key); 

    unsigned char convertedKey[BN_num_bytes(key_)];
    BIGNUM* bn = BN_new();

    HMAC_CTX_init(&hmac_ctx_);

    BN_bn2bin(bn, convertedKey);
    int length = BN_bn2bin(key_, convertedKey);

    HMAC_Init_ex(&hmac_ctx_, convertedKey, length, EVP_sha1(), NULL);

/*Calc HMAC */
    std::transform( message.begin(), message.end(), message.begin(), ::tolower);
    unsigned char digest[digestLength];

    HMAC_Update(&hmac_ctx_, reinterpret_cast<const unsigned char*>(message.c_str()),
      message.length());
    HMAC_Final(&hmac_ctx_, digest, &digestLength);
    char mdString[40];
    for(unsigned int i = 0; i < 20; ++i){
        sprintf(&mdString[i*2], "%02x", (unsigned int)digest[i]);
    }
     std::cout << "\n\nMSG:\n" << message << "\nKEY:\n" + std::string(BN_bn2hex(key_)) + "\nHMAC\n" + std::string(mdString) + "\n\n";
}

java 测试如下所示:

public String calculateKey(String msg, String key) throws Exception{

    HMAC = Mac.getInstance("HmacSHA1");

    BigInteger k = new BigInteger(key, 16);

    HMAC.init(new SecretKeySpec(k.toByteArray(), "HmacSHA1"));

    msg = msg.toLowerCase();
    HMAC.update(msg.getBytes());
    byte[] digest = HMAC.doFinal();

    System.out.println("Key:\n" + k.toString(16) + "\n");
    System.out.println("HMAC:\n" + DatatypeConverter.printHexBinary(digest).toLowerCase() + "\n");

    return DatatypeConverter.printHexBinary(digest).toLowerCase();
}

一些使用不同键的测试运行(所有字符串都被解释为十六进制):


关键1: 736A66B29072C49AB6DC93BB2BA41A53E169D14621872B0345F01EBBF117FCE48EEEA2409CFC1BD92B0428BA0A34092E3117BEB4A8A14F03391C661994863DAC1A75ED437C1394DA0741B16740D018CA243A800DA25311FDFB9CA4361743E8511E220B79C2A3483FCC29C7A54F1EB804481B2DC87E54A3A7D8A94253A60AC77FA4584A525EDC42BF82AE2A1FD6E3746F626E0AFB211F6984367B34C954B0E08E3F612590EFB8396ECD9AE77F15D5222A6DB106E8325C3ABEA54BB59E060F9EA0

消息: 测试

Hmac OpenSSL: b37f79df52afdbbc4282d3146f9fe7a254dd23b3

Hmac Java Mac: b37f79df52afdbbc4282d3146f9fe7a254dd23b3


Key 2: 636A66B29072C49AB6DC93BB2BA41A53E169D14621872B0345F01EBBF117FCE48EEEA2409CFC1BD92B0428BA0A34092E3117BEB4A8A14F03391C661994863DAC1A75ED437C1394DA0741B16740D018CA243A800DA25311FDFB9CA4361743E8511E220B79C2A3483FCC29C7A54F1EB804481B2DC87E54A3A7D8A94253A60AC77FA4584A525EDC42BF82AE2A1FD6E3746F626E0AFB211F6984367B34C954B0E08E3F612590EFB8396ECD9AE77F15D5222A6DB106E8325C3ABEA54BB59E060F9EA0

消息: 测试

Hmac OpenSSL: bac64a905fa6ae3f7bf5131be06ca037b3b498d7

Hmac Java Mac: bac64a905fa6ae3f7bf5131be06ca037b3b498d7


Key 3: 836A66B29072C49AB6DC93BB2BA41A53E169D14621872B0345F01EBBF117FCE48EEEA2409CFC1BD92B0428BA0A34092E3117BEB4A8A14F03391C661994863DAC1A75ED437C1394DA0741B16740D018CA243A800DA25311FDFB9CA4361743E8511E220B79C2A3483FCC29C7A54F1EB804481B2DC87E54A3A7D8A94253A60AC77FA4584A525EDC42BF82AE2A1FD6E3746F626E0AFB211F6984367B34C954B0E08E3F612590EFB8396ECD9AE77F15D5222A6DB106E8325C3ABEA54BB59E060F9EA0

消息: 测试

Hmac OpenSSL: c189c637317b67cee04361e78c3ef576c3530aa7

Hmac Java Mac: 472d734762c264bea19b043094ad0416d1b2cd9c

如数据所示,当密钥变大时,就会发生错误。如果不知道哪个实现有问题。我也尝试过使用更大的键和更小的键。我还没有确定确切的阈值。谁能发现问题?有没有人能够通过使用不同的软件进行模拟来告诉我在最后一个案例中哪个 HMAC 不正确或者谁能告诉我我可以使用哪个第三实现来检查我的?

亲切的问候,

罗尔风暴

当您将十六进制字符串转换为 Java 中的 BigInt 时,它假定数字为正数(除非字符串包含 - 符号)。

但它的内部表示是二进制补码。表示一位用于符号。

如果您转换的值以 007F 之间的十六进制开头,这不是问题。它可以直接转换字节,因为最左边的位是零,这意味着数字被认为是正数。

但是如果你转换的​​是从80FF开头的值,那么最左边的位是1,这将被认为是负数。为了避免这种情况,并保持 BigInteger 值与提供的值完全相同,它在开头添加了另一个零字节。

所以,在内部,一个数字如7ABCDE的转换就是字节数组

0x7a 0xbc 0xde

但是像FABCDE这样的数的转换(只是第一个字节不一样!),就是:

0x00 0xfa 0xbc 0xde

这意味着对于以 80-FF 范围内的字节开头的键,BigInteger.toByteArray() 生成的数组与您的 C++ 程序生成的数组不同,而是长一个字节的数组。

有几种方法可以解决这个问题——比如使用您自己的十六进制到字节数组解析器或在某个库中找到现有的解析器。如果你想使用 BigInteger 制作的那个,你可以这样做:

BigInteger k = new BigInteger(key, 16);
byte[] kByteArr = k.toByteArray();
if ( kByteArr.length > (key.length() + 1) / 2 ) {
    kByteArr = Arrays.copyOfRange(kByteArr,1,kByteArr.length);
}

现在您可以使用kByteArr来正确执行操作了。

另一个需要注意的问题是长度为奇数的键。一般来说,你不应该有一个长度奇数的十六进制八位字节串。像 F8ACB 这样的字符串实际上是 0F8ACB(这不会在 BigInteger 中导致额外的字节)并且应该这样解释。这就是为什么我在我的公式中写 (key.length() + 1) - 如果密钥是奇数长度,它应该被解释为一个八位字节长。如果您编写自己的十六进制到字节数组转换器,这一点也很重要——如果长度是奇数,您应该在开始转换之前在开头添加一个零。