Search code examples
javascriptgohashhmac

hmac.New(h func() hash.Hash, key []byte) hash.Hash equivalent in javascript


Almost I got stuck in the js implementation of go lang hmac.New for several days. However, no success. I used crypto,crypto-js and stablelib modules for implementation. The problem is that in go lang version, hmac instance can be created by hmac instance. For example (the code block is correct and tested):

hmacf := hmac.New(func() hash.Hash {
    return hmac.New(func() hash.Hash {
        return hmac.New(func() hash.Hash {
            return hmac.New(sha256.New, []byte(SALT))
        }, []byte(path[0]))
    }, []byte(path[1]))
}, []byte(path[2]))

Actually, I don't know how does it work! because in all javascript related modules, You can't create hmac from hmac, and they accept an string value that determines hashing algorithm.

maybe it's better to ask how to create hmac from hmac in javascript.

What is the solution?

When the output of go version is the same with the output of your implementation; Your solution is correct.


Solution

  • According to the specification (RFC 2104), an HMAC uses a digest function internally, e.g. SHA256.

    However, your implementation applies (actually non-compliant) HMACs that internally use another HMAC instead of a digest, where only the lowest level HMAC uses a regular digest internally. In this way, a nested structure is created.

    Based on the specification of a regular HMAC (with a digest), this can be extended to the HMAC with an HMAC (instead of a digest) as used in the Go code:

    HMAC(K XOR opad, HMAC(K XOR ipad, text)) s. RFC2104, sec 2. Definition of HMAC


    Because of the difference from the specification, it will probably not be so easy to find a JavaScript library that supports something like this out of the box.
    Although most libraries of course support an HMAC, but only allow the specification of a digest (and not of an HMAC), e.g. crypto.createHmac() of the crypto module of NodeJS, see also the other answer. I don't think this approach can be used to implement the logic from the Go code.

    If the approach of the other answer doesn't work and you can't find another JavaScript library with the needed functionality, you can implement the logic in JavaScript yourself, because the specification of the HMAC is relatively simple (s. above).


    The following code is a sample implementation with the crypto module of NodeJS:

    var crypto = require('crypto')
    
    const digest = 'sha256'
    const blockSize = 64 // block size of the digest
    
    // define input parameter
    var salt = Buffer.from('salt')
    var path = [ Buffer.from('alfa'), Buffer.from('beta'), Buffer.from('gamma') ]
    var data = Buffer.from('data')
    
    // calculate HMAC
    var hmac = hmac(data, salt, path)
    console.log(hmac.toString('hex'))
    
    function hmac(data, salt, path) {
        
        // create keyList
        var keyList = []
        keyList.push(salt)
        keyList = keyList.concat(path)
    
        // determine HMAC recursively
        var result = hmac_rec(data, keyList)
        return result
    }
    
    function hmac_rec(data, keyList) {
    
        // adjust key (according to HMAC specification)
        var key = keyList.pop()
        if (key.length > blockSize) {        
            k = Buffer.allocUnsafe(blockSize).fill('\x00');
            if (keyList.length > 0) {
                hmac_rec(key, [...keyList]).copy(k)
            } else {
                getHash(key).copy(k)
            }
        } else if (key.length < blockSize) {
            k = Buffer.allocUnsafe(blockSize).fill('\x00');
            key.copy(k)
        } else {
            k = key
        }
    
        // create 'key xor ipad' and 'key xor opad' (according to HMAC specification)  
        var ik = Buffer.allocUnsafe(blockSize)
        var ok = Buffer.allocUnsafe(blockSize)
        k.copy(ik)
        k.copy(ok)
        for (var i = 0; i < ik.length; i++) {
            ik[i] = 0x36 ^ ik[i] 
            ok[i] = 0x5c ^ ok[i]
        }
    
        // calculate HMAC
        if (keyList.length > 0) {
            var innerHMac = hmac_rec(Buffer.concat([ ik, data ]), [...keyList]) 
            var outerHMac = hmac_rec(Buffer.concat([ ok, innerHMac ]), [...keyList])
        } else {
            var innerHMac = getHash(Buffer.concat([ik, data]))
            var outerHMac = getHash(Buffer.concat([ok, innerHMac]))
        }
      
        return outerHMac 
    }
    
    // calculate SHA256 hash
    function getHash(data){
        var hash = crypto.createHash(digest);
        hash.update(data)
        return hash.digest()
    }
    

    with the result:

    2e631dcb4289f8256861a833ed985fa945cd714ebe7c3bd4ed4b4072b107b073
    

    Test:

    The following Go code produces the same result:

    package main
    
    import (
        "crypto/hmac"
        "crypto/sha256"
        "encoding/hex"
        "fmt"
        "hash"
    )
    
    func main() {
        SALT := "salt"
        path := []string{"alfa", "beta", "gamma"}
        hmacf := hmac.New(func() hash.Hash {
            return hmac.New(func() hash.Hash {
                return hmac.New(func() hash.Hash {
                    return hmac.New(sha256.New, []byte(SALT))
                }, []byte(path[0]))
            }, []byte(path[1]))
        }, []byte(path[2]))
        hmacf.Write([]byte("data"))
        result := hmacf.Sum(nil)
        fmt.Println(hex.EncodeToString(result)) // 2e631dcb4289f8256861a833ed985fa945cd714ebe7c3bd4ed4b4072b107b073
    }
    

    Edit:

    Inspired by this post, the following is a more compact/efficient implementation for hmac_rec() that uses the regular HMac for the last iteration step (which also makes getHash() obsolete):

    function hmac_rec(data, keyList) {
        var key = keyList.pop()
        if (keyList.length > 0) {
            
            // adjust key (according to HMAC specification)
            if (key.length > blockSize) {        
                k = Buffer.allocUnsafe(blockSize).fill('\x00');
                hmac_rec(key, [...keyList]).copy(k)
            } else if (key.length < blockSize) {
                k = Buffer.allocUnsafe(blockSize).fill('\x00');
                key.copy(k)
            } else {
                k = key
            }
        
            // create 'key xor ipad' and 'key xor opad' (according to HMAC specification)  
            var ik = Buffer.allocUnsafe(blockSize)
            var ok = Buffer.allocUnsafe(blockSize)
            k.copy(ik)
            k.copy(ok)
            for (var i = 0; i < ik.length; i++) {
                ik[i] = 0x36 ^ ik[i] 
                ok[i] = 0x5c ^ ok[i]
            }
    
            // calculate HMAC
            var innerHMac = hmac_rec(Buffer.concat([ ik, data ]), [...keyList]) 
            var outerHMac = hmac_rec(Buffer.concat([ ok, innerHMac ]), [...keyList])
        } else {
            var outerHMac = crypto.createHmac(digest, key).update(data).digest();
        }  
        return outerHMac 
    }