first commit
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
# crypto-sync
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`crypto-sync.ts` provides the AEAD-encrypted envelope used by `SyncEngine`
|
||||
and `SyncQueue` to put plaintext sync payloads on the wire. Encryption is
|
||||
XChaCha20-Poly1305 from `@suaveplan/crypto-cipher` with a 24-byte nonce and
|
||||
an AAD-bound version string (`"v1"`). The version binding means a future
|
||||
revision can change the wire layout without risk that a tampered v2
|
||||
payload is silently decoded as v1. The module enforces a 10 MiB plaintext
|
||||
cap to prevent memory abuse from oversized snapshots.
|
||||
|
||||
## Features
|
||||
|
||||
- `encryptSyncPayload(plaintext, key, options?)` — XChaCha20-Poly1305 AEAD
|
||||
- `decryptSyncPayload(payload, key, options?)` — verified decrypt with AAD
|
||||
- AAD = `"v1"` || optional caller-supplied bytes
|
||||
- Random 24-byte nonce sourced via `@suaveplan/crypto-utils.randomBytes`
|
||||
- Injectable `byteSource` for deterministic test nonces
|
||||
- Hard cap of 10 MiB on plaintext (`SYNC.PAYLOAD_TOO_LARGE`)
|
||||
- Structured errors: `SYNC.PAYLOAD_TAMPERED`, `SYNC.PAYLOAD_INVALID_VERSION`
|
||||
- OTel histogram + outcome counter (no key bytes, no plaintext, no IDs)
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { decryptSyncPayload, encryptSyncPayload } from "@suaveplan/vault-sync";
|
||||
|
||||
const key = new Uint8Array(32); // 32 bytes — ChaCha20 key
|
||||
crypto.getRandomValues(key);
|
||||
const plaintext = new TextEncoder().encode(JSON.stringify({ items: [] }));
|
||||
|
||||
const envelope = encryptSyncPayload(plaintext, key);
|
||||
const recovered = decryptSyncPayload(envelope, key);
|
||||
|
||||
console.log(new TextDecoder().decode(recovered));
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { encryptSyncPayload } from "@suaveplan/vault-sync";
|
||||
import { decryptSyncPayload } from "@suaveplan/vault-sync";
|
||||
import { VaultSyncError } from "@suaveplan/vault-sync";
|
||||
import { VAULT_SYNC_CODES } from "@suaveplan/vault-sync/errors";
|
||||
import { zeroize } from "@suaveplan/crypto-utils";
|
||||
|
||||
const key = crypto.getRandomValues(new Uint8Array(32));
|
||||
const plaintext = new TextEncoder().encode("snapshot");
|
||||
const aad = new TextEncoder().encode("vault-id-binding"); // domain separator
|
||||
|
||||
const envelope = encryptSyncPayload(plaintext, key, { additionalData: aad });
|
||||
|
||||
try {
|
||||
// Caller MUST pass the same additionalData on decrypt — otherwise the
|
||||
// tag check fails with SYNC.PAYLOAD_TAMPERED.
|
||||
const round = decryptSyncPayload(envelope, key, { additionalData: aad });
|
||||
zeroize(round); // clear plaintext when done
|
||||
} catch (err) {
|
||||
if (
|
||||
err instanceof VaultSyncError &&
|
||||
err.code === VAULT_SYNC_CODES["SYNC.PAYLOAD_TAMPERED"]
|
||||
) {
|
||||
// Telemetry already recorded; trigger a fresh full pull.
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `encryptSyncPayload(plaintext: Uint8Array, key: Uint8Array, options?: EncryptSyncPayloadOptions): EncryptedSyncPayload`
|
||||
— encrypts with XChaCha20-Poly1305. Throws
|
||||
`VaultSyncError(SYNC.PAYLOAD_TOO_LARGE)` if `plaintext.length` exceeds
|
||||
`MAX_SYNC_PAYLOAD_BYTES`.
|
||||
- `decryptSyncPayload(payload: EncryptedSyncPayload, key: Uint8Array, options?): Uint8Array`
|
||||
— validates the envelope shape via `EncryptedSyncPayloadSchema` then
|
||||
verifies and decrypts. Throws
|
||||
`VaultSyncError(SYNC.PAYLOAD_INVALID_VERSION)` for shape failures and
|
||||
`VaultSyncError(SYNC.PAYLOAD_TAMPERED)` for tag mismatch (wrong key,
|
||||
flipped byte, mismatched AAD).
|
||||
- `EncryptSyncPayloadOptions` — `{ byteSource?, additionalData? }`.
|
||||
- `MAX_SYNC_PAYLOAD_BYTES` — `10 * 1024 * 1024` (10 MiB).
|
||||
- `SYNC_PAYLOAD_VERSION` — literal `"v1"`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The package never generates a nonce internally beyond delegating to
|
||||
`randomBytes` from `@suaveplan/crypto-utils`. Callers wanting determinism
|
||||
in tests pass a custom `byteSource`. There is no notion of nonce reuse
|
||||
detection at this layer — XChaCha20's 192-bit nonce makes random
|
||||
selection collision-safe at any realistic call rate, and higher-level
|
||||
wrapping is left to the consumer if needed.
|
||||
- The plaintext returned by `decryptSyncPayload` is a fresh `Uint8Array`.
|
||||
The caller is responsible for `zeroize`-ing it after use; the module
|
||||
cannot do this safely because it does not know when the caller is done
|
||||
reading the value.
|
||||
- Both shape failures and tag failures emit `payload.outcome.total{status:
|
||||
"tampered"}` so a single counter captures the integrity-violation rate.
|
||||
- Decryption-side schema validation uses the same `EncryptedSyncPayloadSchema`
|
||||
exposed publicly, ensuring schema and runtime checks are perfectly aligned.
|
||||
Reference in New Issue
Block a user