protocol

package
v0.25.9 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 16, 2026 License: MIT Imports: 23 Imported by: 0

Documentation

Index

Constants

View Source
const (
	// SeedSize is the length of a chat identity seed in bytes (256-bit, a
	// 24-word recovery mnemonic).
	SeedSize = 32
	// AddressSize is the length of a chat address in bytes.
	AddressSize = 12
	// X25519KeySize is the length of an x25519 public or private key.
	X25519KeySize = 32
	// ChatSrvMACSize is the truncated HMAC length authenticating a message
	// envelope to the server.
	ChatSrvMACSize = 8
	// ChatChunkMACSize is the truncated HMAC length authenticating one
	// upload chunk to the session.
	ChatChunkMACSize = 4
	// ChatReceiptMACSize is the truncated HMAC length of an end-to-end delivery
	// receipt. 6 bytes keeps the ACK and SEND_STATUS response in one cell, and
	// the MAC is verified offline by the sender (no online forgery oracle), so a
	// malicious server still cannot fabricate a ✓✓ — only withhold a real one.
	ChatReceiptMACSize = 6
)
View Source
const (
	// ChatMessageVersion is the wire version of message envelopes.
	ChatMessageVersion = 1
	// ChatRegisterVersion is the wire version of registration records.
	ChatRegisterVersion = 1

	// ChatMaxPlaintextBytes caps a decompressed message body. Real messages are
	// well under MaxMsgBytes; the cap only bounds memory if a hostile peer sends
	// a deflate bomb (a tiny ciphertext that inflates ~1000x). 64 KiB is far
	// above any legitimate text and far below a memory-DoS.
	ChatMaxPlaintextBytes = 64 * 1024
)
View Source
const (
	// ChatChannel is a reserved feed-channel number that feed domains refuse.
	// Chat is served only on its own sub-domains (dispatched by domain), so it
	// is no longer carried in chat queries — it just stays reserved.
	ChatChannel uint16 = 0xFFF6
	// ChatInfoChannel serves the signed chat capability payload on the feed
	// metadata path (block-count-prefixed, like TitlesChannel).
	ChatInfoChannel uint16 = 0xFFF5

	// ChatProtocolVersion is the chat request/response wire version (the high
	// nibble of an op byte). Kept for future changes; not bumped (chat is
	// unreleased, so there is no prior wire to stay compatible with).
	ChatProtocolVersion = 1

	// ChatCellPayloadSize is the per-cell payload after selector+counter.
	ChatCellPayloadSize = chatCellLen - chatSelectorSize - chatCounterSize // 19
	// ChatCellPlainSize is the max sealed plaintext (op + fields) per in-context
	// cell (payload minus the seal tag).
	ChatCellPlainSize = ChatCellPayloadSize - ChatSealTagSize // 15
	// ChatDataChunkSize is the message-body bytes carried by one DATA cell at the
	// default budget.
	ChatDataChunkSize = ChatCellPlainSize - 2 // op(1)+idx(1)+chunk → 13

	// ChatCellPlainMin / ChatCellPlainMax bound the configurable per-cell op
	// plaintext budget B (RFC §8.2 — v1 constant, client-chosen). The default is
	// ChatCellPlainSize (15); a client may pick any B in [min,max] to trade query
	// length against query count. Floor = room for the smallest framing; ceiling =
	// fits a single DNS label.
	ChatCellPlainMin = 6
	ChatCellPlainMax = 21
	// ChatCellPayloadMax is the largest sealed cell payload (max budget + jitter +
	// tag).
	ChatCellPayloadMax = ChatCellPlainMax + ChatJitterMax + ChatSealTagSize
	// ChatJitterMax bounds the deterministic per-cell length jitter (RFC §8.2):
	// 0..ChatJitterMax extra zero-pad bytes inside the seal, so cells vary in
	// length like the feed's 0-4 suffix instead of sitting at a single point. The
	// pad is keyed by (selector, counter) so a retransmit reproduces the exact
	// query name (resolver caches stay warm).
	ChatJitterMax = 4

	// ChatSelectorSize is exported for the session/selector layer.
	ChatSelectorSize = chatSelectorSize
)
View Source
const (
	ChatOpStatus     = 1
	ChatOpFetch      = 2
	ChatOpAck        = 3
	ChatOpKeyFetch   = 4
	ChatOpSendStatus = 5
	ChatOpSendStart  = 6
	ChatOpData       = 7
	ChatOpFin        = 8
	// ChatOpFrag carries one fragment of a control op too large for the current
	// cell budget B (RFC §8.2): plaintext = op(1) ‖ idx(1) ‖ total(1) ‖ chunk.
	// The server reassembles, then dispatches the inner op.
	ChatOpFrag = 9
)

Chat opcodes (low nibble of the first sealed-plaintext byte).

View Source
const (
	ChatHandshakeAuth     = 1 // bootstrap = addr ‖ ts ‖ account proof
	ChatHandshakeRegister = 2 // bootstrap = full self-signed register record
)

Handshake kinds (the cleartext byte after the eph key in the stream).

View Source
const (
	ChatStatusOK               = 0
	ChatStatusUnknownRecipient = 1
	ChatStatusInboxFull        = 2
	ChatStatusPairQuota        = 3
	ChatStatusRateLimited      = 4
	ChatStatusUnknownSender    = 5
	ChatStatusBadVersion       = 6
	ChatStatusBusy             = 7
	ChatStatusUnknownSession   = 8
	ChatStatusBadAuth          = 9
	// ChatStatusFragMore acks a non-final OP_FRAG cell: "fragment received, keep
	// sending"; the cell that completes the op returns the inner op's status.
	ChatStatusFragMore   = 10
	ChatStatusNotFound   = 11
	ChatStatusIncomplete = 12
	ChatStatusReplay     = 13
	ChatStatusBadRequest = 14
	ChatStatusDisabled   = 15
)

Chat response status codes.

View Source
const (
	KeySize   = 32 // AES-256
	NonceSize = 12 // GCM nonce
)
View Source
const (

	// SendChannel is the special channel number used for upstream message sending.
	// When a query has channel == SendChannel, the block field encodes the target
	// channel number, and additional data labels carry the encrypted message text.
	SendChannel uint16 = 0xFFFE

	// AdminChannel is the special channel number for admin commands (add/remove
	// channels, hard refresh). The encrypted payload is "password\ncmd\narg".
	AdminChannel uint16 = 0xFFFD

	// UpstreamInitChannel starts a chunked upstream session for admin/send payloads.
	UpstreamInitChannel uint16 = 0xFFFC
	// UpstreamDataChannel carries one chunk of a chunked upstream session.
	UpstreamDataChannel uint16 = 0xFFFB

	// VersionChannel serves latest release version with random suffix.
	VersionChannel uint16 = 0xFFFA

	// TitlesChannel serves per-channel human-readable display names.
	TitlesChannel uint16 = 0xFFF9

	// RelayInfoChannel serves the relay-discovery payload (GitHub
	// owner/repo + domain segment). Block 0 carries it.
	RelayInfoChannel uint16 = 0xFFF8

	// ProfilePicsChannel serves the per-channel profile-picture index:
	// for every Telegram channel that has a profile photo we emit
	// (username, mediaCh, size, crc32). Bytes themselves live on the
	// referenced mediaCh and are fetched via the regular media path.
	// Off by default on the client.
	ProfilePicsChannel uint16 = 0xFFF7

	// MaxUpstreamBlockPayload keeps uploaded query chunks comfortably below DNS
	// name limits across typical domains and resolver paths.
	MaxUpstreamBlockPayload = 8
	// MaxUpstreamBlocks bounds the amount of server-side session state.
	MaxUpstreamBlocks = 128
)
View Source
const (
	ExtraBlockVersion = 1
	// ExtraDigestSize is the truncated-SHA-256 length used for content
	// digests. 16 bytes (128-bit) is ample second-preimage resistance for
	// authenticating feed content while keeping the block small.
	ExtraDigestSize = 16
)
View Source
const (
	ExtraTLVTimestamp = 0x01 // int64 unix seconds, 8 bytes big-endian
	ExtraTLVDigest    = 0x02 // ExtraDigestSize bytes
	ExtraTLVSignature = 0x03 // ed25519 signature, ed25519.SignatureSize bytes
)

