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

888 wiersze
43 KiB
Java

package org.whispersystems.signalservice.api.groupsv2;
import com.google.protobuf.ByteString;
import com.google.protobuf.InvalidProtocolBufferException;
import org.signal.storageservice.protos.groups.AccessControl;
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.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.signal.zkgroup.InvalidInputException;
import org.signal.zkgroup.NotarySignature;
import org.signal.zkgroup.ServerPublicParams;
import org.signal.zkgroup.VerificationFailedException;
import org.signal.zkgroup.auth.ClientZkAuthOperations;
import org.signal.zkgroup.groups.ClientZkGroupCipher;
import org.signal.zkgroup.groups.GroupSecretParams;
import org.signal.zkgroup.groups.ProfileKeyCiphertext;
import org.signal.zkgroup.groups.UuidCiphertext;
import org.signal.zkgroup.profiles.ClientZkProfileOperations;
import org.signal.zkgroup.profiles.ProfileKey;
import org.signal.zkgroup.profiles.ProfileKeyCredential;
import org.signal.zkgroup.profiles.ProfileKeyCredentialPresentation;
import org.whispersystems.libsignal.logging.Log;
import org.whispersystems.libsignal.util.guava.Optional;
import org.whispersystems.signalservice.api.util.UuidUtil;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.UUID;
/**
* 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 = 3;
private final ServerPublicParams serverPublicParams;
private final ClientZkProfileOperations clientZkProfileOperations;
private final ClientZkAuthOperations clientZkAuthOperations;
private final SecureRandom random;
public GroupsV2Operations(ClientZkOperations clientZkOperations) {
this.serverPublicParams = clientZkOperations.getServerPublicParams();
this.clientZkProfileOperations = clientZkOperations.getProfileOperations();
this.clientZkAuthOperations = clientZkOperations.getAuthOperations();
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.getProfileKeyCredential().get(), Member.Role.ADMINISTRATOR));
for (GroupCandidate credential : members) {
ProfileKeyCredential profileKeyCredential = credential.getProfileKeyCredential().orNull();
if (profileKeyCredential != null) {
group.addMembers(groupOperations.member(profileKeyCredential, 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, UUID selfUuid) {
final GroupOperations groupOperations = forGroup(groupSecretParams);
GroupChange.Actions.Builder actions = GroupChange.Actions.newBuilder();
for (GroupCandidate credential : membersToAdd) {
Member.Role newMemberRole = Member.Role.DEFAULT;
ProfileKeyCredential profileKeyCredential = credential.getProfileKeyCredential().orNull();
if (profileKeyCredential != null) {
actions.addAddMembers(GroupChange.Actions.AddMemberAction
.newBuilder()
.setAdded(groupOperations.member(profileKeyCredential, newMemberRole)));
} else {
actions.addAddPendingMembers(GroupChange.Actions.AddPendingMemberAction
.newBuilder()
.setAdded(groupOperations.invitee(credential.getUuid(), newMemberRole)
.setAddedByUserId(encryptUuid(selfUuid))));
}
}
return actions;
}
public GroupChange.Actions.Builder createGroupJoinRequest(ProfileKeyCredential profileKeyCredential) {
GroupOperations groupOperations = forGroup(groupSecretParams);
GroupChange.Actions.Builder actions = GroupChange.Actions.newBuilder();
actions.addAddRequestingMembers(GroupChange.Actions.AddRequestingMemberAction
.newBuilder()
.setAdded(groupOperations.requestingMember(profileKeyCredential)));
return actions;
}
public GroupChange.Actions.Builder createGroupJoinDirect(ProfileKeyCredential profileKeyCredential) {
GroupOperations groupOperations = forGroup(groupSecretParams);
GroupChange.Actions.Builder actions = GroupChange.Actions.newBuilder();
actions.addAddMembers(GroupChange.Actions.AddMemberAction
.newBuilder()
.setAdded(groupOperations.member(profileKeyCredential, Member.Role.DEFAULT)));
return actions;
}
public GroupChange.Actions.Builder createRefuseGroupJoinRequest(Set<UUID> requestsToRemove) {
GroupChange.Actions.Builder actions = 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) {
GroupChange.Actions.Builder actions = 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));
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(ProfileKeyCredential profileKeyCredential) {
ProfileKeyCredentialPresentation presentation = clientZkProfileOperations.createProfileKeyCredentialPresentation(random, groupSecretParams, profileKeyCredential);
return GroupChange.Actions
.newBuilder()
.addModifyMemberProfileKeys(GroupChange.Actions.ModifyMemberProfileKeyAction
.newBuilder()
.setPresentation(ByteString.copyFrom(presentation.serialize())));
}
public GroupChange.Actions.Builder createAcceptInviteChange(ProfileKeyCredential profileKeyCredential) {
ProfileKeyCredentialPresentation presentation = clientZkProfileOperations.createProfileKeyCredentialPresentation(random, groupSecretParams, profileKeyCredential);
return GroupChange.Actions
.newBuilder()
.addPromotePendingMembers(GroupChange.Actions.PromotePendingMemberAction
.newBuilder()
.setPresentation(ByteString.copyFrom(presentation.serialize())));
}
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));
}
private Member.Builder member(ProfileKeyCredential 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(ProfileKeyCredential 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());
decryptedMembers.add(DecryptedMember.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());
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));
}
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())
.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#absent} 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.absent();
}
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 {
ProfileKeyCredentialPresentation presentation = new ProfileKeyCredentialPresentation(modifyMemberProfileKeyAction.getPresentation().toByteArray());
presentation.getProfileKeyCiphertext();
UUID uuid = decryptUuid(ByteString.copyFrom(presentation.getUuidCiphertext().serialize()));
builder.addModifiedProfileKeys(DecryptedMember.newBuilder()
.setRole(Member.Role.UNKNOWN)
.setJoinedAtRevision(-1)
.setUuid(UuidUtil.toByteString(uuid))
.setProfileKey(ByteString.copyFrom(decryptProfileKey(ByteString.copyFrom(presentation.getProfileKeyCiphertext().serialize()), uuid).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()) {
ProfileKeyCredentialPresentation profileKeyCredentialPresentation;
try {
profileKeyCredentialPresentation = new ProfileKeyCredentialPresentation(promotePendingMemberAction.getPresentation().toByteArray());
} catch (InvalidInputException e) {
throw new InvalidGroupStateException(e);
}
UUID uuid = clientZkGroupCipher.decryptUuid(profileKeyCredentialPresentation.getUuidCiphertext());
ProfileKey profileKey = clientZkGroupCipher.decryptProfileKey(profileKeyCredentialPresentation.getProfileKeyCiphertext(), uuid);
builder.addPromotePendingMembers(DecryptedMember.newBuilder()
.setJoinedAtRevision(-1)
.setRole(Member.Role.DEFAULT)
.setUuid(UuidUtil.toByteString(uuid))
.setProfileKey(ByteString.copyFrom(profileKey.serialize())));
}
// 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);
}
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 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;
}
}
}