31

How can I get HMAC-SHA512(key, data) in the browser using Crypto Web API (window.crypto)?

Currently I am using CryptoJS library and it is pretty simple:

CryptoJS.HmacSHA512("myawesomedata", "mysecretkey").toString();

Result is 91c14b8d3bcd48be0488bfb8d96d52db6e5f07e5fc677ced2c12916dc87580961f422f9543c786eebfb5797bc3febf796b929efac5c83b4ec69228927f21a03a.

I want to get rid of extra dependencies and start using Crypto Web API instead. How can I get the same result with it?

2

3 Answers 3

51

Answering my own question. The code below returns the same result as CryptoJS.HmacSHA512("myawesomedata", "mysecretkey").toString();

There are promises everywhere as WebCrypto is asynchronous:

// encoder to convert string to Uint8Array var enc = new TextEncoder("utf-8"); window.crypto.subtle.importKey( "raw", // raw format of the key - should be Uint8Array enc.encode("mysecretkey"), { // algorithm details name: "HMAC", hash: {name: "SHA-512"} }, false, // export = false ["sign", "verify"] // what this key can do ).then( key => { window.crypto.subtle.sign( "HMAC", key, enc.encode("myawesomedata") ).then(signature => { var b = new Uint8Array(signature); var str = Array.prototype.map.call(b, x => x.toString(16).padStart(2, '0')).join("") console.log(str); }); }); 
Sign up to request clarification or add additional context in comments.

2 Comments

The code is understandable to me until the last bit where you convert the ArrayBuffer to string. Why doesn't TextDecoder work for me?
Thanks for your answer. WebCrypto API returns an array buffer. So, I Converted the ArrayBuffer to Uint8Array and then used btoa to convert it to base64. btoa(String.fromCharCode(...signatureUint8))
34

Async/Await Crypto Subtle HMAC SHA-256/512 with Base64 Digest

The following is a copy of the ✅ answer. This time we are using async/await for clean syntax. This approach also offers a base64 encoded digest.

  • secret is the secret key that will be used to sign the body.
  • body is the string-to-sign.
  • enc is a text encoder that converts the UTF-8 to JavaScript byte arrays.
  • algorithm is a JS object which is used to identify the signature methods.
  • key is a CryptoKey.
  • signature is the byte array hash.
  • digest is the base64 encoded signature.

The JavaScript code follows:

(async ()=>{ 'use strict'; let secret = "sec-demo"; // the secret key let enc = new TextEncoder("utf-8"); let body = "GET\npub-demo\n/v2/auth/grant/sub-key/sub-demo\nauth=myAuthKey&g=1&target-uuid=user-1&timestamp=1595619509&ttl=300"; let algorithm = { name: "HMAC", hash: "SHA-256" }; let key = await crypto.subtle.importKey("raw", enc.encode(secret), algorithm, false, ["sign", "verify"]); let signature = await crypto.subtle.sign(algorithm.name, key, enc.encode(body)); let digest = btoa(String.fromCharCode(...new Uint8Array(signature))); console.log(digest); })();

The original answer on this page was helpful in a debugging effort earlier today. We're using it to help identify a bug in our documentation for creating signatures for granting access tokens to use APIs with read/write permissions.

Comments

8

Somehow @StephenBlum's answer doesn't work for me.

I rewrite @StepanSnigirev' answer as async below instead.

"use strict"; (async () => { const secret = "mysecretkey"; const enc = new TextEncoder(); const body = "myawesomedata"; const algorithm = { name: "HMAC", hash: "SHA-512" }; const key = await crypto.subtle.importKey( "raw", enc.encode(secret), algorithm, false, ["sign", "verify"] ); const signature = await crypto.subtle.sign( algorithm.name, key, enc.encode(body) ); // convert buffer to byte array const hashArray = Array.from(new Uint8Array(signature)); // convert bytes to hex string const digest = hashArray .map((b) => b.toString(16).padStart(2, "0")) .join(""); console.log(digest); })();

Note: We cannot use new Uint8Array(arrayBuffer).map(...). Although Uint8Array implements the ArrayLike interface, its map method will return another Uint8Array which cannot contain strings (hex octets in our case), hence the Array.from hack

Reference: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array#instance_properties

6 Comments

Good example for a proper async version with hex output. A few notes though: Don't use Array.from on a new Uint8Array, you're making an array of an array... And then instead of .map(...).join(""), please use reduce instead, that's what it's for.
The point of not needing Array.from is because Uint8Array already makes an array (I double-checked in console). You can directly .reduce it, no need to transform. The reason I suggested .reduce is a reduction in loops, as you can immediately reduce the array to the final input and make your already beautiful solution pretty much perfect.
@Windgazer Oh I miss my point. We cannot use new Uint8Array(arrayBuffer).map(...). Although Uint8Array implements the ArrayLike interface, its map method will return another Uint8Array which cannot contain strings (hex octets in our case), hence the Array.from hack. Same applied to reduce(), I believe. Reference: stackoverflow.com/questions/40031688/…
Weird, by all official documentation (as well as lightly testing) the result of new Uint8Array(arrayBuffer) should be an array, the result even has map and reduce, but somehow calling them does not actually do anything. I stand corrected and confused. Usually array-like objects only are alike in that they have numeric keys, not the rest of the Array interface. edit Should've read the MDN entry first, the quirks come from it being a TypedArray instead.
Stephen's answer returns a Base64 representation of the buffer, you return a hex string, that's the only difference I can see. By the way, the lambda can be passed to the Array.from function: Array.from(new Uint8Array(signature), b => b.toString(16).padStart(2, '0')).join('').
|

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.