Move docs/ package mirror into document-navigator plugin

Relocate the ~6k-file docs tree from the repo root into
plugins/document-navigator/docs/ so the plugin is self-contained and
portable. docs/superpowers/ (specs and plans) stays at the repo root.

Moved via cp+rm rather than git mv because directory rename() fails with
EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the
identical-content files at commit time.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-30 01:10:00 +01:00
parent fe3fedf42e
commit 9bf56a806b
6022 changed files with 12437 additions and 0 deletions

View File

@@ -0,0 +1,16 @@
# @suaveplan/crypto-browser Changelog
## 0.0.0 — suaveplan initial
- Initial implementation: WebCrypto-backed AEAD, sign, and agree engines
- `webCryptoAeadEngine` — AES-256-GCM via `crypto.subtle`
- `webCryptoSignEngine` / `webCryptoEd25519Engine` / `webCryptoEcdsaP256Engine` — Ed25519 and ECDSA-P256
- `webCryptoAgreeEngine` / `webCryptoX25519Engine` / `webCryptoEcdhP256Engine` — X25519 and ECDH-P256
- `rsa` namespace — RSA-PSS, RSA-OAEP, RSASSA-PKCS1-v1_5
- `aesKw` namespace — AES Key Wrap (RFC 3394)
- `ecdsaP384` namespace — ECDSA with P-384 / SHA-384
- `ecdhP384` namespace — ECDH with P-384
- `nonExtractableKey()` — non-extractable CryptoKey generation
- `isWebCryptoAvailable()` / `assertWebCryptoAvailable()` — runtime checks
- `BrowserCryptoError` with codes: `NOT_AVAILABLE`, `KEY.GENERATION_FAILED`, `OPERATION.FAILED`
- 100% coverage threshold (statements, branches, functions, lines)

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,106 @@
# aes-kw
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Provides AES Key Wrap (AES-KW, RFC 3394) operations via `crypto.subtle`. AES-KW is an authenticated key-wrapping algorithm that encapsulates cryptographic key material with integrity protection, producing output that is 8 bytes longer than the input. It is widely used in key management systems, JOSE (JSON Web Encryption), CMS enveloped-data, and TLS session ticket encryption.
Unlike general-purpose AES-GCM, AES-KW is specifically designed for wrapping other cryptographic keys. The algorithm provides integrity verification — any tampering with the wrapped output will be detected during unwrapping.
## Features
- `wrapKey(wrappingKey, keyToWrap)` — wraps raw key bytes using AES-KW via `crypto.subtle.wrapKey`
- `unwrapKey(wrappingKey, wrappedKey)` — unwraps and returns raw key bytes via `crypto.subtle.unwrapKey`
- Supports 128, 192, and 256-bit (16, 24, 32-byte) wrapping keys
- Input key must be a multiple of 8 bytes (AES-KW requirement)
- Wrapped output is input length + 8 (integrity check value)
- `assertWebCryptoAvailable()` guard on every call
- Typed `BrowserCryptoError` on all failure paths
## Usage: Basic Example
```ts
import { aesKw } from "@suaveplan/crypto-browser/aes-kw/aes-kw";
const wrappingKey = crypto.getRandomValues(new Uint8Array(32)); // 256-bit KEK
const aesKey = crypto.getRandomValues(new Uint8Array(32)); // 256-bit content key
const wrapped = await aesKw.wrapKey(wrappingKey, aesKey);
// wrapped.length === aesKey.length + 8 === 40
const recovered = await aesKw.unwrapKey(wrappingKey, wrapped);
// recovered deepEquals aesKey
```
## Usage: Advanced Example
```ts
import { aesKw } from "@suaveplan/crypto-browser/aes-kw/aes-kw";
import { webCryptoAeadEngine } from "@suaveplan/crypto-browser/engines/webcrypto-aead-engine";
import { BrowserCryptoError } from "@suaveplan/crypto-browser/errors";
// Key Encryption Key (KEK) derived from a user password or stored securely
declare const kek: Uint8Array; // 32-byte wrapping key
// Wrap content encryption key for storage or transmission
async function storeWrappedKey(contentKey: Uint8Array): Promise<Uint8Array> {
try {
return await aesKw.wrapKey(kek, contentKey);
} catch (err) {
if (err instanceof BrowserCryptoError && err.code === "BROWSER_CRYPTO.OPERATION.FAILED") {
throw new Error("Key wrapping failed — check wrapping key size");
}
throw err;
}
}
// Later: unwrap and use the content key
async function loadAndEncrypt(
wrappedKey: Uint8Array,
plaintext: Uint8Array
): Promise<Uint8Array> {
const contentKey = await aesKw.unwrapKey(kek, wrappedKey);
const nonce = crypto.getRandomValues(new Uint8Array(12));
return webCryptoAeadEngine.encrypt(contentKey, plaintext, nonce) as unknown as Promise<Uint8Array>;
}
```
## API Reference
### `aesKw.wrapKey(wrappingKey, keyToWrap): Promise<Uint8Array>`
Wraps `keyToWrap` using AES-KW.
| Parameter | Type | Description |
|-----------|------|-------------|
| `wrappingKey` | `Uint8Array` | 16, 24, or 32-byte AES Key Encryption Key (KEK) |
| `keyToWrap` | `Uint8Array` | Key bytes to wrap; must be a multiple of 8 bytes |
**Returns:** `Promise<Uint8Array>` — wrapped bytes, `keyToWrap.length + 8` bytes long.
**Throws:** `BrowserCryptoError` — `BROWSER_CRYPTO.NOT_AVAILABLE` or `BROWSER_CRYPTO.OPERATION.FAILED`.
---
### `aesKw.unwrapKey(wrappingKey, wrappedKey): Promise<Uint8Array>`
Unwraps `wrappedKey` using AES-KW and returns the original key bytes.
| Parameter | Type | Description |
|-----------|------|-------------|
| `wrappingKey` | `Uint8Array` | 16, 24, or 32-byte KEK (must match wrapping key) |
| `wrappedKey` | `Uint8Array` | Previously wrapped bytes from `wrapKey()` |
**Returns:** `Promise<Uint8Array>` — original raw key bytes.
**Throws:** `BrowserCryptoError` — `BROWSER_CRYPTO.OPERATION.FAILED` when integrity check fails (wrong key or tampered data).
## Implementation Notes
`crypto.subtle.wrapKey` requires a `CryptoKey` object as input (not a raw `Uint8Array`). The implementation imports `keyToWrap` as a temporary `AES-GCM` key with `extractable: true` before wrapping — this is the minimal ceremony needed to satisfy the `crypto.subtle` API. The temporary key is never exported directly; only the AES-KW wrapped form is returned.
Similarly, `crypto.subtle.unwrapKey` produces a `CryptoKey` internally; the implementation immediately re-exports it as raw bytes via `exportKey("raw", ...)` so the caller receives a portable `Uint8Array`. This round-trip through `CryptoKey` is required by the Web Crypto API and adds negligible overhead.
AES-KW does not use a nonce — it is deterministic for a given (wrapping-key, key-to-wrap) pair. This is intentional for RFC 3394 compliance; nonce-based confidentiality is provided by AES-GCM when encrypting arbitrary data.

