26

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?

Stepan Snigirev
  • 816
  • 1
  • 7
  • 11

3 Answers3

41

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);
    });
});
lmcarreiro
  • 5,312
  • 7
  • 36
  • 63
Stepan Snigirev
  • 816
  • 1
  • 7
  • 11
  • The code is understandable to me until the last bit where you convert the ArrayBuffer to string. Why doesn't `TextDecoder` work for me? – David Min Aug 05 '21 at 22:30
25

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.

Stephen Blum
  • 6,498
  • 2
  • 34
  • 46
  • I tried this codes with OP's data but I cannot get the same result as OP digest `91c14b8d3bcd48be0488bfb8d96d52db6e5f07e5fc677ced2c12916dc87580961f422f9543c786eebfb5797bc3febf796b929efac5c83b4ec69228927f21a03a`. – ikhvjs Feb 16 '23 at 07:14
3

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

ikhvjs
  • 5,316
  • 2
  • 13
  • 36
  • 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. – Windgazer Nov 21 '22 at 13:13
  • 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 Aug 17 '23 at 09:48
  • 1
    @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: https://stackoverflow.com/questions/40031688/javascript-arraybuffer-to-hex – ikhvjs Aug 17 '23 at 10:23
  • 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. – Windgazer Aug 22 '23 at 14:01