90 lines
3.3 KiB
Go
90 lines
3.3 KiB
Go
// Package frame defines the wire format of the unibus message bus.
|
|
//
|
|
// A Frame is the unit transported over NATS. It carries an envelope (type,
|
|
// subject, sender, message id, epoch) plus an optional payload that, in
|
|
// encrypted rooms, is an AEAD ciphertext. Frames may be signed end-to-end with
|
|
// Ed25519 so that any receiver can authenticate the sender without trusting the
|
|
// transport.
|
|
//
|
|
// v1 serializes frames as JSON for legibility and forward-compatibility. The
|
|
// canonical signing bytes are the JSON of the frame with Sig cleared, so that
|
|
// signature verification is independent of field ordering as long as the
|
|
// marshaler is deterministic (encoding/json emits struct fields in declaration
|
|
// order).
|
|
package frame
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
)
|
|
|
|
// FrameType enumerates the kinds of frames that travel over the bus.
|
|
type FrameType uint8
|
|
|
|
const (
|
|
// PUB is a published message to a room/subject.
|
|
PUB FrameType = iota
|
|
// INVITE announces a new member was invited (informational; the key
|
|
// distribution itself happens over the control plane).
|
|
INVITE
|
|
// JOIN announces a member joined a room.
|
|
JOIN
|
|
// LEAVE announces a member voluntarily left a room.
|
|
LEAVE
|
|
// KICK announces a member was removed from a room.
|
|
KICK
|
|
// ACK acknowledges receipt of a previous frame.
|
|
ACK
|
|
)
|
|
|
|
// BlobRef references an out-of-band encrypted blob stored in the object store.
|
|
// The bus never carries blob bytes; only this reference travels over NATS.
|
|
type BlobRef struct {
|
|
Hash string `json:"h"` // sha256 hex of the ciphertext in the blob store
|
|
Nonce []byte `json:"n"` // AEAD nonce used to encrypt the blob
|
|
Size int64 `json:"sz"` // size in bytes of the ciphertext
|
|
}
|
|
|
|
// Frame is the unit of transport on the unibus message bus.
|
|
type Frame struct {
|
|
Type FrameType `json:"t"`
|
|
Subject string `json:"s"`
|
|
Sender string `json:"from"` // endpoint id = EndpointID(signPub)
|
|
MsgID string `json:"id"` // ULID
|
|
Epoch int `json:"e"` // epoch of the room key K used to encrypt
|
|
Nonce []byte `json:"n,omitempty"` // AEAD nonce (encrypted rooms only)
|
|
Payload []byte `json:"p,omitempty"` // AEAD ciphertext (or cleartext if the room does not encrypt)
|
|
Blob *BlobRef `json:"b,omitempty"`
|
|
Sig []byte `json:"sig,omitempty"` // Ed25519 signature over SigningBytes()
|
|
}
|
|
|
|
// Marshal serializes the frame to its wire representation (JSON in v1).
|
|
func (f Frame) Marshal() ([]byte, error) {
|
|
return json.Marshal(f)
|
|
}
|
|
|
|
// Unmarshal parses a wire representation back into a Frame.
|
|
func Unmarshal(b []byte) (Frame, error) {
|
|
var f Frame
|
|
err := json.Unmarshal(b, &f)
|
|
return f, err
|
|
}
|
|
|
|
// EndpointID derives a stable, transport-agnostic endpoint identifier from an
|
|
// Ed25519 signing public key: base64url(sha256(signPub)), unpadded.
|
|
func EndpointID(signPub []byte) string {
|
|
sum := sha256.Sum256(signPub)
|
|
return base64.RawURLEncoding.EncodeToString(sum[:])
|
|
}
|
|
|
|
// SigningBytes returns the canonical bytes that are signed and verified. The
|
|
// signature covers the entire frame except the Sig field itself, so we clear
|
|
// Sig before marshaling. Errors are intentionally swallowed: the frame is a
|
|
// plain struct of JSON-serializable fields, so json.Marshal cannot fail here.
|
|
func (f Frame) SigningBytes() []byte {
|
|
f.Sig = nil
|
|
b, _ := json.Marshal(f)
|
|
return b
|
|
}
|