Files
2026-05-27 23:47:53 +01:00

120 lines
3.9 KiB
TypeScript

/**
* Test-only DI bindings for `@suaveplan/bitpacker-crypto` primitives.
*
* Implemented directly against the lower-level `@suaveplan/crypto-core`
* (sign/verify) and `@suaveplan/crypto-cipher` (AEAD) packages so this
* test surface has NO dependency on `@suaveplan/crypto`. That breaks an
* otherwise-circular dep chain: `crypto` may depend on `bitpacker-crypto`
* (e.g. for `EnvelopeSchema` in `chain-bundle`) without inducing a cycle
* via this test helper.
*
* Production callers should still wire `verifyAuthoritySignature` from
* `@suaveplan/crypto/authority` and `aeadEncrypt` / `aeadDecrypt` from
* `@suaveplan/crypto` — those are the canonical implementations. This
* file mirrors their shape for test purposes only.
*/
import * as aesGcm from "@suaveplan/crypto-cipher/aes-256-gcm";
import * as chacha20 from "@suaveplan/crypto-cipher/chacha20-poly1305";
import * as xchacha20 from "@suaveplan/crypto-cipher/xchacha20-poly1305";
import { ed25519 } from "@suaveplan/crypto-core/sign";
import type {
AeadAlgorithm,
AeadOps,
AeadOpsOptions,
AuthorityVerifier,
DomainTag,
} from "@suaveplan/types";
const ED25519_ALGORITHM_ID = 0;
/**
* Inline copy of `buildSigningInput` from `@suaveplan/crypto/domain-tags`.
* Reproduced here to keep this test helper free of any `@suaveplan/crypto`
* runtime dep. The byte layout is identical: `tag-bytes ‖ 0x00 ‖ payload`.
*/
function buildSigningInput(domain: DomainTag, payload: Uint8Array): Uint8Array {
const tag = new TextEncoder().encode(domain);
const out = new Uint8Array(tag.length + 1 + payload.length);
out.set(tag, 0);
out[tag.length] = 0;
out.set(payload, tag.length + 1);
return out;
}
/**
* Test verifier — mirrors `verifyAuthoritySignature` from
* `@suaveplan/crypto/authority` byte-for-byte. Only ed25519 is supported
* (matches the canonical impl); other algorithms throw to preserve the
* "structural error → swallow as false" convention used by primitives.
*/
export const cryptoVerifier: AuthorityVerifier = (
domain,
publicSlot,
payload,
signature
) => {
if (publicSlot.algorithm !== ED25519_ALGORITHM_ID) {
throw new Error(
`cryptoVerifier: unsupported algorithm ${publicSlot.algorithm}`
);
}
const message = buildSigningInput(domain, payload);
return ed25519.verify(publicSlot.publicKey, signature, message);
};
function encrypt(
algorithm: AeadAlgorithm,
key: Uint8Array,
plaintext: Uint8Array,
options: AeadOpsOptions
): Uint8Array {
switch (algorithm) {
case "AES-256-GCM":
return aesGcm.encrypt(key, options.nonce, plaintext, options.aad);
case "ChaCha20-Poly1305":
return chacha20.encrypt(key, options.nonce, plaintext, options.aad);
case "XChaCha20-Poly1305":
return xchacha20.encrypt(
key,
options.nonce,
plaintext,
options.aad
);
}
}
function decrypt(
algorithm: AeadAlgorithm,
key: Uint8Array,
ciphertext: Uint8Array,
options: AeadOpsOptions
): Uint8Array {
switch (algorithm) {
case "AES-256-GCM":
return aesGcm.decrypt(key, options.nonce, ciphertext, options.aad);
case "ChaCha20-Poly1305":
return chacha20.decrypt(
key,
options.nonce,
ciphertext,
options.aad
);
case "XChaCha20-Poly1305":
return xchacha20.decrypt(
key,
options.nonce,
ciphertext,
options.aad
);
}
}
/**
* Test AEAD bundle — dispatches to the per-algorithm functions exposed by
* `@suaveplan/crypto-cipher`. Behaviorally identical to the canonical
* `{ encrypt: aeadEncrypt, decrypt: aeadDecrypt }` shape from
* `@suaveplan/crypto`.
*/
export const cryptoAead: AeadOps = { encrypt, decrypt };