telegabber/e2ee/types.go
2026-07-30 00:09:22 -04:00

192 lines
9.9 KiB
Go

// Package e2ee is a backend-agnostic end-to-end encryption abstraction for
// telegabber's XMPP side. It knows nothing about stanza XML shape - a
// Backend deals only in opaque byte envelopes; wrapping/unwrapping those
// into <encrypted>/PEP XML is the caller's job (see e2ee/omemo for the
// first, and so far only, implementation, and its envelope.go for the
// XEP-0384 wire format specifically).
package e2ee
// PeerID identifies one bridging endpoint's cryptographic identity scope.
// For telegabber this is always a bare XMPP JID: either a bridged chat's
// pseudo-JID (whose keys the gateway owns and generates) or a real remote
// user's bare JID (whose keys the gateway only ever fetches).
type PeerID string
// DeviceID is an opaque per-backend device identifier, serialized as its
// natural string form (OMEMO: decimal uint32; a hypothetical single-key
// backend could always use "0" or the empty string).
type DeviceID string
// Envelope is an opaque, backend-defined encrypted payload, plus enough
// metadata for the xmpp layer to build the wire XML generically.
type Envelope struct {
Backend string // e.g. "omemo" - which backend produced Raw, so the xmpp layer picks the right XML shape
Raw []byte // backend-specific serialized envelope
}
// Namespaces is what a Backend wants advertised/served.
type Namespaces struct {
Disco []string // disco#info features to advertise, across every supported Variant (e.g. "...+notify" hints)
}
// Variant identifies one of a Backend's parallel wire-format generations
// (e.g. OMEMO's omemo0/omemo1/omemo2 - three simultaneously-supported
// namespaces sharing one identity/prekey set). It's an opaque string a
// Backend defines and interprets itself; other packages only ever pass a
// Variant back between Variants()/DefaultVariant()/NegotiatedVariant() and
// DeviceListNode()/BundleNode()/PublishedIdentity()/PublishedBundle() -
// never construct or compare one directly. A backend with only one
// generation can just always return/accept a single fixed value.
type Variant string
// DeviceListDoc is an opaque, backend-defined serialization of a peer's
// PEP device-list document (fetched from them, or published for a chat
// pseudo-JID we own).
type DeviceListDoc struct{ Raw []byte }
// BundleDoc is an opaque, backend-defined serialization of one device's
// PEP bundle document.
type BundleDoc struct{ Raw []byte }
// DeviceInfo describes one known device of a peer, for trust-listing
// ad-hoc commands. Trusted is false only if the device has been manually
// blocked (see Backend.BlockDevice) - TOFU itself always trusts a
// never-before-seen device automatically, so a session existing at all
// already implies TOFU accepted it; Trusted here reflects the manual
// override layered on top, not TOFU's own verdict.
type DeviceInfo struct {
ID DeviceID
Trusted bool
Fingerprint string
}
// Backend is the minimal contract a pluggable E2EE scheme must implement.
type Backend interface {
// Name identifies the backend for config/logging (e.g. "omemo").
Name() string
// Namespaces lists what this backend wants advertised/served.
Namespaces() Namespaces
// Variants lists every wire-format variant this backend can
// simultaneously serve/negotiate right now (e.g. all three OMEMO
// namespaces, or fewer if this build lacks support for some of them -
// see libsignal.ProtocolV4Supported). All variants are always
// servable/fetchable regardless of any given chat's negotiated state
// (see NegotiatedVariant) - "supported" is a deploy-time/build-time
// property, not a per-chat one.
Variants() []Variant
// DefaultVariant is what a brand new chat (no negotiation evidence
// yet) starts out using for outgoing content - e.g. OMEMO's omemo0,
// the most widely deployed variant.
DefaultVariant() Variant
// NegotiatedVariant is the wire-format variant currently in use for
// owner's (an OwnedPeer) outgoing content: DefaultVariant until an
// inbound message from owner's peer is decrypted, at which point it
// tracks whatever variant that peer's own client is actually using
// (see Decrypt). Implementations may only be able to fix this at
// session establishment time, not change it later for an
// already-established chat - see e2ee/omemo.Backend.NegotiatedVariant's
// doc comment for a concrete example of why.
NegotiatedVariant(owner PeerID) (Variant, error)
// DeviceListNode and BundleNode name the PEP node one of this backend's
// own device-list/bundle documents is published/fetched under, for the
// given variant. Note BundleNode's return value may or may not depend
// on device - some backends address every device's bundle under one
// shared node (item id = device), others use a distinct node per
// device; callers (e2ee/fetch.go, and the xmpp package's PEP-serving IQ
// handlers) must not assume either shape.
DeviceListNode(variant Variant) string
BundleNode(variant Variant, device DeviceID) string
// EnsureIdentity creates (idempotently) a local cryptographic identity
// for a PeerID this gateway owns (a bridged chat pseudo-JID - built via
// OwnedPeer, since a chat pseudo-JID alone isn't globally unique across
// telegabber's multiple bridged Telegram logins), generating and
// persisting whatever key material the backend needs. One identity
// serves every Variant simultaneously. Every other method that touches
// identity/prekey material calls this internally too (it's cheap and
// idempotent), so callers never strictly need to call it themselves -
// it's exposed mainly for a future eager-bootstrap use case (e.g.
// pre-creating identities for every personal chat when an account's
// OMEMO toggle is turned on, instead of lazily on first use).
EnsureIdentity(peer PeerID) error
// OwnDevice is this backend's own (and only) published device id for
// any owned peer - telegabber only ever needs one device per bridged
// chat identity (see EnsureIdentity's doc comment), so unlike Devices
// (which lists a remote peer's possibly-many devices) this takes no
// arguments and returns no error.
OwnDevice() DeviceID
// PublishedIdentity returns the material this gateway must SERVE to
// pubsub GET requests for its own PeerID (an OwnedPeer - see
// EnsureIdentity) device list under the given variant, pre-marshaled to
// opaque bytes the xmpp layer wraps in <items>/<item>.
PublishedIdentity(peer PeerID, variant Variant) (DeviceListDoc, error)
// PublishedBundle is the same, for one specific device's bundle node.
PublishedBundle(peer PeerID, device DeviceID, variant Variant) (BundleDoc, error)
// IngestRemoteDeviceList / IngestRemoteBundle feed the results of an
// outbound PEP fetch (device-list, then per-device bundle) for a real
// remote peer (owner is an OwnedPeer identifying which of this
// gateway's identities is doing the fetching; peer is the remote's
// plain bare JID, no scoping needed) into the backend's store,
// establishing/refreshing sessions as needed (trust decisions happen
// inside here). IngestRemoteDeviceList returns the device ids it found,
// so a caller like e2ee/fetch.go knows which bundles to fetch next
// without needing to parse the backend-specific document itself.
IngestRemoteDeviceList(owner PeerID, peer PeerID, doc DeviceListDoc) ([]DeviceID, error)
IngestRemoteBundle(owner PeerID, peer PeerID, device DeviceID, doc BundleDoc) error
// Encrypt produces an opaque envelope addressed to every known device
// of every given recipient, ready to be embedded in a stanza by the
// xmpp layer. from is the identity doing the encrypting (an OwnedPeer -
// see EnsureIdentity); to are the real remote peers' plain bare JIDs.
Encrypt(from PeerID, to []PeerID, plaintext []byte) (Envelope, error)
// Decrypt consumes an inbound envelope addressed to "to" (an
// OwnedPeer - see EnsureIdentity) from "from" (the real remote peer's
// plain bare JID) and returns plaintext. A successful decrypt also
// updates "to"'s NegotiatedVariant to whichever variant this envelope
// actually used - see NegotiatedVariant's doc comment.
Decrypt(from PeerID, to PeerID, env Envelope) ([]byte, error)
// Devices lists peer's known devices, as seen from owner's (an
// OwnedPeer) point of view - for trust-listing ad-hoc commands.
Devices(owner PeerID, peer PeerID) ([]DeviceInfo, error)
// Enabled and SetEnabled track the per-chat "OMEMO is active" sticky
// flag (the auto-upgrade trigger: set the first time an inbound
// message decrypts successfully for owner, read by the outbound
// encrypt hook alongside the account-wide config toggle). owner is an
// OwnedPeer; a chat that was never marked active reports false, not an
// error.
Enabled(owner PeerID) (bool, error)
SetEnabled(owner PeerID, enabled bool) error
// Override, SetOverride, and ClearOverride manage owner's (an
// OwnedPeer) manual on/off override of the account-wide Mode (see
// ShouldEncrypt) - a per-chat "/omemo on"/"/omemo off" that wins
// regardless of the global setting, until cleared. isSet is false when
// no override has been set (follow the account-wide mode instead).
Override(owner PeerID) (on bool, isSet bool, err error)
SetOverride(owner PeerID, on bool) error
ClearOverride(owner PeerID) error
// BlockDevice and UnblockDevice manually override one of peer's
// devices' TOFU trust, as seen from owner's (an OwnedPeer) point of
// view - TOFU itself always stays on (a device's first-seen key is
// always trusted automatically), this only ever narrows it further. A
// blocked device's messages are rejected by Decrypt and skipped by
// Encrypt, regardless of any session already established with it - see
// Devices' Trusted field for the resulting per-device state.
BlockDevice(owner PeerID, peer PeerID, device DeviceID) error
UnblockDevice(owner PeerID, peer PeerID, device DeviceID) error
// Close flushes and releases whatever storage/resources this backend
// holds (e.g. the underlying Badger DB), for a clean process shutdown -
// mirrors gateway.IdsDB.Close()'s role for the id-mapping store.
Close() error
}