From be4adcbd1712b6f51eba87411780079cb1c0ae52 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Tue, 30 Jun 2026 21:39:51 +0300 Subject: [PATCH 1/3] login: add support for passkeys --- go.mod | 4 +- go.sum | 8 ++-- pkg/connector/login.go | 105 ++++++++++++++++++++++++++++++++++++++--- 3 files changed, 105 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index 791f3e77..7a0809a5 100644 --- a/go.mod +++ b/go.mod @@ -12,13 +12,13 @@ require ( github.com/tidwall/gjson v1.19.0 go.mau.fi/util v0.9.10 go.mau.fi/webp v0.3.0 - go.mau.fi/whatsmeow v0.0.0-20260622124518-e94c58afd43b + go.mau.fi/whatsmeow v0.0.0-20260630180629-b572e5bcb92b golang.org/x/image v0.42.0 golang.org/x/net v0.56.0 golang.org/x/sync v0.21.0 google.golang.org/protobuf v1.36.11 gopkg.in/yaml.v3 v3.0.1 - maunium.net/go/mautrix v0.28.1 + maunium.net/go/mautrix v0.28.2-0.20260630183807-09c1bfd87299 ) require ( diff --git a/go.sum b/go.sum index 0112e109..7069892a 100644 --- a/go.sum +++ b/go.sum @@ -75,8 +75,8 @@ go.mau.fi/util v0.9.10 h1:wzvz5iDHyqDXB8vgisD4d3SzucLXNM3iNY+1O1RoHtg= go.mau.fi/util v0.9.10/go.mod h1:YQOxySn+ZE3qSYqNxvyX7Yi3suA8YK17PS6QqBREW7A= go.mau.fi/webp v0.3.0 h1:gVHQZtz21Ziwj+CDuklbX9mqpsnDIFKxs/BJyV7iZzA= go.mau.fi/webp v0.3.0/go.mod h1:rlZFTev+dYxhvk+XNBP/5GcTt4gXmzAB4DU0aGUYIQo= -go.mau.fi/whatsmeow v0.0.0-20260622124518-e94c58afd43b h1:SeXzfZd3zFyHg+JEDOb5NZqMJRCRK5NXtXy+qzDJGSs= -go.mau.fi/whatsmeow v0.0.0-20260622124518-e94c58afd43b/go.mod h1:9dmNTYZ/1pHjPw/bz+azBsGjAkcrZbqzMrKcvG5bJ8U= +go.mau.fi/whatsmeow v0.0.0-20260630180629-b572e5bcb92b h1:ZUk1ErarDNpnbosXR/MeOz2gkqA4S1bh8zjaSRj7N+Y= +go.mau.fi/whatsmeow v0.0.0-20260630180629-b572e5bcb92b/go.mod h1:9dmNTYZ/1pHjPw/bz+azBsGjAkcrZbqzMrKcvG5bJ8U= go.mau.fi/zeroconfig v0.2.0 h1:e/OGEERqVRRKlgaro7E6bh8xXiKFSXB3eNNIud7FUjU= go.mau.fi/zeroconfig v0.2.0/go.mod h1:J0Vn0prHNOm493oZoQ84kq83ZaNCYZnq+noI1b1eN8w= golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= @@ -107,5 +107,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M= maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA= -maunium.net/go/mautrix v0.28.1 h1:Hic3oDMPbLbQu1fhboTRAKZcORMjzzkjxsa+SGk60b0= -maunium.net/go/mautrix v0.28.1/go.mod h1:mWXQNmOlrq4VTDU9f1HO03BSIswdUIyyY4wUKHqwzzY= +maunium.net/go/mautrix v0.28.2-0.20260630183807-09c1bfd87299 h1:qNqSM7czMzSlVOwb5CJuV+1yEXrhwIk+hDyvJ7X5eks= +maunium.net/go/mautrix v0.28.2-0.20260630183807-09c1bfd87299/go.mod h1:mWXQNmOlrq4VTDU9f1HO03BSIswdUIyyY4wUKHqwzzY= diff --git a/pkg/connector/login.go b/pkg/connector/login.go index b70ab1a7..5da419f6 100644 --- a/pkg/connector/login.go +++ b/pkg/connector/login.go @@ -2,6 +2,7 @@ package connector import ( "context" + "encoding/json" "errors" "fmt" "net/http" @@ -12,6 +13,7 @@ import ( "go.mau.fi/util/exsync" "go.mau.fi/util/jsontime" "go.mau.fi/whatsmeow" + "go.mau.fi/whatsmeow/types" "go.mau.fi/whatsmeow/types/events" waLog "go.mau.fi/whatsmeow/util/log" "maunium.net/go/mautrix/bridgev2" @@ -25,6 +27,7 @@ const ( LoginStepIDQR = "fi.mau.whatsapp.login.qr" LoginStepIDPhoneNumber = "fi.mau.whatsapp.login.phone" LoginStepIDCode = "fi.mau.whatsapp.login.code" + LoginStepIDPasskey = "fi.mau.whatsapp.login.passkey" LoginStepIDComplete = "fi.mau.whatsapp.login.complete" LoginFlowIDQR = "qr" @@ -91,9 +94,11 @@ func (wa *WhatsAppConnector) CreateLogin(_ context.Context, user *bridgev2.User, Bool("phone_code", flowID == LoginFlowIDPhone). Logger(), - WaitForQRs: exsync.NewEvent(), - LoginComplete: exsync.NewEvent(), - Received515: exsync.NewEvent(), + WaitForQRs: exsync.NewEvent(), + LoginComplete: exsync.NewEvent(), + PasskeyRequest: exsync.NewEvent(), + PasskeyConfirmation: exsync.NewEvent(), + Received515: exsync.NewEvent(), }, nil } @@ -114,6 +119,11 @@ type WALogin struct { Received515 *exsync.Event PrevQRIndex atomic.Int32 + PasskeyRequest *exsync.Event + PasskeyRequestData *events.PairPasskeyRequest + PasskeyConfirmation *exsync.Event + PasskeyConfirmationData *events.PairPasskeyConfirmation + Closed atomic.Bool EventHandlerID uint32 } @@ -122,6 +132,7 @@ var ( _ bridgev2.LoginProcessDisplayAndWait = (*WALogin)(nil) _ bridgev2.LoginProcessUserInput = (*WALogin)(nil) _ bridgev2.LoginProcessWithOverride = (*WALogin)(nil) + _ bridgev2.LoginProcessWebAuthn = (*WALogin)(nil) ) const LoginConnectWait = 15 * time.Second @@ -262,6 +273,17 @@ func (wl *WALogin) handleEvent(rawEvt any) { case *events.ClientOutdated: wl.Log.Error().Msg("Got client outdated error") wl.LoginError = ErrLoginClientOutdated + case *events.PairPasskeyRequest: + wl.PasskeyRequestData = evt + wl.PasskeyRequest.Set() + return + case *events.PairPasskeyConfirmation: + wl.PasskeyConfirmationData = evt + wl.PasskeyConfirmation.Set() + return + case *events.PairPasskeyError: + wl.Log.Err(evt.Error).Msg("Got passkey error") + wl.LoginError = evt.Error case *events.PairSuccess: wl.Log.Info().Any("event_data", evt).Msg("Got pair successful event") wl.LoginSuccess = evt @@ -289,10 +311,14 @@ func (wl *WALogin) handleEvent(rawEvt any) { func (wl *WALogin) Wait(ctx context.Context) (*bridgev2.LoginStep, error) { if wl.PhoneCode { - err := wl.LoginComplete.Wait(ctx) - if err != nil { + select { + case <-ctx.Done(): wl.Cancel() - return nil, err + return nil, ctx.Err() + case <-wl.PasskeyRequest.GetChan(): + return wl.makePasskeyStep() + case <-wl.LoginComplete.GetChan(): + // continue } } else { prevIndex := int(wl.PrevQRIndex.Load()) @@ -319,9 +345,16 @@ func (wl *WALogin) Wait(ctx context.Context) (*bridgev2.LoginStep, error) { case <-ctx.Done(): wl.Cancel() return nil, ctx.Err() + case <-wl.PasskeyRequest.GetChan(): + return wl.makePasskeyStep() case <-wl.LoginComplete.GetChan(): + // continue } } + return wl.onLoginComplete(ctx) +} + +func (wl *WALogin) onLoginComplete(ctx context.Context) (*bridgev2.LoginStep, error) { if wl.LoginError != nil { wl.Log.Debug().Err(wl.LoginError).Msg("Login completed with error") wl.Cancel() @@ -372,6 +405,66 @@ func (wl *WALogin) Wait(ctx context.Context) (*bridgev2.LoginStep, error) { }, nil } +func (wl *WALogin) makePasskeyStep() (*bridgev2.LoginStep, error) { + pubkeyData, err := json.Marshal(wl.PasskeyRequestData.PublicKey) + if err != nil { + wl.Log.Err(err).Msg("Failed to marshal public key data") + return nil, err + } + return &bridgev2.LoginStep{ + Type: bridgev2.LoginStepTypeWebAuthn, + StepID: LoginStepIDPasskey, + WebAuthnParams: &bridgev2.LoginWebAuthnParams{ + URL: "https://web.whatsapp.com", + PublicKey: pubkeyData, + }, + }, nil +} + +func (wl *WALogin) SubmitWebAuthnResponse(ctx context.Context, rawResp json.RawMessage) (*bridgev2.LoginStep, error) { + var resp types.WebAuthnResponse + err := json.Unmarshal(rawResp, &resp) + if err != nil { + wl.Log.Err(err).Msg("Failed to unmarshal WebAuthn response") + wl.Cancel() + return nil, err + } + err = wl.Client.SendPasskeyResponse(ctx, &resp) + if err != nil { + wl.Log.Err(err).Msg("Failed to send WebAuthn response") + wl.Cancel() + return nil, err + } + select { + case <-ctx.Done(): + wl.Cancel() + return nil, ctx.Err() + case <-wl.PasskeyConfirmation.GetChan(): + if !wl.PasskeyConfirmationData.SkipHandoffUX { + wl.Cancel() + return nil, bridgev2.RespError{ + ErrCode: "FI.MAU.WHATSAPP.NOT_IMPLEMENTED", + Err: "Displaying WebAuthn pairing confirmation codes is not yet implemented", + StatusCode: http.StatusBadRequest, + } + } + err = wl.Client.SendPasskeyConfirmation(ctx) + if err != nil { + wl.Log.Err(err).Msg("Failed to send WebAuthn confirmation") + wl.Cancel() + return nil, err + } + case <-wl.LoginComplete.GetChan(): + } + select { + case <-ctx.Done(): + wl.Cancel() + return nil, ctx.Err() + case <-wl.LoginComplete.GetChan(): + } + return wl.onLoginComplete(ctx) +} + func (wl *WALogin) Cancel() { wl.Closed.Store(true) wl.Client.RemoveEventHandler(wl.EventHandlerID) From d28aea648df649fcb03dc0704bf796f40c201e06 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Wed, 1 Jul 2026 00:19:20 +0300 Subject: [PATCH 2/3] Update mautrix-go --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 7a0809a5..9d8a528f 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( golang.org/x/sync v0.21.0 google.golang.org/protobuf v1.36.11 gopkg.in/yaml.v3 v3.0.1 - maunium.net/go/mautrix v0.28.2-0.20260630183807-09c1bfd87299 + maunium.net/go/mautrix v0.28.2-0.20260630211833-462862143e8b ) require ( diff --git a/go.sum b/go.sum index 7069892a..7a867460 100644 --- a/go.sum +++ b/go.sum @@ -107,5 +107,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M= maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA= -maunium.net/go/mautrix v0.28.2-0.20260630183807-09c1bfd87299 h1:qNqSM7czMzSlVOwb5CJuV+1yEXrhwIk+hDyvJ7X5eks= -maunium.net/go/mautrix v0.28.2-0.20260630183807-09c1bfd87299/go.mod h1:mWXQNmOlrq4VTDU9f1HO03BSIswdUIyyY4wUKHqwzzY= +maunium.net/go/mautrix v0.28.2-0.20260630211833-462862143e8b h1:9CdHJu38bRNALRucssrwefHxZEyLij7b/YxFRQtJSs8= +maunium.net/go/mautrix v0.28.2-0.20260630211833-462862143e8b/go.mod h1:mWXQNmOlrq4VTDU9f1HO03BSIswdUIyyY4wUKHqwzzY= From c9ca762275a42a01d5215227113a5aaa0826b3e6 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Wed, 1 Jul 2026 22:57:54 +0300 Subject: [PATCH 3/3] Update mautrix-go --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 9d8a528f..c16824cb 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( golang.org/x/sync v0.21.0 google.golang.org/protobuf v1.36.11 gopkg.in/yaml.v3 v3.0.1 - maunium.net/go/mautrix v0.28.2-0.20260630211833-462862143e8b + maunium.net/go/mautrix v0.28.2-0.20260701195654-d33ead4e1d86 ) require ( diff --git a/go.sum b/go.sum index 7a867460..d38c8cb1 100644 --- a/go.sum +++ b/go.sum @@ -107,5 +107,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M= maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA= -maunium.net/go/mautrix v0.28.2-0.20260630211833-462862143e8b h1:9CdHJu38bRNALRucssrwefHxZEyLij7b/YxFRQtJSs8= -maunium.net/go/mautrix v0.28.2-0.20260630211833-462862143e8b/go.mod h1:mWXQNmOlrq4VTDU9f1HO03BSIswdUIyyY4wUKHqwzzY= +maunium.net/go/mautrix v0.28.2-0.20260701195654-d33ead4e1d86 h1:OKNg/klCID3MoZHuhpMSQThvWOqKtbCrXC6HjlY4+QA= +maunium.net/go/mautrix v0.28.2-0.20260701195654-d33ead4e1d86/go.mod h1:mWXQNmOlrq4VTDU9f1HO03BSIswdUIyyY4wUKHqwzzY=