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,117 @@
# NobleSealedBoxEngine
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`NobleSealedBoxEngine` implements the `SealedBoxEngine` interface using a `crypto_box_seal`-style anonymous-sender encryption protocol. The sender generates a fresh ephemeral X25519 keypair per message, performs ECDH with the recipient's long-term public key, derives a symmetric key via HKDF-SHA256, and encrypts with XChaCha20-Poly1305. The ephemeral public key and nonce are prepended to the output so the recipient can recover the shared secret without any out-of-band communication. Sender anonymity is preserved because the ephemeral secret key is discarded immediately after sealing.
## Features
- Implements `SealedBoxEngine` (`seal` / `open`) for anonymous authenticated encryption
- Protocol output layout: `ephemeralPub (32) || nonce (24) || ciphertext || tag (16)`
- HKDF-SHA256 key derivation with zero salt and info `"suaveplan-sealed-box-v1"` for domain separation
- Ephemeral keypair generated fresh per `seal` call using `defaultByteSource` (secure random)
- Minimum ciphertext length validated during `open`; short ciphertexts throw `CryptoError` before attempting decryption
- All decryption failures wrapped in `CryptoError` with code `AEAD.DECRYPT_FAILED`
## Usage: Basic Example
```typescript
import { NobleSealedBoxEngine } from "@suaveplan/crypto-core/sealed-box/noble-sealed-box-engine";
import { NobleX25519Engine } from "@suaveplan/crypto-core/agree/noble-agree-engine";
const agreeEngine = new NobleX25519Engine();
const sealedBoxEngine = new NobleSealedBoxEngine();
const recipient = agreeEngine.generateKeypair();
// Any party can seal to recipient without knowing sender's identity
const plaintext = new TextEncoder().encode("anonymous message");
const ciphertext = sealedBoxEngine.seal(recipient.publicKey, plaintext);
// Only recipient can open
const recovered = sealedBoxEngine.open(
recipient.secretKey,
recipient.publicKey,
ciphertext
);
console.log(new TextDecoder().decode(recovered)); // "anonymous message"
```
## Usage: Advanced Example
```typescript
import { NobleSealedBoxEngine } from "@suaveplan/crypto-core/sealed-box/noble-sealed-box-engine";
import { NobleX25519Engine } from "@suaveplan/crypto-core/agree/noble-agree-engine";
import { CryptoError } from "@suaveplan/crypto-core/errors/crypto-error";
import { CRYPTO_CODES } from "@suaveplan/crypto-core/errors/codes";
const agreeEngine = new NobleX25519Engine();
const engine = new NobleSealedBoxEngine();
const recipient = agreeEngine.generateKeypair();
// Each seal call produces a different ciphertext (fresh ephemeral key + nonce)
const msg = new TextEncoder().encode("one-time sealed value");
const ct1 = engine.seal(recipient.publicKey, msg);
const ct2 = engine.seal(recipient.publicKey, msg);
console.log(ct1.length === ct2.length); // true (same size)
// Truncated ciphertext is caught before decryption attempt
try {
engine.open(recipient.secretKey, recipient.publicKey, new Uint8Array(10));
} catch (err) {
if (err instanceof CryptoError && err.code === CRYPTO_CODES["AEAD.DECRYPT_FAILED"]) {
console.log("Short ciphertext rejected early");
}
}
// Tampered ciphertext also fails at authentication
try {
const tampered = ct1.slice();
tampered[40] ^= 0x01;
engine.open(recipient.secretKey, recipient.publicKey, tampered);
} catch (err) {
if (err instanceof CryptoError && err.code === CRYPTO_CODES["AEAD.DECRYPT_FAILED"]) {
console.log("Tampered ciphertext rejected");
}
}
```
## API Reference
### `NobleSealedBoxEngine`
```typescript
class NobleSealedBoxEngine implements SealedBoxEngine
```
**`seal(recipientPublic, plaintext): Uint8Array`**
| Parameter | Type | Description |
|-------------------|--------------|--------------------------------------------------------|
| `recipientPublic` | `Uint8Array` | Recipient's 32-byte X25519 public key. |
| `plaintext` | `Uint8Array` | Message bytes to encrypt. |
Returns a self-contained blob: `ephemeralPublicKey (32) || nonce (24) || ciphertext || poly1305Tag (16)`. Total overhead is 72 bytes beyond plaintext length.
**`open(recipientSecret, recipientPublic, ciphertext): Uint8Array`**
| Parameter | Type | Description |
|-------------------|--------------|----------------------------------------------------------------|
| `recipientSecret` | `Uint8Array` | Recipient's 32-byte X25519 secret key. |
| `recipientPublic` | `Uint8Array` | Recipient's 32-byte X25519 public key (currently unused but kept for API symmetry). |
| `ciphertext` | `Uint8Array` | Sealed blob produced by `seal`. |
Returns plaintext bytes. Throws `CryptoError` (`AEAD.DECRYPT_FAILED`) if:
- The ciphertext is shorter than 72 bytes (minimum: 32 ephKey + 24 nonce + 16 tag)
- Authentication tag verification fails
## Implementation Notes
The `_recipientPublic` parameter in `open` is accepted to keep the `SealedBoxEngine` interface consistent with engines that do require it (e.g. the libsodium adapter), but is not used in the computation — the recipient public key is only needed to reconstruct the shared secret on the _sender_ side. The Noble implementation derives everything from `recipientSecret` and the extracted `ephemeralPublic` alone.
Each `seal` call draws fresh randomness for both the ephemeral secret (32 bytes) and the XChaCha20-Poly1305 nonce (24 bytes) from `defaultByteSource`. This means two invocations with identical plaintext and recipient key will always produce different ciphertexts — forward-secrecy is automatic. The trade-off is that the nonce does not need to be managed by the caller for this primitive, unlike `authBox` where nonce uniqueness is the caller's responsibility.

