How to get HMAC with Crypto Web API
Asked Answered
J

3

29

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?

Jus answered 16/11, 2017 at 12:0 Comment(2)
developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/signBullen
See this github.com/diafygi/webcrypto-examples#hmacSaint
J
47

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);
    });
});
Jus answered 16/11, 2017 at 14:33 Comment(1)
The code is understandable to me until the last bit where you convert the ArrayBuffer to string. Why doesn't TextDecoder work for me?Deprecative
S
32

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.

Sapsucker answered 28/5, 2021 at 19:8 Comment(0)
L
7

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

Levkas answered 14/11, 2022 at 8:29 Comment(6)
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.Jennings
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.Jennings
@Jennings 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: #40032188Levkas
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.Jennings
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('').Salify
@Salify to use Hex string is to answer OP question to get the same result.Levkas

© 2022 - 2024 — McMap. All rights reserved.