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:
@@ -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.
|
||||
@@ -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`.
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user