1

我一直在尝试用 Javascript 编写 HMAC 算法,但已经到了无法弄清楚出了什么问题的地步。我正处于创建内部哈希的位置,但在使用 SHA1(步骤 6)时,返回的值与 FIPS 198 文档示例 A1 中指定的值不匹配。

/*
function hmac (key, message)
    if (length(key) > blocksize) then
        key = hash(key) // keys longer than blocksize are shortened
    end if
    if (length(key) < blocksize) then
        key = key ∥ [0x00 * (blocksize - length(key))] // keys shorter than blocksize are zero-padded ('∥' is concatenation) 
    end if

    o_key_pad = [0x5c * blocksize] ⊕ key // Where blocksize is that of the underlying hash function
    i_key_pad = [0x36 * blocksize] ⊕ key // Where ⊕ is exclusive or (XOR)

    return hash(o_key_pad ∥ hash(i_key_pad ∥ message)) // Where '∥' is concatenation
end function
*/

/*
STEPS
Step 1
Table 1: The HMAC Algorithm
STEP-BY-STEP DESCRIPTION
If the length of K = B: set K0 = K. Go to step 4.
Step 2 If the length of K > B: hash K to obtain an L byte string, then append (B-L)
      zeros to create a B-byte string K0 (i.e., K0 = H(K) || 00...00). Go to step 4.
Step 3 If the length of K < B: append zeros to the end of K to create a B-byte string K0
      (e.g., if K is 20 bytes in length and B = 64, then K will be appended with 44
     zero bytes 0x00).
Step 4 Exclusive-Or K0 with ipad to produce a B-byte string: K0  ̄ ipad.
Step 5 Append the stream of data 'text' to the string resulting from step 4:
      (K0  ̄ ipad) || text.
Step 6 Apply H to the stream generated in step 5: H((K0  ̄ ipad) || text).
Step 7 Exclusive-Or K0 with opad: K0  ̄ opad.
Step 8 Append the result from step 6 to step 7:
      (K0  ̄ opad) || H((K0  ̄ ipad) || text).
Step 9 Apply H to the result from step 8:
      H((K0  ̄ opad )|| H((K0  ̄ ipad) || text)).
Step 10 Select the leftmost t bytes of the result of step 9 as the MAC.
*/

/*
FIPS PUB 198, The Keyed-Hash Message Authentication Code
http://csrc.nist.gov/publications/fips/fips198/fips-198a.pdf

A.1
SHA-1 with 64-Byte Key
*/


//Check sha1 hashers
if ($u.sha1("test") !==  CryptoJS.SHA1("test").toString()) {
    throw new Error("hasher output mismatch");
}

var key = "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f";
var k0 = "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f";
var k0ipad = "36373435323330313e3f3c3d3a3b383926272425222320212e2f2c2d2a2b282916171415121310111e1f1c1d1a1b181906070405020300010e0f0c0d0a0b0809";
var k0opad = "5c5d5e5f58595a5b54555657505152534c4d4e4f48494a4b44454647404142437c7d7e7f78797a7b74757677707172736c6d6e6f68696a6b6465666760616263";
var ipt = "36373435323330313e3f3c3d3a3b383926272425222320212e2f2c2d2a2b282916171415121310111e1f1c1d1a1b181906070405020300010e0f0c0d0a0b080953616d706c65202331";
var h1 = "bcc2c68cabbbf1c3f5b05d8e7e73a4d27b7e1b20";
var message = "Sample #1";
var result = "";

