4.4 KiB
4.4 KiB
crypto-sync
(no submodules)
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 AEADdecryptSyncPayload(payload, key, options?)— verified decrypt with AAD- AAD =
"v1"|| optional caller-supplied bytes - Random 24-byte nonce sourced via
@suaveplan/crypto-utils.randomBytes - Injectable
byteSourcefor 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
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
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. ThrowsVaultSyncError(SYNC.PAYLOAD_TOO_LARGE)ifplaintext.lengthexceedsMAX_SYNC_PAYLOAD_BYTES.decryptSyncPayload(payload: EncryptedSyncPayload, key: Uint8Array, options?): Uint8Array— validates the envelope shape viaEncryptedSyncPayloadSchemathen verifies and decrypts. ThrowsVaultSyncError(SYNC.PAYLOAD_INVALID_VERSION)for shape failures andVaultSyncError(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
randomBytesfrom@suaveplan/crypto-utils. Callers wanting determinism in tests pass a custombyteSource. 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
decryptSyncPayloadis a freshUint8Array. The caller is responsible forzeroize-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
EncryptedSyncPayloadSchemaexposed publicly, ensuring schema and runtime checks are perfectly aligned.