View File

@@ -0,0 +1,94 @@
# sealed-box
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
- [noble-sealed-box-engine](./noble-sealed-box-engine/noble-sealed-box-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [sealed-box](./sealed-box.md) — <!-- begin:auto-docs --> _(no submodules)_- noble-sealed-box-engine — <!-- begin:auto-docs --> _…
- [noble-sealed-box-engine](./noble-sealed-box-engine/noble-sealed-box-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [sealed-box](./sealed-box.md) — <!-- begin:auto-docs --> _(no submodules)_- [noble-sealed-box-engine](./noble-sealed-box-engine/noble-sealed-box-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [sealed-box](./sealed-box.md) — <!-- begin:auto-docs --> _(no submodules)_- noble-sealed-box-engine — <!…
- [noble-sealed-box-engine](./noble-sealed-box-engine/noble-sealed-box-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [sealed-box](./sealed-box.md) — <!-- begin:auto-docs --> _(no submodules)_- noble-sealed-box-engine — <!-- begin:auto-docs -->…
- [noble-sealed-box-engine](./noble-sealed-box-engine/noble-sealed-box-engine.md) — <!-- begin:auto-docs --> _(no submodules)_- [sealed-box](./sealed-box.md) — <!-- begin:auto-docs --> _(no submodules)_
## Purpose
Provides anonymous-sender sealed-box encryption following `crypto_box_seal` semantics. The sender generates an ephemeral X25519 keypair, performs ECDH with the recipient's public key, derives a symmetric key via HKDF-SHA256, then encrypts with XChaCha20-Poly1305. The ephemeral public key is prepended to the ciphertext blob so the recipient can derive the same symmetric key without the sender needing to reveal their identity. This pattern is commonly used for one-way encrypted messages where sender anonymity is desired.
## Features
- Anonymous sender — no sender keypair required
- X25519 ECDH + HKDF-SHA256 + XChaCha20-Poly1305 (crypto_box_seal semantics)
- Self-contained output: `ephemeralPub (32) || nonce (24) || ciphertext || tag (16)`
- Engine-pluggable via `SealedBoxEngine` interface
- Authenticated decryption — rejects tampered ciphertext with `CRYPTO.AEAD.DECRYPT_FAILED`
## Usage: Basic Example
```ts
import { sealedBox } from "@suaveplan/crypto-core";
import { x25519 } from "@suaveplan/crypto-core";
const recipient = x25519.generateKeypair();
// Seal (any sender, no keypair needed)
const ciphertext = sealedBox.seal(recipient.publicKey, new TextEncoder().encode("secret"));
// Open (recipient only)
const plaintext = sealedBox.open(recipient.secretKey, recipient.publicKey, ciphertext);
```
## Usage: Advanced Example
```ts
import { sealedBox } from "@suaveplan/crypto-core";
import { x25519 } from "@suaveplan/crypto-core";
import type { SealedBoxEngine } from "@suaveplan/crypto-core/engines";
// Custom engine for testing (deterministic, not secure)
const stubEngine: SealedBoxEngine = {
seal: (_pub, pt) => pt.slice(),
open: (_sec, _pub, ct) => ct.slice(),
};
const recipient = x25519.generateKeypair();
const msg = new Uint8Array([1, 2, 3]);
const ct = sealedBox.seal(recipient.publicKey, msg, { engine: stubEngine });
const pt = sealedBox.open(recipient.secretKey, recipient.publicKey, ct, { engine: stubEngine });
```
## API Reference
### `seal(recipientPublic, plaintext, opts?)`
```ts
function seal(
recipientPublic: Uint8Array,
plaintext: Uint8Array,
opts?: SealedBoxEngineOptions
): Uint8Array
```
Encrypts and authenticates `plaintext` for the recipient. Returns a self-contained blob containing the ephemeral public key, nonce, ciphertext, and authentication tag.
| Param | Type | Description |
|-------|------|-------------|
| `recipientPublic` | `Uint8Array` | Recipient's X25519 public key (32 bytes). |
| `plaintext` | `Uint8Array` | Data to encrypt. |
| `opts.engine` | `SealedBoxEngine?` | Override the default noble engine. |
### `open(recipientSecret, recipientPublic, ciphertext, opts?)`
```ts
function open(
recipientSecret: Uint8Array,
recipientPublic: Uint8Array,
ciphertext: Uint8Array,
opts?: SealedBoxEngineOptions
): Uint8Array
```
Decrypts a sealed box. Throws on authentication failure.
**Throws:** `CryptoError("CRYPTO.AEAD.DECRYPT_FAILED")` — ciphertext is too short, or authentication tag verification fails.
## Implementation Notes
The HKDF info string `"suaveplan-sealed-box-v1"` is domain-separated to prevent key reuse across protocols. A zero salt is used because the X25519 shared secret already provides sufficient entropy for HKDF extraction. The nonce is randomly generated per seal operation and stored in the ciphertext blob, so the same key can be reused across many messages without nonce-reuse risk. The 24-byte XChaCha20-Poly1305 nonce is large enough that random generation is safe for up to 2^96 messages per recipient key before birthday collisions become plausible.