function hmac(key, message) {
    key = key.replace(/\s*/g, "");

    var swap = false, // for swap endianess
        length = key.length,
        blockSize = 64 * 2, // for sha 1 = 64, as hex * 2
        ml = message.length,
        i = 0,
        o_key_pad = "",
        i_key_pad = "",
        ikeypmessage = "",
        hipt,
        temp1,
        temp2;

    // 1. If the length of K = B: set K0 = K. Go to step 4.
    if (length !== blockSize) {
        // 2. If the length of K > B: hash K to obtain an L byte string, then append (B-L)
        //    zeros to create a B-byte string K0 (i.e., K0 = H(K) || 00...00). Go to step 4.
        //    Actually in code, goto step3 ri append zeros
        if (length > blockSize) {
            key = $u.sha1(key);
        }

        // 3. If the length of K < B: append zeros to the end of K to create a B-byte string K0
        //   (e.g., if K is 20 bytes in length and B = 64, then K will be appended with 44
        //   zero bytes 0x00).
        while (key.length < blockSize) {
            key += "0";
            i += 1;
        }
    }

    // check against the FIP198 example
    if (key !== k0) {
        console.log(key, k0);
        throw new Error("key and k0 mismatch");
    }

    // 4. Exclusive-Or K0 with ipad to produce a B-byte string: K0  ̄ ipad.
    // 7. Exclusive-Or K0 with opad: K0  ̄ opad.
    i = 0;
    while (i < blockSize) {
        temp1 = parseInt(key.slice(i, i + 2), 16);

        temp2 = (temp1 ^ 0x36).toString(16);
        i_key_pad += temp2.length > 1 ? temp2 : "0" + temp2;

        temp2 = (temp1 ^ 0x5c).toString(16);
        o_key_pad += temp2.length > 1 ? temp2 : "0" + temp2;

        i += 2;
    }

    if (i_key_pad !== k0ipad) {
        console.log(i_key_pad, k0ipad);
        throw new Error("i_key_pad and k0ipad mismatch");
    }

    if (o_key_pad !== k0opad) {
        console.log(o_key_pad, k0opad);
        throw new Error("o_key_pad and k0opad mismatch");
    }

    // 5. Append the stream of data 'text' to the string resulting from step 4:
    //    (K0  ̄ ipad) || text.
    i = 0;
    temp1 = "";
    while (i < ml) {
        temp1 += message.charCodeAt(i).toString(16);
        i += 1;
    }

    ikeypmessage = i_key_pad + temp1;
    if (ikeypmessage !== ipt) {
        console.log(i_key_pad + temp1, ipt);
        throw new Error("i_key_pad + temp1 and ipt mismatch");
    }

    // convert hex string to ucs2 string
    ml = ikeypmessage.length;
    temp1 = [];
    i = 0;
    while (i < ml) {
        // for changinging endianess
        if (swap) {
            temp1[i >> 1] = ikeypmessage.charAt(i + 1) + ikeypmessage.charAt(i);
        } else {
            temp1[i >> 1] = ikeypmessage.slice(i, i + 2);
        }

        i += 2;
    }

    // for changinging endianess
    if (swap) {
        temp1.reverse();
    }

    // convert byte to ucs2 string
    ml = temp1.length;
    temp2 = "";
    i = 0;
    while (i < ml) {
        temp2 += String.fromCharCode(parseInt(temp1[i], 16));
        i += 1;
    }

    ikeypmessage = temp2;

    // This is the point where it goes bottom up
    // 6. Apply H to the stream generated in step 5: H((K0  ̄ ipad) || text).
    console.log(ikeypmessage);
    hipt = $u.sha1(ikeypmessage);
    if (hipt !== h1) {
        console.log(hipt, h1);
        throw new Error("hipt and h1 mismatch");
    }
}

console.log(hmac(key, message));

此代码可从jsfiddle 获得,如果有人可以给我一个关于我哪里出错的指针,我将不胜感激。

我尝试从十六进制字符串转换为 ucs2 字符串并更改字节顺序,都给了我不同的结果,但没有一个与示例匹配。

4

2 回答 2

3

你的问题是你有错误的测试向量。你的钥匙:

000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3

并且您的消息“ Sample #1”来自Example A.1: SHA-1 with 64-Byte Key in FIPS 198a,而您的预期输出:

74766e5f6913e8cb6f7f108a11298b15010c353a

来自示例 A.2: SHA-1 with 20-Byte Key。示例 A.1 的正确第一阶段哈希输出是:

bcc2c68cabbbf1c3f5b05d8e7e73a4d27b7e1b20

另请注意,NIST 发布了一套更新的、更全面的HMAC-SHA-1 和 HMAC-SHA-2 测试向量


好的,我发现了第二个问题。查看 的源代码$u.sha1(),该函数以:

var msg = internal.utf8EncodeToCharCodeArray(str)

也就是说,它希望其输入是一个 Unicode 字符串,并在对其进行散列之前使用 UTF-8 编码将其转换为八位字节字符串。特别是,这意味着代码点高于 127 的字符将转换为多个字节。

不幸的是,HMAC 构造在原始八位字节字符串上运行,而不是在 Unicode 字符串上运行。更糟糕的是,似乎没有任何方法可以将原始八位字节字符串提供给$u.sha1(); UTF-8 转换是自动完成的,您需要在 HMAC 中散列的八位字节字符串甚至不太可能成为任何Unicode 字符串的有效 UTF-8 编码。

但是,如果您改用CryptoJS,则可以将八位字节字符串(或它的十六进制表示)转换为 aWordArray并将其直接传递给CryptoJS.SHA1()

var words = CryptoJS.enc.Latin1.parse(ikeypmessage);
hipt = CryptoJS.SHA1(words).toString();

当然,如果您使用的是 CryptoJS,将密钥和消息转换为WordArrays 开始,然后直接使用它们会更容易和更有效。或者您可以只使用内置CryptoJS.HmacSHA1()方法。

