Search code examples
javascriptcryptographypbkdf2hmacsha1webcrypto

Webcrypto PBKDF2-SHA1


I am having an issue using PBKDF2 with Webcrypto. I would like to use PBKDF2 with the SHA-1 algorithm. Currently, I have,

const ENCODING = "utf-8";
const HMACSHA1 = {name: "HMAC", "hash" : "SHA-1"};
const PBKDF2SHA1 = {name: "PBKDF2", "hash": "SHA-1"};

// str2binb takes a string and outputs an ArrayBuffer

async function pbkdf2_generate_key_from_string(string) { //  Working
  return crypto.subtle.importKey(
    "raw",
    str2binb(string),
    PBKDF2SHA1,
    false,
    ["deriveKey", "deriveBits"],
  );
}

async function pbkdf2_derive_salted_key(key, salt, iterations) {  // Not working
  return crypto.subtle.deriveKey(
    {
      "name": "PBKDF2",
      "salt": salt,
      "iterations": iterations,
      "hash": "SHA-1",
      "length": 160
    },
    key,
    {
      "name": "HMAC"
      "hash": "SHA-1",
      "length": 160
    },
    true,
    [ "encrypt", "decrypt"]
  );
}

However I know I must be using it wrong, because it is a supported key derivation algorithm, and according to Mozilla's fantastic docs, under derivedKeyAlgorithm HMAC is supported as well as HMAC-SHA1. I also have it working with AES-GCM fairly straight forwardly.

The error message I am getting, when I try,

salt = b64binb("QSXCR+Q6sek8bf92"); // ArrayBuffer
key = await pbkdf2_generate_key_from_string("pencil");
x = await pbkdf2_derive_salted_key(key, salt, 4096)

is Uncaught DOMException: Cannot create a key using the specified key usages.

Note: I understand SHA1 is no longer recommended, this is for legacy support.

Note 2: It works when I replaced the above with,

async function pbkdf2_derive_salted_key(key, data, salt, iterations) {  // Not working
  return crypto.subtle.deriveKey(
    {
      "name": "PBKDF2",
      salt: salt,
      "iterations": iterations,
      "hash": "SHA-1",
    },
    key,
    {
      "name": "AES-GCM",
      "length": 256
    },
    true,
    [ "encrypt", "decrypt"]
  );
}

Thank you.


Solution

  • In deriveKey, the parameter keyUsages must be changed to ["sign", "verify"]. Then the code works (since b64binb, str2binb were not posted, the following code uses appropriate substitutes):

    const b64binb = base64String => Uint8Array.from(atob(base64String), c => c.charCodeAt(0));
    const str2binb = str => new TextEncoder().encode(str);
    const buf2hex = buffer => Array.prototype.map.call(new Uint8Array(buffer), x => ('00' + x.toString(16)).slice(-2)).join('');
    
    async function pbkdf2_generate_key_from_string(string) { 
        return crypto.subtle.importKey(
            "raw",
            str2binb(string),
            {
                name: "PBKDF2",
            },
            false,
            ["deriveKey", "deriveBits"], 
        );
    }
    
    async function pbkdf2_derive_salted_key(key, salt, iterations) {  
        return crypto.subtle.deriveKey(
            {
                name: "PBKDF2",
                salt: salt,
                iterations: iterations,
                hash: {name: "SHA-1"}
            },
            key,
            {
                name: "HMAC",
                hash: "SHA-1",
                length: 160
            },
            true,
            ["sign", "verify"] // <--------------------- Fix!
        );
    }
    
    async function test(){
        salt = b64binb("QSXCR+Q6sek8bf92"); // ArrayBuffer
        key = await pbkdf2_generate_key_from_string("pencil");
        x = await pbkdf2_derive_salted_key(key, salt, 4096)
    
        console.log(buf2hex(await window.crypto.subtle.exportKey("raw", x)));
    }
    
    test();

    With this key, window.crypto.subtle.sign creates a signature using HMAC-SHA1.

    Update:

    Since in SubtleCrypto the use of a key is generally specified, ["encrypt", "decrypt"] must be applied as keyUsages parameter for AES-GCM and ["sign", "verify"] for HMAC-SHA.

    Why ["encrypt", "decrypt"] for AES-GCM? AES-GCM is used for encrypting / decrypting messages: AES describes a block cipher (allowing the encryption of a single block) and GCM the mode of operation (allowing the encryption of more than a single block). GCM provides confidentiality, authenticity and integrity.

    And why ["sign", "verify"] for HMAC-SHA? An HMAC is used for signing / verifying messages: Some modes of operation such as CBC only provide confidentiality. To additionally supply authenticity and integrity, the message can be signed with a MAC (e.g. an HMAC which is a specific type of MAC based on a cryptographic hash function e.g. one of the SHA family). The MAC is typically calculated from the ciphertext and not the plaintext (Encrypt-then-MAC). With regard to the SHA1 vulnerabilities, see for a comparison of HMAC-SHA1 and HMAC-SHA256 here and here.