diff --git a/pkg/connector/client.go b/pkg/connector/client.go index adc016a4..333482e3 100644 --- a/pkg/connector/client.go +++ b/pkg/connector/client.go @@ -36,15 +36,16 @@ type MetaClient struct { UserLogin *bridgev2.UserLogin Ghost *bridgev2.Ghost - stopHandlingTables atomic.Pointer[context.CancelFunc] - initialTable atomic.Pointer[table.LSTable] - initialTableHandled atomic.Bool - parsedTables chan *parsedTable - backfillCollectors map[int64]*BackfillCollector - backfillLock sync.Mutex - connectLock sync.Mutex - stopConnectAttempt atomic.Pointer[context.CancelFunc] - permanentErrored atomic.Bool + stopHandlingTables atomic.Pointer[context.CancelFunc] + initialTable atomic.Pointer[table.LSTable] + initialTableHandled atomic.Bool + parsedTables chan *parsedTable + backfillCollectors map[int64]*BackfillCollector + backfillLock sync.Mutex + connectLock sync.Mutex + pushRegistrationLock sync.Mutex + stopConnectAttempt atomic.Pointer[context.CancelFunc] + permanentErrored atomic.Bool editChannels *exsync.Map[string, chan *FBEditEvent] @@ -81,6 +82,12 @@ func (m *MetaConnector) getMessagixConfig() *messagix.Config { func (m *MetaConnector) LoadUserLogin(ctx context.Context, login *bridgev2.UserLogin) error { loginMetadata := login.Metadata.(*metaid.UserLoginMetadata) + if err := validateNativeSession(loginMetadata.NativeSession); err != nil { + return err + } + if login.Client != nil { + login.Client.Disconnect() + } c := &MetaClient{ Main: m, LoginMeta: loginMetadata, @@ -150,6 +157,7 @@ func (m *MetaClient) ensureMessagixClient() { m.Main.getMessagixConfig(), ) m.Client.SetEventHandler(m.handleMetaEvent) + m.Client.MessengerLite.SetNativeSession(m.LoginMeta.NativeSession) } } @@ -157,7 +165,14 @@ func (m *MetaClient) ExportCredentials(ctx context.Context) any { if m.Client == nil { return nil } - return m.Client.GetCookies() + if m.LoginMeta.NativeSession == nil { + return m.Client.GetCookies() + } + return &metaCredentials{ + Platform: m.LoginMeta.Platform, + Cookies: m.Client.GetCookies(), + NativeSession: m.LoginMeta.NativeSession, + } } func (m *MetaClient) Connect(ctx context.Context) { @@ -454,9 +469,11 @@ func (m *MetaClient) connectE2EE() error { if m.WADevice == nil { isNew = true m.WADevice = m.Main.DeviceStore.NewDevice() - } - if suggested := m.Client.MessengerLite.GetSuggestedDeviceID(); suggested != uuid.Nil { - m.WADevice.FacebookUUID = suggested + if session := m.LoginMeta.NativeSession; session != nil { + m.WADevice.FacebookUUID = session.DeviceID + } else if suggested := m.Client.MessengerLite.GetSuggestedDeviceID(); suggested != uuid.Nil { + m.WADevice.FacebookUUID = suggested + } } m.Client.SetDevice(m.WADevice) diff --git a/pkg/connector/handlewhatsapp.go b/pkg/connector/handlewhatsapp.go index 8bf72e36..d0f7e4b3 100644 --- a/pkg/connector/handlewhatsapp.go +++ b/pkg/connector/handlewhatsapp.go @@ -32,7 +32,9 @@ func (m *MetaClient) e2eeEventHandler(rawEvt any) bool { Any("ig_transport", evt.IGTransport). Any("payload", evt.Message). Msg("Received WhatsApp message") - m.Main.Bridge.QueueRemoteEvent(m.UserLogin, &EnsureWAChatStateEvent{JID: evt.Info.Chat, m: m}) + if !m.Main.Bridge.QueueRemoteEvent(m.UserLogin, &EnsureWAChatStateEvent{JID: evt.Info.Chat, m: m}).Success { + return false + } return m.Main.Bridge.QueueRemoteEvent(m.UserLogin, &WAMessageEvent{FBMessage: evt, m: m}).Success case *events.ChatPresence: m.handleWAChatPresence(m.Main.Bridge.BackgroundCtx, evt) diff --git a/pkg/connector/login.go b/pkg/connector/login.go index e10add31..a76d75a6 100644 --- a/pkg/connector/login.go +++ b/pkg/connector/login.go @@ -2,6 +2,7 @@ package connector import ( "context" + "encoding/json" "errors" "fmt" "maps" @@ -9,6 +10,7 @@ import ( "slices" "time" + "github.com/google/uuid" "github.com/rs/zerolog" "maunium.net/go/mautrix/bridgev2" "maunium.net/go/mautrix/bridgev2/database" @@ -73,24 +75,76 @@ func (m *MetaConnector) CreateLogin(ctx context.Context, user *bridgev2.User, fl }, nil } -// This creates a user login using credentials transferred from another instance of the meta bridge, -// via the `ExportCredentials` API. +type metaCredentials struct { + Platform types.Platform `json:"platform"` + Cookies *cookies.Cookies `json:"cookies"` + NativeSession *types.NativeSession `json:"native_session,omitempty"` +} + +func validateNativeSession(session *types.NativeSession) error { + if session != nil && (session.AccessToken == "" || session.AppID != useragent.MessengerLiteAndroidAppID || session.DeviceID == uuid.Nil || session.FamilyDeviceID == uuid.Nil) { + return fmt.Errorf("invalid native Messenger session") + } + return nil +} + func (m *MetaConnector) CreateUserLoginFromCredentials(ctx context.Context, user *bridgev2.User, credentials any) error { creds, ok := credentials.(map[string]any) if !ok { return fmt.Errorf("invalid credentials type: %T", credentials) } - cleanCreds := make(map[string]string, len(creds)) - for k, v := range creds { - cleanCreds[k] = v.(string) + var transferred metaCredentials + if _, structured := creds["cookies"]; structured { + data, err := json.Marshal(creds) + if err != nil { + return fmt.Errorf("failed to encode transferred credentials: %w", err) + } else if err = json.Unmarshal(data, &transferred); err != nil { + return fmt.Errorf("failed to decode transferred credentials: %w", err) + } else if transferred.Cookies == nil || !transferred.Platform.IsMessenger() { + return fmt.Errorf("invalid transferred Messenger credentials") + } else if err = validateNativeSession(transferred.NativeSession); err != nil { + return err + } + transferred.Cookies.Platform = transferred.Platform + } else { + transferred.Platform = types.Facebook + if m.Config.Tor { + transferred.Platform = types.FacebookTor + } + transferred.Cookies = &cookies.Cookies{Platform: transferred.Platform} + values := make(map[cookies.MetaCookieName]string, len(creds)) + for key, value := range creds { + str, ok := value.(string) + if !ok { + return fmt.Errorf("invalid cookie value type for %s", key) + } + values[cookies.MetaCookieName(key)] = str + } + transferred.Cookies.UpdateValues(values) + } + log := zerolog.Ctx(ctx).With().Str("component", "messagix").Logger() + if session := transferred.NativeSession; session != nil { + authClient, err := getMessagixClient(log, m, &cookies.Cookies{Platform: types.MessengerLiteAndroid}, m.Config.ProxyMessengerLite) + if err != nil { + return err + } + session.DeviceID = uuid.New() + authClient.MessengerLite.SetNativeSession(session) + newCookies, err := authClient.MessengerLite.ExchangeTransientToken(ctx, session.AccessToken) + if err != nil { + return fmt.Errorf("failed to create transferred native session: %w", err) + } else if newCookies.GetUserID() != transferred.Cookies.GetUserID() { + return fmt.Errorf("exchanged native session account does not match login") + } + newCookies.Platform = transferred.Platform + transferred.Cookies = newCookies + transferred.NativeSession = authClient.MessengerLite.GetNativeSession() } - - login, err := m.CreateLogin(ctx, user, FlowIDFacebookCookies) + client, err := getMessagixClient(log, m, transferred.Cookies, m.Config.ProxyOther) if err != nil { return err } - - step, err := login.(bridgev2.LoginProcessCookies).SubmitCookies(ctx, cleanCreds) + step, err := loginWithCookies(ctx, log, client, user, m, transferred.Cookies, transferred.NativeSession) if err != nil { return err } else if step.Type != bridgev2.LoginStepTypeComplete { @@ -198,7 +252,14 @@ func loginWithCookies( bridgeUser *bridgev2.User, conn *MetaConnector, c *cookies.Cookies, + nativeSession *types.NativeSession, ) (*bridgev2.LoginStep, error) { + if missing := c.GetMissingCookieNames(); len(missing) > 0 { + return nil, loginerrors.MissingCookies.AppendMessage(": %v", missing) + } else if err := validateNativeSession(nativeSession); err != nil { + return nil, err + } + client.MessengerLite.SetNativeSession(nativeSession) log.Debug(). Strs("cookie_names", exslices.CastToString[string](slices.Collect(maps.Keys(c.GetAll())))). @@ -221,11 +282,13 @@ func loginWithCookies( id := user.GetFBID() loginID := metaid.MakeUserLoginID(id) + if id != c.GetUserID() { + return nil, fmt.Errorf("logged-in account does not match cookies") + } var loginUA string if req, ok := ctx.Value("fi.mau.provision.request").(*http.Request); ok { loginUA = req.Header.Get("User-Agent") } - ul, err := bridgeUser.NewLogin(ctx, &database.UserLogin{ ID: loginID, RemoteName: user.GetName(), @@ -233,9 +296,10 @@ func loginWithCookies( Name: user.GetName(), }, Metadata: &metaid.UserLoginMetadata{ - Platform: c.Platform, - Cookies: c, - LoginUA: loginUA, + Platform: c.Platform, + Cookies: c, + LoginUA: loginUA, + NativeSession: nativeSession, }, }, nil) if err != nil { @@ -281,7 +345,7 @@ func (m *MetaCookieLogin) SubmitCookies(ctx context.Context, strCookies map[stri if err != nil { return nil, err } - return loginWithCookies(ctx, log, client, m.User, m.Main, c) + return loginWithCookies(ctx, log, client, m.User, m.Main, c, nil) } type MetaNativeLogin struct { @@ -366,7 +430,7 @@ func (m *MetaNativeLogin) proceed(ctx context.Context, userInput map[string]stri newClient.GetCookies().UpdateValues(newCookies.GetAll()) - step, err = loginWithCookies(ctx, log, newClient, m.User, m.Main, newCookies) + step, err = loginWithCookies(ctx, log, newClient, m.User, m.Main, newCookies, m.SavedClient.MessengerLite.GetNativeSession()) if err != nil { return nil, err } diff --git a/pkg/connector/push.go b/pkg/connector/push.go index 9f265ed3..56bda959 100644 --- a/pkg/connector/push.go +++ b/pkg/connector/push.go @@ -24,7 +24,6 @@ import ( "time" "github.com/rs/zerolog" - "go.mau.fi/whatsmeow" "maunium.net/go/mautrix/bridgev2" "maunium.net/go/mautrix/bridgev2/simplevent" @@ -33,6 +32,7 @@ import ( "go.mau.fi/mautrix-meta/pkg/messagix/pushcrypto" "go.mau.fi/mautrix-meta/pkg/messagix/table" "go.mau.fi/mautrix-meta/pkg/messagix/types" + "go.mau.fi/mautrix-meta/pkg/messagix/useragent" "go.mau.fi/mautrix-meta/pkg/metaid" ) @@ -45,17 +45,26 @@ var pushCfg = &bridgev2.PushConfig{ Web: &bridgev2.WebPushConfig{VapidKey: "BIBn3E_rWTci8Xn6P9Xj3btShT85Wdtne0LtwNUyRQ5XjFNkuTq9j4MPAVLvAFhXrUU1A9UxyxBA7YIOjqDIDHI"}, } -func (m *MetaClient) GetPushConfigs() *bridgev2.PushConfig { - return pushCfg +var nativePushCfg = &bridgev2.PushConfig{ + FCM: &bridgev2.FCMPushConfig{SenderID: "622912139302"}, } -type DoubleToken struct { - Unencrypted string `json:"unencrypted"` - Encrypted string `json:"encrypted"` +func (m *MetaClient) GetPushConfigs() *bridgev2.PushConfig { + if session := m.LoginMeta.NativeSession; session != nil && session.AccessToken != "" && session.AppID == useragent.MessengerLiteAndroidAppID { + return nativePushCfg + } + return pushCfg } func (m *MetaClient) RegisterPushNotifications(ctx context.Context, pushType bridgev2.PushType, token string) error { - if pushType != bridgev2.PushTypeWeb { + m.pushRegistrationLock.Lock() + defer m.pushRegistrationLock.Unlock() + if token == "" { + return errors.New("empty push token") + } + if pushType == bridgev2.PushTypeFCM { + return m.registerNativePush(ctx, token) + } else if pushType != bridgev2.PushTypeWeb { return fmt.Errorf("unsupported push type %s", pushType) } meta := m.UserLogin.Metadata.(*metaid.UserLoginMetadata) @@ -70,26 +79,6 @@ func (m *MetaClient) RegisterPushNotifications(ctx context.Context, pushType bri P256DH: meta.PushKeys.P256DH, Auth: meta.PushKeys.Auth, } - var encToken string - if token[0] == '{' && token[len(token)-1] == '}' { - var dt DoubleToken - err := json.Unmarshal([]byte(token), &dt) - if err != nil { - return fmt.Errorf("failed to unmarshal double token: %w", err) - } - token = dt.Unencrypted - encToken = dt.Encrypted - } - if encToken != "" { - err := m.E2EEClient.RegisterForPushNotifications(ctx, &whatsmeow.WebPushConfig{ - Endpoint: encToken, - Auth: meta.PushKeys.Auth, - P256DH: meta.PushKeys.P256DH, - }) - if err != nil { - return fmt.Errorf("failed to register e2ee notifications: %w", err) - } - } cli := m.Client if cli == nil { return messagix.ErrClientIsNil @@ -105,6 +94,43 @@ func (m *MetaClient) RegisterPushNotifications(ctx context.Context, pushType bri return err } +func (m *MetaClient) registerNativePush(ctx context.Context, token string) error { + if m.Client == nil || m.LoginMeta.NativeSession == nil { + return bridgev2.ErrNotLoggedIn + } + if err := m.e2eeConnectWaiter.WaitTimeoutCtx(ctx, ConnectWaitTimeout); err != nil { + return fmt.Errorf("waiting for encrypted push connection: %w", err) + } + client := m.Client + e2eeClient := m.E2EEClient + device := m.WADevice + if client == nil || e2eeClient == nil || !e2eeClient.IsLoggedIn() || device == nil { + return ErrNotConnected + } + session := m.LoginMeta.NativeSession + if device.FacebookUUID != session.DeviceID { + return errors.New("native push session does not match encrypted device") + } + if m.LoginMeta.NativePushKeys == nil { + keys, err := pushcrypto.NewNativePushKeys(metaid.ParseUserLoginID(m.UserLogin.ID)) + if err != nil { + return fmt.Errorf("failed to generate native push keys: %w", err) + } + m.LoginMeta.NativePushKeys = keys + if err = m.UserLogin.Save(ctx); err != nil { + m.LoginMeta.NativePushKeys = nil + return fmt.Errorf("failed to save native push keys: %w", err) + } + } + if err := client.Facebook.RegisterNativePushNotifications(ctx, session, token, m.LoginMeta.NativePushKeys); err != nil { + return err + } + if err := e2eeClient.RegisterForPushNotifications(ctx, &messagix.NativePushConfig{AppID: session.AppID, DeviceID: session.DeviceID}); err != nil { + return fmt.Errorf("failed to register encrypted native push: %w", err) + } + return nil +} + func (m *MetaClient) notifyBackgroundConnAboutEvent(isProcessing bool) { if ch := m.connectBackgroundEvt; ch != nil { select { @@ -118,17 +144,15 @@ type connectBackgroundEvent struct { isProcessing bool } -func (m *MetaClient) ensurePushMessageReceived(ctx context.Context, pd *pushcrypto.DecryptedPushData, parsed *methods.MetaMessageID) { +func (m *MetaClient) ensurePushMessageReceived(ctx context.Context, pd *pushcrypto.DecryptedPushData, parsed *methods.MetaMessageID) error { if pd == nil || parsed == nil { - return + return nil } log := zerolog.Ctx(ctx) msgID := parsed.String() part, err := m.Main.Bridge.DB.Message.GetFirstPartByID(ctx, m.UserLogin.ID, metaid.MakeFBMessageID(msgID)) if err != nil { - log.Err(err).Str("message_id", msgID). - Msg("Failed to look up push message in database") - return + return fmt.Errorf("failed to look up push message %s: %w", msgID, err) } else if part != nil { log.Debug(). Str("message_id", msgID). @@ -136,7 +160,7 @@ func (m *MetaClient) ensurePushMessageReceived(ctx context.Context, pd *pushcryp Str("f_param", pd.Params["f"]). Stringer("event_id", part.MXID). Msg("Confirmed push message was bridged") - return + return nil } threadType := table.ONE_TO_ONE chatID := parsed.ChatID @@ -160,10 +184,12 @@ func (m *MetaClient) ensurePushMessageReceived(ctx context.Context, pd *pushcryp LatestMessageTS: parsed.Time, }) log.Debug().Any("result", res).Msg("Event handling result for push backfill") + if !res.Success { + return fmt.Errorf("failed to queue push backfill: %w", res.Error) + } part, err = m.Main.Bridge.DB.Message.GetFirstPartByID(ctx, m.UserLogin.ID, metaid.MakeFBMessageID(msgID)) if err != nil { - log.Err(err).Str("message_id", msgID). - Msg("Failed to look up push message in database after backfill") + return fmt.Errorf("failed to look up push message %s after backfill: %w", msgID, err) } else if part != nil { log.Debug(). Str("message_id", msgID). @@ -175,12 +201,23 @@ func (m *MetaClient) ensurePushMessageReceived(ctx context.Context, pd *pushcryp Str("message_id", msgID). Msg("Push message still wasn't bridged after backfill") } + return nil } func (m *MetaClient) ConnectBackground(ctx context.Context, params *bridgev2.ConnectBackgroundParams) error { log := zerolog.Ctx(ctx) var parsedMsgID *methods.MetaMessageID - data, err := m.UserLogin.Metadata.(*metaid.UserLoginMetadata).PushKeys.Decrypt(ctx, params.RawData) + var envelope struct { + PIM string `json:"pim"` + Data struct { + PIM string `json:"pim"` + } `json:"data"` + } + var data *pushcrypto.DecryptedPushData + var err error + if json.Unmarshal(params.RawData, &envelope) != nil || (envelope.PIM == "" && envelope.Data.PIM == "") { + data, err = m.LoginMeta.PushKeys.Decrypt(ctx, params.RawData) + } if err != nil { log.Warn().Err(err).Msg("Failed to decrypt web push") } else if data != nil { @@ -231,8 +268,10 @@ func (m *MetaClient) ConnectBackground(ctx context.Context, params *bridgev2.Con Bool("wa_queue_empty", waDone). Int("wa_message_count", waCount). Msg("Closing background connection due to timeout") - m.ensurePushMessageReceived(ctx, data, parsedMsgID) - return nil + if parsedMsgID == nil && m.LoginMeta.Platform.IsMessenger() && !m.connectBackgroundWAOfflineSync.IsSet() { + return errors.New("encrypted offline sync did not complete") + } + return m.ensurePushMessageReceived(ctx, data, parsedMsgID) case <-ctx.Done(): log.Debug(). Bool("fb_tables_received", anythingReceived). @@ -240,7 +279,7 @@ func (m *MetaClient) ConnectBackground(ctx context.Context, params *bridgev2.Con Bool("wa_queue_empty", waDone). Int("wa_message_count", waCount). Msg("Closing background connection due to cancellation") - return nil + return ctx.Err() case <-waOfflineSyncChan: waOfflineSyncChan = nil waDone = true diff --git a/pkg/messagix/bloks/selenium_platform.go b/pkg/messagix/bloks/selenium_platform.go index cf0e2c76..3f7ddfdb 100644 --- a/pkg/messagix/bloks/selenium_platform.go +++ b/pkg/messagix/bloks/selenium_platform.go @@ -191,6 +191,9 @@ func (profile browserPlatformProfile) fallbackCodeInput( if profile.isInstagram && input == nil { return page.FindDescendant(FilterByComponent("bk.components.TextInput")) } + if input == nil { + return page.FindDescendant(FilterByAttribute("bk.components.TextInput", "type", "number")) + } return input } diff --git a/pkg/messagix/messengerlite.go b/pkg/messagix/messengerlite.go index 88488e2e..a2e8d797 100644 --- a/pkg/messagix/messengerlite.go +++ b/pkg/messagix/messengerlite.go @@ -30,6 +30,23 @@ type MessengerLiteMethods struct { deviceID uuid.UUID familyDeviceID uuid.UUID machineID string + nativeSession *types.NativeSession +} + +func (m *MessengerLiteMethods) GetNativeSession() *types.NativeSession { + if m == nil { + return nil + } + return m.nativeSession +} + +func (m *MessengerLiteMethods) SetNativeSession(session *types.NativeSession) { + if m == nil || session == nil { + return + } + m.nativeSession = session + m.deviceID = session.DeviceID + m.familyDeviceID = session.FamilyDeviceID } func (fb *MessengerLiteMethods) GetSuggestedDeviceID() uuid.UUID { @@ -270,7 +287,7 @@ func (m *MessengerLiteMethods) GetSessionForApp(ctx context.Context, accessToken params.Set("generate_session_cookies", "1") params.Set("generate_analytics_claim", "1") if m.deviceID != uuid.Nil { - params.Set("device_id", strings.ToUpper(m.deviceID.String())) + params.Set("device_id", m.deviceID.String()) } if m.machineID != "" { params.Set("machine_id", m.machineID) @@ -325,6 +342,20 @@ func (m *MessengerLiteMethods) ExchangeTransientToken(ctx context.Context, acces if !newCookies.IsLoggedIn() { return nil, fmt.Errorf("session-for-app response is missing cookies: %v", newCookies.GetMissingCookieNames()) } + if m.client.Platform == types.MessengerLiteAndroid { + uid, err := resp.UID.Int64() + if err != nil || uid <= 0 || uid != newCookies.GetUserID() { + return nil, fmt.Errorf("session-for-app response account does not match cookies") + } else if resp.AccessToken == "" { + return nil, fmt.Errorf("session-for-app response is missing an access token") + } + m.SetNativeSession(&types.NativeSession{ + AccessToken: resp.AccessToken, + AppID: useragent.MessengerLiteAndroidAppID, + DeviceID: m.deviceID, + FamilyDeviceID: m.familyDeviceID, + }) + } return newCookies, nil } @@ -381,7 +412,7 @@ func (m *MessengerLiteMethods) DoLoginSteps(ctx context.Context, userInput map[s } m.client.Logger.Debug(). Str("credential_type", loginRespPayload.CredentialType). - Msg("Login response didn't include session cookies, exchanging access token for a session") + Msg("Exchanging login access token for session cookies") newCookies, err := m.ExchangeTransientToken(ctx, loginRespPayload.AccessToken) if err != nil { m.client.Logger.Warn().Err(err). @@ -389,6 +420,9 @@ func (m *MessengerLiteMethods) DoLoginSteps(ctx context.Context, userInput map[s Msg("Failed to exchange access token for session cookies") return nil, nil, loginerrors.TokenExchange } + if loginRespPayload.UID != 0 && newCookies.GetUserID() != loginRespPayload.UID { + return nil, nil, loginerrors.TokenExchange + } m.client.Logger.Debug(). Str("credential_type", loginRespPayload.CredentialType). Any("cookie_names", slices.Collect(maps.Keys(newCookies.GetAll()))). @@ -396,7 +430,19 @@ func (m *MessengerLiteMethods) DoLoginSteps(ctx context.Context, userInput map[s return nil, newCookies, nil } - return nil, m.convertCookies(loginRespPayload.SessionCookies), nil + newCookies := m.convertCookies(loginRespPayload.SessionCookies) + if newCookies.GetUserID() <= 0 || (loginRespPayload.UID != 0 && loginRespPayload.UID != newCookies.GetUserID()) { + return nil, nil, loginerrors.TokenExchange + } + if m.client.Platform == types.MessengerLiteAndroid && loginRespPayload.AccessToken != "" { + m.SetNativeSession(&types.NativeSession{ + AccessToken: loginRespPayload.AccessToken, + AppID: useragent.MessengerLiteAndroidAppID, + DeviceID: m.deviceID, + FamilyDeviceID: m.familyDeviceID, + }) + } + return nil, newCookies, nil } func (m *MessengerLiteMethods) CancelLoginStep(ctx context.Context) error { diff --git a/pkg/messagix/nativepush.go b/pkg/messagix/nativepush.go new file mode 100644 index 00000000..cc9f36bd --- /dev/null +++ b/pkg/messagix/nativepush.go @@ -0,0 +1,116 @@ +package messagix + +import ( + "context" + "crypto/ecdh" + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "net/http" + "net/url" + "strconv" + "strings" + + "github.com/google/uuid" + waBinary "go.mau.fi/whatsmeow/binary" + + "go.mau.fi/mautrix-meta/pkg/messagix/pushcrypto" + "go.mau.fi/mautrix-meta/pkg/messagix/types" + "go.mau.fi/mautrix-meta/pkg/messagix/useragent" +) + +type NativePushConfig struct { + AppID string + DeviceID uuid.UUID +} + +func (cfg *NativePushConfig) GetPushConfigAttrs() waBinary.Attrs { + return waBinary.Attrs{ + "platform": "fb", + "appid": cfg.AppID, + "deviceid": cfg.DeviceID.String(), + } +} + +func (fb *FacebookMethods) RegisterNativePushNotifications(ctx context.Context, session *types.NativeSession, token string, keys *pushcrypto.NativePushKeys) error { + if session == nil || session.AccessToken == "" || session.AppID != useragent.MessengerLiteAndroidAppID || session.DeviceID == uuid.Nil || session.FamilyDeviceID == uuid.Nil { + return errors.New("missing native Messenger session") + } + userID := fb.client.GetCookies().GetUserID() + if userID <= 0 || token == "" { + return errors.New("missing native push account or token") + } + if keys == nil || len(keys.AESKey) != 32 || keys.AESKeyID>>8 != userID || keys.HPKEKeyID == "" { + return errors.New("invalid native push keys") + } + private, err := ecdh.P256().NewPrivateKey(keys.HPKEPrivateKey) + if err != nil { + return fmt.Errorf("invalid native push private key: %w", err) + } + deviceID := session.DeviceID.String() + androidID := sha256.Sum256([]byte(deviceID)) + protocol, err := json.Marshal(map[string]any{ + "url": "https://fcm.googleapis.com/fcm/send", + "token": token, + "device_id": deviceID, + "family_device_id": strings.ToUpper(session.FamilyDeviceID.String()), + "sub_type": 0, + "encryption": map[string]any{ + "key": base64.StdEncoding.EncodeToString(keys.AESKey) + "\n", + "key_id": keys.AESKeyID, + "algorithm": "AES_GCM", + }, + "hpke_params": map[string]string{ + "hpke_ciphersuite": "1001000010000", + "hpke_pubkey": base64.StdEncoding.EncodeToString(private.PublicKey().Bytes()), + "hpke_keystore_id": keys.HPKEKeyID, + }, + "typed_os_settings": map[string]int{"notificationEnabled": 1}, + "extra_data": map[string]int{ + "android_build": useragent.MessengerLiteAndroidBuild, + "android_setting_mask": 240, + "orca_muted_until_ms": 0, + "sys_notif": 1, + "messaging_channel_mask": 118, + }, + "android_id": hex.EncodeToString(androidID[:8]), + "request_id": uuid.NewString(), + "client_reported_user_ids": strconv.FormatInt(userID, 10), + }) + if err != nil { + return fmt.Errorf("failed to encode native push parameters: %w", err) + } + form := url.Values{ + "format": {"json"}, + "return_structure": {"1"}, + "protocol_params": {string(protocol)}, + "locale": {"en_US"}, + "client_country_code": {"US"}, + "fb_api_req_friendly_name": {"registerPush"}, + "fb_api_caller_class": {"FacebookPushServerRegisterJobImpl"}, + } + headers := http.Header{} + headers.Set("Authorization", "OAuth "+session.AccessToken) + headers.Set("User-Agent", useragent.MessengerLiteAndroidUserAgent) + headers.Set("X-FB-Connection-Quality", "EXCELLENT") + headers.Set("X-FB-Friendly-Name", "registerPush") + headers.Set("X-ZERO-STATE", "unknown") + headers.Set("X-FB-Family-Device-Id", session.FamilyDeviceID.String()) + _, response, err := fb.client.http.MakeRequest(ctx, "https://b-graph.facebook.com/me/register_push_tokens", http.MethodPost, headers, []byte(form.Encode()), types.FORM) + if err != nil { + return fmt.Errorf("failed to register native push: %w", err) + } + var result struct { + Success bool `json:"success"` + DisabledSource int `json:"disabled_source"` + } + if err = json.Unmarshal(response, &result); err != nil { + return fmt.Errorf("failed to decode native push response: %w", err) + } else if !result.Success || result.DisabledSource != 0 { + return fmt.Errorf("native push registration rejected (disabled source %d)", result.DisabledSource) + } + return nil +} diff --git a/pkg/messagix/pushcrypto/decrypt.go b/pkg/messagix/pushcrypto/decrypt.go index c40c9ea4..32706730 100644 --- a/pkg/messagix/pushcrypto/decrypt.go +++ b/pkg/messagix/pushcrypto/decrypt.go @@ -110,6 +110,9 @@ func (keys *PushKeys) Decrypt(ctx context.Context, push json.RawMessage) (*Decry if err != nil { return nil, fmt.Errorf("failed to open gcm: %w", err) } + if len(decrypted) < 2 { + return nil, errors.New("missing push padding length") + } padLen := int(binary.BigEndian.Uint16(decrypted[:2])) if 2+padLen > len(decrypted) { return nil, errors.New("invalid padding length") diff --git a/pkg/messagix/pushcrypto/native.go b/pkg/messagix/pushcrypto/native.go new file mode 100644 index 00000000..33008dd6 --- /dev/null +++ b/pkg/messagix/pushcrypto/native.go @@ -0,0 +1,30 @@ +package pushcrypto + +import ( + "crypto/ecdh" + "crypto/rand" + "strconv" + "time" + + "go.mau.fi/util/random" +) + +type NativePushKeys struct { + AESKey []byte `json:"aes_key"` + AESKeyID int64 `json:"aes_key_id"` + HPKEPrivateKey []byte `json:"hpke_private_key"` + HPKEKeyID string `json:"hpke_key_id"` +} + +func NewNativePushKeys(userID int64) (*NativePushKeys, error) { + private, err := ecdh.P256().GenerateKey(rand.Reader) + if err != nil { + return nil, err + } + return &NativePushKeys{ + AESKey: random.Bytes(32), + AESKeyID: userID<<8 | 1, + HPKEPrivateKey: private.Bytes(), + HPKEKeyID: strconv.FormatInt(time.Now().UnixMilli(), 10), + }, nil +} diff --git a/pkg/messagix/types/native_session.go b/pkg/messagix/types/native_session.go new file mode 100644 index 00000000..cba5dd33 --- /dev/null +++ b/pkg/messagix/types/native_session.go @@ -0,0 +1,10 @@ +package types + +import "github.com/google/uuid" + +type NativeSession struct { + AccessToken string `json:"access_token"` + AppID string `json:"app_id"` + DeviceID uuid.UUID `json:"device_id"` + FamilyDeviceID uuid.UUID `json:"family_device_id"` +} diff --git a/pkg/messagix/useragent/useragent.go b/pkg/messagix/useragent/useragent.go index 5a8f8fcd..d4c336c6 100644 --- a/pkg/messagix/useragent/useragent.go +++ b/pkg/messagix/useragent/useragent.go @@ -25,6 +25,7 @@ const MessengerLiteIOSUserAgent = "LightSpeed [FBAN/MessengerLiteForiOS;FBAV/571 const MessengerLiteAndroidAppID = "256002347743983" const MessengerLiteAndroidAccessToken = MessengerLiteAndroidAppID + "|374e60f8b9bb6b8cbb30f78030438895" const MessengerLiteAndroidUserAgent = "Dalvik/2.1.0 (Linux; U; Android 16; Pixel 7 Build/BP2A.250805.005) [FBAN/Orca-Android;FBAV/569.0.0.44.91;FBPN/com.facebook.orca;FBLC/en_US;FBBV/1012430703;FBCR/Tello;FBMF/Google;FBBD/google;FBDV/Pixel 7;FBSV/16;FBCA/arm64-v8a:null;FBDM/{density=2.625,width=1080,height=2400};FB_FW/1;]" +const MessengerLiteAndroidBuild = 345613452 // FIXME the user agent above and this build number don't match const IGDUserAgent = "WMI Web" diff --git a/pkg/metaid/dbmeta.go b/pkg/metaid/dbmeta.go index ace0958e..d90c35a5 100644 --- a/pkg/metaid/dbmeta.go +++ b/pkg/metaid/dbmeta.go @@ -40,12 +40,14 @@ type GhostMetadata struct { } type UserLoginMetadata struct { - Platform types.Platform `json:"platform"` - Cookies *cookies.Cookies `json:"cookies"` - WADeviceID uint16 `json:"wa_device_id,omitempty"` - PushKeys *pushcrypto.PushKeys `json:"push_keys,omitempty"` - LoginUA string `json:"login_ua,omitempty"` - IGID string `json:"igid,omitempty"` + Platform types.Platform `json:"platform"` + Cookies *cookies.Cookies `json:"cookies"` + WADeviceID uint16 `json:"wa_device_id,omitempty"` + PushKeys *pushcrypto.PushKeys `json:"push_keys,omitempty"` + LoginUA string `json:"login_ua,omitempty"` + IGID string `json:"igid,omitempty"` + NativeSession *types.NativeSession `json:"native_session,omitempty"` + NativePushKeys *pushcrypto.NativePushKeys `json:"native_push_keys,omitempty"` // Thread backfill state BackfillCompleted bool `json:"backfill_completed,omitempty"`