mirror of
https://dev.narayana.im/narayana/telegabber.git
synced 2026-08-05 04:07:07 +00:00
/omemo commands for transport and chats
This commit is contained in:
parent
942352a8df
commit
aa7d31a4f7
10 changed files with 464 additions and 14 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
83
e2ee/policy.go
Normal file
83
e2ee/policy.go
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -30,15 +30,19 @@ var _ libsignal.Store = (*Store)(nil)
|
|||
//
|
||||
// <login>/<owner>/enabled
|
||||
// <login>/<owner>/negotiatedversion
|
||||
// <login>/<owner>/override
|
||||
// <login>/<owner>/identity/keypair
|
||||
// <login>/<owner>/identity/registrationid
|
||||
// <login>/<owner>/identity/signedprekey/<id>
|
||||
// <login>/<owner>/identity/prekey/<id>
|
||||
// <login>/<owner>/identity/prekey/counter
|
||||
// <login>/<owner>/remote/<remote>/identity/<deviceId>
|
||||
// <login>/<owner>/remote/<remote>/trust/<deviceId>
|
||||
// <login>/<owner>/remote/<remote>/blocked/<deviceId>
|
||||
// <login>/<owner>/remote/<remote>/session/<deviceId>
|
||||
// <login>/<owner>/remote/<remote>/devicelist
|
||||
//
|
||||
// (identity/<deviceId> 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})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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":
|
||||
|
|
|
|||
|
|
@ -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 <id>|accept <id>)", 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{"<bool>", "if enabled, XMPP read receipts are synced to Telegram, otherwise, messages are marked as read automatically (true/false)"},
|
||||
"nativeedits": configurationOption{"<bool>", "if possible, edit XMPP messages instead of showing Telegram edits as separate messages (true/false)"},
|
||||
"ignoregroupdeletions": configurationOption{"<bool>", "suppress message deletion messages in group chats (true/false)"},
|
||||
"omemo": configurationOption{"<on|auto|off>", "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:
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
|
|
|
|||
129
telegram/omemo_commands.go
Normal file
129
telegram/omemo_commands.go
Normal file
|
|
@ -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 <id>|devices accept <id>]", 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 <id>|accept <id>]", false
|
||||
}
|
||||
}
|
||||
|
||||
func onOffString(b bool) string {
|
||||
if b {
|
||||
return "on"
|
||||
}
|
||||
return "off"
|
||||
}
|
||||
|
||||
func trustString(trusted bool) string {
|
||||
if trusted {
|
||||
return "trusted"
|
||||
}
|
||||
return "blocked"
|
||||
}
|
||||
|
|
@ -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"))
|
||||
|
|
|
|||
Loading…
Reference in a new issue