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 {
|
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())
|
cipher, err := libsignal.NewSessionCipher(b.libctx, storeCtx, libsignal.Address{Name: string(peer), DeviceID: deviceID}, version.libsignalVersion())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return e2ee.Envelope{}, fmt.Errorf("omemo: NewSessionCipher(%s:%d): %w", peer, deviceID, err)
|
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 {
|
if err := b.ensureIdentity(store); err != nil {
|
||||||
return nil, fmt.Errorf("omemo: ensureIdentity: %w", err)
|
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()
|
regID, err := store.GetLocalRegistrationID()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("omemo: GetLocalRegistrationID: %w", err)
|
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))
|
infos := make([]e2ee.DeviceInfo, 0, len(deviceIDs))
|
||||||
for _, id := range deviceIDs {
|
for _, id := range deviceIDs {
|
||||||
// A stored session implies IsTrustedIdentity already accepted this
|
// A stored session implies IsTrustedIdentity already accepted this
|
||||||
// device's key (TOFU) - finer-grained trust states (manually
|
// device's key (TOFU, which always stays on) - Trusted here
|
||||||
// verified/revoked) are a documented future enhancement, not
|
// reflects the separate manual block override layered on top (see
|
||||||
// needed for this phase.
|
// BlockDevice), not TOFU's own verdict.
|
||||||
infos = append(infos, e2ee.DeviceInfo{ID: e2ee.DeviceID(strconv.FormatUint(uint64(id), 10)), Trusted: true})
|
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
|
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) {
|
func (b *Backend) Enabled(owner e2ee.PeerID) (bool, error) {
|
||||||
login, ownerJID, ok := e2ee.SplitOwnedPeer(owner)
|
login, ownerJID, ok := e2ee.SplitOwnedPeer(owner)
|
||||||
if !ok {
|
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>/enabled
|
||||||
// <login>/<owner>/negotiatedversion
|
// <login>/<owner>/negotiatedversion
|
||||||
|
// <login>/<owner>/override
|
||||||
// <login>/<owner>/identity/keypair
|
// <login>/<owner>/identity/keypair
|
||||||
// <login>/<owner>/identity/registrationid
|
// <login>/<owner>/identity/registrationid
|
||||||
// <login>/<owner>/identity/signedprekey/<id>
|
// <login>/<owner>/identity/signedprekey/<id>
|
||||||
// <login>/<owner>/identity/prekey/<id>
|
// <login>/<owner>/identity/prekey/<id>
|
||||||
// <login>/<owner>/identity/prekey/counter
|
// <login>/<owner>/identity/prekey/counter
|
||||||
// <login>/<owner>/remote/<remote>/identity/<deviceId>
|
// <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>/session/<deviceId>
|
||||||
// <login>/<owner>/remote/<remote>/devicelist
|
// <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 {
|
func (s *Store) enabledKey() []byte {
|
||||||
return []byte(fmt.Sprintf("%s/%s/enabled", s.login, s.owner))
|
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))
|
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 {
|
func (s *Store) identityKeyPairKey() []byte {
|
||||||
return []byte(fmt.Sprintf("%s/%s/identity/keypair", s.login, s.owner))
|
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))
|
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 ---
|
// --- generic byte get/set/delete/has, mirroring badger/ids.go's style ---
|
||||||
|
|
||||||
func (s *Store) get(key []byte) ([]byte, error) {
|
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})
|
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
|
// NegotiatedVersion returns the wire-format version last negotiated for
|
||||||
// this chat's outgoing content (see e2ee/omemo's Version enum - this
|
// this chat's outgoing content (see e2ee/omemo's Version enum - this
|
||||||
// package stays version-agnostic and only stores the raw byte), and
|
// 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 {
|
func (s *Store) SaveRemoteDeviceListCache(remote string, doc []byte) error {
|
||||||
return s.set(s.remoteDeviceListKey(remote), doc)
|
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 }
|
type BundleDoc struct{ Raw []byte }
|
||||||
|
|
||||||
// DeviceInfo describes one known device of a peer, for trust-listing
|
// 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 {
|
type DeviceInfo struct {
|
||||||
ID DeviceID
|
ID DeviceID
|
||||||
Trusted bool
|
Trusted bool
|
||||||
|
|
@ -162,6 +166,25 @@ type Backend interface {
|
||||||
Enabled(owner PeerID) (bool, error)
|
Enabled(owner PeerID) (bool, error)
|
||||||
SetEnabled(owner PeerID, enabled 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
|
// Close flushes and releases whatever storage/resources this backend
|
||||||
// holds (e.g. the underlying Badger DB), for a clean process shutdown -
|
// holds (e.g. the underlying Badger DB), for a clean process shutdown -
|
||||||
// mirrors gateway.IdsDB.Close()'s role for the id-mapping store.
|
// mirrors gateway.IdsDB.Close()'s role for the id-mapping store.
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,14 @@ type Session struct {
|
||||||
ignoredChatsMap map[int64]bool `yaml:"-"`
|
ignoredChatsMap map[int64]bool `yaml:"-"`
|
||||||
|
|
||||||
IgnoreGroupDeletions bool `yaml:":ignoregroupdeletions"`
|
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 (
|
const (
|
||||||
|
|
@ -70,6 +78,7 @@ var ConfigKeys = []string{
|
||||||
"receipts",
|
"receipts",
|
||||||
"nativeedits",
|
"nativeedits",
|
||||||
"ignoregroupdeletions",
|
"ignoregroupdeletions",
|
||||||
|
"omemo",
|
||||||
}
|
}
|
||||||
|
|
||||||
var Presets = map[string][][]string{
|
var Presets = map[string][][]string{
|
||||||
|
|
@ -199,6 +208,11 @@ func (s *Session) get(key string) (string, error) {
|
||||||
return fromBool(s.NativeEdits), nil
|
return fromBool(s.NativeEdits), nil
|
||||||
case "ignoregroupdeletions":
|
case "ignoregroupdeletions":
|
||||||
return fromBool(s.IgnoreGroupDeletions), nil
|
return fromBool(s.IgnoreGroupDeletions), nil
|
||||||
|
case "omemo":
|
||||||
|
if s.OMEMO == "" {
|
||||||
|
return "auto", nil
|
||||||
|
}
|
||||||
|
return s.OMEMO, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return "", errors.New("Unknown session property")
|
return "", errors.New("Unknown session property")
|
||||||
|
|
@ -297,6 +311,13 @@ func (s *Session) Set(key string, value string) (string, error) {
|
||||||
}
|
}
|
||||||
s.IgnoreGroupDeletions = b
|
s.IgnoreGroupDeletions = b
|
||||||
return value, nil
|
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")
|
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
|
// PropertyType determines the property type
|
||||||
func PropertyType(key string) byte {
|
func PropertyType(key string) byte {
|
||||||
switch key {
|
switch key {
|
||||||
case "timezone":
|
case "timezone", "omemo":
|
||||||
return PropertyTypeString
|
return PropertyTypeString
|
||||||
case "keeponline", "rawmessages", "asciiarrows", "muc", "oobmode", "carbons", "hideids",
|
case "keeponline", "rawmessages", "asciiarrows", "muc", "oobmode", "carbons", "hideids",
|
||||||
"receipts", "nativeedits", "ignoregroupdeletions":
|
"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},
|
"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},
|
"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},
|
"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},
|
"pass": command{0, []string{}, "proceed to next login stage", false, nil, false},
|
||||||
"finish": command{0, []string{}, "skip post-login configuration", 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},
|
"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},
|
"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},
|
"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},
|
"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},
|
"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},
|
"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)"},
|
"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)"},
|
"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)"},
|
"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 {
|
type command struct {
|
||||||
|
|
@ -572,6 +575,17 @@ func (c *Client) ProcessTransportCommand(cmdline string, resource string) (strin
|
||||||
cancelNextStage = true
|
cancelNextStage = true
|
||||||
return "Invalid argument. Allowed ones are " + transportCommands["preset"].Arguments[0], false
|
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":
|
case "pass":
|
||||||
switch c.loginStage {
|
switch c.loginStage {
|
||||||
case LoginStagePreset:
|
case LoginStagePreset:
|
||||||
|
|
@ -1180,6 +1194,9 @@ func (c *Client) ProcessChatCommand(chatID int64, cmdline string) (string, bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
return strings.Join(entries, "\n"), true, true
|
return strings.Join(entries, "\n"), true, true
|
||||||
|
case "omemo":
|
||||||
|
response, success := c.cmdOmemoChat(chatID, args)
|
||||||
|
return response, true, success
|
||||||
case "help":
|
case "help":
|
||||||
return c.helpString(CommandTypeChat, chatID), true, true
|
return c.helpString(CommandTypeChat, chatID), true, true
|
||||||
default:
|
default:
|
||||||
|
|
|
||||||
|
|
@ -418,7 +418,13 @@ func (c *Client) updateMessageContent(update *client.UpdateMessageContent) {
|
||||||
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))
|
||||||
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)
|
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"))
|
||||||
|
|
|
||||||
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
|
// OMEMO encrypt hook - personal-chat pseudo-JIDs only (true MUC/XEP-0045
|
||||||
// groupchat OMEMO is out of scope entirely, not deferred - see the plan
|
// 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
|
// doc). Whether to encrypt is decided by e2ee.ShouldEncrypt, combining
|
||||||
// auto-upgrade trigger (xmpp/handlers.go). The account-wide config
|
// this chat's optional /omemo on|off override, the account-wide
|
||||||
// toggle (persistence.Session.OMEMO) is a separate, not-yet-wired-up
|
// persistence.Session.OMEMO mode, and (for the default "auto" mode) the
|
||||||
// piece (M4) meant to OR into this same gate. Encryption happens once
|
// 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
|
// per logical message (not once per jids resource copy), addressed to
|
||||||
// every known device of the real user's bare JID - carbon-copy
|
// 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
|
// 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 !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 {
|
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)
|
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"))
|
||||||
|
|
@ -3047,7 +3055,13 @@ func (c *Client) sendMessagesReverse(chatID int64, messages []*client.Message, p
|
||||||
if !isMUC && body != "" {
|
if !isMUC && body != "" {
|
||||||
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 {
|
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)
|
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"))
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue