1

I've been trying to code HMAC algorithm in Javascript but have got to a point where I can not figure out what is going wrong. I'm at the point where you create the inner hash, but the returned value does not match that specified in the FIPS 198 document example A1 when using SHA1 (step 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));

This code is available of jsfiddle and if there is anyone that can give me a pointer as to where I am going wrong it would be much appreciated.

I have tried converting from a hex string to a ucs2 string and changing endianess, all give me different results but none match the example.

4
  • 1
    One basic thing to consider is that JavaScript strings use UTF-16 characters; the elements of a string aren't bytes, in other words. Commented Apr 16, 2013 at 16:16
  • Ah yes, I had forgotten about the possible multi byte string issue after the XORing. I will have to check that. Commented Apr 16, 2013 at 16:20
  • I don't think this is the issue as all the characters in use should be single byte UCS2, all are below char code 256, correct me if I am wrong. Commented Apr 16, 2013 at 16:33
  • I think temp1.reverse(); is misguided. Whatever the endianness HMAC, data is processed from left to right. Update: Ah you removed it. Commented Apr 17, 2013 at 9:11

2 Answers 2

3
+50

Your problem is that you've got the wrong test vector. Your key:

000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f

and your message "Sample #1" are from Example A.1: SHA-1 with 64-Byte Key in FIPS 198a, whereas your expected output:

74766e5f6913e8cb6f7f108a11298b15010c353a

is from Example A.2: SHA-1 with 20-Byte Key. The correct first-stage hash output for Example A.1 is:

bcc2c68cabbbf1c3f5b05d8e7e73a4d27b7e1b20

Also note that NIST has published a newer, somewhat more comprehensive set of test vectors for HMAC-SHA-1 and HMAC-SHA-2.


OK, I found the second problem. Peeking at the source code of $u.sha1(), that function starts with:

var msg = internal.utf8EncodeToCharCodeArray(str)

That is, it expects its input to be a Unicode string, and converts it into an octet string using the UTF-8 encoding before hashing it. In particular, this means that characters with codepoints above 127 are converted into multiple bytes.

Unfortunately, the HMAC construction operates on raw octet strings, not on Unicode strings. Worse yet, there doesn't appear to be any way to feed a raw octet string to $u.sha1(); the UTF-8 conversion is done automatically, and it is very unlikely for the octet strings you need to hash in HMAC to even be the valid UTF-8 encoding of any Unicode string.

If you used CryptoJS instead, however, you could convert the octet string (or the hex representation of it) to a WordArray and pass that directly to CryptoJS.SHA1():

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

Of course, if you were using CryptoJS, it would be easier and more efficient to convert the key and message to WordArrays to begin with, and then work directly with those. Or you could just use the built-in CryptoJS.HmacSHA1() method.

Sign up to request clarification or add additional context in comments.

6 Comments

Dope! Ok, that doesn't appear to be the only problem though as even after change I still get a mismatch. Did you actually manage to get a match on jsfiddle?
Nope, you're right, there's still an encoding issue with octets above 127. See edit above.
Disabled UTF8 encoding and use raw strings, updated fiddle, still get different results. So,there is still more to the story. CrytoJS lib is included in the fiddle, so you can try with its functions if you would like. This is an exercise of learning, not just using someone else's code for a result, and I would most likely use Stanford's crypto library if it was :)
That's strange. If I replace the line hipt = $u.sha1(ikeypmessage, utf8encoding); in your jsFiddle with the two lines I gave above, with no other changes, the Error goes away. Anyway, you should first make sure that your SHA-1 implementation matches the CryptoJS one, even for arbitrary byte sequences and not just for ASCII text. Once you do that, correctly implementing HMAC should be easier.
I haven't forgotten about this and will be looking at it during the comming week, thanks.
|
0

If I see correctly you are appending "0" to the key, which is a char for number 0. And "0" char has hex number of 0x30, and by HMAC rfc documentation , you need to apply 0x00 byte that is NULL char in ascii table, not 0x30.


##Idea## It seems that sha1 functions by default return string of 40 hex chars that is just, well hex string representation of underlying data, not the data itself. Meaning if sha1 produces data stream of, for example:

0100 1110 which if represent as hex string is "4e"

, it returns "4e". By default.

But we cant directly use that in our hmac alghorithm since "4e" is different stream of data:

0011 0110 0110 1001 its hex is "3465"

So we cant use different data then what sha1 has really produced, what we can do is this hex string representation ("4e") of underlying data convert to it's character counterpart:

0100 1110 <-- ("4e") becomes char ("N")--> 0100 1110.

In this context it's good to think that sha1 spits out hex string by treating every 4 bits of underlying data, a nibble. And that we compress that string by mapping 8 bits of data to their string representation.

That is char "N" is exact mapping of data that sha1 produced it is not hex string representation of data. If we have 40 hex chars, that means 40 bytes and by sha1 rfc, sha1 produces 20 byte data. By doing this conversion we get that 20 byte data and all the time we are using strings with same effect if we were using ArrayBuffer, at least that's the idea.


Implementation

I've done some code that uses method explained above. So it should work where you can't have access to ArrayBufer for what ever reason. It works only with plain javascript strings.

I'm using Rusha.js as sha1 function, all info you can find here. You can use anything. Can't include it here since post body is limited to 30000 chars. It's all in jsfiddle link bellow code. There the key and message(baseString) for testing are used from twitter api example. Also there are 3 functions, byteLength hexToString, and oneByteChar for operations that hmacSha1 uses.

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 (open browser console to see digest)

Additional test vectors (keys and messages) can be find: here from official hmac_sha1 guys, and again wiki.

Or you can enter pretty much anything on jsSHA, and see if that complies from what you see in hmacSha1 digests.


Note: if key or message have "" escape sequence character the functions will produce incorect digest (result). Example: key = "ke\y" and baseStr = "So\me messa\ge" the function produces digest like the "" is not present in both strings.

You should escape it like so:

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

Then it's digest is as expected. Pls, report errors and bugs.

2 Comments

Thanks for the info. It was such a long time ago now that I can't even remember exactly what I was doing. :) But thank you!
@Xotic 750. I saw it was in `13 but still wanted to post it, since I was messing with HMAC last week, and your post keep coming out in google search. :)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.