2015-11-06 110 views
1

这将是一个长期的问题,但我有一个非常奇怪的错误。我在C++中使用OpenSSL来计算HMAC,并将它们与使用javax.crypto.Mac的相似实现进行比较。对于某些密钥,HMAC计算是正确的,对于其他密钥,HMAC存在差异。我相信当钥匙变大时会出现问题。这里是细节。Java Mac HMAC vs C++ OpenSSL 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: 736A66B29072C49AB6DC93BB2BA41A53E169D14621872B0345F01EBB F117FCE48EEEA2409CFC1BD92B0428BA0A34092E3117BEB4A8A14F03391C661994863DAC1A75ED437C1394DA0741B16740D018CA243A800DA25311FDFB9CA4361743E8511E220B79C2A3483FCC29C7A54F1EB804481B2DC87E54A3A7D8A94253A60AC77FA4584A525EDC42BF82AE2A1FD6E3746F626E0AFB211F6984367B34C954B0E08E3F612590EFB8396ECD9AE77F15D5222A6DB106E8325C3ABEA54BB59E060F9EA0

消息: 测试

HMAC的OpenSSL: b37f79df52afdbbc4282d3146f9fe7a254dd23b3

HMAC的Java的Mac: b37f79df52afdbbc4282d3146f9fe7a254dd23b3


键2:636A66B29072C49AB6DC93BB2BA41A53E169D14621872B0345F01EBBF117FCE48EEEA2409CFC1BD92B0428BA0A34092E3117BEB4A8A14F03391C661994863DAC1A75ED437C1394DA0741B16740D018CA243A800DA25311FDFB9CA4361743E8511E220B79C2A3483FCC29C7A54F1EB804481B2DC87E54A3A7D8A94253A60AC77FA4584A525EDC42BF82AE2A1FD6E3746F626E0AFB211F6984367B34C954B0E08E3F612590EFB8396ECD9AE77F15D5222A6DB106E8325C3ABEA54BB59E060F9EA0

消息: 测试

HMAC的OpenSSL: bac64a905fa6ae3f7bf5131be06ca037b3b498d7

HMAC的Java的Mac: bac64a905fa6ae3f7bf5131be06ca037b3b498d7


重点3:836A66B29072C49AB6DC93BB2BA41A53E169D14621872B0345F01EBBF117FCE48EEEA2409CFC1BD92B0428BA0A34092E3117BEB4A8A14F03391C661994863DAC1A75ED437C1394DA0741B16740D018CA243A800DA25311FDFB9CA4361743E8511E220B79C2A3483FCC29C7A54F1EB804481B2DC87E54A3A7D8A94253A60AC77FA4584A525EDC42BF82AE2A1FD6E3746F626E0AFB211F6984367B34C954B0E08E3F612590EFB8396ECD9AE77F15D5222A6DB106E8325C3ABEA54BB59E060F9EA0

消息: 测试

HMAC运算enSSL: c189c637317b67cee04361e78c3ef576c3530aa7

HMAC的Java的Mac: 472d734762c264bea19b043094ad0416d1b2cd9c

如数据所示,当钥匙获取到大,发生错误。如果不知道哪个实现有问题。我也尝试了更大的按键和更小的按键。我还没有确定确切的门槛。任何人都可以发现问题吗?有没有人能够通过使用不同的软件进行仿真来告诉我哪种HMAC在最后一种情况下是不正确的,或者任何人都可以告诉我可以使用哪种第三种实现来检查我的?

亲切的问候,

罗埃尔风暴

+0

我认为问题不在关键的长度,而是在其符号。你可以尝试用一个“FF”替换一个短键的起始字节,看看你在C++和Java之间得到不同的结果吗? – RealSkeptic

+0

那么用FF代替83例如? – Silver

+0

这样做仍然会导致不同的HMAC。 Openssl:53e8fab89762b945f08d245f963aab72dfd47533 Java:15996db38398cb114f73f81645d7bbb87b24c2e4 – Silver

回答

2

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

但它的内部表示是二补。这意味着一个位用于标志。

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

但是,如果您要将一个以80开头的值转换为FF,那么最左边的位是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) - 如果密钥是奇数长度的,应该将其解释为更长的一个八位字节。如果您编写自己的十六进制字节数组转换器,这一点也很重要 - 如果长度很奇怪,您应该在开始转换之前在开始处添加一个零。

+0

我要明天测试一下,因为我必须现在去,但你可以完美地描述行为,所以我认为这确实是正确的答案。 – Silver

+0

工程就像一个魅力。如果我理解正确,您的代码正在检查生成的数组的长度是否正确。如果不是这种情况,它会复制字符1的字节数组,直到数组的末尾,从而缩短1个字节? 我注意到领先的0之前,但我认为这并不重要,因为他们是领先0。 – Silver

+0

@RoelStorms是的,正好。当您将数据视为“数字”时,前导零并不重要。但是,当您将数据视为数据时 - 要加密和解密 - 则零是合法的数据(例如,考虑黑色像素)。 – RealSkeptic