2023-06-30 11:05:33 +00:00
|
|
|
package libgm
|
2023-06-30 09:54:08 +00:00
|
|
|
|
|
|
|
import (
|
2023-07-16 10:33:51 +00:00
|
|
|
"crypto/ecdsa"
|
|
|
|
"crypto/rand"
|
|
|
|
"crypto/sha256"
|
2023-06-30 10:48:52 +00:00
|
|
|
"encoding/base64"
|
2023-07-09 11:16:52 +00:00
|
|
|
"fmt"
|
2023-06-30 09:54:08 +00:00
|
|
|
"io"
|
|
|
|
"net/http"
|
|
|
|
"net/url"
|
2023-07-16 11:36:13 +00:00
|
|
|
"strconv"
|
2023-06-30 09:54:08 +00:00
|
|
|
"time"
|
|
|
|
|
2023-07-16 10:23:44 +00:00
|
|
|
"github.com/google/uuid"
|
2023-06-30 09:54:08 +00:00
|
|
|
"github.com/rs/zerolog"
|
2023-07-15 17:43:28 +00:00
|
|
|
"google.golang.org/protobuf/proto"
|
2023-06-30 09:54:08 +00:00
|
|
|
|
|
|
|
"go.mau.fi/mautrix-gmessages/libgm/crypto"
|
2023-07-09 11:16:52 +00:00
|
|
|
"go.mau.fi/mautrix-gmessages/libgm/events"
|
2023-07-17 13:51:31 +00:00
|
|
|
"go.mau.fi/mautrix-gmessages/libgm/gmproto"
|
2023-07-09 11:16:52 +00:00
|
|
|
"go.mau.fi/mautrix-gmessages/libgm/pblite"
|
2023-06-30 09:54:08 +00:00
|
|
|
"go.mau.fi/mautrix-gmessages/libgm/util"
|
|
|
|
)
|
|
|
|
|
2023-07-09 11:16:52 +00:00
|
|
|
type AuthData struct {
|
2023-07-16 11:36:13 +00:00
|
|
|
// Keys used to encrypt communication with the phone
|
|
|
|
RequestCrypto *crypto.AESCTRHelper `json:"request_crypto,omitempty"`
|
|
|
|
// Key used to sign requests to refresh the tachyon auth token from the server
|
|
|
|
RefreshKey *crypto.JWK `json:"refresh_key,omitempty"`
|
|
|
|
// Identity of the paired phone and browser
|
2023-07-17 13:51:31 +00:00
|
|
|
Browser *gmproto.Device `json:"browser,omitempty"`
|
|
|
|
Mobile *gmproto.Device `json:"mobile,omitempty"`
|
2023-07-16 11:36:13 +00:00
|
|
|
// Key used to authenticate with the server
|
|
|
|
TachyonAuthToken []byte `json:"tachyon_token,omitempty"`
|
|
|
|
TachyonExpiry time.Time `json:"tachyon_expiry,omitempty"`
|
|
|
|
TachyonTTL int64 `json:"tachyon_ttl,omitempty"`
|
|
|
|
// Unknown encryption key, not used for anything
|
|
|
|
WebEncryptionKey []byte `json:"web_encryption_key,omitempty"`
|
2023-06-30 09:54:08 +00:00
|
|
|
}
|
2023-07-16 11:36:13 +00:00
|
|
|
|
|
|
|
const RefreshTachyonBuffer = 1 * time.Hour
|
|
|
|
|
2023-06-30 09:54:08 +00:00
|
|
|
type Proxy func(*http.Request) (*url.URL, error)
|
2023-07-15 22:45:19 +00:00
|
|
|
type EventHandler func(evt any)
|
2023-07-16 11:36:13 +00:00
|
|
|
|
2023-06-30 09:54:08 +00:00
|
|
|
type Client struct {
|
|
|
|
Logger zerolog.Logger
|
|
|
|
rpc *RPC
|
|
|
|
evHandler EventHandler
|
|
|
|
sessionHandler *SessionHandler
|
|
|
|
|
2023-07-18 21:59:51 +00:00
|
|
|
conversationsFetchedOnce bool
|
|
|
|
|
2023-07-16 12:55:30 +00:00
|
|
|
AuthData *AuthData
|
2023-06-30 09:54:08 +00:00
|
|
|
|
|
|
|
proxy Proxy
|
|
|
|
http *http.Client
|
|
|
|
}
|
|
|
|
|
2023-07-16 11:36:13 +00:00
|
|
|
func NewAuthData() *AuthData {
|
|
|
|
return &AuthData{
|
|
|
|
RequestCrypto: crypto.NewAESCTRHelper(),
|
|
|
|
RefreshKey: crypto.GenerateECDSAKey(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-09 11:16:52 +00:00
|
|
|
func NewClient(authData *AuthData, logger zerolog.Logger) *Client {
|
2023-06-30 09:54:08 +00:00
|
|
|
sessionHandler := &SessionHandler{
|
2023-07-17 23:01:06 +00:00
|
|
|
responseWaiters: make(map[string]chan<- *IncomingRPCMessage),
|
2023-06-30 09:54:08 +00:00
|
|
|
responseTimeout: time.Duration(5000) * time.Millisecond,
|
|
|
|
}
|
|
|
|
cli := &Client{
|
2023-07-16 12:55:30 +00:00
|
|
|
AuthData: authData,
|
2023-06-30 09:54:08 +00:00
|
|
|
Logger: logger,
|
2023-07-09 11:16:52 +00:00
|
|
|
sessionHandler: sessionHandler,
|
|
|
|
http: &http.Client{},
|
2023-06-30 09:54:08 +00:00
|
|
|
}
|
|
|
|
sessionHandler.client = cli
|
|
|
|
rpc := &RPC{client: cli, http: &http.Client{Transport: &http.Transport{Proxy: cli.proxy}}}
|
|
|
|
cli.rpc = rpc
|
2023-07-09 11:16:52 +00:00
|
|
|
cli.FetchConfigVersion()
|
2023-06-30 09:54:08 +00:00
|
|
|
return cli
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Client) SetEventHandler(eventHandler EventHandler) {
|
|
|
|
c.evHandler = eventHandler
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Client) SetProxy(proxy string) error {
|
|
|
|
proxyParsed, err := url.Parse(proxy)
|
|
|
|
if err != nil {
|
|
|
|
c.Logger.Fatal().Err(err).Msg("Failed to set proxy")
|
|
|
|
}
|
|
|
|
proxyUrl := http.ProxyURL(proxyParsed)
|
|
|
|
c.http.Transport = &http.Transport{
|
|
|
|
Proxy: proxyUrl,
|
|
|
|
}
|
|
|
|
c.proxy = proxyUrl
|
|
|
|
c.Logger.Debug().Any("proxy", proxyParsed.Host).Msg("SetProxy")
|
|
|
|
return nil
|
|
|
|
}
|
2023-07-16 12:55:30 +00:00
|
|
|
|
2023-07-09 11:16:52 +00:00
|
|
|
func (c *Client) Connect() error {
|
2023-07-16 12:55:30 +00:00
|
|
|
if c.AuthData.TachyonAuthToken == nil {
|
|
|
|
return fmt.Errorf("no auth token")
|
|
|
|
} else if c.AuthData.Browser == nil {
|
|
|
|
return fmt.Errorf("not logged in")
|
|
|
|
}
|
|
|
|
|
2023-07-16 13:18:45 +00:00
|
|
|
err := c.refreshAuthToken()
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("failed to refresh auth token: %w", err)
|
2023-07-16 12:55:30 +00:00
|
|
|
}
|
|
|
|
|
2023-07-16 13:18:45 +00:00
|
|
|
webEncryptionKeyResponse, err := c.GetWebEncryptionKey()
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("failed to get web encryption key: %w", err)
|
2023-07-16 12:55:30 +00:00
|
|
|
}
|
|
|
|
c.updateWebEncryptionKey(webEncryptionKeyResponse.GetKey())
|
2023-07-18 11:51:18 +00:00
|
|
|
go c.rpc.ListenReceiveMessages(true)
|
2023-07-16 12:55:30 +00:00
|
|
|
c.sessionHandler.startAckInterval()
|
|
|
|
|
|
|
|
bugleRes, bugleErr := c.IsBugleDefault()
|
|
|
|
if bugleErr != nil {
|
2023-07-16 13:18:45 +00:00
|
|
|
return fmt.Errorf("failed to check bugle default: %w", err)
|
2023-07-16 12:55:30 +00:00
|
|
|
}
|
2023-07-17 13:43:34 +00:00
|
|
|
c.Logger.Debug().Bool("bugle_default", bugleRes.Success).Msg("Got is bugle default response on connect")
|
2023-07-16 12:55:30 +00:00
|
|
|
sessionErr := c.SetActiveSession()
|
|
|
|
if sessionErr != nil {
|
2023-07-16 13:18:45 +00:00
|
|
|
return fmt.Errorf("failed to set active session: %w", err)
|
2023-07-16 12:55:30 +00:00
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Client) StartLogin() (string, error) {
|
|
|
|
registered, err := c.RegisterPhoneRelay()
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
c.AuthData.TachyonAuthToken = registered.AuthKeyData.TachyonAuthToken
|
2023-07-18 11:51:18 +00:00
|
|
|
go c.rpc.ListenReceiveMessages(false)
|
2023-07-16 12:55:30 +00:00
|
|
|
qr, err := c.GenerateQRCodeData(registered.GetPairingKey())
|
|
|
|
if err != nil {
|
|
|
|
return "", fmt.Errorf("failed to generate QR code: %w", err)
|
2023-06-30 09:54:08 +00:00
|
|
|
}
|
2023-07-16 12:55:30 +00:00
|
|
|
return qr, nil
|
2023-06-30 09:54:08 +00:00
|
|
|
}
|
|
|
|
|
2023-07-17 13:43:34 +00:00
|
|
|
func (c *Client) GenerateQRCodeData(pairingKey []byte) (string, error) {
|
2023-07-17 13:51:31 +00:00
|
|
|
urlData := &gmproto.URLData{
|
2023-07-17 13:43:34 +00:00
|
|
|
PairingKey: pairingKey,
|
|
|
|
AESKey: c.AuthData.RequestCrypto.AESKey,
|
|
|
|
HMACKey: c.AuthData.RequestCrypto.HMACKey,
|
|
|
|
}
|
|
|
|
encodedURLData, err := proto.Marshal(urlData)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
cData := base64.StdEncoding.EncodeToString(encodedURLData)
|
|
|
|
return util.QRCodeURLBase + cData, nil
|
|
|
|
}
|
|
|
|
|
2023-07-01 15:19:57 +00:00
|
|
|
func (c *Client) Disconnect() {
|
|
|
|
c.rpc.CloseConnection()
|
|
|
|
c.http.CloseIdleConnections()
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Client) IsConnected() bool {
|
|
|
|
return c.rpc != nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Client) IsLoggedIn() bool {
|
2023-07-16 12:55:30 +00:00
|
|
|
return c.AuthData != nil && c.AuthData.Browser != nil
|
2023-07-09 11:16:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Client) Reconnect() error {
|
|
|
|
c.rpc.CloseConnection()
|
|
|
|
for c.rpc.conn != nil {
|
|
|
|
time.Sleep(time.Millisecond * 100)
|
|
|
|
}
|
|
|
|
err := c.Connect()
|
|
|
|
if err != nil {
|
2023-07-17 13:43:34 +00:00
|
|
|
c.Logger.Err(err).Msg("Failed to reconnect")
|
2023-07-09 11:16:52 +00:00
|
|
|
return err
|
|
|
|
}
|
2023-07-17 13:43:34 +00:00
|
|
|
c.Logger.Debug().Msg("Successfully reconnected to server")
|
2023-07-09 11:16:52 +00:00
|
|
|
return nil
|
2023-07-01 15:19:57 +00:00
|
|
|
}
|
|
|
|
|
2023-06-30 09:54:08 +00:00
|
|
|
func (c *Client) triggerEvent(evt interface{}) {
|
|
|
|
if c.evHandler != nil {
|
|
|
|
c.evHandler(evt)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-09 11:16:52 +00:00
|
|
|
func (c *Client) FetchConfigVersion() {
|
2023-07-15 23:21:53 +00:00
|
|
|
req, bErr := http.NewRequest("GET", util.ConfigUrl, nil)
|
2023-07-09 11:16:52 +00:00
|
|
|
if bErr != nil {
|
2023-07-09 20:32:19 +00:00
|
|
|
panic(bErr)
|
2023-07-09 11:16:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
configRes, requestErr := c.http.Do(req)
|
|
|
|
if requestErr != nil {
|
2023-07-09 20:32:19 +00:00
|
|
|
panic(requestErr)
|
2023-07-09 11:16:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
responseBody, readErr := io.ReadAll(configRes.Body)
|
|
|
|
if readErr != nil {
|
2023-07-09 20:32:19 +00:00
|
|
|
panic(readErr)
|
2023-07-09 11:16:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
version, parseErr := util.ParseConfigVersion(responseBody)
|
|
|
|
if parseErr != nil {
|
2023-07-09 20:32:19 +00:00
|
|
|
panic(parseErr)
|
2023-07-09 11:16:52 +00:00
|
|
|
}
|
|
|
|
|
2023-07-16 12:55:30 +00:00
|
|
|
currVersion := util.ConfigMessage
|
2023-07-15 13:25:54 +00:00
|
|
|
if version.Year != currVersion.Year || version.Month != currVersion.Month || version.Day != currVersion.Day {
|
2023-07-09 11:16:52 +00:00
|
|
|
toLog := c.diffVersionFormat(currVersion, version)
|
|
|
|
c.Logger.Info().Any("version", toLog).Msg("There's a new version available!")
|
|
|
|
} else {
|
|
|
|
c.Logger.Info().Any("version", currVersion).Msg("You are running on the latest version.")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-07-17 13:51:31 +00:00
|
|
|
func (c *Client) diffVersionFormat(curr *gmproto.ConfigVersion, latest *gmproto.ConfigVersion) string {
|
2023-07-15 13:25:54 +00:00
|
|
|
return fmt.Sprintf("%d.%d.%d -> %d.%d.%d", curr.Year, curr.Month, curr.Day, latest.Year, latest.Month, latest.Day)
|
2023-07-09 11:16:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Client) updateWebEncryptionKey(key []byte) {
|
2023-07-17 13:43:34 +00:00
|
|
|
c.Logger.Debug().Msg("Updated WebEncryptionKey")
|
2023-07-16 12:55:30 +00:00
|
|
|
c.AuthData.WebEncryptionKey = key
|
2023-07-09 11:16:52 +00:00
|
|
|
}
|
|
|
|
|
2023-07-16 11:36:13 +00:00
|
|
|
func (c *Client) updateTachyonAuthToken(t []byte, validFor int64) {
|
2023-07-16 12:55:30 +00:00
|
|
|
c.AuthData.TachyonAuthToken = t
|
2023-07-16 11:36:13 +00:00
|
|
|
validForDuration := time.Duration(validFor) * time.Microsecond
|
|
|
|
if validForDuration == 0 {
|
|
|
|
validForDuration = 24 * time.Hour
|
|
|
|
}
|
2023-07-16 12:55:30 +00:00
|
|
|
c.AuthData.TachyonExpiry = time.Now().UTC().Add(time.Microsecond * time.Duration(validFor))
|
|
|
|
c.AuthData.TachyonTTL = validForDuration.Microseconds()
|
2023-07-17 13:43:34 +00:00
|
|
|
c.Logger.Debug().
|
|
|
|
Time("tachyon_expiry", c.AuthData.TachyonExpiry).
|
|
|
|
Int64("valid_for", validFor).
|
|
|
|
Msg("Updated tachyon token")
|
2023-07-09 11:16:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Client) refreshAuthToken() error {
|
2023-07-16 12:55:30 +00:00
|
|
|
if c.AuthData.Browser == nil || time.Until(c.AuthData.TachyonExpiry) > RefreshTachyonBuffer {
|
2023-07-16 11:36:13 +00:00
|
|
|
return nil
|
|
|
|
}
|
2023-07-17 13:43:34 +00:00
|
|
|
c.Logger.Debug().
|
|
|
|
Time("tachyon_expiry", c.AuthData.TachyonExpiry).
|
|
|
|
Msg("Refreshing auth token")
|
2023-07-16 12:55:30 +00:00
|
|
|
jwk := c.AuthData.RefreshKey
|
2023-07-16 10:23:44 +00:00
|
|
|
requestID := uuid.NewString()
|
2023-07-09 11:16:52 +00:00
|
|
|
timestamp := time.Now().UnixMilli() * 1000
|
|
|
|
|
2023-07-16 10:33:51 +00:00
|
|
|
signBytes := sha256.Sum256([]byte(fmt.Sprintf("%s:%d", requestID, timestamp)))
|
|
|
|
sig, err := ecdsa.SignASN1(rand.Reader, jwk.GetPrivateKey(), signBytes[:])
|
|
|
|
if err != nil {
|
|
|
|
return err
|
2023-07-09 11:16:52 +00:00
|
|
|
}
|
|
|
|
|
2023-07-17 23:57:20 +00:00
|
|
|
payload, err := pblite.Marshal(&gmproto.RegisterRefreshRequest{
|
2023-07-17 13:51:31 +00:00
|
|
|
MessageAuth: &gmproto.AuthMessage{
|
2023-07-16 11:36:13 +00:00
|
|
|
RequestID: requestID,
|
2023-07-16 12:55:30 +00:00
|
|
|
TachyonAuthToken: c.AuthData.TachyonAuthToken,
|
|
|
|
ConfigVersion: util.ConfigMessage,
|
2023-07-16 11:36:13 +00:00
|
|
|
},
|
2023-07-16 12:55:30 +00:00
|
|
|
CurrBrowserDevice: c.AuthData.Browser,
|
2023-07-16 11:36:13 +00:00
|
|
|
UnixTimestamp: timestamp,
|
|
|
|
Signature: sig,
|
2023-07-17 23:57:20 +00:00
|
|
|
EmptyRefreshArr: &gmproto.RegisterRefreshRequest_NestedEmptyArr{EmptyArr: &gmproto.EmptyArr{}},
|
2023-07-16 11:36:13 +00:00
|
|
|
MessageType: 2, // hmm
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return err
|
2023-07-09 11:16:52 +00:00
|
|
|
}
|
|
|
|
|
2023-07-16 11:36:13 +00:00
|
|
|
refreshResponse, requestErr := c.rpc.sendMessageRequest(util.RegisterRefreshURL, payload)
|
2023-07-09 11:16:52 +00:00
|
|
|
if requestErr != nil {
|
|
|
|
return requestErr
|
|
|
|
}
|
|
|
|
|
|
|
|
if refreshResponse.StatusCode == 401 {
|
|
|
|
return fmt.Errorf("failed to refresh auth token: unauthorized (try reauthenticating through qr code)")
|
|
|
|
}
|
|
|
|
|
|
|
|
if refreshResponse.StatusCode == 400 {
|
|
|
|
return fmt.Errorf("failed to refresh auth token: signature failed")
|
|
|
|
}
|
|
|
|
responseBody, readErr := io.ReadAll(refreshResponse.Body)
|
|
|
|
if readErr != nil {
|
|
|
|
return readErr
|
|
|
|
}
|
|
|
|
|
2023-07-17 13:51:31 +00:00
|
|
|
resp := &gmproto.RegisterRefreshResponse{}
|
2023-07-15 13:11:36 +00:00
|
|
|
deserializeErr := pblite.Unmarshal(responseBody, resp)
|
2023-07-09 11:16:52 +00:00
|
|
|
if deserializeErr != nil {
|
|
|
|
return deserializeErr
|
|
|
|
}
|
|
|
|
|
|
|
|
token := resp.GetTokenData().GetTachyonAuthToken()
|
|
|
|
if token == nil {
|
|
|
|
return fmt.Errorf("failed to refresh auth token: something happened")
|
|
|
|
}
|
|
|
|
|
2023-07-16 11:36:13 +00:00
|
|
|
validFor, _ := strconv.ParseInt(resp.GetTokenData().GetValidFor(), 10, 64)
|
|
|
|
|
|
|
|
c.updateTachyonAuthToken(token, validFor)
|
2023-07-16 12:55:30 +00:00
|
|
|
c.triggerEvent(&events.AuthTokenRefreshed{})
|
2023-07-09 11:16:52 +00:00
|
|
|
return nil
|
|
|
|
}
|