diff --git a/e2ee/omemo/encrypt.go b/e2ee/omemo/encrypt.go index 7ff331a..4a73fd3 100644 --- a/e2ee/omemo/encrypt.go +++ b/e2ee/omemo/encrypt.go @@ -59,6 +59,16 @@ func (b *Backend) Encrypt(from e2ee.PeerID, to []e2ee.PeerID, plaintext []byte) } for _, deviceID := range deviceIDs { + // A manually blocked device (see Backend.BlockDevice) never + // gets a copy, regardless of any session already established + // with it - TOFU trust itself is untouched, this is purely the + // manual override layered on top. + if blocked, err := store.IsDeviceBlocked(string(peer), deviceID); err != nil { + return e2ee.Envelope{}, fmt.Errorf("omemo: IsDeviceBlocked(%s:%d): %w", peer, deviceID, err) + } else if blocked { + continue + } + cipher, err := libsignal.NewSessionCipher(b.libctx, storeCtx, libsignal.Address{Name: string(peer), DeviceID: deviceID}, version.libsignalVersion()) if err != nil { return e2ee.Envelope{}, fmt.Errorf("omemo: NewSessionCipher(%s:%d): %w", peer, deviceID, err) @@ -116,6 +126,17 @@ func (b *Backend) Decrypt(from, to e2ee.PeerID, env e2ee.Envelope) ([]byte, erro if err := b.ensureIdentity(store); err != nil { return nil, fmt.Errorf("omemo: ensureIdentity: %w", err) } + + // A manually blocked sender device (see Backend.BlockDevice) is + // rejected outright, regardless of any session already established + // with it - TOFU trust itself is untouched, this is purely the manual + // override layered on top. + if blocked, err := store.IsDeviceBlocked(string(from), wireEnv.SenderSID); err != nil { + return nil, fmt.Errorf("omemo: IsDeviceBlocked: %w", err) + } else if blocked { + return nil, fmt.Errorf("omemo: Decrypt: sender device %s:%d is blocked", from, wireEnv.SenderSID) + } + regID, err := store.GetLocalRegistrationID() if err != nil { return nil, fmt.Errorf("omemo: GetLocalRegistrationID: %w", err) diff --git a/e2ee/omemo/identity.go b/e2ee/omemo/identity.go index b865874..2ff7776 100644 --- a/e2ee/omemo/identity.go +++ b/e2ee/omemo/identity.go @@ -486,14 +486,91 @@ func (b *Backend) Devices(owner, peer e2ee.PeerID) ([]e2ee.DeviceInfo, error) { infos := make([]e2ee.DeviceInfo, 0, len(deviceIDs)) for _, id := range deviceIDs { // A stored session implies IsTrustedIdentity already accepted this - // device's key (TOFU) - finer-grained trust states (manually - // verified/revoked) are a documented future enhancement, not - // needed for this phase. - infos = append(infos, e2ee.DeviceInfo{ID: e2ee.DeviceID(strconv.FormatUint(uint64(id), 10)), Trusted: true}) + // device's key (TOFU, which always stays on) - Trusted here + // reflects the separate manual block override layered on top (see + // BlockDevice), not TOFU's own verdict. + blocked, err := store.IsDeviceBlocked(string(peer), id) + if err != nil { + return nil, fmt.Errorf("omemo: IsDeviceBlocked(%s:%d): %w", peer, id, err) + } + infos = append(infos, e2ee.DeviceInfo{ID: e2ee.DeviceID(strconv.FormatUint(uint64(id), 10)), Trusted: !blocked}) } return infos, nil } +// Override implements e2ee.Backend. +func (b *Backend) Override(owner e2ee.PeerID) (bool, bool, error) { + login, ownerJID, ok := e2ee.SplitOwnedPeer(owner) + if !ok { + return false, false, fmt.Errorf("omemo: Override: %q is not an OwnedPeer", owner) + } + + b.mu.Lock() + defer b.mu.Unlock() + + return b.db.Store(b.libctx, login, ownerJID).Override() +} + +// SetOverride implements e2ee.Backend. +func (b *Backend) SetOverride(owner e2ee.PeerID, on bool) error { + login, ownerJID, ok := e2ee.SplitOwnedPeer(owner) + if !ok { + return fmt.Errorf("omemo: SetOverride: %q is not an OwnedPeer", owner) + } + + b.mu.Lock() + defer b.mu.Unlock() + + return b.db.Store(b.libctx, login, ownerJID).SetOverride(on) +} + +// ClearOverride implements e2ee.Backend. +func (b *Backend) ClearOverride(owner e2ee.PeerID) error { + login, ownerJID, ok := e2ee.SplitOwnedPeer(owner) + if !ok { + return fmt.Errorf("omemo: ClearOverride: %q is not an OwnedPeer", owner) + } + + b.mu.Lock() + defer b.mu.Unlock() + + return b.db.Store(b.libctx, login, ownerJID).ClearOverride() +} + +// BlockDevice implements e2ee.Backend. +func (b *Backend) BlockDevice(owner, peer e2ee.PeerID, device e2ee.DeviceID) error { + login, ownerJID, ok := e2ee.SplitOwnedPeer(owner) + if !ok { + return fmt.Errorf("omemo: BlockDevice: %q is not an OwnedPeer", owner) + } + deviceID, err := parseDeviceID(device) + if err != nil { + return err + } + + b.mu.Lock() + defer b.mu.Unlock() + + return b.db.Store(b.libctx, login, ownerJID).SetDeviceBlocked(string(peer), deviceID, true) +} + +// UnblockDevice implements e2ee.Backend. +func (b *Backend) UnblockDevice(owner, peer e2ee.PeerID, device e2ee.DeviceID) error { + login, ownerJID, ok := e2ee.SplitOwnedPeer(owner) + if !ok { + return fmt.Errorf("omemo: UnblockDevice: %q is not an OwnedPeer", owner) + } + deviceID, err := parseDeviceID(device) + if err != nil { + return err + } + + b.mu.Lock() + defer b.mu.Unlock() + + return b.db.Store(b.libctx, login, ownerJID).SetDeviceBlocked(string(peer), deviceID, false) +} + func (b *Backend) Enabled(owner e2ee.PeerID) (bool, error) { login, ownerJID, ok := e2ee.SplitOwnedPeer(owner) if !ok { diff --git a/e2ee/policy.go b/e2ee/policy.go new file mode 100644 index 0000000..5c10f5d --- /dev/null +++ b/e2ee/policy.go @@ -0,0 +1,83 @@ +package e2ee + +import "fmt" + +// Mode is the account-wide OMEMO policy (see persistence.Session.OMEMO), +// combined with a chat's optional Override and its auto-upgrade sticky +// flag (Backend.Enabled) by ShouldEncrypt to decide whether to attempt +// outgoing encryption for that chat right now. +type Mode int + +const ( + // ModeAuto (the default) only encrypts once a chat has auto-upgraded + // (Backend.Enabled), i.e. its peer has already sent this chat an OMEMO + // message. This is the original, only behavior before Mode existed. + ModeAuto Mode = iota + + // ModeOn always attempts to encrypt, regardless of whether the peer + // has ever shown OMEMO support. If no session can be established, the + // send fails outright rather than falling back to plaintext - the + // same OMEMOSendFailedBody convention already used at every Encrypt + // call site applies, it's just reached unconditionally now instead of + // only for already-active chats. Incoming plaintext is still accepted + // and processed as usual - this mode only ever affects outgoing. + ModeOn + + // ModeOff never encrypts outgoing content, even for a chat that has + // already auto-upgraded - inbound OMEMO messages are still decrypted + // normally regardless of this mode (decryption never consults Mode at + // all, only the encrypt hooks do). + ModeOff +) + +func (m Mode) String() string { + switch m { + case ModeOn: + return "on" + case ModeOff: + return "off" + default: + return "auto" + } +} + +// ParseMode maps persistence.Session.OMEMO's stored string to a Mode - an +// empty string (an account that predates this field, or never touched it) +// is treated the same as "auto", matching Mode's zero value and default. +func ParseMode(s string) (Mode, error) { + switch s { + case "", "auto": + return ModeAuto, nil + case "on": + return ModeOn, nil + case "off": + return ModeOff, nil + default: + return ModeAuto, fmt.Errorf("e2ee: unknown mode %q (want on, auto, or off)", s) + } +} + +// ShouldEncrypt decides whether owner's outgoing content should be +// encrypted right now, combining (in priority order): owner's explicit +// per-chat Override, if one has been set; otherwise mode's account-wide +// policy, consulting backend's auto-upgrade sticky flag (Enabled) only for +// ModeAuto. This is the single place that combination logic lives - every +// encrypt hook (SendMessageToGateway, sendMessagesReverse, +// updateMessageContent) calls this instead of checking Enabled directly, +// so a chat's effective behavior can never drift between call sites. +func ShouldEncrypt(backend Backend, owner PeerID, mode Mode) (bool, error) { + if override, isSet, err := backend.Override(owner); err != nil { + return false, err + } else if isSet { + return override, nil + } + + switch mode { + case ModeOn: + return true, nil + case ModeOff: + return false, nil + default: + return backend.Enabled(owner) + } +} diff --git a/e2ee/store/badgerstore/store.go b/e2ee/store/badgerstore/store.go index eaa1bdb..c22ca37 100644 --- a/e2ee/store/badgerstore/store.go +++ b/e2ee/store/badgerstore/store.go @@ -30,15 +30,19 @@ var _ libsignal.Store = (*Store)(nil) // // //enabled // //negotiatedversion +// //override // //identity/keypair // //identity/registrationid // //identity/signedprekey/ // //identity/prekey/ // //identity/prekey/counter // //remote//identity/ -// //remote//trust/ +// //remote//blocked/ // //remote//session/ // //remote//devicelist +// +// (identity/ doubles as the TOFU trust record - see +// IsTrustedIdentity; there's no separate "trust" key.) func (s *Store) enabledKey() []byte { return []byte(fmt.Sprintf("%s/%s/enabled", s.login, s.owner)) @@ -48,6 +52,10 @@ func (s *Store) negotiatedVersionKey() []byte { return []byte(fmt.Sprintf("%s/%s/negotiatedversion", s.login, s.owner)) } +func (s *Store) overrideKey() []byte { + return []byte(fmt.Sprintf("%s/%s/override", s.login, s.owner)) +} + func (s *Store) identityKeyPairKey() []byte { return []byte(fmt.Sprintf("%s/%s/identity/keypair", s.login, s.owner)) } @@ -88,6 +96,10 @@ func (s *Store) remoteDeviceListKey(remote string) []byte { return []byte(fmt.Sprintf("%s/%s/remote/%s/devicelist", s.login, s.owner, remote)) } +func (s *Store) blockedKey(remote string, deviceID uint32) []byte { + return []byte(fmt.Sprintf("%s/%s/remote/%s/blocked/%d", s.login, s.owner, remote, deviceID)) +} + // --- generic byte get/set/delete/has, mirroring badger/ids.go's style --- func (s *Store) get(key []byte) ([]byte, error) { @@ -418,6 +430,36 @@ func (s *Store) SetEnabled(enabled bool) error { return s.set(s.enabledKey(), []byte{v}) } +// Override returns this chat's manual on/off override of the account-wide +// OMEMO mode (see e2ee.Mode/e2ee.ShouldEncrypt), and whether one has been +// set at all - isSet=false means "no override, follow the account-wide +// mode", not "forced off". +func (s *Store) Override() (on bool, isSet bool, err error) { + val, err := s.get(s.overrideKey()) + if err != nil { + return false, false, err + } + if len(val) != 1 { + return false, false, nil + } + return val[0] == 1, true, nil +} + +// SetOverride persists this chat's manual on/off override. +func (s *Store) SetOverride(on bool) error { + v := byte(0) + if on { + v = 1 + } + return s.set(s.overrideKey(), []byte{v}) +} + +// ClearOverride removes this chat's manual override, reverting it to +// following the account-wide OMEMO mode. +func (s *Store) ClearOverride() error { + return s.delete(s.overrideKey()) +} + // NegotiatedVersion returns the wire-format version last negotiated for // this chat's outgoing content (see e2ee/omemo's Version enum - this // package stays version-agnostic and only stores the raw byte), and @@ -456,3 +498,20 @@ func (s *Store) RemoteDeviceListCache(remote string) ([]byte, bool, error) { func (s *Store) SaveRemoteDeviceListCache(remote string, doc []byte) error { return s.set(s.remoteDeviceListKey(remote), doc) } + +// IsDeviceBlocked reports whether remote's device deviceID has been +// manually blocked - a manual override layered on top of TOFU trust +// (IsTrustedIdentity), which always stays on regardless; this only ever +// narrows it further. Works independent of whether a session currently +// exists for that device. +func (s *Store) IsDeviceBlocked(remote string, deviceID uint32) (bool, error) { + return s.has(s.blockedKey(remote, deviceID)) +} + +// SetDeviceBlocked sets or clears remote's device deviceID's manual block. +func (s *Store) SetDeviceBlocked(remote string, deviceID uint32, blocked bool) error { + if !blocked { + return s.delete(s.blockedKey(remote, deviceID)) + } + return s.set(s.blockedKey(remote, deviceID), []byte{1}) +} diff --git a/e2ee/types.go b/e2ee/types.go index 506f2a5..8d67129 100644 --- a/e2ee/types.go +++ b/e2ee/types.go @@ -49,7 +49,11 @@ type DeviceListDoc struct{ Raw []byte } type BundleDoc struct{ Raw []byte } // DeviceInfo describes one known device of a peer, for trust-listing -// ad-hoc commands. +// 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 @@ -162,6 +166,25 @@ type Backend interface { 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. diff --git a/persistence/sessions.go b/persistence/sessions.go index c75dea0..790b439 100644 --- a/persistence/sessions.go +++ b/persistence/sessions.go @@ -50,6 +50,14 @@ type Session struct { ignoredChatsMap map[int64]bool `yaml:"-"` IgnoreGroupDeletions bool `yaml:":ignoregroupdeletions"` + + // OMEMO is the account-wide OMEMO mode: "on", "auto", or "off" (see + // e2ee.Mode/e2ee.ParseMode - kept as a plain string here rather than + // importing the e2ee package, to avoid coupling this generic, + // feature-agnostic session store to one specific feature). Empty + // (the zero value, e.g. for an account that predates this field) + // means "auto", matching e2ee.ModeAuto's own default. + OMEMO string `yaml:":omemo"` } const ( @@ -70,6 +78,7 @@ var ConfigKeys = []string{ "receipts", "nativeedits", "ignoregroupdeletions", + "omemo", } var Presets = map[string][][]string{ @@ -199,6 +208,11 @@ func (s *Session) get(key string) (string, error) { return fromBool(s.NativeEdits), nil case "ignoregroupdeletions": return fromBool(s.IgnoreGroupDeletions), nil + case "omemo": + if s.OMEMO == "" { + return "auto", nil + } + return s.OMEMO, nil } return "", errors.New("Unknown session property") @@ -297,6 +311,13 @@ func (s *Session) Set(key string, value string) (string, error) { } s.IgnoreGroupDeletions = b return value, nil + case "omemo": + switch value { + case "on", "auto", "off": + s.OMEMO = value + return value, nil + } + return "", errors.New("Invalid omemo mode (want on, auto, or off)") } return "", errors.New("Unknown session property") @@ -305,7 +326,7 @@ func (s *Session) Set(key string, value string) (string, error) { // PropertyType determines the property type func PropertyType(key string) byte { switch key { - case "timezone": + case "timezone", "omemo": return PropertyTypeString case "keeponline", "rawmessages", "asciiarrows", "muc", "oobmode", "carbons", "hideids", "receipts", "nativeedits", "ignoregroupdeletions": diff --git a/telegram/commands.go b/telegram/commands.go index d97bea9..f691e7a 100644 --- a/telegram/commands.go +++ b/telegram/commands.go @@ -70,6 +70,7 @@ var transportCommands = map[string]command{ "supergroup": command{1, []string{"title", "description"}, "create new supergroup «title» with «description»", true, nil, true}, "channel": command{1, []string{"title", "description"}, "create new channel «title» with «description»", true, nil, true}, "preset": command{1, []string{"modern|classic|pass"}, "apply a config preset", false, nil, false}, + "omemo": command{0, []string{"on|auto|off"}, "view or set the account-wide OMEMO encryption mode", false, nil, false}, "pass": command{0, []string{}, "proceed to next login stage", false, nil, false}, "finish": command{0, []string{}, "skip post-login configuration", false, nil, false}, } @@ -88,6 +89,7 @@ var chatCommands = map[string]command{ "raw": command{1, []string{"message"}, "send a raw message not interpeted as a transport command (e.g. a bot command)", true, nil, true}, "forward": command{2, []string{"message_id", "target_chat"}, "forwards a message", true, nil, true}, "vcard": command{0, []string{}, "print vCard as text", true, nil, true}, + "omemo": command{0, []string{"[on|off|clear|devices ...]"}, "view or override this chat's OMEMO state, or manage its peer's device trust (devices list|block |accept )", true, nil, false}, "add": command{1, []string{"@username"}, "add @username to your chat list", true, nil, true}, "join": command{1, []string{"https://t.me/invite_link"}, "join to chat via invite link or @publicname", true, nil, true}, "group": command{1, []string{"title"}, "create groupchat «title» with current user", true, ¬ForGroups, true}, @@ -126,6 +128,7 @@ var transportConfigurationOptions = map[string]configurationOption{ "receipts": configurationOption{"", "if enabled, XMPP read receipts are synced to Telegram, otherwise, messages are marked as read automatically (true/false)"}, "nativeedits": configurationOption{"", "if possible, edit XMPP messages instead of showing Telegram edits as separate messages (true/false)"}, "ignoregroupdeletions": configurationOption{"", "suppress message deletion messages in group chats (true/false)"}, + "omemo": configurationOption{"", "account-wide OMEMO encryption mode - see /omemo for a dedicated command and per-chat overrides"}, } type command struct { @@ -572,6 +575,17 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) (strin cancelNextStage = true return "Invalid argument. Allowed ones are " + transportCommands["preset"].Arguments[0], false } + case "omemo": + if len(args) > 0 { + value, err := c.Session.Set("omemo", args[0]) + if err != nil { + return err.Error(), false + } + gateway.DirtySessions = true + return fmt.Sprintf("omemo mode set to %s", value), true + } + value, _ := c.Session.Get("omemo") + return fmt.Sprintf("omemo mode is set to %s (per-chat overrides: use /omemo on|off|clear in a chat)", value), true case "pass": switch c.loginStage { case LoginStagePreset: @@ -1180,6 +1194,9 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool, } return strings.Join(entries, "\n"), true, true + case "omemo": + response, success := c.cmdOmemoChat(chatID, args) + return response, true, success case "help": return c.helpString(CommandTypeChat, chatID), true, true default: diff --git a/telegram/handlers.go b/telegram/handlers.go index fa57214..20408a2 100644 --- a/telegram/handlers.go +++ b/telegram/handlers.go @@ -418,7 +418,13 @@ func (c *Client) updateMessageContent(update *client.UpdateMessageContent) { if !isMUC { if backend, ok := gateway.E2EE.Backend(); ok { owner := e2ee.OwnedPeer(c.Session.Login, gateway.CHATJID(update.ChatId, false)) - if active, _ := backend.Enabled(owner); active { + mode, modeErr := e2ee.ParseMode(c.Session.OMEMO) + if modeErr != nil { + log.Error(errors.Wrap(modeErr, "Invalid omemo mode")) + } + if active, err := e2ee.ShouldEncrypt(backend, owner, mode); err != nil { + log.Error(errors.Wrap(err, "Failed to evaluate OMEMO policy")) + } else if active { peer := e2ee.PeerID(c.jid) if err := c.ensureOMEMOSession(backend, owner, peer); err != nil { log.Error(errors.Wrap(err, "Failed to establish OMEMO session")) diff --git a/telegram/omemo_commands.go b/telegram/omemo_commands.go new file mode 100644 index 0000000..95fbf38 --- /dev/null +++ b/telegram/omemo_commands.go @@ -0,0 +1,129 @@ +package telegram + +import ( + "fmt" + "strings" + + "dev.narayana.im/narayana/telegabber/e2ee" + "dev.narayana.im/narayana/telegabber/xmpp/gateway" +) + +// cmdOmemoChat implements the chat-scoped "/omemo" command: with no +// arguments, reports this chat's current OMEMO state (account-wide mode, +// any override, and the resulting effective state); "on"/"off" forces this +// specific chat regardless of the account-wide mode (persistence.Session.OMEMO, +// via e2ee.Mode/e2ee.ShouldEncrypt); "clear" removes that override, reverting +// to following the account-wide mode; "devices ..." manages this chat's +// peer's device trust (see cmdOmemoDevices). +func (c *Client) cmdOmemoChat(chatID int64, args []string) (string, bool) { + backend, ok := gateway.E2EE.Backend() + if !ok { + return "OMEMO is not enabled on this transport", false + } + owner := e2ee.OwnedPeer(c.Session.Login, gateway.CHATJID(chatID, false)) + + if len(args) == 0 { + return c.omemoChatStatus(backend, owner) + } + + switch args[0] { + case "on", "off": + if err := backend.SetOverride(owner, args[0] == "on"); err != nil { + return err.Error(), false + } + return fmt.Sprintf("OMEMO forced %s for this chat", args[0]), true + case "clear": + if err := backend.ClearOverride(owner); err != nil { + return err.Error(), false + } + return "OMEMO override cleared for this chat, now following the account-wide mode", true + case "devices": + return c.cmdOmemoDevices(backend, owner, args[1:]) + default: + return "Usage: /omemo [on|off|clear|devices list|devices block |devices accept ]", false + } +} + +func (c *Client) omemoChatStatus(backend e2ee.Backend, owner e2ee.PeerID) (string, bool) { + mode, err := e2ee.ParseMode(c.Session.OMEMO) + if err != nil { + return err.Error(), false + } + override, isSet, err := backend.Override(owner) + if err != nil { + return err.Error(), false + } + active, err := e2ee.ShouldEncrypt(backend, owner, mode) + if err != nil { + return err.Error(), false + } + + overrideText := "not set (following the account-wide mode)" + if isSet { + overrideText = fmt.Sprintf("forced %s", onOffString(override)) + } + + return fmt.Sprintf("Account-wide mode: %s\nThis chat's override: %s\nEffective right now: %s", + mode, overrideText, onOffString(active)), true +} + +// cmdOmemoDevices lists, blocks, or accepts (unblocks) devices of owner's +// chat's real peer. TOFU trust itself always stays on (a never-before-seen +// device is always trusted automatically) - block/accept only manage a +// manual override layered on top of that, per Backend.BlockDevice's doc +// comment. +func (c *Client) cmdOmemoDevices(backend e2ee.Backend, owner e2ee.PeerID, args []string) (string, bool) { + peer := e2ee.PeerID(c.jid) + + sub := "list" + if len(args) > 0 { + sub = args[0] + } + + switch sub { + case "list": + devices, err := backend.Devices(owner, peer) + if err != nil { + return err.Error(), false + } + if len(devices) == 0 { + return "No known devices for this chat's peer yet", true + } + lines := make([]string, 0, len(devices)) + for _, d := range devices { + lines = append(lines, fmt.Sprintf("%s: %s", d.ID, trustString(d.Trusted))) + } + return strings.Join(lines, "\n"), true + case "block", "accept": + if len(args) < 2 { + return notEnoughArguments, false + } + device := e2ee.DeviceID(args[1]) + var err error + if sub == "block" { + err = backend.BlockDevice(owner, peer, device) + } else { + err = backend.UnblockDevice(owner, peer, device) + } + if err != nil { + return err.Error(), false + } + return fmt.Sprintf("Device %s %sed", device, sub), true + default: + return "Usage: /omemo devices [list|block |accept ]", false + } +} + +func onOffString(b bool) string { + if b { + return "on" + } + return "off" +} + +func trustString(trusted bool) string { + if trusted { + return "trusted" + } + return "blocked" +} diff --git a/telegram/utils.go b/telegram/utils.go index a1de641..18369eb 100644 --- a/telegram/utils.go +++ b/telegram/utils.go @@ -2062,10 +2062,12 @@ func (c *Client) SendMessageToGateway(chatId int64, message *client.Message, id // OMEMO encrypt hook - personal-chat pseudo-JIDs only (true MUC/XEP-0045 // groupchat OMEMO is out of scope entirely, not deferred - see the plan - // doc), gated on the per-chat active flag set by the decrypt hook's - // auto-upgrade trigger (xmpp/handlers.go). The account-wide config - // toggle (persistence.Session.OMEMO) is a separate, not-yet-wired-up - // piece (M4) meant to OR into this same gate. Encryption happens once + // doc). Whether to encrypt is decided by e2ee.ShouldEncrypt, combining + // this chat's optional /omemo on|off override, the account-wide + // persistence.Session.OMEMO mode, and (for the default "auto" mode) the + // per-chat active flag set by the decrypt hook's auto-upgrade trigger + // (xmpp/handlers.go) - see ShouldEncrypt's own doc comment for the + // combination rules. Encryption happens once // per logical message (not once per jids resource copy), addressed to // every known device of the real user's bare JID - carbon-copy // resources all share the same OMEMO device fan-out. OOB file links are @@ -2083,7 +2085,13 @@ func (c *Client) SendMessageToGateway(chatId int64, message *client.Message, id if !isGroupchat && needsOMEMOEncryption { if backend, ok := gateway.E2EE.Backend(); ok { owner := e2ee.OwnedPeer(c.Session.Login, gateway.CHATJID(chatId, false)) - if active, _ := backend.Enabled(owner); active { + mode, modeErr := e2ee.ParseMode(c.Session.OMEMO) + if modeErr != nil { + log.Error(errors.Wrap(modeErr, "Invalid omemo mode")) + } + if active, err := e2ee.ShouldEncrypt(backend, owner, mode); err != nil { + log.Error(errors.Wrap(err, "Failed to evaluate OMEMO policy")) + } else if active { peer := e2ee.PeerID(c.jid) if err := c.ensureOMEMOSession(backend, owner, peer); err != nil { log.Error(errors.Wrap(err, "Failed to establish OMEMO session")) @@ -3047,7 +3055,13 @@ func (c *Client) sendMessagesReverse(chatID int64, messages []*client.Message, p if !isMUC && body != "" { if backend, ok := gateway.E2EE.Backend(); ok { owner := e2ee.OwnedPeer(c.Session.Login, gateway.CHATJID(chatID, false)) - if active, _ := backend.Enabled(owner); active { + mode, modeErr := e2ee.ParseMode(c.Session.OMEMO) + if modeErr != nil { + log.Error(errors.Wrap(modeErr, "Invalid omemo mode")) + } + if active, err := e2ee.ShouldEncrypt(backend, owner, mode); err != nil { + log.Error(errors.Wrap(err, "Failed to evaluate OMEMO policy")) + } else if active { peer := e2ee.PeerID(c.jid) if err := c.ensureOMEMOSession(backend, owner, peer); err != nil { log.Error(errors.Wrap(err, "Failed to establish OMEMO session"))