View File

@@ -0,0 +1,85 @@
# availability
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Provides a runtime check for the presence of the Web Crypto API (`crypto.subtle`). The check is a prerequisite for every operation in `@suaveplan/crypto-browser`: all engine methods and primitives call `assertWebCryptoAvailable()` before attempting any `crypto.subtle` call, so callers receive a structured, typed `BrowserCryptoError` rather than an opaque `TypeError: Cannot read properties of undefined (reading 'subtle')`.
The two exported helpers form a small contract: `isWebCryptoAvailable` is a pure predicate useful for conditional branching or feature-flag evaluation, while `assertWebCryptoAvailable` is the guard variant used internally throughout the package.
## Features
- `isWebCryptoAvailable(): boolean` — returns `true` when both `globalThis.crypto` and `globalThis.crypto.subtle` are defined; otherwise `false`
- `assertWebCryptoAvailable(): void` — throws `BrowserCryptoError` with code `BROWSER_CRYPTO.NOT_AVAILABLE` if the check fails
- Zero side effects — purely reads globals, never modifies them
- Works across Node 20+, Bun, Deno, and modern browsers without polyfills
## Usage: Basic Example
```ts
import { isWebCryptoAvailable } from "@suaveplan/crypto-browser/availability/availability";
if (!isWebCryptoAvailable()) {
console.warn("WebCrypto not available — using fallback cipher");
}
```
## Usage: Advanced Example
```ts
import { assertWebCryptoAvailable } from "@suaveplan/crypto-browser/availability/availability";
import { BrowserCryptoError } from "@suaveplan/crypto-browser/errors";
async function encryptUserData(key: Uint8Array, data: Uint8Array): Promise<Uint8Array> {
// Guard before any crypto.subtle calls
assertWebCryptoAvailable();
// ... proceed with crypto.subtle.encrypt
return new Uint8Array();
}
// Caller can catch the specific structured error
try {
await encryptUserData(key, data);
} catch (err) {
if (err instanceof BrowserCryptoError && err.code === "BROWSER_CRYPTO.NOT_AVAILABLE") {
// Fall back to a server-side encryption endpoint
}
}
```
## API Reference
### `isWebCryptoAvailable(): boolean`
Returns `true` when `typeof crypto !== 'undefined' && typeof crypto.subtle !== 'undefined'`.
| Parameter | Type | Description |
|-----------|------|-------------|
| _(none)_ | — | — |
**Returns:** `boolean` — `true` if `crypto.subtle` is accessible.
**Throws:** never.
---
### `assertWebCryptoAvailable(): void`
Calls `isWebCryptoAvailable()` and throws if the result is `false`.
| Parameter | Type | Description |
|-----------|------|-------------|
| _(none)_ | — | — |
**Returns:** `void`
**Throws:** `BrowserCryptoError` — code `BROWSER_CRYPTO.NOT_AVAILABLE` when `crypto.subtle` is absent.
## Implementation Notes
The check deliberately uses `typeof crypto !== 'undefined'` (rather than a `try/catch` or optional chaining on a possibly-unset global) to avoid triggering a `ReferenceError` in environments where `crypto` is not declared at all. Optional chaining `globalThis?.crypto?.subtle` is not used because `globalThis` itself might be unavailable in some older embedded runtimes.
Both exports are side-effect-free and tree-shakeable: `"sideEffects": false` is set on the package, so bundlers that import only `isWebCryptoAvailable` will not include `BrowserCryptoError` in the output unless it is actually used.

View File

@@ -0,0 +1,105 @@
# ecdh-p384
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Provides Elliptic Curve Diffie-Hellman key agreement using the NIST P-384 curve via `crypto.subtle`. P-384 ECDH delivers 192 bits of security, matching the security level of AES-256 and making it appropriate for long-term key agreement in high-security contexts. This module is the P-384 analogue of the `webCryptoEcdhP256Engine` provided in the engines module, exposed as a standalone namespace for direct use outside of the `AgreeEngine` interface.
Key agreement produces a 48-byte (384-bit) shared secret by calling `crypto.subtle.deriveBits` with `length: 384`. Callers are responsible for passing this secret through a KDF before using it as an encryption key.
## Features
- `generateKeypair()` — generates a P-384 ECDH keypair via `crypto.subtle.generateKey`
- `agree(privateKey, publicKey)` — derives 48-byte shared secret via `crypto.subtle.deriveBits`
- Full DH symmetry: `agree(alice, bob.pub)` equals `agree(bob, alice.pub)`
- Public key: 97-byte uncompressed point (raw format, 0x04 prefix)
- Private key: PKCS8 DER bytes
- `assertWebCryptoAvailable()` guard on every call
- Typed `BrowserCryptoError` for all failure paths
## Usage: Basic Example
```ts
import { ecdhP384 } from "@suaveplan/crypto-browser/ecdh-p384/ecdh-p384";
const alice = await ecdhP384.generateKeypair();
const bob = await ecdhP384.generateKeypair();
// Both sides independently compute the same 48-byte secret
const aliceShared = await ecdhP384.agree(alice.privateKey, bob.publicKey);
const bobShared = await ecdhP384.agree(bob.privateKey, alice.publicKey);
// aliceShared deepEquals bobShared — 48 bytes (384 bits)
```
## Usage: Advanced Example
```ts
import { ecdhP384 } from "@suaveplan/crypto-browser/ecdh-p384/ecdh-p384";
// KDF using HKDF to derive a 32-byte AES key from the P-384 shared secret
async function deriveAesKey(
privateKey: Uint8Array,
peerPublicKey: Uint8Array,
info: Uint8Array
): Promise<Uint8Array> {
const sharedSecret = await ecdhP384.agree(privateKey, peerPublicKey);
// Import shared secret as HKDF key material
const hkdfKey = await crypto.subtle.importKey(
"raw",
sharedSecret,
"HKDF",
false,
["deriveBits"]
);
const aesKeyBits = await crypto.subtle.deriveBits(
{
name: "HKDF",
hash: "SHA-384",
salt: new Uint8Array(48), // zero salt
info,
},
hkdfKey,
256
);
return new Uint8Array(aesKeyBits);
}
```
## API Reference
### `ecdhP384.generateKeypair(): Promise<{ publicKey: Uint8Array; privateKey: Uint8Array }>`
Generates a P-384 ECDH keypair.
**Returns:** `Promise<{ publicKey: Uint8Array; privateKey: Uint8Array }>` — 97-byte uncompressed public key and PKCS8 DER private key.
**Throws:** `BrowserCryptoError` — `BROWSER_CRYPTO.KEY.GENERATION_FAILED` or `BROWSER_CRYPTO.NOT_AVAILABLE`.
---
### `ecdhP384.agree(privateKey, publicKey): Promise<Uint8Array>`
Performs ECDH key agreement, deriving 384 bits from `deriveBits`.
| Parameter | Type | Description |
|-----------|------|-------------|
| `privateKey` | `Uint8Array` | PKCS8 DER private key bytes |
| `publicKey` | `Uint8Array` | 97-byte uncompressed P-384 public key |
**Returns:** `Promise<Uint8Array>` — 48-byte (384-bit) shared secret.
**Throws:** `BrowserCryptoError` — `BROWSER_CRYPTO.OPERATION.FAILED` or `BROWSER_CRYPTO.NOT_AVAILABLE`.
## Implementation Notes
P-384 `deriveBits` is called with `length: 384` to extract the full 384-bit shared secret. This matches the curve's security level and avoids the truncation that would occur if 256 bits were requested. Callers using the output as an AES-256 key should pass through HKDF first to properly condition the key material.
The public key is exported from `crypto.subtle` via `"raw"` format, which produces the standard uncompressed point encoding. This is directly importable on any Web Crypto implementation that supports P-384.
P-384 is approximately 2–3× slower than P-256 for key generation and agreement. For applications where P-256's 128-bit security is sufficient, `webCryptoEcdhP256Engine` provides the same API with better performance.

View File

@@ -0,0 +1,117 @@
# ecdsa-p384
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Provides ECDSA (Elliptic Curve Digital Signature Algorithm) operations using the NIST P-384 curve and SHA-384 hash, backed by `crypto.subtle`. P-384 offers 192 bits of security — stronger than P-256's 128 bits — and is required by many compliance frameworks (NIST SP 800-57, CNSA Suite 2.0, NSA Suite B). This module is intended for contexts where P-384 is mandated by policy or interoperability requirements.
The public key format is the 97-byte uncompressed point representation (0x04 prefix followed by 48-byte X and Y coordinates). The private key is PKCS8 DER. Both formats are standard and directly importable by other Web Crypto implementations.
## Features
- `generateKeypair()` — generates a P-384 ECDSA keypair using `crypto.subtle.generateKey`
- `sign(privateKey, message)` — signs with ECDSA-P384 / SHA-384
- `verify(publicKey, signature, message)` — verifies an ECDSA-P384 signature
- Public key: 97-byte uncompressed point (raw format)
- Private key: PKCS8 DER bytes
- Signature format: IEEE P1363 (R || S) as returned by `crypto.subtle`
- `assertWebCryptoAvailable()` guard on every call
- Typed `BrowserCryptoError` for all failure paths
## Usage: Basic Example
```ts
import { ecdsaP384 } from "@suaveplan/crypto-browser/ecdsa-p384/ecdsa-p384";
const kp = await ecdsaP384.generateKeypair();
// kp.publicKey is 97 bytes (uncompressed P-384 point)
// kp.privateKey is PKCS8 DER bytes
const message = new TextEncoder().encode("document to sign");
const signature = await ecdsaP384.sign(kp.privateKey, message);
const valid = await ecdsaP384.verify(kp.publicKey, signature, message);
console.log(valid); // true
```
## Usage: Advanced Example
```ts
import { ecdsaP384 } from "@suaveplan/crypto-browser/ecdsa-p384/ecdsa-p384";
import { BrowserCryptoError } from "@suaveplan/crypto-browser/errors";
// Issue a signed certificate-like structure
async function issueAttestation(
issuerPrivateKey: Uint8Array,
payload: Uint8Array
): Promise<{ signature: Uint8Array; algorithm: string }> {
try {
const signature = await ecdsaP384.sign(issuerPrivateKey, payload);
return { signature, algorithm: "ECDSA-P384-SHA384" };
} catch (err) {
if (err instanceof BrowserCryptoError) {
throw new Error(`Signing failed: ${err.code}`);
}
throw err;
}
}
// Verify the attestation with a stored public key
async function verifyAttestation(
publicKey: Uint8Array,
payload: Uint8Array,
signature: Uint8Array
): Promise<boolean> {
return ecdsaP384.verify(publicKey, signature, payload);
}
```
## API Reference
### `ecdsaP384.generateKeypair(): Promise<{ publicKey: Uint8Array; privateKey: Uint8Array }>`
Generates a P-384 ECDSA keypair.
**Returns:** `Promise<{ publicKey: Uint8Array; privateKey: Uint8Array }>` — 97-byte uncompressed public key and PKCS8 DER private key.
**Throws:** `BrowserCryptoError` — `BROWSER_CRYPTO.KEY.GENERATION_FAILED` or `BROWSER_CRYPTO.NOT_AVAILABLE`.
---
### `ecdsaP384.sign(privateKey, message): Promise<Uint8Array>`
Signs `message` using ECDSA-P384 with SHA-384.
| Parameter | Type | Description |
|-----------|------|-------------|
| `privateKey` | `Uint8Array` | PKCS8 DER private key bytes |
| `message` | `Uint8Array` | Bytes to sign |
**Returns:** `Promise<Uint8Array>` — IEEE P1363 signature (96 bytes for P-384).
**Throws:** `BrowserCryptoError` — `BROWSER_CRYPTO.OPERATION.FAILED` or `BROWSER_CRYPTO.NOT_AVAILABLE`.
---
### `ecdsaP384.verify(publicKey, signature, message): Promise<boolean>`
Verifies an ECDSA-P384 / SHA-384 signature.
| Parameter | Type | Description |
|-----------|------|-------------|
| `publicKey` | `Uint8Array` | 97-byte uncompressed P-384 public key |
| `signature` | `Uint8Array` | IEEE P1363 signature bytes |
| `message` | `Uint8Array` | Original message bytes |
**Returns:** `Promise<boolean>` — `true` if signature is valid.
**Throws:** `BrowserCryptoError` — `BROWSER_CRYPTO.OPERATION.FAILED` or `BROWSER_CRYPTO.NOT_AVAILABLE`.
## Implementation Notes
SHA-384 is paired with P-384 because the security level of the hash should match that of the curve. Using SHA-256 with P-384 would create a mismatch: the 192-bit curve security would be limited to the 128-bit collision resistance of SHA-256.
The signature output format from `crypto.subtle.sign` for ECDSA is IEEE P1363 (raw R || S concatenation), not DER/ASN.1. This differs from OpenSSL's default DER encoding. When interoperating with systems that expect DER signatures, a conversion layer is needed.
P-384 key generation is typically 2–5× slower than P-256 due to the larger field size. For high-throughput signing workloads where P-256 security is sufficient, consider using `webCryptoEcdsaP256Engine` instead.

View File

@@ -0,0 +1,108 @@
# webcrypto-aead-engine
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Provides an `AeadEngine` implementation that delegates all AES-256-GCM operations to the browser's (or Node's) native `crypto.subtle` API. This lets `@suaveplan/crypto` facade functions — `encrypt()` and `decrypt()` from the `aead` sub-module — run on hardware-accelerated paths wherever the Web Crypto API is available, instead of the default pure-JavaScript `@noble/ciphers` path.
The engine is designed for environments where minimising bundle size matters most: importing it adds zero third-party cryptography code because everything is provided by the host runtime.
## Features
- `encrypt(key, plaintext, nonce, aad?)` — AES-256-GCM encryption via `crypto.subtle.encrypt`, including automatic 16-byte authentication tag appended to ciphertext
- `decrypt(key, ciphertext, nonce, aad?)` — AES-256-GCM decryption via `crypto.subtle.decrypt` with authenticated tag verification
- Optional additional authenticated data (AAD) for binding ciphertext to a context
- Throws typed `BrowserCryptoError` with stable codes for every failure path
- Returns `Promise<Uint8Array>` — engine methods are async even though the `AeadEngine` interface declares sync return types; facade functions accept Promise results
- Guards every call with `assertWebCryptoAvailable()` to produce a structured error instead of a runtime `TypeError`
## Usage: Basic Example
```ts
import { webCryptoAeadEngine } from "@suaveplan/crypto-browser/engines/webcrypto-aead-engine";
const key = crypto.getRandomValues(new Uint8Array(32));
const nonce = crypto.getRandomValues(new Uint8Array(12));
const plaintext = new TextEncoder().encode("hello world");
const ciphertext = await (webCryptoAeadEngine.encrypt(key, plaintext, nonce) as unknown as Promise<Uint8Array>);
const recovered = await (webCryptoAeadEngine.decrypt(key, ciphertext, nonce) as unknown as Promise<Uint8Array>);
console.log(new TextDecoder().decode(recovered)); // "hello world"
```
## Usage: Advanced Example
```ts
import { encrypt } from "@suaveplan/crypto/aead";
import { webCryptoAeadEngine } from "@suaveplan/crypto-browser/engines/webcrypto-aead-engine";
import { BrowserCryptoError } from "@suaveplan/crypto-browser/errors";
async function sealPayload(
key: Uint8Array,
payload: Uint8Array,
context: Uint8Array // AAD binds the ciphertext to e.g. a session ID
): Promise<Uint8Array> {
try {
const nonce = crypto.getRandomValues(new Uint8Array(12));
const sealed = await (webCryptoAeadEngine.encrypt(key, payload, nonce, context) as unknown as Promise<Uint8Array>);
// Prepend nonce for transport: [12 bytes nonce | ciphertext+tag]
const out = new Uint8Array(12 + sealed.length);
out.set(nonce, 0);
out.set(sealed, 12);
return out;
} catch (err) {
if (err instanceof BrowserCryptoError) {
// err.code is one of: BROWSER_CRYPTO.NOT_AVAILABLE | BROWSER_CRYPTO.OPERATION.FAILED
throw err;
}
throw err;
}
}
```
## API Reference
### `webCryptoAeadEngine: AeadEngine`
An object conforming to the `AeadEngine` interface from `@suaveplan/crypto/engines`.
#### `.encrypt(key, plaintext, nonce, aad?): Uint8Array`
Encrypts plaintext using AES-256-GCM.
| Parameter | Type | Description |
|-----------|------|-------------|
| `key` | `Uint8Array` | 32-byte (256-bit) AES key |
| `plaintext` | `Uint8Array` | Bytes to encrypt |
| `nonce` | `Uint8Array` | 12-byte IV (use a fresh random nonce per message) |
| `aad` | `Uint8Array \| undefined` | Optional additional authenticated data |
**Returns:** `Uint8Array` (runtime: `Promise<Uint8Array>`) — ciphertext with 16-byte GCM tag appended.
**Throws:** `BrowserCryptoError` — `BROWSER_CRYPTO.NOT_AVAILABLE` or `BROWSER_CRYPTO.OPERATION.FAILED`.
#### `.decrypt(key, ciphertext, nonce, aad?): Uint8Array`
Decrypts ciphertext and verifies the GCM authentication tag.
| Parameter | Type | Description |
|-----------|------|-------------|
| `key` | `Uint8Array` | 32-byte AES key (must match encryption key) |
| `ciphertext` | `Uint8Array` | Ciphertext with 16-byte GCM tag appended |
| `nonce` | `Uint8Array` | 12-byte IV used during encryption |
| `aad` | `Uint8Array \| undefined` | Optional AAD (must match encryption AAD exactly) |
**Returns:** `Uint8Array` (runtime: `Promise<Uint8Array>`) — decrypted plaintext.
**Throws:** `BrowserCryptoError` — `BROWSER_CRYPTO.OPERATION.FAILED` on authentication failure or bad key.
## Implementation Notes
`crypto.subtle.importKey` is called on every encrypt/decrypt invocation rather than being cached, because raw `Uint8Array` keys are the idiomatic interface in `AeadEngine` and caching a `CryptoKey` across calls would require a `Map<Uint8Array, CryptoKey>` with careful lifecycle management. The overhead of `importKey` is dominated by the actual AES-GCM operation for typical payload sizes.
The `AeadEngine` interface uses synchronous return types (`Uint8Array`), while `crypto.subtle` is entirely asynchronous. The implementation returns `Promise<Uint8Array>` cast to `Uint8Array` at the type level. Facade functions in `@suaveplan/crypto` use `await` on engine results, so this is transparent to consumers using those facades.
The 12-byte nonce is the NIST-recommended IV length for AES-GCM and provides the best performance with `crypto.subtle`. Longer IVs (96–128 bits) would work but require internal counter splitting which is not done here — callers must supply exactly a 12-byte nonce for maximum compatibility.

View File

@@ -0,0 +1,118 @@
# webcrypto-agree-engine
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Provides two `AgreeEngine` implementations — `webCryptoX25519Engine` (X25519 Diffie-Hellman) and `webCryptoEcdhP256Engine` (ECDH with P-256) — both backed by the native `crypto.subtle` API. A convenience alias `webCryptoAgreeEngine` resolves to the X25519 engine. These engines implement the `AgreeEngine` interface from `@suaveplan/crypto/engines`, making them drop-in substitutes for the default `@noble/curves`-backed engine in environments where `crypto.subtle` is available.
Key agreement is the foundation of forward-secrecy protocols (TLS, Noise, Signal). Using `crypto.subtle` for ECDH ensures hardware acceleration is used when the runtime supports it, and the key material never leaves the key management subsystem if non-extractable keys are used.
## Features
- `generateKeypair()` — generates an ephemeral X25519 or ECDH-P256 keypair; returns portable raw bytes
- `agree(mySecret, theirPublic)` — derives a 32-byte shared secret via `crypto.subtle.deriveBits`
- Full DH symmetry: `agree(alice, bob.pub) === agree(bob, alice.pub)` for any keypair pair
- All methods are async at runtime despite `AgreeEngine` sync declarations
- `assertWebCryptoAvailable()` guard on every call
- Typed `BrowserCryptoError` on generation or agreement failure
## Usage: Basic Example
```ts
import { webCryptoX25519Engine } from "@suaveplan/crypto-browser/engines/webcrypto-agree-engine";
const alice = await (webCryptoX25519Engine.generateKeypair() as unknown as Promise<{
publicKey: Uint8Array;
secretKey: Uint8Array;
}>);
const bob = await (webCryptoX25519Engine.generateKeypair() as unknown as Promise<{
publicKey: Uint8Array;
secretKey: Uint8Array;
}>);
const aliceSecret = await (webCryptoX25519Engine.agree(alice.secretKey, bob.publicKey) as unknown as Promise<Uint8Array>);
const bobSecret = await (webCryptoX25519Engine.agree(bob.secretKey, alice.publicKey) as unknown as Promise<Uint8Array>);
// aliceSecret deepEquals bobSecret — both parties derived the same 32 bytes
```
## Usage: Advanced Example
```ts
import { webCryptoX25519Engine } from "@suaveplan/crypto-browser/engines/webcrypto-agree-engine";
import { webCryptoAeadEngine } from "@suaveplan/crypto-browser/engines/webcrypto-aead-engine";
// Simple Diffie-Hellman key exchange followed by AES-GCM encryption
async function ephemeralEncrypt(
recipientPublicKey: Uint8Array,
plaintext: Uint8Array
): Promise<{ ephemeralPublic: Uint8Array; ciphertext: Uint8Array }> {
const ephemeral = await (webCryptoX25519Engine.generateKeypair() as unknown as Promise<{
publicKey: Uint8Array;
secretKey: Uint8Array;
}>);
const sharedSecret = await (webCryptoX25519Engine.agree(
ephemeral.secretKey,
recipientPublicKey
) as unknown as Promise<Uint8Array>);
const nonce = crypto.getRandomValues(new Uint8Array(12));
const ciphertext = await (webCryptoAeadEngine.encrypt(
sharedSecret,
plaintext,
nonce
) as unknown as Promise<Uint8Array>);
const out = new Uint8Array(12 + ciphertext.length);
out.set(nonce, 0);
out.set(ciphertext, 12);
return { ephemeralPublic: ephemeral.publicKey, ciphertext: out };
}
```
## API Reference
### `webCryptoX25519Engine: AgreeEngine`
X25519 engine via `crypto.subtle` with `{ name: "X25519" }`.
#### `.generateKeypair(): AgreeKeypair`
Returns `Promise<AgreeKeypair>` at runtime. Public key is 32 raw bytes. Secret key is 32-byte scalar.
**Throws:** `BrowserCryptoError` — `BROWSER_CRYPTO.NOT_AVAILABLE` or `BROWSER_CRYPTO.KEY.GENERATION_FAILED`.
#### `.agree(mySecret, theirPublic): Uint8Array`
Returns `Promise<Uint8Array>` (32 bytes) at runtime.
| Parameter | Type | Description |
|-----------|------|-------------|
| `mySecret` | `Uint8Array` | 32-byte X25519 private scalar |
| `theirPublic` | `Uint8Array` | 32-byte X25519 public key |
**Throws:** `BrowserCryptoError` — `BROWSER_CRYPTO.NOT_AVAILABLE` or `BROWSER_CRYPTO.OPERATION.FAILED`.
---
### `webCryptoEcdhP256Engine: AgreeEngine`
ECDH-P256 engine via `crypto.subtle`. Public key is 65-byte uncompressed point. Secret key is PKCS8 DER. `agree()` returns 32 bytes via `deriveBits(..., 256)`.
---
### `webCryptoAgreeEngine: AgreeEngine`
Alias for `webCryptoX25519Engine`.
## Implementation Notes
X25519 public keys are exported from `crypto.subtle` in SPKI format and the raw 32-byte scalar is extracted from the last 32 bytes of the DER envelope. On import the scalar is re-wrapped with a minimal 48-byte PKCS8 structure. This keeps the external representation simple (just 32 bytes) while satisfying `crypto.subtle`'s requirement for DER-encoded keys.
For ECDH-P256 the key agreement produces a 256-bit shared secret by calling `deriveBits` with `length: 256`. The raw shared secret should be passed through a KDF (e.g., HKDF) before use as an encryption key; this is not done inside the engine so callers retain full control over key derivation.
X25519 requires Node 20+, Bun 1.0+, or Chrome 113+/Firefox 130+/Safari 15.4+. Older environments will receive `BROWSER_CRYPTO.KEY.GENERATION_FAILED`.

View File

