mirror of
https://dev.narayana.im/narayana/telegabber.git
synced 2026-08-05 12:17:06 +00:00
Avoid leaking unencrypted messages on PEP timeout
This commit is contained in:
parent
09e0c793bf
commit
44ff56742b
6 changed files with 67 additions and 12 deletions
|
|
@ -25,7 +25,14 @@ const FetchTimeout = 15 * time.Second
|
||||||
// entirely by whatever PEP node names backend itself reports via
|
// entirely by whatever PEP node names backend itself reports via
|
||||||
// DeviceListNode/BundleNode, it has no OMEMO-specific knowledge of its own,
|
// DeviceListNode/BundleNode, it has no OMEMO-specific knowledge of its own,
|
||||||
// so it works unchanged for any future Backend implementation.
|
// so it works unchanged for any future Backend implementation.
|
||||||
func FetchAndIngestPeer(component *xmpp.Component, backend Backend, owner PeerID, peer PeerID) error {
|
//
|
||||||
|
// ctx should be a long-lived, process-scoped context (e.g.
|
||||||
|
// gateway.ShutdownCtx), not context.Background() - each PEP request derives
|
||||||
|
// its own FetchTimeout-bounded child from it (see fetchOneItem), and
|
||||||
|
// deriving from a shutdown-aware parent means process teardown cancels any
|
||||||
|
// in-flight request immediately instead of leaving it to run out its own
|
||||||
|
// timeout regardless of teardown.
|
||||||
|
func FetchAndIngestPeer(ctx context.Context, component *xmpp.Component, backend Backend, owner PeerID, peer PeerID) error {
|
||||||
_, ownerBareJID, ok := SplitOwnedPeer(owner)
|
_, ownerBareJID, ok := SplitOwnedPeer(owner)
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("e2ee: FetchAndIngestPeer: %q is not an OwnedPeer", owner)
|
return fmt.Errorf("e2ee: FetchAndIngestPeer: %q is not an OwnedPeer", owner)
|
||||||
|
|
@ -39,7 +46,7 @@ func FetchAndIngestPeer(component *xmpp.Component, backend Backend, owner PeerID
|
||||||
return fmt.Errorf("e2ee: NegotiatedVariant: %w", err)
|
return fmt.Errorf("e2ee: NegotiatedVariant: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
deviceListItem, err := fetchOneItem(component, ownerBareJID, string(peer), backend.DeviceListNode(variant))
|
deviceListItem, err := fetchOneItem(ctx, component, ownerBareJID, string(peer), backend.DeviceListNode(variant))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("e2ee: fetch device list for %s: %w", peer, err)
|
return fmt.Errorf("e2ee: fetch device list for %s: %w", peer, err)
|
||||||
}
|
}
|
||||||
|
|
@ -68,7 +75,7 @@ func FetchAndIngestPeer(component *xmpp.Component, backend Backend, owner PeerID
|
||||||
if known[id] {
|
if known[id] {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
bundleItem, err := fetchOneItem(component, ownerBareJID, string(peer), backend.BundleNode(variant, id))
|
bundleItem, err := fetchOneItem(ctx, component, ownerBareJID, string(peer), backend.BundleNode(variant, id))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
lastErr = fmt.Errorf("e2ee: fetch bundle for %s:%s: %w", peer, id, err)
|
lastErr = fmt.Errorf("e2ee: fetch bundle for %s:%s: %w", peer, id, err)
|
||||||
continue
|
continue
|
||||||
|
|
@ -94,14 +101,14 @@ func FetchAndIngestPeer(component *xmpp.Component, backend Backend, owner PeerID
|
||||||
// fetchOneItem requests the single most recent item of node from jid's PEP
|
// fetchOneItem requests the single most recent item of node from jid's PEP
|
||||||
// service, sent as if from ownerBareJID (the bridged chat's pseudo-JID doing
|
// service, sent as if from ownerBareJID (the bridged chat's pseudo-JID doing
|
||||||
// the fetching), and returns its payload node.
|
// the fetching), and returns its payload node.
|
||||||
func fetchOneItem(component *xmpp.Component, ownerBareJID, jid, node string) (*stanza.Node, error) {
|
func fetchOneItem(parent context.Context, component *xmpp.Component, ownerBareJID, jid, node string) (*stanza.Node, error) {
|
||||||
iq, err := stanza.NewItemsRequest(jid, node, 1)
|
iq, err := stanza.NewItemsRequest(jid, node, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
iq.Attrs.From = ownerBareJID
|
iq.Attrs.From = ownerBareJID
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), FetchTimeout)
|
ctx, cancel := context.WithTimeout(parent, FetchTimeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
ch, err := component.SendIQ(ctx, iq)
|
ch, err := component.SendIQ(ctx, iq)
|
||||||
|
|
|
||||||
|
|
@ -409,8 +409,12 @@ func (c *Client) updateMessageContent(update *client.UpdateMessageContent) {
|
||||||
// OMEMO encrypt hook - same rules as SendMessageToGateway's (see
|
// OMEMO encrypt hook - same rules as SendMessageToGateway's (see
|
||||||
// its doc comment): personal-chat pseudo-JIDs only, gated on the
|
// its doc comment): personal-chat pseudo-JIDs only, gated on the
|
||||||
// per-chat active flag, once per logical edit rather than per
|
// per-chat active flag, once per logical edit rather than per
|
||||||
// jids resource copy.
|
// jids resource copy. If OMEMO is active but a session can't be
|
||||||
|
// established or encryption fails, the real edited content must
|
||||||
|
// NEVER go out in the clear - omemoFailed sends
|
||||||
|
// gateway.OMEMOSendFailedBody instead.
|
||||||
var envelope *e2ee.Envelope
|
var envelope *e2ee.Envelope
|
||||||
|
omemoFailed := false
|
||||||
if !isMUC {
|
if !isMUC {
|
||||||
if backend, ok := gateway.E2EE.Backend(); ok {
|
if backend, ok := gateway.E2EE.Backend(); ok {
|
||||||
owner := e2ee.OwnedPeer(c.Session.Login, gateway.CHATJID(update.ChatId, false))
|
owner := e2ee.OwnedPeer(c.Session.Login, gateway.CHATJID(update.ChatId, false))
|
||||||
|
|
@ -418,8 +422,10 @@ func (c *Client) updateMessageContent(update *client.UpdateMessageContent) {
|
||||||
peer := e2ee.PeerID(c.jid)
|
peer := e2ee.PeerID(c.jid)
|
||||||
if err := c.ensureOMEMOSession(backend, owner, peer); err != nil {
|
if err := c.ensureOMEMOSession(backend, owner, peer); err != nil {
|
||||||
log.Error(errors.Wrap(err, "Failed to establish OMEMO session"))
|
log.Error(errors.Wrap(err, "Failed to establish OMEMO session"))
|
||||||
|
omemoFailed = true
|
||||||
} else if env, err := backend.Encrypt(owner, []e2ee.PeerID{peer}, []byte(text.String())); err != nil {
|
} else if env, err := backend.Encrypt(owner, []e2ee.PeerID{peer}, []byte(text.String())); err != nil {
|
||||||
log.Error(errors.Wrap(err, "Failed to encrypt OMEMO message"))
|
log.Error(errors.Wrap(err, "Failed to encrypt OMEMO message"))
|
||||||
|
omemoFailed = true
|
||||||
} else {
|
} else {
|
||||||
envelope = &env
|
envelope = &env
|
||||||
}
|
}
|
||||||
|
|
@ -427,15 +433,20 @@ func (c *Client) updateMessageContent(update *client.UpdateMessageContent) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body := text.String()
|
||||||
|
if omemoFailed {
|
||||||
|
body = gateway.OMEMOSendFailedBody
|
||||||
|
}
|
||||||
|
|
||||||
for _, jid := range jids {
|
for _, jid := range jids {
|
||||||
if safeToSend {
|
if safeToSend {
|
||||||
gateway.SendMessage(jid, from, c.xmpp,
|
gateway.SendMessage(jid, from, c.xmpp,
|
||||||
gateway.SMBody(text.String()), gateway.SMId(id), gateway.SMReplaceId(replaceId),
|
gateway.SMBody(body), gateway.SMId(id), gateway.SMReplaceId(replaceId),
|
||||||
gateway.SMIsCarbon(isCarbon), gateway.SMIsGroupchat(isMUC), gateway.SMOriginalFrom(originalFrom),
|
gateway.SMIsCarbon(isCarbon), gateway.SMIsGroupchat(isMUC), gateway.SMOriginalFrom(originalFrom),
|
||||||
gateway.SMOMEMOEnvelope(envelope),
|
gateway.SMOMEMOEnvelope(envelope),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
gateway.SendMUCAnnouncement(jid, from, text.String(), nickname, id, c.xmpp)
|
gateway.SendMUCAnnouncement(jid, from, body, nickname, id, c.xmpp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
|
||||||
|
|
@ -2071,26 +2071,36 @@ func (c *Client) SendMessageToGateway(chatId int64, message *client.Message, id
|
||||||
// resources all share the same OMEMO device fan-out. OOB file links are
|
// resources all share the same OMEMO device fan-out. OOB file links are
|
||||||
// intentionally left in the clear (encrypting a URL string doesn't
|
// intentionally left in the clear (encrypting a URL string doesn't
|
||||||
// protect the file itself); only auxText, and text when there's no OOB
|
// protect the file itself); only auxText, and text when there's no OOB
|
||||||
// swap, are ever encrypted.
|
// swap, are ever encrypted. If OMEMO is active but a session can't be
|
||||||
|
// established or encryption fails (e.g. the peer never answers the PEP
|
||||||
|
// bundle fetch - see ensureOMEMOSession/gateway.ShutdownCtx), the real
|
||||||
|
// content must NEVER go out in the clear as a fallback - omemoFailed
|
||||||
|
// short-circuits the send loop below to gateway.OMEMOSendFailedBody
|
||||||
|
// instead.
|
||||||
var textEnvelope, auxEnvelope *e2ee.Envelope
|
var textEnvelope, auxEnvelope *e2ee.Envelope
|
||||||
if !isGroupchat {
|
omemoFailed := false
|
||||||
|
needsOMEMOEncryption := (oob == "" && text != "") || auxText != ""
|
||||||
|
if !isGroupchat && needsOMEMOEncryption {
|
||||||
if backend, ok := gateway.E2EE.Backend(); ok {
|
if backend, ok := gateway.E2EE.Backend(); ok {
|
||||||
owner := e2ee.OwnedPeer(c.Session.Login, gateway.CHATJID(chatId, false))
|
owner := e2ee.OwnedPeer(c.Session.Login, gateway.CHATJID(chatId, false))
|
||||||
if active, _ := backend.Enabled(owner); active {
|
if active, _ := backend.Enabled(owner); active {
|
||||||
peer := e2ee.PeerID(c.jid)
|
peer := e2ee.PeerID(c.jid)
|
||||||
if err := c.ensureOMEMOSession(backend, owner, peer); err != nil {
|
if err := c.ensureOMEMOSession(backend, owner, peer); err != nil {
|
||||||
log.Error(errors.Wrap(err, "Failed to establish OMEMO session"))
|
log.Error(errors.Wrap(err, "Failed to establish OMEMO session"))
|
||||||
|
omemoFailed = true
|
||||||
} else {
|
} else {
|
||||||
if oob == "" && text != "" {
|
if oob == "" && text != "" {
|
||||||
if env, err := backend.Encrypt(owner, []e2ee.PeerID{peer}, []byte(text)); err != nil {
|
if env, err := backend.Encrypt(owner, []e2ee.PeerID{peer}, []byte(text)); err != nil {
|
||||||
log.Error(errors.Wrap(err, "Failed to encrypt OMEMO message"))
|
log.Error(errors.Wrap(err, "Failed to encrypt OMEMO message"))
|
||||||
|
omemoFailed = true
|
||||||
} else {
|
} else {
|
||||||
textEnvelope = &env
|
textEnvelope = &env
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if auxText != "" {
|
if !omemoFailed && auxText != "" {
|
||||||
if env, err := backend.Encrypt(owner, []e2ee.PeerID{peer}, []byte(auxText)); err != nil {
|
if env, err := backend.Encrypt(owner, []e2ee.PeerID{peer}, []byte(auxText)); err != nil {
|
||||||
log.Error(errors.Wrap(err, "Failed to encrypt OMEMO auxiliary message"))
|
log.Error(errors.Wrap(err, "Failed to encrypt OMEMO auxiliary message"))
|
||||||
|
omemoFailed = true
|
||||||
} else {
|
} else {
|
||||||
auxEnvelope = &env
|
auxEnvelope = &env
|
||||||
}
|
}
|
||||||
|
|
@ -2107,6 +2117,11 @@ func (c *Client) SendMessageToGateway(chatId int64, message *client.Message, id
|
||||||
gateway.SMOriginalFrom(originalFrom), gateway.SMMamQueryId(mamQueryId),
|
gateway.SMOriginalFrom(originalFrom), gateway.SMMamQueryId(mamQueryId),
|
||||||
gateway.SMMucJID(mucJID), gateway.SMMucUserItem(mucUserItem),
|
gateway.SMMucJID(mucJID), gateway.SMMucUserItem(mucUserItem),
|
||||||
}
|
}
|
||||||
|
if omemoFailed {
|
||||||
|
gateway.SendMessage(jid, from, c.xmpp, append(commonArgs,
|
||||||
|
gateway.SMBody(gateway.OMEMOSendFailedBody), gateway.SMId(sId), gateway.SMStanzaId(stanzaId))...)
|
||||||
|
continue
|
||||||
|
}
|
||||||
gateway.SendMessage(jid, from, c.xmpp, append(commonArgs,
|
gateway.SendMessage(jid, from, c.xmpp, append(commonArgs,
|
||||||
gateway.SMBody(text), gateway.SMId(sId), gateway.SMStanzaId(stanzaId), gateway.SMOOB(oob),
|
gateway.SMBody(text), gateway.SMId(sId), gateway.SMStanzaId(stanzaId), gateway.SMOOB(oob),
|
||||||
gateway.SMOMEMOEnvelope(textEnvelope))...)
|
gateway.SMOMEMOEnvelope(textEnvelope))...)
|
||||||
|
|
@ -2132,7 +2147,7 @@ func (c *Client) ensureOMEMOSession(backend e2ee.Backend, owner e2ee.PeerID, pee
|
||||||
if len(devices) > 0 {
|
if len(devices) > 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return e2ee.FetchAndIngestPeer(c.xmpp, backend, owner, peer)
|
return e2ee.FetchAndIngestPeer(gateway.ShutdownCtx, c.xmpp, backend, owner, peer)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendDelayedMUCMessage is used to send MUC history via the legacy method or MAM
|
// SendDelayedMUCMessage is used to send MUC history via the legacy method or MAM
|
||||||
|
|
|
||||||
|
|
@ -287,6 +287,11 @@ func SaveSessions() {
|
||||||
func Close(component *xmpp.Component) {
|
func Close(component *xmpp.Component) {
|
||||||
log.Error("Disconnecting...")
|
log.Error("Disconnecting...")
|
||||||
|
|
||||||
|
// cancel any in-flight background operations (e.g. e2ee bundle
|
||||||
|
// fetches) derived from gateway.ShutdownCtx, rather than letting them
|
||||||
|
// run out their own timeout regardless of teardown
|
||||||
|
gateway.CancelShutdown()
|
||||||
|
|
||||||
sessionLock.Lock()
|
sessionLock.Lock()
|
||||||
// close all sessions
|
// close all sessions
|
||||||
for _, session := range sessions {
|
for _, session := range sessions {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package gateway
|
package gateway
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
@ -78,6 +79,15 @@ var IdsDB badger.IdsDB
|
||||||
// feature didn't exist" rather than nil-checking E2EE itself.
|
// feature didn't exist" rather than nil-checking E2EE itself.
|
||||||
var E2EE *e2ee.Manager
|
var E2EE *e2ee.Manager
|
||||||
|
|
||||||
|
// ShutdownCtx is cancelled once (see xmpp.Close) when the component begins
|
||||||
|
// tearing down. Long-running background operations that would otherwise
|
||||||
|
// wait out their own timeout regardless of process lifecycle - e.g. e2ee's
|
||||||
|
// PEP bundle fetches - should derive their per-call timeout from this
|
||||||
|
// (context.WithTimeout(gateway.ShutdownCtx, ...)) rather than
|
||||||
|
// context.Background(), so teardown cancels them immediately instead of
|
||||||
|
// leaving them to run for up to their full timeout past Close().
|
||||||
|
var ShutdownCtx, CancelShutdown = context.WithCancel(context.Background())
|
||||||
|
|
||||||
// DirtySessions denotes that some Telegram session configurations
|
// DirtySessions denotes that some Telegram session configurations
|
||||||
// were changed and need to be re-flushed to the YamlDB
|
// were changed and need to be re-flushed to the YamlDB
|
||||||
var DirtySessions = false
|
var DirtySessions = false
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,13 @@ import (
|
||||||
// actual machine-readable hint).
|
// actual machine-readable hint).
|
||||||
const omemoFallbackBody = "[This message is OMEMO end-to-end encrypted]"
|
const omemoFallbackBody = "[This message is OMEMO end-to-end encrypted]"
|
||||||
|
|
||||||
|
// OMEMOSendFailedBody is sent in place of the real message body when OMEMO
|
||||||
|
// is active for a chat but encryption couldn't go through (no session
|
||||||
|
// could be established, e.g. the peer never answered a PEP bundle
|
||||||
|
// request) - callers (telegram/utils.go, telegram/handlers.go) must never
|
||||||
|
// fall back to sending the real plaintext in this case, only this notice.
|
||||||
|
const OMEMOSendFailedBody = "[Failed to send: could not establish an OMEMO session]"
|
||||||
|
|
||||||
// omemoStanzaExtension converts a just-produced OMEMO e2ee.Envelope's raw
|
// omemoStanzaExtension converts a just-produced OMEMO e2ee.Envelope's raw
|
||||||
// bytes (an omemo.WireEnvelope, gob-encoded - see e2ee/omemo/envelope.go)
|
// bytes (an omemo.WireEnvelope, gob-encoded - see e2ee/omemo/envelope.go)
|
||||||
// into the wire stanza.MsgExtension for its version, plus the XEP-0380 EME
|
// into the wire stanza.MsgExtension for its version, plus the XEP-0380 EME
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue