From 5e71ba954c84e2fc64ea339c21986893e7d63b53 Mon Sep 17 00:00:00 2001 From: Bohdan Horbeshko Date: Tue, 28 Jul 2026 19:10:01 -0400 Subject: [PATCH] Abstract E2EE manager/registry --- e2ee/manager.go | 35 +++++ e2ee/omemo/envelope.go | 288 ++++++++++++++++++++++++++++++++++++ e2ee/omemo/envelope_test.go | 119 +++++++++++++++ e2ee/omemo/sce.go | 76 ++++++++++ e2ee/registry.go | 20 +++ e2ee/types.go | 90 +++++++++++ go.mod | 1 + go.sum | 15 +- 8 files changed, 630 insertions(+), 14 deletions(-) create mode 100644 e2ee/manager.go create mode 100644 e2ee/omemo/envelope.go create mode 100644 e2ee/omemo/envelope_test.go create mode 100644 e2ee/omemo/sce.go create mode 100644 e2ee/registry.go create mode 100644 e2ee/types.go diff --git a/e2ee/manager.go b/e2ee/manager.go new file mode 100644 index 0000000..2d8bf5b --- /dev/null +++ b/e2ee/manager.go @@ -0,0 +1,35 @@ +package e2ee + +import "fmt" + +// Manager holds the process-wide selected E2EE backend (or none, if E2EE +// is disabled), reachable from both the xmpp and telegram packages without +// either importing the other's e2ee-specific glue. +type Manager struct { + backend Backend +} + +// NewManager selects a backend by name from the registry. An empty name +// means E2EE is disabled: the returned Manager's Backend() always reports +// ok=false, and every call site is expected to treat that as "behave +// exactly as if this feature didn't exist". +func NewManager(name string) (*Manager, error) { + if name == "" { + return &Manager{}, nil + } + b, ok := Get(name) + if !ok { + return nil, fmt.Errorf("e2ee: unknown backend %q (forgot to import its package for its init() side effect?)", name) + } + return &Manager{backend: b}, nil +} + +// Backend returns the selected backend. ok is false if m is nil or no +// backend was selected (E2EE disabled) - callers should treat a nil +// Manager the same as one built via NewManager(""). +func (m *Manager) Backend() (backend Backend, ok bool) { + if m == nil || m.backend == nil { + return nil, false + } + return m.backend, true +} diff --git a/e2ee/omemo/envelope.go b/e2ee/omemo/envelope.go new file mode 100644 index 0000000..fefe233 --- /dev/null +++ b/e2ee/omemo/envelope.go @@ -0,0 +1,288 @@ +// Package omemo is the OMEMO (XEP-0384) implementation of e2ee.Backend. +package omemo + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/hmac" + "crypto/rand" + "crypto/sha256" + "encoding/gob" + "errors" + "fmt" + "io" + + "golang.org/x/crypto/hkdf" + + "dev.narayana.im/narayana/telegabber/e2ee/omemo/libsignal" +) + +// This file is OMEMO's outer, non-Signal content encryption: the layer +// that encrypts the actual message body once per message, independent of +// how many recipient devices it goes to. The resulting small "content key" +// blob (not the body itself) is what gets Signal/Double-Ratchet-encrypted +// once per recipient device (see libsignal.SessionCipher.Encrypt) - that +// per-device wrapping, and the device/session bookkeeping around it, is +// omemo.go's job, built on top of EncryptOuter/DecryptOuter here. +// +// Two schemes, matching the two wire versions (confirmed against the +// XEP-0384 spec text directly, not memory - see the M2 kickoff discussion): +// +// - Legacy (ProtocolVersionLegacy, eu.siacs.conversations.axolotl): +// AES-128-GCM directly on the plaintext body. The GCM tag is stripped +// from the ciphertext and instead appended to the key (key||tag, 32 +// bytes) before that blob is Signal-encrypted per recipient; +// carries ciphertext only, carries the GCM nonce. +// +// - Modern (ProtocolVersionModern, OMEMO 1/2): the plaintext is first +// wrapped in a minimal XEP-0420 SCE envelope (sce.go). That envelope is +// encrypted with AES-256-CBC using a key/authKey/iv derived via +// HKDF-SHA256(random 32-byte key, salt=32 zero bytes, info="OMEMO +// Payload") -> 80 bytes, split 32/32/16. The ciphertext is +// HMAC-SHA256'd with authKey and truncated to 16 bytes. key||tag (48 +// bytes) is the per-recipient blob; carries ciphertext only; +// there is no element - the receiver re-derives it from the key +// via the same HKDF. + +const ( + legacyKeyLen = 16 // AES-128 + legacyIVLen = 12 // GCM nonce, standard size (crypto/cipher.NewGCM's default) + legacyTagLen = 16 + + modernKeyLen = 32 // AES-256 + modernAuthKeyLen = 32 + modernIVLen = 16 // AES block size + modernTagLen = 16 + modernHKDFInfo = "OMEMO Payload" +) + +// EncryptedPayload is the result of OMEMO's outer content-encryption step. +type EncryptedPayload struct { + Ciphertext []byte + IV []byte // legacy only; nil for modern + ContentKey []byte // the blob to Signal-encrypt once per recipient device +} + +// EncryptOuter performs OMEMO's outer content encryption for plaintext, +// per version (ProtocolVersionLegacy or ProtocolVersionModern). fromJID is +// used as the SCE binding for modern; ignored for legacy. +func EncryptOuter(version int, plaintext []byte, fromJID string) (*EncryptedPayload, error) { + switch version { + case libsignal.ProtocolVersionLegacy: + ciphertext, iv, contentKey, err := encryptPayloadLegacy(plaintext) + if err != nil { + return nil, err + } + return &EncryptedPayload{Ciphertext: ciphertext, IV: iv, ContentKey: contentKey}, nil + case libsignal.ProtocolVersionModern: + sceXML, err := sceEncode(plaintext, fromJID) + if err != nil { + return nil, err + } + ciphertext, contentKey, err := encryptPayloadModern(sceXML) + if err != nil { + return nil, err + } + return &EncryptedPayload{Ciphertext: ciphertext, ContentKey: contentKey}, nil + default: + return nil, fmt.Errorf("omemo: unsupported protocol version %d", version) + } +} + +// DecryptOuter reverses EncryptOuter. +func DecryptOuter(version int, payload *EncryptedPayload) ([]byte, error) { + switch version { + case libsignal.ProtocolVersionLegacy: + return decryptPayloadLegacy(payload.Ciphertext, payload.IV, payload.ContentKey) + case libsignal.ProtocolVersionModern: + sceXML, err := decryptPayloadModern(payload.Ciphertext, payload.ContentKey) + if err != nil { + return nil, err + } + return sceDecode(sceXML) + default: + return nil, fmt.Errorf("omemo: unsupported protocol version %d", version) + } +} + +func encryptPayloadLegacy(plaintext []byte) (ciphertext, iv, contentKey []byte, err error) { + key := make([]byte, legacyKeyLen) + if _, err := rand.Read(key); err != nil { + return nil, nil, nil, err + } + iv = make([]byte, legacyIVLen) + if _, err := rand.Read(iv); err != nil { + return nil, nil, nil, err + } + block, err := aes.NewCipher(key) + if err != nil { + return nil, nil, nil, err + } + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, nil, nil, err + } + sealed := gcm.Seal(nil, iv, plaintext, nil) + if len(sealed) < legacyTagLen { + return nil, nil, nil, errors.New("omemo: GCM output shorter than its own tag") + } + ciphertext = sealed[:len(sealed)-legacyTagLen] + tag := sealed[len(sealed)-legacyTagLen:] + contentKey = append(append([]byte{}, key...), tag...) + return ciphertext, iv, contentKey, nil +} + +func decryptPayloadLegacy(ciphertext, iv, contentKey []byte) ([]byte, error) { + if len(contentKey) != legacyKeyLen+legacyTagLen { + return nil, fmt.Errorf("omemo: legacy content key must be %d bytes, got %d", legacyKeyLen+legacyTagLen, len(contentKey)) + } + key := contentKey[:legacyKeyLen] + tag := contentKey[legacyKeyLen:] + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + sealed := append(append([]byte{}, ciphertext...), tag...) + return gcm.Open(nil, iv, sealed, nil) +} + +func hkdfSplit(key []byte) (encKey, authKey, iv []byte, err error) { + h := hkdf.New(sha256.New, key, make([]byte, sha256.Size), []byte(modernHKDFInfo)) + out := make([]byte, modernKeyLen+modernAuthKeyLen+modernIVLen) + if _, err := io.ReadFull(h, out); err != nil { + return nil, nil, nil, err + } + return out[:modernKeyLen], out[modernKeyLen : modernKeyLen+modernAuthKeyLen], out[modernKeyLen+modernAuthKeyLen:], nil +} + +func encryptPayloadModern(plaintext []byte) (ciphertext, contentKey []byte, err error) { + key := make([]byte, modernKeyLen) + if _, err := rand.Read(key); err != nil { + return nil, nil, err + } + encKey, authKey, iv, err := hkdfSplit(key) + if err != nil { + return nil, nil, err + } + block, err := aes.NewCipher(encKey) + if err != nil { + return nil, nil, err + } + padded := pkcs7Pad(plaintext, aes.BlockSize) + ciphertext = make([]byte, len(padded)) + cipher.NewCBCEncrypter(block, iv).CryptBlocks(ciphertext, padded) + + mac := hmac.New(sha256.New, authKey) + mac.Write(ciphertext) + tag := mac.Sum(nil)[:modernTagLen] + + contentKey = append(append([]byte{}, key...), tag...) + return ciphertext, contentKey, nil +} + +func decryptPayloadModern(ciphertext, contentKey []byte) ([]byte, error) { + if len(contentKey) != modernKeyLen+modernTagLen { + return nil, fmt.Errorf("omemo: modern content key must be %d bytes, got %d", modernKeyLen+modernTagLen, len(contentKey)) + } + key := contentKey[:modernKeyLen] + tag := contentKey[modernKeyLen:] + + encKey, authKey, iv, err := hkdfSplit(key) + if err != nil { + return nil, err + } + + mac := hmac.New(sha256.New, authKey) + mac.Write(ciphertext) + expectedTag := mac.Sum(nil)[:modernTagLen] + if !hmac.Equal(expectedTag, tag) { + return nil, errors.New("omemo: payload authentication failed") + } + + if len(ciphertext) == 0 || len(ciphertext)%aes.BlockSize != 0 { + return nil, errors.New("omemo: invalid ciphertext length") + } + block, err := aes.NewCipher(encKey) + if err != nil { + return nil, err + } + padded := make([]byte, len(ciphertext)) + cipher.NewCBCDecrypter(block, iv).CryptBlocks(padded, ciphertext) + return pkcs7Unpad(padded) +} + +func pkcs7Pad(data []byte, blockSize int) []byte { + padLen := blockSize - (len(data) % blockSize) + padded := make([]byte, len(data)+padLen) + copy(padded, data) + for i := len(data); i < len(padded); i++ { + padded[i] = byte(padLen) + } + return padded +} + +func pkcs7Unpad(data []byte) ([]byte, error) { + n := len(data) + if n == 0 { + return nil, errors.New("omemo: cannot unpad empty data") + } + padLen := int(data[n-1]) + if padLen == 0 || padLen > n || padLen > aes.BlockSize { + return nil, errors.New("omemo: invalid PKCS7 padding") + } + for _, b := range data[n-padLen:] { + if int(b) != padLen { + return nil, errors.New("omemo: invalid PKCS7 padding") + } + } + return data[:n-padLen], nil +} + +// WireKey is one recipient device's Signal-encrypted content-key blob, +// version-and-namespace-agnostic - the xmpp-layer glue turns this into a +// (legacy) or +// (modern) element; both attributes mean the same thing (IsPreKey). +type WireKey struct { + RecipientJID string + DeviceID uint32 + IsPreKey bool + Ciphertext []byte // libsignal.CiphertextMessage.Serialized - already in the correct per-version wire format (see the M2 architecture note: libomemo-c itself produces OMEMOKeyExchange/OMEMOAuthenticatedMessage protobuf bytes for protocol v4, no extra wrapping needed here) +} + +// WireEnvelope is the decoded structure of an OMEMO element, +// independent of which XML shape (legacy vs OMEMO 2/SCE) it came from or +// will be marshaled to - that XML-specific glue lives in the xmpp package, +// which imports this package explicitly when the omemo backend is active. +type WireEnvelope struct { + Version int + SenderSID uint32 + IV []byte // legacy only + Payload []byte // outer ciphertext + Keys []WireKey +} + +// Encode serializes a WireEnvelope for e2ee.Envelope.Raw. This is +// Go-to-Go-only plumbing (produced by omemo.go's Encrypt, consumed by the +// xmpp-layer glue in the same process), not a wire format in its own +// right, so gob is a fine fit - no schema stability concerns. +func (e *WireEnvelope) Encode() ([]byte, error) { + var buf bytes.Buffer + if err := gob.NewEncoder(&buf).Encode(e); err != nil { + return nil, err + } + return buf.Bytes(), nil +} + +// DecodeWireEnvelope reverses WireEnvelope.Encode. +func DecodeWireEnvelope(raw []byte) (*WireEnvelope, error) { + var e WireEnvelope + if err := gob.NewDecoder(bytes.NewReader(raw)).Decode(&e); err != nil { + return nil, err + } + return &e, nil +} diff --git a/e2ee/omemo/envelope_test.go b/e2ee/omemo/envelope_test.go new file mode 100644 index 0000000..5f9c22c --- /dev/null +++ b/e2ee/omemo/envelope_test.go @@ -0,0 +1,119 @@ +package omemo + +import ( + "bytes" + "reflect" + "testing" + + "dev.narayana.im/narayana/telegabber/e2ee/omemo/libsignal" +) + +func TestEncryptOuterRoundTrip(t *testing.T) { + for _, tc := range []struct { + name string + version int + }{ + {"legacy", libsignal.ProtocolVersionLegacy}, + {"modern", libsignal.ProtocolVersionModern}, + } { + t.Run(tc.name, func(t *testing.T) { + plaintext := []byte("hello from telegram") + + payload, err := EncryptOuter(tc.version, plaintext, "gateway@example.com") + if err != nil { + t.Fatalf("EncryptOuter: %v", err) + } + if len(payload.Ciphertext) == 0 { + t.Fatalf("expected non-empty ciphertext") + } + if tc.version == libsignal.ProtocolVersionLegacy && len(payload.IV) != legacyIVLen { + t.Fatalf("expected a %d-byte IV for legacy, got %d", legacyIVLen, len(payload.IV)) + } + if tc.version == libsignal.ProtocolVersionModern && payload.IV != nil { + t.Fatalf("expected no IV for modern (receiver re-derives it), got %d bytes", len(payload.IV)) + } + + got, err := DecryptOuter(tc.version, payload) + if err != nil { + t.Fatalf("DecryptOuter: %v", err) + } + if !bytes.Equal(got, plaintext) { + t.Fatalf("round trip mismatch: got %q, want %q", got, plaintext) + } + }) + } +} + +func TestEncryptOuterTamperDetection(t *testing.T) { + for _, tc := range []struct { + name string + version int + }{ + {"legacy", libsignal.ProtocolVersionLegacy}, + {"modern", libsignal.ProtocolVersionModern}, + } { + t.Run(tc.name, func(t *testing.T) { + payload, err := EncryptOuter(tc.version, []byte("secret"), "gateway@example.com") + if err != nil { + t.Fatalf("EncryptOuter: %v", err) + } + + tampered := *payload + tampered.Ciphertext = append([]byte{}, payload.Ciphertext...) + tampered.Ciphertext[0] ^= 0xFF + + if _, err := DecryptOuter(tc.version, &tampered); err == nil { + t.Fatalf("expected DecryptOuter to reject tampered ciphertext") + } + }) + } +} + +func TestSCERoundTrip(t *testing.T) { + plaintext := []byte("hello sce") + xmlBytes, err := sceEncode(plaintext, "alice@example.com") + if err != nil { + t.Fatalf("sceEncode: %v", err) + } + got, err := sceDecode(xmlBytes) + if err != nil { + t.Fatalf("sceDecode: %v", err) + } + if !bytes.Equal(got, plaintext) { + t.Fatalf("got %q, want %q", got, plaintext) + } +} + +func TestWireEnvelopeEncodeDecode(t *testing.T) { + env := &WireEnvelope{ + Version: libsignal.ProtocolVersionModern, + SenderSID: 12345, + Payload: []byte("ciphertext-bytes"), + Keys: []WireKey{ + {RecipientJID: "bob@example.com", DeviceID: 1, IsPreKey: true, Ciphertext: []byte("key-ciphertext-1")}, + {RecipientJID: "bob@example.com", DeviceID: 2, IsPreKey: false, Ciphertext: []byte("key-ciphertext-2")}, + }, + } + + raw, err := env.Encode() + if err != nil { + t.Fatalf("Encode: %v", err) + } + + got, err := DecodeWireEnvelope(raw) + if err != nil { + t.Fatalf("DecodeWireEnvelope: %v", err) + } + + if got.Version != env.Version || got.SenderSID != env.SenderSID || !bytes.Equal(got.Payload, env.Payload) { + t.Fatalf("round trip mismatch: got %+v", got) + } + if len(got.Keys) != len(env.Keys) { + t.Fatalf("expected %d keys, got %d", len(env.Keys), len(got.Keys)) + } + for i := range env.Keys { + if !reflect.DeepEqual(got.Keys[i], env.Keys[i]) { + t.Fatalf("key %d mismatch: got %+v, want %+v", i, got.Keys[i], env.Keys[i]) + } + } +} diff --git a/e2ee/omemo/sce.go b/e2ee/omemo/sce.go new file mode 100644 index 0000000..0417ca8 --- /dev/null +++ b/e2ee/omemo/sce.go @@ -0,0 +1,76 @@ +package omemo + +import ( + "crypto/rand" + "encoding/base64" + "encoding/xml" + "math/big" +) + +// Minimal XEP-0420 Stanza Content Encryption envelope, used only as the +// plaintext OMEMO 2 (modern) content encryption operates on - just enough +// to carry a plain message body, per the spec's requirement that the +// envelope MUST contain and SHOULD contain (MUST contain +// for MUC, which this gateway doesn't do OMEMO for - see the plan's +// scope note on MUC). +type sceEnvelope struct { + XMLName xml.Name `xml:"urn:xmpp:sce:1 envelope"` + Content sceContent `xml:"content"` + RPad string `xml:"rpad"` + From *sceJIDAttr `xml:"from"` +} + +type sceContent struct { + Body sceBody `xml:"body"` +} + +type sceBody struct { + XMLName xml.Name `xml:"jabber:client body"` + Text string `xml:",chardata"` +} + +type sceJIDAttr struct { + JID string `xml:"jid,attr"` +} + +// sceEncode wraps plaintext in an SCE envelope and serializes it - the +// "plaintext" that modern OMEMO's outer content encryption actually +// operates on. fromJID becomes the binding (empty to omit it). +func sceEncode(plaintext []byte, fromJID string) ([]byte, error) { + rpad, err := randomPadding() + if err != nil { + return nil, err + } + env := sceEnvelope{ + Content: sceContent{Body: sceBody{Text: string(plaintext)}}, + RPad: rpad, + } + if fromJID != "" { + env.From = &sceJIDAttr{JID: fromJID} + } + return xml.Marshal(env) +} + +// sceDecode reverses sceEncode, extracting the plain body text. +func sceDecode(data []byte) ([]byte, error) { + var env sceEnvelope + if err := xml.Unmarshal(data, &env); err != nil { + return nil, err + } + return []byte(env.Content.Body.Text), nil +} + +// randomPadding returns base64-encoded random padding of a random length +// (1-63 bytes before encoding) to mask plaintext length, per XEP-0420's +// MUST-contain- requirement. +func randomPadding() (string, error) { + n, err := rand.Int(rand.Reader, big.NewInt(63)) + if err != nil { + return "", err + } + buf := make([]byte, n.Int64()+1) + if _, err := rand.Read(buf); err != nil { + return "", err + } + return base64.StdEncoding.EncodeToString(buf), nil +} diff --git a/e2ee/registry.go b/e2ee/registry.go new file mode 100644 index 0000000..a6bec24 --- /dev/null +++ b/e2ee/registry.go @@ -0,0 +1,20 @@ +package e2ee + +// backends is a process-wide registry of available Backend implementations, +// keyed by Backend.Name(). Backends self-register from an init() function +// in their own package (mirroring the self-registration idiom already used +// by xmpp/extensions for stanza payload types), so importing a backend +// package for its side effect is what makes it available here. +var backends = map[string]Backend{} + +// Register adds b to the registry, keyed by b.Name(). Call from the +// backend package's own init(). +func Register(b Backend) { + backends[b.Name()] = b +} + +// Get looks up a registered backend by name. +func Get(name string) (Backend, bool) { + b, ok := backends[name] + return b, ok +} diff --git a/e2ee/types.go b/e2ee/types.go new file mode 100644 index 0000000..fa28961 --- /dev/null +++ b/e2ee/types.go @@ -0,0 +1,90 @@ +// 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 /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 for a given peer. +type Namespaces struct { + Disco []string // disco#info features to advertise (e.g. "...+notify" hints) + EME string // urn:xmpp:eme:0 encryption-namespace hint for non-supporting clients +} + +// 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. +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 + + // EnsureIdentity creates (idempotently) a local cryptographic identity + // for a PeerID this gateway owns (a bridged chat pseudo-JID), + // generating and persisting whatever key material the backend needs. + EnsureIdentity(peer PeerID) error + + // PublishedIdentity returns the material this gateway must SERVE to + // pubsub GET requests for its own PeerID (device list), pre-marshaled + // to opaque bytes the xmpp layer wraps in /. + PublishedIdentity(peer PeerID) (DeviceListDoc, error) + // PublishedBundle is the same, for one specific device's bundle node. + PublishedBundle(peer PeerID, device DeviceID) (BundleDoc, error) + + // IngestRemoteDeviceList / IngestRemoteBundle feed the results of an + // outbound PEP fetch (device-list, then per-device bundle) for a real + // remote peer into the backend's store, establishing/refreshing + // sessions as needed (trust decisions happen inside here). + IngestRemoteDeviceList(peer PeerID, doc DeviceListDoc) error + IngestRemoteBundle(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 (a chat + // pseudo-JID this gateway owns). + Encrypt(from PeerID, to []PeerID, plaintext []byte) (Envelope, error) + + // Decrypt consumes an inbound envelope addressed to "to" (a chat + // pseudo-JID this gateway owns) from "from" (the real remote peer) + // and returns plaintext. + Decrypt(from PeerID, to PeerID, env Envelope) ([]byte, error) + + // Devices lists known devices for a peer (trust-listing ad-hoc commands). + Devices(peer PeerID) ([]DeviceInfo, error) +} diff --git a/go.mod b/go.mod index 2a1c994..78cfbac 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/soheilhy/args v0.0.0-20150720134047-6bcf4c78e87e github.com/xdg-go/stringprep v1.0.4 github.com/zelenin/go-tdlib v0.5.2 + golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 gopkg.in/yaml.v2 v2.2.4 gosrc.io/xmpp v0.5.2-0.20211214110136-5f99e1cd06e1 ) diff --git a/go.sum b/go.sum index 83a3e9e..2e1339b 100644 --- a/go.sum +++ b/go.sum @@ -1,18 +1,6 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -dev.narayana.im/narayana/go-tdlib v0.0.0-20230730021136-47da33180615 h1:RRUZJSro+k8FkazNx7QEYLVoO4wZtchvsd0Y2RBWjeU= -dev.narayana.im/narayana/go-tdlib v0.0.0-20230730021136-47da33180615/go.mod h1:Xs8fXbk5n7VaPyrSs9DP7QYoBScWYsjX+lUcWmx1DIU= -dev.narayana.im/narayana/go-tdlib v0.0.0-20231111182840-bc2f985e6268 h1:NCbc2bYuUGQsb/3z5SCIia3N34Ktwq3FwaUAfgF/WEU= -dev.narayana.im/narayana/go-tdlib v0.0.0-20231111182840-bc2f985e6268/go.mod h1:Xs8fXbk5n7VaPyrSs9DP7QYoBScWYsjX+lUcWmx1DIU= dev.narayana.im/narayana/go-tdlib v0.0.0-20240124222245-b4c12addb061 h1:CWAQT74LwQne/3Po5KXDvudu3N0FBWm3XZZZhtl5j2w= dev.narayana.im/narayana/go-tdlib v0.0.0-20240124222245-b4c12addb061/go.mod h1:Xs8fXbk5n7VaPyrSs9DP7QYoBScWYsjX+lUcWmx1DIU= -dev.narayana.im/narayana/go-xmpp v0.0.0-20220524203317-306b4ff58e8f h1:6249ajbMjgYz53Oq0IjTvjHXbxTfu29Mj1J/6swRHs4= -dev.narayana.im/narayana/go-xmpp v0.0.0-20220524203317-306b4ff58e8f/go.mod h1:L3NFMqYOxyLz3JGmgFyWf7r9htE91zVGiK40oW4RwdY= -dev.narayana.im/narayana/go-xmpp v0.0.0-20240131013505-18c46e6c59fd h1:+UW+E7JjI88aH4beDn1cw6D8rs1I061hN91HU4Y4pT8= -dev.narayana.im/narayana/go-xmpp v0.0.0-20240131013505-18c46e6c59fd/go.mod h1:L3NFMqYOxyLz3JGmgFyWf7r9htE91zVGiK40oW4RwdY= -dev.narayana.im/narayana/go-xmpp v0.0.0-20240512132113-6725c3862314 h1:29/NjOGOUDceO73Hk4Nj4uVa1je8MULJlsDSvKxSN/k= -dev.narayana.im/narayana/go-xmpp v0.0.0-20240512132113-6725c3862314/go.mod h1:L3NFMqYOxyLz3JGmgFyWf7r9htE91zVGiK40oW4RwdY= -dev.narayana.im/narayana/go-xmpp v0.0.0-20250818040038-376b5d77528a h1:9PPqmhy6HbhhCS5EZzw+sdi4EpWW+LOwnz+/JXTcHjQ= -dev.narayana.im/narayana/go-xmpp v0.0.0-20250818040038-376b5d77528a/go.mod h1:L3NFMqYOxyLz3JGmgFyWf7r9htE91zVGiK40oW4RwdY= dev.narayana.im/narayana/go-xmpp v0.0.0-20250823114312-ed4011fc17e4 h1:HQT33Zp3iRkbCiijWDo943K//wQgzoMccIP7Vb2uEfY= dev.narayana.im/narayana/go-xmpp v0.0.0-20250823114312-ed4011fc17e4/go.mod h1:L3NFMqYOxyLz3JGmgFyWf7r9htE91zVGiK40oW4RwdY= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= @@ -125,8 +113,6 @@ github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gi github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/zelenin/go-tdlib v0.5.2 h1:inEATEM0Pz6/HBI3wTlhd+brDHpmoXGgwdSb8/V6GiA= -github.com/zelenin/go-tdlib v0.5.2/go.mod h1:Xs8fXbk5n7VaPyrSs9DP7QYoBScWYsjX+lUcWmx1DIU= go.coder.com/go-tools v0.0.0-20190317003359-0c6a35b74a16/go.mod h1:iKV5yK9t+J5nG9O3uF6KYdPEz3dyfMyB15MN1rbQ8Qw= go.opencensus.io v0.22.5 h1:dntmOdLpSpHlVqbW5Eay97DelsZHe+55D+xC6i0dDS0= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= @@ -138,6 +124,7 @@ golang.org/x/crypto v0.0.0-20180426230345-b49d69b5da94/go.mod h1:6SG95UA2DQfeDnf golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=