diff --git a/pkg/signalmeow/groups.go b/pkg/signalmeow/groups.go index 38cd2902..568ba266 100644 --- a/pkg/signalmeow/groups.go +++ b/pkg/signalmeow/groups.go @@ -176,6 +176,7 @@ type BannedMember struct { type GroupChange struct { groupMasterKey types.SerializedGroupMasterKey + SourceServiceId uuid.UUID Revision uint32 AddMembers []*AddMember DeleteMembers []*uuid.UUID @@ -315,6 +316,11 @@ func (groupChange *GroupChange) GetAvatarPath() *string { return groupChange.ModifyAvatar } +type GroupChangeState struct { + GroupState *Group + GroupChange *GroupChange +} + type GroupAuth struct { Username string Password string @@ -816,21 +822,26 @@ type GroupCache struct { func (cli *Client) DecryptGroupChange(ctx context.Context, groupContext *signalpb.GroupContextV2) (*GroupChange, error) { masterKeyBytes := libsignalgo.GroupMasterKey(groupContext.MasterKey) groupMasterKey := masterKeyFromBytes(masterKeyBytes) - log := zerolog.Ctx(ctx).With().Str("action", "decrypt group change").Logger() - - encryptedGroupChange := &signalpb.GroupChange{} groupChangeBytes := groupContext.GroupChange + encryptedGroupChange := &signalpb.GroupChange{} err := proto.Unmarshal(groupChangeBytes, encryptedGroupChange) if err != nil { return nil, fmt.Errorf("Error unmarshalling group change: %w", err) } + return cli.decryptGroupChange(ctx, encryptedGroupChange, groupMasterKey, true) +} +func (cli *Client) decryptGroupChange(ctx context.Context, encryptedGroupChange *signalpb.GroupChange, groupMasterKey types.SerializedGroupMasterKey, verifySignature bool) (*GroupChange, error) { + log := zerolog.Ctx(ctx).With().Str("action", "decrypt group change").Logger() serverSignature := encryptedGroupChange.ServerSignature encryptedActionsBytes := encryptedGroupChange.Actions - err = libsignalgo.ServerPublicParamsVerifySignature(prodServerPublicParams, encryptedActionsBytes, libsignalgo.NotarySignature(serverSignature)) - if err != nil { - return nil, fmt.Errorf("Failed to verify Server Signature: %w", err) + var err error + if verifySignature { + err = libsignalgo.ServerPublicParamsVerifySignature(prodServerPublicParams, encryptedActionsBytes, libsignalgo.NotarySignature(serverSignature)) + if err != nil { + return nil, fmt.Errorf("Failed to verify Server Signature: %w", err) + } } encryptedActions := signalpb.GroupChange_Actions{} @@ -846,9 +857,16 @@ func (cli *Client) DecryptGroupChange(ctx context.Context, groupContext *signalp return nil, err } + sourceServiceId, err := groupSecretParams.DecryptUUID(libsignalgo.UUIDCiphertext(encryptedActions.SourceServiceId)) + if err != nil { + log.Err(err).Msg("Couldn't decrypt source serviceID") + return nil, err + } + decryptedGroupChange := &GroupChange{ - groupMasterKey: groupMasterKey, - Revision: encryptedActions.Revision, + groupMasterKey: groupMasterKey, + Revision: encryptedActions.Revision, + SourceServiceId: sourceServiceId, } if encryptedActions.ModifyTitle != nil { @@ -1708,3 +1726,79 @@ func GenerateInviteLinkPassword() types.SerializedInviteLinkPassword { rand.Read(inviteLinkPasswordBytes) return InviteLinkPasswordFromBytes(inviteLinkPasswordBytes) } + +func (cli *Client) GetGroupHistoryPage(ctx context.Context, gid types.GroupIdentifier, fromRevision uint32, includeFirstState bool) ([]*GroupChangeState, error) { + log := zerolog.Ctx(ctx).With().Str("action", "GetGroupHistoryPage").Logger() + groupMasterKey, err := cli.Store.GroupStore.MasterKeyFromGroupIdentifier(ctx, gid) + if err != nil { + log.Err(err).Msg("Failed to get group master key") + return nil, err + } + if groupMasterKey == "" { + return nil, fmt.Errorf("No group master key found for group identifier %s", gid) + } + masterKeyBytes := masterKeyToBytes(groupMasterKey) + groupAuth, err := cli.GetAuthorizationForToday(ctx, masterKeyBytes) + if err != nil { + return nil, err + } + opts := &web.HTTPReqOpt{ + Username: &groupAuth.Username, + Password: &groupAuth.Password, + ContentType: web.ContentTypeProtobuf, + Host: web.StorageHostname, + } + // highest known epoch seems to always be 5, but that may change in the future. includeLastState is always false + path := fmt.Sprintf("/v1/groups/logs/%d?maxSupportedChangeEpoch=%d&includeFirstState=%t&includeLastState=false", fromRevision, 5, includeFirstState) + response, err := web.SendHTTPRequest(ctx, http.MethodGet, path, opts) + if err != nil { + return nil, err + } + if response.StatusCode != 200 { + return nil, fmt.Errorf("fetchGroupByID SendHTTPRequest bad status: %d", response.StatusCode) + } + var encryptedGroupChanges signalpb.GroupChanges + groupChangesBytes, err := io.ReadAll(response.Body) + if err != nil { + return nil, err + } + err = proto.Unmarshal(groupChangesBytes, &encryptedGroupChanges) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal group: %w", err) + } + + groupChanges, err := cli.decryptGroupChanges(ctx, &encryptedGroupChanges, groupMasterKey) + if err != nil { + return nil, fmt.Errorf("failed to decrypt group: %w", err) + } + return groupChanges, nil +} + +func (cli *Client) decryptGroupChanges(ctx context.Context, encryptedGroupChanges *signalpb.GroupChanges, groupMasterKey types.SerializedGroupMasterKey) ([]*GroupChangeState, error) { + log := zerolog.Ctx(ctx).With().Str("action", "decryptGroupChanges").Logger() + var groupChanges []*GroupChangeState + for _, groupChangeState := range encryptedGroupChanges.GroupChanges { + var group *Group + var err error + if groupChangeState.GroupState != nil { + group, err = decryptGroup(ctx, groupChangeState.GroupState, groupMasterKey) + if err != nil { + log.Err(err).Msg("Failed to decrypt Group") + return nil, err + } + } + var groupChange *GroupChange + if groupChangeState.GroupChange != nil { + groupChange, err = cli.decryptGroupChange(ctx, groupChangeState.GroupChange, groupMasterKey, false) + if err != nil { + log.Err(err).Msg("Failed to decrypt GroupChange") + return nil, err + } + } + groupChanges = append(groupChanges, &GroupChangeState{ + GroupState: group, + GroupChange: groupChange, + }) + } + return groupChanges, nil +} diff --git a/portal.go b/portal.go index b5be7f37..d2ba2f61 100644 --- a/portal.go +++ b/portal.go @@ -893,9 +893,14 @@ func (portal *Portal) handleSignalDataMessage(source *User, sender *Puppet, msg // Always update sender info when we receive a message from them, there's caching inside the function sender.UpdateInfo(genericCtx, source) // Handle earlier missed group changes here. - // If this message is a group change, don't handle it here, it's handled below. - if msg.GetGroupV2().GetGroupChange() == nil && portal.Revision < msg.GetGroupV2().GetRevision() { - portal.UpdateInfo(genericCtx, source, nil, msg.GetGroupV2().GetRevision()) + if msg.GetGroupV2() != nil { + requiredRevision := msg.GetGroupV2().GetRevision() + if msg.GetGroupV2().GetGroupChange() != nil { + requiredRevision = requiredRevision - 1 + } + if portal.Revision < requiredRevision { + portal.catchUpHistory(source, portal.Revision+1, requiredRevision, msg.GetTimestamp()) + } } else if portal.IsPrivateChat() && portal.UserID().UUID == portal.Receiver && portal.Name != NoteToSelfName { // Slightly hacky way to make note to self names backfill portal.UpdateDMInfo(genericCtx, false) @@ -921,6 +926,28 @@ func (portal *Portal) handleSignalDataMessage(source *User, sender *Puppet, msg } } +func (portal *Portal) catchUpHistory(source *User, fromRevision uint32, toRevision uint32, ts uint64) { + log := portal.log.With(). + Str("action", "catchUpHistory"). + Stringer("source", source.MXID). + Uint32("from revision", fromRevision). + Uint32("to revision", toRevision). + Logger() + ctx := log.WithContext(context.TODO()) + groupChanges, err := source.Client.GetGroupHistoryPage(ctx, portal.GroupID(), fromRevision, false) + if err != nil { + log.Err(err).Msg("Failed to get GroupChanges") + } + for _, groupChangeState := range groupChanges { + sender := portal.bridge.GetPuppetBySignalID(groupChangeState.GroupChange.SourceServiceId) + portal.applySignalGroupChange(ctx, source, sender, groupChangeState.GroupChange, ts) + // for revision > toRevision, we should have received a group change already + if groupChangeState.GroupChange.Revision == toRevision { + break + } + } +} + func (portal *Portal) handleSignalGroupChange(source *User, sender *Puppet, groupMeta *signalpb.GroupContextV2, ts uint64) { log := portal.log.With(). Str("action", "handle signal group change"). @@ -934,6 +961,11 @@ func (portal *Portal) handleSignalGroupChange(source *User, sender *Puppet, grou log.Err(err).Msg("Handling GroupChange failed") return } + portal.applySignalGroupChange(ctx, source, sender, groupChange, ts) +} + +func (portal *Portal) applySignalGroupChange(ctx context.Context, source *User, sender *Puppet, groupChange *signalmeow.GroupChange, ts uint64) { + log := zerolog.Ctx(ctx) if groupChange.Revision <= portal.Revision { return } @@ -952,6 +984,7 @@ func (portal *Portal) handleSignalGroupChange(source *User, sender *Puppet, grou } intent := sender.IntentFor(portal) modifyRoles := groupChange.ModifyMemberRoles + var err error for _, deleteBannedMember := range groupChange.DeleteBannedMembers { _, err := portal.sendMembershipForPuppetAndUser(ctx, sender, *deleteBannedMember, event.MembershipLeave, "unbanned") if err != nil {