From baae66ed0473c3a3f0cfba68c49e710759c84ab2 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sat, 9 May 2020 02:03:59 +0300 Subject: [PATCH] Add basic end-to-bridge encryption support Still missing persisting sync tokens and crypto state in DB --- commands.go | 42 ++++ config/bridge.go | 5 + crypto.go | 219 ++++++++++++++++++ database/portal.go | 14 +- database/statestore.go | 38 +++ .../2020-05-09-add-portal-encrypted-field.go | 12 + database/upgrades/upgrades.go | 2 +- example-config.yaml | 12 + main.go | 17 ++ matrix.go | 59 ++++- nocrypto.go | 26 +++ portal.go | 52 +++-- 12 files changed, 460 insertions(+), 38 deletions(-) create mode 100644 crypto.go create mode 100644 database/upgrades/2020-05-09-add-portal-encrypted-field.go create mode 100644 nocrypto.go diff --git a/commands.go b/commands.go index ec4fb9b..c1f3000 100644 --- a/commands.go +++ b/commands.go @@ -18,6 +18,7 @@ package main import ( "fmt" + "strconv" "strings" "github.com/Rhymen/go-whatsapp" @@ -118,6 +119,8 @@ func (handler *CommandHandler) CommandMux(ce *CommandEvent) { handler.CommandDeleteAllPortals(ce) case "dev-test": handler.CommandDevTest(ce) + case "set-pl": + handler.CommandSetPowerLevel(ce) case "login-matrix", "logout", "sync", "list", "open", "pm": if !ce.User.HasSession() { ce.Reply("You are not logged in. Use the `login` command to log into WhatsApp.") @@ -169,6 +172,45 @@ func (handler *CommandHandler) CommandDevTest(ce *CommandEvent) { } +func (handler *CommandHandler) CommandSetPowerLevel(ce *CommandEvent) { + portal := ce.Bridge.GetPortalByMXID(ce.RoomID) + if portal == nil { + ce.Reply("Not a portal room") + return + } + var level int + var userID id.UserID + var err error + if len(ce.Args) == 1 { + level, err = strconv.Atoi(ce.Args[0]) + if err != nil { + ce.Reply("Invalid power level \"%s\"", ce.Args[0]) + return + } + userID = ce.User.MXID + } else if len(ce.Args) == 2 { + userID = id.UserID(ce.Args[0]) + _, _, err := userID.Parse() + if err != nil { + ce.Reply("Invalid user ID \"%s\"", ce.Args[0]) + return + } + level, err = strconv.Atoi(ce.Args[1]) + if err != nil { + ce.Reply("Invalid power level \"%s\"", ce.Args[1]) + return + } + } else { + ce.Reply("**Usage:** `set-pl [user] `") + return + } + intent := portal.MainIntent() + _, err = intent.SetPowerLevel(ce.RoomID, userID, level) + if err != nil { + ce.Reply("Failed to set power levels: %v", err) + } +} + const cmdLoginHelp = `login - Authenticate this Bridge as WhatsApp Web Client` // CommandLogin handles login command diff --git a/config/bridge.go b/config/bridge.go index b7efad5..82cdd16 100644 --- a/config/bridge.go +++ b/config/bridge.go @@ -64,6 +64,11 @@ type BridgeConfig struct { CommandPrefix string `yaml:"command_prefix"` + Encryption struct { + Allow bool `yaml:"allow"` + Default bool `yaml:"default"` + } `yaml:"encryption"` + Permissions PermissionConfig `yaml:"permissions"` Relaybot RelaybotConfig `yaml:"relaybot"` diff --git a/crypto.go b/crypto.go new file mode 100644 index 0000000..17094c6 --- /dev/null +++ b/crypto.go @@ -0,0 +1,219 @@ +// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge. +// Copyright (C) 2020 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +// +build cgo + +package main + +import ( + "crypto/hmac" + "crypto/sha512" + "encoding/hex" + "time" + + "github.com/pkg/errors" + "maunium.net/go/maulogger/v2" + + "maunium.net/go/mautrix" + "maunium.net/go/mautrix/crypto" + "maunium.net/go/mautrix/event" + "maunium.net/go/mautrix/id" +) + +var levelTrace = maulogger.Level{ + Name: "Trace", + Severity: -10, + Color: -1, +} + +type CryptoHelper struct { + bridge *Bridge + client *mautrix.Client + mach *crypto.OlmMachine + log maulogger.Logger +} + +func (bridge *Bridge) initCrypto() error { + if !bridge.Config.Bridge.Encryption.Allow { + bridge.Log.Debugln("Bridge built with end-to-bridge encryption, but disabled in config") + return nil + } else if bridge.Config.Bridge.LoginSharedSecret == "" { + bridge.Log.Warnln("End-to-bridge encryption enabled, but login_shared_secret not set") + return nil + } + bridge.Log.Debugln("Initializing end-to-bridge encryption...") + client, err := bridge.loginBot() + if err != nil { + return err + } + // TODO put this in the database + cryptoStore, err := crypto.NewGobStore("crypto.gob") + if err != nil { + return err + } + + log := bridge.Log.Sub("Crypto") + logger := &cryptoLogger{log} + stateStore := &cryptoStateStore{bridge} + helper := &CryptoHelper{ + bridge: bridge, + client: client, + log: log.Sub("Helper"), + mach: crypto.NewOlmMachine(client, logger, cryptoStore, stateStore), + } + + client.Logger = logger.int.Sub("Bot") + client.Syncer = &cryptoSyncer{helper.mach} + // TODO put this in the database too + client.Store = mautrix.NewInMemoryStore() + + err = helper.mach.Load() + if err != nil { + return err + } + + bridge.Crypto = helper + return nil +} + +func (helper *CryptoHelper) Start() { + helper.log.Debugln("Starting syncer for receiving to-device messages") + err := helper.client.Sync() + if err != nil { + helper.log.Errorln("Fatal error syncing:", err) + } +} + +func (helper *CryptoHelper) Stop() { + helper.client.StopSync() +} + +func (bridge *Bridge) loginBot() (*mautrix.Client, error) { + mac := hmac.New(sha512.New, []byte(bridge.Config.Bridge.LoginSharedSecret)) + mac.Write([]byte(bridge.AS.BotMXID())) + resp, err := bridge.AS.BotClient().Login(&mautrix.ReqLogin{ + Type: "m.login.password", + Identifier: mautrix.UserIdentifier{Type: "m.id.user", User: string(bridge.AS.BotMXID())}, + Password: hex.EncodeToString(mac.Sum(nil)), + DeviceID: "WhatsApp Bridge", + InitialDeviceDisplayName: "WhatsApp Bridge", + }) + if err != nil { + return nil, err + } + client, err := mautrix.NewClient(bridge.AS.HomeserverURL, bridge.AS.BotMXID(), resp.AccessToken) + if err != nil { + return nil, err + } + client.DeviceID = "WhatsApp Bridge" + return client, nil +} + +func (helper *CryptoHelper) Decrypt(evt *event.Event) (*event.Event, error) { + return helper.mach.DecryptMegolmEvent(evt) +} + +func (helper *CryptoHelper) Encrypt(roomID id.RoomID, evtType event.Type, content event.Content) (*event.EncryptedEventContent, error) { + encrypted, err := helper.mach.EncryptMegolmEvent(roomID, evtType, content) + if err != nil { + if err != crypto.SessionExpired && err != crypto.SessionNotShared && err != crypto.NoGroupSession { + return nil, err + } + helper.log.Debugfln("Got %v while encrypting event for %s, sharing group session and trying again...", err, roomID) + users, err := helper.bridge.StateStore.GetRoomMemberList(roomID) + if err != nil { + return nil, errors.Wrap(err, "failed to get room member list") + } + err = helper.mach.ShareGroupSession(roomID, users) + if err != nil { + return nil, errors.Wrap(err, "failed to share group session") + } + encrypted, err = helper.mach.EncryptMegolmEvent(roomID, evtType, content) + if err != nil { + return nil, errors.Wrap(err, "failed to encrypt event after re-sharing group session") + } + } + return encrypted, nil +} + +func (helper *CryptoHelper) HandleMemberEvent(evt *event.Event) { + helper.mach.HandleMemberEvent(evt) +} + +type cryptoSyncer struct { + *crypto.OlmMachine +} + +func (syncer *cryptoSyncer) ProcessResponse(resp *mautrix.RespSync, since string) error { + syncer.ProcessSyncResponse(resp, since) + return nil +} + +func (syncer *cryptoSyncer) OnFailedSync(_ *mautrix.RespSync, err error) (time.Duration, error) { + syncer.Log.Error("Error /syncing, waiting 10 seconds: %v", err) + return 10 * time.Second, nil +} + +func (syncer *cryptoSyncer) GetFilterJSON(_ id.UserID) *mautrix.Filter { + everything := []event.Type{{Type: "*"}} + return &mautrix.Filter{ + Presence: mautrix.FilterPart{NotTypes: everything}, + AccountData: mautrix.FilterPart{NotTypes: everything}, + Room: mautrix.RoomFilter{ + IncludeLeave: false, + Ephemeral: mautrix.FilterPart{NotTypes: everything}, + AccountData: mautrix.FilterPart{NotTypes: everything}, + State: mautrix.FilterPart{NotTypes: everything}, + Timeline: mautrix.FilterPart{NotTypes: everything}, + }, + } +} + +type cryptoLogger struct { + int maulogger.Logger +} + +func (c *cryptoLogger) Error(message string, args ...interface{}) { + c.int.Errorfln(message, args...) +} + +func (c *cryptoLogger) Warn(message string, args ...interface{}) { + c.int.Warnfln(message, args...) +} + +func (c *cryptoLogger) Debug(message string, args ...interface{}) { + c.int.Debugfln(message, args...) +} + +func (c *cryptoLogger) Trace(message string, args ...interface{}) { + c.int.Logfln(levelTrace, message, args...) +} + +type cryptoStateStore struct { + bridge *Bridge +} + +func (c *cryptoStateStore) IsEncrypted(id id.RoomID) bool { + portal := c.bridge.GetPortalByMXID(id) + if portal != nil { + return portal.Encrypted + } + return false +} + +func (c *cryptoStateStore) FindSharedRooms(id id.UserID) []id.RoomID { + return c.bridge.StateStore.FindSharedRooms(id) +} diff --git a/database/portal.go b/database/portal.go index 6164266..194d9f3 100644 --- a/database/portal.go +++ b/database/portal.go @@ -22,8 +22,9 @@ import ( log "maunium.net/go/maulogger/v2" - "maunium.net/go/mautrix-whatsapp/types" "maunium.net/go/mautrix/id" + + "maunium.net/go/mautrix-whatsapp/types" ) type PortalKey struct { @@ -114,11 +115,12 @@ type Portal struct { Topic string Avatar string AvatarURL id.ContentURI + Encrypted bool } func (portal *Portal) Scan(row Scannable) *Portal { var mxid, avatarURL sql.NullString - err := row.Scan(&portal.Key.JID, &portal.Key.Receiver, &mxid, &portal.Name, &portal.Topic, &portal.Avatar, &avatarURL) + err := row.Scan(&portal.Key.JID, &portal.Key.Receiver, &mxid, &portal.Name, &portal.Topic, &portal.Avatar, &avatarURL, &portal.Encrypted) if err != nil { if err != sql.ErrNoRows { portal.log.Errorln("Database scan failed:", err) @@ -138,8 +140,8 @@ func (portal *Portal) mxidPtr() *id.RoomID { } func (portal *Portal) Insert() { - _, err := portal.db.Exec("INSERT INTO portal VALUES ($1, $2, $3, $4, $5, $6, $7)", - portal.Key.JID, portal.Key.Receiver, portal.mxidPtr(), portal.Name, portal.Topic, portal.Avatar, portal.AvatarURL.String()) + _, err := portal.db.Exec("INSERT INTO portal (jid, receiver, mxid, name, topic, avatar, avatar_url, encrypted) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)", + portal.Key.JID, portal.Key.Receiver, portal.mxidPtr(), portal.Name, portal.Topic, portal.Avatar, portal.AvatarURL.String(), portal.Encrypted) if err != nil { portal.log.Warnfln("Failed to insert %s: %v", portal.Key, err) } @@ -150,8 +152,8 @@ func (portal *Portal) Update() { if len(portal.MXID) > 0 { mxid = &portal.MXID } - _, err := portal.db.Exec("UPDATE portal SET mxid=$1, name=$2, topic=$3, avatar=$4, avatar_url=$5 WHERE jid=$6 AND receiver=$7", - mxid, portal.Name, portal.Topic, portal.Avatar, portal.AvatarURL.String(), portal.Key.JID, portal.Key.Receiver) + _, err := portal.db.Exec("UPDATE portal SET mxid=$1, name=$2, topic=$3, avatar=$4, avatar_url=$5, encrypted=$6 WHERE jid=$7 AND receiver=$8", + mxid, portal.Name, portal.Topic, portal.Avatar, portal.AvatarURL.String(), portal.Encrypted, portal.Key.JID, portal.Key.Receiver) if err != nil { portal.log.Warnfln("Failed to update %s: %v", portal.Key, err) } diff --git a/database/statestore.go b/database/statestore.go index ff2e41f..ac1f0f5 100644 --- a/database/statestore.go +++ b/database/statestore.go @@ -90,6 +90,24 @@ func (store *SQLStateStore) GetRoomMembers(roomID id.RoomID) map[id.UserID]*even return members } +func (store *SQLStateStore) GetRoomMemberList(roomID id.RoomID) (members []id.UserID, err error) { + var rows *sql.Rows + rows, err = store.db.Query("SELECT user_id FROM mx_user_profile WHERE room_id=$1", roomID) + if err != nil { + return + } + for rows.Next() { + var userID id.UserID + err := rows.Scan(&userID) + if err != nil { + store.log.Warnfln("Failed to scan member in %s: %v", roomID, err) + } else { + members = append(members, userID) + } + } + return +} + func (store *SQLStateStore) GetMembership(roomID id.RoomID, userID id.UserID) event.Membership { row := store.db.QueryRow("SELECT membership FROM mx_user_profile WHERE room_id=$1 AND user_id=$2", roomID, userID) membership := event.MembershipLeave @@ -118,6 +136,26 @@ func (store *SQLStateStore) TryGetMember(roomID id.RoomID, userID id.UserID) (*e return &member, err == nil } +func (store *SQLStateStore) FindSharedRooms(userID id.UserID) (rooms []id.RoomID) { + rows, err := store.db.Query(` + SELECT room_id FROM mx_user_profile WHERE user_id=$2 AND portal.encrypted=true + LEFT JOIN portal WHEN portal.mxid=mx_user_profile.room_id`, userID) + if err != nil { + store.log.Warnfln("Failed to query shared rooms with %s: %v", userID, err) + return + } + for rows.Next() { + var roomID id.RoomID + err := rows.Scan(&roomID) + if err != nil { + store.log.Warnfln("Failed to scan room ID: %v", err) + } else { + rooms = append(rooms, roomID) + } + } + return +} + func (store *SQLStateStore) IsInRoom(roomID id.RoomID, userID id.UserID) bool { return store.IsMembership(roomID, userID, "join") } diff --git a/database/upgrades/2020-05-09-add-portal-encrypted-field.go b/database/upgrades/2020-05-09-add-portal-encrypted-field.go new file mode 100644 index 0000000..ef0f963 --- /dev/null +++ b/database/upgrades/2020-05-09-add-portal-encrypted-field.go @@ -0,0 +1,12 @@ +package upgrades + +import ( + "database/sql" +) + +func init() { + upgrades[12] = upgrade{"Add encryption status to portal table", func(tx *sql.Tx, ctx context) error { + _, err := tx.Exec(`ALTER TABLE portal ADD COLUMN encrypted BOOLEAN NOT NULL DEFAULT false`) + return err + }} +} diff --git a/database/upgrades/upgrades.go b/database/upgrades/upgrades.go index dfaec11..3126cc7 100644 --- a/database/upgrades/upgrades.go +++ b/database/upgrades/upgrades.go @@ -28,7 +28,7 @@ type upgrade struct { fn upgradeFunc } -const NumberOfUpgrades = 12 +const NumberOfUpgrades = 13 var upgrades [NumberOfUpgrades]upgrade diff --git a/example-config.yaml b/example-config.yaml index 061f433..6b66025 100644 --- a/example-config.yaml +++ b/example-config.yaml @@ -138,6 +138,18 @@ bridge: # The prefix for commands. Only required in non-management rooms. command_prefix: "!wa" + # End-to-bridge encryption support options. This requires login_shared_secret to be configured + # in order to get a device for the bridge bot. + # + # Additionally, https://github.com/matrix-org/synapse/pull/5758 is required if using a normal + # application service. + encryption: + # Allow encryption, work in group chat rooms with e2ee enabled + allow: false + # Default to encryption, force-enable encryption in all portals the bridge creates + # This will cause the bridge bot to be in private chats for the encryption to work properly. + default: false + # Permissions for using the bridge. # Permitted values: # relaybot - Talk through the relaybot (if enabled), no access otherwise diff --git a/main.go b/main.go index cb9667c..5da6685 100644 --- a/main.go +++ b/main.go @@ -26,6 +26,7 @@ import ( flag "maunium.net/go/mauflag" log "maunium.net/go/maulogger/v2" + "maunium.net/go/mautrix/event" "maunium.net/go/mautrix" "maunium.net/go/mautrix-appservice" @@ -106,6 +107,7 @@ type Bridge struct { Bot *appservice.IntentAPI Formatter *Formatter Relaybot *User + Crypto Crypto usersByMXID map[id.UserID]*User usersByJID map[types.WhatsAppID]*User @@ -120,6 +122,14 @@ type Bridge struct { puppetsLock sync.Mutex } +type Crypto interface { + HandleMemberEvent(*event.Event) + Decrypt(*event.Event) (*event.Event, error) + Encrypt(id.RoomID, event.Type, event.Content) (*event.EncryptedEventContent, error) + Start() + Stop() +} + func NewBridge() *Bridge { bridge := &Bridge{ usersByMXID: make(map[id.UserID]*User), @@ -215,6 +225,11 @@ func (bridge *Bridge) Init() { bridge.Log.Debugln("Initializing Matrix event handler") bridge.MatrixHandler = NewMatrixHandler(bridge) bridge.Formatter = NewFormatter(bridge) + err = bridge.initCrypto() + if err != nil { + bridge.Log.Fatalln("Error initializing end-to-bridge encryption:", err) + os.Exit(19) + } } func (bridge *Bridge) Start() { @@ -235,6 +250,7 @@ func (bridge *Bridge) Start() { bridge.Log.Debugln("Starting event processor") go bridge.EventProcessor.Start() go bridge.UpdateBotProfile() + go bridge.Crypto.Start() go bridge.StartUsers() } @@ -299,6 +315,7 @@ func (bridge *Bridge) StartUsers() { } func (bridge *Bridge) Stop() { + bridge.Crypto.Stop() bridge.AS.Stop() bridge.EventProcessor.Stop() for _, user := range bridge.usersByJID { diff --git a/matrix.go b/matrix.go index 3f30be7..12e1114 100644 --- a/matrix.go +++ b/matrix.go @@ -21,7 +21,6 @@ import ( "strings" "maunium.net/go/maulogger/v2" - "maunium.net/go/mautrix-appservice" "maunium.net/go/mautrix/event" "maunium.net/go/mautrix/format" @@ -43,15 +42,30 @@ func NewMatrixHandler(bridge *Bridge) *MatrixHandler { cmd: NewCommandHandler(bridge), } bridge.EventProcessor.On(event.EventMessage, handler.HandleMessage) + bridge.EventProcessor.On(event.EventEncrypted, handler.HandleEncrypted) bridge.EventProcessor.On(event.EventSticker, handler.HandleMessage) bridge.EventProcessor.On(event.EventRedaction, handler.HandleRedaction) bridge.EventProcessor.On(event.StateMember, handler.HandleMembership) bridge.EventProcessor.On(event.StateRoomName, handler.HandleRoomMetadata) bridge.EventProcessor.On(event.StateRoomAvatar, handler.HandleRoomMetadata) bridge.EventProcessor.On(event.StateTopic, handler.HandleRoomMetadata) + bridge.EventProcessor.On(event.StateEncryption, handler.HandleEncryption) return handler } +func (mx *MatrixHandler) HandleEncryption(evt *event.Event) { + if evt.Content.AsEncryption().Algorithm != id.AlgorithmMegolmV1 { + return + } + portal := mx.bridge.GetPortalByMXID(evt.RoomID) + mx.log.Debugln(portal) + if portal != nil && !portal.Encrypted { + mx.log.Debugfln("%s enabled encryption in %s", evt.Sender, evt.RoomID) + portal.Encrypted = true + portal.Update() + } +} + func (mx *MatrixHandler) HandleBotInvite(evt *event.Event) { intent := mx.as.BotIntent() @@ -115,6 +129,10 @@ func (mx *MatrixHandler) HandleBotInvite(evt *event.Event) { } func (mx *MatrixHandler) HandleMembership(evt *event.Event) { + if mx.bridge.Crypto != nil { + mx.bridge.Crypto.HandleMemberEvent(evt) + } + content := evt.Content.AsMember() if content.Membership == event.MembershipInvite && id.UserID(evt.GetStateKey()) == mx.as.BotMXID() { mx.HandleBotInvite(evt) @@ -125,7 +143,7 @@ func (mx *MatrixHandler) HandleMembership(evt *event.Event) { return } - user := mx.bridge.GetUserByMXID(id.UserID(evt.Sender)) + user := mx.bridge.GetUserByMXID(evt.Sender) if user == nil || !user.Whitelisted || !user.IsConnected() { return } @@ -148,7 +166,7 @@ func (mx *MatrixHandler) HandleMembership(evt *event.Event) { } func (mx *MatrixHandler) HandleRoomMetadata(evt *event.Event) { - user := mx.bridge.GetUserByMXID(id.UserID(evt.Sender)) + user := mx.bridge.GetUserByMXID(evt.Sender) if user == nil || !user.Whitelisted || !user.IsConnected() { return } @@ -176,21 +194,40 @@ func (mx *MatrixHandler) HandleRoomMetadata(evt *event.Event) { } } -func (mx *MatrixHandler) HandleMessage(evt *event.Event) { +func (mx *MatrixHandler) shouldIgnoreEvent(evt *event.Event) bool { if _, isPuppet := mx.bridge.ParsePuppetMXID(evt.Sender); evt.Sender == mx.bridge.Bot.UserID || isPuppet { - return + return true } isCustomPuppet, ok := evt.Content.Raw["net.maunium.whatsapp.puppet"].(bool) if ok && isCustomPuppet && mx.bridge.GetPuppetByCustomMXID(evt.Sender) != nil { + return true + } + user := mx.bridge.GetUserByMXID(evt.Sender) + if !user.RelaybotWhitelisted { + return true + } + return false +} + +func (mx *MatrixHandler) HandleEncrypted(evt *event.Event) { + if mx.shouldIgnoreEvent(evt) || mx.bridge.Crypto == nil { + return + } + + decrypted, err := mx.bridge.Crypto.Decrypt(evt) + if err != nil { + mx.log.Warnln("Failed to decrypt %s: %v", evt.ID, err) + return + } + mx.bridge.EventProcessor.Dispatch(decrypted) +} + +func (mx *MatrixHandler) HandleMessage(evt *event.Event) { + if mx.shouldIgnoreEvent(evt) { return } user := mx.bridge.GetUserByMXID(evt.Sender) - - if !user.RelaybotWhitelisted { - return - } - content := evt.Content.AsMessage() if user.Whitelisted && content.MsgType == event.MsgText { commandPrefix := mx.bridge.Config.Bridge.CommandPrefix @@ -215,7 +252,7 @@ func (mx *MatrixHandler) HandleRedaction(evt *event.Event) { return } - user := mx.bridge.GetUserByMXID(id.UserID(evt.Sender)) + user := mx.bridge.GetUserByMXID(evt.Sender) if !user.Whitelisted { return diff --git a/nocrypto.go b/nocrypto.go new file mode 100644 index 0000000..0479daf --- /dev/null +++ b/nocrypto.go @@ -0,0 +1,26 @@ +// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge. +// Copyright (C) 2020 Tulir Asokan +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +// +build !cgo + +package main + +func (bridge *Bridge) initCrypto() error { + if !bridge.Config.Bridge.Encryption.Allow { + bridge.Log.Warnln("Bridge built without end-to-bridge encryption, but encryption is enabled in config") + } + bridge.Log.Debugln("Bridge built without end-to-bridge encryption") +} diff --git a/portal.go b/portal.go index 1fb6a7a..87723fc 100644 --- a/portal.go +++ b/portal.go @@ -35,6 +35,7 @@ import ( "time" "github.com/chai2010/webp" + "github.com/pkg/errors" log "maunium.net/go/maulogger/v2" "github.com/Rhymen/go-whatsapp" @@ -908,6 +909,32 @@ func (portal *Portal) HandleFakeMessage(source *User, message FakeMessage) { portal.recentlyHandled[index] = message.ID } +func (portal *Portal) sendMainIntentMessage(content interface{}) (*mautrix.RespSendEvent, error) { + return portal.sendMessage(portal.MainIntent(), event.EventMessage, content, 0) +} + +func (portal *Portal) sendMessage(intent *appservice.IntentAPI, eventType event.Type, content interface{}, timestamp int64) (*mautrix.RespSendEvent, error) { + wrappedContent := event.Content{Parsed: content} + if timestamp != 0 && intent.IsCustomPuppet { + wrappedContent.Raw = map[string]interface{}{ + "net.maunium.whatsapp.puppet": intent.IsCustomPuppet, + } + } + if portal.Encrypted && portal.bridge.Crypto != nil { + encrypted, err := portal.bridge.Crypto.Encrypt(portal.MXID, eventType, wrappedContent) + if err != nil { + return nil, errors.Wrap(err, "failed to encrypt event") + } + eventType = event.EventEncrypted + wrappedContent.Parsed = encrypted + } + if timestamp == 0 { + return intent.SendMessageEvent(portal.MXID, eventType, &wrappedContent) + } else { + return intent.SendMassagedMessageEvent(portal.MXID, eventType, &wrappedContent, timestamp) + } +} + func (portal *Portal) HandleTextMessage(source *User, message whatsapp.TextMessage) { if !portal.startHandling(message.Info) { return @@ -927,12 +954,7 @@ func (portal *Portal) HandleTextMessage(source *User, message whatsapp.TextMessa portal.SetReply(content, message.ContextInfo) _, _ = intent.UserTyping(portal.MXID, false, 0) - resp, err := intent.SendMassagedMessageEvent(portal.MXID, event.EventMessage, &event.Content{ - Parsed: content, - Raw: map[string]interface{}{ - "net.maunium.whatsapp.puppet": intent.IsCustomPuppet, - }, - }, int64(message.Info.Timestamp*1000)) + resp, err := portal.sendMessage(intent, event.EventMessage, content, int64(message.Info.Timestamp*1000)) if err != nil { portal.log.Errorfln("Failed to handle message %s: %v", message.Info.Id, err) return @@ -1042,12 +1064,7 @@ func (portal *Portal) HandleMediaMessage(source *User, download func() ([]byte, if sendAsSticker { eventType = event.EventSticker } - resp, err := intent.SendMassagedMessageEvent(portal.MXID, eventType, &event.Content{ - Parsed: content, - Raw: map[string]interface{}{ - "net.maunium.whatsapp.puppet": intent.IsCustomPuppet, - }, - }, ts) + resp, err := portal.sendMessage(intent, eventType, content, ts) if err != nil { portal.log.Errorfln("Failed to handle message %s: %v", info.Id, err) return @@ -1061,12 +1078,7 @@ func (portal *Portal) HandleMediaMessage(source *User, download func() ([]byte, portal.bridge.Formatter.ParseWhatsApp(captionContent) - _, err := intent.SendMassagedMessageEvent(portal.MXID, event.EventMessage, &event.Content{ - Parsed: content, - Raw: map[string]interface{}{ - "net.maunium.whatsapp.puppet": intent.IsCustomPuppet, - }, - }, ts) + _, err := portal.sendMessage(intent, event.EventMessage, content, ts) if err != nil { portal.log.Warnfln("Failed to handle caption of message %s: %v", info.Id, err) } @@ -1178,7 +1190,7 @@ func (portal *Portal) sendMatrixConnectionError(sender *User, eventID id.EventID } msg := format.RenderMarkdown("\u26a0 You are not connected to WhatsApp, so your message was not bridged. " + reconnect, true, false) msg.MsgType = event.MsgNotice - _, err := portal.MainIntent().SendMessageEvent(portal.MXID, event.EventMessage, msg) + _, err := portal.sendMainIntentMessage(msg) if err != nil { portal.log.Errorln("Failed to send bridging failure message:", err) } @@ -1353,7 +1365,7 @@ func (portal *Portal) HandleMatrixMessage(sender *User, evt *event.Event) { portal.log.Errorfln("Error handling Matrix event %s: %v", evt.ID, err) msg := format.RenderMarkdown(fmt.Sprintf("\u26a0 Your message may not have been bridged: %v", err), false, false) msg.MsgType = event.MsgNotice - _, err := portal.MainIntent().SendMessageEvent(portal.MXID, event.EventMessage, msg) + _, err := portal.sendMainIntentMessage(msg) if err != nil { portal.log.Errorln("Failed to send bridging failure message:", err) }