Add basic end-to-bridge encryption support

Still missing persisting sync tokens and crypto state in DB
This commit is contained in:
Tulir Asokan 2020-05-09 02:03:59 +03:00
parent edd91510f1
commit baae66ed04
12 changed files with 460 additions and 38 deletions

View file

@ -18,6 +18,7 @@ package main
import ( import (
"fmt" "fmt"
"strconv"
"strings" "strings"
"github.com/Rhymen/go-whatsapp" "github.com/Rhymen/go-whatsapp"
@ -118,6 +119,8 @@ func (handler *CommandHandler) CommandMux(ce *CommandEvent) {
handler.CommandDeleteAllPortals(ce) handler.CommandDeleteAllPortals(ce)
case "dev-test": case "dev-test":
handler.CommandDevTest(ce) handler.CommandDevTest(ce)
case "set-pl":
handler.CommandSetPowerLevel(ce)
case "login-matrix", "logout", "sync", "list", "open", "pm": case "login-matrix", "logout", "sync", "list", "open", "pm":
if !ce.User.HasSession() { if !ce.User.HasSession() {
ce.Reply("You are not logged in. Use the `login` command to log into WhatsApp.") 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` const cmdLoginHelp = `login - Authenticate this Bridge as WhatsApp Web Client`
// CommandLogin handles login command // CommandLogin handles login command

View file

@ -64,6 +64,11 @@ type BridgeConfig struct {
CommandPrefix string `yaml:"command_prefix"` CommandPrefix string `yaml:"command_prefix"`
Encryption struct {
Allow bool `yaml:"allow"`
Default bool `yaml:"default"`
} `yaml:"encryption"`
Permissions PermissionConfig `yaml:"permissions"` Permissions PermissionConfig `yaml:"permissions"`
Relaybot RelaybotConfig `yaml:"relaybot"` Relaybot RelaybotConfig `yaml:"relaybot"`

219
crypto.go Normal file
View file

@ -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)
}

View file

@ -22,8 +22,9 @@ import (
log "maunium.net/go/maulogger/v2" log "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix-whatsapp/types"
"maunium.net/go/mautrix/id" "maunium.net/go/mautrix/id"
"maunium.net/go/mautrix-whatsapp/types"
) )
type PortalKey struct { type PortalKey struct {
@ -114,11 +115,12 @@ type Portal struct {
Topic string Topic string
Avatar string Avatar string
AvatarURL id.ContentURI AvatarURL id.ContentURI
Encrypted bool
} }
func (portal *Portal) Scan(row Scannable) *Portal { func (portal *Portal) Scan(row Scannable) *Portal {
var mxid, avatarURL sql.NullString 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 != nil {
if err != sql.ErrNoRows { if err != sql.ErrNoRows {
portal.log.Errorln("Database scan failed:", err) portal.log.Errorln("Database scan failed:", err)
@ -138,8 +140,8 @@ func (portal *Portal) mxidPtr() *id.RoomID {
} }
func (portal *Portal) Insert() { func (portal *Portal) Insert() {
_, err := portal.db.Exec("INSERT INTO portal VALUES ($1, $2, $3, $4, $5, $6, $7)", _, 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.Key.JID, portal.Key.Receiver, portal.mxidPtr(), portal.Name, portal.Topic, portal.Avatar, portal.AvatarURL.String(), portal.Encrypted)
if err != nil { if err != nil {
portal.log.Warnfln("Failed to insert %s: %v", portal.Key, err) portal.log.Warnfln("Failed to insert %s: %v", portal.Key, err)
} }
@ -150,8 +152,8 @@ func (portal *Portal) Update() {
if len(portal.MXID) > 0 { if len(portal.MXID) > 0 {
mxid = &portal.MXID 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", _, 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.Key.JID, portal.Key.Receiver) mxid, portal.Name, portal.Topic, portal.Avatar, portal.AvatarURL.String(), portal.Encrypted, portal.Key.JID, portal.Key.Receiver)
if err != nil { if err != nil {
portal.log.Warnfln("Failed to update %s: %v", portal.Key, err) portal.log.Warnfln("Failed to update %s: %v", portal.Key, err)
} }

View file

@ -90,6 +90,24 @@ func (store *SQLStateStore) GetRoomMembers(roomID id.RoomID) map[id.UserID]*even
return members 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 { 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) row := store.db.QueryRow("SELECT membership FROM mx_user_profile WHERE room_id=$1 AND user_id=$2", roomID, userID)
membership := event.MembershipLeave membership := event.MembershipLeave
@ -118,6 +136,26 @@ func (store *SQLStateStore) TryGetMember(roomID id.RoomID, userID id.UserID) (*e
return &member, err == nil 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 { func (store *SQLStateStore) IsInRoom(roomID id.RoomID, userID id.UserID) bool {
return store.IsMembership(roomID, userID, "join") return store.IsMembership(roomID, userID, "join")
} }

View file

@ -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
}}
}

View file

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

View file

@ -138,6 +138,18 @@ bridge:
# The prefix for commands. Only required in non-management rooms. # The prefix for commands. Only required in non-management rooms.
command_prefix: "!wa" 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. # Permissions for using the bridge.
# Permitted values: # Permitted values:
# relaybot - Talk through the relaybot (if enabled), no access otherwise # relaybot - Talk through the relaybot (if enabled), no access otherwise

17
main.go
View file

@ -26,6 +26,7 @@ import (
flag "maunium.net/go/mauflag" flag "maunium.net/go/mauflag"
log "maunium.net/go/maulogger/v2" log "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix" "maunium.net/go/mautrix"
"maunium.net/go/mautrix-appservice" "maunium.net/go/mautrix-appservice"
@ -106,6 +107,7 @@ type Bridge struct {
Bot *appservice.IntentAPI Bot *appservice.IntentAPI
Formatter *Formatter Formatter *Formatter
Relaybot *User Relaybot *User
Crypto Crypto
usersByMXID map[id.UserID]*User usersByMXID map[id.UserID]*User
usersByJID map[types.WhatsAppID]*User usersByJID map[types.WhatsAppID]*User
@ -120,6 +122,14 @@ type Bridge struct {
puppetsLock sync.Mutex 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 { func NewBridge() *Bridge {
bridge := &Bridge{ bridge := &Bridge{
usersByMXID: make(map[id.UserID]*User), usersByMXID: make(map[id.UserID]*User),
@ -215,6 +225,11 @@ func (bridge *Bridge) Init() {
bridge.Log.Debugln("Initializing Matrix event handler") bridge.Log.Debugln("Initializing Matrix event handler")
bridge.MatrixHandler = NewMatrixHandler(bridge) bridge.MatrixHandler = NewMatrixHandler(bridge)
bridge.Formatter = NewFormatter(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() { func (bridge *Bridge) Start() {
@ -235,6 +250,7 @@ func (bridge *Bridge) Start() {
bridge.Log.Debugln("Starting event processor") bridge.Log.Debugln("Starting event processor")
go bridge.EventProcessor.Start() go bridge.EventProcessor.Start()
go bridge.UpdateBotProfile() go bridge.UpdateBotProfile()
go bridge.Crypto.Start()
go bridge.StartUsers() go bridge.StartUsers()
} }
@ -299,6 +315,7 @@ func (bridge *Bridge) StartUsers() {
} }
func (bridge *Bridge) Stop() { func (bridge *Bridge) Stop() {
bridge.Crypto.Stop()
bridge.AS.Stop() bridge.AS.Stop()
bridge.EventProcessor.Stop() bridge.EventProcessor.Stop()
for _, user := range bridge.usersByJID { for _, user := range bridge.usersByJID {

View file

@ -21,7 +21,6 @@ import (
"strings" "strings"
"maunium.net/go/maulogger/v2" "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix-appservice" "maunium.net/go/mautrix-appservice"
"maunium.net/go/mautrix/event" "maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/format" "maunium.net/go/mautrix/format"
@ -43,15 +42,30 @@ func NewMatrixHandler(bridge *Bridge) *MatrixHandler {
cmd: NewCommandHandler(bridge), cmd: NewCommandHandler(bridge),
} }
bridge.EventProcessor.On(event.EventMessage, handler.HandleMessage) 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.EventSticker, handler.HandleMessage)
bridge.EventProcessor.On(event.EventRedaction, handler.HandleRedaction) bridge.EventProcessor.On(event.EventRedaction, handler.HandleRedaction)
bridge.EventProcessor.On(event.StateMember, handler.HandleMembership) bridge.EventProcessor.On(event.StateMember, handler.HandleMembership)
bridge.EventProcessor.On(event.StateRoomName, handler.HandleRoomMetadata) bridge.EventProcessor.On(event.StateRoomName, handler.HandleRoomMetadata)
bridge.EventProcessor.On(event.StateRoomAvatar, handler.HandleRoomMetadata) bridge.EventProcessor.On(event.StateRoomAvatar, handler.HandleRoomMetadata)
bridge.EventProcessor.On(event.StateTopic, handler.HandleRoomMetadata) bridge.EventProcessor.On(event.StateTopic, handler.HandleRoomMetadata)
bridge.EventProcessor.On(event.StateEncryption, handler.HandleEncryption)
return handler 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) { func (mx *MatrixHandler) HandleBotInvite(evt *event.Event) {
intent := mx.as.BotIntent() intent := mx.as.BotIntent()
@ -115,6 +129,10 @@ func (mx *MatrixHandler) HandleBotInvite(evt *event.Event) {
} }
func (mx *MatrixHandler) HandleMembership(evt *event.Event) { func (mx *MatrixHandler) HandleMembership(evt *event.Event) {
if mx.bridge.Crypto != nil {
mx.bridge.Crypto.HandleMemberEvent(evt)
}
content := evt.Content.AsMember() content := evt.Content.AsMember()
if content.Membership == event.MembershipInvite && id.UserID(evt.GetStateKey()) == mx.as.BotMXID() { if content.Membership == event.MembershipInvite && id.UserID(evt.GetStateKey()) == mx.as.BotMXID() {
mx.HandleBotInvite(evt) mx.HandleBotInvite(evt)
@ -125,7 +143,7 @@ func (mx *MatrixHandler) HandleMembership(evt *event.Event) {
return return
} }
user := mx.bridge.GetUserByMXID(id.UserID(evt.Sender)) user := mx.bridge.GetUserByMXID(evt.Sender)
if user == nil || !user.Whitelisted || !user.IsConnected() { if user == nil || !user.Whitelisted || !user.IsConnected() {
return return
} }
@ -148,7 +166,7 @@ func (mx *MatrixHandler) HandleMembership(evt *event.Event) {
} }
func (mx *MatrixHandler) HandleRoomMetadata(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() { if user == nil || !user.Whitelisted || !user.IsConnected() {
return 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 { 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) isCustomPuppet, ok := evt.Content.Raw["net.maunium.whatsapp.puppet"].(bool)
if ok && isCustomPuppet && mx.bridge.GetPuppetByCustomMXID(evt.Sender) != nil { 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 return
} }
user := mx.bridge.GetUserByMXID(evt.Sender) user := mx.bridge.GetUserByMXID(evt.Sender)
if !user.RelaybotWhitelisted {
return
}
content := evt.Content.AsMessage() content := evt.Content.AsMessage()
if user.Whitelisted && content.MsgType == event.MsgText { if user.Whitelisted && content.MsgType == event.MsgText {
commandPrefix := mx.bridge.Config.Bridge.CommandPrefix commandPrefix := mx.bridge.Config.Bridge.CommandPrefix
@ -215,7 +252,7 @@ func (mx *MatrixHandler) HandleRedaction(evt *event.Event) {
return return
} }
user := mx.bridge.GetUserByMXID(id.UserID(evt.Sender)) user := mx.bridge.GetUserByMXID(evt.Sender)
if !user.Whitelisted { if !user.Whitelisted {
return return

26
nocrypto.go Normal file
View file

@ -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")
}

View file

@ -35,6 +35,7 @@ import (
"time" "time"
"github.com/chai2010/webp" "github.com/chai2010/webp"
"github.com/pkg/errors"
log "maunium.net/go/maulogger/v2" log "maunium.net/go/maulogger/v2"
"github.com/Rhymen/go-whatsapp" "github.com/Rhymen/go-whatsapp"
@ -908,6 +909,32 @@ func (portal *Portal) HandleFakeMessage(source *User, message FakeMessage) {
portal.recentlyHandled[index] = message.ID 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) { func (portal *Portal) HandleTextMessage(source *User, message whatsapp.TextMessage) {
if !portal.startHandling(message.Info) { if !portal.startHandling(message.Info) {
return return
@ -927,12 +954,7 @@ func (portal *Portal) HandleTextMessage(source *User, message whatsapp.TextMessa
portal.SetReply(content, message.ContextInfo) portal.SetReply(content, message.ContextInfo)
_, _ = intent.UserTyping(portal.MXID, false, 0) _, _ = intent.UserTyping(portal.MXID, false, 0)
resp, err := intent.SendMassagedMessageEvent(portal.MXID, event.EventMessage, &event.Content{ resp, err := portal.sendMessage(intent, event.EventMessage, content, int64(message.Info.Timestamp*1000))
Parsed: content,
Raw: map[string]interface{}{
"net.maunium.whatsapp.puppet": intent.IsCustomPuppet,
},
}, int64(message.Info.Timestamp*1000))
if err != nil { if err != nil {
portal.log.Errorfln("Failed to handle message %s: %v", message.Info.Id, err) portal.log.Errorfln("Failed to handle message %s: %v", message.Info.Id, err)
return return
@ -1042,12 +1064,7 @@ func (portal *Portal) HandleMediaMessage(source *User, download func() ([]byte,
if sendAsSticker { if sendAsSticker {
eventType = event.EventSticker eventType = event.EventSticker
} }
resp, err := intent.SendMassagedMessageEvent(portal.MXID, eventType, &event.Content{ resp, err := portal.sendMessage(intent, eventType, content, ts)
Parsed: content,
Raw: map[string]interface{}{
"net.maunium.whatsapp.puppet": intent.IsCustomPuppet,
},
}, ts)
if err != nil { if err != nil {
portal.log.Errorfln("Failed to handle message %s: %v", info.Id, err) portal.log.Errorfln("Failed to handle message %s: %v", info.Id, err)
return return
@ -1061,12 +1078,7 @@ func (portal *Portal) HandleMediaMessage(source *User, download func() ([]byte,
portal.bridge.Formatter.ParseWhatsApp(captionContent) portal.bridge.Formatter.ParseWhatsApp(captionContent)
_, err := intent.SendMassagedMessageEvent(portal.MXID, event.EventMessage, &event.Content{ _, err := portal.sendMessage(intent, event.EventMessage, content, ts)
Parsed: content,
Raw: map[string]interface{}{
"net.maunium.whatsapp.puppet": intent.IsCustomPuppet,
},
}, ts)
if err != nil { if err != nil {
portal.log.Warnfln("Failed to handle caption of message %s: %v", info.Id, err) 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 := format.RenderMarkdown("\u26a0 You are not connected to WhatsApp, so your message was not bridged. " + reconnect, true, false)
msg.MsgType = event.MsgNotice msg.MsgType = event.MsgNotice
_, err := portal.MainIntent().SendMessageEvent(portal.MXID, event.EventMessage, msg) _, err := portal.sendMainIntentMessage(msg)
if err != nil { if err != nil {
portal.log.Errorln("Failed to send bridging failure message:", err) 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) 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 := format.RenderMarkdown(fmt.Sprintf("\u26a0 Your message may not have been bridged: %v", err), false, false)
msg.MsgType = event.MsgNotice msg.MsgType = event.MsgNotice
_, err := portal.MainIntent().SendMessageEvent(portal.MXID, event.EventMessage, msg) _, err := portal.sendMainIntentMessage(msg)
if err != nil { if err != nil {
portal.log.Errorln("Failed to send bridging failure message:", err) portal.log.Errorln("Failed to send bridging failure message:", err)
} }