diff --git a/bridge/attachments.go b/bridge/attachments.go index 206cf6e..c3e8569 100644 --- a/bridge/attachments.go +++ b/bridge/attachments.go @@ -9,6 +9,7 @@ import ( "github.com/bwmarrin/discordgo" + "maunium.net/go/mautrix" "maunium.net/go/mautrix/appservice" "maunium.net/go/mautrix/event" "maunium.net/go/mautrix/id" @@ -76,13 +77,26 @@ func (p *Portal) downloadMatrixAttachment(eventID id.EventID, content *event.Mes } func (p *Portal) uploadMatrixAttachment(intent *appservice.IntentAPI, data []byte, content *event.MessageEventContent) error { - uploaded, err := intent.UploadBytes(data, content.Info.MimeType) - if err != nil { - return err + req := mautrix.ReqUploadMedia{ + ContentBytes: data, + ContentType: content.Info.MimeType, + } + var mxc id.ContentURI + if p.bridge.Config.Homeserver.AsyncMedia { + uploaded, err := intent.UnstableUploadAsync(req) + if err != nil { + return err + } + mxc = uploaded.ContentURI + } else { + uploaded, err := intent.UploadMedia(req) + if err != nil { + return err + } + mxc = uploaded.ContentURI } - content.URL = uploaded.ContentURI.CUString() - + content.URL = mxc.CUString() content.Info.Size = len(data) if content.Info.Width == 0 && content.Info.Height == 0 && strings.HasPrefix(content.Info.MimeType, "image/") { diff --git a/bridge/bridge.go b/bridge/bridge.go index 07e0653..720ea83 100644 --- a/bridge/bridge.go +++ b/bridge/bridge.go @@ -48,6 +48,8 @@ type Bridge struct { puppetsLock sync.Mutex StateStore *database.SQLStateStore + + crypto Crypto } func New(cfg *config.Config) (*Bridge, error) { @@ -104,6 +106,8 @@ func New(cfg *config.Config) (*Bridge, error) { StateStore: stateStore, } + bridge.crypto = NewCryptoHelper(bridge) + if cfg.Appservice.Provisioning.Enabled() { bridge.provisioning = newProvisioningAPI(bridge) } @@ -151,6 +155,13 @@ func (b *Bridge) Start() error { return err } + if b.crypto != nil { + if err := b.crypto.Init(); err != nil { + b.log.Fatalln("Error initializing end-to-bridge encryption:", err) + return err + } + } + b.log.Debugln("Starting application service HTTP server") go b.as.Start() @@ -159,6 +170,10 @@ func (b *Bridge) Start() error { go b.updateBotProfile() + if b.crypto != nil { + go b.crypto.Start() + } + go b.startUsers() // Finally tell the appservice we're ready @@ -168,5 +183,21 @@ func (b *Bridge) Start() error { } func (b *Bridge) Stop() { + if b.crypto != nil { + b.crypto.Stop() + } + + b.as.Stop() + b.eventProcessor.Stop() + + for _, user := range b.usersByMXID { + if user.Session == nil { + continue + } + + b.log.Debugln("Disconnecting", user.MXID) + user.Session.Close() + } + b.log.Infoln("Bridge stopped") } diff --git a/bridge/commands.go b/bridge/commands.go index ed94503..25b707e 100644 --- a/bridge/commands.go +++ b/bridge/commands.go @@ -306,7 +306,7 @@ func (m *pingMatrixCmd) Run(g *globals) error { type guildsCmd struct { Status guildStatusCmd `kong:"cmd,help='Show the bridge status for the guilds you are in'"` Bridge guildBridgeCmd `kong:"cmd,help='Bridge a guild'"` - Unbridge guildUnbridgeCmd `kong:"cmd,help="Unbridge a guild'"` + Unbridge guildUnbridgeCmd `kong:"cmd,help='Unbridge a guild'"` } type guildStatusCmd struct{} diff --git a/bridge/crypto.go b/bridge/crypto.go new file mode 100644 index 0000000..343eedb --- /dev/null +++ b/bridge/crypto.go @@ -0,0 +1,339 @@ +package bridge + +import ( + "fmt" + "runtime/debug" + "time" + + "maunium.net/go/maulogger/v2" + + "maunium.net/go/mautrix" + "maunium.net/go/mautrix/crypto" + "maunium.net/go/mautrix/event" + "maunium.net/go/mautrix/id" + + "go.mau.fi/mautrix-discord/database" +) + +var NoSessionFound = crypto.NoSessionFound + +var levelTrace = maulogger.Level{ + Name: "TRACE", + Severity: -10, + Color: -1, +} + +type Crypto interface { + HandleMemberEvent(*event.Event) + Decrypt(*event.Event) (*event.Event, error) + Encrypt(id.RoomID, event.Type, event.Content) (*event.EncryptedEventContent, error) + WaitForSession(id.RoomID, id.SenderKey, id.SessionID, time.Duration) bool + RequestSession(id.RoomID, id.SenderKey, id.SessionID, id.UserID, id.DeviceID) + ResetSession(id.RoomID) + Init() error + Start() + Stop() +} + +type CryptoHelper struct { + bridge *Bridge + client *mautrix.Client + mach *crypto.OlmMachine + store *database.SQLCryptoStore + log maulogger.Logger + baseLog maulogger.Logger +} + +func NewCryptoHelper(bridge *Bridge) Crypto { + if !bridge.Config.Bridge.Encryption.Allow { + bridge.log.Debugln("Bridge built with end-to-bridge encryption, but disabled in config") + return nil + } + + baseLog := bridge.log.Sub("Crypto") + return &CryptoHelper{ + bridge: bridge, + log: baseLog.Sub("Helper"), + baseLog: baseLog, + } +} + +func (helper *CryptoHelper) Init() error { + helper.log.Debugln("Initializing end-to-bridge encryption...") + + helper.store = database.NewSQLCryptoStore(helper.bridge.db, helper.bridge.as.BotMXID(), + fmt.Sprintf("@%s:%s", helper.bridge.Config.Bridge.FormatUsername("%"), helper.bridge.as.HomeserverDomain)) + + var err error + helper.client, err = helper.loginBot() + if err != nil { + return err + } + + helper.log.Debugln("Logged in as bridge bot with device ID", helper.client.DeviceID) + + logger := &cryptoLogger{helper.baseLog} + stateStore := &cryptoStateStore{helper.bridge} + helper.mach = crypto.NewOlmMachine(helper.client, logger, helper.store, stateStore) + helper.mach.AllowKeyShare = helper.allowKeyShare + + helper.client.Syncer = &cryptoSyncer{helper.mach} + helper.client.Store = &cryptoClientStore{helper.store} + + return helper.mach.Load() +} + +func (helper *CryptoHelper) allowKeyShare(device *crypto.DeviceIdentity, info event.RequestedKeyInfo) *crypto.KeyShareRejection { + cfg := helper.bridge.Config.Bridge.Encryption.KeySharing + if !cfg.Allow { + return &crypto.KeyShareRejectNoResponse + } else if device.Trust == crypto.TrustStateBlacklisted { + return &crypto.KeyShareRejectBlacklisted + } else if device.Trust == crypto.TrustStateVerified || !cfg.RequireVerification { + portal := helper.bridge.GetPortalByMXID(info.RoomID) + if portal == nil { + helper.log.Debugfln("Rejecting key request for %s from %s/%s: room is not a portal", info.SessionID, device.UserID, device.DeviceID) + + return &crypto.KeyShareRejection{Code: event.RoomKeyWithheldUnavailable, Reason: "Requested room is not a portal room"} + } + user := helper.bridge.GetUserByMXID(device.UserID) + // FIXME reimplement IsInPortal + if !user.Admin /*&& !user.IsInPortal(portal.Key)*/ { + helper.log.Debugfln("Rejecting key request for %s from %s/%s: user is not in portal", info.SessionID, device.UserID, device.DeviceID) + + return &crypto.KeyShareRejection{Code: event.RoomKeyWithheldUnauthorized, Reason: "You're not in that portal"} + } + helper.log.Debugfln("Accepting key request for %s from %s/%s", info.SessionID, device.UserID, device.DeviceID) + + return nil + } + + return &crypto.KeyShareRejectUnverified +} + +func (helper *CryptoHelper) loginBot() (*mautrix.Client, error) { + deviceID := helper.store.FindDeviceID() + if len(deviceID) > 0 { + helper.log.Debugln("Found existing device ID for bot in database:", deviceID) + } + + client, err := mautrix.NewClient(helper.bridge.as.HomeserverURL, "", "") + if err != nil { + return nil, fmt.Errorf("failed to initialize client: %w", err) + } + + client.Logger = helper.baseLog.Sub("Bot") + client.Client = helper.bridge.as.HTTPClient + client.DefaultHTTPRetries = helper.bridge.as.DefaultHTTPRetries + flows, err := client.GetLoginFlows() + if err != nil { + return nil, fmt.Errorf("failed to get supported login flows: %w", err) + } + + flow := flows.FirstFlowOfType(mautrix.AuthTypeAppservice, mautrix.AuthTypeHalfyAppservice) + if flow == nil { + return nil, fmt.Errorf("homeserver does not support appservice login") + } + + // We set the API token to the AS token here to authenticate the appservice login + // It'll get overridden after the login + client.AccessToken = helper.bridge.as.Registration.AppToken + resp, err := client.Login(&mautrix.ReqLogin{ + Type: flow.Type, + Identifier: mautrix.UserIdentifier{Type: mautrix.IdentifierTypeUser, User: string(helper.bridge.as.BotMXID())}, + DeviceID: deviceID, + InitialDeviceDisplayName: "Discord Bridge", + StoreCredentials: true, + }) + if err != nil { + return nil, fmt.Errorf("failed to log in as bridge bot: %w", err) + } + + helper.store.DeviceID = resp.DeviceID + + return client, 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) + } else { + helper.log.Infoln("Bridge bot to-device syncer stopped without error") + } +} + +func (helper *CryptoHelper) Stop() { + helper.log.Debugln("CryptoHelper.Stop() called, stopping bridge bot sync") + helper.client.StopSync() +} + +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.store.GetRoomMembers(roomID) + if err != nil { + return nil, fmt.Errorf("failed to get room member list: %w", err) + } + + err = helper.mach.ShareGroupSession(roomID, users) + if err != nil { + return nil, fmt.Errorf("failed to share group session: %w", err) + } + + encrypted, err = helper.mach.EncryptMegolmEvent(roomID, evtType, &content) + if err != nil { + return nil, fmt.Errorf("failed to encrypt event after re-sharing group session: %w", err) + } + } + + return encrypted, nil +} + +func (helper *CryptoHelper) WaitForSession(roomID id.RoomID, senderKey id.SenderKey, sessionID id.SessionID, timeout time.Duration) bool { + return helper.mach.WaitForSession(roomID, senderKey, sessionID, timeout) +} + +func (helper *CryptoHelper) RequestSession(roomID id.RoomID, senderKey id.SenderKey, sessionID id.SessionID, userID id.UserID, deviceID id.DeviceID) { + err := helper.mach.SendRoomKeyRequest(roomID, senderKey, sessionID, "", map[id.UserID][]id.DeviceID{userID: {deviceID}}) + if err != nil { + helper.log.Warnfln("Failed to send key request to %s/%s for %s in %s: %v", userID, deviceID, sessionID, roomID, err) + } else { + helper.log.Debugfln("Sent key request to %s/%s for %s in %s", userID, deviceID, sessionID, roomID) + } +} + +func (helper *CryptoHelper) ResetSession(roomID id.RoomID) { + err := helper.mach.CryptoStore.RemoveOutboundGroupSession(roomID) + if err != nil { + helper.log.Debugfln("Error manually removing outbound group session in %s: %v", roomID, err) + } +} + +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 { + done := make(chan struct{}) + go func() { + defer func() { + if err := recover(); err != nil { + syncer.Log.Error("Processing sync response (%s) panicked: %v\n%s", since, err, debug.Stack()) + } + done <- struct{}{} + }() + syncer.Log.Trace("Starting sync response handling (%s)", since) + syncer.ProcessSyncResponse(resp, since) + syncer.Log.Trace("Successfully handled sync response (%s)", since) + }() + + select { + case <-done: + case <-time.After(30 * time.Second): + syncer.Log.Warn("Handling sync response (%s) is taking unusually long", 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 cryptoClientStore struct { + int *database.SQLCryptoStore +} + +func (c cryptoClientStore) SaveFilterID(_ id.UserID, _ string) {} +func (c cryptoClientStore) LoadFilterID(_ id.UserID) string { return "" } +func (c cryptoClientStore) SaveRoom(_ *mautrix.Room) {} +func (c cryptoClientStore) LoadRoom(_ id.RoomID) *mautrix.Room { return nil } + +func (c cryptoClientStore) SaveNextBatch(_ id.UserID, nextBatchToken string) { + c.int.PutNextBatch(nextBatchToken) +} + +func (c cryptoClientStore) LoadNextBatch(_ id.UserID) string { + return c.int.GetNextBatch() +} + +var _ mautrix.Storer = (*cryptoClientStore)(nil) + +type cryptoStateStore struct { + bridge *Bridge +} + +var _ crypto.StateStore = (*cryptoStateStore)(nil) + +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) +} + +func (c *cryptoStateStore) GetEncryptionEvent(id.RoomID) *event.EncryptionEventContent { + // TODO implement + return nil +} diff --git a/bridge/matrix.go b/bridge/matrix.go index 4e88f18..536c35a 100644 --- a/bridge/matrix.go +++ b/bridge/matrix.go @@ -1,7 +1,10 @@ package bridge import ( + "errors" + "fmt" "strings" + "time" "maunium.net/go/maulogger/v2" "maunium.net/go/mautrix" @@ -29,9 +32,11 @@ func (b *Bridge) setupEvents() { } b.eventProcessor.On(event.EventMessage, b.matrixHandler.handleMessage) + b.eventProcessor.On(event.EventEncrypted, b.matrixHandler.handleEncrypted) b.eventProcessor.On(event.EventReaction, b.matrixHandler.handleReaction) b.eventProcessor.On(event.EventRedaction, b.matrixHandler.handleRedaction) b.eventProcessor.On(event.StateMember, b.matrixHandler.handleMembership) + b.eventProcessor.On(event.StateEncryption, b.matrixHandler.handleEncryption) } func (mh *matrixHandler) join(evt *event.Event, intent *appservice.IntentAPI) *mautrix.RespJoinedMembers { @@ -101,30 +106,30 @@ func (mh *matrixHandler) handleMessage(evt *event.Event) { } -func (mh *matrixHandler) joinAndCheckMembers(evt *event.Event, intent *appservice.IntentAPI) *mautrix.RespJoinedMembers { +func (mh *matrixHandler) joinAndCheckMembers(evt *event.Event, intent *appservice.IntentAPI) int { resp, err := intent.JoinRoomByID(evt.RoomID) if err != nil { mh.log.Debugfln("Failed to join room %q as %q with invite from %q: %v", evt.RoomID, intent.UserID, evt.Sender, err) - return nil + return 0 } - members, err := intent.JoinedMembers(resp.RoomID) + members, err := intent.Members(resp.RoomID) if err != nil { mh.log.Debugfln("Failed to get members in room %q with invite from %q as %q: %v", resp.RoomID, evt.Sender, intent.UserID, err) - return nil + return 0 } - if len(members.Joined) < 2 { + if len(members.Chunk) < 2 { mh.log.Debugfln("Leaving empty room %q with invite from %q as %q", resp.RoomID, evt.Sender, intent.UserID) intent.LeaveRoom(resp.RoomID) - return nil + return 0 } - return members + return len(members.Chunk) } func (mh *matrixHandler) sendNoticeWithmarkdown(roomID id.RoomID, message string) (*mautrix.RespSendEvent, error) { @@ -144,24 +149,24 @@ func (mh *matrixHandler) handleBotInvite(evt *event.Event) { } members := mh.joinAndCheckMembers(evt, intent) - if members == nil { + if members == 0 { return } // If this is a DM and the user doesn't have a management room, make this // the management room. - if len(members.Joined) == 2 && (user.ManagementRoom == "" || evt.Content.AsMember().IsDirect) { + if members == 2 && (user.ManagementRoom == "" || evt.Content.AsMember().IsDirect) { user.SetManagementRoom(evt.RoomID) intent.SendNotice(user.ManagementRoom, "This room has been registered as your bridge management/status room") mh.log.Debugfln("%q registered as management room with %q", evt.RoomID, evt.Sender) } - // Wait to send the welcome message until we're sure we're not in an empty - // room. - mh.sendNoticeWithmarkdown(evt.RoomID, mh.bridge.Config.Bridge.ManagementRoomText.Welcome) - if evt.RoomID == user.ManagementRoom { + // Wait to send the welcome message until we're sure we're not in an empty + // room. + mh.sendNoticeWithmarkdown(evt.RoomID, mh.bridge.Config.Bridge.ManagementRoomText.Welcome) + if user.Connected() { mh.sendNoticeWithmarkdown(evt.RoomID, mh.bridge.Config.Bridge.ManagementRoomText.Connected) } else { @@ -185,6 +190,10 @@ func (mh *matrixHandler) handleMembership(evt *event.Event) { return } + if mh.bridge.crypto != nil { + mh.bridge.crypto.HandleMemberEvent(evt) + } + // Grab the content of the event. content := evt.Content.AsMember() @@ -255,3 +264,113 @@ func (mh *matrixHandler) handleRedaction(evt *event.Event) { portal.handleMatrixRedaction(evt) } } + +func (mh *matrixHandler) handleEncryption(evt *event.Event) { + if evt.Content.AsEncryption().Algorithm != id.AlgorithmMegolmV1 { + return + } + + portal := mh.bridge.GetPortalByMXID(evt.RoomID) + if portal != nil && !portal.Encrypted { + mh.log.Debugfln("%s enabled encryption in %s", evt.Sender, evt.RoomID) + portal.Encrypted = true + portal.Update() + } +} + +const sessionWaitTimeout = 5 * time.Second + +func (mh *matrixHandler) handleEncrypted(evt *event.Event) { + if mh.ignoreEvent(evt) || mh.bridge.crypto == nil { + return + } + + decrypted, err := mh.bridge.crypto.Decrypt(evt) + decryptionRetryCount := 0 + if errors.Is(err, NoSessionFound) { + content := evt.Content.AsEncrypted() + mh.log.Debugfln("Couldn't find session %s trying to decrypt %s, waiting %d seconds...", content.SessionID, evt.ID, int(sessionWaitTimeout.Seconds())) + mh.as.SendErrorMessageSendCheckpoint(evt, appservice.StepDecrypted, err, false, decryptionRetryCount) + decryptionRetryCount++ + + if mh.bridge.crypto.WaitForSession(evt.RoomID, content.SenderKey, content.SessionID, sessionWaitTimeout) { + mh.log.Debugfln("Got session %s after waiting, trying to decrypt %s again", content.SessionID, evt.ID) + decrypted, err = mh.bridge.crypto.Decrypt(evt) + } else { + mh.as.SendErrorMessageSendCheckpoint(evt, appservice.StepDecrypted, fmt.Errorf("didn't receive encryption keys"), false, decryptionRetryCount) + + go mh.waitLongerForSession(evt) + + return + } + } + + if err != nil { + mh.as.SendErrorMessageSendCheckpoint(evt, appservice.StepDecrypted, err, true, decryptionRetryCount) + + mh.log.Warnfln("Failed to decrypt %s: %v", evt.ID, err) + _, _ = mh.bridge.bot.SendNotice(evt.RoomID, fmt.Sprintf( + "\u26a0 Your message was not bridged: %v", err)) + + return + } + + mh.as.SendMessageSendCheckpoint(decrypted, appservice.StepDecrypted, decryptionRetryCount) + mh.bridge.eventProcessor.Dispatch(decrypted) +} + +func (mh *matrixHandler) waitLongerForSession(evt *event.Event) { + const extendedTimeout = sessionWaitTimeout * 3 + + content := evt.Content.AsEncrypted() + mh.log.Debugfln("Couldn't find session %s trying to decrypt %s, waiting %d more seconds...", + content.SessionID, evt.ID, int(extendedTimeout.Seconds())) + + go mh.bridge.crypto.RequestSession(evt.RoomID, content.SenderKey, content.SessionID, evt.Sender, content.DeviceID) + + resp, err := mh.bridge.bot.SendNotice(evt.RoomID, fmt.Sprintf( + "\u26a0 Your message was not bridged: the bridge hasn't received the decryption keys. "+ + "The bridge will retry for %d seconds. If this error keeps happening, try restarting your client.", + int(extendedTimeout.Seconds()))) + if err != nil { + mh.log.Errorfln("Failed to send decryption error to %s: %v", evt.RoomID, err) + } + + update := event.MessageEventContent{MsgType: event.MsgNotice} + + if mh.bridge.crypto.WaitForSession(evt.RoomID, content.SenderKey, content.SessionID, extendedTimeout) { + mh.log.Debugfln("Got session %s after waiting more, trying to decrypt %s again", content.SessionID, evt.ID) + + decrypted, err := mh.bridge.crypto.Decrypt(evt) + if err == nil { + mh.as.SendMessageSendCheckpoint(decrypted, appservice.StepDecrypted, 2) + mh.bridge.eventProcessor.Dispatch(decrypted) + _, _ = mh.bridge.bot.RedactEvent(evt.RoomID, resp.EventID) + + return + } + + mh.log.Warnfln("Failed to decrypt %s: %v", evt.ID, err) + mh.as.SendErrorMessageSendCheckpoint(evt, appservice.StepDecrypted, err, true, 2) + update.Body = fmt.Sprintf("\u26a0 Your message was not bridged: %v", err) + } else { + mh.log.Debugfln("Didn't get %s, giving up on %s", content.SessionID, evt.ID) + mh.as.SendErrorMessageSendCheckpoint(evt, appservice.StepDecrypted, fmt.Errorf("didn't receive encryption keys"), true, 2) + update.Body = "\u26a0 Your message was not bridged: the bridge hasn't received the decryption keys. " + + "If this error keeps happening, try restarting your client." + } + + newContent := update + update.NewContent = &newContent + if resp != nil { + update.RelatesTo = &event.RelatesTo{ + Type: event.RelReplace, + EventID: resp.EventID, + } + } + + _, err = mh.bridge.bot.SendMessageEvent(evt.RoomID, event.EventMessage, &update) + if err != nil { + mh.log.Debugfln("Failed to update decryption error notice %s: %v", resp.EventID, err) + } +} diff --git a/bridge/portal.go b/bridge/portal.go index 7f5e847..f8d5064 100644 --- a/bridge/portal.go +++ b/bridge/portal.go @@ -35,6 +35,7 @@ type Portal struct { log log.Logger roomCreateLock sync.Mutex + encryptLock sync.Mutex discordMessages chan portalDiscordMessage matrixMessages chan portalMatrixMessage @@ -144,7 +145,7 @@ func (p *Portal) handleMatrixInvite(sender *User, evt *event.Event) { p.log.Infoln("no puppet for %v", sender) // Open a conversation on the discord side? } - p.log.Infoln("puppet:", puppet) + p.log.Infoln("matrixInvite: puppet:", puppet) } func (p *Portal) messageLoop() { @@ -171,14 +172,14 @@ func (p *Portal) MainIntent() *appservice.IntentAPI { } func (p *Portal) createMatrixRoom(user *User, channel *discordgo.Channel) error { + p.roomCreateLock.Lock() + defer p.roomCreateLock.Unlock() + // If we have a matrix id the room should exist so we have nothing to do. if p.MXID != "" { return nil } - p.roomCreateLock.Lock() - defer p.roomCreateLock.Unlock() - p.Type = channel.Type if p.Type == discordgo.ChannelTypeDM { p.DMUser = channel.Recipients[0].ID @@ -212,14 +213,25 @@ func (p *Portal) createMatrixRoom(user *User, channel *discordgo.Channel) error var invite []id.UserID - if p.IsPrivateChat() { - invite = append(invite, p.bridge.bot.UserID) + if p.bridge.Config.Bridge.Encryption.Default { + initialState = append(initialState, &event.Event{ + Type: event.StateEncryption, + Content: event.Content{ + Parsed: event.EncryptionEventContent{Algorithm: id.AlgorithmMegolmV1}, + }, + }) + p.Encrypted = true + + if p.IsPrivateChat() { + invite = append(invite, p.bridge.bot.UserID) + } } resp, err := intent.CreateRoom(&mautrix.ReqCreateRoom{ Visibility: "private", Name: p.Name, Topic: p.Topic, + Invite: invite, Preset: "private_chat", IsDirect: p.IsPrivateChat(), InitialState: initialState, @@ -325,7 +337,7 @@ func (p *Portal) sendMediaFailedMessage(intent *appservice.IntentAPI, bridgeErr MsgType: event.MsgNotice, } - _, err := intent.SendMessageEvent(p.MXID, event.EventMessage, content) + _, err := p.sendMatrixMessage(intent, event.EventMessage, content, nil, time.Now().UTC().UnixMilli()) if err != nil { p.log.Warnfln("failed to send error message to matrix: %v", err) } @@ -379,7 +391,7 @@ func (p *Portal) handleDiscordAttachment(intent *appservice.IntentAPI, msgID str return } - resp, err := intent.SendMessageEvent(p.MXID, event.EventMessage, content) + resp, err := p.sendMatrixMessage(intent, event.EventMessage, content, nil, time.Now().UTC().UnixMilli()) if err != nil { p.log.Warnfln("failed to send media message to matrix: %v", err) } @@ -399,6 +411,29 @@ func (p *Portal) handleDiscordMessageCreate(user *User, msg *discordgo.Message) return } + // Handle room name changes + if msg.Type == discordgo.MessageTypeChannelNameChange { + channel, err := user.Session.Channel(msg.ChannelID) + if err != nil { + p.log.Errorf("Failed to find the channel for portal %s", p.Key) + return + } + + name, err := p.bridge.Config.Bridge.FormatChannelname(channel, user.Session) + if err != nil { + p.log.Errorf("Failed to format name for portal %s", p.Key) + return + } + + p.Name = name + p.Update() + + p.MainIntent().SetRoomName(p.MXID, name) + + return + } + + // Handle normal message existing := p.bridge.db.Message.GetByDiscordID(p.Key, msg.ID) if existing != nil { p.log.Debugln("not handling duplicate message", msg.ID) @@ -406,7 +441,9 @@ func (p *Portal) handleDiscordMessageCreate(user *User, msg *discordgo.Message) return } - intent := p.bridge.GetPuppetByID(msg.Author.ID).IntentFor(p) + puppet := p.bridge.GetPuppetByID(msg.Author.ID) + puppet.SyncContact(user) + intent := puppet.IntentFor(p) if msg.Content != "" { content := &event.MessageEventContent{ @@ -418,7 +455,7 @@ func (p *Portal) handleDiscordMessageCreate(user *User, msg *discordgo.Message) key := database.PortalKey{msg.MessageReference.ChannelID, user.ID} existing := p.bridge.db.Message.GetByDiscordID(key, msg.MessageReference.MessageID) - if existing.MatrixID != "" { + if existing != nil && existing.MatrixID != "" { content.RelatesTo = &event.RelatesTo{ Type: event.RelReply, EventID: existing.MatrixID, @@ -426,7 +463,7 @@ func (p *Portal) handleDiscordMessageCreate(user *User, msg *discordgo.Message) } } - resp, err := intent.SendMessageEvent(p.MXID, event.EventMessage, content) + resp, err := p.sendMatrixMessage(intent, event.EventMessage, content, nil, time.Now().UTC().UnixMilli()) if err != nil { p.log.Warnfln("failed to send message %q to matrix: %v", msg.ID, err) @@ -450,6 +487,23 @@ func (p *Portal) handleDiscordMessagesUpdate(user *User, msg *discordgo.Message) return } + // There's a few scenarios where the author is nil but I haven't figured + // them all out yet. + if msg.Author == nil { + // If the server has to lookup opengraph previews it'll send the + // message through without the preview and then add the preview later + // via a message update. However, when it does this there is no author + // as it's just the server, so for the moment we'll ignore this to + // avoid a crash. + if len(msg.Embeds) > 0 { + p.log.Debugln("ignoring update for opengraph attachment") + + return + } + + p.log.Errorfln("author is nil: %#v", msg) + } + intent := p.bridge.GetPuppetByID(msg.Author.ID).IntentFor(p) existing := p.bridge.db.Message.GetByDiscordID(p.Key, msg.ID) @@ -498,7 +552,7 @@ func (p *Portal) handleDiscordMessagesUpdate(user *User, msg *discordgo.Message) content.SetEdit(existing.MatrixID) - resp, err := intent.SendMessageEvent(p.MXID, event.EventMessage, content) + resp, err := p.sendMatrixMessage(intent, event.EventMessage, content, nil, time.Now().UTC().UnixMilli()) if err != nil { p.log.Warnfln("failed to send message %q to matrix: %v", msg.ID, err) @@ -567,6 +621,57 @@ func (p *Portal) syncParticipants(source *User, participants []*discordgo.User) } } +func (portal *Portal) encrypt(content *event.Content, eventType event.Type) (event.Type, error) { + if portal.Encrypted && portal.bridge.crypto != nil { + // TODO maybe the locking should be inside mautrix-go? + portal.encryptLock.Lock() + encrypted, err := portal.bridge.crypto.Encrypt(portal.MXID, eventType, *content) + portal.encryptLock.Unlock() + if err != nil { + return eventType, fmt.Errorf("failed to encrypt event: %w", err) + } + eventType = event.EventEncrypted + content.Parsed = encrypted + } + return eventType, nil +} + +const doublePuppetKey = "fi.mau.double_puppet_source" +const doublePuppetValue = "mautrix-discord" + +func (portal *Portal) sendMatrixMessage(intent *appservice.IntentAPI, eventType event.Type, content *event.MessageEventContent, extraContent map[string]interface{}, timestamp int64) (*mautrix.RespSendEvent, error) { + wrappedContent := event.Content{Parsed: content, Raw: extraContent} + if timestamp != 0 && intent.IsCustomPuppet { + if wrappedContent.Raw == nil { + wrappedContent.Raw = map[string]interface{}{} + } + if intent.IsCustomPuppet { + wrappedContent.Raw[doublePuppetKey] = doublePuppetValue + } + } + var err error + eventType, err = portal.encrypt(&wrappedContent, eventType) + if err != nil { + return nil, err + } + + if eventType == event.EventEncrypted { + // Clear other custom keys if the event was encrypted, but keep the double puppet identifier + if intent.IsCustomPuppet { + wrappedContent.Raw = map[string]interface{}{doublePuppetKey: doublePuppetValue} + } else { + wrappedContent.Raw = nil + } + } + + _, _ = intent.UserTyping(portal.MXID, false, 0) + if timestamp == 0 { + return intent.SendMessageEvent(portal.MXID, eventType, &wrappedContent) + } else { + return intent.SendMassagedMessageEvent(portal.MXID, eventType, &wrappedContent, timestamp) + } +} + func (p *Portal) handleMatrixMessages(msg portalMatrixMessage) { switch msg.evt.Type { case event.EventMessage: diff --git a/bridge/puppet.go b/bridge/puppet.go index 37c58e2..3b0753c 100644 --- a/bridge/puppet.go +++ b/bridge/puppet.go @@ -164,7 +164,10 @@ func (p *Puppet) CustomIntent() *appservice.IntentAPI { func (p *Puppet) updatePortalMeta(meta func(portal *Portal)) { for _, portal := range p.bridge.GetAllPortalsByID(p.ID) { + // Get room create lock to prevent races between receiving contact info and room creation. + portal.roomCreateLock.Lock() meta(portal) + portal.roomCreateLock.Unlock() } } diff --git a/bridge/user.go b/bridge/user.go index d6b967e..f7170f8 100644 --- a/bridge/user.go +++ b/bridge/user.go @@ -32,6 +32,9 @@ type User struct { bridge *Bridge log log.Logger + // TODO finish implementing + Admin bool + guilds map[string]*database.Guild guildsLock sync.Mutex @@ -717,7 +720,7 @@ func (u *User) updateDirectChats(chats map[id.UserID][]id.RoomID) { var err error if u.bridge.Config.Homeserver.Asmux { - urlPath := intent.BuildBaseURL("_matrix", "client", "unstable", "com.beeper.asmux", "dms") + urlPath := intent.BuildURL(mautrix.ClientURLPath{"unstable", "com.beeper.asmux", "dms"}) _, err = intent.MakeFullRequest(mautrix.FullRequest{ Method: method, URL: urlPath, diff --git a/config/bridge.go b/config/bridge.go index 9d0510c..ce863db 100644 --- a/config/bridge.go +++ b/config/bridge.go @@ -30,6 +30,8 @@ type bridge struct { DoublePuppetAllowDiscovery bool `yaml:"double_puppet_allow_discovery"` LoginSharedSecretMap map[string]string `yaml:"login_shared_secret_map"` + Encryption encryption `yaml:"encryption"` + usernameTemplate *template.Template `yaml:"-"` displaynameTemplate *template.Template `yaml:"-"` channelnameTemplate *template.Template `yaml:"-"` diff --git a/config/encryption.go b/config/encryption.go new file mode 100644 index 0000000..1d57c39 --- /dev/null +++ b/config/encryption.go @@ -0,0 +1,29 @@ +package config + +type encryption struct { + Allow bool `yaml:"allow"` + Default bool `yaml:"default"` + + KeySharing struct { + Allow bool `yaml:"allow"` + RequireCrossSigning bool `yaml:"require_cross_signing"` + RequireVerification bool `yaml:"require_verification"` + } `yaml:"key_sharing"` +} + +func (e *encryption) validate() error { + return nil +} + +func (e *encryption) UnmarshalYAML(unmarshal func(interface{}) error) error { + type rawEncryption encryption + + raw := rawEncryption{} + if err := unmarshal(&raw); err != nil { + return err + } + + *e = encryption(raw) + + return e.validate() +} diff --git a/config/homeserver.go b/config/homeserver.go index f66864b..bb3aeb1 100644 --- a/config/homeserver.go +++ b/config/homeserver.go @@ -14,6 +14,7 @@ type homeserver struct { Domain string `yaml:"domain"` Asmux bool `yaml:"asmux"` StatusEndpoint string `yaml:"status_endpoint"` + AsyncMedia bool `yaml:"async_media"` } func (h *homeserver) validate() error { diff --git a/database/cryptostore.go b/database/cryptostore.go new file mode 100644 index 0000000..171c824 --- /dev/null +++ b/database/cryptostore.go @@ -0,0 +1,97 @@ +package database + +import ( + "database/sql" + + log "maunium.net/go/maulogger/v2" + + "maunium.net/go/mautrix/crypto" + "maunium.net/go/mautrix/id" +) + +type SQLCryptoStore struct { + *crypto.SQLCryptoStore + UserID id.UserID + GhostIDFormat string +} + +var _ crypto.Store = (*SQLCryptoStore)(nil) + +func NewSQLCryptoStore(db *Database, userID id.UserID, ghostIDFormat string) *SQLCryptoStore { + return &SQLCryptoStore{ + SQLCryptoStore: crypto.NewSQLCryptoStore(db.DB, db.dialect, "", "", + []byte("maunium.net/go/mautrix-whatsapp"), + &cryptoLogger{db.log.Sub("CryptoStore")}), + UserID: userID, + GhostIDFormat: ghostIDFormat, + } +} + +func (store *SQLCryptoStore) FindDeviceID() id.DeviceID { + var deviceID id.DeviceID + + query := `SELECT device_id FROM crypto_account WHERE account_id=$1` + err := store.DB.QueryRow(query, store.AccountID).Scan(&deviceID) + if err != nil && err != sql.ErrNoRows { + store.Log.Warn("Failed to scan device ID: %v", err) + } + + return deviceID +} + +func (store *SQLCryptoStore) GetRoomMembers(roomID id.RoomID) ([]id.UserID, error) { + query := ` + SELECT user_id FROM mx_user_profile + WHERE room_id=$1 + AND (membership='join' OR membership='invite') + AND user_id<>$2 + AND user_id NOT LIKE $3 + ` + + members := []id.UserID{} + + rows, err := store.DB.Query(query, roomID, store.UserID, store.GhostIDFormat) + if err != nil { + return members, err + } + + for rows.Next() { + var userID id.UserID + err := rows.Scan(&userID) + if err != nil { + store.Log.Warn("Failed to scan member in %s: %v", roomID, err) + return members, err + } + + members = append(members, userID) + } + + return members, nil +} + +// TODO merge this with the one in the parent package +type cryptoLogger struct { + int log.Logger +} + +var levelTrace = log.Level{ + Name: "TRACE", + Severity: -10, + Color: -1, +} + +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...) +} diff --git a/database/migrations/08-add-crypto-store-to-database.sql b/database/migrations/08-add-crypto-store-to-database.sql new file mode 100644 index 0000000..c615976 --- /dev/null +++ b/database/migrations/08-add-crypto-store-to-database.sql @@ -0,0 +1,3 @@ +-- This migration is implemented in migrations.go as it comes from +-- maunium.net/go/mautrix/crypto/sql_store_upgrade. It runs upgrade at index 0 +-- which is described as "Add crypto store to database". diff --git a/database/migrations/09-add-account_id-to-crypto-store.sql b/database/migrations/09-add-account_id-to-crypto-store.sql new file mode 100644 index 0000000..03dc1cc --- /dev/null +++ b/database/migrations/09-add-account_id-to-crypto-store.sql @@ -0,0 +1,3 @@ +-- This migration is implemented in migrations.go as it comes from +-- maunium.net/go/mautrix/crypto/sql_store_upgrade. It runs upgrade at index 1 +-- which is described as "Add account_id to crypto store". diff --git a/database/migrations/10-add-megolm-withheld-data-to-crypto-store.sql b/database/migrations/10-add-megolm-withheld-data-to-crypto-store.sql new file mode 100644 index 0000000..38813b8 --- /dev/null +++ b/database/migrations/10-add-megolm-withheld-data-to-crypto-store.sql @@ -0,0 +1,3 @@ +-- This migration is implemented in migrations.go as it comes from +-- maunium.net/go/mautrix/crypto/sql_store_upgrade. It runs upgrade at index 2 +-- which is described as "Add megolm withheld data to crypto store". diff --git a/database/migrations/11-add-cross-signing-keys-to-crypto-store.sql b/database/migrations/11-add-cross-signing-keys-to-crypto-store.sql new file mode 100644 index 0000000..39f5041 --- /dev/null +++ b/database/migrations/11-add-cross-signing-keys-to-crypto-store.sql @@ -0,0 +1,3 @@ +-- This migration is implemented in migrations.go as it comes from +-- maunium.net/go/mautrix/crypto/sql_store_upgrade. It runs upgrade at index 3 +-- which is described as "Add cross-signing keys to crypto store". diff --git a/database/migrations/12-replace-varchar-with-text-in-the-crypto-database.sql b/database/migrations/12-replace-varchar-with-text-in-the-crypto-database.sql new file mode 100644 index 0000000..adb841e --- /dev/null +++ b/database/migrations/12-replace-varchar-with-text-in-the-crypto-database.sql @@ -0,0 +1,4 @@ +-- This migration is implemented in migrations.go as it comes from +-- maunium.net/go/mautrix/crypto/sql_store_upgrade. It runs upgrade at index 4 +-- which is described as "Replace VARCHAR(255) with TEXT in the crypto +-- database". diff --git a/database/migrations/13-split-last_used-into-last_encrypted-and-last_decrypted-in-crypto-store.sql b/database/migrations/13-split-last_used-into-last_encrypted-and-last_decrypted-in-crypto-store.sql new file mode 100644 index 0000000..28906bd --- /dev/null +++ b/database/migrations/13-split-last_used-into-last_encrypted-and-last_decrypted-in-crypto-store.sql @@ -0,0 +1,4 @@ +-- This migration is implemented in migrations.go as it comes from +-- maunium.net/go/mautrix/crypto/sql_store_upgrade. It runs upgrade at index 5 +-- which is described as "Split last_used into last_encrypted and +-- last_decrypted in crypto store". diff --git a/database/migrations/14-add-encrypted-column-to-portal-table.sql b/database/migrations/14-add-encrypted-column-to-portal-table.sql new file mode 100644 index 0000000..d032fee --- /dev/null +++ b/database/migrations/14-add-encrypted-column-to-portal-table.sql @@ -0,0 +1 @@ +ALTER TABLE portal ADD COLUMN encrypted BOOLEAN NOT NULL DEFAULT false; diff --git a/database/migrations/migrations.go b/database/migrations/migrations.go index 06fcce4..904b9f4 100644 --- a/database/migrations/migrations.go +++ b/database/migrations/migrations.go @@ -3,37 +3,18 @@ package migrations import ( "database/sql" "embed" - "sort" "github.com/lopezator/migrator" log "maunium.net/go/maulogger/v2" + "maunium.net/go/mautrix/crypto/sql_store_upgrade" ) //go:embed *.sql var embeddedMigrations embed.FS -var ( - commonMigrations = []string{ - "01-initial.sql", - "02-attachments.sql", - "03-emoji.sql", - "04-custom-puppet.sql", - "05-additional-puppet-fields.sql", - "07-guilds.sql", - } - - sqliteMigrations = []string{ - "06-remove-unique-user-constraint.sqlite.sql", - } - - postgresMigrations = []string{ - "06-remove-unique-user-constraint.postgres.sql", - } -) - -func migrationFromFile(filename string) *migrator.Migration { +func migrationFromFile(description, filename string) *migrator.Migration { return &migrator.Migration{ - Name: filename, + Name: description, Func: func(tx *sql.Tx) error { data, err := embeddedMigrations.ReadFile(filename) if err != nil { @@ -49,31 +30,83 @@ func migrationFromFile(filename string) *migrator.Migration { } } +func migrationFromFileWithDialect(dialect, description, sqliteFile, postgresFile string) *migrator.Migration { + switch dialect { + case "sqlite3": + return migrationFromFile(description, sqliteFile) + case "postgres": + return migrationFromFile(description, postgresFile) + default: + return nil + } +} + func Run(db *sql.DB, baseLog log.Logger, dialect string) error { subLogger := baseLog.Sub("Migrations") logger := migrator.LoggerFunc(func(msg string, args ...interface{}) { subLogger.Infof(msg, args...) }) - migrationNames := commonMigrations - switch dialect { - case "sqlite3": - migrationNames = append(migrationNames, sqliteMigrations...) - case "postgres": - migrationNames = append(migrationNames, postgresMigrations...) - } - - sort.Strings(migrationNames) - - migrations := make([]interface{}, len(migrationNames)) - for idx, name := range migrationNames { - migrations[idx] = migrationFromFile(name) - } - m, err := migrator.New( migrator.TableName("version"), migrator.WithLogger(logger), - migrator.Migrations(migrations...), + migrator.Migrations( + migrationFromFile("Initial Schema", "01-initial.sql"), + migrationFromFile("Attachments", "02-attachments.sql"), + migrationFromFile("Emoji", "03-emoji.sql"), + migrationFromFile("Custom Puppets", "04-custom-puppet.sql"), + migrationFromFile( + "Additional puppet fields", + "05-additional-puppet-fields.sql", + ), + migrationFromFileWithDialect( + dialect, + "Remove unique user constraint", + "06-remove-unique-user-constraint.sqlite.sql", + "06-remove-unique-user-constraint.postgres.sql", + ), + migrationFromFile("Guild Bridging", "07-guilds.sql"), + &migrator.Migration{ + Name: "Add crypto store to database", + Func: func(tx *sql.Tx) error { + return sql_store_upgrade.Upgrades[0](tx, dialect) + }, + }, + &migrator.Migration{ + Name: "Add account_id to crypto store", + Func: func(tx *sql.Tx) error { + return sql_store_upgrade.Upgrades[1](tx, dialect) + }, + }, + &migrator.Migration{ + Name: "Add megolm withheld data to crypto store", + Func: func(tx *sql.Tx) error { + return sql_store_upgrade.Upgrades[2](tx, dialect) + }, + }, + &migrator.Migration{ + Name: "Add cross-signing keys to crypto store", + Func: func(tx *sql.Tx) error { + return sql_store_upgrade.Upgrades[3](tx, dialect) + }, + }, + &migrator.Migration{ + Name: "Replace VARCHAR(255) with TEXT in the crypto database", + Func: func(tx *sql.Tx) error { + return sql_store_upgrade.Upgrades[4](tx, dialect) + }, + }, + &migrator.Migration{ + Name: "Split last_used into last_encrypted and last_decrypted in crypto store", + Func: func(tx *sql.Tx) error { + return sql_store_upgrade.Upgrades[5](tx, dialect) + }, + }, + migrationFromFile( + "Add encryption column to portal table", + "14-add-encrypted-column-to-portal-table.sql", + ), + ), ) if err != nil { return err diff --git a/database/portal.go b/database/portal.go index 663651b..96e4c45 100644 --- a/database/portal.go +++ b/database/portal.go @@ -19,6 +19,8 @@ type Portal struct { Name string Topic string + Encrypted bool + Avatar string AvatarURL id.ContentURI @@ -33,7 +35,8 @@ func (p *Portal) Scan(row Scannable) *Portal { var typ sql.NullInt32 err := row.Scan(&p.Key.ChannelID, &p.Key.Receiver, &mxid, &p.Name, - &p.Topic, &p.Avatar, &avatarURL, &typ, &p.DMUser, &firstEventID) + &p.Topic, &p.Avatar, &avatarURL, &typ, &p.DMUser, &firstEventID, + &p.Encrypted) if err != nil { if err != sql.ErrNoRows { @@ -62,12 +65,12 @@ func (p *Portal) mxidPtr() *id.RoomID { func (p *Portal) Insert() { query := "INSERT INTO portal" + " (channel_id, receiver, mxid, name, topic, avatar, avatar_url," + - " type, dmuser, first_event_id)" + - " VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)" + " type, dmuser, first_event_id, encrypted)" + + " VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)" _, err := p.db.Exec(query, p.Key.ChannelID, p.Key.Receiver, p.mxidPtr(), p.Name, p.Topic, p.Avatar, p.AvatarURL.String(), p.Type, p.DMUser, - p.FirstEventID.String()) + p.FirstEventID.String(), p.Encrypted) if err != nil { p.log.Warnfln("Failed to insert %s: %v", p.Key, err) @@ -77,11 +80,12 @@ func (p *Portal) Insert() { func (p *Portal) Update() { query := "UPDATE portal SET" + " mxid=$1, name=$2, topic=$3, avatar=$4, avatar_url=$5, type=$6," + - " dmuser=$7, first_event_id=$8" + - " WHERE channel_id=$9 AND receiver=$10" + " dmuser=$7, first_event_id=$8, encrypted=$9" + + " WHERE channel_id=$10 AND receiver=$11" _, err := p.db.Exec(query, p.mxidPtr(), p.Name, p.Topic, p.Avatar, p.AvatarURL.String(), p.Type, p.DMUser, p.FirstEventID.String(), + p.Encrypted, p.Key.ChannelID, p.Key.Receiver) if err != nil { diff --git a/database/portalquery.go b/database/portalquery.go index 8b4353a..d9dd1ba 100644 --- a/database/portalquery.go +++ b/database/portalquery.go @@ -6,6 +6,12 @@ import ( "maunium.net/go/mautrix/id" ) +const ( + portalSelect = "SELECT channel_id, receiver, mxid, name, topic, avatar," + + " avatar_url, type, dmuser, first_event_id, encrypted" + + " FROM portal" +) + type PortalQuery struct { db *Database log log.Logger @@ -19,23 +25,23 @@ func (pq *PortalQuery) New() *Portal { } func (pq *PortalQuery) GetAll() []*Portal { - return pq.getAll("SELECT * FROM portal") + return pq.getAll(portalSelect) } func (pq *PortalQuery) GetByID(key PortalKey) *Portal { - return pq.get("SELECT * FROM portal WHERE channel_id=$1 AND receiver=$2", key.ChannelID, key.Receiver) + return pq.get(portalSelect+" WHERE channel_id=$1 AND receiver=$2", key.ChannelID, key.Receiver) } func (pq *PortalQuery) GetByMXID(mxid id.RoomID) *Portal { - return pq.get("SELECT * FROM portal WHERE mxid=$1", mxid) + return pq.get(portalSelect+" WHERE mxid=$1", mxid) } func (pq *PortalQuery) GetAllByID(id string) []*Portal { - return pq.getAll("SELECT * FROM portal WHERE receiver=$1", id) + return pq.getAll(portalSelect+" WHERE receiver=$1", id) } func (pq *PortalQuery) FindPrivateChats(receiver string) []*Portal { - query := "SELECT * FROM portal WHERE receiver=$1 AND type=$2;" + query := portalSelect + " portal WHERE receiver=$1 AND type=$2;" return pq.getAll(query, receiver, discordgo.ChannelTypeDM) } diff --git a/database/sqlstatestore.go b/database/sqlstatestore.go index c5d800b..9fcfd1c 100644 --- a/database/sqlstatestore.go +++ b/database/sqlstatestore.go @@ -272,3 +272,33 @@ func (s *SQLStateStore) HasPowerLevel(roomID id.RoomID, userID id.UserID, eventT return s.GetPowerLevel(roomID, userID) >= s.GetPowerLevelRequirement(roomID, eventType) } + +func (store *SQLStateStore) FindSharedRooms(userID id.UserID) []id.RoomID { + query := ` + SELECT room_id FROM mx_user_profile + LEFT JOIN portal ON portal.mxid=mx_user_profile.room_id + WHERE user_id=$1 AND portal.encrypted=true + ` + + rooms := []id.RoomID{} + + rows, err := store.db.Query(query, userID) + if err != nil { + store.log.Warnfln("Failed to query shared rooms with %s: %v", userID, err) + + return rooms + } + + 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 rooms +} diff --git a/example-config.yaml b/example-config.yaml index 3399a60..060d1f8 100644 --- a/example-config.yaml +++ b/example-config.yaml @@ -11,6 +11,8 @@ homeserver: # If set, the bridge will make POST requests to this URL whenever a user's discord connection state changes. # The bridge will use the appservice as_token to authorize requests. status_endpoint: null + # Does the homeserver support https://github.com/matrix-org/matrix-spec-proposals/pull/2246? + async_media: false # Application service host/registration related details. # Changing these values requires regeneration of the registration. @@ -110,6 +112,29 @@ bridge: # Optional extra text sent when joining a management room. additional_help: "" + # End-to-bridge encryption support options. + # + # See https://docs.mau.fi/bridges/general/end-to-bridge-encryption.html for more info. + 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. + # It is recommended to also set private_chat_portal_meta to true when using this. + default: false + # Options for automatic key sharing. + key_sharing: + # Enable key sharing? If enabled, key requests for rooms where users are in will be fulfilled. + # You must use a client that supports requesting keys from other users to use this feature. + allow: false + # Require the requesting device to have a valid cross-signing signature? + # This doesn't require that the bridge has verified the device, only that the user has verified it. + # Not yet implemented. + require_cross_signing: false + # Require devices to be verified by the bridge? + # Verification by the bridge is not yet implemented. + require_verification: true + logging: directory: ./logs file_name_format: '{{.Date}}-{{.Index}}.log' diff --git a/go.mod b/go.mod index e5b1ade..912fe88 100644 --- a/go.mod +++ b/go.mod @@ -6,23 +6,27 @@ require ( github.com/alecthomas/kong v0.5.0 github.com/bwmarrin/discordgo v0.23.2 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 + github.com/gorilla/mux v1.8.0 github.com/gorilla/websocket v1.5.0 - github.com/lib/pq v1.10.4 + github.com/lib/pq v1.10.5 github.com/lopezator/migrator v0.3.0 github.com/mattn/go-sqlite3 v1.14.12 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e gopkg.in/yaml.v2 v2.4.0 maunium.net/go/maulogger/v2 v2.3.2 - maunium.net/go/mautrix v0.10.12 + maunium.net/go/mautrix v0.10.13-0.20220417095934-0eee489b6417 ) require ( - github.com/gorilla/mux v1.8.0 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/russross/blackfriday/v2 v2.1.0 // indirect - golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 // indirect - golang.org/x/net v0.0.0-20220403103023-749bd193bc2b // indirect - golang.org/x/sys v0.0.0-20220406163625-3f8b81556e12 // indirect + github.com/tidwall/gjson v1.14.1 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.0 // indirect + github.com/tidwall/sjson v1.2.4 // indirect + github.com/yuin/goldmark v1.4.12 // indirect + golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f // indirect + golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 // indirect + golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 // indirect ) replace github.com/bwmarrin/discordgo v0.23.2 => gitlab.com/beeper/discordgo v0.23.3-0.20220219094025-13ff4cc63da7 diff --git a/go.sum b/go.sum index 2066e59..d2713ad 100644 --- a/go.sum +++ b/go.sum @@ -18,6 +18,8 @@ github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.10.4 h1:SO9z7FRPzA03QhHKJrH5BXA6HU1rS4V2nIVrrNC1iYk= github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.10.5 h1:J+gdV2cUmX7ZqL2B0lFcW0m+egaHC2V3lpO8nWxyYiQ= +github.com/lib/pq v1.10.5/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lopezator/migrator v0.3.0 h1:VW/rR+J8NYwPdkBxjrFdjwejpgvP59LbmANJxXuNbuk= github.com/lopezator/migrator v0.3.0/go.mod h1:bpVAVPkWSvTw8ya2Pk7E/KiNAyDWNImgivQY79o8/8I= github.com/mattn/go-sqlite3 v1.14.12 h1:TJ1bhYJPV44phC+IMu1u2K/i5RriLTPe+yc68XDJ1Z0= @@ -26,8 +28,6 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -35,31 +35,41 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/tidwall/gjson v1.12.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.14.0 h1:6aeJ0bzojgWLa82gDQHcx3S0Lr/O51I9bJ5nv6JFx5w= github.com/tidwall/gjson v1.14.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.14.1 h1:iymTbGkQBhveq21bEvAQ81I0LEBork8BFe1CUZXdyuo= +github.com/tidwall/gjson v1.14.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.4 h1:cuiLzLnaMeBhRmEv00Lpk3tkYrcxpmbU81tAY4Dw0tc= github.com/tidwall/sjson v1.2.4/go.mod h1:098SZ494YoMWPmMO6ct4dcFnqxwj9r/gF0Etp19pSNM= +github.com/yuin/goldmark v1.4.11 h1:i45YIzqLnUc2tGaTlJCyUxSG8TvgyGqhqOZOUKIjJ6w= +github.com/yuin/goldmark v1.4.11/go.mod h1:rmuwmfZ0+bvzB24eSC//bk1R1Zp3hM0OXYv/G2LIilg= +github.com/yuin/goldmark v1.4.12 h1:6hffw6vALvEDqJ19dOJvJKOoAOKe4NDaTqvd2sktGN0= +github.com/yuin/goldmark v1.4.12/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= gitlab.com/beeper/discordgo v0.23.3-0.20220219094025-13ff4cc63da7 h1:8ieR27GadHnShqhsvPrDzL1/ZOntavGGt4TXqafncYE= gitlab.com/beeper/discordgo v0.23.3-0.20220219094025-13ff4cc63da7/go.mod h1:Hwfv4M8yP/MDh47BN+4Z1WItJ1umLKUyplCH5KcQPgE= golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd h1:XcWmESyNjXJMLahc3mqVQJcgSTDxFxhETVlfk9uGc38= -golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 h1:tkVvjkPTB7pnW3jnid7kNyAMPVWllTNOf/qKDze4p9o= -golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220408190544-5352b0902921 h1:iU7T1X1J6yxDr0rda54sWGkHgOp5XJrqm79gcNlC2VM= +golang.org/x/crypto v0.0.0-20220408190544-5352b0902921/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f h1:OeJjE6G4dgCY4PIXvIRQbE8+RX+uXZyGhUy/ksMGJoc= +golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220225172249-27dd8689420f h1:oA4XRj0qtSt8Yo1Zms0CUlsT3KG69V2UGQWPBxujDmc= -golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220403103023-749bd193bc2b h1:vI32FkLJNAWtGD4BwkThwEy6XS7ZLLMHkSkYfF8M0W0= -golang.org/x/net v0.0.0-20220403103023-749bd193bc2b/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220407224826-aac1ed45d8e3 h1:EN5+DfgmRMvRUrMGERW2gQl3Vc+Z7ZMnI/xdEpPSf0c= +golang.org/x/net v0.0.0-20220407224826-aac1ed45d8e3/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 h1:HVyaeDAYux4pnY+D/SiwmLOR36ewZ4iGQIIrtnuCjFA= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220315194320-039c03cc5b86 h1:A9i04dxx7Cribqbs8jf3FQLogkL/CV2YN7hj9KWJCkc= -golang.org/x/sys v0.0.0-20220315194320-039c03cc5b86/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220406163625-3f8b81556e12 h1:QyVthZKMsyaQwBTJE04jdNN0Pp5Fn9Qga0mrgxyERQM= golang.org/x/sys v0.0.0-20220406163625-3f8b81556e12/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 h1:xHms4gcpe1YE7A3yIllJXP16CMAGuqwO2lX1mTyyRRc= +golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -77,5 +87,5 @@ gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= maunium.net/go/maulogger/v2 v2.3.2 h1:1XmIYmMd3PoQfp9J+PaHhpt80zpfmMqaShzUTC7FwY0= maunium.net/go/maulogger/v2 v2.3.2/go.mod h1:TYWy7wKwz/tIXTpsx8G3mZseIRiC5DoMxSZazOHy68A= -maunium.net/go/mautrix v0.10.12 h1:GqmsksKyKrTqmLb2B6yGOawoFLPTJ3A3NtXrygAvKM8= -maunium.net/go/mautrix v0.10.12/go.mod h1:xTq6+uMCAXtQwfqjUrYd8O10oIyymbzZm02CYOMt4ek= +maunium.net/go/mautrix v0.10.13-0.20220417095934-0eee489b6417 h1:dEJ9MKQvd4v2Rk2W6EUiO1T6PrSWPsB/JQOHQn4H6X0= +maunium.net/go/mautrix v0.10.13-0.20220417095934-0eee489b6417/go.mod h1:zOor2zO/F10T/GbU67vWr0vnhLso88rlRr1HIrb1XWU=