HKDF Key Derivation With Web Crypto

Derive AES-GCM keys or raw bytes from a shared secret with HKDF-SHA-256, an explicit salt and context-specific info strings.

TypescriptSecurity Boundaries

Code

snippet
function assertSecret(secret: string): void {
  if (typeof secret !== "string" || secret.length === 0) {
    throw new TypeError("secret must be a non-empty string");
  }
}

export async function deriveAesKey(
  secret: string,
  salt: Uint8Array,
  info: string,
): Promise<CryptoKey> {
  assertSecret(secret);
  if (!(salt instanceof Uint8Array)) throw new TypeError("salt must be a Uint8Array");
  if (typeof info !== "string") throw new TypeError("info must be a string");
  const material = await crypto.subtle.importKey(
    "raw",
    new TextEncoder().encode(secret),
    "HKDF",
    false,
    ["deriveKey"],
  );
  return crypto.subtle.deriveKey(
    { name: "HKDF", hash: "SHA-256", salt, info: new TextEncoder().encode(info) },
    material,
    { name: "AES-GCM", length: 256 },
    false,
    ["encrypt", "decrypt"],
  );
}

export async function deriveBytes(
  secret: string,
  salt: Uint8Array,
  info: string,
  length = 32,
): Promise<Uint8Array> {
  assertSecret(secret);
  if (!(salt instanceof Uint8Array)) throw new TypeError("salt must be a Uint8Array");
  if (typeof info !== "string") throw new TypeError("info must be a string");
  if (!Number.isInteger(length) || length < 1 || length > 255) {
    throw new RangeError("length must be an integer between 1 and 255");
  }
  const material = await crypto.subtle.importKey(
    "raw",
    new TextEncoder().encode(secret),
    "HKDF",
    false,
    ["deriveBits"],
  );
  const bits = await crypto.subtle.deriveBits(
    { name: "HKDF", hash: "SHA-256", salt, info: new TextEncoder().encode(info) },
    material,
    length * 8,
  );
  return new Uint8Array(bits);
}