Mostly implement double puppeting

This commit is contained in:
Tulir Asokan 2024-06-11 17:04:06 +03:00
commit 1ff72aeffb
6 changed files with 125 additions and 98 deletions

View file

@ -26,12 +26,12 @@ func fnLoginMatrix(ce *bridgev2.CommandEvent) {
ce.Reply("**Usage:** `login-matrix <access token>`")
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.")
}

View file

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

View file

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

View file

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

View file

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

View file

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