javascriptencryptioncryptographyrsa

Key unwrapping fails


I want to protect a RSA private key stored in localStorage by wrapping it with a key derived from the user's password.

However when unwrapping the key the error DOMException: An invalid or illegal string was specified is thrown.

This is a minimal snippet for reproducing the issue:

const base64ToArrayBuffer = (data) => {
    const binaryKey = atob(data);
    const keyBytes = new Uint8Array(binaryKey.length);

    for (let i = 0; i < binaryKey.length; i++) {
        keyBytes[i] = binaryKey.charCodeAt(i);
    }

    return keyBytes.buffer;
}

const bufferToBase64 = (data) => btoa(String.fromCharCode(... new Uint8Array(data)));

const fn = async () => {
// Generate RSA key pair

  const keyPair = await crypto.subtle.generateKey({
      name: "RSA-OAEP",
      modulusLength: 4096,
      publicExponent: new Uint8Array([1, 0, 1]),
      hash: "SHA-512"
  }, true, ["encrypt", "decrypt"]);

// Save private key

// Encrypt the private key

  const textEncoder = new TextEncoder();
  const salt = new Uint8Array(16);

  crypto.getRandomValues(salt);

  const passwordKey = await crypto.subtle.importKey("raw", textEncoder.encode(window.prompt("password")), "PBKDF2", true, ["deriveKey"]);
  const derivedKey = await crypto.subtle.deriveKey({
      name: "PBKDF2",
      hash: "SHA-256",
      salt,
      iterations: 210000
  }, passwordKey, {
      name: "AES-CBC",
      length: 256
  }, true, ["wrapKey", "unwrapKey"]);
  const iv = new Uint8Array(16);

  crypto.getRandomValues(iv);

  const wrappedPrivateKey = await crypto.subtle.wrapKey("pkcs8", keyPair.privateKey, derivedKey, {
      name: "AES-CBC",
      iv
  });

  const b64WrappedPrivateKey = bufferToBase64(wrappedPrivateKey);
  const b64Salt = bufferToBase64(salt);
  const b64IV = bufferToBase64(iv);

  const encryptedPrivateKey = base64ToArrayBuffer(b64WrappedPrivateKey);
  const unwrapSalt = base64ToArrayBuffer(b64Salt);
  const unwrapIV = base64ToArrayBuffer(b64IV);
  const unwrapPasswordKey = await crypto.subtle.importKey("raw", textEncoder.encode(window.prompt("password unwrap")), "PBKDF2", true, ["deriveKey"]);
  const unwrappingKey = await crypto.subtle.deriveKey({
      name: "PBKDF2",
      hash: "SHA-256",
      salt: unwrapSalt,
      iterations: 210000
  }, unwrapPasswordKey, {
      name: "AES-CBC",
      length: 256
  }, true, ["wrapKey", "unwrapKey"]);

  try {
    const privateKey = await crypto.subtle.unwrapKey("pkcs8", encryptedPrivateKey, unwrappingKey, {
      name: "AES-CBC",
      iv: unwrapIV
  }, {
      name: "RSA-OAEP",
      hash: "SHA-512"
  }, true, ["encrypt", "decrypt"]);

  console.log("Success");
  } catch (err) {
    console.log(err);
  }
};

(async () => {
  await fn();
})()

Security is considered up to a certain point because this is just a demo project.


Solution

  • After debugging with the Node.js crypto library, I found out that the actual error was Unsupported key usage for an RSA-OAEP key because I was passing the encrypt usage for a RSA private key which is not possible.