CARLOS rastrillo docs

๐Ÿค– crypto

amadan.net/rastrillo/rastrillo/crypto

The family envelope: asymmetric sealing, signing, and the symmetric half โ€” every operation domain-separated by a caller-supplied context string.

This is a compatibility contract. It is byte-compatible with keymail's crypto.go, amadan's internal/envelope and internal/repokey, and seapointish's internal/seal โ€” the hand-rolled copies it retires. testdata/golden.json is amadan's pinned cross-implementation fixture, and this package and its JavaScript twin both have to pass it.

If you change anything here, the vectors are the specification.

The wire formats

Seal/Open        ephPub(65, uncompressed point) โ€– iv(12) โ€– AES-256-GCM ciphertext
Sign/Verify      ECDSA P-256 over SHA-256(context โ€– 0x00 โ€– msg), raw rโ€–s (32+32, zero-padded)
SealSym/OpenSym  iv(12) โ€– AES-256-GCM ciphertext
Derive           HKDF-SHA256, salt=nil, info=context, 32 bytes

Signatures are raw rโ€–s, not ASN.1 DER. A verifier expecting DER will reject every signature this package produces.

Keys

func Generate() (Keypair, error)
func NewKey() ([]byte, error)
func MarshalKeypair(k Keypair) ([]byte, error)
func UnmarshalKeypair(b []byte) (Keypair, error)

A Keypair carries both halves. Keypair.BoxPub is the public key for Seal, Keypair.SignPub the one for Verify. They are separate keys with separate jobs, and using one where the other belongs fails loudly instead of quietly weakening something.

NewKey mints raw symmetric key bytes for SealSym.

Asymmetric

func Seal(recipientBoxPub []byte, context string, msg []byte) ([]byte, error)
func Open(k Keypair, context string, sealed []byte) ([]byte, error)

ECDH P-256 with an ephemeral key, HKDF-SHA256 to a content key, then AES-256-GCM.

The context string is domain separation. The same bytes sealed under two contexts produce independent ciphertexts, and opening with the wrong context fails. Pick one per purpose and keep it stable: changing it is a format break for everything already sealed.

Signing

func Sign(k Keypair, context string, msg []byte) ([]byte, error)
func Verify(signPub []byte, context string, msg, sig []byte) bool

The signed digest is SHA-256(context โ€– 0x00 โ€– msg). That 0x00 stops the context/message boundary being ambiguous: without it, signing ("ab", "c") and ("a", "bc") would produce the same digest.

Symmetric

func Derive(secret []byte, context string) ([]byte, error)
func SealSym(key []byte, msg []byte) ([]byte, error)
func OpenSym(key []byte, sealed []byte) ([]byte, error)

Derive is HKDF-SHA256 with the context as info, producing 32 bytes. It is how one root secret becomes many purpose-specific keys that cannot be used interchangeably.

Invites

func NewInviteSecret() ([]byte, error)
func DeriveInvite(secret []byte, context string) (Invite, error)
func WrapKey(key, secret []byte, context string) (string, error)
func UnwrapKey(wrapped string, secret []byte, context string) ([]byte, error)

DeriveInvite derives an invite's id, wrapKey and claimSecret from one root secret by context suffix โ€” -id, -wrap, -claim โ€” with claimHash the hex SHA-256 of the claim secret. WrapKey and UnwrapKey are SealSym in base64url.

These waited until a consumer pinned their contract. Eleven's messenger did, and testdata/invites.json carries its vectors verbatim โ€” context "lchat-invite" reproduces its wire format byte for byte.

JS

func JS() []byte

The WebCrypto twin as an embedded ES module, so a browser can open what a server sealed. It passes the same golden vectors, which is what makes it a twin instead of a second implementation.

Read this page as markdown โ€” exact, unstyled, and cheap for an agent to fetch.