TLV entry types inside an ExtraBlock.

View Source
const (
	RelayDNS    = 0 // slow path — bytes assembled from DNS blocks
	RelayGitHub = 1 // fast path — bytes pulled from a GitHub repo
)

Relay indices: each MediaMeta.Relays[N] flags whether the file is reachable via that relay. Order is fixed so the wire format is positional. Future relays append to this list; older clients ignore unknown trailing flags.

View Source
const (
	EMHMagic0    byte = 0xFE
	EMHMagic1    byte = 0xED
	EMHHeaderLen      = MarkerSize + 4 // 3 (Marker) + 4 (Timestamp) = 7
	EMHHashLen        = 3
	EMHMaxBlocks      = 255

	// EMHFlagNewerAvailable, when bit-set in the flags byte at offset [2],
	// signals "a newer metadata format exists; if you support it, switch".
	// Reserved for future use — current encoders never set it; current
	// decoders never read it. Bits 1..7 are reserved for future flags.
	EMHFlagNewerAvailable byte = 1 << 0
)

Extended metadata header packs block_count + content_hash into the Marker[3] and Timestamp[4] fields of V0 metadata. Those fields are serialized + deserialized but never read by clients, so old clients keep working with no schema change while new clients use the embedded header to fetch all metadata blocks in parallel and validate the assembled payload was not mixed across server snapshots.

Wire layout (occupies the first 7 bytes of V0-serialized metadata, in the slots Marker[3] and Timestamp[4]):

[0]    EMHMagic0    = 0xFE
[1]    EMHMagic1    = 0xED
[2]    flags        uint8 — reserved (see EMHFlag*)
[3]    block_count  uint8 (1..255 total metadata blocks)
[4..6] content_hash 3 bytes — first 3 bytes of SHA-256(payload[7:])

The hash covers the body only — everything after the 7-byte header — so it changes whenever the actual metadata content changes (channel list, NextFetch, flags, etc.) but is independent of the block split. Magic bytes. 2 bytes of magic → 1/65536 probability that an old server's random 3-byte Marker happens to start with FE ED. False-positive cost is bounded: hash verify fails → retry → eventually cooldown for 10 min and fall back to the legacy fetch path. Acceptable; reduce by adding a third magic byte if we ever observe this misbehaviour in the wild.

View Source
const (
	ProfilePicMimeJPEG uint8 = 0
	ProfilePicMimePNG  uint8 = 1
	ProfilePicMimeWebP uint8 = 2
)

MIME tag values.

View Source
const (
	// MinBlockPayload is the minimum decrypted payload per DNS TXT block.
	MinBlockPayload = 200
	// MaxBlockPayload is the maximum decrypted payload per DNS TXT block.
	MaxBlockPayload = 600
	// DefaultBlockPayload is kept for compatibility; equals MaxBlockPayload.
	DefaultBlockPayload = MaxBlockPayload

	// DefaultMaxPadding is the default random padding added to responses to vary DNS response size.
	DefaultMaxPadding = 32

	// PadLengthSize is the 2-byte length prefix added before real data when padding is used.
	PadLengthSize = 2

	// MetadataChannel is the special channel number for server metadata.
	MetadataChannel = 0

	// MediaChannelStart and MediaChannelEnd bound the channel-number range
	// reserved for cached binary media (images, files, ...). Each cached file
	// occupies one channel; bytes are split into raw blocks served via the
	// usual DNS TXT path. The range is well above typical feed channel counts
	// and well below the special control channels at the top of uint16 space.
	MediaChannelStart uint16 = 10000
	MediaChannelEnd   uint16 = 60000 // inclusive

	// MarkerSize is the random marker in metadata to verify data freshness.
	MarkerSize = 3

	// Query payload structure sizes.
	QueryPaddingSize = 4
	QueryChannelSize = 2
	QueryBlockSize   = 2
	QueryPayloadSize = QueryPaddingSize + QueryChannelSize + QueryBlockSize // 8

	// Message header sizes (in the serialized message stream).
	MsgIDSize          = 4
	MsgTimestampSize   = 4
	MsgLengthSize      = 2
	MsgHeaderSize      = MsgIDSize + MsgTimestampSize + MsgLengthSize // 10
	MsgContentHashSize = 4
)
View Source
const (
	MediaImage    = "[IMAGE]"
	MediaVideo    = "[VIDEO]"
	MediaFile     = "[FILE]"
	MediaAudio    = "[AUDIO]"
	MediaSticker  = "[STICKER]"
	MediaGIF      = "[GIF]"
	MediaPoll     = "[POLL]"
	MediaContact  = "[CONTACT]"
	MediaLocation = "[LOCATION]"
	MediaReply    = "[REPLY]"
	// MediaMe marks an outgoing private-chat message — sent by the
	// authenticated user. The client renders these right-aligned with
	// a [YOU] label instead of the sender-name prefix.
	MediaMe = "[ME]"
)

Media placeholder strings for non-text content.

View Source
const ChatAccountProofSize = 8

ChatAccountProofSize is the truncated HMAC proving account control in an auth handshake.

View Source
const ChatPeerHandleSize = 4

ChatPeerHandleSize is the short reference to a peer used in ACK/SENDSTATUS instead of the full 12-byte address.

View Source
const (

	// ChatSealTagSize is the truncated per-query MAC length.
	ChatSealTagSize = 4
)
View Source
const GCMNonceSize = 12

GCMNonceSize is the AES-GCM nonce length (carried in the chat envelope).

View Source
const MediaBlockHeaderLen = 16

MediaBlockHeaderLen is the fixed length of the metadata prefix that the server prepends to a cached media file's bytes before splitting into blocks. Block 0 of every media channel begins with these bytes.

Layout (big-endian where multi-byte):
[0:4]   CRC32(IEEE) of the DECOMPRESSED file content
[4]     header version (currently 1)
[5]     compression byte (MediaCompression*)
[6:16]  reserved (zero) — room for future protocol fields without
        bumping the version byte
View Source
const MediaHeaderVersion uint8 = 1

MediaHeaderVersion is the current header version. Bumped when the layout changes incompatibly; until then, the reserved bytes carry future fields.

View Source
const RegisterEnvelopeLen = 1 + ed25519.PublicKeySize + X25519KeySize + 4 + ed25519.SignatureSize

RegisterEnvelopeLen is the fixed registration-record length.

Variables

View Source
var ChatSessionLostResp = []byte{0xE5}

ChatSessionLostResp is the 1-byte unsealed sentinel a server returns for an in-context cell whose session it no longer knows (TTL expiry or reboot). The client can't open a sealed reply for a dead session, so this length-1 marker (a sealed reply is always ≥1+ChatSealTagSize) tells it to re-handshake.

Functions

func Address added in v0.25.9

func Address(identityPub ed25519.PublicKey) [AddressSize]byte

Address returns the chat address for an ed25519 identity public key: the first AddressSize bytes of its SHA-256 hash.

func BuildChatAckPlain added in v0.25.9

func BuildChatAckPlain(peer [ChatPeerHandleSize]byte, upToSeq uint32, receipt [ChatReceiptMACSize]byte) []byte

BuildChatAckPlain: ACK peer's messages up to upToSeq (peer by handle). The receipt is the recipient's E2E proof of delivery (ChatReceiptMAC); it rides the spare cell bytes (op1+handle4+seq3+receipt6 = 14 ≤ 15). An all-zero receipt means "no proof" — the ack still frees quota, the sender just won't see a verified ✓✓.

func BuildChatAuthBootstrapPlain added in v0.25.9

func BuildChatAuthBootstrapPlain(addr [AddressSize]byte, ts uint32, proof [ChatAccountProofSize]byte) []byte

BuildChatAuthBootstrapPlain builds an auth-handshake bootstrap plaintext.

func BuildChatDataPlain added in v0.25.9

func BuildChatDataPlain(idx uint8, chunk []byte) ([]byte, error)

