first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,97 @@
# non-extractable-key
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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
```ts
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
```ts
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.