Files
skills/docs/security-stack/crypto/crypto-browser/src/non-extractable-key/non-extractable-key.md
2026-05-29 08:20:56 +01:00

4.5 KiB

non-extractable-key

(no submodules)

Purpose

Provides nonExtractableKey(), a helper that generates a CryptoKey with extractable: false. Key material generated this way never leaves the cryptographic engine — it cannot be read by JavaScript code via crypto.subtle.exportKey. The key handle can be used for cryptographic operations but its raw bytes are permanently inaccessible, providing a hardware-security-module-like boundary in software.

This is the correct approach whenever keys must remain hardware-bound or compliance requirements prohibit key export. Examples include session-scoped encryption keys, FIDO2-style signing keys, and keys derived from hardware-backed key material in PWA contexts. The non-extractability also means that if an adversary gains script execution access, they cannot exfiltrate the key bytes — they can only use the key handle for operations it was created for.

Features

  • Generates a CryptoKey with extractable: false for any crypto.subtle-supported algorithm
  • Supports symmetric algorithms: AES-GCM, AES-CBC, AES-KW, HMAC
  • Supports asymmetric algorithms: ECDSA (P-256, P-384), ECDH (P-256, P-384), Ed25519, X25519
  • For asymmetric algorithms, returns only the private key (the non-extractable half)
  • assertWebCryptoAvailable() guard before any crypto.subtle call
  • Typed BrowserCryptoError on generation failure

Usage: Basic Example

import { nonExtractableKey } from "@suaveplan/crypto-browser/non-extractable-key/non-extractable-key";

// Generate a non-extractable AES-256-GCM key for session encryption
const sessionKey = await nonExtractableKey(
  { name: "AES-GCM", length: 256 },
  ["encrypt", "decrypt"]
);

// sessionKey.extractable === false
// crypto.subtle.exportKey("raw", sessionKey) will throw

const nonce = crypto.getRandomValues(new Uint8Array(12));
const ciphertext = await crypto.subtle.encrypt(
  { name: "AES-GCM", iv: nonce },
  sessionKey,
  new TextEncoder().encode("private data")
);

Usage: Advanced Example

import { nonExtractableKey } from "@suaveplan/crypto-browser/non-extractable-key/non-extractable-key";

// Hardware-bound ECDSA signing key — private key material never leaves the engine
async function createBoundSigningKey(): Promise<CryptoKey> {
  return nonExtractableKey(
    { name: "ECDSA", namedCurve: "P-256" },
    ["sign"]
  );
}

// Use the key for signing without any risk of key extraction
async function signChallenge(
  key: CryptoKey,
  challenge: Uint8Array
): Promise<Uint8Array> {
  const sig = await crypto.subtle.sign(
    { name: "ECDSA", hash: "SHA-256" },
    key,
    challenge
  );
  return new Uint8Array(sig);
}

// Store the key in IndexedDB for persistence (CryptoKey is structured-cloneable)
// async function storeKey(db: IDBDatabase, key: CryptoKey): Promise<void> { ... }

API Reference

nonExtractableKey(algorithm, usages): Promise<CryptoKey>

Generates a non-extractable CryptoKey.

Parameter Type Description
algorithm AlgorithmIdentifier | RsaHashedKeyGenParams | EcKeyGenParams | HmacKeyGenParams | AesKeyGenParams Key generation algorithm descriptor
usages KeyUsage[] Intended key operations, e.g. ["encrypt", "decrypt"]

Returns: Promise<CryptoKey> with extractable === false.

  • For symmetric algorithms: returns the CryptoKey directly.
  • For asymmetric algorithms: returns the privateKey from the generated CryptoKeyPair.

Throws: BrowserCryptoError — BROWSER_CRYPTO.KEY.GENERATION_FAILED or BROWSER_CRYPTO.NOT_AVAILABLE.

Implementation Notes

The implementation calls crypto.subtle.generateKey with extractable: false. The Web Crypto API guarantees that once a key is created non-extractable, no subsequent API call can extract the raw bytes — exportKey will throw InvalidAccessError unconditionally.

For asymmetric algorithms, generateKey returns a CryptoKeyPair. The function detects this via "privateKey" in result and returns only the private key. The public key is discarded; if callers need the public key they should use the specific engine modules (ecdsaP384.generateKeypair(), etc.) which export both.

Non-extractable keys are structured-cloneable and can be persisted to IndexedDB, shared with BroadcastChannel, or transferred to Worker contexts via postMessage — without ever exposing the raw key bytes. This makes them suitable for long-lived application keys that survive page reloads.