Merge branch 'tulir/google-account-pairing'

This commit is contained in:
Tulir Asokan 2024-02-23 21:20:16 +02:00
commit 00488409ed
43 changed files with 4743 additions and 662 deletions

View file

@ -27,6 +27,7 @@ const (
GMListenError status.BridgeStateErrorCode = "gm-listen-error"
GMFatalError status.BridgeStateErrorCode = "gm-listen-fatal-error"
GMUnpaired status.BridgeStateErrorCode = "gm-unpaired"
GMUnpairedGaia status.BridgeStateErrorCode = "gm-unpaired-gaia"
GMUnpaired404 status.BridgeStateErrorCode = "gm-unpaired-entity-not-found"
GMNotConnected status.BridgeStateErrorCode = "gm-not-connected"
GMConnecting status.BridgeStateErrorCode = "gm-connecting"
@ -49,12 +50,13 @@ func init() {
GMFatalError: "Fatal error polling messages from Google Messages server, please re-link the bridge",
GMUnpaired: "Unpaired from Google Messages, please re-link the connection to continue using SMS/RCS",
GMUnpaired404: "Unpaired from Google Messages, please re-link the connection to continue using SMS/RCS",
GMUnpairedGaia: "Unpaired from Google Messages, please re-link the connection to continue using SMS/RCS",
GMNotDefaultSMSApp: "Google Messages isn't set as the default SMS app. Please set the default SMS app on your Android phone to Google Messages to continue using SMS/RCS.",
GMBrowserInactive: "Google Messages opened in another browser",
GMBrowserInactiveTimeout: "Google Messages disconnected due to timeout",
GMBrowserInactiveInactivity: "Google Messages disconnected due to inactivity",
GMPhoneNotResponding: "Your phone is not responding, please check that it is connected to the internet. You may need to open the Messages app on your phone for it to reconnect.",
GMSwitchedToGoogleLogin: "Please switch to the QR code pairing method in the Google Messages app to continue using SMS/RCS",
GMSwitchedToGoogleLogin: "You switched to Google account pairing, please log in to continue using SMS/RCS",
})
}

View file

