commit 7162269b91daa0f5c82e58d2a84a31abdb8d84e7
Author: Joseph Crowell <joseph.w.crowell@gmail.com>
Date: Tue Dec 23 22:27:44 2025 +0000
diff --git a/.gitignore b/.gitignore
index 1c9d9f9..d7006a9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -46 +47 @@
# Exclude configuration file
matterbridge.toml
+*.db*
# Exclude IDE Files
.vscode
diff --git a/bridge/config/config.go b/bridge/config/config.go
index 0ee0651..7079d42 100644
--- a/bridge/config/config.go
+++ b/bridge/config/config.go
@@ -1356 +1357 @@ type Protocol struct {
ColorNicks bool // only irc for now
Debug bool // general
DebugLevel int // only for irc now
+ DeviceID string // matrix
DisableWebPagePreview bool // telegram
EditSuffix string // mattermost, slack, discord, telegram
EditDisable bool // mattermost, slack, discord, telegram
@@ -1746 +1757 @@ type Protocol struct {
NoSendJoinPart bool // all protocols
NoTLS bool // mattermost, xmpp
Password string // IRC,mattermost,XMPP,matrix
+ PickleKey string // matrix
PrefixMessagesWithNick bool // mattemost, slack
PreserveThreading bool // slack
Protocol string // all protocols
@@ -1816 +1837 @@ type Protocol struct {
QuoteFormat string // telegram
QuoteLengthLimit int // telegram
RealName string // IRC
+ RecoveryKey string // matrix
RejoinDelay int // IRC
ReplaceMessages [][]string // all protocols
ReplaceNicks [][]string // all protocols
diff --git a/bridge/matrix/helpers.go b/bridge/matrix/helpers.go
index f61a46a..caa17fb 100644
--- a/bridge/matrix/helpers.go
+++ b/bridge/matrix/helpers.go
@@ -96 +99 @@ import (
"time"
mautrix "maunium.net/go/mautrix"
+ /* trunk-ignore(golangci-lint2/typecheck) */
+ "maunium.net/go/mautrix/crypto"
+ "maunium.net/go/mautrix/crypto/cryptohelper"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
)
@@ -4217 +456 @@ func (b *Bmatrix) getRoomID(channel string) id.RoomID {
return ""
}
-// interface2Struct marshals and immediately unmarshals an interface.
-// Useful for converting map[string]interface{} to a struct.
-func interface2Struct(in interface{}, out interface{}) error {
- jsonObj, err := json.Marshal(in)
- if err != nil {
- return err //nolint:wrapcheck
- }
-
- return json.Unmarshal(jsonObj, out)
-}
-
// getDisplayName retrieves the displayName for mxid, querying the homeserver if the mxid is not in the cache.
func (b *Bmatrix) getDisplayName(ctx context.Context, mxid id.UserID) string {
// Localpart is the user name. Return it if UseUserName is set.
@@ -2123 +20451 @@ func (b *Bmatrix) retry(f func() error) error {
}
}
}
+
+// Use this function to set up your client for E2EE
+func setupEncryptedClientHelper(client *mautrix.Client, pickleKey []byte, sessionFile string) (*cryptohelper.CryptoHelper, error) {
+ // The cryptohelper manages the creation of the correct CryptoStore and StateStore implementations.
+ ch, err := cryptohelper.NewCryptoHelper(client, pickleKey, sessionFile)
+ if err != nil {
+ // This usually catches errors with opening the database file or key issues.
+ return nil, fmt.Errorf("failed to create crypto helper: %w", err)
+ }
+
+ // Init MUST be called to load data from the database and prepare the internal machine.
+ err = ch.Init(context.Background())
+ if err != nil {
+ return nil, fmt.Errorf("failed to initialize crypto helper: %w", err)
+ }
+
+ // Hook the helper into the client
+ client.Crypto = ch
+
+ return ch, nil
+}
+
+// Verifies your recovery key with Matrix so you can message unimpeded
+func verifyWithRecoveryKey(ctx context.Context, machine *crypto.OlmMachine, recoveryKey string) error {
+ keyId, keyData, err := machine.SSSS.GetDefaultKeyData(ctx)
+ if err != nil {
+ return err
+ }
+
+ key, err := keyData.VerifyRecoveryKey(keyId, recoveryKey)
+ if err != nil {
+ return err
+ }
+
+ err = machine.FetchCrossSigningKeysFromSSSS(ctx, key)
+ if err != nil {
+ return err
+ }
+
+ err = machine.SignOwnDevice(ctx, machine.OwnIdentity())
+ if err != nil {
+ return err
+ }
+
+ err = machine.SignOwnMasterKey(ctx)
+
+ return err
+}
diff --git a/bridge/matrix/matrix.go b/bridge/matrix/matrix.go
index cfa7b29..508c98c 100644
--- a/bridge/matrix/matrix.go
+++ b/bridge/matrix/matrix.go
@@ -256 +257 @@ import (
"github.com/matterbridge-org/matterbridge/bridge/helper"
mautrix "maunium.net/go/mautrix"
+ "maunium.net/go/mautrix/crypto/cryptohelper"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
)
@@ -427 +436 @@ type NicknameCacheEntry struct {
type Bmatrix struct {
mc *mautrix.Client
UserID id.UserID
- AccessToken string
NicknameMap map[string]NicknameCacheEntry
RoomMap map[id.RoomID]string
rateMutex sync.RWMutex
@@ -1198 +1199 @@ func (b *Bmatrix) Connect() error {
}
b.UserID = userID
- b.AccessToken = b.GetString("Token")
b.Log.Info("Using existing Matrix credentials")
+
+ b.mc.DeviceID = id.DeviceID(b.GetString("DeviceID"))
} else {
b.mc, err = mautrix.NewClient(b.GetString("Server"), "", "")
if err != nil {
@@ -14017 +14117 @@ func (b *Bmatrix) Connect() error {
return err2
}
b.UserID = resp.UserID
- b.AccessToken = resp.AccessToken
}
- /**
- // BEGIN CACHED MESSAGES FIX
- **/
+
+ b.Log.Info("Connection succeeded")
+
+ b.Log.Infof("MxID: %s", b.mc.UserID)
+ b.Log.Infof("Token: %s", b.mc.AccessToken)
+ b.Log.Infof("Device ID: %s", b.mc.DeviceID)
accountStore := mautrix.NewAccountDataStore("org.example.mybot.synctoken", b.mc)
b.mc.Store = accountStore
- b.Log.Info("Connection succeeded")
-
initialFilter := mautrix.Filter{
Room: &mautrix.RoomFilter{
Timeline: &mautrix.FilterPart{
@@ -1769 +1776 @@ func (b *Bmatrix) Connect() error {
if err != nil {
b.Log.Fatalf("Failed to save initial sync token: %v", err)
}
- /**
- // END CACHED MESSAGES FIX
- **/
go b.handlematrix()
return nil
@@ -4707 +46838 @@ func (b *Bmatrix) NewHttpRequest(method, uri string, body io.Reader) (*http.Requ
}
func (b *Bmatrix) handlematrix() {
+ var (
+ ch *cryptohelper.CryptoHelper = nil
+ err error
+ )
+
+ if b.GetString("SessionFile") != "" &&
+ b.GetString("PickleKey") != "" {
+ // Use a robust key generation method in production (e.g. from environment variable or key management service)
+ pickleKey := []byte(b.GetString("PickleKey"))
+
+ ch, err = setupEncryptedClientHelper(b.mc, pickleKey, b.GetString("SessionFile"))
+ if err != nil {
+ b.Log.Error(err)
+ } else {
+ b.Log.Info("Encryption subsystem configured and attached.")
+ }
+ }
+
syncer := b.mc.Syncer.(*mautrix.DefaultSyncer) //nolint:forcetypeassert // We're only using DefaultSyncer
+
+ readyChan := make(chan bool)
+ var once sync.Once
+
+ syncer.OnSync(func(ctx context.Context, resp *mautrix.RespSync, since string) bool {
+ once.Do(func() {
+ if ch != nil && b.GetString("RecoveryKey") != "" {
+ close(readyChan)
+ }
+ })
+
+ return true
+ })
syncer.OnEventType(event.EventRedaction, b.handleRedactionEvent)
syncer.OnEventType(event.EventMessage, b.handleMessageEvent)
syncer.OnEventType(event.StateMember, b.handleMemberChange)
@@ -4906 +51917 @@ func (b *Bmatrix) handlematrix() {
}
}
}()
+
+ b.Log.Debug("Waiting for sync to receive first event from an encrypted room...")
+ <-readyChan
+ b.Log.Debug("First sync received")
+
+ err = verifyWithRecoveryKey(context.Background(), ch.Machine(), b.GetString("RecoveryKey"))
+ if err != nil {
+ panic(err)
+ } else {
+ b.Log.Info("Verify with recovery key succeeded")
+ }
}
func (b *Bmatrix) handleEdit(ev *event.Event, rmsg config.Message) bool {
diff --git a/docs/protocols/matrix/README.md b/docs/protocols/matrix/README.md
index ccc8175..966793c 100644
--- a/docs/protocols/matrix/README.md
+++ b/docs/protocols/matrix/README.md
@@ -20136 +2050 @@ RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
Server="https://matrix.org"
Login="yourlogin"
Password="yourpass"
-# Alternatively, you can use MXID + a session Token
+# Alternatively, you can use MXID + a session Token + a device id
#MxID="@yourbot:example.net"
#Token="tokenforthebotuser"
+#DeviceID="deviceidofmxidandtokenlogin"
```
## FAQ
### How to encrypt matterbridge messages to Matrix?
-Matterbridge doesn't properly encrypt its messages. So although matterbridge *does* work with matrix, even with matrix' unencrypted rooms, the messages sent by matterbridge will all show a warning symbol to everyone, something about "WARNING: This message was sent unencrypted!", which might irritate users.
-
-So there is a need for something that sits in the middle, pretends to be a matrix server (so that matterbridge can talk to it), and can forward everything to the real matrix server (so that the messages actually arrive), and also magically transparently "encrypts" everything (so that the messages show no "unencrypted" warning). This is exactly what pantalaimon does. Keep in mind that this effectively means you do a MITM-attack on yourself, so the connection between matterbridge and pantalaimon is *basically plaintext* and very vulnerable. You really should run matterbridge and pantalaimon on the same machine, and make sure that pantalaimon is only accessible to yourself. (I don't know if VPS is a problem here, so if you are running on a VPS then think twice before you do this setup.)
-
-#### bridge.toml
-
-```toml
-[general]
-MediaDownloadPath="/path/to/http/server/"
-MediaServerDownload="https://foo.bar.org/server/"
-MediaDownloadSize=10000000
-
-[telegram.mytelegram]
-Token="1234567890:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
-RemoteNickFormat="{NICK}@{PROTOCOL}: "
-MediaConvertWebPToPNG=true
-MediaConvertTgs="png"
-#QuoteFormat="{MESSAGE} (re @{QUOTENICK}: {QUOTEMESSAGE})"
-UseFirstName=true
-
-[matrix.mymatrix]
-# Server="https://matrix.org"
-# Matterbridge does not support encrypted group chats.
-# Therefore, use Pantalaimon to MiTM myself:
-Server="http://localhost:20662"
-# Dedicated user
-# Messages sent from this user will not be relayed to avoid loops.
-Login="mybot"
-Password="abcdefghijklmnopqrstuvwxyz"
-RemoteNickFormat="{NICK}@{PROTOCOL}: "
-#Whether to send the homeserver suffix. eg ":matrix.org" in @username:matrix.org
-#to other bridges, or only send "username".(true only sends username)
-NoHomeServerSuffix=true
-HTMLDisable=true
-
-[[gateway]]
-name="foobar"
-enable=true
-
-[[gateway.inout]]
-account="telegram.mytelegram"
-channel="-1234567890123"
-
-[[gateway.inout]]
-account="matrix.mymatrix"
-channel="!abcdefghijklmnopqr:matrix.org"
-```
-
-#### pantalaimon.conf
-
-```ini
-[Default]
-LogLevel = Debug
-SSL = True
-
-[local-matrix]
-Homeserver = https://matrix.org
-ListenAddress = localhost
-ListenPort = 20662
-SSL = False
-IgnoreVerification = True
-UseKeyring = False
-```
-
-#### run_pantalaimon.sh
-
-In theory, it suffices to just call `dbus-run-session -- pantalaimon --config pantalaimon.conf`
-
-However, I want *all* the logs, so I run this:
-
-```sh
-dbus-run-session -- pantalaimon --log-level debug --config pantalaimon.conf 2>&1 | \
- ./tee_unless_regex.py 'INFO: pantalaimon: Trying to decrypt sync|INFO: pantalaimon: Decrypting sync' \
- 2> pantalaimon_$(date +%s).log
-```
+[matrix.test]
+RemoteNickFormat="{NICK} ({LABEL}) "
+Server="<https://domain.tld>"
+Login="yourlogin"
+Password="yourpass"
+SessionFile="matrix_crypto.db" # sqlite database file used to store the login session persistently
+PickleKey="yourreallylongandcomplicatedpickle" # a long password to use when accessing the session store
+RecoveryKey="this thing isss real long bubb" # your account recovery key from matrix for this account
+MxID="@yourusername:domain.tld" # your mxid from the logs
+Token="your token from log output" # your token from the logs
+DeviceID="yourdeviceid" # your deviceid from the logs
-#### tee_unless_regex.py
+#### Steps for getting an encrypted connection working
-```
-#!/usr/bin/env python3
-
-import re
-import sys
-
-def run_regex(regex):
- while True:
- try:
- line = sys.stdin.readline()
- except KeyboardInterrupt:
- # Ctrl-C
- return
- if not line:
- # EOF
- return
- line = line.rstrip('\n')
- print(line)
- if not regex.search(line):
- print(line, file=sys.stderr)
- sys.stderr.flush() # This flush() is the entire reason why I don't just use 'grep -v'. Somehow, unbuffer+grep just doesn't work. But why!?
-
-
-def run():
- if len(sys.argv) != 2:
- print('USAGE: {} <SOME_REGEX>'.format(argv[0]))
- exit(1)
-
- run_regex(re.compile(sys.argv[1]))
-
-
-if __name__ == '__main__':
- run()
-```
+MxID, Token and DeviceID are required for encryption even though they are optional normally
-#### Setup
+1. Generate a recovery key using your preferred Matrix client for your account and make sure to store it for later
+2. Fill in the following
-There are setup-steps missing. In particular, you absolutely need pactl at some point. TODO: Please fill in these details.
+ - RemoteNickFormat (Optional)
+ - Server
+ - Login
+ - Password
+ - SessionFile (make sure this is readable and writable by the bot's run account)
+ - PickleKey
+ - RecoveryKey
-#### Invocation
+3. Start your bot and log in making sure to keep a log or watch output
+4. Copy these from the logs in to your configuration file
-In one screen: `./run_pantalaimon.sh`
+ - MxID
+ - Token
+ - DeviceID
-In another screen: `./matterbridge-THEVERSION-linux-arm -conf bridge.toml -debug | tee bridge_$(date +%s).log`
+5. Restart the bot and enjoy encrypted messaging
-(Again, the `-debug | …` stuff isn't necessary, but I personally want permanent logs of everything, just so I can trace back if something ever goes wrong. And I suggest that you do that, too.)
\ No newline at end of file
+Note: If you enable encryption on a channel after enabling encryption on the bot, you may need to regenerate your RecoveryKey, Token and DeviceID before the bot can send encrypted messages
diff --git a/docs/protocols/matrix/settings.md b/docs/protocols/matrix/settings.md
index ed47111..9c27372 100644
--- a/docs/protocols/matrix/settings.md
+++ b/docs/protocols/matrix/settings.md
@@ -36 +319 @@
> [!TIP]
> This page contains the details about matrix settings. More general information about matrix support in matterbridge can be found in [README.md](README.md).
+## DeviceID
+
+The device id use when logging in with MxID.
+
+Unless this option is set, the Matrix client is unencrypted and MxID based login won't work.
+
+- Setting: **OPTIONAL**
+- Format: *string*
+- Example:
+ ```toml
+ DeviceID="yourdeviceid"
+ ```
+
## HTMLDisable
Whether to disable sending of HTML content to matrix
@@ -286 +4119 @@ Messages sent from this user will not be relayed to avoid loops.
Login="yourlogin"
```
+## MxID
+
+MxID of your bot.
+Use a dedicated user for this and not your own!
+Messages sent from this user will not be relayed to avoid loops.
+
+- Setting: **REQUIRED**
+- Format: *string*
+- Example:
+ ```toml
+ MxID="@yourbot:example.net"
+ ```
+
## NoHomeServerSuffix
Whether to send the homeserver suffix. eg ":matrix.org" in @username:matrix.org
@@ -516 +7732 @@ password of your bot.
Password="yourpass"
```
+## PickleKey
+
+The key to use when accessing E2EE encryption in an encryption database.
+
+Unless this option is set, the Matrix client is unencrypted.
+
+- Setting: **OPTIONAL**
+- Format: *string*
+- Example:
+ ```toml
+ Password="yourpicklekey"
+ ```
+
+## RecoveryKey
+
+The key to use when accessing E2EE encryption in an encryption database.
+
+Unless this option is set, the Matrix client won't be verified for encryption.
+
+- Setting: **OPTIONAL**
+- Format: *string*
+- Example:
+ ```toml
+ RecoveryKey="yourrecoverykey"
+ ```
+
## Server
Server is your homeserver (eg https://matrix.org)
@@ -626 +11419 @@ Server is your homeserver (eg https://matrix.org)
Server="https://matrix.org"
```
+## SessionFile
+
+The database file to use when accessing E2EE encryption in an encryption database.
+
+Unless this option is set, the Matrix client is unencrypted.
+
+- Setting: **OPTIONAL**
+- Format: *string*
+- Example:
+ ```toml
+ SessionFile="yourdatabasefile.db"
+ ```
+
## UseUserName
Shows the username instead of the displayname
diff --git a/go.mod b/go.mod
index b044a1a..1638863 100644
--- a/go.mod
+++ b/go.mod
@@ -896 +897 @@ require (
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
+ github.com/mattn/go-sqlite3 v1.14.32 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
github.com/monaco-io/request v1.0.5 // indirect
@@ -1117 +1126 @@ require (
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/shazow/rateio v0.0.0-20200113175441-4461efc8bdc4 // indirect
github.com/sizeofint/webpanimation v0.0.0-20210809145948-1d2b32119882 // indirect
- github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.6.0 // indirect
diff --git a/go.sum b/go.sum
index 115140d..090927b 100644
--- a/go.sum
+++ b/go.sum
@@ -3418 +3416 @@ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sizeofint/webpanimation v0.0.0-20210809145948-1d2b32119882 h1:A7o8tOERTtpD/poS+2VoassCjXpjHn916luXbf5QKD0=
github.com/sizeofint/webpanimation v0.0.0-20210809145948-1d2b32119882/go.mod h1:5IwJoz9Pw7JsrCN4/skkxUtSWT7myuUPLhCgv6Q5vvQ=
-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/slack-go/slack v0.17.3 h1:zV5qO3Q+WJAQ/XwbGfNFrRMaJ5T/naqaonyPV/1TP4g=
github.com/slack-go/slack v0.17.3/go.mod h1:X+UqOufi3LYQHDnMG1vxf0J8asC6+WllXrVrhl8/Prk=
github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM=