Browse Source

Add basic end-to-bridge encryption support

Still missing persisting sync tokens and crypto state in DB
Tulir Asokan 5 years ago
parent
commit
baae66ed04
12 changed files with 457 additions and 35 deletions
  1. 42 0
      commands.go
  2. 5 0
      config/bridge.go
  3. 219 0
      crypto.go
  4. 8 6
      database/portal.go
  5. 38 0
      database/statestore.go
  6. 12 0
      database/upgrades/2020-05-09-add-portal-encrypted-field.go
  7. 1 1
      database/upgrades/upgrades.go
  8. 12 0
      example-config.yaml
  9. 17 0
      main.go
  10. 45 8
      matrix.go
  11. 26 0
      nocrypto.go
  12. 32 20
      portal.go

+ 42 - 0
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] <level>`")
+		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

+ 5 - 0
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"`

+ 219 - 0
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 <https://www.gnu.org/licenses/>.
+
+// +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)
+}

+ 8 - 6
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)
 	}

+ 38 - 0
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")
 }

+ 12 - 0
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
+	}}
+}

+ 1 - 1
database/upgrades/upgrades.go

@@ -28,7 +28,7 @@ type upgrade struct {
 	fn      upgradeFunc
 }
 
-const NumberOfUpgrades = 12
+const NumberOfUpgrades = 13
 
 var upgrades [NumberOfUpgrades]upgrade
 

+ 12 - 0
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

+ 17 - 0
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 {

+ 45 - 8
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
 	}
 
-	user := mx.bridge.GetUserByMXID(evt.Sender)
+	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)
+}
 
-	if !user.RelaybotWhitelisted {
+func (mx *MatrixHandler) HandleMessage(evt *event.Event) {
+	if mx.shouldIgnoreEvent(evt) {
 		return
 	}
 
+	user := mx.bridge.GetUserByMXID(evt.Sender)
 	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

+ 26 - 0
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 <https://www.gnu.org/licenses/>.
+
+// +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")
+}

+ 32 - 20
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)
 		}