SubtleCrypto failure

For reasons unknown to me I can't wrap an RSA key with another RSA key.

A unlocked lock
Photo by iMattSmart on Unsplash

One of the features I want to build for a product is the ability to send encrypted data with the key to decrypt it. The key to decrypt the data would also be encrypted and can only be decrypted by a universal key someone else had. On request, another party could then ask the person with the universal key to send the unlock code of the encrypted key so that they could decrypt the data.

And the advice I read everywhere is that one should use wrapping for this since it adds additional layers of security. Wrapping is in essence just encrypting and then exporting a key using another key.

So all I wanted was to public-private keys, one being a universal key and the other being a decryption key that will be encrypted. Sadly, it turns out you can’t do that, and after numerous attempts and searching, I gave up.

Here is the code to setup, two functions that create the key. Since any key used for encryption can also be used for wrapping we can call the method twice and tell it will be used for something different.

async function createAesCbcKey(keyUsages /* : KeyUsage[] */){
  const iv = window.crypto.getRandomValues(new Uint8Array(16));
  const key = await window.crypto.subtle.generateKey(
    {
      name: "AES-CBC",
      length: 256
    },
    true,
    keyUsages
  );

  return { key, iv };
}

async function createRsaOaepKey(keyUsages /*: KeyUsage[] */){
  return await window.crypto.subtle.generateKey(
    {
      name: "RSA-OAEP",
      modulusLength: 4096,
      publicExponent: new Uint8Array([1, 0, 1]),
      hash: "SHA-256",
    },
    true,
    keyUsages
  );
}
😎
If you want you can just copy javascript console of your browser. The typescript has been put in comments.

And here was my first two test case that both succeeded. In both instances I would get a wrapped key. In the below examples I wrap both encryption keys (RSA and AES) with the wrapping key of the other (AES and RSA).

// Create the keys
const rsaEncryptionKey = await createRsaOaepKey(["encrypt", "decrypt"]);
const rsaWrappingKey = await createRsaOaepKey(["wrapKey", "unwrapKey"]);
const aesEncryptionKey = await createAesCbcKey(["encrypt", "decrypt"]);
const aesWrappingKey = await createAesCbcKey(["wrapKey", "unwrapKey"]);


const wrappedKeyRsaWithAes = await window.crypto.subtle.wrapKey(
  "spki",
  rsaEncryptionKey.publicKey,
  aesWrappingKey.key,
  { name: "AES-CBC", iv: aesWrappingKey.iv }
);
console.log("RsaWithAes: ", new Uint8Array(wrappedKeyRsaWithAes));

const wrappedKeyAesWithRsa = await window.crypto.subtle.wrapKey(
  "jwk",
  aesEncryptionKey.key,
  rsaWrappingKey.publicKey,
  { name: "RSA-OAEP" }
);
console.log("AesWithRsa: ", new Uint8Array(wrappedKeyAesWithRsa));

Then I tried to wrap the rsaEncryptionKey with the rsaWrappingKey and that didn't work.

// ❌ The following code doesn't work
// Each export format gave a different error. The message is complete.
// "pkcs8" -> "InvalidAccessError: The key is not of the expected type"
// "raw" -> "NotSupportedError: Unsupported export key for algorithm"
// "jwk" -> "OperationError" 
// "spki" -> "OperationError"
const wrappedKeyRsaWithRsa = await window.crypto.subtle.wrapKey(
  "spki",
  rsaEncryptionKey.publicKey,
  rsaWrappingKey.publicKey,
  { name: "RSA-OAEP" }
);
console.log("RsaWithRsa: ", new Uint8Array(wrappedKeyRsaWithRsa));

From what I read online, the creators seem to suggest that wrapping one key with a key that uses the same algorithm is a bad idea since you would be fully compromised. I say "suggest" because I can't find a source that suggests this is impossible. For example the following does work.

const wrappedKeyAesWithAeas = await window.crypto.subtle.wrapKey(
  "jwk",
  aesEncryptionKey.key,
  aesWrappingKey.key,
  { name: "AES-CBC", iv: aesWrappingKey.iv }
);
console.log("AesWithAes: ", new Uint8Array(wrappedKeyAesWithAeas));

Frankly, my idea on Sunday was overkill, and it might be better if I think of another approach. But the complete lack of error messages in a domain this complex is baffling. It does make me rethink if I should only use the encryption that the Web Crypto API provides.

Open-Source Sunday: The cryptographic proof in Sport
Building a zero trust sport application for a large audience requires the use of cryptographic proof, but how do we handle secrets?
The idea so overkill it was killed during development πŸ˜