@@ -0,0 +1,119 @@
# webcrypto-sign-engine
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Provides two `SignEngine` implementations — `webCryptoEd25519Engine` (Ed25519) and `webCryptoEcdsaP256Engine` (ECDSA with P-256) — both backed by `crypto.subtle`. The module also exports a convenience alias `webCryptoSignEngine` that resolves to the Ed25519 engine. These engines implement the `SignEngine` interface from `@suaveplan/crypto/engines`, making them drop-in replacements for the default `@noble/curves`-backed engine wherever `crypto.subtle` is available.
Ed25519 provides faster key generation, smaller signatures (64 bytes), and stronger security properties for most use-cases. ECDSA-P256 is provided for interoperability with systems that require NIST curves.
## Features
- `generateKeypair()` — generates an ephemeral Ed25519 or ECDSA-P256 keypair; returns raw public key bytes and raw/PKCS8 private key bytes
- `sign(secretKey, message)` — signs an arbitrary byte array using `crypto.subtle.sign`
- `verify(publicKey, signature, message)` — verifies a signature using `crypto.subtle.verify`
- All methods are async at runtime despite the `SignEngine` interface's sync declarations
- All methods guard with `assertWebCryptoAvailable()` before any `crypto.subtle` calls
- Typed `BrowserCryptoError` thrown on generation failures and operation failures
## Usage: Basic Example
```ts
import { webCryptoEd25519Engine } from "@suaveplan/crypto-browser/engines/webcrypto-sign-engine";
const kp = await (webCryptoEd25519Engine.generateKeypair() as unknown as Promise<{
publicKey: Uint8Array;
secretKey: Uint8Array;
}>);
const message = new TextEncoder().encode("hello");
const signature = await (webCryptoEd25519Engine.sign(kp.secretKey, message) as unknown as Promise<Uint8Array>);
const valid = await (webCryptoEd25519Engine.verify(kp.publicKey, signature, message) as unknown as Promise<boolean>);
console.log(valid); // true
```
## Usage: Advanced Example
```ts
import {
webCryptoEcdsaP256Engine,
webCryptoEd25519Engine,
} from "@suaveplan/crypto-browser/engines/webcrypto-sign-engine";
import { BrowserCryptoError } from "@suaveplan/crypto-browser/errors";
// Use P-256 for JWS / JOSE interoperability
async function signJwsPayload(payload: Uint8Array): Promise<{ publicKey: Uint8Array; signature: Uint8Array }> {
try {
const kp = await (webCryptoEcdsaP256Engine.generateKeypair() as unknown as Promise<{
publicKey: Uint8Array;
secretKey: Uint8Array;
}>);
const signature = await (webCryptoEcdsaP256Engine.sign(kp.secretKey, payload) as unknown as Promise<Uint8Array>);
return { publicKey: kp.publicKey, signature };
} catch (err) {
if (err instanceof BrowserCryptoError) {
// Structured error with err.code = "BROWSER_CRYPTO.KEY.GENERATION_FAILED"
// or "BROWSER_CRYPTO.OPERATION.FAILED"
throw err;
}
throw err;
}
}
```
## API Reference
### `webCryptoEd25519Engine: SignEngine`
Ed25519 engine via `crypto.subtle` with `{ name: "Ed25519" }` algorithm.
#### `.generateKeypair(): SignKeypair`
Returns `Promise<SignKeypair>` at runtime. Public key is 32 raw bytes. Secret key is the 32-byte Ed25519 seed.
**Throws:** `BrowserCryptoError` — `BROWSER_CRYPTO.NOT_AVAILABLE` or `BROWSER_CRYPTO.KEY.GENERATION_FAILED`.
#### `.sign(secretKey, message): Uint8Array`
Returns `Promise<Uint8Array>` at runtime. Signature is 64 bytes.
| Parameter | Type | Description |
|-----------|------|-------------|
| `secretKey` | `Uint8Array` | 32-byte Ed25519 seed |
| `message` | `Uint8Array` | Arbitrary bytes to sign |
**Throws:** `BrowserCryptoError` — `BROWSER_CRYPTO.NOT_AVAILABLE` or `BROWSER_CRYPTO.OPERATION.FAILED`.
#### `.verify(publicKey, signature, message): boolean`
Returns `Promise<boolean>` at runtime.
| Parameter | Type | Description |
|-----------|------|-------------|
| `publicKey` | `Uint8Array` | 32-byte Ed25519 public key |
| `signature` | `Uint8Array` | 64-byte signature |
| `message` | `Uint8Array` | Original message bytes |
**Throws:** `BrowserCryptoError` — `BROWSER_CRYPTO.NOT_AVAILABLE` or `BROWSER_CRYPTO.OPERATION.FAILED`.
---
### `webCryptoEcdsaP256Engine: SignEngine`
ECDSA-P256 engine via `crypto.subtle` with `{ name: "ECDSA", namedCurve: "P-256" }`. Same method signatures as above. Public key is 65-byte uncompressed (0x04 prefix). Secret key is PKCS8 DER.
---
### `webCryptoSignEngine: SignEngine`
Alias for `webCryptoEd25519Engine`. Use this when you want the recommended default.
## Implementation Notes
Key export: `crypto.subtle.exportKey("pkcs8", privateKey)` returns the PKCS8 DER-encoded private key. For Ed25519, the 32-byte seed is located at byte offset 16 within the DER envelope; this is extracted to produce the compact secret key stored and transported by callers. On import, the seed is re-wrapped into the minimal 48-byte PKCS8 ASN.1 structure before calling `crypto.subtle.importKey`.
For ECDSA-P256, public keys are exported via `"raw"` format (65-byte uncompressed point), which is directly importable via `"raw"` as well. This avoids SPKI round-trip complexity.
Ed25519 support requires Node 20+, Bun 1.0+, or a modern browser (Chrome 113+, Firefox 129+, Safari 17+). Calling these engines in older environments will produce a `BrowserCryptoError.KEY.GENERATION_FAILED` from the `crypto.subtle.generateKey` rejection.

View File

@@ -0,0 +1,91 @@
# browser-crypto-error
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Defines `BrowserCryptoError`, the structured error class for all `@suaveplan/crypto-browser` failure paths. It extends `SuaveplanError<BrowserCryptoCode>` from `@suaveplan/error`, inheriting cause-chain traversal, OTel span-event adapter support, and wire-format serialization. Every thrown error in this package is an instance of `BrowserCryptoError` with a stable `BrowserCryptoCode` string code, enabling callers to discriminate failure modes with type-safe `instanceof` + `err.code` checks.
The error class follows the project convention: a package-scoped error with a typed code union, `cause`, `context`, and `timestamp` fields. This makes crypto failures observable, loggable, and serializable without depending on runtime-specific error formats.
## Features
- Extends `SuaveplanError<BrowserCryptoCode>` — participates in all project error infrastructure
- `code` is one of the stable `BROWSER_CRYPTO.*` codes from `codes.ts`
- `cause` captures the underlying `DOMException` or `Error` from `crypto.subtle`
- `context` carries optional structured diagnostic data
- `timestamp` records when the error was thrown
- Stacktrace captured via `Error.captureStackTrace` where available (V8/Node)
## Usage: Basic Example
```ts
import { BrowserCryptoError } from "@suaveplan/crypto-browser/errors";
try {
await webCryptoAeadEngine.decrypt(wrongKey, ciphertext, nonce);
} catch (err) {
if (err instanceof BrowserCryptoError) {
console.error(err.code); // "BROWSER_CRYPTO.OPERATION.FAILED"
console.error(err.cause); // DOMException: The operation failed ...
}
}
```
## Usage: Advanced Example
```ts
import { BrowserCryptoError, BROWSER_CRYPTO_CODES } from "@suaveplan/crypto-browser/errors";
function isNotAvailable(err: unknown): boolean {
return (
err instanceof BrowserCryptoError &&
err.code === BROWSER_CRYPTO_CODES["NOT_AVAILABLE"]
);
}
async function encryptOrFallback(
key: Uint8Array,
data: Uint8Array
): Promise<Uint8Array> {
try {
return await (webCryptoAeadEngine.encrypt(key, data, nonce) as unknown as Promise<Uint8Array>);
} catch (err) {
if (isNotAvailable(err)) {
// Fall back to @noble/ciphers-backed implementation
return nobleAeadFallback(key, data);
}
throw err;
}
}
```
## API Reference
### `class BrowserCryptoError extends SuaveplanError<BrowserCryptoCode>`
**Constructor:**
```ts
new BrowserCryptoError(code: BrowserCryptoCode, messageKey: string, options?: SuaveplanErrorOptions)
```
| Parameter | Type | Description |
|-----------|------|-------------|
| `code` | `BrowserCryptoCode` | Stable error code, e.g. `"BROWSER_CRYPTO.NOT_AVAILABLE"` |
| `messageKey` | `string` | i18n key for the human-readable message |
| `options` | `SuaveplanErrorOptions` | Optional `{ cause, context, traceId, clock }` |
**Inherited properties:**
- `code: BrowserCryptoCode` — stable error code
- `cause?: Error` — underlying cause
- `context?: ErrorContext` — structured diagnostic data
- `timestamp: number` — `Date.now()` at throw time
## Implementation Notes
The constructor is declared `public` despite mirroring the protected base — Biome lint would flag a "useless constructor" but the intent is to widen visibility. The `biome-ignore` comment is present to document this intent.
All `crypto.subtle` rejections produce `DOMException` instances. These are captured as the `cause` field, making the original platform error available for debugging while presenting a structured `BrowserCryptoError` to callers.

View File

@@ -0,0 +1,81 @@
# BROWSER_CRYPTO_CODES
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`codes.ts` defines the complete set of stable error codes used throughout `@suaveplan/crypto-browser`. Each code is a namespaced string constant produced by `defineCodes` from `@suaveplan/error`, ensuring codes are unique within the `"BROWSER_CRYPTO"` namespace and that the `BrowserCryptoCode` union type is always in sync with the registered values at compile time. All WebCrypto-related failures in this package surface under one of these codes, allowing consumers to branch deterministically without parsing message strings.
## Features
- Three codes covering the main WebCrypto failure domains: API unavailability, key generation, and general operation failures
- Generated via `defineCodes("BROWSER_CRYPTO", [...])` which prefixes each entry and freezes the resulting object
- `BrowserCryptoCode` is derived as a `typeof` union for compile-time exhaustive narrowing
- No runtime dependencies beyond `@suaveplan/error`
## Usage: Basic Example
```typescript
import { BROWSER_CRYPTO_CODES } from "@suaveplan/crypto-browser/errors/codes";
import { BrowserCryptoError } from "@suaveplan/crypto-browser/errors/browser-crypto-error";
throw new BrowserCryptoError(
BROWSER_CRYPTO_CODES["NOT_AVAILABLE"],
"error.crypto-browser.not-available"
);
```
## Usage: Advanced Example
```typescript
import {
BROWSER_CRYPTO_CODES,
type BrowserCryptoCode,
} from "@suaveplan/crypto-browser/errors/codes";
import { BrowserCryptoError } from "@suaveplan/crypto-browser/errors/browser-crypto-error";
function handleBrowserCryptoError(err: BrowserCryptoError): string {
switch (err.code as BrowserCryptoCode) {
case BROWSER_CRYPTO_CODES["NOT_AVAILABLE"]:
return "WebCrypto API is not available in this environment (requires HTTPS or localhost).";
case BROWSER_CRYPTO_CODES["KEY.GENERATION_FAILED"]:
return "Key generation failed — the requested algorithm may not be supported by this browser.";
case BROWSER_CRYPTO_CODES["OPERATION.FAILED"]:
return "Cryptographic operation failed — check key type, algorithm, and input size.";
default:
return "Unknown browser crypto error.";
}
}
```
## API Reference
### `BROWSER_CRYPTO_CODES`
```typescript
const BROWSER_CRYPTO_CODES: Readonly<Record<string, string>>
```
A frozen map from short code keys to their namespaced string values. All values are prefixed with `"BROWSER_CRYPTO."` by `defineCodes`.
| Key | Namespaced value | When thrown |
|------------------------|----------------------------------------------|-------------------------------------------------------------------|
| `NOT_AVAILABLE` | `"BROWSER_CRYPTO.NOT_AVAILABLE"` | `globalThis.crypto.subtle` is `undefined` or `null` |
| `KEY.GENERATION_FAILED`| `"BROWSER_CRYPTO.KEY.GENERATION_FAILED"` | `SubtleCrypto.generateKey` or `importKey` rejects |
| `OPERATION.FAILED` | `"BROWSER_CRYPTO.OPERATION.FAILED"` | Any `SubtleCrypto` operation (`encrypt`, `decrypt`, `sign`, `verify`, `deriveBits`, `wrapKey`, `unwrapKey`) rejects |
### `BrowserCryptoCode`
```typescript
type BrowserCryptoCode =
(typeof BROWSER_CRYPTO_CODES)[keyof typeof BROWSER_CRYPTO_CODES];
```
Union of all namespaced code strings. Use this type to annotate `err.code` when narrowing `BrowserCryptoError` in typed catch blocks.
## Implementation Notes
`defineCodes` returns a plain frozen object and the `BrowserCryptoCode` type collapses to a string literal union. Adding a new entry to the array automatically widens the union without requiring any manual type maintenance.
`OPERATION.FAILED` intentionally covers a broad set of `SubtleCrypto` rejections rather than splitting each operation into its own code. The distinction between, say, a failed `encrypt` and a failed `sign` is captured in the structured `context` field on the thrown `BrowserCryptoError` rather than in the code itself, which keeps the code registry small and stable across algorithm additions.

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.

View File

@@ -0,0 +1,138 @@
# rsa
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Provides a high-level `rsa` namespace object wrapping RSA cryptographic operations from `crypto.subtle`. Supports three algorithm variants — `RSA-PSS`, `RSA-OAEP`, and `RSASSA-PKCS1-v1_5` — for signing/verification and asymmetric encryption/decryption. Keys are represented as standard DER byte arrays (SPKI for public keys, PKCS8 for private keys), which are universally portable across platforms and easy to store and transmit.
RSA is provided alongside the elliptic-curve engines for interoperability with legacy systems, PKI infrastructure, and protocols that require RSA (e.g., certain TLS cipher suites, JWT RSA algorithms, legacy JOSE). For new designs, Ed25519 and ECDH-X25519 are preferred.
## Features
- `generateKeypair(algorithm, modulusLength?)` — generates an RSA keypair; default 2048-bit modulus, configurable up to 4096 bits
- `sign(algorithm, privateKey, message)` — RSA-PSS or RSASSA-PKCS1-v1_5 signature generation
- `verify(algorithm, publicKey, signature, message)` — RSA signature verification
- `encrypt(publicKey, plaintext)` — RSA-OAEP encryption (public-key encryption)
- `decrypt(privateKey, ciphertext)` — RSA-OAEP decryption
- SHA-256 hash used for all operations
- RSA-PSS uses 32-byte salt length
- All methods guard with `assertWebCryptoAvailable()`
- Typed `BrowserCryptoError` for all failure paths
## Usage: Basic Example
```ts
import { rsa } from "@suaveplan/crypto-browser/rsa/rsa";
// Generate and use RSA-PSS for signing
const { publicKey, privateKey } = await rsa.generateKeypair("RSA-PSS");
const message = new TextEncoder().encode("document to sign");
const signature = await rsa.sign("RSA-PSS", privateKey, message);
const valid = await rsa.verify("RSA-PSS", publicKey, signature, message);
console.log(valid); // true
```
## Usage: Advanced Example
```ts
import { rsa, type RsaAlgorithm } from "@suaveplan/crypto-browser/rsa/rsa";
import { BrowserCryptoError } from "@suaveplan/crypto-browser/errors";
// RSA-OAEP for encrypting a small payload (e.g., a symmetric key)
async function wrapAesKey(
recipientPublicKey: Uint8Array,
aesKey: Uint8Array // must be <= ~190 bytes for RSA-2048
): Promise<Uint8Array> {
try {
return await rsa.encrypt(recipientPublicKey, aesKey);
} catch (err) {
if (err instanceof BrowserCryptoError) {
console.error(`Crypto failure: ${err.code}`);
}
throw err;
}
}
async function unwrapAesKey(
recipientPrivateKey: Uint8Array,
wrappedKey: Uint8Array
): Promise<Uint8Array> {
return await rsa.decrypt(recipientPrivateKey, wrappedKey);
}
// 4096-bit keypair for higher-security contexts
const { publicKey, privateKey } = await rsa.generateKeypair("RSA-OAEP", 4096);
```
## API Reference
### `rsa.generateKeypair(algorithm, modulusLength?): Promise<{ publicKey, privateKey }>`
Generates an RSA keypair.
| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `algorithm` | `RsaAlgorithm` | — | `"RSA-PSS"` \| `"RSA-OAEP"` \| `"RSASSA-PKCS1-v1_5"` |
| `modulusLength` | `number` | `2048` | Key bit length (2048 or 4096 recommended) |
**Returns:** `Promise<{ publicKey: Uint8Array; privateKey: Uint8Array }>` — SPKI/PKCS8 DER bytes.
**Throws:** `BrowserCryptoError` — `BROWSER_CRYPTO.KEY.GENERATION_FAILED`.
---
### `rsa.sign(algorithm, privateKey, message): Promise<Uint8Array>`
Signs a message. Use with `"RSA-PSS"` or `"RSASSA-PKCS1-v1_5"`.
| Parameter | Type | Description |
|-----------|------|-------------|
| `algorithm` | `RsaAlgorithm` | Must match keypair generation algorithm |
| `privateKey` | `Uint8Array` | PKCS8 DER private key |
| `message` | `Uint8Array` | Message to sign |
**Returns:** `Promise<Uint8Array>` — signature bytes.
**Throws:** `BrowserCryptoError` — `BROWSER_CRYPTO.OPERATION.FAILED`.
---
### `rsa.verify(algorithm, publicKey, signature, message): Promise<boolean>`
| Parameter | Type | Description |
|-----------|------|-------------|
| `algorithm` | `RsaAlgorithm` | Must match signing algorithm |
| `publicKey` | `Uint8Array` | SPKI DER public key |
| `signature` | `Uint8Array` | Signature bytes |
| `message` | `Uint8Array` | Original message |
**Returns:** `Promise<boolean>`.
---
### `rsa.encrypt(publicKey, plaintext): Promise<Uint8Array>`
RSA-OAEP encryption. For RSA-2048, plaintext must be ≤ 190 bytes (2048-bit modulus minus OAEP overhead).
---
### `rsa.decrypt(privateKey, ciphertext): Promise<Uint8Array>`
RSA-OAEP decryption.
---
### `type RsaAlgorithm`
`"RSA-PSS" | "RSA-OAEP" | "RSASSA-PKCS1-v1_5"`
## Implementation Notes
All RSA operations use SHA-256 as the hash algorithm, which is the most widely supported and appropriate choice for 2048- and 4096-bit RSA. RSA-PSS uses a 32-byte (256-bit) salt, matching the hash output length, which is the PKCS#1v2.2 recommendation.
The `encrypt()` and `decrypt()` methods are hardcoded to RSA-OAEP — only RSA-OAEP provides semantic security for asymmetric encryption. `RSA-PSS` and `RSASSA-PKCS1-v1_5` are signing-only algorithms and cannot be used with `encrypt`/`decrypt`.
Key generation for RSA-2048 typically takes 50–200 ms in `crypto.subtle`; RSA-4096 can take 500–2000 ms. This is significantly slower than elliptic-curve key generation (< 5 ms). All operations use `await` and do not block the main thread.