mirror of
https://github.com/AsamK/signal-cli.git
synced 2026-08-25 05:26:03 +00:00
Implement group send endorsements
This commit is contained in:
parent
b2076cfc12
commit
f469a4d166
@ -31,8 +31,8 @@ import org.asamk.signal.manager.util.Utils;
|
||||
import org.signal.libsignal.zkgroup.InvalidInputException;
|
||||
import org.signal.libsignal.zkgroup.groups.GroupMasterKey;
|
||||
import org.signal.libsignal.zkgroup.groups.GroupSecretParams;
|
||||
import org.signal.libsignal.zkgroup.groupsend.GroupSendEndorsementsResponse;
|
||||
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
|
||||
import org.signal.storageservice.protos.groups.GroupChange;
|
||||
import org.signal.storageservice.protos.groups.GroupChangeResponse;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroupChange;
|
||||
@ -42,7 +42,6 @@ import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupChangeLog;
|
||||
import org.whispersystems.signalservice.api.groupsv2.DecryptedGroupResponse;
|
||||
import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException;
|
||||
import org.whispersystems.signalservice.api.groupsv2.ReceivedGroupSendEndorsements;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
||||
@ -59,9 +58,11 @@ import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class GroupHelper {
|
||||
|
||||
@ -145,8 +146,9 @@ public class GroupHelper {
|
||||
final var response = context.getGroupV2Helper().getDecryptedGroup(groupSecretParams);
|
||||
|
||||
if (response != null) {
|
||||
group = handleDecryptedGroupResponse(groupInfoV2, response);
|
||||
group = response.getGroup();
|
||||
storeProfileKeysFromHistory(groupSecretParams, groupInfoV2, group);
|
||||
handleDecryptedGroupResponse(groupInfoV2, response);
|
||||
}
|
||||
} catch (NotAGroupMemberException ignored) {
|
||||
}
|
||||
@ -166,35 +168,53 @@ public class GroupHelper {
|
||||
return groupInfoV2;
|
||||
}
|
||||
|
||||
private DecryptedGroup handleDecryptedGroupResponse(
|
||||
private void handleDecryptedGroupResponse(
|
||||
GroupInfoV2 groupInfoV2,
|
||||
final DecryptedGroupResponse decryptedGroupResponse
|
||||
) {
|
||||
final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey());
|
||||
ReceivedGroupSendEndorsements groupSendEndorsements = dependencies.getGroupsV2Operations()
|
||||
.forGroup(groupSecretParams)
|
||||
.receiveGroupSendEndorsements(account.getAci(),
|
||||
decryptedGroupResponse.getGroup(),
|
||||
decryptedGroupResponse.getGroupSendEndorsementsResponse());
|
||||
|
||||
// TODO save group endorsements
|
||||
|
||||
return decryptedGroupResponse.getGroup();
|
||||
updateGroupEndorsements(groupInfoV2.getGroupId(),
|
||||
groupInfoV2.getMasterKey(),
|
||||
decryptedGroupResponse.getGroup(),
|
||||
decryptedGroupResponse.getGroupSendEndorsementsResponse());
|
||||
}
|
||||
|
||||
private GroupChange handleGroupChangeResponse(
|
||||
private void handleGroupChangeResponse(
|
||||
final GroupInfoV2 groupInfoV2,
|
||||
final GroupChangeResponse groupChangeResponse
|
||||
) {
|
||||
ReceivedGroupSendEndorsements groupSendEndorsements = dependencies.getGroupsV2Operations()
|
||||
.forGroup(GroupSecretParams.deriveFromMasterKey(groupInfoV2.getMasterKey()))
|
||||
.receiveGroupSendEndorsements(account.getAci(),
|
||||
if (groupChangeResponse.groupSendEndorsementsResponse.size() > 0) {
|
||||
try {
|
||||
final var groupSendEndorsementsResponse = new GroupSendEndorsementsResponse(groupChangeResponse.groupSendEndorsementsResponse.toByteArray());
|
||||
|
||||
updateGroupEndorsements(groupInfoV2.getGroupId(),
|
||||
groupInfoV2.getMasterKey(),
|
||||
groupInfoV2.getGroup(),
|
||||
groupChangeResponse.groupSendEndorsementsResponse);
|
||||
groupSendEndorsementsResponse);
|
||||
} catch (InvalidInputException e) {
|
||||
logger.debug("Unable to parse send endorsements response", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO save group endorsements
|
||||
private void updateGroupEndorsements(
|
||||
final GroupIdV2 groupId,
|
||||
final GroupMasterKey masterKey,
|
||||
final DecryptedGroup group,
|
||||
final GroupSendEndorsementsResponse groupSendEndorsementsResponse
|
||||
) {
|
||||
final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(masterKey);
|
||||
final var groupEndorsements = dependencies.getGroupsV2Operations()
|
||||
.forGroup(groupSecretParams)
|
||||
.receiveGroupSendEndorsements(account.getAci(), group, groupSendEndorsementsResponse);
|
||||
|
||||
return groupChangeResponse.groupChange;
|
||||
if (groupEndorsements != null) {
|
||||
final var endorsements = groupEndorsements.getEndorsements()
|
||||
.entrySet()
|
||||
.stream()
|
||||
.collect(Collectors.toMap(entry -> account.getRecipientResolver().resolveRecipient(entry.getKey()),
|
||||
Map.Entry::getValue));
|
||||
account.getGroupStore().updateGroupEndorsements(groupId, groupEndorsements.getExpirationMs(), endorsements);
|
||||
}
|
||||
}
|
||||
|
||||
public Pair<GroupId, SendGroupMessageResults> createGroup(
|
||||
@ -224,7 +244,9 @@ public class GroupHelper {
|
||||
final var gv2 = gv2Pair.first();
|
||||
final var decryptedGroup = gv2Pair.second();
|
||||
|
||||
gv2.setGroup(handleDecryptedGroupResponse(gv2, decryptedGroup));
|
||||
gv2.setGroup(decryptedGroup.getGroup());
|
||||
handleDecryptedGroupResponse(gv2, decryptedGroup);
|
||||
|
||||
gv2.setProfileSharingEnabled(true);
|
||||
if (avatarBytes != null) {
|
||||
context.getAvatarStore()
|
||||
@ -329,9 +351,7 @@ public class GroupHelper {
|
||||
groupChangePair = context.getGroupV2Helper().updateSelfProfileKey(groupInfoV2);
|
||||
}
|
||||
if (groupChangePair != null) {
|
||||
sendUpdateGroupV2Message(groupInfoV2,
|
||||
groupChangePair.first(),
|
||||
handleGroupChangeResponse(groupInfoV2, groupChangePair.second()));
|
||||
sendUpdateGroupV2Message(groupInfoV2, groupChangePair.first(), groupChangePair.second());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -354,14 +374,13 @@ public class GroupHelper {
|
||||
final var group = getOrMigrateGroup(inviteLinkUrl.getGroupMasterKey(),
|
||||
groupJoinInfo.revision + 1,
|
||||
changeResponse.groupChange == null ? null : changeResponse.groupChange.encode());
|
||||
final var groupChange = handleGroupChangeResponse(group, changeResponse);
|
||||
|
||||
if (group.getGroup() == null) {
|
||||
// Only requested member, can't send update to group members
|
||||
return new Pair<>(group.getGroupId(), new SendGroupMessageResults(0, List.of()));
|
||||
}
|
||||
|
||||
final var result = sendUpdateGroupV2Message(group, group.getGroup(), groupChange);
|
||||
final var result = sendUpdateGroupV2Message(group, group.getGroup(), changeResponse);
|
||||
|
||||
context.getJobExecutor().enqueueJob(new SyncStorageJob());
|
||||
return new Pair<>(group.getGroupId(), result);
|
||||
@ -454,7 +473,8 @@ public class GroupHelper {
|
||||
if (response == null) {
|
||||
return;
|
||||
}
|
||||
decryptedGroup = handleDecryptedGroupResponse(groupInfoV2, response);
|
||||
decryptedGroup = response.getGroup();
|
||||
handleDecryptedGroupResponse(groupInfoV2, response);
|
||||
} catch (NotAGroupMemberException e) {
|
||||
groupInfoV2.setPermissionDenied(true);
|
||||
account.getGroupStore().updateGroup(group);
|
||||
@ -544,7 +564,8 @@ public class GroupHelper {
|
||||
) throws NotAGroupMemberException {
|
||||
final var revisionWeWereAdded = context.getGroupV2Helper().findRevisionWeWereAdded(newDecryptedGroup);
|
||||
final var localRevision = localGroup.getGroup() == null ? 0 : localGroup.getGroup().revision;
|
||||
final var sendEndorsementsExpirationMs = 0L;// TODO store expiration localGroup.getGroup() == null ? 0 : localGroup.getGroup().revision;
|
||||
final var sendEndorsementsExpirationMs = account.getGroupStore()
|
||||
.getGroupEndorsementExpirationMs(localGroup.getGroupId());
|
||||
var fromRevision = Math.max(revisionWeWereAdded, localRevision);
|
||||
final var newProfileKeys = new HashMap<RecipientId, ProfileKey>();
|
||||
while (true) {
|
||||
@ -666,9 +687,7 @@ public class GroupHelper {
|
||||
final var groupV2Helper = context.getGroupV2Helper();
|
||||
if (group.isPendingMember(account.getSelfRecipientId())) {
|
||||
var groupGroupChangePair = groupV2Helper.acceptInvite(group);
|
||||
result = sendUpdateGroupV2Message(group,
|
||||
groupGroupChangePair.first(),
|
||||
handleGroupChangeResponse(group, groupGroupChangePair.second()));
|
||||
result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
|
||||
}
|
||||
|
||||
if (members != null) {
|
||||
@ -676,18 +695,14 @@ public class GroupHelper {
|
||||
requestingMembers.retainAll(group.getRequestingMembers());
|
||||
if (!requestingMembers.isEmpty()) {
|
||||
var groupGroupChangePair = groupV2Helper.approveJoinRequestMembers(group, requestingMembers);
|
||||
result = sendUpdateGroupV2Message(group,
|
||||
groupGroupChangePair.first(),
|
||||
handleGroupChangeResponse(group, groupGroupChangePair.second()));
|
||||
result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
|
||||
}
|
||||
final var newMembers = new HashSet<>(members);
|
||||
newMembers.removeAll(group.getMembers());
|
||||
newMembers.removeAll(group.getRequestingMembers());
|
||||
if (!newMembers.isEmpty()) {
|
||||
var groupGroupChangePair = groupV2Helper.addMembers(group, newMembers);
|
||||
result = sendUpdateGroupV2Message(group,
|
||||
groupGroupChangePair.first(),
|
||||
handleGroupChangeResponse(group, groupGroupChangePair.second()));
|
||||
result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
|
||||
}
|
||||
}
|
||||
|
||||
@ -703,26 +718,20 @@ public class GroupHelper {
|
||||
existingRemoveMembers.remove(account.getSelfRecipientId());// self can be removed with sendQuitGroupMessage
|
||||
if (!existingRemoveMembers.isEmpty()) {
|
||||
var groupGroupChangePair = groupV2Helper.removeMembers(group, existingRemoveMembers);
|
||||
result = sendUpdateGroupV2Message(group,
|
||||
groupGroupChangePair.first(),
|
||||
handleGroupChangeResponse(group, groupGroupChangePair.second()));
|
||||
result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
|
||||
}
|
||||
|
||||
var pendingRemoveMembers = new HashSet<>(removeMembers);
|
||||
pendingRemoveMembers.retainAll(group.getPendingMembers());
|
||||
if (!pendingRemoveMembers.isEmpty()) {
|
||||
var groupGroupChangePair = groupV2Helper.revokeInvitedMembers(group, pendingRemoveMembers);
|
||||
result = sendUpdateGroupV2Message(group,
|
||||
groupGroupChangePair.first(),
|
||||
handleGroupChangeResponse(group, groupGroupChangePair.second()));
|
||||
result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
|
||||
}
|
||||
var requestingRemoveMembers = new HashSet<>(removeMembers);
|
||||
requestingRemoveMembers.retainAll(group.getRequestingMembers());
|
||||
if (!requestingRemoveMembers.isEmpty()) {
|
||||
var groupGroupChangePair = groupV2Helper.refuseJoinRequestMembers(group, requestingRemoveMembers);
|
||||
result = sendUpdateGroupV2Message(group,
|
||||
groupGroupChangePair.first(),
|
||||
handleGroupChangeResponse(group, groupGroupChangePair.second()));
|
||||
result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
|
||||
}
|
||||
}
|
||||
|
||||
@ -735,7 +744,7 @@ public class GroupHelper {
|
||||
var groupGroupChangePair = groupV2Helper.setMemberAdmin(group, admin, true);
|
||||
result = sendUpdateGroupV2Message(group,
|
||||
groupGroupChangePair.first(),
|
||||
handleGroupChangeResponse(group, groupGroupChangePair.second()));
|
||||
groupGroupChangePair.second());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -748,7 +757,7 @@ public class GroupHelper {
|
||||
var groupGroupChangePair = groupV2Helper.setMemberAdmin(group, admin, false);
|
||||
result = sendUpdateGroupV2Message(group,
|
||||
groupGroupChangePair.first(),
|
||||
handleGroupChangeResponse(group, groupGroupChangePair.second()));
|
||||
groupGroupChangePair.second());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -758,9 +767,7 @@ public class GroupHelper {
|
||||
newlyBannedMembers.removeAll(group.getBannedMembers());
|
||||
if (!newlyBannedMembers.isEmpty()) {
|
||||
var groupGroupChangePair = groupV2Helper.banMembers(group, newlyBannedMembers);
|
||||
result = sendUpdateGroupV2Message(group,
|
||||
groupGroupChangePair.first(),
|
||||
handleGroupChangeResponse(group, groupGroupChangePair.second()));
|
||||
result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
|
||||
}
|
||||
}
|
||||
|
||||
@ -769,52 +776,38 @@ public class GroupHelper {
|
||||
existingUnbanMembers.retainAll(group.getBannedMembers());
|
||||
if (!existingUnbanMembers.isEmpty()) {
|
||||
var groupGroupChangePair = groupV2Helper.unbanMembers(group, existingUnbanMembers);
|
||||
result = sendUpdateGroupV2Message(group,
|
||||
groupGroupChangePair.first(),
|
||||
handleGroupChangeResponse(group, groupGroupChangePair.second()));
|
||||
result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
|
||||
}
|
||||
}
|
||||
|
||||
if (resetGroupLink) {
|
||||
var groupGroupChangePair = groupV2Helper.resetGroupLinkPassword(group);
|
||||
result = sendUpdateGroupV2Message(group,
|
||||
groupGroupChangePair.first(),
|
||||
handleGroupChangeResponse(group, groupGroupChangePair.second()));
|
||||
result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
|
||||
}
|
||||
|
||||
if (groupLinkState != null) {
|
||||
var groupGroupChangePair = groupV2Helper.setGroupLinkState(group, groupLinkState);
|
||||
result = sendUpdateGroupV2Message(group,
|
||||
groupGroupChangePair.first(),
|
||||
handleGroupChangeResponse(group, groupGroupChangePair.second()));
|
||||
result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
|
||||
}
|
||||
|
||||
if (addMemberPermission != null) {
|
||||
var groupGroupChangePair = groupV2Helper.setAddMemberPermission(group, addMemberPermission);
|
||||
result = sendUpdateGroupV2Message(group,
|
||||
groupGroupChangePair.first(),
|
||||
handleGroupChangeResponse(group, groupGroupChangePair.second()));
|
||||
result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
|
||||
}
|
||||
|
||||
if (editDetailsPermission != null) {
|
||||
var groupGroupChangePair = groupV2Helper.setEditDetailsPermission(group, editDetailsPermission);
|
||||
result = sendUpdateGroupV2Message(group,
|
||||
groupGroupChangePair.first(),
|
||||
handleGroupChangeResponse(group, groupGroupChangePair.second()));
|
||||
result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
|
||||
}
|
||||
|
||||
if (expirationTimer != null) {
|
||||
var groupGroupChangePair = groupV2Helper.setMessageExpirationTimer(group, expirationTimer);
|
||||
result = sendUpdateGroupV2Message(group,
|
||||
groupGroupChangePair.first(),
|
||||
handleGroupChangeResponse(group, groupGroupChangePair.second()));
|
||||
result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
|
||||
}
|
||||
|
||||
if (isAnnouncementGroup != null) {
|
||||
var groupGroupChangePair = groupV2Helper.setIsAnnouncementGroup(group, isAnnouncementGroup);
|
||||
result = sendUpdateGroupV2Message(group,
|
||||
groupGroupChangePair.first(),
|
||||
handleGroupChangeResponse(group, groupGroupChangePair.second()));
|
||||
result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
|
||||
}
|
||||
|
||||
if (name != null || description != null || avatarFile != null) {
|
||||
@ -823,9 +816,7 @@ public class GroupHelper {
|
||||
context.getAvatarStore()
|
||||
.storeGroupAvatar(group.getGroupId(), outputStream -> outputStream.write(avatarFile));
|
||||
}
|
||||
result = sendUpdateGroupV2Message(group,
|
||||
groupGroupChangePair.first(),
|
||||
handleGroupChangeResponse(group, groupGroupChangePair.second()));
|
||||
result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
|
||||
}
|
||||
|
||||
return result;
|
||||
@ -862,8 +853,12 @@ public class GroupHelper {
|
||||
groupInfoV2.setGroup(groupGroupChangePair.first());
|
||||
account.getGroupStore().updateGroup(groupInfoV2);
|
||||
|
||||
var messageBuilder = getGroupUpdateMessageBuilder(groupInfoV2,
|
||||
handleGroupChangeResponse(groupInfoV2, groupGroupChangePair.second()).encode());
|
||||
final var groupChangeResponse = groupGroupChangePair.second();
|
||||
handleGroupChangeResponse(groupInfoV2, groupChangeResponse);
|
||||
if (groupChangeResponse.groupChange == null) {
|
||||
throw new AssertionError("groupChange is null");
|
||||
}
|
||||
var messageBuilder = getGroupUpdateMessageBuilder(groupInfoV2, groupChangeResponse.groupChange.encode());
|
||||
return sendGroupMessage(messageBuilder,
|
||||
groupInfoV2.getMembersIncludingPendingWithout(account.getSelfRecipientId()),
|
||||
groupInfoV2);
|
||||
@ -902,15 +897,19 @@ public class GroupHelper {
|
||||
private SendGroupMessageResults sendUpdateGroupV2Message(
|
||||
GroupInfoV2 group,
|
||||
DecryptedGroup newDecryptedGroup,
|
||||
GroupChange groupChange
|
||||
GroupChangeResponse groupChangeResponse
|
||||
) throws IOException {
|
||||
final var selfRecipientId = account.getSelfRecipientId();
|
||||
final var members = group.getMembersIncludingPendingWithout(selfRecipientId);
|
||||
group.setGroup(newDecryptedGroup);
|
||||
handleGroupChangeResponse(group, groupChangeResponse);
|
||||
members.addAll(group.getMembersIncludingPendingWithout(selfRecipientId));
|
||||
account.getGroupStore().updateGroup(group);
|
||||
|
||||
final var messageBuilder = getGroupUpdateMessageBuilder(group, groupChange.encode());
|
||||
if (groupChangeResponse.groupChange == null) {
|
||||
throw new AssertionError("groupChange is null");
|
||||
}
|
||||
final var messageBuilder = getGroupUpdateMessageBuilder(group, groupChangeResponse.groupChange.encode());
|
||||
return sendGroupMessage(messageBuilder, members, group);
|
||||
}
|
||||
|
||||
|
||||
@ -5,20 +5,23 @@ import org.asamk.signal.manager.api.GroupId;
|
||||
import org.asamk.signal.manager.api.GroupNotFoundException;
|
||||
import org.asamk.signal.manager.api.GroupSendingNotAllowedException;
|
||||
import org.asamk.signal.manager.api.NotAGroupMemberException;
|
||||
import org.asamk.signal.manager.api.Pair;
|
||||
import org.asamk.signal.manager.api.Profile;
|
||||
import org.asamk.signal.manager.api.UnregisteredRecipientException;
|
||||
import org.asamk.signal.manager.groups.GroupUtils;
|
||||
import org.asamk.signal.manager.internal.SignalDependencies;
|
||||
import org.asamk.signal.manager.storage.SignalAccount;
|
||||
import org.asamk.signal.manager.storage.groups.GroupInfo;
|
||||
import org.asamk.signal.manager.storage.groups.GroupInfoV2;
|
||||
import org.asamk.signal.manager.storage.recipients.RecipientId;
|
||||
import org.asamk.signal.manager.storage.sendLog.MessageSendLogEntry;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.signal.libsignal.protocol.InvalidKeyException;
|
||||
import org.signal.libsignal.protocol.InvalidRegistrationIdException;
|
||||
import org.signal.libsignal.protocol.NoSessionException;
|
||||
import org.signal.libsignal.protocol.SignalProtocolAddress;
|
||||
import org.signal.libsignal.protocol.message.DecryptionErrorMessage;
|
||||
import org.signal.libsignal.zkgroup.groups.GroupSecretParams;
|
||||
import org.signal.libsignal.zkgroup.groupsend.GroupSendEndorsement;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
|
||||
@ -35,6 +38,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage;
|
||||
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
|
||||
import org.whispersystems.signalservice.api.push.DistributionId;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId.ACI;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.NotFoundException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.ProofRequiredException;
|
||||
@ -45,6 +49,7 @@ import org.whispersystems.signalservice.internal.push.http.PartialSendCompleteLi
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
@ -479,12 +484,18 @@ public class SendHelper {
|
||||
final var addressesMap = recipientIds.stream()
|
||||
.collect(Collectors.toMap(id -> id, context.getRecipientHelper()::resolveSignalServiceAddress));
|
||||
final var unidentifiedAccessesMap = context.getUnidentifiedAccessHelper().getAccessFor(recipientIds);
|
||||
final GroupSendEndorsements groupSendEndorsements = null; //TODO
|
||||
final var groupSendEndorsements = getGroupSendEndorsements(groupInfo);
|
||||
final var groupSecretParams = groupInfo instanceof GroupInfoV2 gv2
|
||||
? GroupSecretParams.deriveFromMasterKey((gv2.getMasterKey()))
|
||||
: null;
|
||||
|
||||
Set<RecipientId> senderKeyTargets = groupInfo.getDistributionId() == null
|
||||
Set<RecipientId> senderKeyTargets = groupInfo.getDistributionId() == null || groupSendEndorsements == null
|
||||
? Set.of()
|
||||
: recipientIds.stream()
|
||||
.filter(s -> this.isSenderKeyCapable(addressesMap.get(s), unidentifiedAccessesMap.get(s)))
|
||||
.filter(s -> this.isSenderKeyCapable(s,
|
||||
addressesMap.get(s),
|
||||
unidentifiedAccessesMap.get(s),
|
||||
groupSendEndorsements.second()))
|
||||
.collect(Collectors.toSet());
|
||||
if (senderKeyTargets.size() < 2) {
|
||||
logger.debug("Too few sender-key-capable users ({}). Doing all legacy sends.", senderKeyTargets.size());
|
||||
@ -495,11 +506,23 @@ public class SendHelper {
|
||||
|
||||
final var allResults = new ArrayList<SendMessageResult>(recipientIds.size());
|
||||
if (!senderKeyTargets.isEmpty()) {
|
||||
final var senderCertificate = this.context.getUnidentifiedAccessHelper().getSenderCertificateFor(null);
|
||||
final var addresses = senderKeyTargets.stream().map(addressesMap::get).toList();
|
||||
final var targets = senderKeyTargets;
|
||||
final var requiredGroupSendEndorsements = new GroupSendEndorsements(groupSendEndorsements.first(),
|
||||
groupSendEndorsements.second()
|
||||
.entrySet()
|
||||
.stream()
|
||||
.filter(entry -> targets.contains(entry.getKey()))
|
||||
.collect(Collectors.toMap(entry -> (ACI) addressesMap.get(entry.getKey()).getServiceId(),
|
||||
Map.Entry::getValue)),
|
||||
senderCertificate,
|
||||
groupSecretParams);
|
||||
final var results = sendGroupMessageInternalWithSenderKey(senderKeySender,
|
||||
groupInfo.getDistributionId(),
|
||||
senderKeyTargets.stream().map(addressesMap::get).toList(),
|
||||
addresses,
|
||||
senderKeyTargets.stream().map(unidentifiedAccessesMap::get).toList(),
|
||||
groupSendEndorsements,
|
||||
requiredGroupSendEndorsements,
|
||||
isRecipientUpdate);
|
||||
|
||||
if (results == null) {
|
||||
@ -536,8 +559,16 @@ public class SendHelper {
|
||||
.findFirst()
|
||||
.map(UnidentifiedAccess::getUnidentifiedCertificate)
|
||||
.orElse(null);
|
||||
final var groupSendTokens = groupSendEndorsements != null
|
||||
? groupSendEndorsements.forIndividuals(addresses)
|
||||
final var groupSendTokens = groupSendEndorsements != null ? groupSendEndorsements.second()
|
||||
.entrySet()
|
||||
.stream()
|
||||
.filter(entry -> legacyTargets.contains(entry.getKey()))
|
||||
.map(Map.Entry::getValue)
|
||||
.map(e -> e == null || groupSecretParams == null
|
||||
? null
|
||||
: e.toFullToken(groupSecretParams, Instant.ofEpochMilli(groupSendEndorsements.first())))
|
||||
.toList()
|
||||
|
||||
: null;
|
||||
final var sealedSenderAccesses = SealedSenderAccess.forFanOutGroupSend(groupSendTokens,
|
||||
senderCertificate,
|
||||
@ -553,16 +584,45 @@ public class SendHelper {
|
||||
return allResults;
|
||||
}
|
||||
|
||||
private boolean isSenderKeyCapable(final SignalServiceAddress address, final UnidentifiedAccess access) {
|
||||
if (access == null) {
|
||||
private Pair<Long, Map<RecipientId, GroupSendEndorsement>> getGroupSendEndorsements(final GroupInfo groupInfo) {
|
||||
if (!(groupInfo instanceof GroupInfoV2 groupInfoV2)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var groupSendEndorsementMap = account.getGroupStore().getGroupEndorsements(groupInfoV2.getGroupId());
|
||||
var groupSendEndorsementExpirationMs = account.getGroupStore()
|
||||
.getGroupEndorsementExpirationMs(groupInfoV2.getGroupId());
|
||||
if (groupSendEndorsementMap.isEmpty()
|
||||
|| groupSendEndorsementExpirationMs - TimeUnit.HOURS.toMillis(2) < System.currentTimeMillis()) {
|
||||
logger.debug("No group send endorsements available, trying to update");
|
||||
this.context.getGroupHelper().updateGroupSendEndorsements(groupInfoV2.getGroupId());
|
||||
groupSendEndorsementMap = account.getGroupStore().getGroupEndorsements(groupInfoV2.getGroupId());
|
||||
groupSendEndorsementExpirationMs = account.getGroupStore()
|
||||
.getGroupEndorsementExpirationMs(groupInfoV2.getGroupId());
|
||||
if (groupSendEndorsementMap.isEmpty()
|
||||
|| groupSendEndorsementExpirationMs - TimeUnit.HOURS.toMillis(2) < System.currentTimeMillis()) {
|
||||
logger.debug("Updating group send endorsements was not successful");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return new Pair(groupSendEndorsementExpirationMs, groupSendEndorsementMap);
|
||||
}
|
||||
|
||||
private boolean isSenderKeyCapable(
|
||||
final RecipientId recipientId,
|
||||
final SignalServiceAddress address,
|
||||
final UnidentifiedAccess access,
|
||||
final Map<RecipientId, GroupSendEndorsement> groupSendEndorsements
|
||||
) {
|
||||
if (access == null || !address.hasValidServiceId() || !(address.getServiceId() instanceof ACI aci)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!address.hasValidServiceId()) {
|
||||
if (!groupSendEndorsements.containsKey(recipientId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final var identity = account.getIdentityKeyStore().getIdentityInfo(address.getServiceId());
|
||||
final var identity = account.getIdentityKeyStore().getIdentityInfo(aci);
|
||||
if (identity == null || !identity.getTrustLevel().isTrusted()) {
|
||||
return false;
|
||||
}
|
||||
@ -771,7 +831,7 @@ public class SendHelper {
|
||||
SendMessageResult send(
|
||||
SignalServiceMessageSender messageSender,
|
||||
SignalServiceAddress address,
|
||||
@Nullable SealedSenderAccess unidentifiedAccess,
|
||||
SealedSenderAccess unidentifiedAccess,
|
||||
boolean includePniSignature
|
||||
) throws IOException, UnregisteredUserException, ProofRequiredException, RateLimitException, org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
|
||||
}
|
||||
|
||||
@ -33,7 +33,7 @@ import java.util.UUID;
|
||||
public class AccountDatabase extends Database {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(AccountDatabase.class);
|
||||
private static final long DATABASE_VERSION = 27;
|
||||
private static final long DATABASE_VERSION = 28;
|
||||
|
||||
private AccountDatabase(final HikariDataSource dataSource) {
|
||||
super(logger, DATABASE_VERSION, dataSource);
|
||||
@ -608,6 +608,21 @@ public class AccountDatabase extends Database {
|
||||
""");
|
||||
}
|
||||
}
|
||||
if (oldVersion < 28) {
|
||||
logger.debug("Updating database: Adding group endorsements");
|
||||
try (final var statement = connection.createStatement()) {
|
||||
statement.executeUpdate("""
|
||||
ALTER TABLE group_v2 ADD endorsement_expiration_time INTEGER DEFAULT 0 NOT NULL;
|
||||
CREATE TABLE group_v2_member (
|
||||
_id INTEGER PRIMARY KEY,
|
||||
group_id INTEGER NOT NULL REFERENCES group_v2 (_id) ON DELETE CASCADE,
|
||||
recipient_id INTEGER NOT NULL REFERENCES recipient (_id) ON DELETE CASCADE,
|
||||
endorsement BLOB NOT NULL,
|
||||
UNIQUE(group_id, recipient_id)
|
||||
) STRICT;
|
||||
""");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void createUuidMappingTable(
|
||||
|
||||
@ -3,6 +3,7 @@ package org.asamk.signal.manager.storage.groups;
|
||||
import org.asamk.signal.manager.api.GroupId;
|
||||
import org.asamk.signal.manager.api.GroupIdV1;
|
||||
import org.asamk.signal.manager.api.GroupIdV2;
|
||||
import org.asamk.signal.manager.api.Pair;
|
||||
import org.asamk.signal.manager.groups.GroupUtils;
|
||||
import org.asamk.signal.manager.storage.Database;
|
||||
import org.asamk.signal.manager.storage.Utils;
|
||||
@ -13,6 +14,7 @@ import org.asamk.signal.manager.util.KeyUtils;
|
||||
import org.signal.libsignal.zkgroup.InvalidInputException;
|
||||
import org.signal.libsignal.zkgroup.groups.GroupMasterKey;
|
||||
import org.signal.libsignal.zkgroup.groups.GroupSecretParams;
|
||||
import org.signal.libsignal.zkgroup.groupsend.GroupSendEndorsement;
|
||||
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@ -39,6 +41,7 @@ public class GroupStore {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(GroupStore.class);
|
||||
private static final String TABLE_GROUP_V2 = "group_v2";
|
||||
private static final String TABLE_GROUP_V2_MEMBER = "group_v2_member";
|
||||
private static final String TABLE_GROUP_V1 = "group_v1";
|
||||
private static final String TABLE_GROUP_V1_MEMBER = "group_v1_member";
|
||||
|
||||
@ -58,10 +61,18 @@ public class GroupStore {
|
||||
master_key BLOB NOT NULL,
|
||||
group_data BLOB,
|
||||
distribution_id BLOB UNIQUE NOT NULL,
|
||||
endorsement_expiration_time INTEGER NOT NULL DEFAULT 0,
|
||||
blocked INTEGER NOT NULL DEFAULT FALSE,
|
||||
profile_sharing INTEGER NOT NULL DEFAULT FALSE,
|
||||
permission_denied INTEGER NOT NULL DEFAULT FALSE
|
||||
) STRICT;
|
||||
CREATE TABLE group_v2_member (
|
||||
_id INTEGER PRIMARY KEY,
|
||||
group_id INTEGER NOT NULL REFERENCES group_v2 (_id) ON DELETE CASCADE,
|
||||
recipient_id INTEGER NOT NULL REFERENCES recipient (_id) ON DELETE CASCADE,
|
||||
endorsement BLOB NOT NULL,
|
||||
UNIQUE(group_id, recipient_id)
|
||||
) STRICT;
|
||||
CREATE TABLE group_v1 (
|
||||
_id INTEGER PRIMARY KEY,
|
||||
storage_id BLOB UNIQUE,
|
||||
@ -100,7 +111,7 @@ public class GroupStore {
|
||||
updateGroup(connection, group);
|
||||
connection.commit();
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException("Failed update recipient store", e);
|
||||
throw new RuntimeException("Failed update group store", e);
|
||||
}
|
||||
}
|
||||
|
||||
@ -120,6 +131,113 @@ public class GroupStore {
|
||||
insertOrReplaceGroup(connection, internalId, group);
|
||||
}
|
||||
|
||||
public void updateGroupEndorsements(
|
||||
final GroupIdV2 groupId,
|
||||
final long expirationMs,
|
||||
Map<RecipientId, GroupSendEndorsement> endorsements
|
||||
) {
|
||||
try (final var connection = database.getConnection()) {
|
||||
connection.setAutoCommit(false);
|
||||
final var sql = (
|
||||
"""
|
||||
UPDATE %s
|
||||
SET endorsement_expiration_time = ?
|
||||
WHERE group_id = ?
|
||||
RETURNING _id
|
||||
"""
|
||||
).formatted(TABLE_GROUP_V2);
|
||||
long internalId;
|
||||
try (final var statement = connection.prepareStatement(sql)) {
|
||||
statement.setLong(1, endorsements == null ? 0 : expirationMs);
|
||||
statement.setBytes(2, groupId.serialize());
|
||||
final var result = Utils.executeQueryForOptional(statement, Utils::getIdMapper);
|
||||
if (result.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
internalId = result.get();
|
||||
}
|
||||
|
||||
final var sqlDeleteMembers = "DELETE FROM %s where group_id = ?".formatted(TABLE_GROUP_V2_MEMBER);
|
||||
try (final var statement = connection.prepareStatement(sqlDeleteMembers)) {
|
||||
statement.setLong(1, internalId);
|
||||
statement.executeUpdate();
|
||||
}
|
||||
|
||||
if (endorsements != null) {
|
||||
final var sqlInsertMember = """
|
||||
INSERT OR REPLACE INTO %s (group_id, recipient_id, endorsement)
|
||||
VALUES (?, ?, ?)
|
||||
""".formatted(TABLE_GROUP_V2_MEMBER);
|
||||
try (final var statement = connection.prepareStatement(sqlInsertMember)) {
|
||||
for (final var entry : endorsements.entrySet()) {
|
||||
final var recipientId = entry.getKey();
|
||||
final var endorsement = entry.getValue();
|
||||
statement.setLong(1, internalId);
|
||||
statement.setLong(2, recipientId.id());
|
||||
statement.setBytes(3, endorsement.serialize());
|
||||
statement.executeUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
connection.commit();
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException("Failed update group store", e);
|
||||
}
|
||||
}
|
||||
|
||||
public long getGroupEndorsementExpirationMs(final GroupIdV2 groupId) {
|
||||
final var sql = (
|
||||
"""
|
||||
SELECT g.endorsement_expiration_time
|
||||
FROM %s g
|
||||
WHERE g.group_id = ?
|
||||
"""
|
||||
).formatted(TABLE_GROUP_V2);
|
||||
try (final var connection = database.getConnection()) {
|
||||
try (final var statement = connection.prepareStatement(sql)) {
|
||||
statement.setBytes(1, groupId.serialize());
|
||||
return Utils.executeQueryForOptional(statement, this::getGroupEndorsementMsFromResultSet).orElse(0L);
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException("Failed read from group store", e);
|
||||
}
|
||||
}
|
||||
|
||||
public Map<RecipientId, GroupSendEndorsement> getGroupEndorsements(final GroupIdV2 groupId) {
|
||||
final var sql = (
|
||||
"""
|
||||
SELECT gm.recipient_id, gm.endorsement
|
||||
FROM %s gm, %s g
|
||||
WHERE gm.group_id = g._id AND g.group_id = ?
|
||||
"""
|
||||
).formatted(TABLE_GROUP_V2_MEMBER, TABLE_GROUP_V2);
|
||||
try (final var connection = database.getConnection()) {
|
||||
try (final var statement = connection.prepareStatement(sql)) {
|
||||
statement.setBytes(1, groupId.serialize());
|
||||
return Utils.executeQueryForStream(statement, this::getGroupEndorsementsFromResultSet)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toMap(Pair::first, Pair::second));
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
throw new RuntimeException("Failed read from group store", e);
|
||||
}
|
||||
}
|
||||
|
||||
private Pair<RecipientId, GroupSendEndorsement> getGroupEndorsementsFromResultSet(ResultSet resultSet) throws SQLException {
|
||||
final var endorsement = resultSet.getBytes("endorsement");
|
||||
final var recipientId = resultSet.getLong("recipient_id");
|
||||
|
||||
try {
|
||||
return new Pair<>(recipientIdCreator.create(recipientId), new GroupSendEndorsement(endorsement));
|
||||
} catch (InvalidInputException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
private long getGroupEndorsementMsFromResultSet(ResultSet resultSet) throws SQLException {
|
||||
return resultSet.getLong("endorsement_expiration_time");
|
||||
}
|
||||
|
||||
public void storeStorageRecord(
|
||||
final Connection connection,
|
||||
final GroupId groupId,
|
||||
@ -333,19 +451,34 @@ public class GroupStore {
|
||||
final RecipientId recipientId,
|
||||
final RecipientId toBeMergedRecipientId
|
||||
) throws SQLException {
|
||||
final var sql = (
|
||||
final var sqlV1 = (
|
||||
"""
|
||||
UPDATE OR REPLACE %s
|
||||
SET recipient_id = ?
|
||||
WHERE recipient_id = ?
|
||||
"""
|
||||
).formatted(TABLE_GROUP_V1_MEMBER);
|
||||
try (final var statement = connection.prepareStatement(sql)) {
|
||||
try (final var statement = connection.prepareStatement(sqlV1)) {
|
||||
statement.setLong(1, recipientId.id());
|
||||
statement.setLong(2, toBeMergedRecipientId.id());
|
||||
final var updatedRows = statement.executeUpdate();
|
||||
if (updatedRows > 0) {
|
||||
logger.debug("Updated {} group members when merging recipients", updatedRows);
|
||||
logger.debug("Updated {} group v1 members when merging recipients", updatedRows);
|
||||
}
|
||||
}
|
||||
final var sqlV2 = (
|
||||
"""
|
||||
UPDATE OR REPLACE %s
|
||||
SET recipient_id = ?
|
||||
WHERE recipient_id = ?
|
||||
"""
|
||||
).formatted(TABLE_GROUP_V2_MEMBER);
|
||||
try (final var statement = connection.prepareStatement(sqlV2)) {
|
||||
statement.setLong(1, recipientId.id());
|
||||
statement.setLong(2, toBeMergedRecipientId.id());
|
||||
final var updatedRows = statement.executeUpdate();
|
||||
if (updatedRows > 0) {
|
||||
logger.debug("Updated {} group v2 members when merging recipients", updatedRows);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -521,8 +654,9 @@ public class GroupStore {
|
||||
} else if (group instanceof GroupInfoV2 groupV2) {
|
||||
final var sql = (
|
||||
"""
|
||||
INSERT OR REPLACE INTO %s (_id, group_id, master_key, group_data, distribution_id, blocked, permission_denied, storage_id, profile_sharing)
|
||||
INSERT INTO %s (_id, group_id, master_key, group_data, distribution_id, blocked, permission_denied, storage_id, profile_sharing)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT (_id) DO UPDATE SET group_id=excluded.group_id, master_key=excluded.master_key, group_data=excluded.group_data, distribution_id=excluded.distribution_id, blocked=excluded.blocked, permission_denied=excluded.permission_denied, storage_id=excluded.storage_id, profile_sharing=excluded.profile_sharing
|
||||
"""
|
||||
).formatted(TABLE_GROUP_V2);
|
||||
try (final var statement = connection.prepareStatement(sql)) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user