first commit
This commit is contained in:
58
docs/foundation/data/packer/src/chunked/codec/chunk.md
Normal file
58
docs/foundation/data/packer/src/chunked/codec/chunk.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# chunk
|
||||
|
||||
## Purpose
|
||||
|
||||
Encodes and parses the wire-format v1 chunk in its entirety: `metadata(100) || sig(64) || ciphertext(payloadLen)`. Composes `encodeHeader` / `parseHeader` with byte-level concatenation, length validation, and the canonical signing-payload helper. Every receive path in the packer (chunked unpacker, chunk verifier, mesh transport) calls `parseChunk` before any cryptographic work; every send path calls `encodeChunk` once metadata, signature, and ciphertext are ready.
|
||||
|
||||
## Features
|
||||
|
||||
- `encodeChunk({ metadata, sig, ciphertext })` returns a single `Uint8Array` of `164 + payloadLen` bytes.
|
||||
- `parseChunk(buf)` returns `{ metadata, sig, ciphertext }` with all three fields as zero-copy subarrays of `buf`.
|
||||
- `signingPayload(metadataBytes, ciphertext)` returns the canonical bytes the server signs / receivers verify.
|
||||
- Length and structural validation lifted from `parseHeader`; chunk-level validation adds the total-length check.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { encodeChunk, parseChunk } from "@suaveplan/packer/chunked";
|
||||
|
||||
const wire = encodeChunk({
|
||||
metadata, // ChunkV1Metadata
|
||||
sig: new Uint8Array(64), // ed25519 signature
|
||||
ciphertext: new Uint8Array(payloadLen),
|
||||
});
|
||||
|
||||
const back = parseChunk(wire);
|
||||
// back.metadata, back.sig, back.ciphertext
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { encodeChunk, parseChunk, signingPayload, encodeHeader } from "@suaveplan/packer/chunked";
|
||||
import { ed25519 } from "@noble/curves/ed25519";
|
||||
|
||||
// Producer:
|
||||
const headerBytes = encodeHeader(metadata);
|
||||
const signed = signingPayload(headerBytes, ciphertext);
|
||||
const sig = ed25519.sign(signed, serverPrivKey);
|
||||
const wire = encodeChunk({ metadata, sig, ciphertext });
|
||||
|
||||
// Receiver:
|
||||
const { metadata: m, sig: s, ciphertext: c } = parseChunk(wire);
|
||||
const signedAgain = signingPayload(encodeHeader(m), c);
|
||||
if (!ed25519.verify(s, signedAgain, serverPubKey)) {
|
||||
throw new Error("bad sig");
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- **`interface EncodeChunkInput { metadata: ChunkV1Metadata; sig: Uint8Array; ciphertext: Uint8Array }`**
|
||||
- **`encodeChunk(input: EncodeChunkInput): Uint8Array`** — total length `164 + payloadLen`. Throws `PackerError(PACKER.INVALID_FORMAT)` on `sig.byteLength !== 64` and `PackerError(PACKER.PAYLOAD_LEN_INVALID)` on ciphertext-length mismatch.
|
||||
- **`parseChunk(buf: Uint8Array): ParsedChunkV1`** — validates total length matches `164 + metadata.payloadLen`. Throws on header-parse failures and on length mismatch.
|
||||
- **`signingPayload(metadataBytes: Uint8Array, ciphertext: Uint8Array): Uint8Array`** — concatenates the two for ed25519 sign/verify.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`encodeChunk` performs `metadata` field-length validation indirectly via `encodeHeader`, then writes a single fresh `Uint8Array(164 + payloadLen)` with three `set` calls — no intermediate copy of the metadata or ciphertext beyond the final placement. `parseChunk` re-uses `parseHeader`'s zero-copy subarray pattern; the returned struct shares its underlying buffer with the input, so callers must clone if they intend to mutate either side independently. `signingPayload` allocates a fresh buffer because the AEAD-side callers must not mutate the wire bytes after sig generation.
|
||||
62
docs/foundation/data/packer/src/chunked/codec/header.md
Normal file
62
docs/foundation/data/packer/src/chunked/codec/header.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# header
|
||||
|
||||
## Purpose
|
||||
|
||||
Encodes and parses the 100-byte metadata portion of a v1 chunk on its own (without sig or ciphertext). Every chunk on the wire begins with this exact byte layout; receivers parse the header first, validate structural invariants, and only then proceed to signature verification and AEAD decryption. The parser enforces every PACKER-CHUNK-* requirement that can be checked from the header alone — version pin, reserved-flags zero, payloadLen non-final/final invariants, in-range chunkId, sane chunkSize.
|
||||
|
||||
## Features
|
||||
|
||||
- `encodeHeader(meta)` writes a fresh 100-byte big-endian buffer at the locked field offsets.
|
||||
- `parseHeader(buf)` reads the first 100 bytes and validates structural invariants.
|
||||
- Returns metadata fields as zero-copy `Uint8Array.subarray` views into the input buffer.
|
||||
- Throws `PackerError` with stable codes on every failure mode (`INVALID_FORMAT`, `VERSION_UNSUPPORTED`, `FLAGS_RESERVED_NONZERO`, `PAYLOAD_LEN_INVALID`).
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { encodeHeader, parseHeader, CHUNK_VERSION_V1 } from "@suaveplan/packer/chunked";
|
||||
|
||||
const meta = {
|
||||
version: CHUNK_VERSION_V1,
|
||||
fileId: new Uint8Array(32),
|
||||
chunkId: 0,
|
||||
totalChunks: 4,
|
||||
fileSize: 4_000_000n,
|
||||
chunkSize: 1_048_576,
|
||||
payloadLen: 1_048_576 - 164,
|
||||
flags: 0,
|
||||
nonce: new Uint8Array(24),
|
||||
aeadTag: new Uint8Array(16),
|
||||
};
|
||||
const bytes = encodeHeader(meta);
|
||||
const round = parseHeader(bytes);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { parseHeader, PackerError, PACKER_CODES } from "@suaveplan/packer/chunked";
|
||||
|
||||
function safeParse(buf: Uint8Array) {
|
||||
try { return { ok: true as const, meta: parseHeader(buf) }; }
|
||||
catch (err) {
|
||||
if (err instanceof PackerError) return { ok: false as const, code: err.code };
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// Reject v2 chunks early without engaging the verify path:
|
||||
const result = safeParse(receivedBytes);
|
||||
if (!result.ok && result.code === PACKER_CODES.VERSION_UNSUPPORTED) {
|
||||
metrics.versionRejects.inc();
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- **`encodeHeader(meta: ChunkV1Metadata): Uint8Array`** — writes 100 bytes at locked offsets. Throws `PackerError(PACKER.INVALID_FORMAT)` on `fileId.byteLength !== 32`, `nonce.byteLength !== 24`, or `aeadTag.byteLength !== 16`.
|
||||
- **`parseHeader(buf: Uint8Array): ChunkV1Metadata`** — validates length >= 100, `version === 0x0001`, `flags === 0x0000`, `totalChunks >= 1`, `chunkId < totalChunks`, `chunkSize > 164`, and the non-final / final `payloadLen` invariants. Returns metadata; field byte ranges are zero-copy subarrays of `buf`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`DataView` is used for all multi-byte numeric reads/writes with `littleEndian: false` to enforce big-endian wire format. `Uint8Array.subarray` (not `slice`) is used for byte-range fields so the parser is zero-allocation in the happy path. The payloadLen invariant uses `BigInt` arithmetic for the final-chunk derivation because `fileSize` is `u64` and may exceed `Number.MAX_SAFE_INTEGER`. The non-final case stays in `number` arithmetic (chunkSize is `u32`).
|
||||
Reference in New Issue
Block a user