diff --git a/bridgev2/matrix/cmddoublepuppet.go b/bridgev2/matrix/cmddoublepuppet.go index 1b755f36..13d24f54 100644 --- a/bridgev2/matrix/cmddoublepuppet.go +++ b/bridgev2/matrix/cmddoublepuppet.go @@ -26,12 +26,12 @@ func fnLoginMatrix(ce *bridgev2.CommandEvent) { ce.Reply("**Usage:** `login-matrix `") return } - //err := ce.User.SwitchCustomMXID(ce.Args[0], ce.User.GetMXID()) - //if err != nil { - // ce.Reply("Failed to enable double puppeting: %v", err) - //} else { - // ce.Reply("Successfully switched puppet") - //} + err := ce.User.LoginDoublePuppet(ce.Ctx, ce.Args[0]) + if err != nil { + ce.Reply("Failed to enable double puppeting: %v", err) + } else { + ce.Reply("Successfully switched puppets") + } } var CommandPingMatrix = &bridgev2.FullHandler{ @@ -41,16 +41,25 @@ var CommandPingMatrix = &bridgev2.FullHandler{ Section: bridgev2.HelpSectionAuth, Description: "Ping the Matrix server with the double puppet.", }, - RequiresLogin: true, } func fnPingMatrix(ce *bridgev2.CommandEvent) { - //resp, err := puppet.CustomIntent().Whoami(ce.Ctx) - //if err != nil { - // ce.Reply("Failed to validate Matrix login: %v", err) - //} else { - // ce.Reply("Confirmed valid access token for %s / %s", resp.UserID, resp.DeviceID) - //} + intent := ce.User.DoublePuppet(ce.Ctx) + if intent == nil { + ce.Reply("You don't have double puppeting enabled.") + return + } + asIntent := intent.(*ASIntent) + resp, err := asIntent.Matrix.Whoami(ce.Ctx) + if err != nil { + ce.Reply("Failed to validate Matrix login: %v", err) + } else { + if asIntent.Matrix.SetAppServiceUserID && resp.DeviceID == "" { + ce.Reply("Confirmed valid access token for %s (appservice double puppeting)", resp.UserID) + } else { + ce.Reply("Confirmed valid access token for %s / %s", resp.UserID, resp.DeviceID) + } + } } var CommandLogoutMatrix = &bridgev2.FullHandler{ @@ -64,11 +73,10 @@ var CommandLogoutMatrix = &bridgev2.FullHandler{ } func fnLogoutMatrix(ce *bridgev2.CommandEvent) { - //puppet := ce.User.GetIDoublePuppet() - //if puppet == nil || puppet.CustomIntent() == nil { - // ce.Reply("You don't have double puppeting enabled.") - // return - //} - //puppet.ClearCustomMXID() - //ce.Reply("Successfully disabled double puppeting.") + if ce.User.AccessToken == "" { + ce.Reply("You don't have double puppeting enabled.") + return + } + ce.User.LogoutDoublePuppet(ce.Ctx) + ce.Reply("Successfully disabled double puppeting.") } diff --git a/bridgev2/matrix/connector.go b/bridgev2/matrix/connector.go index bc1c9b8b..6511b6ac 100644 --- a/bridgev2/matrix/connector.go +++ b/bridgev2/matrix/connector.go @@ -105,7 +105,10 @@ func (br *Connector) Init(bridge *bridgev2.Bridge) { br.EventProcessor.On(event.StateMember, br.handleRoomEvent) br.Bot = br.AS.BotIntent() br.Crypto = NewCryptoHelper(br) - br.Bridge.Commands.AddHandlers(CommandDiscardMegolmSession, CommandSetPowerLevel) + br.Bridge.Commands.AddHandlers( + CommandDiscardMegolmSession, CommandSetPowerLevel, + CommandLoginMatrix, CommandPingMatrix, CommandLogoutMatrix, + ) br.Provisioning = &ProvisioningAPI{br: br} br.DoublePuppet = newDoublePuppetUtil(br) } @@ -367,9 +370,15 @@ func (br *Connector) FormatGhostMXID(userID networkid.UserID) id.UserID { return id.NewUserID(localpart, br.Config.Homeserver.Domain) } -func (br *Connector) UserIntent(user *bridgev2.User) bridgev2.MatrixAPI { - // TODO implement double puppeting - return nil +func (br *Connector) NewUserIntent(ctx context.Context, userID id.UserID, accessToken string) (bridgev2.MatrixAPI, string, error) { + intent, newToken, err := br.DoublePuppet.Setup(ctx, userID, accessToken) + if err != nil { + if errors.Is(err, ErrNoAccessToken) { + err = nil + } + return nil, accessToken, err + } + return &ASIntent{Connector: br, Matrix: intent}, newToken, nil } func (br *Connector) BotIntent() bridgev2.MatrixAPI { diff --git a/bridgev2/matrix/doublepuppet.go b/bridgev2/matrix/doublepuppet.go index 410b2652..ace33f30 100644 --- a/bridgev2/matrix/doublepuppet.go +++ b/bridgev2/matrix/doublepuppet.go @@ -8,9 +8,6 @@ package matrix import ( "context" - "crypto/hmac" - "crypto/sha512" - "encoding/hex" "errors" "fmt" "strings" @@ -24,8 +21,7 @@ import ( ) type doublePuppetUtil struct { - br *Connector - log zerolog.Logger + br *Connector discoveryCache map[string]string discoveryCacheLock sync.Mutex @@ -34,7 +30,6 @@ type doublePuppetUtil struct { func newDoublePuppetUtil(br *Connector) *doublePuppetUtil { return &doublePuppetUtil{ br: br, - log: br.Log.With().Str("component", "double puppet").Logger(), discoveryCache: make(map[string]string), } } @@ -58,7 +53,7 @@ func (dp *doublePuppetUtil) newClient(ctx context.Context, mxid id.UserID, acces } homeserverURL = resp.Homeserver.BaseURL dp.discoveryCache[homeserver] = homeserverURL - dp.log.Debug(). + zerolog.Ctx(ctx).Debug(). Str("homeserver", homeserver). Str("url", homeserverURL). Str("user_id", mxid.String()). @@ -85,47 +80,6 @@ func (dp *doublePuppetUtil) newIntent(ctx context.Context, mxid id.UserID, acces return ia, nil } -func (dp *doublePuppetUtil) autoLogin(ctx context.Context, mxid id.UserID, loginSecret string) (string, error) { - dp.log.Debug().Str("user_id", mxid.String()).Msg("Logging into user account with shared secret") - client, err := dp.newClient(ctx, mxid, "") - if err != nil { - return "", fmt.Errorf("failed to create mautrix client to log in: %v", err) - } - bridgeName := fmt.Sprintf("%s Bridge", dp.br.Bridge.Network.GetName().DisplayName) - req := mautrix.ReqLogin{ - Identifier: mautrix.UserIdentifier{Type: mautrix.IdentifierTypeUser, User: string(mxid)}, - DeviceID: id.DeviceID(bridgeName), - InitialDeviceDisplayName: bridgeName, - } - if loginSecret == "appservice" { - client.AccessToken = dp.br.AS.Registration.AppToken - req.Type = mautrix.AuthTypeAppservice - } else { - loginFlows, err := client.GetLoginFlows(ctx) - if err != nil { - return "", fmt.Errorf("failed to get supported login flows: %w", err) - } - mac := hmac.New(sha512.New, []byte(loginSecret)) - mac.Write([]byte(mxid)) - token := hex.EncodeToString(mac.Sum(nil)) - switch { - case loginFlows.HasFlow(mautrix.AuthTypeDevtureSharedSecret): - req.Type = mautrix.AuthTypeDevtureSharedSecret - req.Token = token - case loginFlows.HasFlow(mautrix.AuthTypePassword): - req.Type = mautrix.AuthTypePassword - req.Password = token - default: - return "", fmt.Errorf("no supported auth types for shared secret auth found") - } - } - resp, err := client.Login(ctx, &req) - if err != nil { - return "", err - } - return resp.AccessToken, nil -} - var ( ErrMismatchingMXID = errors.New("whoami result does not match custom mxid") ErrNoAccessToken = errors.New("no access token provided") @@ -135,14 +89,13 @@ var ( const useConfigASToken = "appservice-config" const asTokenModePrefix = "as_token:" -func (dp *doublePuppetUtil) Setup(ctx context.Context, mxid id.UserID, savedAccessToken string, reloginOnFail bool) (intent *appservice.IntentAPI, newAccessToken string, err error) { +func (dp *doublePuppetUtil) Setup(ctx context.Context, mxid id.UserID, savedAccessToken string) (intent *appservice.IntentAPI, newAccessToken string, err error) { if len(mxid) == 0 { err = ErrNoMXID return } _, homeserver, _ := mxid.Parse() loginSecret, hasSecret := dp.br.Config.DoublePuppet.Secrets[homeserver] - // Special case appservice: prefix to not login and use it as an as_token directly. if hasSecret && strings.HasPrefix(loginSecret, asTokenModePrefix) { intent, err = dp.newIntent(ctx, mxid, strings.TrimPrefix(loginSecret, asTokenModePrefix)) if err != nil { @@ -157,16 +110,9 @@ func (dp *doublePuppetUtil) Setup(ctx context.Context, mxid id.UserID, savedAcce } } return intent, useConfigASToken, err - } - if savedAccessToken == "" || savedAccessToken == useConfigASToken { - if reloginOnFail && hasSecret { - savedAccessToken, err = dp.autoLogin(ctx, mxid, loginSecret) - } else { - err = ErrNoAccessToken - } - if err != nil { - return - } + } else if savedAccessToken == "" || savedAccessToken == useConfigASToken { + err = ErrNoAccessToken + return } intent, err = dp.newIntent(ctx, mxid, savedAccessToken) if err != nil { @@ -174,17 +120,12 @@ func (dp *doublePuppetUtil) Setup(ctx context.Context, mxid id.UserID, savedAcce } var resp *mautrix.RespWhoami resp, err = intent.Whoami(ctx) - if err != nil { - if reloginOnFail && hasSecret && errors.Is(err, mautrix.MUnknownToken) { - intent.AccessToken, err = dp.autoLogin(ctx, mxid, loginSecret) - if err == nil { - newAccessToken = intent.AccessToken - } + if err == nil { + if resp.UserID != mxid { + err = ErrMismatchingMXID + } else { + newAccessToken = savedAccessToken } - } else if resp.UserID != mxid { - err = ErrMismatchingMXID - } else { - newAccessToken = savedAccessToken } return } diff --git a/bridgev2/matrixinterface.go b/bridgev2/matrixinterface.go index b791a028..c017eb92 100644 --- a/bridgev2/matrixinterface.go +++ b/bridgev2/matrixinterface.go @@ -24,7 +24,7 @@ type MatrixConnector interface { FormatGhostMXID(userID networkid.UserID) id.UserID GhostIntent(userID id.UserID) MatrixAPI - UserIntent(user *User) MatrixAPI + NewUserIntent(ctx context.Context, userID id.UserID, accessToken string) (MatrixAPI, string, error) BotIntent() MatrixAPI SendMessageStatus(ctx context.Context, status *MessageStatus, evt *MessageStatusEventInfo) diff --git a/bridgev2/portal.go b/bridgev2/portal.go index 504d3359..49ed3f96 100644 --- a/bridgev2/portal.go +++ b/bridgev2/portal.go @@ -691,12 +691,12 @@ func (portal *Portal) handleRemoteEvent(source *UserLogin, evt RemoteEvent) { func (portal *Portal) getIntentFor(ctx context.Context, sender EventSender, source *UserLogin) MatrixAPI { var intent MatrixAPI if sender.IsFromMe { - intent = portal.Bridge.Matrix.UserIntent(source.User) + intent = source.User.DoublePuppet(ctx) } if intent == nil && sender.SenderLogin != "" { senderLogin := portal.Bridge.GetCachedUserLoginByID(sender.SenderLogin) if senderLogin != nil { - intent = portal.Bridge.Matrix.UserIntent(senderLogin.User) + intent = senderLogin.User.DoublePuppet(ctx) } } if intent == nil { @@ -1184,7 +1184,7 @@ func (portal *Portal) SyncParticipants(ctx context.Context, members []networkid. for _, login := range loginsInPortal { if login.Client.IsThisUser(ctx, member) { isLoggedInUser = true - userIntent := portal.Bridge.Matrix.UserIntent(login.User) + userIntent := login.User.DoublePuppet(ctx) if userIntent != nil { expectedIntents[i] = userIntent } else { diff --git a/bridgev2/user.go b/bridgev2/user.go index 82d841c2..bf8eaf13 100644 --- a/bridgev2/user.go +++ b/bridgev2/user.go @@ -9,6 +9,7 @@ package bridgev2 import ( "context" "fmt" + "sync" "sync/atomic" "github.com/rs/zerolog" @@ -25,6 +26,10 @@ type User struct { CommandState atomic.Pointer[CommandState] + doublePuppetIntent MatrixAPI + doublePuppetInitialized bool + doublePuppetLock sync.Mutex + logins map[networkid.UserLoginID]*UserLogin } @@ -83,3 +88,67 @@ func (br *Bridge) GetExistingUserByMXID(ctx context.Context, userID id.UserID) ( defer br.cacheLock.Unlock() return br.unlockedGetUserByMXID(ctx, userID, true) } + +func (user *User) LogoutDoublePuppet(ctx context.Context) { + user.doublePuppetLock.Lock() + defer user.doublePuppetLock.Unlock() + user.AccessToken = "" + err := user.Save(ctx) + if err != nil { + zerolog.Ctx(ctx).Err(err).Msg("Failed to save removed access token") + } + user.doublePuppetIntent = nil + user.doublePuppetInitialized = false +} + +func (user *User) LoginDoublePuppet(ctx context.Context, token string) error { + if token == "" { + return fmt.Errorf("no token provided") + } + user.doublePuppetLock.Lock() + defer user.doublePuppetLock.Unlock() + intent, newToken, err := user.Bridge.Matrix.NewUserIntent(ctx, user.MXID, token) + if err != nil { + return err + } + user.AccessToken = newToken + user.doublePuppetIntent = intent + user.doublePuppetInitialized = true + err = user.Save(ctx) + if err != nil { + zerolog.Ctx(ctx).Err(err).Msg("Failed to save new access token") + } + if newToken != token { + return fmt.Errorf("logging in manually is not supported when automatic double puppeting is enabled") + } + return nil +} + +func (user *User) DoublePuppet(ctx context.Context) MatrixAPI { + user.doublePuppetLock.Lock() + defer user.doublePuppetLock.Unlock() + if user.doublePuppetInitialized { + return user.doublePuppetIntent + } + user.doublePuppetInitialized = true + log := user.Log.With().Str("action", "setup double puppet").Logger() + ctx = log.WithContext(ctx) + intent, newToken, err := user.Bridge.Matrix.NewUserIntent(ctx, user.MXID, user.AccessToken) + if err != nil { + log.Err(err).Msg("Failed to create new user intent") + return nil + } + user.doublePuppetIntent = intent + if newToken != user.AccessToken { + user.AccessToken = newToken + err = user.Save(ctx) + if err != nil { + log.Warn().Err(err).Msg("Failed to save new access token") + } + } + return intent +} + +func (user *User) Save(ctx context.Context) error { + return user.Bridge.DB.User.Update(ctx, user.User) +}