Signal-Android/libsignal/service/src/main/java/org/whispersystems/signalservice/api/groupsv2/GroupsV2Operations.java

1022 wiersze
51 KiB
Java

package org.whispersystems.signalservice.api.groupsv2;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import org.signal.libsignal.protocol.logging.Log;
import org.signal.libsignal.zkgroup.InvalidInputException;
import org.signal.libsignal.zkgroup.NotarySignature;
import org.signal.libsignal.zkgroup.ServerPublicParams;
import org.signal.libsignal.zkgroup.VerificationFailedException;
import org.signal.libsignal.zkgroup.auth.ClientZkAuthOperations;
import org.signal.libsignal.zkgroup.groups.ClientZkGroupCipher;
import org.signal.libsignal.zkgroup.groups.GroupSecretParams;
import org.signal.libsignal.zkgroup.groups.ProfileKeyCiphertext;
import org.signal.libsignal.zkgroup.groups.UuidCiphertext;
import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations;
import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential;
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
import org.signal.libsignal.zkgroup.profiles.ProfileKeyCredentialPresentation;
import org.signal.storageservice.protos.groups.AccessControl;
import org.signal.storageservice.protos.groups.BannedMember;
import org.signal.storageservice.protos.groups.Group;
import org.signal.storageservice.protos.groups.GroupAttributeBlob;
import org.signal.storageservice.protos.groups.GroupChange;
import org.signal.storageservice.protos.groups.GroupJoinInfo;
import org.signal.storageservice.protos.groups.Member;
import org.signal.storageservice.protos.groups.PendingMember;
import org.signal.storageservice.protos.groups.RequestingMember;
import org.signal.storageservice.protos.groups.local.DecryptedApproveMember;
import org.signal.storageservice.protos.groups.local.DecryptedBannedMember;
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
import org.signal.storageservice.protos.groups.local.DecryptedGroupJoinInfo;
import org.signal.storageservice.protos.groups.local.DecryptedMember;
import org.signal.storageservice.protos.groups.local.DecryptedModifyMemberRole;
import org.signal.storageservice.protos.groups.local.DecryptedPendingMember;
import org.signal.storageservice.protos.groups.local.DecryptedPendingMemberRemoval;
import org.signal.storageservice.protos.groups.local.DecryptedRequestingMember;
import org.signal.storageservice.protos.groups.local.DecryptedString;
import org.signal.storageservice.protos.groups.local.DecryptedTimer;
import org.signal.storageservice.protos.groups.local.EnabledState;
import org.whispersystems.signalservice.api.push.ServiceId;
import org.whispersystems.signalservice.api.util.UuidUtil;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
/**
* Contains operations to create, modify and validate groups and group changes.
*/
public final class GroupsV2Operations {
private static final String TAG = GroupsV2Operations.class.getSimpleName();
/** Used for undecryptable pending invites */
public static final UUID UNKNOWN_UUID = UuidUtil.UNKNOWN_UUID;
/** Highest change epoch this class knows now to decrypt */
public static final int HIGHEST_KNOWN_EPOCH = 5;
private final ServerPublicParams serverPublicParams;
private final ClientZkProfileOperations clientZkProfileOperations;
private final ClientZkAuthOperations clientZkAuthOperations;
private final int maxGroupSize;
private final SecureRandom random;
public GroupsV2Operations(ClientZkOperations clientZkOperations, int maxGroupSize) {
this.serverPublicParams = clientZkOperations.getServerPublicParams();
this.clientZkProfileOperations = clientZkOperations.getProfileOperations();
this.clientZkAuthOperations = clientZkOperations.getAuthOperations();
this.maxGroupSize = maxGroupSize;
this.random = new SecureRandom();
}
/**
* Creates a new group with the title and avatar.
*
* @param self You will be member 0 and the only admin.
* @param members Members must not contain self. Members will be non-admin members of the group.
*/
public NewGroup createNewGroup(final GroupSecretParams groupSecretParams,
final String title,
final Optional<byte[]> avatar,
final GroupCandidate self,
final Set<GroupCandidate> members,
final Member.Role memberRole,
final int disappearingMessageTimerSeconds)
{
if (members.contains(self)) {
throw new IllegalArgumentException("Members must not contain self");
}
final GroupOperations groupOperations = forGroup(groupSecretParams);
Group.Builder group = Group.newBuilder()
.setRevision(0)
.setPublicKey(ByteString.copyFrom(groupSecretParams.getPublicParams().serialize()))
.setTitle(groupOperations.encryptTitle(title))
.setDisappearingMessagesTimer(groupOperations.encryptTimer(disappearingMessageTimerSeconds))
.setAccessControl(AccessControl.newBuilder()
.setAttributes(AccessControl.AccessRequired.MEMBER)
.setMembers(AccessControl.AccessRequired.MEMBER));
group.addMembers(groupOperations.member(self.requireExpiringProfileKeyCredential(), Member.Role.ADMINISTRATOR));
for (GroupCandidate credential : members) {
ExpiringProfileKeyCredential expiringProfileKeyCredential = credential.getExpiringProfileKeyCredential().orElse(null);
if (expiringProfileKeyCredential != null) {
group.addMembers(groupOperations.member(expiringProfileKeyCredential, memberRole));
} else {
group.addPendingMembers(groupOperations.invitee(credential.getUuid(), memberRole));
}
}
return new NewGroup(groupSecretParams, group.build(), avatar);
}
public GroupOperations forGroup(final GroupSecretParams groupSecretParams) {
return new GroupOperations(groupSecretParams);
}
public ClientZkProfileOperations getProfileOperations() {
return clientZkProfileOperations;
}
public ClientZkAuthOperations getAuthOperations() {
return clientZkAuthOperations;
}
/**
* Operations on a single group.
*/
public final class GroupOperations {
private final GroupSecretParams groupSecretParams;
private final ClientZkGroupCipher clientZkGroupCipher;
private GroupOperations(GroupSecretParams groupSecretParams) {
this.groupSecretParams = groupSecretParams;
this.clientZkGroupCipher = new ClientZkGroupCipher(groupSecretParams);
}
public GroupChange.Actions.Builder createModifyGroupTitle(final String title) {
return GroupChange.Actions.newBuilder().setModifyTitle(GroupChange.Actions.ModifyTitleAction
.newBuilder()
.setTitle(encryptTitle(title)));
}
public GroupChange.Actions.ModifyDescriptionAction.Builder createModifyGroupDescriptionAction(final String description) {
return GroupChange.Actions.ModifyDescriptionAction.newBuilder().setDescription(encryptDescription(description));
}
public GroupChange.Actions.Builder createModifyGroupDescription(final String description) {
return GroupChange.Actions.newBuilder().setModifyDescription(createModifyGroupDescriptionAction(description));
}
public GroupChange.Actions.Builder createModifyGroupMembershipChange(Set<GroupCandidate> membersToAdd, Set<UUID> bannedMembers, UUID selfUuid) {
final GroupOperations groupOperations = forGroup(groupSecretParams);
Set<UUID> membersToUnban = membersToAdd.stream().map(GroupCandidate::getUuid).filter(bannedMembers::contains).collect(Collectors.toSet());
GroupChange.Actions.Builder actions = membersToUnban.isEmpty() ? GroupChange.Actions.newBuilder()
: createUnbanUuidsChange(membersToUnban);
for (GroupCandidate credential : membersToAdd) {
Member.Role newMemberRole = Member.Role.DEFAULT;
ExpiringProfileKeyCredential expiringProfileKeyCredential = credential.getExpiringProfileKeyCredential().orElse(null);
if (expiringProfileKeyCredential != null) {
actions.addAddMembers(GroupChange.Actions.AddMemberAction
.newBuilder()
.setAdded(groupOperations.member(expiringProfileKeyCredential, newMemberRole)));
} else {
actions.addAddPendingMembers(GroupChange.Actions.AddPendingMemberAction
.newBuilder()
.setAdded(groupOperations.invitee(credential.getUuid(), newMemberRole)
.setAddedByUserId(encryptUuid(selfUuid))));
}
}
return actions;
}
public GroupChange.Actions.Builder createGroupJoinRequest(ExpiringProfileKeyCredential expiringProfileKeyCredential) {
GroupOperations groupOperations = forGroup(groupSecretParams);
GroupChange.Actions.Builder actions = GroupChange.Actions.newBuilder();
actions.addAddRequestingMembers(GroupChange.Actions.AddRequestingMemberAction
.newBuilder()
.setAdded(groupOperations.requestingMember(expiringProfileKeyCredential)));
return actions;
}
public GroupChange.Actions.Builder createGroupJoinDirect(ExpiringProfileKeyCredential expiringProfileKeyCredential) {
GroupOperations groupOperations = forGroup(groupSecretParams);
GroupChange.Actions.Builder actions = GroupChange.Actions.newBuilder();
actions.addAddMembers(GroupChange.Actions.AddMemberAction
.newBuilder()
.setAdded(groupOperations.member(expiringProfileKeyCredential, Member.Role.DEFAULT)));
return actions;
}
public GroupChange.Actions.Builder createRefuseGroupJoinRequest(Set<UUID> requestsToRemove, boolean alsoBan, List<DecryptedBannedMember> bannedMembers) {
GroupChange.Actions.Builder actions = alsoBan ? createBanUuidsChange(requestsToRemove, false, bannedMembers)
: GroupChange.Actions.newBuilder();
for (UUID uuid : requestsToRemove) {
actions.addDeleteRequestingMembers(GroupChange.Actions.DeleteRequestingMemberAction
.newBuilder()
.setDeletedUserId(encryptUuid(uuid)));
}
return actions;
}
public GroupChange.Actions.Builder createApproveGroupJoinRequest(Set<UUID> requestsToApprove) {
GroupChange.Actions.Builder actions = GroupChange.Actions.newBuilder();
for (UUID uuid : requestsToApprove) {
actions.addPromoteRequestingMembers(GroupChange.Actions.PromoteRequestingMemberAction
.newBuilder()
.setRole(Member.Role.DEFAULT)
.setUserId(encryptUuid(uuid)));
}
return actions;
}
public GroupChange.Actions.Builder createRemoveMembersChange(final Set<UUID> membersToRemove, boolean alsoBan, List<DecryptedBannedMember> bannedMembers) {
GroupChange.Actions.Builder actions = alsoBan ? createBanUuidsChange(membersToRemove, false, bannedMembers)
: GroupChange.Actions.newBuilder();
for (UUID remove: membersToRemove) {
actions.addDeleteMembers(GroupChange.Actions.DeleteMemberAction
.newBuilder()
.setDeletedUserId(encryptUuid(remove)));
}
return actions;
}
public GroupChange.Actions.Builder createLeaveAndPromoteMembersToAdmin(UUID self, List<UUID> membersToMakeAdmin) {
GroupChange.Actions.Builder actions = createRemoveMembersChange(Collections.singleton(self), false, Collections.emptyList());
for (UUID member : membersToMakeAdmin) {
actions.addModifyMemberRoles(GroupChange.Actions.ModifyMemberRoleAction
.newBuilder()
.setUserId(encryptUuid(member))
.setRole(Member.Role.ADMINISTRATOR));
}
return actions;
}
public GroupChange.Actions.Builder createModifyGroupTimerChange(int timerDurationSeconds) {
return GroupChange.Actions
.newBuilder()
.setModifyDisappearingMessagesTimer(GroupChange.Actions.ModifyDisappearingMessagesTimerAction
.newBuilder()
.setTimer(encryptTimer(timerDurationSeconds)));
}
public GroupChange.Actions.Builder createUpdateProfileKeyCredentialChange(ExpiringProfileKeyCredential expiringProfileKeyCredential) {
ProfileKeyCredentialPresentation presentation = clientZkProfileOperations.createProfileKeyCredentialPresentation(random, groupSecretParams, expiringProfileKeyCredential);
return GroupChange.Actions
.newBuilder()
.addModifyMemberProfileKeys(GroupChange.Actions.ModifyMemberProfileKeyAction
.newBuilder()
.setPresentation(ByteString.copyFrom(presentation.serialize())));
}
public GroupChange.Actions.Builder createAcceptInviteChange(ExpiringProfileKeyCredential credential) {
ProfileKeyCredentialPresentation presentation = clientZkProfileOperations.createProfileKeyCredentialPresentation(random, groupSecretParams, credential);
return GroupChange.Actions.newBuilder()
.addPromotePendingMembers(GroupChange.Actions.PromotePendingMemberAction.newBuilder()
.setPresentation(ByteString.copyFrom(presentation.serialize())));
}
public GroupChange.Actions.Builder createAcceptPniInviteChange(ExpiringProfileKeyCredential credential) {
ByteString presentation = ByteString.copyFrom(clientZkProfileOperations.createProfileKeyCredentialPresentation(random, groupSecretParams, credential).serialize());
return GroupChange.Actions.newBuilder()
.addPromotePendingPniAciMembers(GroupChange.Actions.PromotePendingPniAciMemberProfileKeyAction.newBuilder()
.setPresentation(presentation));
}
public GroupChange.Actions.Builder createRemoveInvitationChange(final Set<UuidCiphertext> uuidCipherTextsFromInvitesToRemove) {
GroupChange.Actions.Builder builder = GroupChange.Actions
.newBuilder();
for (UuidCiphertext uuidCipherText: uuidCipherTextsFromInvitesToRemove) {
builder.addDeletePendingMembers(GroupChange.Actions.DeletePendingMemberAction
.newBuilder()
.setDeletedUserId(ByteString.copyFrom(uuidCipherText.serialize())));
}
return builder;
}
public GroupChange.Actions.Builder createModifyGroupLinkPasswordChange(byte[] groupLinkPassword) {
return GroupChange.Actions
.newBuilder()
.setModifyInviteLinkPassword(GroupChange.Actions.ModifyInviteLinkPasswordAction
.newBuilder()
.setInviteLinkPassword(ByteString.copyFrom(groupLinkPassword)));
}
public GroupChange.Actions.Builder createModifyGroupLinkPasswordAndRightsChange(byte[] groupLinkPassword, AccessControl.AccessRequired newRights) {
GroupChange.Actions.Builder change = createModifyGroupLinkPasswordChange(groupLinkPassword);
return change.setModifyAddFromInviteLinkAccess(GroupChange.Actions.ModifyAddFromInviteLinkAccessControlAction
.newBuilder()
.setAddFromInviteLinkAccess(newRights));
}
public GroupChange.Actions.Builder createChangeJoinByLinkRights(AccessControl.AccessRequired newRights) {
return GroupChange.Actions
.newBuilder()
.setModifyAddFromInviteLinkAccess(GroupChange.Actions.ModifyAddFromInviteLinkAccessControlAction
.newBuilder()
.setAddFromInviteLinkAccess(newRights));
}
public GroupChange.Actions.Builder createChangeMembershipRights(AccessControl.AccessRequired newRights) {
return GroupChange.Actions
.newBuilder()
.setModifyMemberAccess(GroupChange.Actions.ModifyMembersAccessControlAction
.newBuilder()
.setMembersAccess(newRights));
}
public GroupChange.Actions.Builder createChangeAttributesRights(AccessControl.AccessRequired newRights) {
return GroupChange.Actions
.newBuilder()
.setModifyAttributesAccess(GroupChange.Actions.ModifyAttributesAccessControlAction
.newBuilder()
.setAttributesAccess(newRights));
}
public GroupChange.Actions.Builder createAnnouncementGroupChange(boolean isAnnouncementGroup) {
return GroupChange.Actions
.newBuilder()
.setModifyAnnouncementsOnly(GroupChange.Actions.ModifyAnnouncementsOnlyAction
.newBuilder()
.setAnnouncementsOnly(isAnnouncementGroup));
}
public GroupChange.Actions.Builder createBanUuidsChange(Set<UUID> banUuids, boolean rejectJoinRequest, List<DecryptedBannedMember> bannedMembersList) {
GroupChange.Actions.Builder builder = rejectJoinRequest ? createRefuseGroupJoinRequest(banUuids, false, Collections.emptyList())
: GroupChange.Actions.newBuilder();
int spacesToFree = bannedMembersList.size() + banUuids.size() - maxGroupSize;
if (spacesToFree > 0) {
List<ByteString> unban = bannedMembersList.stream()
.sorted(Comparator.comparingLong(DecryptedBannedMember::getTimestamp))
.limit(spacesToFree)
.map(DecryptedBannedMember::getUuid)
.collect(Collectors.toList());
for (ByteString uuid : unban) {
builder.addDeleteBannedMembers(GroupChange.Actions.DeleteBannedMemberAction.newBuilder().setDeletedUserId(encryptUuid(UuidUtil.fromByteString(uuid))));
}
}
for (UUID uuid : banUuids) {
builder.addAddBannedMembers(GroupChange.Actions.AddBannedMemberAction.newBuilder().setAdded(BannedMember.newBuilder().setUserId(encryptUuid(uuid)).build()));
}
return builder;
}
public GroupChange.Actions.Builder createUnbanUuidsChange(Set<UUID> banUuids) {
GroupChange.Actions.Builder builder = GroupChange.Actions.newBuilder();
for (UUID uuid : banUuids) {
builder.addDeleteBannedMembers(GroupChange.Actions.DeleteBannedMemberAction.newBuilder().setDeletedUserId(encryptUuid(uuid)).build());
}
return builder;
}
public GroupChange.Actions.Builder replaceAddMembers(GroupChange.Actions.Builder change, List<GroupCandidate> candidates) throws InvalidInputException {
if (change.getAddMembersCount() != candidates.size()) {
throw new InvalidInputException("Replacement candidates not same size as original add");
}
for (int i = 0; i < change.getAddMembersCount(); i++) {
GroupChange.Actions.AddMemberAction original = change.getAddMembers(i);
GroupCandidate candidate = candidates.get(i);
ExpiringProfileKeyCredential expiringProfileKeyCredential = candidate.getExpiringProfileKeyCredential().orElse(null);
if (expiringProfileKeyCredential == null) {
throw new InvalidInputException("Replacement candidate missing credential");
}
change.setAddMembers(i,
GroupChange.Actions.AddMemberAction.newBuilder()
.setAdded(member(expiringProfileKeyCredential, original.getAdded().getRole())));
}
return change;
}
private Member.Builder member(ExpiringProfileKeyCredential credential, Member.Role role) {
ProfileKeyCredentialPresentation presentation = clientZkProfileOperations.createProfileKeyCredentialPresentation(new SecureRandom(), groupSecretParams, credential);
return Member.newBuilder()
.setRole(role)
.setPresentation(ByteString.copyFrom(presentation.serialize()));
}
private RequestingMember.Builder requestingMember(ExpiringProfileKeyCredential credential) {
ProfileKeyCredentialPresentation presentation = clientZkProfileOperations.createProfileKeyCredentialPresentation(new SecureRandom(), groupSecretParams, credential);
return RequestingMember.newBuilder()
.setPresentation(ByteString.copyFrom(presentation.serialize()));
}
public PendingMember.Builder invitee(UUID uuid, Member.Role role) {
UuidCiphertext uuidCiphertext = clientZkGroupCipher.encryptUuid(uuid);
Member member = Member.newBuilder()
.setRole(role)
.setUserId(ByteString.copyFrom(uuidCiphertext.serialize()))
.build();
return PendingMember.newBuilder()
.setMember(member);
}
public PartialDecryptedGroup partialDecryptGroup(Group group)
throws VerificationFailedException, InvalidGroupStateException
{
List<Member> membersList = group.getMembersList();
List<PendingMember> pendingMembersList = group.getPendingMembersList();
List<DecryptedMember> decryptedMembers = new ArrayList<>(membersList.size());
List<DecryptedPendingMember> decryptedPendingMembers = new ArrayList<>(pendingMembersList.size());
for (Member member : membersList) {
UUID memberUuid = decryptUuid(member.getUserId());
decryptedMembers.add(DecryptedMember.newBuilder()
.setUuid(UuidUtil.toByteString(memberUuid))
.setJoinedAtRevision(member.getJoinedAtRevision())
.build());
}
for (PendingMember member : pendingMembersList) {
UUID pendingMemberUuid = decryptUuidOrUnknown(member.getMember().getUserId());
decryptedPendingMembers.add(DecryptedPendingMember.newBuilder()
.setUuid(UuidUtil.toByteString(pendingMemberUuid))
.build());
}
DecryptedGroup decryptedGroup = DecryptedGroup.newBuilder()
.setRevision(group.getRevision())
.addAllMembers(decryptedMembers)
.addAllPendingMembers(decryptedPendingMembers)
.build();
return new PartialDecryptedGroup(group, decryptedGroup, GroupsV2Operations.this, groupSecretParams);
}
public DecryptedGroup decryptGroup(Group group)
throws VerificationFailedException, InvalidGroupStateException
{
List<Member> membersList = group.getMembersList();
List<PendingMember> pendingMembersList = group.getPendingMembersList();
List<RequestingMember> requestingMembersList = group.getRequestingMembersList();
List<DecryptedMember> decryptedMembers = new ArrayList<>(membersList.size());
List<DecryptedPendingMember> decryptedPendingMembers = new ArrayList<>(pendingMembersList.size());
List<DecryptedRequestingMember> decryptedRequestingMembers = new ArrayList<>(requestingMembersList.size());
List<DecryptedBannedMember> decryptedBannedMembers = new ArrayList<>(group.getBannedMembersCount());
for (Member member : membersList) {
try {
decryptedMembers.add(decryptMember(member).build());
} catch (InvalidInputException e) {
throw new InvalidGroupStateException(e);
}
}
for (PendingMember member : pendingMembersList) {
decryptedPendingMembers.add(decryptMember(member));
}
for (RequestingMember member : requestingMembersList) {
decryptedRequestingMembers.add(decryptRequestingMember(member));
}
for (BannedMember member : group.getBannedMembersList()) {
decryptedBannedMembers.add(DecryptedBannedMember.newBuilder().setUuid(decryptUuidToByteString(member.getUserId())).setTimestamp(member.getTimestamp()).build());
}
return DecryptedGroup.newBuilder()
.setTitle(decryptTitle(group.getTitle()))
.setDescription(decryptDescription(group.getDescription()))
.setIsAnnouncementGroup(group.getAnnouncementsOnly() ? EnabledState.ENABLED : EnabledState.DISABLED)
.setAvatar(group.getAvatar())
.setAccessControl(group.getAccessControl())
.setRevision(group.getRevision())
.addAllMembers(decryptedMembers)
.addAllPendingMembers(decryptedPendingMembers)
.addAllRequestingMembers(decryptedRequestingMembers)
.setDisappearingMessagesTimer(DecryptedTimer.newBuilder().setDuration(decryptDisappearingMessagesTimer(group.getDisappearingMessagesTimer())))
.setInviteLinkPassword(group.getInviteLinkPassword())
.addAllBannedMembers(decryptedBannedMembers)
.build();
}
/**
* @param verifySignature You might want to avoid verification if you already know it's correct, or you
* are not going to pass to other clients.
* <p>
* Also, if you know it's version 0, do not verify because changes for version 0
* are not signed, but should be empty.
* @return {@link Optional#empty()} if the epoch for the change is higher that this code can decrypt.
*/
public Optional<DecryptedGroupChange> decryptChange(GroupChange groupChange, boolean verifySignature)
throws InvalidProtocolBufferException, VerificationFailedException, InvalidGroupStateException
{
if (groupChange.getChangeEpoch() > HIGHEST_KNOWN_EPOCH) {
Log.w(TAG, String.format(Locale.US, "Ignoring change from Epoch %d. Highest known Epoch is %d", groupChange.getChangeEpoch(), HIGHEST_KNOWN_EPOCH));
return Optional.empty();
}
GroupChange.Actions actions = verifySignature ? getVerifiedActions(groupChange) : getActions(groupChange);
return Optional.of(decryptChange(actions));
}
public DecryptedGroupChange decryptChange(GroupChange.Actions actions)
throws VerificationFailedException, InvalidGroupStateException
{
return decryptChange(actions, null);
}
public DecryptedGroupChange decryptChange(GroupChange.Actions actions, UUID source)
throws VerificationFailedException, InvalidGroupStateException
{
DecryptedGroupChange.Builder builder = DecryptedGroupChange.newBuilder();
// Field 1
if (source != null) {
builder.setEditor(UuidUtil.toByteString(source));
} else {
builder.setEditor(decryptUuidToByteString(actions.getSourceUuid()));
}
// Field 2
builder.setRevision(actions.getRevision());
// Field 3
for (GroupChange.Actions.AddMemberAction addMemberAction : actions.getAddMembersList()) {
try {
builder.addNewMembers(decryptMember(addMemberAction.getAdded()).setJoinedAtRevision(actions.getRevision()));
} catch (InvalidInputException e) {
throw new InvalidGroupStateException(e);
}
}
// Field 4
for (GroupChange.Actions.DeleteMemberAction deleteMemberAction : actions.getDeleteMembersList()) {
builder.addDeleteMembers(decryptUuidToByteString(deleteMemberAction.getDeletedUserId()));
}
// Field 5
for (GroupChange.Actions.ModifyMemberRoleAction modifyMemberRoleAction : actions.getModifyMemberRolesList()) {
builder.addModifyMemberRoles(DecryptedModifyMemberRole.newBuilder()
.setRole(modifyMemberRoleAction.getRole())
.setUuid(decryptUuidToByteString(modifyMemberRoleAction.getUserId())));
}
// Field 6
for (GroupChange.Actions.ModifyMemberProfileKeyAction modifyMemberProfileKeyAction : actions.getModifyMemberProfileKeysList()) {
try {
UUID uuid;
ProfileKey profileKey;
if (modifyMemberProfileKeyAction.getUserId().isEmpty() || modifyMemberProfileKeyAction.getProfileKey().isEmpty()) {
ProfileKeyCredentialPresentation presentation = new ProfileKeyCredentialPresentation(modifyMemberProfileKeyAction.getPresentation().toByteArray());
uuid = decryptUuid(ByteString.copyFrom(presentation.getUuidCiphertext().serialize()));
profileKey = decryptProfileKey(ByteString.copyFrom(presentation.getProfileKeyCiphertext().serialize()), uuid);
} else {
uuid = decryptUuid(modifyMemberProfileKeyAction.getUserId());
profileKey = decryptProfileKey(modifyMemberProfileKeyAction.getProfileKey(), uuid);
}
builder.addModifiedProfileKeys(DecryptedMember.newBuilder()
.setRole(Member.Role.UNKNOWN)
.setJoinedAtRevision(-1)
.setUuid(UuidUtil.toByteString(uuid))
.setProfileKey(ByteString.copyFrom(profileKey.serialize())));
} catch (InvalidInputException e) {
throw new InvalidGroupStateException(e);
}
}
// Field 7
for (GroupChange.Actions.AddPendingMemberAction addPendingMemberAction : actions.getAddPendingMembersList()) {
PendingMember added = addPendingMemberAction.getAdded();
Member member = added.getMember();
ByteString uuidCipherText = member.getUserId();
UUID uuid = decryptUuidOrUnknown(uuidCipherText);
builder.addNewPendingMembers(DecryptedPendingMember.newBuilder()
.setUuid(UuidUtil.toByteString(uuid))
.setUuidCipherText(uuidCipherText)
.setRole(member.getRole())
.setAddedByUuid(decryptUuidToByteString(added.getAddedByUserId()))
.setTimestamp(added.getTimestamp()));
}
// Field 8
for (GroupChange.Actions.DeletePendingMemberAction deletePendingMemberAction : actions.getDeletePendingMembersList()) {
ByteString uuidCipherText = deletePendingMemberAction.getDeletedUserId();
UUID uuid = decryptUuidOrUnknown(uuidCipherText);
builder.addDeletePendingMembers(DecryptedPendingMemberRemoval.newBuilder()
.setUuid(UuidUtil.toByteString(uuid))
.setUuidCipherText(uuidCipherText));
}
// Field 9
for (GroupChange.Actions.PromotePendingMemberAction promotePendingMemberAction : actions.getPromotePendingMembersList()) {
try {
UUID uuid;
ProfileKey profileKey;
if (promotePendingMemberAction.getUserId().isEmpty() || promotePendingMemberAction.getProfileKey().isEmpty()) {
ProfileKeyCredentialPresentation presentation = new ProfileKeyCredentialPresentation(promotePendingMemberAction.getPresentation().toByteArray());
uuid = decryptUuid(ByteString.copyFrom(presentation.getUuidCiphertext().serialize()));
profileKey = decryptProfileKey(ByteString.copyFrom(presentation.getProfileKeyCiphertext().serialize()), uuid);
} else {
uuid = decryptUuid(promotePendingMemberAction.getUserId());
profileKey = decryptProfileKey(promotePendingMemberAction.getProfileKey(), uuid);
}
builder.addPromotePendingMembers(DecryptedMember.newBuilder()
.setJoinedAtRevision(-1)
.setRole(Member.Role.DEFAULT)
.setUuid(UuidUtil.toByteString(uuid))
.setProfileKey(ByteString.copyFrom(profileKey.serialize())));
} catch (InvalidInputException e) {
throw new InvalidGroupStateException(e);
}
}
// Field 10
if (actions.hasModifyTitle()) {
builder.setNewTitle(DecryptedString.newBuilder().setValue(decryptTitle(actions.getModifyTitle().getTitle())));
}
// Field 11
if (actions.hasModifyAvatar()) {
builder.setNewAvatar(DecryptedString.newBuilder().setValue(actions.getModifyAvatar().getAvatar()));
}
// Field 12
if (actions.hasModifyDisappearingMessagesTimer()) {
int duration = decryptDisappearingMessagesTimer(actions.getModifyDisappearingMessagesTimer().getTimer());
builder.setNewTimer(DecryptedTimer.newBuilder().setDuration(duration));
}
// Field 13
if (actions.hasModifyAttributesAccess()) {
builder.setNewAttributeAccess(actions.getModifyAttributesAccess().getAttributesAccess());
}
// Field 14
if (actions.hasModifyMemberAccess()) {
builder.setNewMemberAccess(actions.getModifyMemberAccess().getMembersAccess());
}
// Field 15
if (actions.hasModifyAddFromInviteLinkAccess()) {
builder.setNewInviteLinkAccess(actions.getModifyAddFromInviteLinkAccess().getAddFromInviteLinkAccess());
}
// Field 16
for (GroupChange.Actions.AddRequestingMemberAction request : actions.getAddRequestingMembersList()) {
builder.addNewRequestingMembers(decryptRequestingMember(request.getAdded()));
}
// Field 17
for (GroupChange.Actions.DeleteRequestingMemberAction delete : actions.getDeleteRequestingMembersList()) {
builder.addDeleteRequestingMembers(decryptUuidToByteString(delete.getDeletedUserId()));
}
// Field 18
for (GroupChange.Actions.PromoteRequestingMemberAction promote : actions.getPromoteRequestingMembersList()) {
builder.addPromoteRequestingMembers(DecryptedApproveMember.newBuilder().setRole(promote.getRole()).setUuid(decryptUuidToByteString(promote.getUserId())));
}
// Field 19
if (actions.hasModifyInviteLinkPassword()) {
builder.setNewInviteLinkPassword(actions.getModifyInviteLinkPassword().getInviteLinkPassword());
}
// Field 20
if (actions.hasModifyDescription()) {
builder.setNewDescription(DecryptedString.newBuilder().setValue(decryptDescription(actions.getModifyDescription().getDescription())));
}
// Field 21
if (actions.hasModifyAnnouncementsOnly()) {
builder.setNewIsAnnouncementGroup(actions.getModifyAnnouncementsOnly().getAnnouncementsOnly() ? EnabledState.ENABLED : EnabledState.DISABLED);
}
// Field 22
for (GroupChange.Actions.AddBannedMemberAction action : actions.getAddBannedMembersList()) {
builder.addNewBannedMembers(DecryptedBannedMember.newBuilder().setUuid(decryptUuidToByteString(action.getAdded().getUserId())).setTimestamp(action.getAdded().getTimestamp()).build());
}
// Field 23
for (GroupChange.Actions.DeleteBannedMemberAction action : actions.getDeleteBannedMembersList()) {
builder.addDeleteBannedMembers(DecryptedBannedMember.newBuilder().setUuid(decryptUuidToByteString(action.getDeletedUserId())).build());
}
// Field 24
for (GroupChange.Actions.PromotePendingPniAciMemberProfileKeyAction promotePendingPniAciMemberAction : actions.getPromotePendingPniAciMembersList()) {
UUID uuid = decryptUuid(promotePendingPniAciMemberAction.getUserId());
UUID pni = decryptUuid(promotePendingPniAciMemberAction.getPni());
ProfileKey profileKey = decryptProfileKey(promotePendingPniAciMemberAction.getProfileKey(), uuid);
builder.setEditor(UuidUtil.toByteString(uuid))
.addPromotePendingPniAciMembers(DecryptedMember.newBuilder()
.setUuid(UuidUtil.toByteString(uuid))
.setRole(Member.Role.DEFAULT)
.setProfileKey(ByteString.copyFrom(profileKey.serialize()))
.setJoinedAtRevision(actions.getRevision())
.setPni(UuidUtil.toByteString(pni)));
}
return builder.build();
}
public DecryptedGroupJoinInfo decryptGroupJoinInfo(GroupJoinInfo joinInfo) {
return DecryptedGroupJoinInfo.newBuilder()
.setTitle(decryptTitle(joinInfo.getTitle()))
.setAvatar(joinInfo.getAvatar())
.setMemberCount(joinInfo.getMemberCount())
.setAddFromInviteLink(joinInfo.getAddFromInviteLink())
.setRevision(joinInfo.getRevision())
.setPendingAdminApproval(joinInfo.getPendingAdminApproval())
.setDescription(decryptDescription(joinInfo.getDescription()))
.build();
}
private DecryptedMember.Builder decryptMember(Member member)
throws InvalidGroupStateException, VerificationFailedException, InvalidInputException
{
if (member.getPresentation().isEmpty()) {
UUID uuid = decryptUuid(member.getUserId());
return DecryptedMember.newBuilder()
.setUuid(UuidUtil.toByteString(uuid))
.setJoinedAtRevision(member.getJoinedAtRevision())
.setProfileKey(decryptProfileKeyToByteString(member.getProfileKey(), uuid))
.setRole(member.getRole());
} else {
ProfileKeyCredentialPresentation profileKeyCredentialPresentation = new ProfileKeyCredentialPresentation(member.getPresentation().toByteArray());
UUID uuid = clientZkGroupCipher.decryptUuid(profileKeyCredentialPresentation.getUuidCiphertext());
ProfileKey profileKey = clientZkGroupCipher.decryptProfileKey(profileKeyCredentialPresentation.getProfileKeyCiphertext(), uuid);
return DecryptedMember.newBuilder()
.setUuid(UuidUtil.toByteString(uuid))
.setJoinedAtRevision(member.getJoinedAtRevision())
.setProfileKey(ByteString.copyFrom(profileKey.serialize()))
.setRole(member.getRole());
}
}
private DecryptedPendingMember decryptMember(PendingMember member)
throws InvalidGroupStateException, VerificationFailedException
{
ByteString userIdCipherText = member.getMember().getUserId();
UUID uuid = decryptUuidOrUnknown(userIdCipherText);
UUID addedBy = decryptUuid(member.getAddedByUserId());
Member.Role role = member.getMember().getRole();
if (role != Member.Role.ADMINISTRATOR && role != Member.Role.DEFAULT) {
role = Member.Role.DEFAULT;
}
return DecryptedPendingMember.newBuilder()
.setUuid(UuidUtil.toByteString(uuid))
.setUuidCipherText(userIdCipherText)
.setAddedByUuid(UuidUtil.toByteString(addedBy))
.setRole(role)
.setTimestamp(member.getTimestamp())
.build();
}
private DecryptedRequestingMember decryptRequestingMember(RequestingMember member)
throws InvalidGroupStateException, VerificationFailedException
{
if (member.getPresentation().isEmpty()) {
UUID uuid = decryptUuid(member.getUserId());
return DecryptedRequestingMember.newBuilder()
.setUuid(UuidUtil.toByteString(uuid))
.setProfileKey(decryptProfileKeyToByteString(member.getProfileKey(), uuid))
.setTimestamp(member.getTimestamp())
.build();
} else {
ProfileKeyCredentialPresentation profileKeyCredentialPresentation;
try {
profileKeyCredentialPresentation = new ProfileKeyCredentialPresentation(member.getPresentation().toByteArray());
} catch (InvalidInputException e) {
throw new InvalidGroupStateException(e);
}
UUID uuid = clientZkGroupCipher.decryptUuid(profileKeyCredentialPresentation.getUuidCiphertext());
ProfileKey profileKey = clientZkGroupCipher.decryptProfileKey(profileKeyCredentialPresentation.getProfileKeyCiphertext(), uuid);
return DecryptedRequestingMember.newBuilder()
.setUuid(UuidUtil.toByteString(uuid))
.setProfileKey(ByteString.copyFrom(profileKey.serialize()))
.build();
}
}
private ProfileKey decryptProfileKey(ByteString profileKey, UUID uuid) throws VerificationFailedException, InvalidGroupStateException {
try {
ProfileKeyCiphertext profileKeyCiphertext = new ProfileKeyCiphertext(profileKey.toByteArray());
return clientZkGroupCipher.decryptProfileKey(profileKeyCiphertext, uuid);
} catch (InvalidInputException e) {
throw new InvalidGroupStateException(e);
}
}
private ByteString decryptProfileKeyToByteString(ByteString profileKey, UUID uuid) throws VerificationFailedException, InvalidGroupStateException {
return ByteString.copyFrom(decryptProfileKey(profileKey, uuid).serialize());
}
private ByteString decryptUuidToByteString(ByteString userId) throws InvalidGroupStateException, VerificationFailedException {
return UuidUtil.toByteString(decryptUuid(userId));
}
// Visible for Testing
public ByteString encryptUuid(UUID uuid) {
return ByteString.copyFrom(clientZkGroupCipher.encryptUuid(uuid).serialize());
}
private UUID decryptUuid(ByteString userId) throws InvalidGroupStateException, VerificationFailedException {
try {
return clientZkGroupCipher.decryptUuid(new UuidCiphertext(userId.toByteArray()));
} catch (InvalidInputException e) {
throw new InvalidGroupStateException(e);
}
}
/**
* Attempts to decrypt a UUID, but will return {@link #UNKNOWN_UUID} if it cannot.
*/
private UUID decryptUuidOrUnknown(ByteString userId) {
try {
return clientZkGroupCipher.decryptUuid(new UuidCiphertext(userId.toByteArray()));
} catch (InvalidInputException | VerificationFailedException e) {
return UNKNOWN_UUID;
}
}
ByteString encryptTitle(String title) {
try {
GroupAttributeBlob blob = GroupAttributeBlob.newBuilder().setTitle(title).build();
return ByteString.copyFrom(clientZkGroupCipher.encryptBlob(blob.toByteArray()));
} catch (VerificationFailedException e) {
throw new AssertionError(e);
}
}
private String decryptTitle(ByteString cipherText) {
return decryptBlob(cipherText).getTitle().trim();
}
ByteString encryptDescription(String description) {
try {
GroupAttributeBlob blob = GroupAttributeBlob.newBuilder().setDescription(description).build();
return ByteString.copyFrom(clientZkGroupCipher.encryptBlob(blob.toByteArray()));
} catch (VerificationFailedException e) {
throw new AssertionError(e);
}
}
private String decryptDescription(ByteString cipherText) {
return decryptBlob(cipherText).getDescription().trim();
}
private int decryptDisappearingMessagesTimer(ByteString encryptedTimerMessage) {
return decryptBlob(encryptedTimerMessage).getDisappearingMessagesDuration();
}
public byte[] decryptAvatar(byte[] bytes) {
return decryptBlob(bytes).getAvatar().toByteArray();
}
private GroupAttributeBlob decryptBlob(ByteString blob) {
return decryptBlob(blob.toByteArray());
}
private GroupAttributeBlob decryptBlob(byte[] bytes) {
// TODO GV2: Minimum field length checking should be responsibility of clientZkGroupCipher#decryptBlob
if (bytes == null || bytes.length == 0) {
return GroupAttributeBlob.getDefaultInstance();
}
if (bytes.length < 29) {
Log.w(TAG, "Bad encrypted blob length");
return GroupAttributeBlob.getDefaultInstance();
}
try {
return GroupAttributeBlob.parseFrom(clientZkGroupCipher.decryptBlob(bytes));
} catch (InvalidProtocolBufferException | VerificationFailedException e) {
Log.w(TAG, "Bad encrypted blob");
return GroupAttributeBlob.getDefaultInstance();
}
}
ByteString encryptTimer(int timerDurationSeconds) {
try {
GroupAttributeBlob timer = GroupAttributeBlob.newBuilder()
.setDisappearingMessagesDuration(timerDurationSeconds)
.build();
return ByteString.copyFrom(clientZkGroupCipher.encryptBlob(timer.toByteArray()));
} catch (VerificationFailedException e) {
throw new AssertionError(e);
}
}
/**
* Verifies signature and parses actions on a group change.
*/
private GroupChange.Actions getVerifiedActions(GroupChange groupChange)
throws VerificationFailedException, InvalidProtocolBufferException
{
byte[] actionsByteArray = groupChange.getActions().toByteArray();
NotarySignature signature;
try {
signature = new NotarySignature(groupChange.getServerSignature().toByteArray());
} catch (InvalidInputException e) {
throw new VerificationFailedException();
}
serverPublicParams.verifySignature(actionsByteArray, signature);
return GroupChange.Actions.parseFrom(actionsByteArray);
}
/**
* Parses actions on a group change without verification.
*/
private GroupChange.Actions getActions(GroupChange groupChange)
throws InvalidProtocolBufferException
{
return GroupChange.Actions.parseFrom(groupChange.getActions());
}
public GroupChange.Actions.Builder createChangeMemberRole(UUID uuid, Member.Role role) {
return GroupChange.Actions.newBuilder()
.addModifyMemberRoles(GroupChange.Actions.ModifyMemberRoleAction.newBuilder()
.setUserId(encryptUuid(uuid))
.setRole(role));
}
public List<ServiceId> decryptAddMembers(List<GroupChange.Actions.AddMemberAction> addMembers) throws InvalidInputException, VerificationFailedException {
List<ServiceId> ids = new ArrayList<>(addMembers.size());
for (int i = 0; i < addMembers.size(); i++) {
GroupChange.Actions.AddMemberAction addMember = addMembers.get(i);
ProfileKeyCredentialPresentation profileKeyCredentialPresentation = new ProfileKeyCredentialPresentation(addMember.getAdded().getPresentation().toByteArray());
ids.add(ServiceId.from(clientZkGroupCipher.decryptUuid(profileKeyCredentialPresentation.getUuidCiphertext())));
}
return ids;
}
}
public static class NewGroup {
private final GroupSecretParams groupSecretParams;
private final Group newGroupMessage;
private final Optional<byte[]> avatar;
private NewGroup(GroupSecretParams groupSecretParams, Group newGroupMessage, Optional<byte[]> avatar) {
this.groupSecretParams = groupSecretParams;
this.newGroupMessage = newGroupMessage;
this.avatar = avatar;
}
public GroupSecretParams getGroupSecretParams() {
return groupSecretParams;
}
public Group getNewGroupMessage() {
return newGroupMessage;
}
public Optional<byte[]> getAvatar() {
return avatar;
}
}
}