@ -17,6 +17,7 @@
package main
import (
"encoding/json"
"fmt"
"strings"
@ -40,7 +41,8 @@ type WrappedCommandEvent struct {
func (br *GMBridge) RegisterCommands() {
proc := br.CommandProcessor.(*commands.Processor)
proc.AddHandlers(
cmdLogin,
cmdLoginQR,
cmdLoginGoogle,
cmdDeleteSession,
cmdLogout,
cmdReconnect,
@ -70,16 +72,17 @@ var (
HelpSectionPortalManagement = commands.HelpSection{Name: "Portal management", Order: 20}
)
var cmdLogin = &commands.FullHandler{
Func: wrapCommand(fnLogin),
Name: "login",
var cmdLoginQR = &commands.FullHandler{
Func: wrapCommand(fnLoginQR),
Name: "login-qr",
Aliases: []string{"login"},
Help: commands.HelpMeta{
Section: commands.HelpSectionAuth,
Description: "Link the bridge to Google Messages on your Android phone as a web client.",
Description: "Link the bridge to Google Messages on your Android phone by scanning a QR code.",
},
}
func fnLogin(ce *WrappedCommandEvent) {
func fnLoginQR(ce *WrappedCommandEvent) {
if ce.User.Session != nil {
if ce.User.IsConnected() {
ce.Reply("You're already logged in")
@ -123,6 +126,64 @@ func fnLogin(ce *WrappedCommandEvent) {
ce.ZLog.Trace().Msg("Login command finished")
}
var cmdLoginGoogle = &commands.FullHandler{
Func: wrapCommand(fnLoginGoogle),
Name: "login-google",
Aliases: []string{"login"},
Help: commands.HelpMeta{
Section: commands.HelpSectionAuth,
Description: "Link the bridge to Google Messages on your Android phone by logging in with your Google account.",
},
}
func fnLoginGoogle(ce *WrappedCommandEvent) {
if ce.User.Session != nil {
if ce.User.IsConnected() {
ce.Reply("You're already logged in")
} else {
ce.Reply("You're already logged in. Perhaps you wanted to `reconnect`?")
}
return
} else if ce.User.pairSuccessChan != nil {
ce.Reply("You already have a login in progress")
return
}
ce.User.CommandState = &commands.CommandState{
Next: commands.MinimalHandlerFunc(wrapCommand(fnLoginGoogleCookies)),
Action: "Login",
}
ce.Reply("Send your Google cookies here, formatted as a key-value JSON object (see <https://docs.mau.fi/bridges/go/gmessages/authentication.html> for details)")
}
func fnLoginGoogleCookies(ce *WrappedCommandEvent) {
ce.User.CommandState = nil
if ce.User.Session != nil {
if ce.User.IsConnected() {
ce.Reply("You're already logged in")
} else {
ce.Reply("You're already logged in. Perhaps you wanted to `reconnect`?")
}
return
} else if ce.User.pairSuccessChan != nil {
ce.Reply("You already have a login in progress")
return
}
var cookies map[string]string
err := json.Unmarshal([]byte(ce.RawArgs), &cookies)
if err != nil {
ce.Reply("Failed to parse cookies: %v", err)
return
}
err = ce.User.LoginGoogle(cookies, func(emoji string) {
ce.Reply(emoji)
})
if err != nil {
ce.Reply("Login failed: %v", err)
} else {
ce.Reply("Login successful")
}
}
func (user *User) sendQREdit(ce *WrappedCommandEvent, content *event.MessageEventContent, prevEvent id.EventID) id.EventID {
if len(prevEvent) != 0 {
content.SetEdit(prevEvent)

16
go.mod
View file

@ -1,17 +1,17 @@
module go.mau.fi/mautrix-gmessages
go 1.20
go 1.21
require (
github.com/gabriel-vasile/mimetype v1.4.3
github.com/lib/pq v1.10.9
github.com/mattn/go-sqlite3 v1.14.19
github.com/rs/zerolog v1.31.0
github.com/mattn/go-sqlite3 v1.14.22
github.com/rs/zerolog v1.32.0
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
go.mau.fi/mautrix-gmessages/libgm v0.2.2
go.mau.fi/util v0.2.1
golang.org/x/exp v0.0.0-20231214170342-aacd6d4b4611
google.golang.org/protobuf v1.31.0
golang.org/x/exp v0.0.0-20240213143201-ec583247a57a
google.golang.org/protobuf v1.32.0
maunium.net/go/maulogger/v2 v2.4.1
maunium.net/go/mautrix v0.16.2
)
@ -19,7 +19,7 @@ require (
require (
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/google/uuid v1.4.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/mux v1.8.0 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/kr/pretty v0.3.1 // indirect
@ -32,9 +32,9 @@ require (
github.com/tidwall/sjson v1.2.5 // indirect
github.com/yuin/goldmark v1.6.0 // indirect
go.mau.fi/zeroconfig v0.1.2 // indirect
golang.org/x/crypto v0.15.0 // indirect
golang.org/x/crypto v0.19.0 // indirect
golang.org/x/net v0.18.0 // indirect
golang.org/x/sys v0.14.0 // indirect
golang.org/x/sys v0.17.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect

36
go.sum
View file

@ -1,17 +1,17 @@
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
@ -30,20 +30,22 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.19 h1:fhGleo2h1p8tVChob4I9HpmVFIAkKGpiukdrgQbWfGI=
github.com/mattn/go-sqlite3 v1.14.19/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0=
github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
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/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.17.0 h1:/Jocvlh98kcTfpN2+JzGQWQcqrPQwDrVEMApx/M5ZwM=
github.com/tidwall/gjson v1.17.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
@ -59,21 +61,19 @@ go.mau.fi/util v0.2.1 h1:eazulhFE/UmjOFtPrGg6zkF5YfAyiDzQb8ihLMbsPWw=
go.mau.fi/util v0.2.1/go.mod h1:MjlzCQEMzJ+G8RsPawHzpLB8rwTo3aPIjG5FzBvQT/c=
go.mau.fi/zeroconfig v0.1.2 h1:DKOydWnhPMn65GbXZOafgkPm11BvFashZWLct0dGFto=
go.mau.fi/zeroconfig v0.1.2/go.mod h1:NcSJkf180JT+1IId76PcMuLTNa1CzsFFZ0nBygIQM70=
golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA=
golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g=
golang.org/x/exp v0.0.0-20231214170342-aacd6d4b4611 h1:qCEDpW1G+vcj3Y7Fy52pEM1AWm3abj8WimGYejI3SC4=
golang.org/x/exp v0.0.0-20231214170342-aacd6d4b4611/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI=
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/exp v0.0.0-20240213143201-ec583247a57a h1:HinSgX1tJRX3KsL//Gxynpw5CTOAIPhgL4W8PNiIpVE=
golang.org/x/exp v0.0.0-20240213143201-ec583247a57a/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg=
golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=

View file

@ -8,7 +8,6 @@ import (
"io"
"net/http"
"net/url"
"strconv"
"time"
"github.com/google/uuid"
@ -34,6 +33,18 @@ type AuthData struct {
TachyonTTL int64 `json:"tachyon_ttl,omitempty"`
// Unknown encryption key, not used for anything
WebEncryptionKey []byte `json:"web_encryption_key,omitempty"`
SessionID uuid.UUID `json:"session_id,omitempty"`
DestRegID uuid.UUID `json:"dest_reg_id,omitempty"`
PairingID uuid.UUID `json:"pairing_id,omitempty"`
Cookies map[string]string `json:"cookies,omitempty"`
}
func (ad *AuthData) AuthNetwork() string {
if ad.Cookies != nil {
return util.GoogleNetwork
}
return ""
}
const RefreshTachyonBuffer = 1 * time.Hour
@ -64,6 +75,7 @@ type Client struct {
conversationsFetchedOnce bool
AuthData *AuthData
cfg *gmproto.Config
proxy Proxy
http *http.Client
@ -89,9 +101,15 @@ func NewClient(authData *AuthData, logger zerolog.Logger) *Client {
pingShortCircuit: make(chan struct{}),
}
sessionHandler.client = cli
err := cli.FetchConfigVersion()
var err error
cli.cfg, err = cli.FetchConfig()
if err != nil {
cli.Logger.Warn().Err(err).Msg("Failed to fetch latest web version")
cli.Logger.Err(err).Msg("Failed to fetch web config")
} else if deviceID := cli.cfg.GetDeviceInfo().GetDeviceID(); deviceID != "" {
authData.SessionID, err = uuid.Parse(deviceID)
if err != nil {
cli.Logger.Err(err).Str("device_id", deviceID).Msg("Failed to parse device ID")
}
}
return cli
}
@ -130,11 +148,11 @@ func (c *Client) Connect() error {
return fmt.Errorf("failed to refresh auth token: %w", err)
}
webEncryptionKeyResponse, err := c.GetWebEncryptionKey()
if err != nil {
return fmt.Errorf("failed to get web encryption key: %w", err)
}
c.updateWebEncryptionKey(webEncryptionKeyResponse.GetKey())
//webEncryptionKeyResponse, err := c.GetWebEncryptionKey()
//if err != nil {
// return fmt.Errorf("failed to get web encryption key: %w", err)
//}
//c.updateWebEncryptionKey(webEncryptionKeyResponse.GetKey())
go c.doLongPoll(true)
c.sessionHandler.startAckInterval()
go c.postConnect()
@ -150,6 +168,10 @@ func (c *Client) postConnect() {
})
return
}
if c.AuthData.Mobile.Network != util.QRNetwork {
// Don't check bugle default unless using bugle
return
}
doneChan := make(chan struct{})
go func() {
@ -166,7 +188,6 @@ func (c *Client) postConnect() {
return
}
c.Logger.Debug().Bool("bugle_default", bugleRes.Success).Msg("Got is bugle default response on connect")
}
func (c *Client) Disconnect() {
@ -200,25 +221,29 @@ func (c *Client) triggerEvent(evt interface{}) {
}
}
func (c *Client) FetchConfigVersion() error {
func (c *Client) FetchConfig() (*gmproto.Config, error) {
req, err := http.NewRequest(http.MethodGet, util.ConfigURL, nil)
if err != nil {
return fmt.Errorf("failed to prepare request: %w", err)
return nil, fmt.Errorf("failed to prepare request: %w", err)
}
util.BuildRelayHeaders(req, "", "*/*")
req.Header.Set("sec-fetch-site", "same-origin")
req.Header.Del("x-user-agent")
req.Header.Del("origin")
c.AddCookieHeaders(req)
configRes, err := c.http.Do(req)
resp, err := c.http.Do(req)
if resp != nil {
c.HandleCookieUpdates(resp)
}
config, err := typedHTTPResponse[*gmproto.Config](resp, err)
if err != nil {
return fmt.Errorf("failed to send request: %w", err)
return nil, err
}
responseBody, err := io.ReadAll(configRes.Body)
if err != nil {
return fmt.Errorf("failed to read response body: %w", err)
}
version, parseErr := util.ParseConfigVersion(responseBody)
version, parseErr := config.ParsedClientVersion()
if parseErr != nil {
return fmt.Errorf("failed to parse response body: %w", err)
return nil, fmt.Errorf("failed to parse client version: %w", err)
}
currVersion := util.ConfigMessage
@ -228,7 +253,8 @@ func (c *Client) FetchConfigVersion() error {
} else {
c.Logger.Debug().Any("version", currVersion).Msg("Using latest messages for web version")
}
return nil
return config, nil
}
func (c *Client) diffVersionFormat(curr *gmproto.ConfigVersion, latest *gmproto.ConfigVersion) string {
@ -240,17 +266,17 @@ func (c *Client) updateWebEncryptionKey(key []byte) {
c.AuthData.WebEncryptionKey = key
}
func (c *Client) updateTachyonAuthToken(t []byte, validFor int64) {
c.AuthData.TachyonAuthToken = t
validForDuration := time.Duration(validFor) * time.Microsecond
func (c *Client) updateTachyonAuthToken(data *gmproto.TokenData) {
c.AuthData.TachyonAuthToken = data.GetTachyonAuthToken()
validForDuration := time.Duration(data.GetTTL()) * time.Microsecond
if validForDuration == 0 {
validForDuration = 24 * time.Hour
}
c.AuthData.TachyonExpiry = time.Now().UTC().Add(time.Microsecond * time.Duration(validFor))
c.AuthData.TachyonExpiry = time.Now().UTC().Add(validForDuration)
c.AuthData.TachyonTTL = validForDuration.Microseconds()
c.Logger.Debug().
Time("tachyon_expiry", c.AuthData.TachyonExpiry).
Int64("valid_for", validFor).
Int64("valid_for", data.GetTTL()).
Msg("Updated tachyon token")
}
@ -275,6 +301,7 @@ func (c *Client) refreshAuthToken() error {
MessageAuth: &gmproto.AuthMessage{
RequestID: requestID,
TachyonAuthToken: c.AuthData.TachyonAuthToken,
Network: c.AuthData.AuthNetwork(),
ConfigVersion: util.ConfigMessage,
},
CurrBrowserDevice: c.AuthData.Browser,
@ -291,14 +318,11 @@ func (c *Client) refreshAuthToken() error {
return err
}
token := resp.GetTokenData().GetTachyonAuthToken()
if token == nil {
if resp.GetTokenData().GetTachyonAuthToken() == nil {
return fmt.Errorf("no tachyon auth token in refresh response")
}
validFor, _ := strconv.ParseInt(resp.GetTokenData().GetValidFor(), 10, 64)
c.updateTachyonAuthToken(token, validFor)
c.updateTachyonAuthToken(resp.GetTokenData())
c.triggerEvent(&events.AuthTokenRefreshed{})
return nil
}

View file

@ -1,6 +1,7 @@
package libgm
import (
"bytes"
"crypto/sha256"
"encoding/base64"
"fmt"
@ -18,6 +19,7 @@ type IncomingRPCMessage struct {
IsOld bool
Pair *gmproto.RPCPairData
Gaia *gmproto.RPCGaiaData
Message *gmproto.RPCMessageData
DecryptedData []byte
@ -57,6 +59,15 @@ func (c *Client) decryptInternalMessage(data *gmproto.IncomingRPCMessage) (*Inco
Msg("Errored pair event content")
return nil, fmt.Errorf("failed to decode pair event: %w", err)
}
case gmproto.BugleRoute_GaiaEvent:
msg.Gaia = &gmproto.RPCGaiaData{}
err := proto.Unmarshal(data.GetMessageData(), msg.Gaia)
if err != nil {
c.Logger.Trace().
Str("data", base64.StdEncoding.EncodeToString(msg.GetMessageData())).
Msg("Errored gaia event content")
return nil, fmt.Errorf("failed to decode gaia event: %w", err)
}
case gmproto.BugleRoute_DataEvent:
msg.Message = &gmproto.RPCMessageData{}
err := proto.Unmarshal(data.GetMessageData(), msg.Message)
@ -176,6 +187,8 @@ func (c *Client) HandleRPCMsg(rawMsg *gmproto.IncomingRPCMessage) {
switch msg.BugleRoute {
case gmproto.BugleRoute_PairEvent:
c.handlePairingEvent(msg)
case gmproto.BugleRoute_GaiaEvent:
c.handleGaiaPairingEvent(msg)
case gmproto.BugleRoute_DataEvent:
if c.skipCount > 0 {
c.skipCount--
@ -191,9 +204,15 @@ type WrappedMessage struct {
Data []byte
}
var hackyLoggedOutBytes = []byte{0x72, 0x00}
func (c *Client) handleUpdatesEvent(msg *IncomingRPCMessage) {
switch msg.Message.Action {
case gmproto.ActionType_GET_UPDATES:
if msg.DecryptedData == nil && bytes.Equal(msg.Message.UnencryptedData, hackyLoggedOutBytes) {
c.triggerEvent(&events.GaiaLoggedOut{})
return
}
data, ok := msg.DecryptedMessage.(*gmproto.UpdateEvents)
if !ok {
c.Logger.Error().
@ -256,11 +275,13 @@ func (c *Client) handleUpdatesEvent(msg *IncomingRPCMessage) {
default:
c.Logger.Warn().
Str("evt_data", base64.StdEncoding.EncodeToString(msg.DecryptedData)).
Str("evt_data", base64.StdEncoding.EncodeToString(msg.GetMessageData())).
Str("decrypted_data", base64.StdEncoding.EncodeToString(msg.DecryptedData)).
Msg("Got unknown event type")
}
default:
c.Logger.Debug().
Str("evt_data", base64.StdEncoding.EncodeToString(msg.GetMessageData())).
Str("request_id", msg.Message.SessionID).
Str("action_type", msg.Message.Action.String()).
Bool("is_old", msg.IsOld).

View file

@ -15,6 +15,8 @@ type ClientReady struct {
type AuthTokenRefreshed struct{}
type GaiaLoggedOut struct{}
type AccountChange struct {
*gmproto.AccountChangeOrSomethingEvent
IsFake bool

File diff suppressed because it is too large Load diff

Binary file not shown.

View file

@ -4,6 +4,7 @@ package authentication;
option go_package = "../gmproto";
import "util.proto";
import "pblite.proto";
enum BrowserType {
UNKNOWN_BROWSER_TYPE = 0;
@ -44,6 +45,95 @@ message ConfigVersion {
int32 V2 = 9;
}
message SignInGaiaRequest {
message Inner {
message DeviceID {
int32 unknownInt1 = 1; // 3
string deviceID = 2; // messages-web-{uuid without dashes}
}
message Data {
bytes someData = 3; // maybe an encryption key?
}
DeviceID deviceID = 1;
Data someData = 36 [(pblite.pblite_binary) = true];
}
AuthMessage authMessage = 1;
Inner inner = 2;
int32 unknownInt3 = 3;
string network = 4;
}
message SignInGaiaResponse {
message Header {
uint64 unknownInt2 = 2;
int64 unknownTimestamp = 4;
}
message DeviceData {
message DeviceWrapper {
Device device = 1;
}
DeviceWrapper deviceWrapper = 1;
repeated RPCGaiaData.UnknownContainer.Item2.Item1 unknownItems2 = 2;
repeated RPCGaiaData.UnknownContainer.Item4 unknownItems3 = 3;
// index 4 is some unknown field with no real data
}
Header header = 1;
string maybeBrowserUUID = 2 [(pblite.pblite_binary) = true];
DeviceData deviceData = 3;
TokenData tokenData = 4;
}
message GaiaPairingRequestContainer {
string pairingAttemptID = 1;
BrowserDetails browserDetails = 2;
int64 startTimestamp = 3;
bytes data = 4;
}
message GaiaPairingResponseContainer {
int32 finishErrorType = 1;
int32 finishErrorCode = 2;
int32 unknownInt3 = 3; // For init, 1
string sessionUUID = 4;
bytes data = 5;
}
message RevokeGaiaPairingRequest {
string pairingAttemptID = 1;
}
message RPCGaiaData {
message UnknownContainer {
message Item2 {
message Item1 {
string destOrSourceUUID = 1 [(pblite.pblite_binary) = true];
int32 unknownInt4 = 4; // 1 for destination device, 6 for local device?
string languageCode = 5;
uint64 unknownBigInt7 = 7;
}
repeated Item1 item1 = 1;
}
message Item4 {
message Item8 {
int32 unknownInt1 = 1; // present for destination device?
int32 unknownTimestamp = 2; // present for destination device?
bytes unknownBytes = 3; // present for local device?
}
string destOrSourceUUID = 1 [(pblite.pblite_binary) = true];
int32 unknownInt3 = 3; // 1 for destination device, 6 for local device?
int32 unknownInt4 = 4; // always 6?
int64 unknownTimestampMicroseconds = 7; // maybe device creation ts?
Item8 item8 = 8 [(pblite.pblite_binary) = true];
}
Item2 item2 = 2;
int64 unknownTimestampMicroseconds = 3; // pairing timestamp?
repeated Item4 item4 = 4;
}
int32 command = 1; // 9
UnknownContainer maybeServerData = 108;
}
message AuthenticationContainer {
AuthMessage authMessage = 1;
BrowserDetails browserDetails = 3;
@ -56,8 +146,8 @@ message AuthenticationContainer {
message AuthMessage {
string requestID = 1;
optional string network = 3;
optional bytes tachyonAuthToken = 6;
string network = 3;
bytes tachyonAuthToken = 6;
ConfigVersion configVersion = 7;
}
@ -84,25 +174,15 @@ message RegisterRefreshRequest {
}
message RegisterRefreshResponse {
message AuthKeyData {
bytes tachyonAuthToken = 1;
string validFor = 2;
}
AuthKeyData tokenData = 2;
TokenData tokenData = 2;
}
message RegisterPhoneRelayResponse {
message AuthKeyData {
bytes tachyonAuthToken = 1;
int64 validFor = 2;
}
CoordinateMessage coordinates = 1;
Device browser = 2;
bytes pairingKey = 3;
int64 validFor = 4;
AuthKeyData authKeyData = 5;
TokenData authKeyData = 5;
string responseID = 6;
}

View file

@ -0,0 +1,43 @@
package gmproto
import (
"strconv"
)
func (c *Config) ParsedClientVersion() (*ConfigVersion, error) {
version := c.ClientVersion
v1 := version[0:4]
v2 := version[4:6]
v3 := version[6:8]
if v2[0] == 48 {
v2 = string(v2[1])
}
if v3[0] == 48 {
v3 = string(v3[1])
}
first, e := strconv.Atoi(v1)
if e != nil {
return nil, e
}
second, e1 := strconv.Atoi(v2)
if e1 != nil {
return nil, e1
}
third, e2 := strconv.Atoi(v3)
if e2 != nil {
return nil, e2
}
return &ConfigVersion{
Year: int32(first),
Month: int32(second),
Day: int32(third),
V1: 4,
V2: 6,
}, nil
}

587
libgm/gmproto/config.pb.go Normal file
View file

@ -0,0 +1,587 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.31.0
// protoc v3.21.12
// source: config.proto
package gmproto
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
import _ "embed"
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type Config struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
ClientVersion string `protobuf:"bytes,1,opt,name=clientVersion,proto3" json:"clientVersion,omitempty"`
ServerVersion string `protobuf:"bytes,2,opt,name=serverVersion,proto3" json:"serverVersion,omitempty"`
IntFlags []*Config_Flag `protobuf:"bytes,3,rep,name=intFlags,proto3" json:"intFlags,omitempty"`
MoreFlags *Config_MoreFlags `protobuf:"bytes,4,opt,name=moreFlags,proto3" json:"moreFlags,omitempty"`
DeviceInfo *Config_DeviceInfo `protobuf:"bytes,5,opt,name=deviceInfo,proto3" json:"deviceInfo,omitempty"`
// item 6 seems like a list of device infos without device ID?
CountryCode string `protobuf:"bytes,7,opt,name=countryCode,proto3" json:"countryCode,omitempty"`
UnknownInts []uint32 `protobuf:"varint,8,rep,packed,name=unknownInts,proto3" json:"unknownInts,omitempty"`
GeneratedAtMS int64 `protobuf:"varint,9,opt,name=generatedAtMS,proto3" json:"generatedAtMS,omitempty"`
}
func (x *Config) Reset() {
*x = Config{}
if protoimpl.UnsafeEnabled {
mi := &file_config_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Config) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Config) ProtoMessage() {}
func (x *Config) ProtoReflect() protoreflect.Message {
mi := &file_config_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Config.ProtoReflect.Descriptor instead.
func (*Config) Descriptor() ([]byte, []int) {
return file_config_proto_rawDescGZIP(), []int{0}
}
func (x *Config) GetClientVersion() string {
if x != nil {
return x.ClientVersion
}
return ""
}
func (x *Config) GetServerVersion() string {
if x != nil {
return x.ServerVersion
}
return ""
}
func (x *Config) GetIntFlags() []*Config_Flag {
if x != nil {
return x.IntFlags
}
return nil
}
func (x *Config) GetMoreFlags() *Config_MoreFlags {
if x != nil {
return x.MoreFlags
}
return nil
}
func (x *Config) GetDeviceInfo() *Config_DeviceInfo {
if x != nil {
return x.DeviceInfo
}
return nil
}
func (x *Config) GetCountryCode() string {
if x != nil {
return x.CountryCode
}
return ""
}
func (x *Config) GetUnknownInts() []uint32 {
if x != nil {
return x.UnknownInts
}
return nil
}
func (x *Config) GetGeneratedAtMS() int64 {
if x != nil {
return x.GeneratedAtMS
}
return 0
}
type Config_Flag struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"`
Value int32 `protobuf:"varint,2,opt,name=value,proto3" json:"value,omitempty"`
UnknownField4 *string `protobuf:"bytes,4,opt,name=unknownField4,proto3,oneof" json:"unknownField4,omitempty"`
UnknownField5 *string `protobuf:"bytes,5,opt,name=unknownField5,proto3,oneof" json:"unknownField5,omitempty"`
}
func (x *Config_Flag) Reset() {
*x = Config_Flag{}
if protoimpl.UnsafeEnabled {
mi := &file_config_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Config_Flag) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Config_Flag) ProtoMessage() {}
func (x *Config_Flag) ProtoReflect() protoreflect.Message {
mi := &file_config_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Config_Flag.ProtoReflect.Descriptor instead.
func (*Config_Flag) Descriptor() ([]byte, []int) {
return file_config_proto_rawDescGZIP(), []int{0, 0}
}
func (x *Config_Flag) GetKey() string {
if x != nil {
return x.Key
}
return ""
}
func (x *Config_Flag) GetValue() int32 {
if x != nil {
return x.Value
}
return 0
}
func (x *Config_Flag) GetUnknownField4() string {
if x != nil && x.UnknownField4 != nil {
return *x.UnknownField4
}
return ""
}
func (x *Config_Flag) GetUnknownField5() string {
if x != nil && x.UnknownField5 != nil {
return *x.UnknownField5
}
return ""
}
type Config_WrappedFlag struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"`
Value *Config_WrappedFlag_Value `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"`
}
func (x *Config_WrappedFlag) Reset() {
*x = Config_WrappedFlag{}
if protoimpl.UnsafeEnabled {
mi := &file_config_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Config_WrappedFlag) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Config_WrappedFlag) ProtoMessage() {}
func (x *Config_WrappedFlag) ProtoReflect() protoreflect.Message {
mi := &file_config_proto_msgTypes[2]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Config_WrappedFlag.ProtoReflect.Descriptor instead.
func (*Config_WrappedFlag) Descriptor() ([]byte, []int) {
return file_config_proto_rawDescGZIP(), []int{0, 1}
}
func (x *Config_WrappedFlag) GetKey() string {
if x != nil {
return x.Key
}
return ""
}
func (x *Config_WrappedFlag) GetValue() *Config_WrappedFlag_Value {
if x != nil {
return x.Value
}
return nil
}
type Config_MoreFlags struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
WrappedFlags []*Config_WrappedFlag `protobuf:"bytes,1,rep,name=wrappedFlags,proto3" json:"wrappedFlags,omitempty"`
}
func (x *Config_MoreFlags) Reset() {
*x = Config_MoreFlags{}
if protoimpl.UnsafeEnabled {
mi := &file_config_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Config_MoreFlags) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Config_MoreFlags) ProtoMessage() {}
func (x *Config_MoreFlags) ProtoReflect() protoreflect.Message {
mi := &file_config_proto_msgTypes[3]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Config_MoreFlags.ProtoReflect.Descriptor instead.
func (*Config_MoreFlags) Descriptor() ([]byte, []int) {
return file_config_proto_rawDescGZIP(), []int{0, 2}
}
func (x *Config_MoreFlags) GetWrappedFlags() []*Config_WrappedFlag {
if x != nil {
return x.WrappedFlags
}
return nil
}
type Config_DeviceInfo struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Email string `protobuf:"bytes,2,opt,name=email,proto3" json:"email,omitempty"`
Zero string `protobuf:"bytes,3,opt,name=zero,proto3" json:"zero,omitempty"`
DeviceID string `protobuf:"bytes,4,opt,name=deviceID,proto3" json:"deviceID,omitempty"`
}
func (x *Config_DeviceInfo) Reset() {
*x = Config_DeviceInfo{}
if protoimpl.UnsafeEnabled {
mi := &file_config_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Config_DeviceInfo) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Config_DeviceInfo) ProtoMessage() {}
func (x *Config_DeviceInfo) ProtoReflect() protoreflect.Message {
mi := &file_config_proto_msgTypes[4]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Config_DeviceInfo.ProtoReflect.Descriptor instead.
func (*Config_DeviceInfo) Descriptor() ([]byte, []int) {
return file_config_proto_rawDescGZIP(), []int{0, 3}
}
func (x *Config_DeviceInfo) GetEmail() string {
if x != nil {
return x.Email
}
return ""
}
func (x *Config_DeviceInfo) GetZero() string {
if x != nil {
return x.Zero
}
return ""
}
func (x *Config_DeviceInfo) GetDeviceID() string {
if x != nil {
return x.DeviceID
}
return ""
}
type Config_WrappedFlag_Value struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// Types that are assignable to Value:
//
// *Config_WrappedFlag_Value_IntVal
// *Config_WrappedFlag_Value_StrVal
Value isConfig_WrappedFlag_Value_Value `protobuf_oneof:"value"`
}
func (x *Config_WrappedFlag_Value) Reset() {
*x = Config_WrappedFlag_Value{}
if protoimpl.UnsafeEnabled {
mi := &file_config_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *Config_WrappedFlag_Value) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Config_WrappedFlag_Value) ProtoMessage() {}
func (x *Config_WrappedFlag_Value) ProtoReflect() protoreflect.Message {
mi := &file_config_proto_msgTypes[5]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Config_WrappedFlag_Value.ProtoReflect.Descriptor instead.
func (*Config_WrappedFlag_Value) Descriptor() ([]byte, []int) {
return file_config_proto_rawDescGZIP(), []int{0, 1, 0}
}
func (m *Config_WrappedFlag_Value) GetValue() isConfig_WrappedFlag_Value_Value {
if m != nil {
return m.Value
}
return nil
}
func (x *Config_WrappedFlag_Value) GetIntVal() int32 {
if x, ok := x.GetValue().(*Config_WrappedFlag_Value_IntVal); ok {
return x.IntVal
}
return 0
}
func (x *Config_WrappedFlag_Value) GetStrVal() string {
if x, ok := x.GetValue().(*Config_WrappedFlag_Value_StrVal); ok {
return x.StrVal
}
return ""
}
type isConfig_WrappedFlag_Value_Value interface {
isConfig_WrappedFlag_Value_Value()
}
type Config_WrappedFlag_Value_IntVal struct {
IntVal int32 `protobuf:"varint,2,opt,name=intVal,proto3,oneof"`
}
type Config_WrappedFlag_Value_StrVal struct {
StrVal string `protobuf:"bytes,3,opt,name=strVal,proto3,oneof"`
}
func (*Config_WrappedFlag_Value_IntVal) isConfig_WrappedFlag_Value_Value() {}
func (*Config_WrappedFlag_Value_StrVal) isConfig_WrappedFlag_Value_Value() {}
var File_config_proto protoreflect.FileDescriptor
//go:embed config.pb.raw
var file_config_proto_rawDesc []byte
var (
file_config_proto_rawDescOnce sync.Once
file_config_proto_rawDescData = file_config_proto_rawDesc
)
func file_config_proto_rawDescGZIP() []byte {
file_config_proto_rawDescOnce.Do(func() {
file_config_proto_rawDescData = protoimpl.X.CompressGZIP(file_config_proto_rawDescData)
})
return file_config_proto_rawDescData
}
var file_config_proto_msgTypes = make([]protoimpl.MessageInfo, 6)
var file_config_proto_goTypes = []interface{}{
(*Config)(nil), // 0: config.Config
(*Config_Flag)(nil), // 1: config.Config.Flag
(*Config_WrappedFlag)(nil), // 2: config.Config.WrappedFlag
(*Config_MoreFlags)(nil), // 3: config.Config.MoreFlags
(*Config_DeviceInfo)(nil), // 4: config.Config.DeviceInfo
(*Config_WrappedFlag_Value)(nil), // 5: config.Config.WrappedFlag.Value
}
var file_config_proto_depIdxs = []int32{
1, // 0: config.Config.intFlags:type_name -> config.Config.Flag
3, // 1: config.Config.moreFlags:type_name -> config.Config.MoreFlags
4, // 2: config.Config.deviceInfo:type_name -> config.Config.DeviceInfo
5, // 3: config.Config.WrappedFlag.value:type_name -> config.Config.WrappedFlag.Value
2, // 4: config.Config.MoreFlags.wrappedFlags:type_name -> config.Config.WrappedFlag
5, // [5:5] is the sub-list for method output_type
5, // [5:5] is the sub-list for method input_type
5, // [5:5] is the sub-list for extension type_name
5, // [5:5] is the sub-list for extension extendee
0, // [0:5] is the sub-list for field type_name
}
func init() { file_config_proto_init() }
func file_config_proto_init() {
if File_config_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_config_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Config); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_config_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Config_Flag); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_config_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Config_WrappedFlag); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_config_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Config_MoreFlags); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_config_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Config_DeviceInfo); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_config_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Config_WrappedFlag_Value); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
}
file_config_proto_msgTypes[1].OneofWrappers = []interface{}{}
file_config_proto_msgTypes[5].OneofWrappers = []interface{}{
(*Config_WrappedFlag_Value_IntVal)(nil),
(*Config_WrappedFlag_Value_StrVal)(nil),
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_config_proto_rawDesc,
NumEnums: 0,
NumMessages: 6,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_config_proto_goTypes,
DependencyIndexes: file_config_proto_depIdxs,
MessageInfos: file_config_proto_msgTypes,
}.Build()
File_config_proto = out.File
file_config_proto_rawDesc = nil
file_config_proto_goTypes = nil
file_config_proto_depIdxs = nil
}

BIN
libgm/gmproto/config.pb.raw Normal file

Binary file not shown.

View file

@ -0,0 +1,40 @@
syntax = "proto3";
package config;
option go_package = "../gmproto";
message Config {
message Flag {
string key = 1;
int32 value = 2;
optional string unknownField4 = 4;
optional string unknownField5 = 5;
}
message WrappedFlag {
message Value {
oneof value {
int32 intVal = 2;
string strVal = 3;
}
}
string key = 1;
Value value = 2;
}
message MoreFlags {
repeated WrappedFlag wrappedFlags = 1;
}
message DeviceInfo {
string email = 2;
string zero = 3;
string deviceID = 4;
}
string clientVersion = 1;
string serverVersion = 2;
repeated Flag intFlags = 3;
MoreFlags moreFlags = 4;
DeviceInfo deviceInfo = 5;
// item 6 seems like a list of device infos without device ID?
string countryCode = 7;
repeated uint32 unknownInts = 8;
int64 generatedAtMS = 9;
}

View file

@ -0,0 +1,82 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.31.0
// protoc v3.21.12
// source: pblite.proto
package gmproto
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
descriptorpb "google.golang.org/protobuf/types/descriptorpb"
reflect "reflect"
)
import _ "embed"
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
var file_pblite_proto_extTypes = []protoimpl.ExtensionInfo{
{
ExtendedType: (*descriptorpb.FieldOptions)(nil),
ExtensionType: (*bool)(nil),
Field: 50000,
Name: "pblite.pblite_binary",
Tag: "varint,50000,opt,name=pblite_binary",
Filename: "pblite.proto",
},
}
// Extension fields to descriptorpb.FieldOptions.
var (
// optional bool pblite_binary = 50000;
E_PbliteBinary = &file_pblite_proto_extTypes[0]
)
var File_pblite_proto protoreflect.FileDescriptor
//go:embed pblite.pb.raw
var file_pblite_proto_rawDesc []byte
var file_pblite_proto_goTypes = []interface{}{
(*descriptorpb.FieldOptions)(nil), // 0: google.protobuf.FieldOptions
}
var file_pblite_proto_depIdxs = []int32{
0, // 0: pblite.pblite_binary:extendee -> google.protobuf.FieldOptions
1, // [1:1] is the sub-list for method output_type
1, // [1:1] is the sub-list for method input_type
1, // [1:1] is the sub-list for extension type_name
0, // [0:1] is the sub-list for extension extendee
0, // [0:0] is the sub-list for field type_name
}
func init() { file_pblite_proto_init() }
func file_pblite_proto_init() {
if File_pblite_proto != nil {
return
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_pblite_proto_rawDesc,
NumEnums: 0,
NumMessages: 0,
NumExtensions: 1,
NumServices: 0,
},
GoTypes: file_pblite_proto_goTypes,
DependencyIndexes: file_pblite_proto_depIdxs,
ExtensionInfos: file_pblite_proto_extTypes,
}.Build()
File_pblite_proto = out.File
file_pblite_proto_rawDesc = nil
file_pblite_proto_goTypes = nil
file_pblite_proto_depIdxs = nil
}

View file

@ -0,0 +1,4 @@
pblite.protopblite google/protobuf/descriptor.proto:G
pblite_binary.google.protobuf.FieldOptionsІ (R pbliteBinary<72>B Z
../gmprotobproto3

View file

@ -0,0 +1,10 @@
syntax = "proto3";
package pblite;
option go_package = "../gmproto";
import "google/protobuf/descriptor.proto";
extend google.protobuf.FieldOptions {
optional bool pblite_binary = 50000;
}

View file

@ -28,6 +28,7 @@ const (
BugleRoute_Unknown BugleRoute = 0
BugleRoute_DataEvent BugleRoute = 19
BugleRoute_PairEvent BugleRoute = 14
BugleRoute_GaiaEvent BugleRoute = 7
)
// Enum value maps for BugleRoute.
@ -36,11 +37,13 @@ var (
0: "Unknown",
19: "DataEvent",
14: "PairEvent",
7: "GaiaEvent",
}
BugleRoute_value = map[string]int32{
"Unknown": 0,
"DataEvent": 19,
"PairEvent": 14,
"GaiaEvent": 7,
}
)
@ -119,6 +122,7 @@ const (
ActionType_CREATE_GAIA_PAIRING_CLIENT_INIT ActionType = 44
ActionType_CREATE_GAIA_PAIRING_CLIENT_FINISHED ActionType = 45
ActionType_UNPAIR_GAIA_PAIRING ActionType = 46
ActionType_CANCEL_GAIA_PAIRING ActionType = 47
)
// Enum value maps for ActionType.
@ -169,6 +173,7 @@ var (
44: "CREATE_GAIA_PAIRING_CLIENT_INIT",
45: "CREATE_GAIA_PAIRING_CLIENT_FINISHED",
46: "UNPAIR_GAIA_PAIRING",
47: "CANCEL_GAIA_PAIRING",
}
ActionType_value = map[string]int32{
"UNSPECIFIED": 0,
@ -216,6 +221,7 @@ var (
"CREATE_GAIA_PAIRING_CLIENT_INIT": 44,
"CREATE_GAIA_PAIRING_CLIENT_FINISHED": 45,
"UNPAIR_GAIA_PAIRING": 46,
"CANCEL_GAIA_PAIRING": 47,
}
)
@ -251,7 +257,9 @@ type MessageType int32
const (
MessageType_UNKNOWN_MESSAGE_TYPE MessageType = 0
MessageType_BUGLE_MESSAGE MessageType = 2
MessageType_GAIA_1 MessageType = 3
MessageType_BUGLE_ANNOTATION MessageType = 16
MessageType_GAIA_2 MessageType = 20
)
// Enum value maps for MessageType.
@ -259,12 +267,16 @@ var (
MessageType_name = map[int32]string{
0: "UNKNOWN_MESSAGE_TYPE",
2: "BUGLE_MESSAGE",
3: "GAIA_1",
16: "BUGLE_ANNOTATION",
20: "GAIA_2",
}
MessageType_value = map[string]int32{
"UNKNOWN_MESSAGE_TYPE": 0,
"BUGLE_MESSAGE": 2,
"GAIA_1": 3,
"BUGLE_ANNOTATION": 16,
"GAIA_2": 20,
}
)
@ -420,16 +432,18 @@ type IncomingRPCMessage struct {
ResponseID string `protobuf:"bytes,1,opt,name=responseID,proto3" json:"responseID,omitempty"`
BugleRoute BugleRoute `protobuf:"varint,2,opt,name=bugleRoute,proto3,enum=rpc.BugleRoute" json:"bugleRoute,omitempty"`
StartExecute string `protobuf:"bytes,3,opt,name=startExecute,proto3" json:"startExecute,omitempty"`
StartExecute uint64 `protobuf:"varint,3,opt,name=startExecute,proto3" json:"startExecute,omitempty"`
MessageType MessageType `protobuf:"varint,5,opt,name=messageType,proto3,enum=rpc.MessageType" json:"messageType,omitempty"`
FinishExecute string `protobuf:"bytes,6,opt,name=finishExecute,proto3" json:"finishExecute,omitempty"`
MillisecondsTaken string `protobuf:"bytes,7,opt,name=millisecondsTaken,proto3" json:"millisecondsTaken,omitempty"`
FinishExecute uint64 `protobuf:"varint,6,opt,name=finishExecute,proto3" json:"finishExecute,omitempty"`
MicrosecondsTaken uint64 `protobuf:"varint,7,opt,name=microsecondsTaken,proto3" json:"microsecondsTaken,omitempty"`
Mobile *Device `protobuf:"bytes,8,opt,name=mobile,proto3" json:"mobile,omitempty"`
Browser *Device `protobuf:"bytes,9,opt,name=browser,proto3" json:"browser,omitempty"`
// Either a RPCMessageData or a RPCPairData encoded as bytes
MessageData []byte `protobuf:"bytes,12,opt,name=messageData,proto3" json:"messageData,omitempty"`
SignatureID string `protobuf:"bytes,17,opt,name=signatureID,proto3" json:"signatureID,omitempty"`
Timestamp string `protobuf:"bytes,21,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
// Completely unsure about this, but it seems to be present for weird intermediate responses
GdittoSource *IncomingRPCMessage_GDittoSource `protobuf:"bytes,23,opt,name=gdittoSource,proto3" json:"gdittoSource,omitempty"`
}
func (x *IncomingRPCMessage) Reset() {
@ -478,11 +492,11 @@ func (x *IncomingRPCMessage) GetBugleRoute() BugleRoute {
return BugleRoute_Unknown
}
func (x *IncomingRPCMessage) GetStartExecute() string {
func (x *IncomingRPCMessage) GetStartExecute() uint64 {
if x != nil {
return x.StartExecute
}
return ""
return 0
}
func (x *IncomingRPCMessage) GetMessageType() MessageType {
@ -492,18 +506,18 @@ func (x *IncomingRPCMessage) GetMessageType() MessageType {
return MessageType_UNKNOWN_MESSAGE_TYPE
}
func (x *IncomingRPCMessage) GetFinishExecute() string {
func (x *IncomingRPCMessage) GetFinishExecute() uint64 {
if x != nil {
return x.FinishExecute
}
return ""
return 0
}
func (x *IncomingRPCMessage) GetMillisecondsTaken() string {
func (x *IncomingRPCMessage) GetMicrosecondsTaken() uint64 {
if x != nil {
return x.MillisecondsTaken
return x.MicrosecondsTaken
}
return ""
return 0
}
func (x *IncomingRPCMessage) GetMobile() *Device {
@ -541,19 +555,27 @@ func (x *IncomingRPCMessage) GetTimestamp() string {
return ""
}
func (x *IncomingRPCMessage) GetGdittoSource() *IncomingRPCMessage_GDittoSource {
if x != nil {
return x.GdittoSource
}
return nil
}
type RPCMessageData struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
SessionID string `protobuf:"bytes,1,opt,name=sessionID,proto3" json:"sessionID,omitempty"`
Timestamp int64 `protobuf:"varint,3,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
Action ActionType `protobuf:"varint,4,opt,name=action,proto3,enum=rpc.ActionType" json:"action,omitempty"`
Bool1 bool `protobuf:"varint,6,opt,name=bool1,proto3" json:"bool1,omitempty"`
Bool2 bool `protobuf:"varint,7,opt,name=bool2,proto3" json:"bool2,omitempty"`
EncryptedData []byte `protobuf:"bytes,8,opt,name=encryptedData,proto3" json:"encryptedData,omitempty"`
Bool3 bool `protobuf:"varint,9,opt,name=bool3,proto3" json:"bool3,omitempty"`
EncryptedData2 []byte `protobuf:"bytes,11,opt,name=encryptedData2,proto3" json:"encryptedData2,omitempty"`
SessionID string `protobuf:"bytes,1,opt,name=sessionID,proto3" json:"sessionID,omitempty"`
Timestamp int64 `protobuf:"varint,3,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
Action ActionType `protobuf:"varint,4,opt,name=action,proto3,enum=rpc.ActionType" json:"action,omitempty"`
UnencryptedData []byte `protobuf:"bytes,5,opt,name=unencryptedData,proto3" json:"unencryptedData,omitempty"`
Bool1 bool `protobuf:"varint,6,opt,name=bool1,proto3" json:"bool1,omitempty"`
Bool2 bool `protobuf:"varint,7,opt,name=bool2,proto3" json:"bool2,omitempty"`
EncryptedData []byte `protobuf:"bytes,8,opt,name=encryptedData,proto3" json:"encryptedData,omitempty"`
Bool3 bool `protobuf:"varint,9,opt,name=bool3,proto3" json:"bool3,omitempty"`
EncryptedData2 []byte `protobuf:"bytes,11,opt,name=encryptedData2,proto3" json:"encryptedData2,omitempty"`
}
func (x *RPCMessageData) Reset() {
@ -609,6 +631,13 @@ func (x *RPCMessageData) GetAction() ActionType {
return ActionType_UNSPECIFIED
}
func (x *RPCMessageData) GetUnencryptedData() []byte {
if x != nil {
return x.UnencryptedData
}
return nil
}
func (x *RPCMessageData) GetBool1() bool {
if x != nil {
return x.Bool1
@ -649,11 +678,11 @@ type OutgoingRPCMessage struct {
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Mobile *Device `protobuf:"bytes,1,opt,name=mobile,proto3" json:"mobile,omitempty"`
Data *OutgoingRPCMessage_Data `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"`
Auth *OutgoingRPCMessage_Auth `protobuf:"bytes,3,opt,name=auth,proto3" json:"auth,omitempty"`
TTL int64 `protobuf:"varint,5,opt,name=TTL,proto3" json:"TTL,omitempty"`
EmptyArr *EmptyArr `protobuf:"bytes,9,opt,name=emptyArr,proto3" json:"emptyArr,omitempty"`
Mobile *Device `protobuf:"bytes,1,opt,name=mobile,proto3" json:"mobile,omitempty"`
Data *OutgoingRPCMessage_Data `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"`
Auth *OutgoingRPCMessage_Auth `protobuf:"bytes,3,opt,name=auth,proto3" json:"auth,omitempty"`
TTL int64 `protobuf:"varint,5,opt,name=TTL,proto3" json:"TTL,omitempty"`
DestRegistrationIDs []string `protobuf:"bytes,9,rep,name=destRegistrationIDs,proto3" json:"destRegistrationIDs,omitempty"`
}
func (x *OutgoingRPCMessage) Reset() {
@ -716,9 +745,9 @@ func (x *OutgoingRPCMessage) GetTTL() int64 {
return 0
}
func (x *OutgoingRPCMessage) GetEmptyArr() *EmptyArr {
func (x *OutgoingRPCMessage) GetDestRegistrationIDs() []string {
if x != nil {
return x.EmptyArr
return x.DestRegistrationIDs
}
return nil
}
@ -728,10 +757,11 @@ type OutgoingRPCData struct {
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
RequestID string `protobuf:"bytes,1,opt,name=requestID,proto3" json:"requestID,omitempty"`
Action ActionType `protobuf:"varint,2,opt,name=action,proto3,enum=rpc.ActionType" json:"action,omitempty"`
EncryptedProtoData []byte `protobuf:"bytes,5,opt,name=encryptedProtoData,proto3" json:"encryptedProtoData,omitempty"`
SessionID string `protobuf:"bytes,6,opt,name=sessionID,proto3" json:"sessionID,omitempty"`
RequestID string `protobuf:"bytes,1,opt,name=requestID,proto3" json:"requestID,omitempty"`
Action ActionType `protobuf:"varint,2,opt,name=action,proto3,enum=rpc.ActionType" json:"action,omitempty"`
UnencryptedProtoData []byte `protobuf:"bytes,3,opt,name=unencryptedProtoData,proto3" json:"unencryptedProtoData,omitempty"`
EncryptedProtoData []byte `protobuf:"bytes,5,opt,name=encryptedProtoData,proto3" json:"encryptedProtoData,omitempty"`
SessionID string `protobuf:"bytes,6,opt,name=sessionID,proto3" json:"sessionID,omitempty"`
}
func (x *OutgoingRPCData) Reset() {
@ -780,6 +810,13 @@ func (x *OutgoingRPCData) GetAction() ActionType {
return ActionType_UNSPECIFIED
}
func (x *OutgoingRPCData) GetUnencryptedProtoData() []byte {
if x != nil {
return x.UnencryptedProtoData
}
return nil
}
func (x *OutgoingRPCData) GetEncryptedProtoData() []byte {
if x != nil {
return x.EncryptedProtoData
@ -850,6 +887,53 @@ func (x *OutgoingRPCResponse) GetTimestamp() string {
return ""
}
type IncomingRPCMessage_GDittoSource struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
DeviceID int32 `protobuf:"varint,2,opt,name=deviceID,proto3" json:"deviceID,omitempty"`
}
func (x *IncomingRPCMessage_GDittoSource) Reset() {
*x = IncomingRPCMessage_GDittoSource{}
if protoimpl.UnsafeEnabled {
mi := &file_rpc_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *IncomingRPCMessage_GDittoSource) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*IncomingRPCMessage_GDittoSource) ProtoMessage() {}
func (x *IncomingRPCMessage_GDittoSource) ProtoReflect() protoreflect.Message {
mi := &file_rpc_proto_msgTypes[7]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use IncomingRPCMessage_GDittoSource.ProtoReflect.Descriptor instead.
func (*IncomingRPCMessage_GDittoSource) Descriptor() ([]byte, []int) {
return file_rpc_proto_rawDescGZIP(), []int{2, 0}
}
func (x *IncomingRPCMessage_GDittoSource) GetDeviceID() int32 {
if x != nil {
return x.DeviceID
}
return 0
}
type OutgoingRPCMessage_Auth struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@ -863,7 +947,7 @@ type OutgoingRPCMessage_Auth struct {
func (x *OutgoingRPCMessage_Auth) Reset() {
*x = OutgoingRPCMessage_Auth{}
if protoimpl.UnsafeEnabled {
mi := &file_rpc_proto_msgTypes[7]
mi := &file_rpc_proto_msgTypes[8]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -876,7 +960,7 @@ func (x *OutgoingRPCMessage_Auth) String() string {
func (*OutgoingRPCMessage_Auth) ProtoMessage() {}
func (x *OutgoingRPCMessage_Auth) ProtoReflect() protoreflect.Message {
mi := &file_rpc_proto_msgTypes[7]
mi := &file_rpc_proto_msgTypes[8]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -928,7 +1012,7 @@ type OutgoingRPCMessage_Data struct {
func (x *OutgoingRPCMessage_Data) Reset() {
*x = OutgoingRPCMessage_Data{}
if protoimpl.UnsafeEnabled {
mi := &file_rpc_proto_msgTypes[8]
mi := &file_rpc_proto_msgTypes[9]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -941,7 +1025,7 @@ func (x *OutgoingRPCMessage_Data) String() string {
func (*OutgoingRPCMessage_Data) ProtoMessage() {}
func (x *OutgoingRPCMessage_Data) ProtoReflect() protoreflect.Message {
mi := &file_rpc_proto_msgTypes[8]
mi := &file_rpc_proto_msgTypes[9]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -997,7 +1081,7 @@ type OutgoingRPCMessage_Data_Type struct {
func (x *OutgoingRPCMessage_Data_Type) Reset() {
*x = OutgoingRPCMessage_Data_Type{}
if protoimpl.UnsafeEnabled {
mi := &file_rpc_proto_msgTypes[9]
mi := &file_rpc_proto_msgTypes[10]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1010,7 +1094,7 @@ func (x *OutgoingRPCMessage_Data_Type) String() string {
func (*OutgoingRPCMessage_Data_Type) ProtoMessage() {}
func (x *OutgoingRPCMessage_Data_Type) ProtoReflect() protoreflect.Message {
mi := &file_rpc_proto_msgTypes[9]
mi := &file_rpc_proto_msgTypes[10]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1052,7 +1136,7 @@ type OutgoingRPCResponse_SomeIdentifier struct {
func (x *OutgoingRPCResponse_SomeIdentifier) Reset() {
*x = OutgoingRPCResponse_SomeIdentifier{}
if protoimpl.UnsafeEnabled {
mi := &file_rpc_proto_msgTypes[10]
mi := &file_rpc_proto_msgTypes[11]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@ -1065,7 +1149,7 @@ func (x *OutgoingRPCResponse_SomeIdentifier) String() string {
func (*OutgoingRPCResponse_SomeIdentifier) ProtoMessage() {}
func (x *OutgoingRPCResponse_SomeIdentifier) ProtoReflect() protoreflect.Message {
mi := &file_rpc_proto_msgTypes[10]
mi := &file_rpc_proto_msgTypes[11]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@ -1106,7 +1190,7 @@ func file_rpc_proto_rawDescGZIP() []byte {
}
var file_rpc_proto_enumTypes = make([]protoimpl.EnumInfo, 3)
var file_rpc_proto_msgTypes = make([]protoimpl.MessageInfo, 11)
var file_rpc_proto_msgTypes = make([]protoimpl.MessageInfo, 12)
var file_rpc_proto_goTypes = []interface{}{
(BugleRoute)(0), // 0: rpc.BugleRoute
(ActionType)(0), // 1: rpc.ActionType
@ -1118,34 +1202,35 @@ var file_rpc_proto_goTypes = []interface{}{
(*OutgoingRPCMessage)(nil), // 7: rpc.OutgoingRPCMessage
(*OutgoingRPCData)(nil), // 8: rpc.OutgoingRPCData
(*OutgoingRPCResponse)(nil), // 9: rpc.OutgoingRPCResponse
(*OutgoingRPCMessage_Auth)(nil), // 10: rpc.OutgoingRPCMessage.Auth
(*OutgoingRPCMessage_Data)(nil), // 11: rpc.OutgoingRPCMessage.Data
(*OutgoingRPCMessage_Data_Type)(nil), // 12: rpc.OutgoingRPCMessage.Data.Type
(*OutgoingRPCResponse_SomeIdentifier)(nil), // 13: rpc.OutgoingRPCResponse.SomeIdentifier
(*EmptyArr)(nil), // 14: util.EmptyArr
(*Device)(nil), // 15: authentication.Device
(*ConfigVersion)(nil), // 16: authentication.ConfigVersion
(*IncomingRPCMessage_GDittoSource)(nil), // 10: rpc.IncomingRPCMessage.GDittoSource
(*OutgoingRPCMessage_Auth)(nil), // 11: rpc.OutgoingRPCMessage.Auth
(*OutgoingRPCMessage_Data)(nil), // 12: rpc.OutgoingRPCMessage.Data
(*OutgoingRPCMessage_Data_Type)(nil), // 13: rpc.OutgoingRPCMessage.Data.Type
(*OutgoingRPCResponse_SomeIdentifier)(nil), // 14: rpc.OutgoingRPCResponse.SomeIdentifier
(*EmptyArr)(nil), // 15: util.EmptyArr
(*Device)(nil), // 16: authentication.Device
(*ConfigVersion)(nil), // 17: authentication.ConfigVersion
}
var file_rpc_proto_depIdxs = []int32{
5, // 0: rpc.LongPollingPayload.data:type_name -> rpc.IncomingRPCMessage
14, // 1: rpc.LongPollingPayload.heartbeat:type_name -> util.EmptyArr
15, // 1: rpc.LongPollingPayload.heartbeat:type_name -> util.EmptyArr
3, // 2: rpc.LongPollingPayload.ack:type_name -> rpc.StartAckMessage
14, // 3: rpc.LongPollingPayload.startRead:type_name -> util.EmptyArr
15, // 3: rpc.LongPollingPayload.startRead:type_name -> util.EmptyArr
0, // 4: rpc.IncomingRPCMessage.bugleRoute:type_name -> rpc.BugleRoute
2, // 5: rpc.IncomingRPCMessage.messageType:type_name -> rpc.MessageType
15, // 6: rpc.IncomingRPCMessage.mobile:type_name -> authentication.Device
15, // 7: rpc.IncomingRPCMessage.browser:type_name -> authentication.Device
1, // 8: rpc.RPCMessageData.action:type_name -> rpc.ActionType
15, // 9: rpc.OutgoingRPCMessage.mobile:type_name -> authentication.Device
11, // 10: rpc.OutgoingRPCMessage.data:type_name -> rpc.OutgoingRPCMessage.Data
10, // 11: rpc.OutgoingRPCMessage.auth:type_name -> rpc.OutgoingRPCMessage.Auth
14, // 12: rpc.OutgoingRPCMessage.emptyArr:type_name -> util.EmptyArr
16, // 6: rpc.IncomingRPCMessage.mobile:type_name -> authentication.Device
16, // 7: rpc.IncomingRPCMessage.browser:type_name -> authentication.Device
10, // 8: rpc.IncomingRPCMessage.gdittoSource:type_name -> rpc.IncomingRPCMessage.GDittoSource
1, // 9: rpc.RPCMessageData.action:type_name -> rpc.ActionType
16, // 10: rpc.OutgoingRPCMessage.mobile:type_name -> authentication.Device
12, // 11: rpc.OutgoingRPCMessage.data:type_name -> rpc.OutgoingRPCMessage.Data
11, // 12: rpc.OutgoingRPCMessage.auth:type_name -> rpc.OutgoingRPCMessage.Auth
1, // 13: rpc.OutgoingRPCData.action:type_name -> rpc.ActionType
13, // 14: rpc.OutgoingRPCResponse.someIdentifier:type_name -> rpc.OutgoingRPCResponse.SomeIdentifier
16, // 15: rpc.OutgoingRPCMessage.Auth.configVersion:type_name -> authentication.ConfigVersion
14, // 14: rpc.OutgoingRPCResponse.someIdentifier:type_name -> rpc.OutgoingRPCResponse.SomeIdentifier
17, // 15: rpc.OutgoingRPCMessage.Auth.configVersion:type_name -> authentication.ConfigVersion
0, // 16: rpc.OutgoingRPCMessage.Data.bugleRoute:type_name -> rpc.BugleRoute
12, // 17: rpc.OutgoingRPCMessage.Data.messageTypeData:type_name -> rpc.OutgoingRPCMessage.Data.Type
14, // 18: rpc.OutgoingRPCMessage.Data.Type.emptyArr:type_name -> util.EmptyArr
13, // 17: rpc.OutgoingRPCMessage.Data.messageTypeData:type_name -> rpc.OutgoingRPCMessage.Data.Type
15, // 18: rpc.OutgoingRPCMessage.Data.Type.emptyArr:type_name -> util.EmptyArr
2, // 19: rpc.OutgoingRPCMessage.Data.Type.messageType:type_name -> rpc.MessageType
20, // [20:20] is the sub-list for method output_type
20, // [20:20] is the sub-list for method input_type
@ -1161,6 +1246,7 @@ func file_rpc_proto_init() {
}
file_authentication_proto_init()
file_util_proto_init()
file_pblite_proto_init()
if !protoimpl.UnsafeEnabled {
file_rpc_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*StartAckMessage); i {
@ -1247,7 +1333,7 @@ func file_rpc_proto_init() {
}
}
file_rpc_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*OutgoingRPCMessage_Auth); i {
switch v := v.(*IncomingRPCMessage_GDittoSource); i {
case 0:
return &v.state
case 1:
@ -1259,7 +1345,7 @@ func file_rpc_proto_init() {
}
}
file_rpc_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*OutgoingRPCMessage_Data); i {
switch v := v.(*OutgoingRPCMessage_Auth); i {
case 0:
return &v.state
case 1:
@ -1271,7 +1357,7 @@ func file_rpc_proto_init() {
}
}
file_rpc_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*OutgoingRPCMessage_Data_Type); i {
switch v := v.(*OutgoingRPCMessage_Data); i {
case 0:
return &v.state
case 1:
@ -1283,6 +1369,18 @@ func file_rpc_proto_init() {
}
}
file_rpc_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*OutgoingRPCMessage_Data_Type); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_rpc_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*OutgoingRPCResponse_SomeIdentifier); i {
case 0:
return &v.state
@ -1304,7 +1402,7 @@ func file_rpc_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_rpc_proto_rawDesc,
NumEnums: 3,
NumMessages: 11,
NumMessages: 12,
NumExtensions: 0,
NumServices: 0,
},

Binary file not shown.

View file

@ -5,6 +5,7 @@ option go_package = "../gmproto";
import "authentication.proto";
import "util.proto";
import "pblite.proto";
message StartAckMessage {
optional int32 count = 1;
@ -20,11 +21,11 @@ message LongPollingPayload {
message IncomingRPCMessage {
string responseID = 1;
BugleRoute bugleRoute = 2;
string startExecute = 3;
uint64 startExecute = 3;
MessageType messageType = 5;
string finishExecute = 6;
string millisecondsTaken = 7;
uint64 finishExecute = 6;
uint64 microsecondsTaken = 7;
authentication.Device mobile = 8;
authentication.Device browser = 9;
@ -34,12 +35,20 @@ message IncomingRPCMessage {
string signatureID = 17;
string timestamp = 21;
message GDittoSource {
int32 deviceID = 2;
}
// Completely unsure about this, but it seems to be present for weird intermediate responses
GDittoSource gdittoSource = 23;
}
message RPCMessageData {
string sessionID = 1;
int64 timestamp = 3;
ActionType action = 4;
bytes unencryptedData = 5;
bool bool1 = 6;
bool bool2 = 7;
bytes encryptedData = 8;
@ -76,12 +85,13 @@ message OutgoingRPCMessage {
int64 TTL = 5;
util.EmptyArr emptyArr = 9;
repeated string destRegistrationIDs = 9 [(pblite.pblite_binary) = true];
}
message OutgoingRPCData {
string requestID = 1;
ActionType action = 2;
bytes unencryptedProtoData = 3;
bytes encryptedProtoData = 5;
string sessionID = 6;
}
@ -101,6 +111,7 @@ enum BugleRoute {
Unknown = 0;
DataEvent = 19;
PairEvent = 14;
GaiaEvent = 7;
}
enum ActionType {
@ -149,10 +160,13 @@ enum ActionType {
CREATE_GAIA_PAIRING_CLIENT_INIT = 44;
CREATE_GAIA_PAIRING_CLIENT_FINISHED = 45;
UNPAIR_GAIA_PAIRING = 46;
CANCEL_GAIA_PAIRING = 47;
}
enum MessageType {
UNKNOWN_MESSAGE_TYPE = 0;
BUGLE_MESSAGE = 2;
GAIA_1 = 3;
BUGLE_ANNOTATION = 16;
GAIA_2 = 20;
}

1091
libgm/gmproto/ukey.pb.go Normal file

File diff suppressed because it is too large Load diff

BIN
libgm/gmproto/ukey.pb.raw Normal file

Binary file not shown.

117
libgm/gmproto/ukey.proto Normal file
View file

@ -0,0 +1,117 @@
syntax = "proto3";
package ukey;
option go_package = "../gmproto";
message Ukey2Message {
enum Type {
UNKNOWN_DO_NOT_USE = 0;
ALERT = 1;
CLIENT_INIT = 2;
SERVER_INIT = 3;
CLIENT_FINISH = 4;
}
Type message_type = 1; // Identifies message type
bytes message_data = 2; // Actual message, to be parsed according to message_type
}
message Ukey2Alert {
enum AlertType {
UNKNOWN_ALERT_TYPE = 0;
// Framing errors
BAD_MESSAGE = 1; // The message could not be deserialized
BAD_MESSAGE_TYPE = 2; // message_type has an undefined value
INCORRECT_MESSAGE = 3; // message_type received does not correspond to expected type at this stage of the protocol
BAD_MESSAGE_DATA = 4; // Could not deserialize message_data as per value in message_type
// ClientInit and ServerInit errors
BAD_VERSION = 100; // version is invalid; server cannot find suitable version to speak with client.
BAD_RANDOM = 101; // Random data is missing or of incorrect length
BAD_HANDSHAKE_CIPHER = 102; // No suitable handshake ciphers were found
BAD_NEXT_PROTOCOL = 103; // The next protocol is missing, unknown, or unsupported
BAD_PUBLIC_KEY = 104; // The public key could not be parsed
// Other errors
INTERNAL_ERROR = 200; // An internal error has occurred. error_message may contain additional details for logging and debugging.
}
AlertType type = 1;
string error_message = 2;
}
enum Ukey2HandshakeCipher {
RESERVED = 0;
P256_SHA512 = 100; // NIST P-256 used for ECDH, SHA512 used for commitment
CURVE25519_SHA512 = 200; // Curve 25519 used for ECDH, SHA512 used for commitment
}
message Ukey2ClientInit {
int32 version = 1; // highest supported version for rollback protection
bytes random = 2; // random bytes for replay/reuse protection
// One commitment (hash of ClientFinished containing public key) per supported cipher
message CipherCommitment {
Ukey2HandshakeCipher handshake_cipher = 1;
bytes commitment = 2;
}
repeated CipherCommitment cipher_commitments = 3;
// Next protocol that the client wants to speak.
string next_protocol = 4;
}
message Ukey2ServerInit {
int32 version = 1; // highest supported version for rollback protection
bytes random = 2; // random bytes for replay/reuse protection
// Selected Cipher and corresponding public key
Ukey2HandshakeCipher handshake_cipher = 3;
GenericPublicKey public_key = 4;
}
message Ukey2ClientFinished {
GenericPublicKey public_key = 1; // public key matching selected handshake cipher
}
// A list of supported public key types
enum PublicKeyType {
UNKNOWN_PUBLIC_KEY_TYPE = 0;
EC_P256 = 1;
RSA2048 = 2;
// 2048-bit MODP group 14, from RFC 3526
DH2048_MODP = 3;
}
// A convenience proto for encoding NIST P-256 elliptic curve public keys
message EcP256PublicKey {
// x and y are encoded in big-endian two's complement (slightly wasteful)
// Client MUST verify (x,y) is a valid point on NIST P256
bytes x = 1;
bytes y = 2;
}
// A convenience proto for encoding RSA public keys with small exponents
message SimpleRsaPublicKey {
// Encoded in big-endian two's complement
bytes n = 1;
int32 e = 2;
}
// A convenience proto for encoding Diffie-Hellman public keys,
// for use only when Elliptic Curve based key exchanges are not possible.
// (Note that the group parameters must be specified separately)
message DhPublicKey {
// Big-endian two's complement encoded group element
bytes y = 1;
}
message GenericPublicKey {
PublicKeyType type = 1;
oneof public_key {
EcP256PublicKey ec_p256_public_key = 2;
SimpleRsaPublicKey rsa2048_public_key = 3;
// Use only as a last resort
DhPublicKey dh2048_public_key = 4;
}
}

View file

@ -1,22 +1,21 @@
module go.mau.fi/mautrix-gmessages/libgm/gmtest
go 1.20
go 1.21
require (
github.com/mdp/qrterminal/v3 v3.2.0
github.com/rs/zerolog v1.31.0
github.com/rs/zerolog v1.32.0
go.mau.fi/mautrix-gmessages/libgm v0.2.2
)
require (
github.com/google/uuid v1.4.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa // indirect
golang.org/x/sys v0.14.0 // indirect
golang.org/x/term v0.13.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect
rsc.io/qr v0.2.0 // indirect
go.mau.fi/util v0.2.1 // indirect
golang.org/x/crypto v0.19.0 // indirect
golang.org/x/exp v0.0.0-20240213143201-ec583247a57a // indirect
golang.org/x/sys v0.17.0 // indirect
google.golang.org/protobuf v1.32.0 // indirect
)
replace go.mau.fi/mautrix-gmessages/libgm => ../

View file

@ -1,38 +1,37 @@
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mdp/qrterminal/v3 v3.2.0 h1:qteQMXO3oyTK4IHwj2mWsKYYRBOp1Pj2WRYFYYNTCdk=
github.com/mdp/qrterminal/v3 v3.2.0/go.mod h1:XGGuua4Lefrl7TLEsSONiD+UEjQXJZ4mPzF+gWYIJkk=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0=
github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ=
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
go.mau.fi/util v0.2.1 h1:eazulhFE/UmjOFtPrGg6zkF5YfAyiDzQb8ihLMbsPWw=
go.mau.fi/util v0.2.1/go.mod h1:MjlzCQEMzJ+G8RsPawHzpLB8rwTo3aPIjG5FzBvQT/c=
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/exp v0.0.0-20240213143201-ec583247a57a h1:HinSgX1tJRX3KsL//Gxynpw5CTOAIPhgL4W8PNiIpVE=
golang.org/x/exp v0.0.0-20240213143201-ec583247a57a/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
rsc.io/qr v0.2.0 h1:6vBLea5/NRMVTz8V66gipeLycZMl/+UlFmk8DvqQ6WY=
rsc.io/qr v0.2.0/go.mod h1:IF+uZjkb9fqyeF/4tlBoynqmQxUoPfWEKh921coOuXs=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View file

@ -4,13 +4,13 @@ import (
"bufio"
"encoding/json"
"errors"
"fmt"
"os"
"os/signal"
"strings"
"syscall"
"time"
"github.com/mdp/qrterminal/v3"
"github.com/rs/zerolog"
"go.mau.fi/mautrix-gmessages/libgm"
@ -46,6 +46,8 @@ func main() {
}
sess = *libgm.NewAuthData()
doLogin = true
cookies := mustReturn(os.Open("cookies.json"))
must(json.NewDecoder(cookies).Decode(&sess.Cookies))
} else {
must(json.NewDecoder(file).Decode(&sess))
log.Info().Msg("Loaded session?")
@ -54,19 +56,12 @@ func main() {
cli = libgm.NewClient(&sess, log)
cli.SetEventHandler(evtHandler)
if doLogin {
qr := mustReturn(cli.StartLogin())
qrterminal.GenerateHalfBlock(qr, qrterminal.L, os.Stdout)
go func() {
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
for range ticker.C {
if sess.Browser != nil {
return
}
qr := mustReturn(cli.RefreshPhoneRelay())
qrterminal.GenerateHalfBlock(qr, qrterminal.L, os.Stdout)
}
}()
err = cli.DoGaiaPairing(func(emoji string) {
fmt.Println(emoji)
})
if err != nil {
log.Fatal().Err(err).Msg("Failed to pair")
}
} else {
must(cli.Connect())
}

View file

@ -1,13 +1,15 @@
module go.mau.fi/mautrix-gmessages/libgm
go 1.20
go 1.21
require (
github.com/google/uuid v1.4.0
github.com/rs/zerolog v1.31.0
github.com/google/uuid v1.6.0
github.com/rs/zerolog v1.32.0
github.com/stretchr/testify v1.8.4
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa
google.golang.org/protobuf v1.31.0
go.mau.fi/util v0.2.1
golang.org/x/crypto v0.19.0
golang.org/x/exp v0.0.0-20240213143201-ec583247a57a
google.golang.org/protobuf v1.32.0
)
require (
@ -15,6 +17,6 @@ require (
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/sys v0.14.0 // indirect
golang.org/x/sys v0.17.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

View file

@ -2,11 +2,10 @@ github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
@ -16,21 +15,23 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A=
github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0=
github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ=
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE=
go.mau.fi/util v0.2.1 h1:eazulhFE/UmjOFtPrGg6zkF5YfAyiDzQb8ihLMbsPWw=
go.mau.fi/util v0.2.1/go.mod h1:MjlzCQEMzJ+G8RsPawHzpLB8rwTo3aPIjG5FzBvQT/c=
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/exp v0.0.0-20240213143201-ec583247a57a h1:HinSgX1tJRX3KsL//Gxynpw5CTOAIPhgL4W8PNiIpVE=
golang.org/x/exp v0.0.0-20240213143201-ec583247a57a/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I=
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View file

@ -3,11 +3,13 @@ package libgm
import (
"bytes"
"context"
"crypto/sha1"
"encoding/base64"
"fmt"
"io"
"mime"
"net/http"
"time"
"github.com/rs/zerolog"
"google.golang.org/protobuf/proto"
@ -41,13 +43,43 @@ func (c *Client) makeProtobufHTTPRequest(url string, data proto.Message, content
return nil, err
}
util.BuildRelayHeaders(req, contentType, "*/*")
c.AddCookieHeaders(req)
res, reqErr := c.http.Do(req)
if reqErr != nil {
return res, reqErr
}
c.HandleCookieUpdates(res)
return res, nil
}
func (c *Client) AddCookieHeaders(req *http.Request) {
if c.AuthData == nil || c.AuthData.Cookies == nil {
return
}
for k, v := range c.AuthData.Cookies {
req.AddCookie(&http.Cookie{Name: k, Value: v})
}
sapisid, ok := c.AuthData.Cookies["SAPISID"]
if ok {
req.Header.Set("Authorization", sapisidHash(util.MessagesBaseURL, sapisid))
}
}
func (c *Client) HandleCookieUpdates(resp *http.Response) {
if c.AuthData.Cookies == nil {
return
}
for _, cookie := range resp.Cookies() {
c.AuthData.Cookies[cookie.Name] = cookie.Value
}
}
func sapisidHash(origin, sapisid string) string {
ts := time.Now().Unix()
hash := sha1.Sum([]byte(fmt.Sprintf("%d %s %s", ts, sapisid, origin)))
return fmt.Sprintf("SAPISIDHASH %d_%x", ts, hash[:])
}
func decodeProtoResp(body []byte, contentType string, into proto.Message) error {
contentType, _, err := mime.ParseMediaType(contentType)
if err != nil {
@ -56,7 +88,7 @@ func decodeProtoResp(body []byte, contentType string, into proto.Message) error
switch contentType {
case ContentTypeProtobuf:
return proto.Unmarshal(body, into)
case ContentTypePBLite:
case ContentTypePBLite, "text/plain":
return pblite.Unmarshal(body, into)
default:
return fmt.Errorf("unknown content type %s in response", contentType)

View file

@ -122,13 +122,19 @@ func (c *Client) doLongPoll(loggedIn bool) {
Auth: &gmproto.AuthMessage{
RequestID: listenReqID,
TachyonAuthToken: c.AuthData.TachyonAuthToken,
Network: c.AuthData.AuthNetwork(),
ConfigVersion: util.ConfigMessage,
},
Unknown: &gmproto.ReceiveMessagesRequest_UnknownEmptyObject2{
Unknown: &gmproto.ReceiveMessagesRequest_UnknownEmptyObject1{},
},
}
resp, err := c.makeProtobufHTTPRequest(util.ReceiveMessagesURL, payload, ContentTypePBLite)
url := util.ReceiveMessagesURL
if c.AuthData.Cookies != nil {
url = util.ReceiveMessagesURLGoogle
payload.Auth.Network = util.GoogleNetwork
}
resp, err := c.makeProtobufHTTPRequest(url, payload, ContentTypePBLite)
if err != nil {
if loggedIn {
c.triggerEvent(&events.ListenTemporaryError{Error: err})

View file

@ -120,9 +120,9 @@ func (c *Client) SetTyping(convID string) error {
func (c *Client) SetActiveSession() error {
c.sessionHandler.ResetSessionID()
return c.sessionHandler.sendMessageNoResponse(SendMessageParams{
Action: gmproto.ActionType_GET_UPDATES,
OmitTTL: true,
UseSessionID: true,
Action: gmproto.ActionType_GET_UPDATES,
OmitTTL: true,
RequestID: c.sessionHandler.sessionID,
})
}

View file

@ -19,7 +19,7 @@ func (c *Client) StartLogin() (string, error) {
if err != nil {
return "", err
}
c.AuthData.TachyonAuthToken = registered.AuthKeyData.TachyonAuthToken
c.updateTachyonAuthToken(registered.GetAuthKeyData())
go c.doLongPoll(false)
qr, err := c.GenerateQRCodeData(registered.GetPairingKey())
if err != nil {
@ -54,7 +54,7 @@ func (c *Client) handlePairingEvent(msg *IncomingRPCMessage) {
}
func (c *Client) completePairing(data *gmproto.PairedData) {
c.updateTachyonAuthToken(data.GetTokenData().GetTachyonAuthToken(), data.GetTokenData().GetTTL())
c.updateTachyonAuthToken(data.GetTokenData())
c.AuthData.Mobile = data.Mobile
c.AuthData.Browser = data.Browser
@ -81,7 +81,7 @@ func (c *Client) RegisterPhoneRelay() (*gmproto.RegisterPhoneRelayResponse, erro
payload := &gmproto.AuthenticationContainer{
AuthMessage: &gmproto.AuthMessage{
RequestID: uuid.NewString(),
Network: &util.Network,
Network: util.QRNetwork,
ConfigVersion: util.ConfigMessage,
},
BrowserDetails: util.BrowserDetailsMessage,
@ -103,7 +103,7 @@ func (c *Client) RefreshPhoneRelay() (string, error) {
payload := &gmproto.AuthenticationContainer{
AuthMessage: &gmproto.AuthMessage{
RequestID: uuid.NewString(),
Network: &util.Network,
Network: util.QRNetwork,
TachyonAuthToken: c.AuthData.TachyonAuthToken,
ConfigVersion: util.ConfigMessage,
},
@ -134,7 +134,7 @@ func (c *Client) GetWebEncryptionKey() (*gmproto.WebEncryptionKeyResponse, error
)
}
func (c *Client) Unpair() (*gmproto.RevokeRelayPairingResponse, error) {
func (c *Client) UnpairBugle() (*gmproto.RevokeRelayPairingResponse, error) {
if c.AuthData.TachyonAuthToken == nil || c.AuthData.Browser == nil {
return nil, nil
}
@ -150,3 +150,12 @@ func (c *Client) Unpair() (*gmproto.RevokeRelayPairingResponse, error) {
c.makeProtobufHTTPRequest(util.RevokeRelayPairingURL, payload, ContentTypeProtobuf),
)
}
func (c *Client) Unpair() (err error) {
if c.AuthData.Cookies != nil {
err = c.UnpairGaia()
} else {
_, err = c.UnpairBugle()
}
return
}

349
libgm/pair_google.go Normal file
View file

@ -0,0 +1,349 @@
// mautrix-gmessages - A Matrix-Google Messages puppeting bridge.
// Copyright (C) 2024 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package libgm
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/sha256"
"crypto/sha512"
"crypto/x509"
"encoding/binary"
"errors"
"fmt"
"io"
"math/big"
"time"
"github.com/google/uuid"
"go.mau.fi/util/random"
"golang.org/x/crypto/hkdf"
"google.golang.org/protobuf/proto"
"go.mau.fi/mautrix-gmessages/libgm/events"
"go.mau.fi/mautrix-gmessages/libgm/gmproto"
"go.mau.fi/mautrix-gmessages/libgm/util"
)
func (c *Client) handleGaiaPairingEvent(msg *IncomingRPCMessage) {
c.Logger.Debug().Any("evt", msg.Gaia).Msg("Gaia event")
}
func (c *Client) baseSignInGaiaPayload() *gmproto.SignInGaiaRequest {
return &gmproto.SignInGaiaRequest{
AuthMessage: &gmproto.AuthMessage{
RequestID: uuid.NewString(),
Network: util.GoogleNetwork,
ConfigVersion: util.ConfigMessage,
},
Inner: &gmproto.SignInGaiaRequest_Inner{
DeviceID: &gmproto.SignInGaiaRequest_Inner_DeviceID{
UnknownInt1: 3,
DeviceID: fmt.Sprintf("messages-web-%x", c.AuthData.SessionID[:]),
},
},
Network: util.GoogleNetwork,
}
}
func (c *Client) signInGaiaInitial() (*gmproto.SignInGaiaResponse, error) {
payload := c.baseSignInGaiaPayload()
payload.UnknownInt3 = 1
return typedHTTPResponse[*gmproto.SignInGaiaResponse](
c.makeProtobufHTTPRequest(util.SignInGaiaURL, payload, ContentTypePBLite),
)
}
func (c *Client) signInGaiaGetToken() (*gmproto.SignInGaiaResponse, error) {
key, err := x509.MarshalPKIXPublicKey(c.AuthData.RefreshKey.GetPublicKey())
if err != nil {
return nil, err
}
payload := c.baseSignInGaiaPayload()
payload.Inner.SomeData = &gmproto.SignInGaiaRequest_Inner_Data{
SomeData: key,
}
resp, err := typedHTTPResponse[*gmproto.SignInGaiaResponse](
c.makeProtobufHTTPRequest(util.SignInGaiaURL, payload, ContentTypePBLite),
)
if err != nil {
return nil, err
}
c.updateTachyonAuthToken(resp.GetTokenData())
c.AuthData.Mobile = resp.GetDeviceData().GetDeviceWrapper().GetDevice()
c.AuthData.Browser = resp.GetDeviceData().GetDeviceWrapper().GetDevice()
return resp, nil
}
type PairingSession struct {
UUID uuid.UUID
Start time.Time
PairingKeyDSA *ecdsa.PrivateKey
InitPayload []byte
NextKey []byte
}
func NewPairingSession() PairingSession {
ec, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
panic(err)
}
return PairingSession{
UUID: uuid.New(),
Start: time.Now(),
PairingKeyDSA: ec,
}
}
func (ps *PairingSession) PreparePayloads() ([]byte, []byte, error) {
pubKey := &gmproto.GenericPublicKey{
Type: gmproto.PublicKeyType_EC_P256,
PublicKey: &gmproto.GenericPublicKey_EcP256PublicKey{
EcP256PublicKey: &gmproto.EcP256PublicKey{
X: make([]byte, 33),
Y: make([]byte, 33),
},
},
}
ps.PairingKeyDSA.X.FillBytes(pubKey.GetEcP256PublicKey().GetX()[1:])
ps.PairingKeyDSA.Y.FillBytes(pubKey.GetEcP256PublicKey().GetY()[1:])
finishPayload, err := proto.Marshal(&gmproto.Ukey2ClientFinished{
PublicKey: pubKey,
})
if err != nil {
return nil, nil, fmt.Errorf("failed to marshal finish payload: %w", err)
}
finish, err := proto.Marshal(&gmproto.Ukey2Message{
MessageType: gmproto.Ukey2Message_CLIENT_FINISH,
MessageData: finishPayload,
})
if err != nil {
return nil, nil, fmt.Errorf("failed to marshal finish message: %w", err)
}
keyCommitment := sha512.Sum512(finish)
initPayload, err := proto.Marshal(&gmproto.Ukey2ClientInit{
Version: 1,
Random: random.Bytes(32),
CipherCommitments: []*gmproto.Ukey2ClientInit_CipherCommitment{{
HandshakeCipher: gmproto.Ukey2HandshakeCipher_P256_SHA512,
Commitment: keyCommitment[:],
}},
NextProtocol: "AES_256_CBC-HMAC_SHA256",
})
if err != nil {
return nil, nil, fmt.Errorf("failed to marshal init payload: %w", err)
}
init, err := proto.Marshal(&gmproto.Ukey2Message{
MessageType: gmproto.Ukey2Message_CLIENT_INIT,
MessageData: initPayload,
})
if err != nil {
return nil, nil, fmt.Errorf("failed to marshal init message: %w", err)
}
ps.InitPayload = init
return init, finish, nil
}
func doHKDF(key []byte, salt, info []byte) []byte {
h := hkdf.New(sha256.New, key, salt, info)
out := make([]byte, 32)
_, err := io.ReadFull(h, out)
if err != nil {
panic(err)
}
return out
}
var encryptionKeyInfo = []byte{130, 170, 85, 160, 211, 151, 248, 131, 70, 202, 28, 238, 141, 57, 9, 185, 95, 19, 250, 125, 235, 29, 74, 179, 131, 118, 184, 37, 109, 168, 85, 16}
var pairingEmojis = []string{"😁", "😅", "🤣", "🫠", "🥰", "😇", "🤩", "😘", "😜", "🤗", "🤔", "🤐", "😴", "🥶", "🤯", "🤠", "🥳", "🥸", "😎", "🤓", "🧐", "🥹", "😭", "😱", "😖", "🥱", "😮\u200d💨", "🤡", "💩", "👻", "👽", "🤖", "😻", "💌", "💘", "💕", "❤", "💢", "💥", "💫", "💬", "🗯", "💤", "👋", "🙌", "🙏", "✍", "🦶", "👂", "🧠", "🦴", "👀", "🧑", "🧚", "🧍", "👣", "🐵", "🐶", "🐺", "🦊", "🦁", "🐯", "🦓", "🦄", "🐑", "🐮", "🐷", "🐿", "🐰", "🦇", "🐻", "🐨", "🐼", "🦥", "🐾", "🐔", "🐥", "🐦", "🕊", "🦆", "🦉", "🪶", "🦩", "🐸", "🐢", "🦎", "🐍", "🐳", "🐬", "🦭", "🐠", "🐡", "🦈", "🪸", "🐌", "🦋", "🐛", "🐝", "🐞", "🪱", "💐", "🌸", "🌹", "🌻", "🌱", "🌲", "🌴", "🌵", "🌾", "☘", "🍁", "🍂", "🍄", "🪺", "🍇", "🍈", "🍉", "🍋", "🍌", "🍍", "🍎", "🍐", "🍒", "🍓", "🥝", "🥥", "🥑", "🥕", "🌽", "🌶", "🫑", "🥦", "🥜", "🍞", "🥐", "🥨", "🧀", "🍗", "🍔", "🍟", "🍕", "🌭", "🌮", "🥗", "🥣", "🍿", "🦀", "🦑", "🍦", "🍩", "🍪", "🍫", "🍰", "🍬", "🍭", "☕", "🫖", "🍹", "🥤", "🧊", "🥢", "🍽", "🥄", "🧭", "🏔", "🌋", "🏕", "🏖", "🪵", "🏗", "🏡", "🏰", "🛝", "🚂", "🛵", "🛴", "🛼", "🚥", "⚓", "🛟", "⛵", "✈", "🚀", "🛸", "🧳", "⏰", "🌙", "🌡", "🌞", "🪐", "🌠", "🌧", "🌀", "🌈", "☂", "⚡", "❄", "⛄", "🔥", "🎇", "🧨", "✨", "🎈", "🎉", "🎁", "🏆", "🏅", "⚽", "⚾", "🏀", "🏐", "🏈", "🎾", "🎳", "🏓", "🥊", "⛳", "⛸", "🎯", "🪁", "🔮", "🎮", "🧩", "🧸", "🪩", "🖼", "🎨", "🧵", "🧶", "🦺", "🧣", "🧤", "🧦", "🎒", "🩴", "👟", "👑", "👒", "🎩", "🧢", "💎", "🔔", "🎤", "📻", "🎷", "🪗", "🎸", "🎺", "🎻", "🥁", "📺", "🔋", "💻", "💿", "☎", "🕯", "💡", "📖", "📚", "📬", "✏", "✒", "🖌", "🖍", "📝", "💼", "📋", "📌", "📎", "🔑", "🔧", "🧲", "🪜", "🧬", "🔭", "🩹", "🩺", "🪞", "🛋", "🪑", "🛁", "🧹", "🧺", "🔱", "🏁", "🐪", "🐘", "🦃", "🍞", "🍜", "🍠", "🚘", "🤿", "🃏", "👕", "📸", "🏷", "✂", "🧪", "🚪", "🧴", "🧻", "🪣", "🧽", "🚸"}
func (ps *PairingSession) ProcessServerInit(msg *gmproto.GaiaPairingResponseContainer) (string, error) {
var ukeyMessage gmproto.Ukey2Message
err := proto.Unmarshal(msg.GetData(), &ukeyMessage)
if err != nil {
return "", fmt.Errorf("failed to unmarshal server init message: %w", err)
} else if ukeyMessage.GetMessageType() != gmproto.Ukey2Message_SERVER_INIT {
return "", fmt.Errorf("unexpected message type: %v", ukeyMessage.GetMessageType())
}
var serverInit gmproto.Ukey2ServerInit
err = proto.Unmarshal(ukeyMessage.GetMessageData(), &serverInit)
if err != nil {
return "", fmt.Errorf("failed to unmarshal server init payload: %w", err)
} else if serverInit.GetVersion() != 1 {
return "", fmt.Errorf("unexpected server init version: %d", serverInit.GetVersion())
} else if serverInit.GetHandshakeCipher() != gmproto.Ukey2HandshakeCipher_P256_SHA512 {
return "", fmt.Errorf("unexpected handshake cipher: %v", serverInit.GetHandshakeCipher())
} else if len(serverInit.GetRandom()) != 32 {
return "", fmt.Errorf("unexpected random length %d", len(serverInit.GetRandom()))
}
serverKeyData := serverInit.GetPublicKey().GetEcP256PublicKey()
x, y := serverKeyData.GetX(), serverKeyData.GetY()
if len(x) == 33 {
if x[0] != 0 {
return "", fmt.Errorf("server key x coordinate has unexpected prefix: %d", x[0])
}
x = x[1:]
}
if len(y) == 33 {
if y[0] != 0 {
return "", fmt.Errorf("server key y coordinate has unexpected prefix: %d", y[0])
}
y = y[1:]
}
serverPairingKeyDSA := &ecdsa.PublicKey{
Curve: elliptic.P256(),
X: big.NewInt(0).SetBytes(x),
Y: big.NewInt(0).SetBytes(y),
}
serverPairingKeyDH, err := serverPairingKeyDSA.ECDH()
if err != nil {
return "", fmt.Errorf("invalid server key: %w", err)
}
ourPairingKeyDH, err := ps.PairingKeyDSA.ECDH()
if err != nil {
return "", fmt.Errorf("invalid our key: %w", err)
}
diffieHellman, err := ourPairingKeyDH.ECDH(serverPairingKeyDH)
if err != nil {
return "", fmt.Errorf("failed to calculate shared secret: %w", err)
}
sharedSecret := sha256.Sum256(diffieHellman)
authInfo := append(ps.InitPayload, msg.GetData()...)
ukeyV1Auth := doHKDF(sharedSecret[:], []byte("UKEY2 v1 auth"), authInfo)
ps.NextKey = doHKDF(sharedSecret[:], []byte("UKEY2 v1 next"), authInfo)
authNumber := binary.BigEndian.Uint32(ukeyV1Auth)
pairingEmoji := pairingEmojis[int(authNumber)%len(pairingEmojis)]
return pairingEmoji, nil
}
var (
ErrNoCookies = errors.New("gaia pairing requires cookies")
ErrNoDevicesFound = errors.New("no devices found for gaia pairing")
ErrIncorrectEmoji = errors.New("user chose incorrect emoji on phone")
ErrPairingCancelled = errors.New("user cancelled pairing on phone")
ErrPairingTimeout = errors.New("pairing timed out")
)
func (c *Client) DoGaiaPairing(emojiCallback func(string)) error {
if len(c.AuthData.Cookies) == 0 {
return ErrNoCookies
}
sigResp, err := c.signInGaiaGetToken()
if err != nil {
return fmt.Errorf("failed to prepare gaia pairing: %w", err)
}
// TODO multiple devices?
var destRegID string
for _, dev := range sigResp.GetDeviceData().GetUnknownItems2() {
if dev.GetUnknownInt4() == 1 {
destRegID = dev.GetDestOrSourceUUID()
break
}
}
if destRegID == "" {
return ErrNoDevicesFound
}
destRegUUID, err := uuid.Parse(destRegID)
if err != nil {
return fmt.Errorf("failed to parse destination UUID: %w", err)
}
c.AuthData.DestRegID = destRegUUID
go c.doLongPoll(false)
ps := NewPairingSession()
clientInit, clientFinish, err := ps.PreparePayloads()
if err != nil {
return fmt.Errorf("failed to prepare pairing payloads: %w", err)
}
serverInit, err := c.sendGaiaPairingMessage(ps, gmproto.ActionType_CREATE_GAIA_PAIRING_CLIENT_INIT, clientInit)
if err != nil {
return fmt.Errorf("failed to send client init: %w", err)
}
pairingEmoji, err := ps.ProcessServerInit(serverInit)
if err != nil {
return fmt.Errorf("error processing server init: %w", err)
}
emojiCallback(pairingEmoji)
finishResp, err := c.sendGaiaPairingMessage(ps, gmproto.ActionType_CREATE_GAIA_PAIRING_CLIENT_FINISHED, clientFinish)
if finishResp.GetFinishErrorType() != 0 {
switch finishResp.GetFinishErrorCode() {
case 5:
return ErrIncorrectEmoji
case 7:
return ErrPairingCancelled
case 6, 2, 3:
return fmt.Errorf("%w (code: %d/%d)", ErrPairingTimeout, finishResp.GetFinishErrorType(), finishResp.GetFinishErrorCode())
default:
return fmt.Errorf("unknown error pairing: %d/%d", finishResp.GetFinishErrorType(), finishResp.GetFinishErrorCode())
}
}
c.AuthData.RequestCrypto.AESKey = doHKDF(ps.NextKey, encryptionKeyInfo, []byte("client"))
c.AuthData.RequestCrypto.HMACKey = doHKDF(ps.NextKey, encryptionKeyInfo, []byte("server"))
c.AuthData.PairingID = ps.UUID
c.triggerEvent(&events.PairSuccessful{})
go func() {
// Sleep for a bit to let the phone save the pair data. If we reconnect too quickly,
// the phone won't recognize the session the bridge will get unpaired.
time.Sleep(2 * time.Second)
err := c.Reconnect()
if err != nil {
c.triggerEvent(&events.ListenFatalError{Error: fmt.Errorf("failed to reconnect after pair success: %w", err)})
}
}()
return nil
}
func (c *Client) sendGaiaPairingMessage(sess PairingSession, action gmproto.ActionType, msg []byte) (*gmproto.GaiaPairingResponseContainer, error) {
resp, err := c.sessionHandler.sendMessageWithParams(SendMessageParams{
Action: action,
Data: &gmproto.GaiaPairingRequestContainer{
PairingAttemptID: sess.UUID.String(),
BrowserDetails: util.BrowserDetailsMessage,
StartTimestamp: sess.Start.UnixMilli(),
Data: msg,
},
DontEncrypt: true,
CustomTTL: (300 * time.Second).Microseconds(),
MessageType: gmproto.MessageType_GAIA_2,
NoPingOnTimeout: true,
})
if err != nil {
return nil, err
}
var respDat gmproto.GaiaPairingResponseContainer
err = proto.Unmarshal(resp.Message.UnencryptedData, &respDat)
if err != nil {
return nil, err
}
return &respDat, nil
}
func (c *Client) UnpairGaia() error {
return c.sessionHandler.sendMessageNoResponse(SendMessageParams{
Action: gmproto.ActionType_UNPAIR_GAIA_PAIRING,
Data: &gmproto.RevokeGaiaPairingRequest{
PairingAttemptID: c.AuthData.PairingID.String(),
},
NoPingOnTimeout: true,
})
}

View file

@ -4,9 +4,13 @@ import (
"encoding/base64"
"encoding/json"
"fmt"
"strconv"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/types/descriptorpb"
"go.mau.fi/mautrix-gmessages/libgm/gmproto"
)
func Unmarshal(data []byte, m proto.Message) error {
@ -21,6 +25,12 @@ func Unmarshal(data []byte, m proto.Message) error {
return deserializeFromSlice(anyDataArr, m.ProtoReflect())
}
func isPbliteBinary(descriptor protoreflect.FieldDescriptor) bool {
opts := descriptor.Options().(*descriptorpb.FieldOptions)
pbliteBinary, ok := proto.GetExtension(opts, gmproto.E_PbliteBinary).(bool)
return ok && pbliteBinary
}
func deserializeOne(val any, index int, ref protoreflect.Message, insideList protoreflect.List, fieldDescriptor protoreflect.FieldDescriptor) (protoreflect.Value, error) {
var num float64
var expectedKind, str string
@ -45,18 +55,33 @@ func deserializeOne(val any, index int, ref protoreflect.Message, insideList pro
switch fieldDescriptor.Kind() {
case protoreflect.MessageKind:
ok = true
nestedData, ok := val.([]any)
if !ok {
return outputVal, fmt.Errorf("expected untyped array at index %d for field %s, got %T", index, fieldDescriptor.FullName(), val)
}
var nestedMessage protoreflect.Message
if insideList != nil {
nestedMessage = insideList.NewElement().Message()
} else {
nestedMessage = ref.NewField(fieldDescriptor).Message()
}
if err := deserializeFromSlice(nestedData, nestedMessage); err != nil {
return outputVal, err
if isPbliteBinary(fieldDescriptor) {
bytesBase64, ok := val.(string)
if !ok {
return outputVal, fmt.Errorf("expected string at index %d for field %s, got %T", index, fieldDescriptor.FullName(), val)
}
bytes, err := base64.StdEncoding.DecodeString(bytesBase64)
if err != nil {
return outputVal, fmt.Errorf("failed to decode base64 at index %d for field %s: %w", index, fieldDescriptor.FullName(), err)
}
err = proto.Unmarshal(bytes, nestedMessage.Interface())
if err != nil {
return outputVal, fmt.Errorf("failed to unmarshal binary protobuf at index %d for field %s: %w", index, fieldDescriptor.FullName(), err)
}
} else {
nestedData, ok := val.([]any)
if !ok {
return outputVal, fmt.Errorf("expected untyped array at index %d for field %s, got %T", index, fieldDescriptor.FullName(), val)
}
if err := deserializeFromSlice(nestedData, nestedMessage); err != nil {
return outputVal, err
}
}
outputVal = protoreflect.ValueOfMessage(nestedMessage)
case protoreflect.BytesKind:
@ -76,21 +101,53 @@ func deserializeOne(val any, index int, ref protoreflect.Message, insideList pro
expectedKind = "float64"
outputVal = protoreflect.ValueOfEnum(protoreflect.EnumNumber(int32(num)))
case protoreflect.Int32Kind:
num, ok = val.(float64)
expectedKind = "float64"
outputVal = protoreflect.ValueOfInt32(int32(num))
if str, ok = val.(string); ok {
parsedVal, err := strconv.ParseInt(str, 10, 32)
if err != nil {
return outputVal, fmt.Errorf("failed to parse int32 at index %d for field %s: %w", index, fieldDescriptor.FullName(), err)
}
outputVal = protoreflect.ValueOfInt32(int32(parsedVal))
} else {
num, ok = val.(float64)
expectedKind = "float64"
outputVal = protoreflect.ValueOfInt32(int32(num))
}
case protoreflect.Int64Kind:
num, ok = val.(float64)
expectedKind = "float64"
outputVal = protoreflect.ValueOfInt64(int64(num))
if str, ok = val.(string); ok {
parsedVal, err := strconv.ParseInt(str, 10, 64)
if err != nil {
return outputVal, fmt.Errorf("failed to parse int64 at index %d for field %s: %w", index, fieldDescriptor.FullName(), err)
}
outputVal = protoreflect.ValueOfInt64(parsedVal)
} else {
num, ok = val.(float64)
expectedKind = "float64"
outputVal = protoreflect.ValueOfInt64(int64(num))
}
case protoreflect.Uint32Kind:
num, ok = val.(float64)
expectedKind = "float64"
outputVal = protoreflect.ValueOfUint32(uint32(num))
if str, ok = val.(string); ok {
parsedVal, err := strconv.ParseUint(str, 10, 32)
if err != nil {
return outputVal, fmt.Errorf("failed to parse uint32 at index %d for field %s: %w", index, fieldDescriptor.FullName(), err)
}
outputVal = protoreflect.ValueOfUint32(uint32(parsedVal))
} else {
num, ok = val.(float64)
expectedKind = "float64"
outputVal = protoreflect.ValueOfUint32(uint32(num))
}
case protoreflect.Uint64Kind:
num, ok = val.(float64)
expectedKind = "float64"
outputVal = protoreflect.ValueOfUint64(uint64(num))
if str, ok = val.(string); ok {
parsedVal, err := strconv.ParseUint(str, 10, 64)
if err != nil {
return outputVal, fmt.Errorf("failed to parse uint64 at index %d for field %s: %w", index, fieldDescriptor.FullName(), err)
}
outputVal = protoreflect.ValueOfUint64(parsedVal)
} else {
num, ok = val.(float64)
expectedKind = "float64"
outputVal = protoreflect.ValueOfUint64(uint64(num))
}
case protoreflect.FloatKind:
num, ok = val.(float64)
expectedKind = "float64"
@ -101,6 +158,13 @@ func deserializeOne(val any, index int, ref protoreflect.Message, insideList pro
outputVal = protoreflect.ValueOfFloat64(num)
case protoreflect.StringKind:
str, ok = val.(string)
if ok && isPbliteBinary(fieldDescriptor) {
bytes, err := base64.StdEncoding.DecodeString(str)
if err != nil {
return outputVal, fmt.Errorf("failed to decode base64 at index %d for field %s: %w", index, fieldDescriptor.FullName(), err)
}
str = string(bytes)
}
expectedKind = "string"
outputVal = protoreflect.ValueOfString(str)
case protoreflect.BoolKind:

View file

@ -82,11 +82,19 @@ func serializeOneOrList(fieldDescriptor protoreflect.FieldDescriptor, fieldValue
func serializeOne(fieldDescriptor protoreflect.FieldDescriptor, fieldValue protoreflect.Value) (any, error) {
switch fieldDescriptor.Kind() {
case protoreflect.MessageKind:
serializedMsg, err := SerializeToSlice(fieldValue.Message().Interface())
if err != nil {
return nil, err
if isPbliteBinary(fieldDescriptor) {
serializedMsg, err := proto.Marshal(fieldValue.Message().Interface())
if err != nil {
return nil, err
}
return base64.StdEncoding.EncodeToString(serializedMsg), nil
} else {
serializedMsg, err := SerializeToSlice(fieldValue.Message().Interface())
if err != nil {
return nil, err
}
return serializedMsg, nil
}
return serializedMsg, nil
case protoreflect.BytesKind:
return base64.StdEncoding.EncodeToString(fieldValue.Bytes()), nil
case protoreflect.Int32Kind, protoreflect.Int64Kind:
@ -100,7 +108,11 @@ func serializeOne(fieldDescriptor protoreflect.FieldDescriptor, fieldValue proto
case protoreflect.BoolKind:
return fieldValue.Bool(), nil
case protoreflect.StringKind:
return fieldValue.String(), nil
if isPbliteBinary(fieldDescriptor) {
return base64.StdEncoding.EncodeToString([]byte(fieldValue.String())), nil
} else {
return fieldValue.String(), nil
}
default:
return nil, fmt.Errorf("unsupported field type %s in %s", fieldDescriptor.Kind(), fieldDescriptor.FullName())
}

View file

@ -0,0 +1,54 @@
package main
import (
"fmt"
"io"
"os"
"google.golang.org/protobuf/encoding/prototext"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/types/dynamicpb"
"go.mau.fi/mautrix-gmessages/libgm/gmproto"
"go.mau.fi/mautrix-gmessages/libgm/pblite"
)
func must[T any](t T, err error) T {
if err != nil {
panic(err)
}
return t
}
func main() {
files := []protoreflect.FileDescriptor{
gmproto.File_authentication_proto,
gmproto.File_config_proto,
gmproto.File_client_proto,
gmproto.File_conversations_proto,
gmproto.File_events_proto,
gmproto.File_rpc_proto,
gmproto.File_settings_proto,
gmproto.File_util_proto,
gmproto.File_ukey_proto,
}
var msgDesc protoreflect.MessageDescriptor
for _, file := range files {
msgDesc = file.Messages().ByName(protoreflect.Name(os.Args[1]))
if msgDesc != nil {
break
}
}
if msgDesc == nil {
fmt.Println("Message not found")
os.Exit(1)
}
msg := dynamicpb.NewMessage(msgDesc)
err := pblite.Unmarshal(must(io.ReadAll(os.Stdin)), msg)
if err != nil {
fmt.Println(err)
os.Exit(2)
}
fmt.Println(prototext.Format(msg))
}

View file

@ -37,8 +37,12 @@ func (s *SessionHandler) sendMessageNoResponse(params SendMessageParams) error {
return err
}
url := util.SendMessageURL
if s.client.AuthData.Cookies != nil {
url = util.SendMessageURLGoogle
}
_, err = typedHTTPResponse[*gmproto.OutgoingRPCResponse](
s.client.makeProtobufHTTPRequest(util.SendMessageURL, payload, ContentTypePBLite),
s.client.makeProtobufHTTPRequest(url, payload, ContentTypePBLite),
)
return err
}
@ -50,8 +54,12 @@ func (s *SessionHandler) sendAsyncMessage(params SendMessageParams) (<-chan *Inc
}
ch := s.waitResponse(requestID)
url := util.SendMessageURL
if s.client.AuthData.Cookies != nil {
url = util.SendMessageURLGoogle
}
_, err = typedHTTPResponse[*gmproto.OutgoingRPCResponse](
s.client.makeProtobufHTTPRequest(util.SendMessageURL, payload, ContentTypePBLite),
s.client.makeProtobufHTTPRequest(url, payload, ContentTypePBLite),
)
if err != nil {
s.cancelResponse(requestID, ch)
@ -68,7 +76,7 @@ func typedResponse[T proto.Message](resp *IncomingRPCMessage, err error) (casted
var ok bool
casted, ok = resp.DecryptedMessage.(T)
if !ok {
retErr = fmt.Errorf("unexpected response type %T, expected %T", resp.DecryptedMessage, casted)
retErr = fmt.Errorf("unexpected response type %T for %s, expected %T", resp.DecryptedMessage, resp.ResponseID, casted)
}
return
}
@ -92,6 +100,17 @@ func (s *SessionHandler) receiveResponse(msg *IncomingRPCMessage) bool {
if msg.Message == nil {
return false
}
if s.client.AuthData.Cookies != nil {
switch msg.Message.Action {
case gmproto.ActionType_CREATE_GAIA_PAIRING_CLIENT_INIT, gmproto.ActionType_CREATE_GAIA_PAIRING_CLIENT_FINISHED:
default:
// Very hacky way to ignore weird messages that come before real responses
// TODO figure out how to properly handle these
if msg.Message.UnencryptedData != nil && msg.Message.EncryptedData == nil {
return false
}
}
}
requestID := msg.Message.SessionID
s.responseWaitersLock.Lock()
ch, ok := s.responseWaiters[requestID]
@ -122,6 +141,10 @@ func (s *SessionHandler) sendMessageWithParams(params SendMessageParams) (*Incom
return nil, err
}
if params.NoPingOnTimeout {
return <-ch, nil
}
select {
case resp := <-ch:
return resp, nil
@ -147,19 +170,21 @@ type SendMessageParams struct {
Action gmproto.ActionType
Data proto.Message
UseSessionID bool
OmitTTL bool
MessageType gmproto.MessageType
RequestID string
OmitTTL bool
CustomTTL int64
DontEncrypt bool
MessageType gmproto.MessageType
NoPingOnTimeout bool
}
func (s *SessionHandler) buildMessage(params SendMessageParams) (string, proto.Message, error) {
var requestID string
var err error
sessionID := s.client.sessionHandler.sessionID
if params.UseSessionID {
requestID = s.sessionID
} else {
requestID := params.RequestID
if requestID == "" {
requestID = uuid.NewString()
}
@ -182,28 +207,38 @@ func (s *SessionHandler) buildMessage(params SendMessageParams) (string, proto.M
TachyonAuthToken: s.client.AuthData.TachyonAuthToken,
ConfigVersion: util.ConfigMessage,
},
EmptyArr: &gmproto.EmptyArr{},
DestRegistrationIDs: []string{},
}
if !params.OmitTTL {
if s.client.AuthData != nil && s.client.AuthData.DestRegID != uuid.Nil {
message.DestRegistrationIDs = append(message.DestRegistrationIDs, s.client.AuthData.DestRegID.String())
}
if params.CustomTTL != 0 {
message.TTL = params.CustomTTL
} else if !params.OmitTTL {
message.TTL = s.client.AuthData.TachyonTTL
}
var encryptedData []byte
var encryptedData, unencryptedData []byte
if params.Data != nil {
var serializedData []byte
serializedData, err = proto.Marshal(params.Data)
if err != nil {
return "", nil, err
}
encryptedData, err = s.client.AuthData.RequestCrypto.Encrypt(serializedData)
if err != nil {
return "", nil, err
if params.DontEncrypt {
unencryptedData = serializedData
} else {
encryptedData, err = s.client.AuthData.RequestCrypto.Encrypt(serializedData)
if err != nil {
return "", nil, err
}
}
}
message.Data.MessageData, err = proto.Marshal(&gmproto.OutgoingRPCData{
RequestID: requestID,
Action: params.Action,
EncryptedProtoData: encryptedData,
SessionID: sessionID,
RequestID: requestID,
Action: params.Action,
UnencryptedProtoData: unencryptedData,
EncryptedProtoData: encryptedData,
SessionID: sessionID,
})
if err != nil {
return "", nil, err
@ -255,13 +290,18 @@ func (s *SessionHandler) sendAckRequest() {
AuthData: &gmproto.AuthMessage{
RequestID: uuid.NewString(),
TachyonAuthToken: s.client.AuthData.TachyonAuthToken,
Network: s.client.AuthData.AuthNetwork(),
ConfigVersion: util.ConfigMessage,
},
EmptyArr: &gmproto.EmptyArr{},
Acks: ackMessages,
}
url := util.AckMessagesURL
if s.client.AuthData.Cookies != nil {
url = util.AckMessagesURLGoogle
}
_, err := typedHTTPResponse[*gmproto.OutgoingRPCResponse](
s.client.makeProtobufHTTPRequest(util.AckMessagesURL, payload, ContentTypePBLite),
s.client.makeProtobufHTTPRequest(url, payload, ContentTypePBLite),
)
if err != nil {
// TODO retry?

View file

@ -5,13 +5,16 @@ import (
)
var ConfigMessage = &gmproto.ConfigVersion{
Year: 2023,
Month: 9,
Day: 28,
Year: 2024,
Month: 2,
Day: 20,
V1: 4,
V2: 6,
}
var Network = "Bugle"
const QRNetwork = "Bugle"
const GoogleNetwork = "GDitto"
var BrowserDetailsMessage = &gmproto.BrowserDetails{
UserAgent: UserAgent,
BrowserType: gmproto.BrowserType_OTHER,

View file

@ -1,7 +1,7 @@
package util
const GoogleAPIKey = "AIzaSyCA4RsOZUFrm9whhtGosPlJLmVPnfSHKz8"
const UserAgent = "Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36"
const UserAgent = "Mozilla/5.0 (Linux; Android 14) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36"
const SecUA = `"Google Chrome";v="113", "Chromium";v="113", "Not-A.Brand";v="24"`
const UAPlatform = "Android"
const XUserAgent = "grpc-web-javascript/0.1"

View file

@ -1,14 +1,10 @@
package util
import (
"encoding/json"
"fmt"
"math/rand"
"net/http"
"strconv"
"time"
"go.mau.fi/mautrix-gmessages/libgm/gmproto"
)
func GenerateTmpID() string {
@ -19,119 +15,71 @@ func GenerateTmpID() string {
}
func BuildRelayHeaders(req *http.Request, contentType string, accept string) {
req.Header.Add("host", "instantmessaging-pa.googleapis.com")
req.Header.Add("connection", "keep-alive")
req.Header.Add("sec-ch-ua", SecUA)
req.Header.Add("x-user-agent", XUserAgent)
req.Header.Add("x-goog-api-key", GoogleAPIKey)
//req.Header.Set("host", "instantmessaging-pa.googleapis.com")
req.Header.Set("sec-ch-ua", SecUA)
req.Header.Set("x-user-agent", XUserAgent)
req.Header.Set("x-goog-api-key", GoogleAPIKey)
if len(contentType) > 0 {
req.Header.Add("content-type", contentType)
req.Header.Set("content-type", contentType)
}
req.Header.Add("sec-ch-ua-mobile", SecUAMobile)
req.Header.Add("user-agent", UserAgent)
req.Header.Add("sec-ch-ua-platform", "\""+UAPlatform+"\"")
req.Header.Add("accept", accept)
req.Header.Add("origin", "https://messages.google.com")
req.Header.Add("sec-fetch-site", "cross-site")
req.Header.Add("sec-fetch-mode", "cors")
req.Header.Add("sec-fetch-dest", "empty")
req.Header.Add("referer", "https://messages.google.com/")
req.Header.Add("accept-language", "en-US,en;q=0.9")
req.Header.Set("sec-ch-ua-mobile", SecUAMobile)
req.Header.Set("user-agent", UserAgent)
req.Header.Set("sec-ch-ua-platform", "\""+UAPlatform+"\"")
req.Header.Set("accept", accept)
req.Header.Set("origin", "https://messages.google.com")
req.Header.Set("sec-fetch-site", "cross-site")
req.Header.Set("sec-fetch-mode", "cors")
req.Header.Set("sec-fetch-dest", "empty")
req.Header.Set("referer", "https://messages.google.com/")
req.Header.Set("accept-language", "en-US,en;q=0.9")
}
func BuildUploadHeaders(req *http.Request, metadata string) {
req.Header.Add("host", "instantmessaging-pa.googleapis.com")
req.Header.Add("connection", "keep-alive")
req.Header.Add("x-goog-download-metadata", metadata)
req.Header.Add("sec-ch-ua", SecUA)
req.Header.Add("sec-ch-ua-mobile", SecUAMobile)
req.Header.Add("user-agent", UserAgent)
req.Header.Add("sec-ch-ua-platform", "\""+UAPlatform+"\"")
req.Header.Add("accept", "*/*")
req.Header.Add("origin", "https://messages.google.com")
req.Header.Add("sec-fetch-site", "cross-site")
req.Header.Add("sec-fetch-mode", "cors")
req.Header.Add("sec-fetch-dest", "empty")
req.Header.Add("referer", "https://messages.google.com/")
req.Header.Add("accept-encoding", "gzip, deflate, br")
req.Header.Add("accept-language", "en-US,en;q=0.9")
//req.Header.Set("host", "instantmessaging-pa.googleapis.com")
req.Header.Set("x-goog-download-metadata", metadata)
req.Header.Set("sec-ch-ua", SecUA)
req.Header.Set("sec-ch-ua-mobile", SecUAMobile)
req.Header.Set("user-agent", UserAgent)
req.Header.Set("sec-ch-ua-platform", "\""+UAPlatform+"\"")
req.Header.Set("accept", "*/*")
req.Header.Set("origin", "https://messages.google.com")
req.Header.Set("sec-fetch-site", "cross-site")
req.Header.Set("sec-fetch-mode", "cors")
req.Header.Set("sec-fetch-dest", "empty")
req.Header.Set("referer", "https://messages.google.com/")
req.Header.Set("accept-encoding", "gzip, deflate, br")
req.Header.Set("accept-language", "en-US,en;q=0.9")
}
func NewMediaUploadHeaders(imageSize string, command string, uploadOffset string, imageContentType string, protocol string) *http.Header {
headers := &http.Header{}
headers.Add("host", "instantmessaging-pa.googleapis.com")
headers.Add("connection", "keep-alive")
headers.Add("sec-ch-ua", SecUA)
headers.Set("host", "instantmessaging-pa.googleapis.com")
headers.Set("sec-ch-ua", SecUA)
if protocol != "" {
headers.Add("x-goog-upload-protocol", protocol)
headers.Set("x-goog-upload-protocol", protocol)
}
headers.Add("x-goog-upload-header-content-length", imageSize)
headers.Add("sec-ch-ua-mobile", SecUAMobile)
headers.Add("user-agent", UserAgent)
headers.Set("x-goog-upload-header-content-length", imageSize)
headers.Set("sec-ch-ua-mobile", SecUAMobile)
headers.Set("user-agent", UserAgent)
if imageContentType != "" {
headers.Add("x-goog-upload-header-content-type", imageContentType)
headers.Set("x-goog-upload-header-content-type", imageContentType)
}
headers.Add("content-type", "application/x-www-form-urlencoded;charset=UTF-8")
headers.Set("content-type", "application/x-www-form-urlencoded;charset=UTF-8")
if command != "" {
headers.Add("x-goog-upload-command", command)
headers.Set("x-goog-upload-command", command)
}
if uploadOffset != "" {
headers.Add("x-goog-upload-offset", uploadOffset)
headers.Set("x-goog-upload-offset", uploadOffset)
}
headers.Add("sec-ch-ua-platform", "\""+UAPlatform+"\"")
headers.Add("accept", "*/*")
headers.Add("origin", "https://messages.google.com")
headers.Add("sec-fetch-site", "cross-site")
headers.Add("sec-fetch-mode", "cors")
headers.Add("sec-fetch-dest", "empty")
headers.Add("referer", "https://messages.google.com/")
headers.Add("accept-encoding", "gzip, deflate, br")
headers.Add("accept-language", "en-US,en;q=0.9")
headers.Set("sec-ch-ua-platform", "\""+UAPlatform+"\"")
headers.Set("accept", "*/*")
headers.Set("origin", "https://messages.google.com")
headers.Set("sec-fetch-site", "cross-site")
headers.Set("sec-fetch-mode", "cors")
headers.Set("sec-fetch-dest", "empty")
headers.Set("referer", "https://messages.google.com/")
headers.Set("accept-encoding", "gzip, deflate, br")
headers.Set("accept-language", "en-US,en;q=0.9")
return headers
}
func ParseConfigVersion(res []byte) (*gmproto.ConfigVersion, error) {
var data []interface{}
marshalErr := json.Unmarshal(res, &data)
if marshalErr != nil {
return nil, marshalErr
}
version := data[0].(string)
v1 := version[0:4]
v2 := version[4:6]
v3 := version[6:8]
if v2[0] == 48 {
v2 = string(v2[1])
}
if v3[0] == 48 {
v3 = string(v3[1])
}
first, e := strconv.Atoi(v1)
if e != nil {
return nil, e
}
second, e1 := strconv.Atoi(v2)
if e1 != nil {
return nil, e1
}
third, e2 := strconv.Atoi(v3)
if e2 != nil {
return nil, e2
}
configMessage := &gmproto.ConfigVersion{
Year: int32(first),
Month: int32(second),
Day: int32(third),
V1: 4,
V2: 6,
}
return configMessage, nil
}

View file

@ -1,26 +1,32 @@
package util
const messagesBaseURL = "https://messages.google.com"
const MessagesBaseURL = "https://messages.google.com"
const GoogleAuthenticationURL = messagesBaseURL + "/web/authentication"
const GoogleTimesourceURL = messagesBaseURL + "/web/timesource"
const GoogleAuthenticationURL = MessagesBaseURL + "/web/authentication"
const GoogleTimesourceURL = MessagesBaseURL + "/web/timesource"
const instantMessangingBaseURL = "https://instantmessaging-pa.googleapis.com"
const instantMessagingBaseURL = "https://instantmessaging-pa.googleapis.com"
const instantMessagingBaseURLGoogle = "https://instantmessaging-pa.clients6.google.com"
const UploadMediaURL = instantMessangingBaseURL + "/upload"
const UploadMediaURL = instantMessagingBaseURL + "/upload"
const pairingBaseURL = instantMessangingBaseURL + "/$rpc/google.internal.communications.instantmessaging.v1.Pairing"
const pairingBaseURL = instantMessagingBaseURL + "/$rpc/google.internal.communications.instantmessaging.v1.Pairing"
const RegisterPhoneRelayURL = pairingBaseURL + "/RegisterPhoneRelay"
const RefreshPhoneRelayURL = pairingBaseURL + "/RefreshPhoneRelay"
const GetWebEncryptionKeyURL = pairingBaseURL + "/GetWebEncryptionKey"
const RevokeRelayPairingURL = pairingBaseURL + "/RevokeRelayPairing"
const messagingBaseURL = instantMessangingBaseURL + "/$rpc/google.internal.communications.instantmessaging.v1.Messaging"
const messagingBaseURL = instantMessagingBaseURL + "/$rpc/google.internal.communications.instantmessaging.v1.Messaging"
const messagingBaseURLGoogle = instantMessagingBaseURLGoogle + "/$rpc/google.internal.communications.instantmessaging.v1.Messaging"
const ReceiveMessagesURL = messagingBaseURL + "/ReceiveMessages"
const SendMessageURL = messagingBaseURL + "/SendMessage"
const AckMessagesURL = messagingBaseURL + "/AckMessages"
const ReceiveMessagesURLGoogle = messagingBaseURLGoogle + "/ReceiveMessages"
const SendMessageURLGoogle = messagingBaseURLGoogle + "/SendMessage"
const AckMessagesURLGoogle = messagingBaseURLGoogle + "/AckMessages"
const registrationBaseURL = instantMessangingBaseURL + "/$rpc/google.internal.communications.instantmessaging.v1.Registration"
const registrationBaseURL = instantMessagingBaseURLGoogle + "/$rpc/google.internal.communications.instantmessaging.v1.Registration"
const SignInGaiaURL = registrationBaseURL + "/SignInGaia"
const RegisterRefreshURL = registrationBaseURL + "/RegisterRefresh"
const ConfigURL = "https://messages.google.com/web/config"

View file

@ -51,6 +51,8 @@ func (prov *ProvisioningAPI) Init() {
r.Use(prov.AuthMiddleware)
r.HandleFunc("/v1/ping", prov.Ping).Methods(http.MethodGet)
r.HandleFunc("/v1/login", prov.Login).Methods(http.MethodPost)
r.HandleFunc("/v1/google_login/emoji", prov.GoogleLoginStart).Methods(http.MethodPost)
r.HandleFunc("/v1/google_login/wait", prov.GoogleLoginWait).Methods(http.MethodPost)
r.HandleFunc("/v1/logout", prov.Logout).Methods(http.MethodPost)
r.HandleFunc("/v1/delete_session", prov.DeleteSession).Methods(http.MethodPost)
r.HandleFunc("/v1/disconnect", prov.Disconnect).Methods(http.MethodPost)
@ -309,6 +311,68 @@ func (prov *ProvisioningAPI) Logout(w http.ResponseWriter, r *http.Request) {
jsonResponse(w, http.StatusOK, Response{true, "Logged out successfully."})
}
type ReqGoogleLoginStart struct {
Cookies map[string]string
}
type RespGoogleLoginStart struct {
Status string `json:"status"`
Emoji string `json:"emoji"`
}
func (prov *ProvisioningAPI) GoogleLoginStart(w http.ResponseWriter, r *http.Request) {
userID := r.URL.Query().Get("user_id")
user := prov.bridge.GetUserByMXID(id.UserID(userID))
log := prov.zlog.With().Str("user_id", user.MXID.String()).Str("endpoint", "login").Logger()
if user.IsLoggedIn() {
jsonResponse(w, http.StatusOK, LoginResponse{Status: "success", ErrCode: "already logged in"})
return
}
var req ReqGoogleLoginStart
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
jsonResponse(w, http.StatusBadRequest, Error{
Error: "Failed to parse request JSON",
ErrCode: "bad json",
})
}
emoji, err := user.AsyncLoginGoogleStart(req.Cookies)
if err != nil {
log.Err(err).Msg("Failed to start login")
// TODO proper error codes
jsonResponse(w, http.StatusInternalServerError, Error{
Error: "Failed to start login",
ErrCode: "start login fail",
})
return
}
jsonResponse(w, http.StatusOK, &RespGoogleLoginStart{Status: "emoji", Emoji: emoji})
}
func (prov *ProvisioningAPI) GoogleLoginWait(w http.ResponseWriter, r *http.Request) {
userID := r.URL.Query().Get("user_id")
user := prov.bridge.GetUserByMXID(id.UserID(userID))
log := prov.zlog.With().Str("user_id", user.MXID.String()).Str("endpoint", "login").Logger()
if user.IsLoggedIn() {
jsonResponse(w, http.StatusOK, LoginResponse{Status: "success", ErrCode: "already logged in"})
return
}
err := user.AsyncLoginGoogleWait()
if err != nil {
log.Err(err).Msg("Failed to start login")
// TODO proper error codes
jsonResponse(w, http.StatusInternalServerError, Error{
Error: "Failed to finish login",
ErrCode: "finish login fail",
})
return
}
jsonResponse(w, http.StatusOK, LoginResponse{Status: "success"})
}
type LoginResponse struct {
Status string `json:"status"`
Code string `json:"code,omitempty"`

109
user.go
View file

@ -35,6 +35,7 @@ import (
"maunium.net/go/mautrix/appservice"
"maunium.net/go/mautrix/bridge"
"maunium.net/go/mautrix/bridge/bridgeconfig"
"maunium.net/go/mautrix/bridge/commands"
"maunium.net/go/mautrix/bridge/status"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/format"
@ -64,7 +65,8 @@ type User struct {
spaceCreateLock sync.Mutex
connLock sync.Mutex
BridgeState *bridge.BridgeStateQueue
BridgeState *bridge.BridgeStateQueue
CommandState *commands.CommandState
spaceMembershipChecked bool
@ -80,16 +82,27 @@ type User struct {
pollErrorAlertSent bool
phoneNotRespondingAlertSent bool
loginInProgress atomic.Bool
pairSuccessChan chan struct{}
ongoingLoginChan <-chan qrChannelItem
loginChanReadLock sync.Mutex
lastQRCode string
cancelLogin func()
loginInProgress atomic.Bool
pairSuccessChan chan struct{}
ongoingLoginChan <-chan qrChannelItem
lastQRCode string
cancelLogin func()
googleAsyncPairErrChan atomic.Pointer[chan error]
DoublePuppetIntent *appservice.IntentAPI
}
func (user *User) GetCommandState() *commands.CommandState {
return user.CommandState
}
func (user *User) SetCommandState(state *commands.CommandState) {
user.CommandState = state
}
var _ commands.CommandingUser = (*User)(nil)
func (br *GMBridge) getUserByMXID(userID id.UserID, onlyIfExists bool) *User {
_, isPuppet := br.ParsePuppetMXID(userID)
if isPuppet || userID == br.Bot.UserID {
@ -155,10 +168,6 @@ func (user *User) GetMXID() id.UserID {
return user.MXID
}
func (user *User) GetCommandState() map[string]interface{} {
return nil
}
func (br *GMBridge) GetUserByMXIDIfExists(userID id.UserID) *User {
return br.getUserByMXID(userID, true)
}
@ -456,6 +465,71 @@ func (user *User) Login(maxAttempts int) (<-chan qrChannelItem, error) {
return ch, nil
}
func (user *User) AsyncLoginGoogleStart(cookies map[string]string) (outEmoji string, outErr error) {
errChan := make(chan error, 1)
if !user.googleAsyncPairErrChan.CompareAndSwap(nil, &errChan) {
close(errChan)
outErr = fmt.Errorf("login already in progress")
return
}
var callbackDone bool
var initialWait sync.WaitGroup
initialWait.Add(1)
callback := func(emoji string) {
callbackDone = true
outEmoji = emoji
initialWait.Done()
}
go func() {
err := user.LoginGoogle(cookies, callback)
if !callbackDone {
initialWait.Done()
outErr = err
close(errChan)
user.googleAsyncPairErrChan.Store(nil)
} else {
errChan <- err
}
}()
initialWait.Wait()
return
}
func (user *User) AsyncLoginGoogleWait() error {
ch := user.googleAsyncPairErrChan.Swap(nil)
if ch == nil {
return fmt.Errorf("no login in progress")
}
return <-*ch
}
func (user *User) LoginGoogle(cookies map[string]string, emojiCallback func(string)) error {
user.connLock.Lock()
defer user.connLock.Unlock()
if user.Session != nil {
return ErrAlreadyLoggedIn
} else if !user.loginInProgress.CompareAndSwap(false, true) {
return ErrLoginInProgress
}
if user.Client != nil {
user.unlockedDeleteConnection()
}
pairSuccessChan := make(chan struct{})
user.pairSuccessChan = pairSuccessChan
authData := libgm.NewAuthData()
authData.Cookies = cookies
user.createClient(authData)
Analytics.Track(user.MXID, "$login_start")
err := user.Client.DoGaiaPairing(emojiCallback)
if err != nil {
user.unlockedDeleteConnection()
user.pairSuccessChan = nil
user.loginInProgress.Store(false)
return fmt.Errorf("failed to connect to Google Messages: %w", err)
}
return nil
}
func (user *User) Connect() bool {
user.connLock.Lock()
defer user.connLock.Unlock()
@ -637,6 +711,13 @@ func (user *User) syncHandleEvent(event any) {
Error: GMUnpaired,
}, false)
go user.sendMarkdownBridgeAlert(true, "Unpaired from Google Messages. Log in again to continue using the bridge.")
case *events.GaiaLoggedOut:
user.zlog.Info().Msg("Got gaia logout event")
go user.Logout(status.BridgeState{
StateEvent: status.StateBadCredentials,
Error: GMUnpaired,
}, false)
go user.sendMarkdownBridgeAlert(true, "Unpaired from Google Messages. Log in again to continue using the bridge.")
case *events.AuthTokenRefreshed:
go func() {
err := user.Update(context.TODO())
@ -731,9 +812,9 @@ func (user *User) handleAccountChange(v *events.AccountChange) {
user.switchedToGoogleLogin = v.GetEnabled() || v.IsFake
if !v.IsFake {
if user.switchedToGoogleLogin {
go user.sendMarkdownBridgeAlert(true, "The bridge will not work when the account-based pairing method is enabled in the Google Messages app. Unlink other devices and switch back to the QR code method to continue using the bridge.")
go user.sendMarkdownBridgeAlert(true, "Switched to Google account pairing, please switch back or relogin with `login-google`.")
} else {
go user.sendMarkdownBridgeAlert(false, "Switched back to QR pairing, bridge should work now")
go user.sendMarkdownBridgeAlert(false, "Switched back to QR pairing, bridge should be reconnected")
// Assume connection is ready now even if it wasn't before
user.ready = true
}
@ -874,7 +955,7 @@ func (user *User) FillBridgeState(state status.BridgeState) status.BridgeState {
func (user *User) Logout(state status.BridgeState, unpair bool) (logoutOK bool) {
if user.Client != nil && unpair {
_, err := user.Client.Unpair()
err := user.Client.Unpair()
if err != nil {
user.zlog.Debug().Err(err).Msg("Error sending unpair request")
} else {