first commit
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user