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
CryptoKeywithextractable: falsefor anycrypto.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 anycrypto.subtlecall- Typed
BrowserCryptoErroron 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
CryptoKeydirectly. - For asymmetric algorithms: returns the
privateKeyfrom the generatedCryptoKeyPair.
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.