node.jstypescriptwebcrypto-apiwebcrypto

How can I make a webcrypto CryptoKeyPair from a Uint8Array?


I'm using the webcrypto API and have a valid Ed25519 private key stored as a UInt8Array.

I would like to use the private key in KeyPair. Here's my attempt so far:

import { webcrypto } from "node:crypto";

const RANDOM_PRIVATE_KEY_DO_NOT_USE = new Uint8Array([
  144, 194, 221, 213, 246, 54, 246, 208, 227, 91, 76, 237, 35, 137, 98, 221, 82,
  87, 49, 240, 137, 166, 174, 39, 208, 143, 252, 120, 182, 49, 102, 139, 134,
  105, 228, 183, 34, 22, 11, 174, 73, 2, 12, 84, 231, 64, 186, 51, 148, 2, 41,
  156, 137, 53, 56, 50, 0, 0, 183, 231, 67, 157, 149, 110,
]);

async function privateKeyBytesToKeyPair(
  privateKeyBytes: Uint8Array
): Promise<CryptoKeyPair> {
  // Import the private key
  const privateKey = await webcrypto.subtle.importKey(
    "raw",
    privateKeyBytes.buffer,
    {
      name: "Ed25519",
    },
    true,
    // These DO match the 'Algortihm Matrix' section of the WebCrypto API
    // See https://nodejs.org/api/webcrypto.html#subtleimportkeyformat-keydata-algorithm-extractable-keyusages
    // However this fails with "Unsupported key usage for a Ed25519 key"
    ["sign", "verify"]
  );

  const keyPair: CryptoKeyPair = {
    publicKey: "TODO", // The private key is not enough to generate the public key, why do I need this?
    privateKey,
  };

  return keyPair;
}

const keyPair = await privateKeyBytesToKeyPair(RANDOM_PRIVATE_KEY_DO_NOT_USE);
console.log("Key pair:", keyPair);

I've had two problems (maybe there are more):

How can I make a webcrypto CryptoKeyPair from a Uint8Array?


Solution

  • Even for asymmetric algorithms, importKey() imports only one key at a time into a CryptoKey (either the private or the public), but does not implicitly generate a CryptoKeyPair (unlike generateKey()). If you want to create a CryptoKeyPair, this must be created explicitly in the context of the key import.
    Furthermore, the key usage must be consistent with the imported key type. In your example, a private key is imported that is not compatible with the key usage verify, which is the reason for the error message.


    The format of the Ed25519 key you posted concatenates the 32 bytes raw private key and the 32 bytes raw public key. You can find a more detailed description of this format, commonly used in NaCl / libsodium. The WebCrypto API does not support this format, so a conversion into formats suitable for the key import is necessary.

    In the following, the format conversion is described and how the key import can be carried out.


    According to the 'Import Key' section in the Secure Curves in the Web Cryptography API spec, the supported formats for Ed25519 are spki, jwk and raw for public keys and pkcs8 and jwk for private keys.

    So you can import the public key in raw format, and the private key e.g. in pkcs8 format, which for Ed25519 is made up of the concatenation of the prefix 0x302e020100300506032b657004220420 followed by the 32 bytes raw private key, as shown below:

    import { webcrypto } from "node:crypto";
    
    const fullKey = new Uint8Array([
      144, 194, 221, 213, 246, 54, 246, 208, 227, 91, 76, 237, 35, 137, 98, 221, 82,
      87, 49, 240, 137, 166, 174, 39, 208, 143, 252, 120, 182, 49, 102, 139, 134,
      105, 228, 183, 34, 22, 11, 174, 73, 2, 12, 84, 231, 64, 186, 51, 148, 2, 41,
      156, 137, 53, 56, 50, 0, 0, 183, 231, 67, 157, 149, 110,
    ]);
    const rawPrivateKey = fullKey.subarray(0, 32);
    const rawPublickey = fullKey.subarray(32, 64);
    
    const pkcs8Prefix = new Uint8Array([
      48, 46, 2, 1, 0, 48, 5, 6, 3, 43, 101, 112, 4, 34, 4, 32,
    ]); // 0x302e020100300506032b657004220420
    const pkcs8PrivateKey = new Uint8Array([...pkcs8Prefix, ...rawPrivateKey]);
    
    const privateKey = await webcrypto.subtle.importKey(
      "pkcs8",
      pkcs8PrivateKey,
      { name: "Ed25519" },
      true,
      ["sign"]
    );
    
    const publicKey = await webcrypto.subtle.importKey(
      "raw",
      rawPublickey,
      { name: "Ed25519" },
      true,
      ["verify"]
    );
    
    const keyPair: CryptoKeyPair = {
      privateKey,
      publicKey,
    };
    
    // test
    
    const message = new TextEncoder().encode(
      "The quick brown fox jumps over the lazy dog"
    );
    const signature = await webcrypto.subtle.sign(
      "ed25519",
      keyPair.privateKey,
      message
    );
    const verified = await webcrypto.subtle.verify(
      "ed25519",
      keyPair.publicKey,
      signature,
      message
    );
    
    console.log();
    console.log("signature:", signature);
    console.log("verification:", verified); // true