Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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.20260701195654-d33ead4e1d86
)

require (
Expand Down
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down Expand Up @@ -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.20260701195654-d33ead4e1d86 h1:OKNg/klCID3MoZHuhpMSQThvWOqKtbCrXC6HjlY4+QA=
maunium.net/go/mautrix v0.28.2-0.20260701195654-d33ead4e1d86/go.mod h1:mWXQNmOlrq4VTDU9f1HO03BSIswdUIyyY4wUKHqwzzY=
105 changes: 99 additions & 6 deletions pkg/connector/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package connector

import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
Expand All @@ -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"
Expand All @@ -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"
Expand Down Expand Up @@ -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
}

Expand All @@ -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
}
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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())
Expand All @@ -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()
Expand Down Expand Up @@ -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)
Expand Down
Loading