于 2013-04-18T18:38:10.750 回答
0

如果我没看错,您将“0”附加到密钥,这是数字 0 的字符。并且“0”字符的十六进制数为 0x30,并且通过HMAC rfc 文档,您需要应用 0x00 字节,即 NULL 字符ascii 表,而不是 0x30。


##Idea## 似乎 sha1 函数默认返回 40 个十六进制字符的字符串,这只是底层数据的十六进制字符串表示,而不是数据本身。意思是如果 sha1 产生数据流,例如:

0100 1110 如果表示为十六进制字符串,则为“4e”

,它返回“4e”。默认。

但是我们不能直接在我们的hmac 算法中使用它,因为“4e”是不同的数据流:

0011 0110 0110 1001 它的十六进制是“3465”

所以我们不能使用 sha1真正产生的不同数据,我们可以做的是底层数据的这个十六进制字符串表示(“4e”)转换成它的字符对应:

0100 1110<-- ("4e") 变为 char ("N")--> 0100 1110

在这种情况下,最好认为 sha1 通过处理每 4 位基础数据(一个nibble )来吐出十六进制字符串。我们通过将 8 位数据映射到它们的字符串表示来压缩该字符串。

也就是说 char "N" 是 sha1 生成的数据的精确映射,它不是 data 的十六进制字符串表示。如果我们有 40 个十六进制字符,这意味着 40 个字节,通过 sha1 rfc,sha1 产生 20 个字节的数据。通过进行这种转换,我们得到了 20 字节的数据,并且我们一直在使用具有相同效果的字符串,如果我们使用ArrayBuffer,至少是这样。


执行

我已经完成了一些使用上述方法的代码。ArrayBufer因此,它应该在您出于任何原因无法访问的地方工作。它仅适用于纯 JavaScript 字符串。

我使用 Rusha.js 作为 sha1 函数,你可以在这里找到所有信息。你可以使用任何东西。由于帖子正文限制为 30000 个字符,因此无法在此处包含它。这一切都在 jsfiddle 链接下面的代码中。用于测试的密钥和消息(baseString)从twitter api 示例中使用。还有 3 个函数,byteLength hexToStringoneByteChar用于hmacSha1使用的操作。

var sha = new Rusha();  
var sha1 = sha.digest; 

function byteLength(str){  // counts characters only 1byte in length, of a string. Very similar to oneByteChar()
                           // For clarity I made 2 functions.
    var len = str.length;
    var i = 0;
    var byteLen = 0;
    for (i; i < len; i++){
      var code = str.charCodeAt(i); 
      if(code >= 0x0 && code <= 0xff) byteLen++; 
      else{
         throw new Error("More the 1 byte code detected, byteLength functon aborted.");
         return;
      }
      
    }
    
    return byteLen;
  
}

function oneByteCharAt(str,idx){
     var code = str.codePointAt(idx);
     if(code >= 0x00 && code <= 0xff){ // we are interested at reading only one byte
          return str.charAt(idx); // return char.
          
     }    
     else{ 
        throw new Error("More then 1byte character detected, |oneByteCharAt()| function  is aborted.")
     }
  
}

function hexToString(sha1Output){ // converts every pair of hex CHARS to their character conterparts
                                  // example1: "4e" is converted to char "N" 
                                  // example2: "34" is converted to char "4"
    
  var l;        // char at "i" place, left
  var lcode;    // code parsed from left char
  var shiftedL; // left character shifted to the left
    
  var r;     // char at "i+1" place, right
  var rcode; // code parsed from right char
  
  var bin;   // code from bitwise OR operation
  var char;  // one character
  var result = ""; // result string 
    
 for (var i = 0; i < sha1Output.length; i+=2){ // in steps by 2
         l = sha1Output[i]; // take "left" char
         
         if(typeof l === "number") lcode = parseInt(l); // parse the number
         else if(typeof l === "string") lcode = parseInt(l,16);  // take the code if char letter is hex number (a-f)
         
          shiftedL = lcode << 4 ; // shift it to left 4 places, gets filled in with 4 zeroes from the right
          r = sha1Output[i+1];    // take next char
         
         if(typeof r === "number") rcode = parseInt(r); // parse the number
         else if(typeof r === "string") rcode = parseInt(r,16); 
         
          bin = shiftedL | rcode; // concatenate left and right hex char, by applying bitwise OR
          char = String.fromCharCode(bin); // convert back code to char
          result += char;
   
          
  }
  // console.log("|"+result+"|", result.length); // prints info, line can be deleted
   
  return result;
}

function hmacSha1(key, baseString){   // the actual HMAC_SHA1 function
    
 
  var blocksize = 64; // 64 when using these hash functions: SHA-1, MD5, RIPEMD-128/160 .
  var kLen = byteLength(key); // length of key in bytes;
  var opad = 0x5c; // outer padding  constant = (0x5c) . And 0x5c is just hexadecimal for backward slash "\" 
  var ipad = 0x36; // inner padding contant = (0x36). And 0x36 is hexadecimal for char "6".
    

     
    
    if(kLen < blocksize){  
       var diff = blocksize - kLen; // diff is how mush  blocksize is bigger then the key
    }
    
    if(kLen > blocksize){ 
       key = hexToString(sha1(key)); // The hash of 40 hex chars(40bytes) convert to exact char mappings, from 0x00 to 0xff,
                                     // Produces string of 20 bytes.
       
       var hashedKeyLen =  byteLength(key); // take the length of key
    }
    
    var opad_key = ""; // outer padded key
    var ipad_key = ""; // inner padded key
  
    (function applyXor(){  // reads one char, at the time, from key and applies XOR constants on it acording to byteLength of the key
       var o_zeroPaddedCode;  // result from opading the zero byte
       var i_zeroPaddedCode;  // res from ipading the zero byte
       var o_paddedCode;      // res from opading the char from key
       var i_paddedCode;      // res from ipading the char from key
       
       var char;      
       var charCode;
      
       for(var j = 0; j < blocksize; j++){ 
             
             if(diff && (j+diff) >= blocksize || j >= hashedKeyLen){ // if diff exists (key is shorter then blocksize) and if we are at boundry 
                                                                     // where we should be, XOR 0x00 byte with constants. Or the key was 
                                                                     // too long and was hashed, then also we need to do the same.
                o_zeroPaddedCode = 0x00 ^ opad; // XOR zero byte with opad constant  
                opad_key += String.fromCharCode(o_zeroPaddedCode); // convert result back to string 
               
                i_zeroPaddedCode = 0x00 ^ ipad;
                ipad_key += String.fromCharCode(i_zeroPaddedCode);
             }
             else {
               
                char = oneByteCharAt(key,j);     // take char from key, only one byte char
                charCode = char.codePointAt(0);  // convert that char to number
                
                o_paddedCode =  charCode ^ opad; // XOR the char code with outer padding constant (opad)
                opad_key += String.fromCharCode(o_paddedCode); // convert back code result to string
                
                i_paddedCode = charCode ^ ipad;  // XOR with the inner padding constant (ipad)
                ipad_key += String.fromCharCode(i_paddedCode);
             
             }
          

            
        }
      //  console.log("opad_key: ", "|"+opad_key+"|", "\nipad_key: ", "|"+ipad_key+"|"); // prints opad and ipad key, line can be deleted
    })()
 
    return sha1(opad_key + hexToString(sha1(ipad_key + baseString))) ;
    
}

var baseStr = "POST&https%3A%2F%2Fapi.twitter.com%2F1%2Fstatuses%2Fupdate.json&include_entities%3Dtrue%26oauth_consumer_key%3Dxvz1evFS4wEEPTGEFPHBog%26oauth_nonce%3DkYjzVBB8Y0ZFabxSWbWovY3uYSQ2pTgmZeNu2VS4cg%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1318622958%26oauth_token%3D370773112-GmHxMAgYyLbNEtIKZeRNFsMKPR9EyMZeS9weJAEb%26oauth_version%3D1.0%26status%3DHello%2520Ladies%2520%252B%2520Gentlemen%252C%2520a%2520signed%2520OAuth%2520request%2521"

var key= "kAcSOqF21Fu85e7zjz7ZN2U4ZRhfV3WpwPAoE3Z7kBw&LswwdoUaIvS8ltyTt5jkRh4J50vUPVVHtR2YPi5kE";
console.log(hmacSha1( key, baseStr ) ); // b679c0af18f4e9c587ab8e200acd4e48a93f8cb6

hmac_sha1 测试(打开浏览器控制台查看摘要)

可以找到其他测试向量(键和消息): here from official hmac_sha1 guywiki

或者,您可以在jsSHA上输入几乎任何内容,看看是否符合您在 hmacSha1 摘要中看到的内容。


注意:如果键或消息具有“”转义序列字符,则函数将产生不正确的摘要(结果)。示例: key = "ke\y"并且baseStr = "So\me messa\ge"该函数生成摘要,例如两个字符串中都不存在“”。

你应该像这样逃避它:

key = "ke\\y"baseStr = "So\\me messa\\ge"

然后它的摘要正如预期的那样。请报告错误和错误。

于 2017-05-14T14:24:53.770 回答