Abstract E2EE manager/registry

This commit is contained in:
Bohdan Horbeshko 2026-07-28 19:10:01 -04:00
parent 3751113015
commit 5e71ba954c
8 changed files with 630 additions and 14 deletions

35
e2ee/manager.go Normal file
View file

@ -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
}

288
e2ee/omemo/envelope.go Normal file
View file

@ -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; <payload>
// carries ciphertext only, <iv> 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; <payload> carries ciphertext only;
// there is no <iv> 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 <from> 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
// <key rid=".." prekey=".."/> (legacy) or <key rid=".." kex=".."/>
// (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 <encrypted> 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
}

119
e2ee/omemo/envelope_test.go Normal file
View file

@ -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])
}
}
}

76
e2ee/omemo/sce.go Normal file
View file

@ -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 <rpad/> and SHOULD contain <from/> (MUST contain
// <to/> 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 <from/> 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-<rpad/> 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
}

20
e2ee/registry.go Normal file
View file

@ -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
}

90
e2ee/types.go Normal file
View file

@ -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 <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 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 <items>/<item>.
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)
}

1
go.mod
View file

@ -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
)

15
go.sum
View file

@ -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=