坚持在javascript中实施HMAC

时间:2013-04-16 16:12:27

标签: javascript cryptography hmac

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

/*
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字符串并更改字节顺序,所有这些都给我不同的结果,但没有一个匹配示例。

2 个答案:

答案 0 :(得分:3)

你的问题是你得到了错误的测试向量。你的钥匙:

  

000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f

并且您的消息“Sample #1”来自FIPS 198a中的示例A.1:带有64字节密钥的SHA-1 ,而您的预期输出为:

  

74766e5f6913e8cb6f7f108a11298b15010c353a

来自示例A.2:带有20字节密钥的SHA-1 。例A.1的正确的第一阶段哈希输出是:

  

bcc2c68cabbbf1c3f5b05d8e7e73a4d27b7e1b20

另请注意,NIST发布了一套更新,更全面的test vectors for HMAC-SHA-1 and 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,则可以将八位字节字符串(或其十六进制表示形式)转换为WordArray并将其直接传递给CryptoJS.SHA1()

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

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

答案 1 :(得分:0)

如果我看到你正在追加&#34; 0&#34;到键,这是0号的字符。 并且&#34; 0&#34; char的十六进制数为0x30,而HMAC rfc documentation则需要在ascii表中应用0x00字节,即NULL,而不是0x30。

似乎sha1函数默认返回40个十六进制字符的字符串,它只是基础数据的十六进制字符串表示不是数据本身。如果sha1生成数据流,例如:

,则表示

0100 1110 如果表示为十六进制字符串,则为&​​#34; 4e&#34;

,它返回&#34; 4e&#34;。默认情况下。

但我们不能直接在hmac alghorithm中使用它,因为&#34; 4e&#34;是不同的数据流:

0011 0110 0110 1001 其十六进制是&#34; 3465&#34;

所以我们不能使用不同的数据然后sha1的真正生成了什么,我们能做的是这个基础数据的十六进制字符串表示(&#34; 4e&#34;)转换为它&#39 ; s字符对应物:

0100 1110&lt; - (&#34; 4e&#34;)成为字符(&#34; N&#34;) - &gt; 0100 1110

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

这是char&#34; N&#34;是sha1产生的数据的精确映射不是数据的十六进制字符串表示。如果我们有40个十六进制字符,这意味着40个字节,并且通过sha1 rfc,sha1产生20个字节的数据。通过执行此转换,我们得到了20字节数据,并且如果我们使用ArrayBuffer,我们一直使用具有相同效果的字符串,至少是这个想法。

实施

我已经完成了一些使用上述方法的代码。因此,它应该在您无法理解的情况下无法访问ArrayBufer的情况下工作。它只适用于普通的javascript字符串。

我使用Rusha.js作为sha1函数,您可以找到所有信息here。你可以使用任何东西。 由于邮政体仅限于30000个字符,因此无法将其包含在此处。它全部在jsfiddle链接下面的代码中。那里使用了用于测试的密钥和消息(baseString) 来自twitter api example。 对于byteLength使用的操作,还有3个函数hexToString oneByteCharhmacSha1

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 test(打开浏览器控制台查看摘要)

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

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

注意:如果密钥或消息有&#34; \&#34;转义序列字符函数将产生incorect摘要(结果)。 例: key = "ke\y"baseStr = "So\me messa\ge"该函数生成摘要,如&#34; \&#34;两个字符串中都不存在。

你应该像这样逃避:

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

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