import org.signal.libsignal.metadata.ProtocolNoSessionException;
import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException;
import org.signal.libsignal.metadata.SelfSendException;
+import org.signal.storageservice.protos.groups.GroupChange;
import org.signal.storageservice.protos.groups.local.DecryptedGroup;
import org.signal.storageservice.protos.groups.local.DecryptedMember;
import org.signal.zkgroup.InvalidInputException;
this.groupHelper = new GroupHelper(this::getRecipientProfileKeyCredential,
this::getRecipientProfile,
account::getSelfAddress,
- groupsV2Operations);
+ groupsV2Operations,
+ groupsV2Api,
+ this::getGroupAuthForToday);
}
public String getUsername() {
) throws IOException, GroupNotFoundException, NotAGroupMemberException {
final GroupInfo g = getGroupForSending(groupId);
- GroupHelper.setGroupContext(messageBuilder, g);
+ GroupUtils.setGroupContext(messageBuilder, g);
messageBuilder.withExpiration(g.getMessageExpirationTime());
return sendMessage(messageBuilder, g.getMembersWithout(account.getSelfAddress()));
return sendMessage(messageBuilder, g.getMembersWithout(account.getSelfAddress()));
}
- private GroupInfoV2 createGroupV2(
- String name, Collection<SignalServiceAddress> members, InputStream avatar
- ) throws IOException {
- byte[] avatarBytes = avatar == null ? null : IOUtils.readFully(avatar);
- final GroupsV2Operations.NewGroup newGroup = groupHelper.createGroupV2(name, members, avatarBytes);
- final GroupSecretParams groupSecretParams = newGroup.getGroupSecretParams();
-
- final GroupsV2AuthorizationString groupAuthForToday;
- final DecryptedGroup decryptedGroup;
- try {
- groupAuthForToday = getGroupAuthForToday(groupSecretParams);
- groupsV2Api.putNewGroup(newGroup, groupAuthForToday);
- decryptedGroup = groupsV2Api.getGroup(groupSecretParams, groupAuthForToday);
- } catch (IOException | VerificationFailedException | InvalidGroupStateException e) {
- System.err.println("Failed to create V2 group: " + e.getMessage());
- return null;
- }
- if (decryptedGroup == null) {
- System.err.println("Failed to create V2 group!");
- return null;
- }
-
- final byte[] groupId = groupSecretParams.getPublicParams().getGroupIdentifier().serialize();
- final GroupMasterKey masterKey = groupSecretParams.getMasterKey();
- GroupInfoV2 g = new GroupInfoV2(groupId, masterKey);
- g.setGroup(decryptedGroup);
-
- return g;
- }
-
private Pair<byte[], List<SendMessageResult>> sendUpdateGroupMessage(
byte[] groupId, String name, Collection<SignalServiceAddress> members, String avatarFile
) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException {
SignalServiceDataMessage.Builder messageBuilder;
if (groupId == null) {
// Create new group
- InputStream avatar = avatarFile == null ? null : new FileInputStream(avatarFile);
- GroupInfoV2 gv2 = createGroupV2(name, members, avatar);
+ GroupInfoV2 gv2 = groupHelper.createGroupV2(name, members, avatarFile);
if (gv2 == null) {
GroupInfoV1 gv1 = new GroupInfoV1(KeyUtils.createGroupId());
gv1.addMembers(Collections.singleton(account.getSelfAddress()));
messageBuilder = getGroupUpdateMessageBuilder(gv1);
g = gv1;
} else {
- messageBuilder = getGroupUpdateMessageBuilder(gv2);
+ messageBuilder = getGroupUpdateMessageBuilder(gv2, null);
g = gv2;
}
} else {
GroupInfo group = getGroupForSending(groupId);
- if (!(group instanceof GroupInfoV1)) {
- throw new RuntimeException("TODO Not implemented!");
+ if (group instanceof GroupInfoV2) {
+ Pair<DecryptedGroup, GroupChange> groupGroupChangePair = null;
+ if (members != null) {
+ final Set<SignalServiceAddress> newMembers = new HashSet<>(members);
+ newMembers.removeAll(group.getMembers());
+ if (newMembers.size() > 0) {
+ groupGroupChangePair = groupHelper.updateGroupV2((GroupInfoV2) group, newMembers);
+ }
+ }
+ if (groupGroupChangePair == null || name != null || avatarFile != null) {
+ if (groupGroupChangePair != null) {
+ ((GroupInfoV2) group).setGroup(groupGroupChangePair.first());
+ messageBuilder = getGroupUpdateMessageBuilder((GroupInfoV2) group,
+ groupGroupChangePair.second().toByteArray());
+ sendMessage(messageBuilder, group.getMembersWithout(account.getSelfAddress()));
+ }
+
+ groupGroupChangePair = groupHelper.updateGroupV2((GroupInfoV2) group, name, avatarFile);
+ }
+
+ ((GroupInfoV2) group).setGroup(groupGroupChangePair.first());
+ messageBuilder = getGroupUpdateMessageBuilder((GroupInfoV2) group,
+ groupGroupChangePair.second().toByteArray());
+ g = group;
+ } else {
+ GroupInfoV1 gv1 = (GroupInfoV1) group;
+ updateGroupV1(gv1, name, members, avatarFile);
+ messageBuilder = getGroupUpdateMessageBuilder(gv1);
+ g = gv1;
}
- GroupInfoV1 gv1 = (GroupInfoV1) group;
- updateGroupV1(gv1, name, members, avatarFile);
- messageBuilder = getGroupUpdateMessageBuilder(gv1);
- g = gv1;
}
account.getGroupStore().updateGroup(g);
GroupInfoV1 g;
GroupInfo group = getGroupForSending(groupId);
if (!(group instanceof GroupInfoV1)) {
- throw new RuntimeException("TODO Not implemented!");
+ throw new RuntimeException("Received an invalid group request for a v2 group!");
}
g = (GroupInfoV1) group;
.withExpiration(g.getMessageExpirationTime());
}
- private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfoV2 g) {
+ private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfoV2 g, byte[] signedGroupChange) {
SignalServiceGroupV2.Builder group = SignalServiceGroupV2.newBuilder(g.getMasterKey())
.withRevision(g.getGroup().getRevision())
-// .withSignedGroupChange() // TODO
- ;
+ .withSignedGroupChange(signedGroupChange);
return SignalServiceDataMessage.newBuilder()
.asGroupMessage(group.build())
.withExpiration(g.getMessageExpirationTime());
private GroupsV2AuthorizationString getGroupAuthForToday(
final GroupSecretParams groupSecretParams
- ) throws IOException, VerificationFailedException {
+ ) throws IOException {
final int today = currentTimeDays();
// Returns credentials for the next 7 days
final HashMap<Integer, AuthCredentialResponse> credentials = groupsV2Api.getCredentials(today);
// TODO cache credentials until they expire
AuthCredentialResponse authCredentialResponse = credentials.get(today);
- return groupsV2Api.getGroupsV2AuthorizationString(account.getUuid(),
- today,
- groupSecretParams,
- authCredentialResponse);
+ try {
+ return groupsV2Api.getGroupsV2AuthorizationString(account.getUuid(),
+ today,
+ groupSecretParams,
+ authCredentialResponse);
+ } catch (VerificationFailedException e) {
+ throw new IOException(e);
+ }
}
private List<HandleAction> handleSignalServiceDataMessage(
if (message.getGroupContext().isPresent()) {
if (message.getGroupContext().get().getGroupV1().isPresent()) {
SignalServiceGroup groupInfo = message.getGroupContext().get().getGroupV1().get();
- GroupInfo group = account.getGroupStore().getGroup(groupInfo.getGroupId());
+ GroupInfo group = account.getGroupStore().getGroupByV1Id(groupInfo.getGroupId());
if (group == null || group instanceof GroupInfoV1) {
GroupInfoV1 groupV1 = (GroupInfoV1) group;
switch (groupInfo.getType()) {
break;
}
} else {
- System.err.println("Received a group v1 message for a v2 group: " + group.getTitle());
+ // Received a group v1 message for a v2 group
}
}
if (message.getGroupContext().get().getGroupV2().isPresent()) {
final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
byte[] groupId = groupSecretParams.getPublicParams().getGroupIdentifier().serialize();
- GroupInfo groupInfo = account.getGroupStore().getGroup(groupId);
+ GroupInfo groupInfo = account.getGroupStore().getGroupByV2Id(groupId);
if (groupInfo instanceof GroupInfoV1) {
- // TODO upgrade group
+ // Received a v2 group message for a v2 group, we need to locally migrate the group
+ account.getGroupStore().deleteGroup(groupInfo.groupId);
+ GroupInfoV2 groupInfoV2 = new GroupInfoV2(groupId, groupMasterKey);
+ groupInfoV2.setGroup(getDecryptedGroup(groupSecretParams));
+ account.getGroupStore().updateGroup(groupInfoV2);
+ System.err.println("Locally migrated group "
+ + Base64.encodeBytes(groupInfo.groupId)
+ + " to group v2, id: "
+ + Base64.encodeBytes(groupInfoV2.groupId)
+ + " !!!");
} else if (groupInfo == null || groupInfo instanceof GroupInfoV2) {
GroupInfoV2 groupInfoV2 = groupInfo == null
? new GroupInfoV2(groupId, groupMasterKey)
if (groupInfoV2.getGroup() == null
|| groupInfoV2.getGroup().getRevision() < groupContext.getRevision()) {
- // TODO check if revision is only 1 behind and a signedGroupChange is available
- try {
- final GroupsV2AuthorizationString groupsV2AuthorizationString = getGroupAuthForToday(
- groupSecretParams);
- final DecryptedGroup group = groupsV2Api.getGroup(groupSecretParams,
- groupsV2AuthorizationString);
- groupInfoV2.setGroup(group);
- for (DecryptedMember member : group.getMembersList()) {
- final SignalServiceAddress address = resolveSignalServiceAddress(new SignalServiceAddress(
- UuidUtil.parseOrThrow(member.getUuid().toByteArray()),
- null));
- try {
- account.getProfileStore()
- .storeProfileKey(address,
- new ProfileKey(member.getProfileKey().toByteArray()));
- } catch (InvalidInputException ignored) {
- }
- }
- } catch (IOException | VerificationFailedException | InvalidGroupStateException e) {
- System.err.println("Failed to retrieve Group V2 info, ignoring ...");
+ DecryptedGroup group = null;
+ if (groupContext.hasSignedGroupChange()
+ && groupInfoV2.getGroup() != null
+ && groupInfoV2.getGroup().getRevision() + 1 == groupContext.getRevision()) {
+ group = groupHelper.getUpdatedDecryptedGroup(groupInfoV2.getGroup(),
+ groupContext.getSignedGroupChange(),
+ groupMasterKey);
}
+ if (group == null) {
+ group = getDecryptedGroup(groupSecretParams);
+ }
+ groupInfoV2.setGroup(group);
account.getGroupStore().updateGroup(groupInfoV2);
}
}
return actions;
}
+ private DecryptedGroup getDecryptedGroup(final GroupSecretParams groupSecretParams) {
+ try {
+ final GroupsV2AuthorizationString groupsV2AuthorizationString = getGroupAuthForToday(groupSecretParams);
+ DecryptedGroup group = groupsV2Api.getGroup(groupSecretParams, groupsV2AuthorizationString);
+ for (DecryptedMember member : group.getMembersList()) {
+ final SignalServiceAddress address = resolveSignalServiceAddress(new SignalServiceAddress(UuidUtil.parseOrThrow(
+ member.getUuid().toByteArray()), null));
+ try {
+ account.getProfileStore()
+ .storeProfileKey(address, new ProfileKey(member.getProfileKey().toByteArray()));
+ } catch (InvalidInputException ignored) {
+ }
+ }
+ return group;
+ } catch (IOException | VerificationFailedException | InvalidGroupStateException e) {
+ System.err.println("Failed to retrieve Group V2 info, ignoring ...");
+ return null;
+ }
+ }
+
private void retryFailedReceivedMessages(
ReceiveMessageHandler handler, boolean ignoreAttachments
) {
return account.getGroupStore().getGroup(groupId);
}
- public byte[] getGroupId(GroupMasterKey groupMasterKey) {
- final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey);
- return groupSecretParams.getPublicParams().getGroupIdentifier().serialize();
- }
-
public List<JsonIdentityKeyStore.Identity> getIdentities() {
return account.getSignalProtocolStore().getIdentities();
}