Skip to content

Different results trying to port SHA-1 digest from Python to browser JavaScript

Main question

I have the following short piece of legacy code that I am trying to port from Python (with just standard lib) to JavaScript – from the name of the methods I assume it creates a SHA-1 digest of the abc string

import hashlib
import hmac

print(hmac.new(b"abc", None, hashlib.sha1).hexdigest())

I searched for how to do that in the browser in JS and found the following code in the Mozilla documentation

var msgUint8 = new TextEncoder().encode('abc');
var hashBuffer = await crypto.subtle.digest('SHA-1', msgUint8);
var hashArray = Array.from(new Uint8Array(hashBuffer));
var hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
console.log(hashHex)

the problem is, they yield two completely different results, and I have no idea why:

  • cc47e3c0aa0c2984454476d061108c0b110177ae – Python
  • a9993e364706816aba3e25717850c26c9cd0d89d – JavaScript

I tried comparing the bytes of b"abc" with what new TextEncoder().encode('abc') returns and they are exactly the same: 0x61 0x62 0x63, so the problem lies somewhere else and I have no idea where.

I need the JavaScript code to return what the Python code returns. Any ideas?


Additionally

My final goal is to actually port this code (note the b"hello" instead of None):

print(hmac.new(b"abc", b"hello", hashlib.sha1).hexdigest())

so if you have an idea on that one too – I would hugely appreciate it!

Answer

The Python code calculates a SHA1 based HMAC. The JavaScript code on the other hand computes a SHA-1 hash. An HMAC needs a key in addition to the data, while a cryptographic hash function works without a key.

The first Python code uses the key abc and an empty message. The posted result for the HMAC is hex encoded:

cc47e3c0aa0c2984454476d061108c0b110177ae

and can be verified here.

The second Python code uses the same key and the message hello. The result for the HMAC is hex encoded:

d373670db3c99ebfa96060e993c340ccf6dd079e

and can be verified here.

The Java code determines the SHA-1 hash for abc. The result is

a9993e364706816aba3e25717850c26c9cd0d89d

and can be verified here.

So all results are correct, but are generated with different input data or algorithms.


The calculation of the HMAC can be implemented with the browser native WebCrypto-API as follows:

(async () => {
    var hmac = await calcHMac('abc', 'hello');
    console.log('HMAC: ', buf2hex(hmac)); 
    var hmac = await calcHMac('abc', '');
    console.log('HMAC: ', buf2hex(hmac)); 
})();
    
async function calcHMac(rawkey, data) {
    var key = await window.crypto.subtle.importKey('raw', utf8Encode(rawkey), {name: 'HMAC', hash: 'SHA-1'},true, ['sign']);        
    var hmac = await window.crypto.subtle.sign('HMAC', key, utf8Encode(data));
    return hmac;
}
    
function utf8Encode(str){
    return new TextEncoder().encode(str);
}
    
function buf2hex(buffer) {
    return Array.prototype.map.call(new Uint8Array(buffer), x => ('00' + x.toString(16)).slice(-2)).join(''); // from: https://stackoverflow.com/a/40031979/9014097 
}

and provides the same result as the two Python codes.

A remark to SHA-1: Although HMAC/SHA-1 is considered to be secure (contrary to SHA-1), there are arguments to switch to SHA-256, see here.


The WebCrypto API is a bit cumbersome. A functionally identical implementation with CryptoJS, the library mentioned in the comment by Maurice Meyer, is simpler and looks like this:

var hmac = CryptoJS.HmacSHA1('hello', 'abc');
console.log('HMAC: ', hmac.toString(CryptoJS.enc.Hex));

var hmac = CryptoJS.HmacSHA1('', 'abc');
console.log('HMAC: ', hmac.toString(CryptoJS.enc.Hex));
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.0.0/crypto-js.min.js"></script>

but requires CryptoJS as external dependency.