BuildChatDataPlain: one body chunk at index.

func BuildChatFetchPlain added in v0.25.9

func BuildChatFetchPlain(peer [ChatPeerHandleSize]byte, seq uint32, block uint8) []byte

BuildChatFetchPlain: INBOX_FETCH for (peer, seq, block). The peer handle disambiguates the sender — seq is per-pair, so two senders can both have a pending message at the same seq; the server resolves the handle to the full address within the caller's known pairs (as ACK does). 10 bytes, fits a cell.

func BuildChatFinPlain added in v0.25.9

func BuildChatFinPlain(crc uint32) []byte

BuildChatFinPlain: commit the upload (crc over the assembled body).

func BuildChatFragPlain added in v0.25.9

func BuildChatFragPlain(idx, total uint8, chunk []byte) []byte

BuildChatFragPlain: one fragment (idx of total) of an inner op too big for the budget. chunk ≤ budget-3 (op+idx+total).

func BuildChatHandshakeStream added in v0.25.9

func BuildChatHandshakeStream(ephPub []byte, protoVer, kind byte, sealedBootstrap []byte) []byte

BuildChatHandshakeStream assembles the reassembled handshake stream: eph(32) ‖ proto_ver(1) ‖ kind(1) ‖ sealedBootstrap.

proto_ver is cleartext (the server must read it before deriving Ksession to pick the version's derivation) but tamper-evident: it is bound into Ksession (see ChatSessionKey), so flipping it just breaks the bootstrap seal.

func BuildChatKeyFetchPlain added in v0.25.9

func BuildChatKeyFetchPlain(addr [AddressSize]byte) []byte

BuildChatKeyFetchPlain: fetch a peer's registration record (full addr).

func BuildChatSendStartPlain added in v0.25.9

func BuildChatSendStartPlain(dst [AddressSize]byte, totalLen uint16) []byte

BuildChatSendStartPlain: start a message upload to dst (src is the session).

func BuildChatSendStatusPlain added in v0.25.9

func BuildChatSendStatusPlain(peer [AddressSize]byte) []byte

BuildChatSendStatusPlain: ✓/✓✓ counters for own messages to peer. The full address is sent (the recipient may not be in the caller's known pairs, so a handle can't be resolved server-side) — it still fits one cell.

func BuildChatStatusPlain added in v0.25.9

func BuildChatStatusPlain() []byte

BuildChatStatusPlain: INBOX_STATUS (freshness comes from the cell counter).

func ChannelEligibleForSharedCache added in v0.19.0

func ChannelEligibleForSharedCache(channel uint16) bool

ChannelEligibleForSharedCache reports whether queries on a channel may use a deterministic suffix. False for metadata (clients must always see fresh metadata) and for write/admin channels (each write is unique by design).

func ChatAccountProof added in v0.25.9

func ChatAccountProof(kss [KeySize]byte, ephPub []byte, addr [AddressSize]byte, ts uint32, domain string) [ChatAccountProofSize]byte

ChatAccountProof binds an auth handshake to the account: HMAC under kss (the client↔server shared key, computable only by the account holder and the server) over the ephemeral key, address, timestamp and chat domain. The domain binding stops cross-server replay.

func ChatAvailableFromBlock0 added in v0.25.9

func ChatAvailableFromBlock0(block0 []byte) (available, ok bool)

ChatAvailableFromBlock0 reads the ChatAvailable advertisement (flags bit 0x02) from metadata block 0 alone — no need to fetch or verify the rest of the metadata. The flags byte sits at a fixed offset (marker+timestamp+nextFetch) in every metadata format, so block 0 is enough. ok is false if block 0 is too short to contain it. This is an optimization hint only: it lets a client skip the (retry-prone) ChatInfo probe on a server that advertises no messenger; the authoritative, signed capability check is still ChatInfo.

func ChatBootstrapCounter added in v0.25.9

func ChatBootstrapCounter() uint32

ChatBootstrapCounter is the counter the bootstrap blob is sealed under.

func ChatCellJitter added in v0.25.9

func ChatCellJitter(queryKey [KeySize]byte, selector [chatSelectorSize]byte, counter uint32) int

ChatCellJitter returns the deterministic extra-pad length (0..ChatJitterMax) for a cell, keyed by the query key and the cell's (selector, counter). Both ends compute it identically, so the client pads to budget+jitter and the server recovers the real budget by subtracting it — and a byte-identical retransmit reproduces the same query name. OP_FRAG cells pass jitter=0 (a fragment's chunk is concatenated, so trailing pad can't ride along).

func ChatChunkMAC added in v0.25.9

func ChatChunkMAC(macKey [KeySize]byte, sessionID uint32, index uint8, chunk []byte) [ChatChunkMACSize]byte

ChatChunkMAC authenticates one upload chunk to its session so an attacker who learned the session id still cannot poison the upload.

func ChatClearHandshakeSelector added in v0.25.9

func ChatClearHandshakeSelector(sel *[chatSelectorSize]byte)

ChatClearHandshakeSelector clears the handshake flag (server session refs).

func ChatContentKey added in v0.25.9

func ChatContentKey(own *ecdh.PrivateKey, peerEncPub []byte, src, dst [AddressSize]byte, seq uint32) ([KeySize]byte, error)

ChatContentKey derives the AES-256 key sealing one message body. Both ends compute the same key: the sender from its enc private key and the recipient's published enc key, the recipient from its enc private key and the sender's published enc key. The key is bound to the message's (src, dst, seq) so it is valid for exactly one message slot.

func ChatIsHandshakeSelector added in v0.25.9

func ChatIsHandshakeSelector(sel [chatSelectorSize]byte) bool

ChatIsHandshakeSelector reports whether the handshake flag is set.

func ChatIsSessionLost added in v0.25.9

func ChatIsSessionLost(resp []byte) bool

ChatIsSessionLost reports whether a decoded response is the session-lost sentinel.

func ChatMarkHandshakeSelector added in v0.25.9

func ChatMarkHandshakeSelector(sel *[chatSelectorSize]byte)

ChatMarkHandshakeSelector sets the handshake flag (client setup tags).

func ChatPeerHandle added in v0.25.9

func ChatPeerHandle(addr [AddressSize]byte) [ChatPeerHandleSize]byte

ChatPeerHandle is the first bytes of a peer address; the server disambiguates it within the caller account's known pairs. A collision is 2^-32 with a handful of contacts and only ever names one of YOUR own contacts.

func ChatPlainOp added in v0.25.9

func ChatPlainOp(pt []byte) byte

ChatPlainOp / ChatPlainVersion read the op / version from a sealed-plaintext.

func ChatPlainVersion added in v0.25.9

func ChatPlainVersion(pt []byte) byte

func ChatReceiptKey added in v0.25.9

func ChatReceiptKey(own *ecdh.PrivateKey, peerEncPub []byte) ([KeySize]byte, error)

ChatReceiptKey derives the pair key that authenticates delivery receipts. Like ChatContentKey it comes from the two peers' static ECDH — so only the two ends can compute it, never the relaying server — but it is independent of any single message (no seq), so one key covers the whole conversation. The recipient signs its ACK watermark with it; the sender verifies, turning ✓✓ from a server claim into a fact the server cannot forge.

func ChatReceiptMAC added in v0.25.9

func ChatReceiptMAC(receiptKey [KeySize]byte, sender, recipient [AddressSize]byte, upToSeq uint32) [ChatReceiptMACSize]byte

ChatReceiptMAC is the recipient's proof that it acknowledged sender→recipient messages up to upToSeq. The (sender, recipient) order is fixed by the message direction (originator first), so both ends bind the same tuple and a receipt can't be reflected onto the reverse direction.

func ChatServerMAC added in v0.25.9

func ChatServerMAC(kss [KeySize]byte, src, dst [AddressSize]byte, seq uint32, ciphertext []byte) [ChatSrvMACSize]byte

ChatServerMAC authenticates a message envelope to the server: only the registered holder of the sender enc key can produce it, and it binds the routing pair, the sequence, and the exact ciphertext.

func ChatServerSharedKey added in v0.25.9

func ChatServerSharedKey(priv *ecdh.PrivateKey, peerPub, clientEncPub, serverEkPub []byte) ([KeySize]byte, error)

ChatServerSharedKey derives the long-lived client↔server key (used for the envelope server MAC). The client calls it with its enc private key and the server's published ek; the server with its ek private key and the client's registered enc key. clientEncPub and serverEkPub fix the info ordering so both sides derive identical bytes.

func ChatSessionKey added in v0.25.9

func ChatSessionKey(own *ecdh.PrivateKey, peerPub []byte, protoVer byte, queryKey [KeySize]byte) ([KeySize]byte, error)

ChatSessionKey derives the per-connection session key from an eph↔ek ECDH, mixing the query key into the HKDF info so the public passphrase is required, and the negotiated protocol version so a tampered/cleartext version byte in the handshake can't downgrade: a different version yields a different key, so the sealed bootstrap fails to open (fail-closed) rather than being accepted under another version.

func ChatSessionKeys added in v0.25.9

func ChatSessionKeys(priv *ecdh.PrivateKey, peerPub, ephPub, serverEkPub []byte) (routing, mac [KeySize]byte, err error)

ChatSessionKeys derives the routing-encryption key and the chunk-MAC key for one upload session from a fresh ephemeral↔ek ECDH. The client calls it with the ephemeral private key and the server ek; the server with the ek private key and the ephemeral public key from INIT.

func CompressMessages

func CompressMessages(data []byte) []byte

CompressMessages compresses serialized message data using deflate. The output has a 1-byte header (compression type) followed by the payload. If compression doesn't reduce size, the raw data is stored instead.

func ContentDigest added in v0.25.9

func ContentDigest(canonical []byte) []byte

ContentDigest returns the truncated SHA-256 of canonical channel content. The caller is responsible for producing the canonical bytes (e.g. SerializeMessages / SerializeMetadata) identically on server and client.

func ContentHashOf

func ContentHashOf(msgs []Message) uint32

ContentHashOf computes a CRC32 hash of serialized message data. This changes when any message is edited, even if IDs stay the same.

func DecodeChatCell added in v0.25.9

func DecodeChatCell(queryKey [KeySize]byte, qname, domain string) (selector [chatSelectorSize]byte, counter uint32, payload []byte, err error)

DecodeChatCell splits a query name into its selector, counter, and the fixed-length payload (still sealed, for in-context cells), un-masking the selector+counter first.

func DecodeQuery

func DecodeQuery(queryKey [KeySize]byte, qname, domain string) (channel, block uint16, err error)

DecodeQuery parses and decrypts a DNS query subdomain. Auto-detects plain-text (c<N>b<M>), single-label base32, or multi-label hex encoding.

func DecodeResponse

func DecodeResponse(responseKey [KeySize]byte, encoded string) ([]byte, error)

DecodeResponse base64-decodes and decrypts a DNS TXT response, stripping padding.

func DecodeSendQuery

func DecodeSendQuery(queryKey [KeySize]byte, qname, domain string) (targetChannel uint16, message []byte, err error)

DecodeSendQuery decodes a send-message DNS query. Returns the target channel number and decrypted message text.

func DecodeTitlesData added in v0.11.0

func DecodeTitlesData(data []byte) (map[string]string, error)

DecodeTitlesData decodes a name→title map from bytes produced by EncodeTitlesData.

func DecodeUpstreamBlockQuery

func DecodeUpstreamBlockQuery(queryKey [KeySize]byte, qname, domain string) (sessionID uint16, index uint8, chunk []byte, err error)

DecodeUpstreamBlockQuery decodes one chunk of a chunked upstream payload. The first 2 bytes of chunk data live in the encrypted header[6:8]; any remaining bytes follow the 16-byte ciphertext as raw bytes.

func DecodeVersionData added in v0.7.0

func DecodeVersionData(block []byte) (string, error)

DecodeVersionData extracts the version string from a block produced by EncodeVersionData.

func DecompressMessages

func DecompressMessages(data []byte) ([]byte, error)

DecompressMessages decompresses data produced by CompressMessages. Reads the 1-byte header to determine the compression type.

func DecompressMessagesLimited added in v0.25.9

func DecompressMessagesLimited(data []byte, maxOut int) ([]byte, error)

DecompressMessagesLimited is DecompressMessages with a cap on the inflated output (maxOut <= 0 means unbounded). A caller that decompresses data from an untrusted source must pass a sane cap: deflate expands up to ~1000x, so a small ciphertext can otherwise inflate into a memory-exhausting "zip bomb".

func Decrypt

func Decrypt(key [KeySize]byte, ciphertext []byte) ([]byte, error)

Decrypt decrypts AES-256-GCM ciphertext (nonce+ciphertext+tag).

func DecryptRelayBlob added in v0.13.0

func DecryptRelayBlob(key [KeySize]byte, blob []byte) ([]byte, error)

DecryptRelayBlob is the inverse of EncryptRelayBlob.

func DecryptWithNonce added in v0.25.9

func DecryptWithNonce(key [KeySize]byte, nonce, ct []byte) ([]byte, error)

DecryptWithNonce reverses EncryptWithNonce.

func DeriveEncryptionKey added in v0.25.9

func DeriveEncryptionKey(seed []byte) (*ecdh.PrivateKey, error)

DeriveEncryptionKey derives the x25519 encryption key from a seed.

func DeriveIdentityKey added in v0.25.9

func DeriveIdentityKey(seed []byte) (ed25519.PrivateKey, error)

DeriveIdentityKey derives the ed25519 identity key from a seed.

func DeriveKeys

func DeriveKeys(passphrase string) (queryKey, responseKey [KeySize]byte, err error)

DeriveKeys derives separate query and response AES-256 keys from a passphrase using HKDF.

func DeriveRelayKey added in v0.13.0

func DeriveRelayKey(passphrase string) ([KeySize]byte, error)

DeriveRelayKey derives the AES-256 key used to encrypt blobs uploaded to a shared relay (e.g. a public GitHub repo) and to HMAC the path segments. Returns an error only if HKDF fails; the result is deterministic for a given passphrase.

func EncodeAdminQuery

func EncodeAdminQuery(queryKey [KeySize]byte, cmd AdminCmd, arg []byte, domain string, mode QueryEncoding) (string, error)

EncodeAdminQuery creates a DNS query that carries an admin command to the server. The payload is a single AdminCmd byte followed by optional argument bytes, GCM-encrypted and split across DNS labels.

func EncodeChatCell added in v0.25.9

func EncodeChatCell(queryKey [KeySize]byte, mode QueryEncoding, selector [chatSelectorSize]byte, counter uint32, payload []byte, domain string) (string, error)

EncodeChatCell packs one uniform cell into a query name. payload must be ≤ ChatCellPayloadSize; it is zero-padded to the fixed cell length and the selector+counter are masked so the whole name looks random.

func EncodeChatInfo added in v0.25.9

func EncodeChatInfo(info ChatInfo) []byte

EncodeChatInfo encodes a ChatInfo payload (TLV; unknown types are skipped by old parsers, so fields can be added later).

func EncodeChatMessage added in v0.25.9

func EncodeChatMessage(contentKey, kss [KeySize]byte, src, dst [AddressSize]byte, seq uint32, text string) ([]byte, error)

EncodeChatMessage seals text into a message envelope. The text is deflate-compressed (store-raw if not smaller) before encryption. contentKey is the pair key from ChatContentKey; kss the client↔server key from ChatServerSharedKey.

func EncodeExtraBlock added in v0.25.9

func EncodeExtraBlock(priv ed25519.PrivateKey, channelID uint16, digest []byte, ts int64) ([]byte, error)

EncodeExtraBlock builds a single signed ExtraBlock (count=1, index=0) for a channel and pads it to a random size in [MinBlockPayload, MaxBlockPayload] so it is indistinguishable from a normal content block. ts is unix seconds; digest must be ExtraDigestSize bytes (see ContentDigest). The returned bytes are plaintext — encrypt them with EncodeResponse like any other block.

func EncodeMediaBlockHeader added in v0.13.0

func EncodeMediaBlockHeader(h MediaBlockHeader) []byte

EncodeMediaBlockHeader writes the binary header into a fresh slice of length MediaBlockHeaderLen. Reserved bytes are zero-padded.

func EncodeMediaText added in v0.13.0

func EncodeMediaText(meta MediaMeta, caption string) string

EncodeMediaText prepends the metadata line to an optional caption and returns the combined message text. A nil/empty caption yields just the tag + metadata + trailing newline-less string (the caption split is by the metadata line's trailing \n, so an empty caption simply has no extra body).

func EncodeMetadataExtended added in v0.19.0

func EncodeMetadataExtended(m *Metadata) ([][]byte, error)

EncodeMetadataExtended serializes metadata with the extended header embedded in Marker/Timestamp, splits into blocks, and patches the final block_count into block 0. Callers feed plain *Metadata; the Marker and Timestamp fields on the input are overwritten with the embedded header.

func EncodeProfilePicsBundle added in v0.16.0

func EncodeProfilePicsBundle(b ProfilePicsBundle) []byte

EncodeProfilePicsBundle serialises the directory.

func EncodeQuery

func EncodeQuery(queryKey [KeySize]byte, channel, block uint16, domain string, mode QueryEncoding) (string, error)

EncodeQuery creates a DNS query subdomain for the given channel and block. Single-label (default): [base32_encrypted].domain Multi-label: [hex_part1].[hex_part2].domain All queries are encrypted to prevent DPI detection.

func EncodeQueryDeterministic added in v0.19.0

func EncodeQueryDeterministic(queryKey [KeySize]byte, channel, block uint16, domain string, mode QueryEncoding, seed []byte) (string, error)

EncodeQueryDeterministic produces the same DNS query subdomain every time it is called with the same (key, channel, block, mode, seed). Lets public DNS resolvers cache the response across users who share the seed.

Caller is responsible for excluding channels that must never be cached (MetadataChannel and the write channels). Use ChannelEligibleForSharedCache to gate this.

func EncodeRegisterEnvelope added in v0.25.9

func EncodeRegisterEnvelope(identity ed25519.PrivateKey, encPub []byte, timestamp uint32) ([]byte, error)

EncodeRegisterEnvelope builds a registration record binding an x25519 encryption key to an identity, signed by the identity key. timestamp is unix seconds (newest record wins on re-registration).

func EncodeResponse

func EncodeResponse(responseKey [KeySize]byte, data []byte, maxPadding int) (string, error)

EncodeResponse encrypts and base64-encodes a block payload for a DNS TXT response. Adds a 2-byte length prefix and random padding to vary response size for anti-DPI.

func EncodeSendQuery

func EncodeSendQuery(queryKey [KeySize]byte, targetChannel uint16, message []byte, domain string, mode QueryEncoding) (string, error)

EncodeSendQuery creates a DNS query that carries an upstream message. Format: [header_b32].[data_b32].domain The header is a normal encrypted 8-byte query with channel=SendChannel and block=targetChannel. The data label contains GCM-encrypted message text. Returns an error if the message is too long for a single DNS query.

func EncodeTitlesData added in v0.11.0

func EncodeTitlesData(titles map[string]string) []byte

EncodeTitlesData encodes a name→title map into bytes for TitlesChannel blocks. Format: count(2) + [nameLen(1)+name+titleLen(1)+title]*count

func EncodeUpstreamBlockQuery

func EncodeUpstreamBlockQuery(queryKey [KeySize]byte, sessionID uint16, index uint8, chunk []byte, domain string, mode QueryEncoding) (string, error)

EncodeUpstreamBlockQuery encodes one chunk of a chunked upstream payload into a single DNS label. The first min(2, len(chunk)) bytes are embedded in the AES-ECB header at [6:8] (not covered by integrity check); any remaining bytes are appended raw after the 16-byte ciphertext. The upstream payload is already GCM-encrypted, so confidentiality is preserved; tampering is caught by GCM on reassembly.

Header: [0:2] session_id, [2] index, [3] chunk_len,
        [4:6] channel=UpstreamDataChannel, [6:8] chunk prefix
Suffix: chunk[2:] (raw, up to 6 bytes)

Max label: 16 + 6 = 22 bytes → 36 base32 chars (fits in 63-char DNS label).

func EncodeUpstreamInitQuery

func EncodeUpstreamInitQuery(queryKey [KeySize]byte, init UpstreamInit, domain string, mode QueryEncoding) (string, error)

EncodeUpstreamInitQuery creates a compact single-label query that registers a chunked upstream session. All init data is packed into the AES-ECB header:

[0:2] session_id, [2] total_blocks, [3] kind,
[4:6] channel=UpstreamInitChannel, [6] target_channel, [7] 0

No GCM data labels — just one 26-char base32 label + domain.

func EncodeVersionData added in v0.7.0

func EncodeVersionData(version string) ([]byte, error)

EncodeVersionData encodes a version string into a single block padded to a random size in [MinBlockPayload, MaxBlockPayload], making it indistinguishable in size from regular content blocks for DPI resistance. Format:

[2 bytes: version byte length][version bytes][random padding]

func Encrypt

func Encrypt(key [KeySize]byte, plaintext []byte) ([]byte, error)

Encrypt encrypts plaintext using AES-256-GCM. Returns nonce+ciphertext+tag.

func EncryptRelayBlob added in v0.13.0

func EncryptRelayBlob(key [KeySize]byte, plaintext []byte) ([]byte, error)

EncryptRelayBlob seals plaintext with the relay key. Output is nonce||ciphertext||tag, identical framing to the DNS response cipher so clients can reuse Decrypt for both paths.

func EncryptWithNonce added in v0.25.9

func EncryptWithNonce(key [KeySize]byte, nonce, plaintext []byte) ([]byte, error)

EncryptWithNonce seals plaintext under key with an explicit (caller-supplied) nonce. The nonce MUST be unique per key; the chat envelope uses a fresh random one per message so a repeated (src,dst,seq) — e.g. the same recipient on two servers — never reuses the keystream. Output is ciphertext+tag (no nonce).

func GenerateEphemeralKey added in v0.25.9

func GenerateEphemeralKey() (*ecdh.PrivateKey, error)

GenerateEphemeralKey returns a fresh x25519 key for one upload session.

func GenerateSeed added in v0.25.9

func GenerateSeed() ([]byte, error)

GenerateSeed returns a new random chat identity seed.

func IsMediaChannel added in v0.13.0

func IsMediaChannel(ch uint16) bool

IsMediaChannel reports whether ch falls inside the reserved media-blob channel range. Media channels are not enumerated in Metadata; the client learns each (channel, blocks, hash) tuple from the corresponding feed message text via [TAG]<size>:<dl>:<ch>:<blk>:<crc32hex>.

func OpenChat added in v0.25.9

func OpenChat(ksession [KeySize]byte, selector []byte, counter uint32, sealed []byte) ([]byte, error)

OpenChat verifies the tag and decrypts. Fail-closed on any mismatch.

func OpenChatCellPayload added in v0.25.9

func OpenChatCellPayload(ksession [KeySize]byte, selector [chatSelectorSize]byte, counter uint32, payload []byte) ([]byte, error)

OpenChatCellPayload reverses SealChatCellPayload, returning the fixed-size plaintext (caller reads op + fields, ignores trailing pad).

func OpenChatResponse added in v0.25.9

func OpenChatResponse(ksession [KeySize]byte, selector [chatSelectorSize]byte, reqCounter uint32, sealed []byte) (status byte, body []byte, err error)

OpenChatResponse reverses SealChatResponse.

func ParseChatHandshakeStream added in v0.25.9

func ParseChatHandshakeStream(stream []byte) (ephPub []byte, protoVer, kind byte, sealedBootstrap []byte, err error)

ParseChatHandshakeStream splits a reassembled handshake stream.

func PeekExtendedHeader added in v0.19.0

func PeekExtendedHeader(block0 []byte) (extended bool, blockCount uint8, hash [EMHHashLen]byte, err error)

PeekExtendedHeader inspects the first EMHHeaderLen bytes of a metadata block 0. Returns (extended=true, count, hash, nil) when the magic + version match; (extended=false, …, nil) when this is a legacy V0 payload from an old server. Errors are reserved for genuinely malformed input (block too short or block_count=0 with magic present).

func RelayDomainSegment added in v0.13.0

func RelayDomainSegment(domain, passphrase string) string

RelayDomainSegment returns the path segment that scopes a deployment's files inside a shared relay repo. Computed as HMAC-SHA256 over the domain, keyed by a passphrase-derived secret, then truncated. Without the passphrase an observer cannot tell which deployment a folder belongs to.

func RelayObjectName added in v0.13.0

func RelayObjectName(size int64, crc uint32, passphrase string) string

RelayObjectName returns the per-file path segment under the domain folder. Computed from (size, crc) so the same content always lives at the same path (dedup), but HMAC'd with the passphrase so an observer can't probe "is a known file present in this repo?".

func SanitiseMediaFilename added in v0.13.0

func SanitiseMediaFilename(s string) string

SanitiseMediaFilename returns a filename safe to embed in the wire metadata line. The output uses a restricted alphabet ([A-Za-z0-9._-]) so no path separator, colon, newline, or control char can ever survive. When the input is too long the base name is replaced with a short hash-derived id but the extension is preserved so other OSes still recognise the file type.

func SealChat added in v0.25.9

func SealChat(ksession [KeySize]byte, selector []byte, counter uint32, plaintext []byte) []byte

SealChat returns ct‖tag(4): AES-CTR of plaintext, then a truncated HMAC over nonce‖ct. Deterministic for the same (key,selector,counter,plaintext).

func SealChatCellPayload added in v0.25.9

func SealChatCellPayload(ksession [KeySize]byte, selector [chatSelectorSize]byte, counter uint32, plaintext []byte) ([]byte, error)

SealChatCellPayload seals an op plaintext into a default-budget (15-byte plain, 19-byte payload) cell. Equivalent to SealChatCellPayloadN at the default budget.

func SealChatCellPayloadN added in v0.25.9

func SealChatCellPayloadN(ksession [KeySize]byte, selector [chatSelectorSize]byte, counter uint32, plaintext []byte, budget int) ([]byte, error)

SealChatCellPayloadN seals an op plaintext into a cell of the given budget B (op plaintext bytes per cell): the plaintext is zero-padded to B before sealing, so every cell at a given budget is the same length. B must be in [ChatCellPlainMin, ChatCellPlainMax].

func SealChatResponse added in v0.25.9

func SealChatResponse(ksession [KeySize]byte, selector [chatSelectorSize]byte, reqCounter uint32, status byte, body []byte) []byte

SealChatResponse seals status‖body under the session key with a response-side counter (distinct from any request nonce).

func SerializeMessages

func SerializeMessages(msgs []Message) []byte

SerializeMessages encodes messages into a byte stream for data channel blocks.

func SerializeMetadata

func SerializeMetadata(m *Metadata) []byte

SerializeMetadata encodes metadata into bytes for channel 0 blocks. Format: marker(3) + timestamp(4) + nextFetch(4) + flags(1) + channelCount(2) + per-channel data Per-channel: nameLen(1) + name + blocks(2) + lastMsgID(4) + contentHash(4) + chatType(1) + flags(1)

New servers wrap the same payload with an extended header that reuses the otherwise-unread Marker + Timestamp fields — see EncodeMetadataExtended in metadata_ext.go. Old clients keep parsing this format unchanged and just ignore those fields.

func SplitChunks added in v0.25.9

func SplitChunks(data []byte, chunkSize int) [][]byte

SplitChunks splits data into fixed-size chunks; the final chunk may be shorter. Returns one empty chunk for empty data so a session always has at least one block.

func SplitIntoBlocks

func SplitIntoBlocks(data []byte) [][]byte

SplitIntoBlocks splits data into blocks of randomly varying size in [MinBlockPayload, MaxBlockPayload]. Random sizes make traffic analysis harder; the client just concatenates all blocks to reassemble.

func VerifyEntry added in v0.16.0

func VerifyEntry(bundle []byte, entry ProfilePicEntry) ([]byte, error)

VerifyEntry returns bundle[entry.Offset:entry.Offset+entry.Size] if the slice is in-range and its CRC32-IEEE matches entry.CRC. The hash check is what stops a misaligned bundle from serving the wrong avatar under a username.

func VerifyExtendedHash added in v0.19.0

func VerifyExtendedHash(hash [EMHHashLen]byte, body []byte) error

VerifyExtendedHash returns nil when the first 3 bytes of SHA-256(body) match the hash embedded in the header. body must be the assembled bytes AFTER the 7-byte EMH header.

func VerifyExtraBlock added in v0.25.9

func VerifyExtraBlock(pub ed25519.PublicKey, channelID uint16, eb *ExtraBlock) error

VerifyExtraBlock checks the signature against the server public key, binding it to channelID. channelID is the channel the client actually requested — if a resolver served a different channel's extra block, the signed channel id will not match and verification fails.

Types

type AdminCmd

type AdminCmd byte

AdminCmd identifies admin commands carried in upstream admin payloads.

const (
	AdminCmdAddChannel    AdminCmd = 1
	AdminCmdRemoveChannel AdminCmd = 2
	AdminCmdListChannels  AdminCmd = 3
	AdminCmdRefresh       AdminCmd = 4
)

func DecodeAdminQuery

func DecodeAdminQuery(queryKey [KeySize]byte, qname, domain string) (cmd AdminCmd, arg []byte, err error)

DecodeAdminQuery decodes an admin command DNS query and returns the command and argument.

type ChannelInfo

type ChannelInfo struct {
	Name        string
	DisplayName string // human-readable title; empty means fall back to Name
	Blocks      uint16
	LastMsgID   uint32
	ContentHash uint32   // CRC32 of serialized message data; changes on edits
	ChatType    ChatType // 0=Telegram channel, 1=private chat, 2=X account
	CanSend     bool     // true if server allows sending messages to this chat
}

ChannelInfo describes a single feed channel.

type ChatAck added in v0.25.9

type ChatAck struct {
	Peer    [ChatPeerHandleSize]byte
	UpToSeq uint32
	Receipt [ChatReceiptMACSize]byte // E2E delivery proof; zero if none
}

ChatAck is a parsed ACK.

func ParseChatAckPlain added in v0.25.9

func ParseChatAckPlain(pt []byte) (*ChatAck, error)

ParseChatAckPlain parses an ACK plaintext. The receipt is optional (a zero-value Receipt is returned if absent) so the op stays parseable even without proof.

type ChatAuthBootstrap added in v0.25.9

type ChatAuthBootstrap struct {
	Addr  [AddressSize]byte
	TS    uint32
	Proof [ChatAccountProofSize]byte
}

ChatAuthBootstrap is a parsed auth-handshake bootstrap.

func ParseChatAuthBootstrapPlain added in v0.25.9

func ParseChatAuthBootstrapPlain(pt []byte) (*ChatAuthBootstrap, error)

ParseChatAuthBootstrapPlain parses an auth-handshake bootstrap plaintext.

type ChatData added in v0.25.9

type ChatData struct {
	Index uint8
	Chunk []byte
}

ChatData is a parsed DATA chunk. Chunk is the fixed-size slice; the server trims it to the real length using the upload's known total length.

func ParseChatDataPlain added in v0.25.9

func ParseChatDataPlain(pt []byte) (*ChatData, error)

ParseChatDataPlain parses a DATA plaintext.

type ChatFetch added in v0.25.9

type ChatFetch struct {
	Peer  [ChatPeerHandleSize]byte
	Seq   uint32
	Block uint8
}

ChatFetch is a parsed INBOX_FETCH.

func ParseChatFetchPlain added in v0.25.9

func ParseChatFetchPlain(pt []byte) (*ChatFetch, error)

ParseChatFetchPlain parses an INBOX_FETCH plaintext.

type ChatFin added in v0.25.9

type ChatFin struct {
	CRC32 uint32
}

ChatFin is a parsed FIN.

func ParseChatFinPlain added in v0.25.9

func ParseChatFinPlain(pt []byte) (*ChatFin, error)

ParseChatFinPlain parses a FIN plaintext.

type ChatFrag added in v0.25.9

type ChatFrag struct {
	Index uint8
	Total uint8
	Chunk []byte
}

ChatFrag is a parsed OP_FRAG.

func ParseChatFragPlain added in v0.25.9

func ParseChatFragPlain(pt []byte) (*ChatFrag, error)

ParseChatFragPlain parses an OP_FRAG plaintext (op already padded to budget, so trailing pad is harmless — the reassembler trims to the inner op length).

type ChatInfo added in v0.25.9

type ChatInfo struct {
	MinVersion uint8
	MaxVersion uint8
	Enabled    bool
	Domains    []string
	EkPub      []byte
	Limits     ChatLimits
}

ChatInfo is the chat capability payload served (signed) on the feed metadata path. EkPub is the server x25519 key clients run the session handshake against — delivered here, under the feed signing key, instead of in the import URI. Enabled is false when the operator has chat domains configured but has turned chat off.

func ParseChatInfo added in v0.25.9

func ParseChatInfo(data []byte) (*ChatInfo, error)

ParseChatInfo decodes a ChatInfo payload.

type ChatKeyFetch added in v0.25.9

type ChatKeyFetch struct {
	Addr [AddressSize]byte
}

ChatKeyFetch is a parsed KEYFETCH.

func ParseChatKeyFetchPlain added in v0.25.9

func ParseChatKeyFetchPlain(pt []byte) (*ChatKeyFetch, error)

ParseChatKeyFetchPlain parses a KEYFETCH plaintext.

type ChatLimits added in v0.25.9

type ChatLimits struct {
	ChunkSize      uint8
	MaxMsgBytes    uint16
	InboxCap       uint16
	PerPairCap     uint16
	SendPerHour    uint16
	SessionIdleSec uint16
	SessionHardSec uint16
	TTLHours       uint16
}

ChatLimits are the server-advertised chat limits.

func DefaultChatLimits added in v0.25.9

func DefaultChatLimits() ChatLimits

DefaultChatLimits returns the default server limits.

type ChatMessage added in v0.25.9

type ChatMessage struct {
	Seq        uint32
	Nonce      []byte
	Ciphertext []byte
	SrvMAC     [ChatSrvMACSize]byte
}

ChatMessage is a parsed message envelope.

Wire layout:

ver(1) msg_seq(4 BE) nonce(12) ciphertext(N) srvmac(8)

ciphertext seals the inner payload with the pair content key (AES-256-GCM) under a fresh RANDOM nonce carried in the envelope. The random nonce is what guarantees no keystream reuse even if the same (src,dst,seq) recurs — e.g. the same recipient on two servers (seq is per-server) — so confidentiality does not hinge on seq uniqueness. Inner layout:

cflag(1) body     // body = deflate(text) or raw text; cflag says which

The sender address is NOT carried: the recipient derives the content key from the inbox entry's src, so a wrong src yields a wrong key and AEAD fails — misattribution is impossible without an inner copy.

func ParseChatMessage added in v0.25.9

func ParseChatMessage(data []byte) (*ChatMessage, error)

ParseChatMessage parses a message envelope. It never panics; any malformation returns an error.

func (*ChatMessage) Open added in v0.25.9

func (m *ChatMessage) Open(contentKey [KeySize]byte) (string, error)

Open decrypts and decompresses the body with the pair content key. AEAD success itself authenticates the sender (only the pair can compute contentKey, which is bound to src,dst,seq), so no inner address check is needed.

func (*ChatMessage) VerifyServerMAC added in v0.25.9

func (m *ChatMessage) VerifyServerMAC(kss [KeySize]byte, src, dst [AddressSize]byte) error

VerifyServerMAC checks the sender-to-server MAC. The server calls this at FIN with the session's routing pair and the sender's registered enc key.

type ChatSendStart added in v0.25.9

type ChatSendStart struct {
	Dst      [AddressSize]byte
	TotalLen uint16
}

ChatSendStart is a parsed SEND_START.

func ParseChatSendStartPlain added in v0.25.9

func ParseChatSendStartPlain(pt []byte) (*ChatSendStart, error)

ParseChatSendStartPlain parses a SEND_START plaintext.

type ChatSendStatus added in v0.25.9

type ChatSendStatus struct {
	Peer [AddressSize]byte
}

ChatSendStatus is a parsed SENDSTATUS.

func ParseChatSendStatusPlain added in v0.25.9

func ParseChatSendStatusPlain(pt []byte) (*ChatSendStatus, error)

ParseChatSendStatusPlain parses a SENDSTATUS plaintext.

type ChatType

type ChatType uint8

ChatType distinguishes channel types in metadata.

const (
	ChatTypeChannel ChatType = 0 // public Telegram channel
	ChatTypePrivate ChatType = 1 // private chat / bot
	ChatTypeX       ChatType = 2 // public X (Twitter) account
)

type ChunkReassembler added in v0.25.9

type ChunkReassembler struct {
	// contains filtered or unexported fields
}

ChunkReassembler reassembles a fixed number of chunks arriving out of order, tracking which have been received. The bitmap is returned to the client so it can retransmit gaps.

func NewChunkReassembler added in v0.25.9

func NewChunkReassembler(total int) *ChunkReassembler

NewChunkReassembler creates a reassembler expecting total chunks.

func (*ChunkReassembler) Add added in v0.25.9

func (r *ChunkReassembler) Add(index int, chunk []byte) bool

Add stores a chunk at index; returns false if index is out of range. Re-adding an index is idempotent.

func (*ChunkReassembler) Assemble added in v0.25.9

func (r *ChunkReassembler) Assemble() []byte

Assemble concatenates the received chunks. Meaningful only when Complete.

func (*ChunkReassembler) Bitmap added in v0.25.9

func (r *ChunkReassembler) Bitmap() []byte

Bitmap returns the received set as a big-endian bit field: chunk i is bit (7 - i%8) of byte i/8.

func (*ChunkReassembler) Complete added in v0.25.9

func (r *ChunkReassembler) Complete() bool

Complete reports whether every chunk has been received.

type ExtraBlock added in v0.25.9

type ExtraBlock struct {
	Version   uint8
	Count     uint8
	Index     uint8
	Timestamp int64
	Digest    []byte
	Signature []byte
}

ExtraBlock is a parsed ExtraBlock.

func ParseExtraBlock added in v0.25.9

func ParseExtraBlock(data []byte) (*ExtraBlock, error)

ParseExtraBlock parses a decrypted ExtraBlock plaintext. It returns an error (never panics) on any malformation, so a caller can treat a spoofed or stale block — or an old server's unrelated response — as "no valid extra block".

func (*ExtraBlock) VerifyChannelContent added in v0.25.9

func (eb *ExtraBlock) VerifyChannelContent(canonical []byte) error

VerifyChannelContent confirms canonical content matches the signed digest. Call only after VerifyExtraBlock has succeeded.

type MediaBlockHeader added in v0.13.0

type MediaBlockHeader struct {
	CRC32       uint32
	Version     uint8
	Compression MediaCompression
}

MediaBlockHeader is the parsed form of a media-channel block-0 header.

func DecodeMediaBlockHeader added in v0.13.0

func DecodeMediaBlockHeader(b []byte) (MediaBlockHeader, error)

DecodeMediaBlockHeader parses the first MediaBlockHeaderLen bytes of a media block. Errors on truncation or unknown header version.

type MediaCompression added in v0.13.0

type MediaCompression byte

MediaCompression names a compression method applied to a cached media file's bytes before they're split into DNS blocks.

const (
	MediaCompressionNone    MediaCompression = 0
	MediaCompressionGzip    MediaCompression = 1
	MediaCompressionDeflate MediaCompression = 2
)

func ParseMediaCompressionName added in v0.13.0

func ParseMediaCompressionName(s string) (MediaCompression, error)

ParseMediaCompressionName returns the MediaCompression matching one of "none", "gzip", "deflate" (case-insensitive). Used by the CLI flag to translate user input.

func (MediaCompression) String added in v0.13.0

func (c MediaCompression) String() string

String returns the canonical name of the compression value.

type MediaMeta added in v0.13.0

type MediaMeta struct {
	Tag      string // e.g. MediaImage, MediaVideo, MediaFile
	Size     int64
	Relays   []bool // index = relay constant, value = availability
	Channel  uint16 // DNS channel (when Relays[RelayDNS])
	Blocks   uint16 // DNS block count (when Relays[RelayDNS])
	CRC32    uint32
	Filename string
}

MediaMeta describes a downloadable media blob attached to a feed message.

Wire format (immediately after the media tag, before any caption):

[IMAGE]<size>:<f0>,<f1>,...:<dnsCh>:<dnsBlk>:<crc32hex>[:<filename>]

where each <fN> is "1" or "0" indicating availability via relay N. <dnsCh>:<dnsBlk> are only meaningful when f0 (RelayDNS) is set.

func ParseMediaText added in v0.13.0

func ParseMediaText(body string) (meta MediaMeta, caption string, ok bool)

ParseMediaText parses a message body that begins with a known media tag. Returns metadata + remaining caption. Legacy "[TAG]\ncaption" bodies parse with empty Relays (HasAnyRelay()==false). Unknown tags return ok=false.

func (MediaMeta) HasAnyRelay added in v0.13.0

func (m MediaMeta) HasAnyRelay() bool

HasAnyRelay reports whether at least one relay can serve this file.

func (MediaMeta) HasRelay added in v0.13.0

func (m MediaMeta) HasRelay(idx int) bool

HasRelay reports whether the relay at idx is available. Out-of-range and nil-relay-list both return false.

func (MediaMeta) String added in v0.13.0

func (m MediaMeta) String() string

String renders the metadata in the wire format documented above.

type Message

type Message struct {
	ID        uint32
	Timestamp uint32
	Text      string
}

Message represents a single feed message in a channel.

func ParseMessages

func ParseMessages(data []byte) ([]Message, error)

ParseMessages decodes messages from concatenated data channel block data.

type Metadata

type Metadata struct {
	// Marker is the 3 bytes at the start of the serialized metadata
	// payload. Legacy encoder fills it with a random per-server marker;
	// the extended encoder fills it with EMH magic + flags. Treat it as
	// opaque on the wire and use PeekExtendedHeader to interpret.
	Marker [MarkerSize]byte
	// Timestamp is the 4 bytes immediately after Marker. Legacy encoder
	// fills it with a unix timestamp; the extended encoder fills it with
	// EMH block_count + content_hash. Treat it as opaque and use
	// PeekExtendedHeader to interpret.
	Timestamp        uint32
	NextFetch        uint32 // unix timestamp of next server-side fetch (0 = unknown)
	TelegramLoggedIn bool   // true if server has an active Telegram session
	// ChatAvailable advertises that this server has the messenger configured
	// (flags bit 0x02). Lets clients skip the ChatInfo probe on chatless
	// servers and tell apart "no chat" from "chat, but you lack the key".
	ChatAvailable bool
	Channels      []ChannelInfo
}

Metadata holds channel 0 data: server info + channel list.

func ParseMetadata

func ParseMetadata(data []byte) (*Metadata, error)

ParseMetadata decodes metadata from concatenated channel 0 block data.

New servers may embed an extended header in the Marker + Timestamp fields of the same wire format — see PeekExtendedHeader for the magic check. This parser ignores those fields by design; clients that care about the embedded block_count / hash check them explicitly.

type ProfilePicEntry added in v0.16.0

type ProfilePicEntry struct {
	Username   string
	Offset     uint32
	Size       uint32
	CRC        uint32
	MIME       uint8
	DNSChannel uint16
	DNSBlocks  uint16
}

ProfilePicEntry points at one avatar via either the GitHub bundle (Offset/Size into the concatenated blob) or its own DNS channel (DNSChannel/DNSBlocks). Both paths verify the same Size + CRC.

type ProfilePicsBundle added in v0.16.0

type ProfilePicsBundle struct {
	Header  ProfilePicsBundleHeader
	Entries []ProfilePicEntry
}

ProfilePicsBundle is the directory (header + entries). The bundle bytes themselves live in the referenced media channel / relay.

func DecodeProfilePicsBundle added in v0.16.0

func DecodeProfilePicsBundle(data []byte) (ProfilePicsBundle, error)

DecodeProfilePicsBundle parses bytes produced by EncodeProfilePicsBundle.

type ProfilePicsBundleHeader added in v0.16.0

type ProfilePicsBundleHeader struct {
	BundleSize uint32
	BundleCRC  uint32
	// One bool per relay constant. RelayGitHub here means the bundle
	// is on GitHub; RelayDNS for the bundle is rare (per-entry DNS
	// channels handle the DNS path).
	Relays []bool
}

Profile pictures use a hybrid layout: every avatar is concatenated into one bundle uploaded to the GitHub relay (one file → no per-file rate limit), and each avatar also gets its own DNS media channel so partial fetches over DNS still display.

Wire layout of ProfilePicsChannel (after the block-count prefix the Feed layer adds):

bundleSize    uint32
bundleCRC     uint32
relayCount    uint8       — N
relays        [N]u8       — bool per relay (RelayDNS=0, RelayGitHub=1, …)
count         uint16
entries:
    usernameLen uint8
    username    [usernameLen]byte
    offset      uint32     — within the GitHub bundle
    size        uint32
    crc         uint32     — CRC32 of bundle[offset:offset+size]
    mime        uint8      — 0=jpeg, 1=png, 2=webp
    dnsChannel  uint16     — 0 if not on DNS
    dnsBlocks   uint16

func (ProfilePicsBundleHeader) HasRelay added in v0.16.0

func (h ProfilePicsBundleHeader) HasRelay(idx int) bool

HasRelay reports whether the relay at idx is set. Out of range returns false.

type QueryEncoding

type QueryEncoding int

QueryEncoding controls how DNS query subdomains are encoded.

const (
	// QuerySingleLabel uses base32 in a single DNS label (default, stealthier).
	QuerySingleLabel QueryEncoding = iota
	// QueryMultiLabel uses hex split across multiple DNS labels.
	QueryMultiLabel
)

type RegisterEnvelope added in v0.25.9

type RegisterEnvelope struct {
	IdentityPub ed25519.PublicKey
	EncPub      []byte
	Timestamp   uint32
	Signature   []byte
	// contains filtered or unexported fields
}

RegisterEnvelope is a parsed key-registration record. Wire layout:

ver(1) identity_pub(32) enc_pub(32) timestamp(4 BE) sig(64)

func ParseRegisterEnvelope added in v0.25.9

func ParseRegisterEnvelope(data []byte) (*RegisterEnvelope, error)

ParseRegisterEnvelope parses a registration record. It never panics.

func (*RegisterEnvelope) Address added in v0.25.9

func (e *RegisterEnvelope) Address() [AddressSize]byte

Address returns the chat address bound to this registration.

func (*RegisterEnvelope) EncKey added in v0.25.9

func (e *RegisterEnvelope) EncKey() (*ecdh.PublicKey, error)

EncKey returns the registered x25519 encryption key, validated.

func (*RegisterEnvelope) Verify added in v0.25.9

func (e *RegisterEnvelope) Verify() error

Verify checks the identity signature. The caller must separately confirm Address(IdentityPub) equals the expected address.

type UpstreamInit

type UpstreamInit struct {
	SessionID     uint16
	TotalBlocks   uint8
	Kind          UpstreamKind
	TargetChannel uint8
}

UpstreamInit describes a chunked upstream session.

func DecodeUpstreamInitQuery

func DecodeUpstreamInitQuery(queryKey [KeySize]byte, qname, domain string) (*UpstreamInit, error)

DecodeUpstreamInitQuery parses a compact single-label upstream init query.

type UpstreamKind

type UpstreamKind byte

UpstreamKind identifies the payload carried by a chunked upstream session.

const (
	UpstreamKindSend  UpstreamKind = 1
	UpstreamKindAdmin UpstreamKind = 2
)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL