mautrix-signal/pkg/signalmeow/groups.go

1889 lines
71 KiB
Go

// mautrix-signal - A Matrix-signal puppeting bridge.
// Copyright (C) 2023 Scott Weber, Malte Eggers
//
// 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 signalmeow
import (
"context"
"crypto/rand"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strings"
"time"
"unicode"
"github.com/google/uuid"
"github.com/rs/zerolog"
"google.golang.org/protobuf/proto"
"go.mau.fi/mautrix-signal/pkg/libsignalgo"
signalpb "go.mau.fi/mautrix-signal/pkg/signalmeow/protobuf"
"go.mau.fi/mautrix-signal/pkg/signalmeow/types"
"go.mau.fi/mautrix-signal/pkg/signalmeow/web"
)
type GroupMemberRole int32
type GroupAvatarMeta interface {
getGroupMasterKey() types.SerializedGroupMasterKey
GetAvatarPath() *string
}
const (
// Note: right now we assume these match the equivalent values in the protobuf (signalpb.Member_Role)
GroupMember_UNKNOWN GroupMemberRole = 0
GroupMember_DEFAULT GroupMemberRole = 1
GroupMember_ADMINISTRATOR GroupMemberRole = 2
)
type AccessControl int32
const (
AccessControl_UNKNOWN AccessControl = 0
AccessControl_ANY AccessControl = 1
AccessControl_MEMBER AccessControl = 2
AccessControl_ADMINISTRATOR AccessControl = 3
AccessControl_UNSATISFIABLE AccessControl = 4
)
type GroupMember struct {
ACI uuid.UUID
Role GroupMemberRole
ProfileKey libsignalgo.ProfileKey
JoinedAtRevision uint32
}
func (gm *GroupMember) UserServiceID() libsignalgo.ServiceID {
return libsignalgo.NewACIServiceID(gm.ACI)
}
type Group struct {
groupMasterKey types.SerializedGroupMasterKey // We should keep this relatively private
GroupIdentifier types.GroupIdentifier // This is what we should use to identify a group outside this file
Title string
AvatarPath string
Members []*GroupMember
Description string
AnnouncementsOnly bool
Revision uint32
DisappearingMessagesDuration uint32
AccessControl *GroupAccessControl
PendingMembers []*PendingMember
RequestingMembers []*RequestingMember
BannedMembers []*BannedMember
InviteLinkPassword *types.SerializedInviteLinkPassword
//PublicKey *libsignalgo.PublicKey
}
func (group *Group) GetInviteLink() (string, error) {
if group.InviteLinkPassword == nil {
return "", fmt.Errorf("no invite link password set")
}
masterKeyBytes := masterKeyToBytes(group.groupMasterKey)
inviteLinkPasswordBytes, err := inviteLinkPasswordToBytes(*group.InviteLinkPassword)
if err != nil {
return "", fmt.Errorf("couldn't decode invite link password")
}
inviteLinkContents := signalpb.GroupInviteLink_V1Contents{
V1Contents: &signalpb.GroupInviteLink_GroupInviteLinkContentsV1{
GroupMasterKey: masterKeyBytes[:],
InviteLinkPassword: inviteLinkPasswordBytes,
},
}
inviteLink := signalpb.GroupInviteLink{Contents: &inviteLinkContents}
inviteLinkEncoded, err := proto.Marshal(&inviteLink)
if err != nil {
return "", fmt.Errorf("failed to marshal invite link")
}
inviteLinkPath := base64.URLEncoding.EncodeToString(inviteLinkEncoded)
return "https://signal.group/#" + inviteLinkPath, nil
}
type GroupAccessControl struct {
Members AccessControl
AddFromInviteLink AccessControl
Attributes AccessControl
}
func (group *Group) getGroupMasterKey() types.SerializedGroupMasterKey {
return group.groupMasterKey
}
func (group *Group) GetAvatarPath() *string {
return &group.AvatarPath
}
type AddMember struct {
GroupMember
JoinFromInviteLink bool
}
type PendingMember struct {
ServiceID libsignalgo.ServiceID
Role GroupMemberRole
AddedByUserID uuid.UUID
Timestamp uint64
}
type ProfileKeyMember struct {
ACI uuid.UUID
ProfileKey libsignalgo.ProfileKey
}
type RequestingMember struct {
ACI uuid.UUID
ProfileKey libsignalgo.ProfileKey
Timestamp uint64
}
type PromotePendingMember struct {
ACI uuid.UUID
ProfileKey libsignalgo.ProfileKey
}
type PromotePendingPniAciMember struct {
ACI uuid.UUID
ProfileKey libsignalgo.ProfileKey
PNI uuid.UUID
}
type RoleMember struct {
ACI uuid.UUID
Role GroupMemberRole
}
type BannedMember struct {
ServiceID libsignalgo.ServiceID
Timestamp uint64
}
type GroupChange struct {
groupMasterKey types.SerializedGroupMasterKey
SourceServiceID libsignalgo.ServiceID
Revision uint32
AddMembers []*AddMember
DeleteMembers []*uuid.UUID
ModifyMemberRoles []*RoleMember
ModifyMemberProfileKeys []*ProfileKeyMember
AddPendingMembers []*PendingMember
DeletePendingMembers []*libsignalgo.ServiceID
PromotePendingMembers []*PromotePendingMember
ModifyTitle *string
ModifyAvatar *string
ModifyDisappearingMessagesDuration *uint32
ModifyAttributesAccess *AccessControl
ModifyMemberAccess *AccessControl
ModifyAddFromInviteLinkAccess *AccessControl
AddRequestingMembers []*RequestingMember
DeleteRequestingMembers []*uuid.UUID
PromoteRequestingMembers []*RoleMember
ModifyDescription *string
ModifyAnnouncementsOnly *bool
AddBannedMembers []*BannedMember
DeleteBannedMembers []*libsignalgo.ServiceID
PromotePendingPniAciMembers []*PromotePendingPniAciMember
ModifyInviteLinkPassword *types.SerializedInviteLinkPassword
}
func (groupChange *GroupChange) isEmptpy() bool {
return len(groupChange.AddMembers) == 0 &&
len(groupChange.DeleteMembers) == 0 &&
len(groupChange.ModifyMemberRoles) == 0 &&
len(groupChange.ModifyMemberProfileKeys) == 0 &&
len(groupChange.AddPendingMembers) == 0 &&
len(groupChange.PromotePendingMembers) == 0 &&
groupChange.ModifyTitle == nil &&
groupChange.ModifyAvatar == nil &&
groupChange.ModifyDisappearingMessagesDuration == nil &&
groupChange.ModifyAttributesAccess == nil &&
groupChange.ModifyMemberAccess == nil &&
groupChange.ModifyAddFromInviteLinkAccess == nil &&
len(groupChange.AddRequestingMembers) == 0 &&
len(groupChange.DeleteRequestingMembers) == 0 &&
len(groupChange.PromoteRequestingMembers) == 0 &&
groupChange.ModifyDescription == nil &&
groupChange.ModifyAnnouncementsOnly == nil &&
len(groupChange.AddBannedMembers) == 0 &&
len(groupChange.DeleteMembers) == 0
}
func (groupChange *GroupChange) resolveConflict(group *Group) {
if *groupChange.ModifyTitle == group.Title {
groupChange.ModifyTitle = nil
}
if *groupChange.ModifyDescription == group.Description {
groupChange.ModifyDescription = nil
}
if *groupChange.ModifyAvatar == group.AvatarPath {
groupChange.ModifyAvatar = nil
}
if *groupChange.ModifyDisappearingMessagesDuration == group.DisappearingMessagesDuration {
groupChange.ModifyDisappearingMessagesDuration = nil
}
if *groupChange.ModifyAttributesAccess == group.AccessControl.Attributes {
groupChange.ModifyAttributesAccess = nil
}
if *groupChange.ModifyMemberAccess == group.AccessControl.Members {
groupChange.ModifyAttributesAccess = nil
}
if *groupChange.ModifyAddFromInviteLinkAccess == group.AccessControl.AddFromInviteLink {
groupChange.ModifyAddFromInviteLinkAccess = nil
}
if *groupChange.ModifyAnnouncementsOnly == group.AnnouncementsOnly {
groupChange.ModifyAnnouncementsOnly = nil
}
members := make(map[uuid.UUID]GroupMemberRole)
for _, member := range group.Members {
members[member.ACI] = member.Role
}
pendingMembers := make(map[libsignalgo.ServiceID]bool)
for _, pendingMember := range group.PendingMembers {
pendingMembers[pendingMember.ServiceID] = true
}
requestingMembers := make(map[uuid.UUID]bool)
for _, requestingMember := range group.RequestingMembers {
requestingMembers[requestingMember.ACI] = true
}
for i, member := range groupChange.AddMembers {
if _, ok := members[member.GroupMember.ACI]; ok {
groupChange.AddMembers = append(groupChange.AddMembers[:i], groupChange.AddMembers[i+1:]...)
}
}
for i, promotePendingMember := range groupChange.PromotePendingMembers {
if _, ok := members[promotePendingMember.ACI]; ok {
groupChange.PromotePendingMembers = append(groupChange.PromotePendingMembers[:i], groupChange.PromotePendingMembers[i+1:]...)
}
}
for i, promoteRequestingMember := range groupChange.PromotePendingMembers {
if _, ok := members[promoteRequestingMember.ACI]; ok {
groupChange.PromoteRequestingMembers = append(groupChange.PromoteRequestingMembers[:i], groupChange.PromoteRequestingMembers[i+1:]...)
}
}
for i, pendingMember := range groupChange.AddPendingMembers {
if pendingMembers[pendingMember.ServiceID] {
groupChange.AddPendingMembers = append(groupChange.AddPendingMembers[:i], groupChange.AddPendingMembers[i+1:]...)
}
}
for i, requestingMember := range groupChange.AddRequestingMembers {
if requestingMembers[requestingMember.ACI] {
groupChange.AddRequestingMembers = append(groupChange.AddRequestingMembers[:i], groupChange.AddRequestingMembers[i+1:]...)
}
}
for i, deletePendingMember := range groupChange.DeletePendingMembers {
if !pendingMembers[*deletePendingMember] {
groupChange.DeletePendingMembers = append(groupChange.DeletePendingMembers[:i], groupChange.DeletePendingMembers[i+1:]...)
}
}
for i, deleteRequestingMember := range groupChange.DeleteRequestingMembers {
if !requestingMembers[*deleteRequestingMember] {
groupChange.DeleteRequestingMembers = append(groupChange.DeleteRequestingMembers[:i], groupChange.DeleteRequestingMembers[i+1:]...)
}
}
for i, deleteMember := range groupChange.DeleteMembers {
if _, ok := members[*deleteMember]; !ok {
groupChange.DeleteMembers = append(groupChange.DeleteMembers[:i], groupChange.DeleteMembers[i+1:]...)
}
}
for i, modifyMemberRole := range groupChange.ModifyMemberRoles {
if members[modifyMemberRole.ACI] == modifyMemberRole.Role {
groupChange.ModifyMemberRoles = append(groupChange.ModifyMemberRoles[:i], groupChange.ModifyMemberRoles[i+1:]...)
}
}
}
func (groupChange *GroupChange) getGroupMasterKey() types.SerializedGroupMasterKey {
return groupChange.groupMasterKey
}
func (groupChange *GroupChange) GetAvatarPath() *string {
return groupChange.ModifyAvatar
}
type GroupChangeState struct {
GroupState *Group
GroupChange *GroupChange
}
type GroupAuth struct {
Username string
Password string
}
func (cli *Client) fetchNewGroupCreds(ctx context.Context, today time.Time) (*GroupCredentials, error) {
log := zerolog.Ctx(ctx).With().
Str("action", "fetch new group creds").
Logger()
sevenDaysOut := today.Add(7 * 24 * time.Hour)
path := fmt.Sprintf("/v1/certificate/auth/group?redemptionStartSeconds=%d&redemptionEndSeconds=%d&pniAsServiceId=true", today.Unix(), sevenDaysOut.Unix())
authRequest := web.CreateWSRequest(http.MethodGet, path, nil, nil, nil)
resp, err := cli.AuthedWS.SendRequest(ctx, authRequest)
if err != nil {
return nil, fmt.Errorf("SendRequest error: %w", err)
}
if *resp.Status != 200 {
return nil, fmt.Errorf("bad status code fetching group creds: %d", *resp.Status)
}
var creds GroupCredentials
err = json.Unmarshal(resp.Body, &creds)
if err != nil {
log.Err(err).Msg("json.Unmarshal error")
return nil, err
}
// make sure pni matches device pni
if creds.PNI != cli.Store.PNI {
err := fmt.Errorf("creds.PNI != d.PNI")
log.Err(err).Msg("creds.PNI != d.PNI")
return nil, err
}
return &creds, nil
}
func (cli *Client) getCachedAuthorizationForToday(today time.Time) *GroupCredential {
if cli.GroupCredentials == nil {
// No cached credentials
return nil
}
allCreds := cli.GroupCredentials
// Get the credential for today
for _, cred := range allCreds.Credentials {
if cred.RedemptionTime == today.Unix() {
return &cred
}
}
return nil
}
func (cli *Client) GetAuthorizationForToday(ctx context.Context, masterKey libsignalgo.GroupMasterKey) (*GroupAuth, error) {
log := zerolog.Ctx(ctx).With().
Str("action", "get authorization for today").
Logger()
// Timestamps for the start of today, and 7 days later
today := time.Now().Truncate(24 * time.Hour)
todayCred := cli.getCachedAuthorizationForToday(today)
if todayCred == nil {
creds, err := cli.fetchNewGroupCreds(ctx, today)
if err != nil {
return nil, fmt.Errorf("fetchNewGroupCreds error: %w", err)
}
cli.GroupCredentials = creds
todayCred = cli.getCachedAuthorizationForToday(today)
}
if todayCred == nil {
return nil, fmt.Errorf("couldn't get credential for today")
}
//TODO: cache cred after unmarshalling
redemptionTime := uint64(todayCred.RedemptionTime)
credential := todayCred.Credential
authCredentialResponse, err := libsignalgo.NewAuthCredentialWithPniResponse(credential)
if err != nil {
log.Err(err).Msg("NewAuthCredentialWithPniResponse error")
return nil, err
}
// Receive the auth credential
authCredential, err := libsignalgo.ReceiveAuthCredentialWithPni(
prodServerPublicParams,
cli.Store.ACI,
cli.Store.PNI,
redemptionTime,
*authCredentialResponse,
)
if err != nil {
log.Err(err).Msg("ReceiveAuthCredentialWithPni error")
return nil, err
}
// get auth presentation
groupSecretParams, err := libsignalgo.DeriveGroupSecretParamsFromMasterKey(masterKey)
if err != nil {
log.Err(err).Msg("DeriveGroupSecretParamsFromMasterKey error")
return nil, err
}
authCredentialPresentation, err := libsignalgo.CreateAuthCredentialWithPniPresentation(
prodServerPublicParams,
libsignalgo.GenerateRandomness(),
groupSecretParams,
*authCredential,
)
if err != nil {
log.Err(err).Msg("CreateAuthCredentialWithPniPresentation error")
return nil, err
}
groupPublicParams, err := groupSecretParams.GetPublicParams()
if err != nil {
log.Err(err).Msg("GetPublicParams error")
return nil, err
}
return &GroupAuth{
Username: hex.EncodeToString(groupPublicParams[:]),
Password: hex.EncodeToString(*authCredentialPresentation),
}, nil
}
func masterKeyToBytes(groupMasterKey types.SerializedGroupMasterKey) libsignalgo.GroupMasterKey {
// We are very tricksy, groupMasterKey is just base64 encoded group master key :O
masterKeyBytes, err := base64.StdEncoding.DecodeString(string(groupMasterKey))
if err != nil {
panic(fmt.Errorf("we should always be able to decode groupMasterKey into masterKeyBytes: %w", err))
}
return libsignalgo.GroupMasterKey(masterKeyBytes)
}
func masterKeyFromBytes(masterKey libsignalgo.GroupMasterKey) types.SerializedGroupMasterKey {
return types.SerializedGroupMasterKey(base64.StdEncoding.EncodeToString(masterKey[:]))
}
func inviteLinkPasswordToBytes(inviteLinkPassword types.SerializedInviteLinkPassword) ([]byte, error) {
inviteLinkPasswordBytes, err := base64.StdEncoding.DecodeString((string(inviteLinkPassword)))
if err != nil {
return nil, err
}
return inviteLinkPasswordBytes, nil
}
func InviteLinkPasswordFromBytes(inviteLinkPassword []byte) types.SerializedInviteLinkPassword {
return types.SerializedInviteLinkPassword(base64.StdEncoding.EncodeToString(inviteLinkPassword))
}
func groupIdentifierFromMasterKey(masterKey types.SerializedGroupMasterKey) (types.GroupIdentifier, error) {
groupSecretParams, err := libsignalgo.DeriveGroupSecretParamsFromMasterKey(masterKeyToBytes(masterKey))
if err != nil {
return "", fmt.Errorf("DeriveGroupSecretParamsFromMasterKey error: %w", err)
}
// Get the "group identifier" that isn't just the master key
groupPublicParams, err := groupSecretParams.GetPublicParams()
if err != nil {
return "", fmt.Errorf("GetPublicParams error: %w", err)
}
groupIdentifier, err := libsignalgo.GetGroupIdentifier(*groupPublicParams)
if err != nil {
return "", fmt.Errorf("GetGroupIdentifier error: %w", err)
}
base64GroupIdentifier := base64.StdEncoding.EncodeToString(groupIdentifier[:])
gid := types.GroupIdentifier(base64GroupIdentifier)
return gid, nil
}
func decryptGroup(ctx context.Context, encryptedGroup *signalpb.Group, groupMasterKey types.SerializedGroupMasterKey) (*Group, error) {
log := zerolog.Ctx(ctx).With().Str("action", "decrypt group").Logger()
decryptedGroup := &Group{
groupMasterKey: groupMasterKey,
}
groupSecretParams, err := libsignalgo.DeriveGroupSecretParamsFromMasterKey(masterKeyToBytes(groupMasterKey))
if err != nil {
log.Err(err).Msg("DeriveGroupSecretParamsFromMasterKey error")
return nil, err
}
gid, err := groupIdentifierFromMasterKey(groupMasterKey)
if err != nil {
log.Err(err).Msg("groupIdentifierFromMasterKey error")
return nil, err
}
decryptedGroup.GroupIdentifier = gid
titleBlob, err := decryptGroupPropertyIntoBlob(groupSecretParams, encryptedGroup.Title)
if err != nil {
return nil, err
}
// The actual title is in the blob
decryptedGroup.Title = cleanupStringProperty(titleBlob.GetTitle())
descriptionBlob, err := decryptGroupPropertyIntoBlob(groupSecretParams, encryptedGroup.Description)
if err == nil {
// treat a failure in obtaining the description as non-fatal
decryptedGroup.Description = cleanupStringProperty(descriptionBlob.GetDescription())
}
if encryptedGroup.DisappearingMessagesTimer != nil && len(encryptedGroup.DisappearingMessagesTimer) > 0 {
timerBlob, err := decryptGroupPropertyIntoBlob(groupSecretParams, encryptedGroup.DisappearingMessagesTimer)
if err != nil {
return nil, err
}
decryptedGroup.DisappearingMessagesDuration = timerBlob.GetDisappearingMessagesDuration()
}
// These aren't encrypted
decryptedGroup.AvatarPath = encryptedGroup.Avatar
decryptedGroup.Revision = encryptedGroup.Revision
// Decrypt members
for _, member := range encryptedGroup.Members {
if member == nil {
continue
}
decryptedMember, err := decryptMember(ctx, member, groupSecretParams)
if err != nil {
return nil, err
}
decryptedGroup.Members = append(decryptedGroup.Members, decryptedMember)
}
for _, pendingMember := range encryptedGroup.PendingMembers {
if pendingMember == nil {
continue
}
decryptedPendingMember, err := decryptPendingMember(ctx, pendingMember, groupSecretParams)
if err != nil {
continue
// decryptPendingMember returns an error if the userID is a PNI, keep decrypting
}
decryptedGroup.PendingMembers = append(decryptedGroup.PendingMembers, decryptedPendingMember)
}
for _, requestingMember := range encryptedGroup.RequestingMembers {
if requestingMember == nil {
continue
}
decryptedRequestingMember, err := decryptRequestingMember(ctx, requestingMember, groupSecretParams)
if err != nil {
return nil, err
}
decryptedGroup.RequestingMembers = append(decryptedGroup.RequestingMembers, decryptedRequestingMember)
}
for _, bannedMember := range encryptedGroup.BannedMembers {
if bannedMember == nil {
continue
}
encryptedUserID := libsignalgo.UUIDCiphertext(bannedMember.UserId)
serviceID, err := groupSecretParams.DecryptServiceID(encryptedUserID)
if err != nil {
log.Err(err).Msg("DecryptUUID UserId error")
return nil, err
}
decryptedGroup.BannedMembers = append(decryptedGroup.BannedMembers, &BannedMember{
ServiceID: serviceID,
Timestamp: bannedMember.Timestamp,
})
}
if encryptedGroup.AccessControl != nil {
decryptedGroup.AccessControl = &GroupAccessControl{
Members: (AccessControl)(encryptedGroup.AccessControl.Members),
Attributes: (AccessControl)(encryptedGroup.AccessControl.Attributes),
AddFromInviteLink: (AccessControl)(encryptedGroup.AccessControl.AddFromInviteLink),
}
}
if len(encryptedGroup.InviteLinkPassword) > 0 {
inviteLinkPassword := InviteLinkPasswordFromBytes(encryptedGroup.InviteLinkPassword)
decryptedGroup.InviteLinkPassword = &inviteLinkPassword
}
return decryptedGroup, nil
}
func decryptGroupPropertyIntoBlob(groupSecretParams libsignalgo.GroupSecretParams, encryptedProperty []byte) (*signalpb.GroupAttributeBlob, error) {
decryptedProperty, err := groupSecretParams.DecryptBlobWithPadding(encryptedProperty)
if err != nil {
return nil, fmt.Errorf("error decrypting blob with padding: %w", err)
}
var propertyBlob signalpb.GroupAttributeBlob
err = proto.Unmarshal(decryptedProperty, &propertyBlob)
if err != nil {
return nil, fmt.Errorf("error unmarshalling blob: %w", err)
}
return &propertyBlob, nil
}
func encryptBlobIntoGroupProperty(groupSecretParams libsignalgo.GroupSecretParams, attributeBlob *signalpb.GroupAttributeBlob) (*[]byte, error) {
decryptedProperty, err := proto.Marshal(attributeBlob)
if err != nil {
return nil, fmt.Errorf("error marshalling groupProperty: %w", err)
}
encryptedProperty, err := groupSecretParams.EncryptBlobWithPaddingDeterministic(libsignalgo.GenerateRandomness(), decryptedProperty, 0)
if err != nil {
return nil, fmt.Errorf("error encrypting blob with padding: %w", err)
}
return &encryptedProperty, nil
}
func cleanupStringProperty(property string) string {
// strip non-printable characters from the string
property = strings.Map(cleanupStringMapping, property)
// strip \n and \t from start and end of the property if it exists
return strings.TrimSpace(property)
}
func cleanupStringMapping(r rune) rune {
if unicode.IsGraphic(r) {
return r
}
return -1
}
func decryptGroupAvatar(encryptedAvatar []byte, groupMasterKey types.SerializedGroupMasterKey) ([]byte, error) {
groupSecretParams, err := libsignalgo.DeriveGroupSecretParamsFromMasterKey(masterKeyToBytes(groupMasterKey))
if err != nil {
return nil, fmt.Errorf("error deriving group secret params from master key: %w", err)
}
avatarBlob, err := decryptGroupPropertyIntoBlob(groupSecretParams, encryptedAvatar)
if err != nil {
return nil, err
}
// The actual avatar is in the blob
decryptedImage := avatarBlob.GetAvatar()
return decryptedImage, nil
}
func groupMetadataForDataMessage(group Group) *signalpb.GroupContextV2 {
masterKey := masterKeyToBytes(group.groupMasterKey)
masterKeyBytes := masterKey[:]
return &signalpb.GroupContextV2{
MasterKey: masterKeyBytes,
Revision: &group.Revision,
}
}
func (cli *Client) fetchGroupByID(ctx context.Context, gid types.GroupIdentifier) (*Group, error) {
groupMasterKey, err := cli.Store.GroupStore.MasterKeyFromGroupIdentifier(ctx, gid)
if err != nil {
return nil, fmt.Errorf("failed to get group master key: %w", err)
}
if groupMasterKey == "" {
return nil, fmt.Errorf("No group master key found for group identifier %s", gid)
}
return cli.fetchGroupWithMasterKey(ctx, groupMasterKey)
}
func (cli *Client) fetchGroupWithMasterKey(ctx context.Context, groupMasterKey types.SerializedGroupMasterKey) (*Group, error) {
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,
}
response, err := web.SendHTTPRequest(ctx, http.MethodGet, "/v1/groups", opts)
if err != nil {
return nil, err
}
if response.StatusCode != 200 {
return nil, fmt.Errorf("fetchGroupByID SendHTTPRequest bad status: %d", response.StatusCode)
}
var encryptedGroup signalpb.Group
groupBytes, err := io.ReadAll(response.Body)
if err != nil {
return nil, err
}
err = proto.Unmarshal(groupBytes, &encryptedGroup)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal group: %w", err)
}
group, err := decryptGroup(ctx, &encryptedGroup, groupMasterKey)
if err != nil {
return nil, fmt.Errorf("failed to decrypt group: %w", err)
}
// Store the profile keys in case they're new
for _, member := range group.Members {
err = cli.Store.RecipientStore.StoreProfileKey(ctx, member.ACI, member.ProfileKey)
if err != nil {
return nil, fmt.Errorf("failed to store profile key: %w", err)
}
}
for _, requestingMember := range group.RequestingMembers {
err = cli.Store.RecipientStore.StoreProfileKey(ctx, requestingMember.ACI, requestingMember.ProfileKey)
if err != nil {
return nil, fmt.Errorf("failed to store profile key: %w", err)
}
}
return group, nil
}
func (cli *Client) DownloadGroupAvatar(ctx context.Context, group GroupAvatarMeta) ([]byte, error) {
groupMasterKey := group.getGroupMasterKey()
avatarPath := group.GetAvatarPath()
username, password := cli.Store.BasicAuthCreds()
opts := &web.HTTPReqOpt{
Host: web.CDN1Hostname,
Username: &username,
Password: &password,
}
resp, err := web.SendHTTPRequest(ctx, http.MethodGet, *avatarPath, opts)
if err != nil {
return nil, fmt.Errorf("failed to send request: %w", err)
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("unexpected response status %d", resp.StatusCode)
}
encryptedAvatar, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}
decrypted, err := decryptGroupAvatar(encryptedAvatar, groupMasterKey)
if err != nil {
return nil, fmt.Errorf("failed to decrypt avatar: %w", err)
}
return decrypted, nil
}
func (cli *Client) RetrieveGroupByID(ctx context.Context, gid types.GroupIdentifier, revision uint32) (*Group, error) {
cli.initGroupCache()
lastFetched, ok := cli.GroupCache.lastFetched[gid]
if ok && time.Since(lastFetched) < 1*time.Hour {
group, ok := cli.GroupCache.groups[gid]
if ok && group.Revision >= revision {
return group, nil
}
}
group, err := cli.fetchGroupByID(ctx, gid)
if err != nil {
return nil, err
}
cli.GroupCache.groups[gid] = group
cli.GroupCache.lastFetched[gid] = time.Now()
return group, nil
}
// We should store the group master key in the group store as soon as we see it,
// then use the group identifier to refer to groups. As a convenience, we return
// the group identifier, which is derived from the group master key.
func (cli *Client) StoreMasterKey(ctx context.Context, groupMasterKey types.SerializedGroupMasterKey) (types.GroupIdentifier, error) {
groupIdentifier, err := groupIdentifierFromMasterKey(groupMasterKey)
if err != nil {
return "", fmt.Errorf("groupIdentifierFromMasterKey error: %w", err)
}
err = cli.Store.GroupStore.StoreMasterKey(ctx, groupIdentifier, groupMasterKey)
if err != nil {
return "", fmt.Errorf("StoreMasterKey error: %w", err)
}
return groupIdentifier, nil
}
// We need to track active calls so we don't send too many IncomingSignalMessageCalls
// Of course for group calls Signal doesn't tell us *anything* so we're mostly just inferring
// So we just jam a new call ID in, and return true if we *think* this is a new incoming call
func (cli *Client) UpdateActiveCalls(gid types.GroupIdentifier, callID string) (isActive bool) {
cli.initGroupCache()
// Check to see if we currently have an active call for this group
currentCallID, ok := cli.GroupCache.activeCalls[gid]
if ok {
// If we do, then this must be ending the call
if currentCallID == callID {
delete(cli.GroupCache.activeCalls, gid)
return false
}
}
cli.GroupCache.activeCalls[gid] = callID
return true
}
func (cli *Client) initGroupCache() {
if cli.GroupCache == nil {
cli.GroupCache = &GroupCache{
groups: make(map[types.GroupIdentifier]*Group),
lastFetched: make(map[types.GroupIdentifier]time.Time),
activeCalls: make(map[types.GroupIdentifier]string),
}
}
}
type GroupCache struct {
groups map[types.GroupIdentifier]*Group
lastFetched map[types.GroupIdentifier]time.Time
activeCalls map[types.GroupIdentifier]string
}
func (cli *Client) DecryptGroupChange(ctx context.Context, groupContext *signalpb.GroupContextV2) (*GroupChange, error) {
masterKeyBytes := libsignalgo.GroupMasterKey(groupContext.MasterKey)
groupMasterKey := masterKeyFromBytes(masterKeyBytes)
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
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{}
err = proto.Unmarshal(encryptedActionsBytes, &encryptedActions)
if err != nil {
return nil, fmt.Errorf("Error unmashalling group change actions: %w", err)
}
groupSecretParams, err := libsignalgo.DeriveGroupSecretParamsFromMasterKey(masterKeyToBytes(groupMasterKey))
if err != nil {
log.Err(err).Msg("DeriveGroupSecretParamsFromMasterKey error")
return nil, err
}
sourceServiceID, err := groupSecretParams.DecryptServiceID(libsignalgo.UUIDCiphertext(encryptedActions.SourceServiceId))
if err != nil {
log.Err(err).Msg("Couldn't decrypt source serviceID")
return nil, err
}
if sourceServiceID.Type != libsignalgo.ServiceIDTypeACI {
return nil, fmt.Errorf("wrong serviceid kind: expected aci, got pni")
}
decryptedGroupChange := &GroupChange{
groupMasterKey: groupMasterKey,
Revision: encryptedActions.Revision,
SourceServiceID: sourceServiceID,
}
if encryptedActions.ModifyTitle != nil {
titleBlob, err := decryptGroupPropertyIntoBlob(groupSecretParams, encryptedActions.ModifyTitle.Title)
if err != nil {
return nil, err
}
// The actual title is in the blob
newTitle := cleanupStringProperty(titleBlob.GetTitle())
decryptedGroupChange.ModifyTitle = &newTitle
}
if encryptedActions.ModifyAvatar != nil {
decryptedGroupChange.ModifyAvatar = &encryptedActions.ModifyAvatar.Avatar
}
if encryptedActions.ModifyDescription != nil {
descriptionBlob, err := decryptGroupPropertyIntoBlob(groupSecretParams, encryptedActions.ModifyDescription.Description)
if err == nil {
// treat a failure in obtaining the description as non-fatal
newDescription := cleanupStringProperty(descriptionBlob.GetDescription())
decryptedGroupChange.ModifyDescription = &newDescription
}
}
for _, addMember := range encryptedActions.AddMembers {
if addMember == nil {
continue
}
decryptedMember, err := decryptMember(ctx, addMember.Added, groupSecretParams)
if err != nil {
return nil, err
}
decryptedGroupChange.AddMembers = append(decryptedGroupChange.AddMembers, &AddMember{
GroupMember: *decryptedMember,
JoinFromInviteLink: addMember.JoinFromInviteLink,
})
err = cli.Store.RecipientStore.StoreProfileKey(ctx, decryptedMember.ACI, decryptedMember.ProfileKey)
if err != nil {
log.Err(err).Msg("failed to store profile key")
return nil, err
}
}
for _, deleteMember := range encryptedActions.DeleteMembers {
if deleteMember == nil {
continue
}
encryptedUserID := libsignalgo.UUIDCiphertext(deleteMember.DeletedUserId)
serviceID, err := groupSecretParams.DecryptServiceID(encryptedUserID)
if err != nil {
log.Err(err).Msg("DecryptUUID UserId error for deleteMember")
return nil, err
}
if serviceID.Type != libsignalgo.ServiceIDTypeACI {
return nil, fmt.Errorf("Wrong ServiceID kind: expected ACI, got PNI")
}
decryptedGroupChange.DeleteMembers = append(decryptedGroupChange.DeleteMembers, &serviceID.UUID)
}
for _, modifyMemberRole := range encryptedActions.ModifyMemberRoles {
encryptedUserID := libsignalgo.UUIDCiphertext(modifyMemberRole.UserId)
serviceID, err := groupSecretParams.DecryptServiceID(encryptedUserID)
if err != nil {
log.Err(err).Msg("DecryptUUID UserId error for modifyMemberRole")
return nil, err
}
if serviceID.Type != libsignalgo.ServiceIDTypeACI {
return nil, fmt.Errorf("Wrong ServiceID kind: expected ACI, got PNI")
}
decryptedGroupChange.ModifyMemberRoles = append(decryptedGroupChange.ModifyMemberRoles, &RoleMember{
ACI: serviceID.UUID,
Role: GroupMemberRole(modifyMemberRole.Role),
})
}
for _, modifyProfileKey := range encryptedActions.ModifyMemberProfileKeys {
if modifyProfileKey == nil {
continue
}
aci, profileKey, err := decryptPKeyAndIDorPresentation(ctx, modifyProfileKey.UserId, modifyProfileKey.ProfileKey, modifyProfileKey.Presentation, groupSecretParams)
if err != nil {
return nil, err
}
decryptedGroupChange.ModifyMemberProfileKeys = append(decryptedGroupChange.ModifyMemberProfileKeys, &ProfileKeyMember{
ACI: *aci,
ProfileKey: *profileKey,
})
err = cli.Store.RecipientStore.StoreProfileKey(ctx, *aci, *profileKey)
if err != nil {
log.Err(err).Msg("failed to store profile key")
return nil, err
}
}
for _, addPendingMember := range encryptedActions.AddPendingMembers {
if addPendingMember == nil {
continue
}
pendingMember := addPendingMember.Added
decryptedPendingMember, err := decryptPendingMember(ctx, pendingMember, groupSecretParams)
if err != nil {
continue
// decryptPendingMember returns an error if the userID is a PNI, keep decrypting
}
decryptedGroupChange.AddPendingMembers = append(decryptedGroupChange.AddPendingMembers, decryptedPendingMember)
}
for _, deletePendingMember := range encryptedActions.DeletePendingMembers {
if deletePendingMember == nil {
continue
}
encryptedUserID := libsignalgo.UUIDCiphertext(deletePendingMember.DeletedUserId)
userID, err := groupSecretParams.DecryptServiceID(encryptedUserID)
if err != nil {
log.Err(err).Msg("DecryptUUID UserId error for deletePendingMember")
return nil, err
}
decryptedGroupChange.DeletePendingMembers = append(decryptedGroupChange.DeletePendingMembers, &userID)
}
for _, promotePendingMember := range encryptedActions.PromotePendingMembers {
if promotePendingMember == nil {
continue
}
aci, profileKey, err := decryptPKeyAndIDorPresentation(ctx, promotePendingMember.UserId, promotePendingMember.ProfileKey, promotePendingMember.Presentation, groupSecretParams)
if err != nil {
return nil, err
}
decryptedGroupChange.PromotePendingMembers = append(decryptedGroupChange.PromotePendingMembers, &PromotePendingMember{
ACI: *aci,
ProfileKey: *profileKey,
})
err = cli.Store.RecipientStore.StoreProfileKey(ctx, *aci, *profileKey)
if err != nil {
log.Err(err).Msg("failed to store profile key")
return nil, err
}
}
for _, promotePendingPniAciMember := range encryptedActions.PromotePendingPniAciMembers {
// TODO: pretending this is a PendingMember should do for mautrix-signal, but we probably want to treat them separately at some point
if promotePendingPniAciMember == nil {
continue
}
aci, profileKey, err := decryptPKeyAndIDorPresentation(ctx, promotePendingPniAciMember.UserId, promotePendingPniAciMember.ProfileKey, promotePendingPniAciMember.Presentation, groupSecretParams)
if err != nil {
return nil, err
}
encryptedUserID := libsignalgo.UUIDCiphertext(promotePendingPniAciMember.Pni)
pniServiceID, err := groupSecretParams.DecryptServiceID(encryptedUserID)
if err != nil {
log.Err(err).Msg("DecryptUUID Pni error for promotePendingPniAciMember")
return nil, err
}
if pniServiceID.Type != libsignalgo.ServiceIDTypePNI {
return nil, fmt.Errorf("Wrong ServiceID kind: expected PNI, got ACI")
}
decryptedGroupChange.PromotePendingPniAciMembers = append(decryptedGroupChange.PromotePendingPniAciMembers, &PromotePendingPniAciMember{
ACI: *aci,
ProfileKey: *profileKey,
PNI: pniServiceID.UUID,
})
err = cli.Store.RecipientStore.StoreProfileKey(ctx, *aci, *profileKey)
if err != nil {
log.Err(err).Msg("failed to store profile key")
return nil, err
}
}
for _, addRequestingMember := range encryptedActions.AddRequestingMembers {
if addRequestingMember == nil {
continue
}
decryptedRequestingMember, err := decryptRequestingMember(ctx, addRequestingMember.Added, groupSecretParams)
if err != nil {
return nil, err
}
decryptedGroupChange.AddRequestingMembers = append(decryptedGroupChange.AddRequestingMembers, decryptedRequestingMember)
err = cli.Store.RecipientStore.StoreProfileKey(ctx, decryptedRequestingMember.ACI, decryptedRequestingMember.ProfileKey)
if err != nil {
log.Err(err).Msg("failed to store profile key")
return nil, err
}
}
for _, deleteRequestingMember := range encryptedActions.DeleteRequestingMembers {
if deleteRequestingMember == nil {
continue
}
encryptedUserID := libsignalgo.UUIDCiphertext(deleteRequestingMember.DeletedUserId)
serviceID, err := groupSecretParams.DecryptServiceID(encryptedUserID)
if err != nil {
log.Err(err).Msg("DecryptUUID UserId error for deleteRequestingMember")
return nil, err
}
decryptedGroupChange.DeleteRequestingMembers = append(decryptedGroupChange.DeleteRequestingMembers, &serviceID.UUID)
}
for _, promoteRequestingMember := range encryptedActions.PromoteRequestingMembers {
if promoteRequestingMember == nil {
continue
}
encryptedUserID := libsignalgo.UUIDCiphertext(promoteRequestingMember.UserId)
serviceID, err := groupSecretParams.DecryptServiceID(encryptedUserID)
if err != nil {
log.Err(err).Msg("DecryptUUID UserId error for promoteRequestingMember")
return nil, err
}
decryptedGroupChange.PromoteRequestingMembers = append(decryptedGroupChange.PromoteRequestingMembers, &RoleMember{
ACI: serviceID.UUID,
Role: GroupMemberRole(promoteRequestingMember.Role),
})
}
for _, addBannedMember := range encryptedActions.AddBannedMembers {
if addBannedMember == nil {
continue
}
bannedMember := addBannedMember.Added
encryptedUserID := libsignalgo.UUIDCiphertext(bannedMember.UserId)
serviceID, err := groupSecretParams.DecryptServiceID(encryptedUserID)
if err != nil {
log.Err(err).Msg("DecryptUUID UserId error for addBannedMember")
return nil, err
}
decryptedGroupChange.AddBannedMembers = append(decryptedGroupChange.AddBannedMembers, &BannedMember{
ServiceID: serviceID,
Timestamp: bannedMember.Timestamp,
})
}
for _, deleteBannedMember := range encryptedActions.DeleteBannedMembers {
if deleteBannedMember == nil {
continue
}
encryptedUserID := libsignalgo.UUIDCiphertext(deleteBannedMember.DeletedUserId)
userID, err := groupSecretParams.DecryptServiceID(encryptedUserID)
if err != nil {
log.Err(err).Msg("DecryptUUID UserId error for deleteBannedMember")
return nil, err
}
decryptedGroupChange.DeleteBannedMembers = append(decryptedGroupChange.DeleteBannedMembers, &userID)
}
if encryptedActions.ModifyAttributesAccess != nil {
decryptedGroupChange.ModifyAttributesAccess = (*AccessControl)(&encryptedActions.ModifyAttributesAccess.AttributesAccess)
}
if encryptedActions.ModifyMemberAccess != nil {
decryptedGroupChange.ModifyMemberAccess = (*AccessControl)(&encryptedActions.ModifyMemberAccess.MembersAccess)
}
if encryptedActions.ModifyAddFromInviteLinkAccess != nil {
decryptedGroupChange.ModifyAddFromInviteLinkAccess = (*AccessControl)(&encryptedActions.ModifyAddFromInviteLinkAccess.AddFromInviteLinkAccess)
}
if encryptedActions.ModifyAnnouncementsOnly != nil {
decryptedGroupChange.ModifyAnnouncementsOnly = &encryptedActions.ModifyAnnouncementsOnly.AnnouncementsOnly
}
if encryptedActions.ModifyDisappearingMessagesTimer != nil && len(encryptedActions.ModifyDisappearingMessagesTimer.Timer) > 0 {
timerBlob, err := decryptGroupPropertyIntoBlob(groupSecretParams, encryptedActions.ModifyDisappearingMessagesTimer.Timer)
if err != nil {
return nil, err
}
newDisappaeringMessagesDuration := timerBlob.GetDisappearingMessagesDuration()
decryptedGroupChange.ModifyDisappearingMessagesDuration = &newDisappaeringMessagesDuration
}
if encryptedActions.ModifyInviteLinkPassword != nil {
inviteLinkPassword := InviteLinkPasswordFromBytes(encryptedActions.ModifyInviteLinkPassword.InviteLinkPassword)
decryptedGroupChange.ModifyInviteLinkPassword = &inviteLinkPassword
}
return decryptedGroupChange, nil
}
func decryptPKeyAndIDorPresentation(ctx context.Context, userID []byte, profileKeyBytes []byte, presentationBytes []byte, groupSecretParams libsignalgo.GroupSecretParams) (*uuid.UUID, *libsignalgo.ProfileKey, error) {
log := zerolog.Ctx(ctx)
var encryptedUserID libsignalgo.UUIDCiphertext
var encryptedProfileKey libsignalgo.ProfileKeyCiphertext
if len(userID) == 0 || len(profileKeyBytes) == 0 {
presentation := libsignalgo.ProfileKeyCredentialPresentation(presentationBytes)
err := presentation.CheckValidContents()
if err != nil {
log.Err(err).Msg("Invalid presentation contents")
return nil, nil, err
}
encryptedUserID, err = presentation.UUIDCiphertext()
if err != nil {
log.Err(err).Msg("unable to get UUID from presentation")
return nil, nil, err
}
encryptedProfileKey, err = presentation.ProfileKeyCiphertext()
if err != nil {
log.Err(err).Msg("unable to get ProfileKey from presentation")
return nil, nil, err
}
} else {
encryptedUserID = libsignalgo.UUIDCiphertext(userID)
encryptedProfileKey = libsignalgo.ProfileKeyCiphertext(profileKeyBytes)
}
serviceID, err := groupSecretParams.DecryptServiceID(encryptedUserID)
if err != nil {
log.Err(err).Msg("Failed to decrypt ServiceID")
return nil, nil, err
}
profileKey, err := groupSecretParams.DecryptProfileKey(encryptedProfileKey, serviceID.UUID)
if err != nil {
return nil, nil, err
}
if serviceID.Type == libsignalgo.ServiceIDTypePNI {
return nil, nil, fmt.Errorf("wrong serviceid kind, expected ACI, got PNI")
}
return &serviceID.UUID, profileKey, nil
}
func decryptMember(ctx context.Context, member *signalpb.Member, groupSecretParams libsignalgo.GroupSecretParams) (*GroupMember, error) {
aci, profileKey, err := decryptPKeyAndIDorPresentation(ctx, member.UserId, member.ProfileKey, member.Presentation, groupSecretParams)
if err != nil {
return nil, err
}
return &GroupMember{
ACI: *aci,
ProfileKey: *profileKey,
Role: GroupMemberRole(member.Role),
JoinedAtRevision: member.JoinedAtRevision,
}, nil
}
func decryptPendingMember(ctx context.Context, pendingMember *signalpb.PendingMember, groupSecretParams libsignalgo.GroupSecretParams) (*PendingMember, error) {
log := zerolog.Ctx(ctx)
encryptedUserID := libsignalgo.UUIDCiphertext(pendingMember.Member.UserId)
userID, err := groupSecretParams.DecryptServiceID(encryptedUserID)
if err != nil {
log.Err(err).Msg("DecryptUUID UserId error for pendingMember")
return nil, err
}
// pendingMembers don't have profile keys
encryptedAddedByUserID := pendingMember.AddedByUserId
addedByServiceId, err := groupSecretParams.DecryptServiceID(libsignalgo.UUIDCiphertext(encryptedAddedByUserID))
if err != nil {
log.Err(err).Msg("DecryptUUID addedByUserId error for pendingMember")
return nil, err
}
return &PendingMember{
ServiceID: userID,
Role: GroupMemberRole(pendingMember.Member.Role),
AddedByUserID: addedByServiceId.UUID,
Timestamp: pendingMember.Timestamp,
}, nil
}
func decryptRequestingMember(ctx context.Context, requestingMember *signalpb.RequestingMember, groupSecretParams libsignalgo.GroupSecretParams) (*RequestingMember, error) {
aci, profileKey, err := decryptPKeyAndIDorPresentation(ctx, requestingMember.UserId, requestingMember.ProfileKey, requestingMember.Presentation, groupSecretParams)
if err != nil {
return nil, err
}
return &RequestingMember{
ACI: *aci,
ProfileKey: *profileKey,
Timestamp: requestingMember.Timestamp,
}, nil
}
func (cli *Client) EncryptAndSignGroupChange(ctx context.Context, decryptedGroupChange *GroupChange, gid types.GroupIdentifier) (*signalpb.GroupChange, error) {
log := zerolog.Ctx(ctx).With().Str("action", "EncryptGroupChange").Logger()
groupMasterKey := decryptedGroupChange.groupMasterKey
masterKeyBytes := masterKeyToBytes(groupMasterKey)
groupSecretParams, err := libsignalgo.DeriveGroupSecretParamsFromMasterKey(masterKeyBytes)
if err != nil {
log.Err(err).Msg("Could not get groupSecretParams from master key")
return nil, err
}
groupChangeActions := &signalpb.GroupChange_Actions{Revision: decryptedGroupChange.Revision}
if decryptedGroupChange.ModifyTitle != nil {
attributeBlob := signalpb.GroupAttributeBlob{Content: &signalpb.GroupAttributeBlob_Title{Title: *decryptedGroupChange.ModifyTitle}}
encryptedTitle, err := encryptBlobIntoGroupProperty(groupSecretParams, &attributeBlob)
if err != nil {
log.Err(err).Msg("Could not get encrypt Title")
return nil, err
}
groupChangeActions.ModifyTitle = &signalpb.GroupChange_Actions_ModifyTitleAction{Title: *encryptedTitle}
}
if decryptedGroupChange.ModifyDescription != nil {
attributeBlob := signalpb.GroupAttributeBlob{Content: &signalpb.GroupAttributeBlob_Description{Description: *decryptedGroupChange.ModifyDescription}}
encryptedDescription, err := encryptBlobIntoGroupProperty(groupSecretParams, &attributeBlob)
if err != nil {
log.Err(err).Msg("Could not get encrypt description")
return nil, err
}
groupChangeActions.ModifyDescription = &signalpb.GroupChange_Actions_ModifyDescriptionAction{Description: *encryptedDescription}
}
if decryptedGroupChange.ModifyAvatar != nil {
groupChangeActions.ModifyAvatar = &signalpb.GroupChange_Actions_ModifyAvatarAction{Avatar: *decryptedGroupChange.ModifyAvatar}
}
for _, addMember := range decryptedGroupChange.AddMembers {
encryptedMember, encryptedPendingMember, err := cli.encryptMember(ctx, &addMember.GroupMember, &groupSecretParams)
if err != nil {
log.Err(err).Msg("Failed to encrypt GroupMember")
}
if encryptedMember != nil {
groupChangeActions.AddMembers = append(groupChangeActions.AddMembers, &signalpb.GroupChange_Actions_AddMemberAction{
Added: encryptedMember,
JoinFromInviteLink: addMember.JoinFromInviteLink,
})
} else {
groupChangeActions.AddPendingMembers = append(groupChangeActions.AddPendingMembers, &signalpb.GroupChange_Actions_AddPendingMemberAction{
Added: encryptedPendingMember,
})
}
}
for _, deleteMember := range decryptedGroupChange.DeleteMembers {
encryptedUserID, err := groupSecretParams.EncryptServiceID(libsignalgo.NewACIServiceID(*deleteMember))
if err != nil {
log.Err(err).Msg("Encrypt UserId error for deleteMember")
return nil, err
}
groupChangeActions.DeleteMembers = append(groupChangeActions.DeleteMembers, &signalpb.GroupChange_Actions_DeleteMemberAction{
DeletedUserId: encryptedUserID[:],
})
}
for _, modifyMemberRoles := range decryptedGroupChange.ModifyMemberRoles {
encryptedUserID, err := groupSecretParams.EncryptServiceID(libsignalgo.NewACIServiceID(modifyMemberRoles.ACI))
if err != nil {
log.Err(err).Msg("Encrypt UserId error for modifyMemberRoles")
return nil, err
}
groupChangeActions.ModifyMemberRoles = append(groupChangeActions.ModifyMemberRoles, &signalpb.GroupChange_Actions_ModifyMemberRoleAction{
UserId: encryptedUserID[:],
Role: signalpb.Member_Role(modifyMemberRoles.Role),
})
}
for _, addPendingMember := range decryptedGroupChange.AddPendingMembers {
encryptedPendingMember, err := cli.encryptPendingMember(ctx, addPendingMember, &groupSecretParams)
if err != nil {
log.Err(err).Msg("Failed to encrypt pendingMember")
return nil, err
}
groupChangeActions.AddPendingMembers = append(groupChangeActions.AddPendingMembers, &signalpb.GroupChange_Actions_AddPendingMemberAction{
Added: encryptedPendingMember,
})
}
for _, deletePendingMember := range decryptedGroupChange.DeletePendingMembers {
encryptedUserID, err := groupSecretParams.EncryptServiceID(*deletePendingMember)
if err != nil {
log.Err(err).Msg("Encrypt UserId error for deletePendingMember")
return nil, err
}
groupChangeActions.DeletePendingMembers = append(groupChangeActions.DeletePendingMembers, &signalpb.GroupChange_Actions_DeletePendingMemberAction{
DeletedUserId: encryptedUserID[:],
})
}
for _, promotePendingMember := range decryptedGroupChange.PromotePendingMembers {
expiringProfileKeyCredential, err := cli.FetchExpiringProfileKeyCredentialById(ctx, promotePendingMember.ACI)
if err != nil {
log.Err(err).Msg("failed getting expiring profile key credential for addMember")
return nil, err
}
presentation, err := groupSecretParams.CreateExpiringProfileKeyCredentialPresentation(
prodServerPublicParams,
*expiringProfileKeyCredential,
)
if err != nil {
log.Err(err).Msg("failed creating expiring profile key credential presentation for addMember")
return nil, err
}
groupChangeActions.PromotePendingMembers = append(groupChangeActions.PromotePendingMembers, &signalpb.GroupChange_Actions_PromotePendingMemberAction{
Presentation: *presentation,
})
}
for _, addRequestingMember := range decryptedGroupChange.AddRequestingMembers {
expiringProfileKeyCredential, err := cli.FetchExpiringProfileKeyCredentialById(ctx, addRequestingMember.ACI)
if err != nil {
log.Err(err).Msg("failed getting expiring profile key credential for addMember")
return nil, err
}
presentation, err := groupSecretParams.CreateExpiringProfileKeyCredentialPresentation(
prodServerPublicParams,
*expiringProfileKeyCredential,
)
if err != nil {
log.Err(err).Msg("failed creating expiring profile key credential presentation for addMember")
return nil, err
}
groupChangeActions.AddRequestingMembers = append(groupChangeActions.AddRequestingMembers, &signalpb.GroupChange_Actions_AddRequestingMemberAction{
Added: &signalpb.RequestingMember{
Presentation: *presentation,
},
})
}
for _, deleteRequestingMember := range decryptedGroupChange.DeleteRequestingMembers {
encryptedUserID, err := groupSecretParams.EncryptServiceID(libsignalgo.NewACIServiceID(*deleteRequestingMember))
if err != nil {
log.Err(err).Msg("Encrypt UserId error for deleteRequestingMember")
return nil, err
}
groupChangeActions.DeleteRequestingMembers = append(groupChangeActions.DeleteRequestingMembers, &signalpb.GroupChange_Actions_DeleteRequestingMemberAction{
DeletedUserId: encryptedUserID[:],
})
}
for _, promoteRequestingMember := range decryptedGroupChange.PromoteRequestingMembers {
encryptedUserID, err := groupSecretParams.EncryptServiceID(libsignalgo.NewACIServiceID(promoteRequestingMember.ACI))
if err != nil {
log.Err(err).Msg("Encrypt UserId error for promoteRequestingMember")
return nil, err
}
groupChangeActions.PromoteRequestingMembers = append(groupChangeActions.PromoteRequestingMembers, &signalpb.GroupChange_Actions_PromoteRequestingMemberAction{
UserId: encryptedUserID[:],
Role: signalpb.Member_Role(promoteRequestingMember.Role),
})
}
for _, addBannedMember := range decryptedGroupChange.AddBannedMembers {
encryptedUserID, err := groupSecretParams.EncryptServiceID(addBannedMember.ServiceID)
if err != nil {
log.Err(err).Msg("Encrypt UserId error for promoteRequestingMember")
return nil, err
}
groupChangeActions.AddBannedMembers = append(groupChangeActions.AddBannedMembers, &signalpb.GroupChange_Actions_AddBannedMemberAction{
Added: &signalpb.BannedMember{
UserId: encryptedUserID[:],
Timestamp: addBannedMember.Timestamp,
},
})
}
for _, deleteBannedMember := range decryptedGroupChange.DeleteBannedMembers {
encryptedUserID, err := groupSecretParams.EncryptServiceID(*deleteBannedMember)
if err != nil {
log.Err(err).Msg("Encrypt UserId error for promoteRequestingMember")
return nil, err
}
groupChangeActions.DeleteBannedMembers = append(groupChangeActions.DeleteBannedMembers, &signalpb.GroupChange_Actions_DeleteBannedMemberAction{
DeletedUserId: encryptedUserID[:],
})
}
if decryptedGroupChange.ModifyAnnouncementsOnly != nil {
groupChangeActions.ModifyAnnouncementsOnly = &signalpb.GroupChange_Actions_ModifyAnnouncementsOnlyAction{
AnnouncementsOnly: *decryptedGroupChange.ModifyAnnouncementsOnly,
}
}
if decryptedGroupChange.ModifyAttributesAccess != nil {
groupChangeActions.ModifyAttributesAccess = &signalpb.GroupChange_Actions_ModifyAttributesAccessControlAction{
AttributesAccess: signalpb.AccessControl_AccessRequired(*decryptedGroupChange.ModifyAttributesAccess),
}
}
if decryptedGroupChange.ModifyMemberAccess != nil {
groupChangeActions.ModifyMemberAccess = &signalpb.GroupChange_Actions_ModifyMembersAccessControlAction{
MembersAccess: signalpb.AccessControl_AccessRequired(*decryptedGroupChange.ModifyMemberAccess),
}
}
if decryptedGroupChange.ModifyAddFromInviteLinkAccess != nil {
groupChangeActions.ModifyAddFromInviteLinkAccess = &signalpb.GroupChange_Actions_ModifyAddFromInviteLinkAccessControlAction{
AddFromInviteLinkAccess: signalpb.AccessControl_AccessRequired(*decryptedGroupChange.ModifyAddFromInviteLinkAccess),
}
}
if decryptedGroupChange.ModifyDisappearingMessagesDuration != nil {
attributeBlob := signalpb.GroupAttributeBlob{Content: &signalpb.GroupAttributeBlob_DisappearingMessagesDuration{DisappearingMessagesDuration: *decryptedGroupChange.ModifyDisappearingMessagesDuration}}
encryptedTimer, err := encryptBlobIntoGroupProperty(groupSecretParams, &attributeBlob)
if err != nil {
log.Err(err).Msg("Could not get encrypt Title")
return nil, err
}
groupChangeActions.ModifyDisappearingMessagesTimer = &signalpb.GroupChange_Actions_ModifyDisappearingMessagesTimerAction{Timer: *encryptedTimer}
}
if decryptedGroupChange.ModifyInviteLinkPassword != nil {
inviteLinkPasswordBytes, err := inviteLinkPasswordToBytes(*decryptedGroupChange.ModifyInviteLinkPassword)
if err != nil {
log.Err(err).Msg("Failed to decode invite link password")
}
groupChangeActions.ModifyInviteLinkPassword = &signalpb.GroupChange_Actions_ModifyInviteLinkPasswordAction{
InviteLinkPassword: inviteLinkPasswordBytes,
}
}
return cli.patchGroup(ctx, groupChangeActions, groupMasterKey, nil)
}
func (cli *Client) encryptMember(ctx context.Context, member *GroupMember, groupSecretParams *libsignalgo.GroupSecretParams) (*signalpb.Member, *signalpb.PendingMember, error) {
log := zerolog.Ctx(ctx)
expiringProfileKeyCredential, err := cli.FetchExpiringProfileKeyCredentialById(ctx, member.ACI)
if err != nil {
log.Err(err).Msg("failed getting expiring profile key credential for member, trying to encrypt as PendingMember")
pendingMember := PendingMember{
ServiceID: member.UserServiceID(),
Role: member.Role,
AddedByUserID: cli.Store.ACI,
}
encryptedPendingMember, err := cli.encryptPendingMember(ctx, &pendingMember, groupSecretParams)
return nil, encryptedPendingMember, err
}
presentation, err := groupSecretParams.CreateExpiringProfileKeyCredentialPresentation(
prodServerPublicParams,
*expiringProfileKeyCredential,
)
if err != nil {
log.Err(err).Msg("failed creating expiring profile key credential presentation for addMember")
return nil, nil, err
}
encryptedMember := signalpb.Member{
Presentation: *presentation,
Role: signalpb.Member_Role(member.Role),
}
return &encryptedMember, nil, nil
}
func (cli *Client) encryptPendingMember(ctx context.Context, pendingMember *PendingMember, groupSecretParams *libsignalgo.GroupSecretParams) (*signalpb.PendingMember, error) {
log := zerolog.Ctx(ctx)
encryptedUserID, err := groupSecretParams.EncryptServiceID(pendingMember.ServiceID)
if err != nil {
log.Err(err).Msg("Encrypt UserId error for addPendingMember")
return nil, err
}
encryptedAddedByUserID, err := groupSecretParams.EncryptServiceID(libsignalgo.NewACIServiceID(pendingMember.AddedByUserID))
if err != nil {
log.Err(err).Msg("Encrypt AddedByUserId error for addPendingMember")
return nil, err
}
encryptedPendingMember := signalpb.PendingMember{
AddedByUserId: encryptedAddedByUserID[:],
Member: &signalpb.Member{
UserId: encryptedUserID[:],
Role: signalpb.Member_Role(pendingMember.Role),
},
}
return &encryptedPendingMember, nil
}
var (
NoContentError = RespError{Err: "NoContentError"}
GroupPatchNotAcceptedError = RespError{Err: "GroupPatchNotAcceptedError"}
ConflictError = RespError{Err: "ConflictError"}
AuthorizationFailedError = RespError{Err: "AuthorizationFailedError"}
NotFoundError = RespError{Err: "NotFoundError"}
ContactManifestMismatchError = RespError{Err: "ContactManifestMismatchError"}
RateLimitError = RespError{Err: "RateLimitError"}
DeprecatedVersionError = RespError{Err: "DeprecatedVersionError"}
GroupExistsError = RespError{Err: "GroupExistsError"}
)
type RespError struct {
Err string
}
func (e RespError) Error() string {
return e.Err
}
func (cli *Client) patchGroup(ctx context.Context, groupChange *signalpb.GroupChange_Actions, groupMasterKey types.SerializedGroupMasterKey, groupLinkPassword []byte) (*signalpb.GroupChange, error) {
log := zerolog.Ctx(ctx).With().Str("action", "patchGroup").Logger()
groupAuth, err := cli.GetAuthorizationForToday(ctx, masterKeyToBytes(groupMasterKey))
if err != nil {
log.Err(err).Msg("Failed to get Authorization for today")
return nil, err
}
var path string
if groupLinkPassword == nil {
path = "/v1/groups/"
} else {
path = fmt.Sprintf("/v1/groups/?inviteLinkPassword=%s", base64.StdEncoding.EncodeToString(groupLinkPassword))
}
requestBody, err := proto.Marshal(groupChange)
if err != nil {
log.Err(err).Msg("Failed to marshal request")
return nil, err
}
opts := &web.HTTPReqOpt{
Username: &groupAuth.Username,
Password: &groupAuth.Password,
ContentType: web.ContentTypeProtobuf,
Body: requestBody,
Host: web.StorageHostname,
}
resp, err := web.SendHTTPRequest(ctx, http.MethodPatch, path, opts)
if err != nil {
return nil, fmt.Errorf("SendRequest error: %w", err)
}
switch resp.StatusCode {
case http.StatusNoContent:
return nil, NoContentError
case http.StatusBadRequest:
return nil, GroupPatchNotAcceptedError
case http.StatusForbidden:
return nil, AuthorizationFailedError
case http.StatusNotFound:
return nil, NotFoundError
case http.StatusConflict:
if resp.Body != nil {
return nil, ContactManifestMismatchError
} else {
return nil, ConflictError
}
case http.StatusTooManyRequests:
return nil, RateLimitError
case 499:
return nil, DeprecatedVersionError
}
if resp.Body == nil {
return nil, errors.New("no response body")
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read storage manifest response: %w", err)
}
signedGroupChange := signalpb.GroupChange{}
err = proto.Unmarshal(body, &signedGroupChange)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal signed groupChange: %w", err)
}
return &signedGroupChange, nil
}
func (cli *Client) UpdateGroup(ctx context.Context, groupChange *GroupChange, gid types.GroupIdentifier) (uint32, error) {
log := zerolog.Ctx(ctx).With().Str("action", "UpdateGroup").Logger()
groupMasterKey, err := cli.Store.GroupStore.MasterKeyFromGroupIdentifier(ctx, gid)
if err != nil {
log.Err(err).Msg("Could not get master key from group id")
return 0, err
}
groupChange.groupMasterKey = groupMasterKey
masterKeyBytes := masterKeyToBytes(groupMasterKey)
var refetchedAddMemberCredentials bool
var signedGroupChange *signalpb.GroupChange
group, err := cli.RetrieveGroupByID(ctx, gid, 0)
if err != nil {
log.Err(err).Msg("Failed to retrieve Group")
}
if group.InviteLinkPassword == nil && groupChange.ModifyAddFromInviteLinkAccess != nil && groupChange.ModifyInviteLinkPassword != nil {
inviteLinkPasswordBytes := make([]byte, 16)
rand.Read(inviteLinkPasswordBytes)
inviteLinkPassword := InviteLinkPasswordFromBytes(inviteLinkPasswordBytes)
groupChange.ModifyInviteLinkPassword = &inviteLinkPassword
}
groupChange.Revision = group.Revision + 1
for attempt := 0; attempt < 5; attempt++ {
signedGroupChange, err = cli.EncryptAndSignGroupChange(ctx, groupChange, gid)
if errors.Is(err, GroupPatchNotAcceptedError) {
log.Warn().Str("Error applying GroupChange, retrying...", err.Error())
if len(groupChange.AddMembers) > 0 && !refetchedAddMemberCredentials {
refetchedAddMemberCredentials = true
// change = refetchAddMemberCredentials(change); TODO
} else {
return 0, fmt.Errorf("Group Change Failed: %w", err)
}
} else if errors.Is(err, ConflictError) {
delete(cli.GroupCache.groups, gid)
delete(cli.GroupCache.lastFetched, gid)
delete(cli.GroupCache.activeCalls, gid)
group, err = cli.RetrieveGroupByID(ctx, gid, 0)
groupChange.resolveConflict(group)
if groupChange.isEmptpy() {
log.Debug().Msg("Change is empty after conflict resolution")
}
groupChange.Revision = group.Revision + 1
} else {
break
}
}
delete(cli.GroupCache.groups, gid)
delete(cli.GroupCache.lastFetched, gid)
delete(cli.GroupCache.activeCalls, gid)
if err != nil {
log.Err(err).Msg("couldn't patch group on server")
return 0, err
}
groupChangeBytes, err := proto.Marshal(signedGroupChange)
if err != nil {
log.Err(err).Msg("Error marshalling signed GroupChange")
return 0, err
}
groupContext := &signalpb.GroupContextV2{Revision: &groupChange.Revision, GroupChange: groupChangeBytes, MasterKey: masterKeyBytes[:]}
_, err = cli.SendGroupUpdate(ctx, group, groupContext, groupChange)
if err != nil {
log.Err(err).Msg("Error sending GroupChange to group members")
}
return groupChange.Revision, nil
}
func (cli *Client) EncryptGroup(ctx context.Context, decryptedGroup *Group, groupSecretParams libsignalgo.GroupSecretParams) (*signalpb.Group, error) {
log := zerolog.Ctx(ctx)
attributeBlob := signalpb.GroupAttributeBlob{Content: &signalpb.GroupAttributeBlob_Title{Title: decryptedGroup.Title}}
encryptedTitle, err := encryptBlobIntoGroupProperty(groupSecretParams, &attributeBlob)
if err != nil {
log.Err(err).Msg("Could not get encrypt Title")
return nil, err
}
groupPublicParams, err := groupSecretParams.GetPublicParams()
if err != nil {
log.Err(err).Msg("Couldn't get public params from GroupSecretParams")
return nil, err
}
encryptedGroup := &signalpb.Group{
PublicKey: groupPublicParams[:],
Title: *encryptedTitle,
Avatar: decryptedGroup.AvatarPath,
AnnouncementsOnly: decryptedGroup.AnnouncementsOnly,
Revision: 0,
}
if decryptedGroup.Description != "" {
attributeBlob := signalpb.GroupAttributeBlob{Content: &signalpb.GroupAttributeBlob_Description{Description: decryptedGroup.Description}}
encryptedDescription, err := encryptBlobIntoGroupProperty(groupSecretParams, &attributeBlob)
if err != nil {
log.Err(err).Msg("Could not get encrypt Description")
return nil, err
}
encryptedGroup.Description = *encryptedDescription
}
if decryptedGroup.AccessControl != nil {
encryptedGroup.AccessControl = &signalpb.AccessControl{
Members: signalpb.AccessControl_AccessRequired(decryptedGroup.AccessControl.Members),
Attributes: signalpb.AccessControl_AccessRequired(decryptedGroup.AccessControl.Attributes),
AddFromInviteLink: signalpb.AccessControl_AccessRequired(decryptedGroup.AccessControl.AddFromInviteLink),
}
if decryptedGroup.AccessControl.AddFromInviteLink != AccessControl_UNSATISFIABLE {
inviteLinkPasswordBytes := make([]byte, 16)
rand.Read(inviteLinkPasswordBytes)
encryptedGroup.InviteLinkPassword = inviteLinkPasswordBytes
}
}
for _, member := range decryptedGroup.Members {
encryptedMember, encryptedPendingMember, err := cli.encryptMember(ctx, member, &groupSecretParams)
if err != nil {
log.Err(err).Msg("Failed to encrypt GroupMember")
}
if encryptedMember != nil {
encryptedGroup.Members = append(encryptedGroup.Members, encryptedMember)
} else {
encryptedGroup.PendingMembers = append(encryptedGroup.PendingMembers, encryptedPendingMember)
}
}
for _, pendingMember := range decryptedGroup.PendingMembers {
encryptedPendingMember, err := cli.encryptPendingMember(ctx, pendingMember, &groupSecretParams)
if err != nil {
log.Err(err).Msg("Failed to encrypt pendingMember")
return nil, err
}
encryptedGroup.PendingMembers = append(encryptedGroup.PendingMembers, encryptedPendingMember)
}
return encryptedGroup, nil
}
func (cli *Client) createGroupOnServer(ctx context.Context, decryptedGroup *Group, avatarBytes []byte) (*Group, error) {
log := zerolog.Ctx(ctx).With().Str("action", "CreateGroupOnServer").Logger()
masterKeyByteArray := make([]byte, 32)
rand.Read(masterKeyByteArray)
masterKeyBytes := libsignalgo.GroupMasterKey(masterKeyByteArray)
groupMasterKey := masterKeyFromBytes(masterKeyBytes)
groupId, err := groupIdentifierFromMasterKey(groupMasterKey)
if err != nil {
log.Err(err).Msg("Couldn't get gid from masterkey")
return nil, err
}
err = cli.Store.GroupStore.StoreMasterKey(ctx, groupId, groupMasterKey)
if err != nil {
return nil, fmt.Errorf("StoreMasterKey error: %w", err)
}
log.Debug().Msg(string(groupMasterKey))
groupSecretParams, err := libsignalgo.DeriveGroupSecretParamsFromMasterKey(masterKeyBytes)
if err != nil {
log.Err(err).Msg("DeriveGroupSecretParamsFromMasterKey error")
return nil, err
}
if len(avatarBytes) > 0 {
avatarPath, err := cli.UploadGroupAvatar(ctx, avatarBytes, groupId)
if err != nil {
log.Err(err).Msg("Failed to upload group avatar")
return nil, err
}
decryptedGroup.AvatarPath = *avatarPath
}
encryptedGroup, err := cli.EncryptGroup(ctx, decryptedGroup, groupSecretParams)
if err != nil {
log.Err(err).Msg("Failed to encrypt group")
return nil, err
}
log.Debug().Stringer("groupID", groupId)
groupAuth, err := cli.GetAuthorizationForToday(ctx, masterKeyBytes)
if err != nil {
log.Err(err).Msg("Failed to get Authorization for today")
return nil, err
}
path := "/v1/groups/"
requestBody, err := proto.Marshal(encryptedGroup)
if err != nil {
log.Err(err).Msg("Failed to marshal request")
return nil, err
}
opts := &web.HTTPReqOpt{
Username: &groupAuth.Username,
Password: &groupAuth.Password,
ContentType: web.ContentTypeProtobuf,
Body: requestBody,
Host: web.StorageHostname,
}
resp, err := web.SendHTTPRequest(ctx, http.MethodPut, path, opts)
if err != nil {
return nil, fmt.Errorf("SendRequest error: %w", err)
}
switch resp.StatusCode {
case http.StatusNoContent:
return nil, NoContentError
case http.StatusForbidden:
return nil, AuthorizationFailedError
case http.StatusNotFound:
return nil, NotFoundError
case http.StatusConflict:
return nil, GroupExistsError
case http.StatusTooManyRequests:
return nil, RateLimitError
case 499:
return nil, DeprecatedVersionError
case http.StatusBadRequest:
return nil, fmt.Errorf("failed to put new group: bad request")
}
group, err := cli.fetchGroupWithMasterKey(ctx, groupMasterKey)
if err != nil {
return nil, fmt.Errorf("failed to get new group: %w", err)
}
log.Debug().Stringer("group id", group.GroupIdentifier).Msg("new group created")
return group, nil
}
func GenerateInviteLinkPassword() types.SerializedInviteLinkPassword {
inviteLinkPasswordBytes := make([]byte, 16)
rand.Read(inviteLinkPasswordBytes)
return InviteLinkPasswordFromBytes(inviteLinkPasswordBytes)
}
func (cli *Client) CreateGroup(ctx context.Context, decryptedGroup *Group, avatarBytes []byte) (*Group, error) {
log := zerolog.Ctx(ctx).With().Str("action", "CreateGroup").Logger()
group, err := cli.createGroupOnServer(ctx, decryptedGroup, avatarBytes)
if err != nil {
log.Err(err).Msg("Error creating group on server")
return nil, err
}
masterKeyBytes := masterKeyToBytes(group.groupMasterKey)
groupContext := &signalpb.GroupContextV2{Revision: &group.Revision, MasterKey: masterKeyBytes[:]}
_, err = cli.SendGroupUpdate(ctx, group, groupContext, nil)
if err != nil {
log.Err(err).Msg("Error sending GroupUpdate to group members")
return nil, err
}
return group, nil
}
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
// GroupState == nil is normal, except for first and last, depending on the parameters it was fetched with
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
// GroupChange shouldn't be nil - if it is, something will probably go wrong
if groupChangeState.GroupChange == nil {
return nil, fmt.Errorf("received group change state without group change")
}
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
}