import org.asamk.signal.manager.groups.GroupNotFoundException;
import org.asamk.signal.manager.groups.GroupUtils;
import org.asamk.signal.manager.groups.NotAGroupMemberException;
-import org.asamk.signal.manager.helper.GroupHelper;
+import org.asamk.signal.manager.helper.GroupV2Helper;
import org.asamk.signal.manager.helper.PinHelper;
import org.asamk.signal.manager.helper.ProfileHelper;
import org.asamk.signal.manager.helper.UnidentifiedAccessHelper;
import org.whispersystems.signalservice.api.SignalServiceMessagePipe;
import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
+import org.whispersystems.signalservice.api.SignalSessionLock;
import org.whispersystems.signalservice.api.crypto.SignalServiceCipher;
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
+import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Function;
import java.util.stream.Collectors;
private final UnidentifiedAccessHelper unidentifiedAccessHelper;
private final ProfileHelper profileHelper;
- private final GroupHelper groupHelper;
+ private final GroupV2Helper groupV2Helper;
private final PinHelper pinHelper;
private final AvatarStore avatarStore;
private final AttachmentStore attachmentStore;
+ private final SignalSessionLock sessionLock = new SignalSessionLock() {
+ private final ReentrantLock LEGACY_LOCK = new ReentrantLock();
+
+ @Override
+ public Lock acquire() {
+ LEGACY_LOCK.lock();
+ return LEGACY_LOCK::unlock;
+ }
+ };
Manager(
SignalAccount account,
unidentified -> unidentified ? getOrCreateUnidentifiedMessagePipe() : getOrCreateMessagePipe(),
() -> messageReceiver,
this::resolveSignalServiceAddress);
- this.groupHelper = new GroupHelper(this::getRecipientProfileKeyCredential,
+ this.groupV2Helper = new GroupV2Helper(this::getRecipientProfileKeyCredential,
this::getRecipientProfile,
account::getSelfRecipientId,
groupsV2Operations,
throw new NotRegisteredException();
}
- var account = SignalAccount.load(pathConfig.getDataPath(), username);
+ var account = SignalAccount.load(pathConfig.getDataPath(), username, true);
if (!account.isRegistered()) {
throw new NotRegisteredException();
newProfile.getInternalServiceName(),
newProfile.getAbout() == null ? "" : newProfile.getAbout(),
newProfile.getAboutEmoji() == null ? "" : newProfile.getAboutEmoji(),
+ Optional.absent(),
streamDetails);
}
account.getPassword(),
account.getDeviceId(),
account.getSignalProtocolStore(),
+ sessionLock,
userAgent,
account.isMultiDevice(),
Optional.fromNullable(messagePipe),
Profile getRecipientProfile(
RecipientId recipientId, boolean force
) {
- var profileKey = account.getProfileStore().getProfileKey(recipientId);
- if (profileKey == null) {
- if (force) {
- // retrieve profile to get identity key
- retrieveEncryptedProfile(recipientId);
- }
- return null;
- }
var profile = account.getProfileStore().getProfile(recipientId);
var now = new Date().getTime();
return null;
}
- profile = decryptProfileAndDownloadAvatar(recipientId, profileKey, encryptedProfile);
+ var profileKey = account.getProfileStore().getProfileKey(recipientId);
+ if (profileKey == null) {
+ profile = new Profile(new Date().getTime(),
+ null,
+ null,
+ null,
+ null,
+ ProfileUtils.getUnidentifiedAccessMode(encryptedProfile, null),
+ ProfileUtils.getCapabilities(encryptedProfile));
+ } else {
+ profile = decryptProfileAndDownloadAvatar(recipientId, profileKey, encryptedProfile);
+ }
account.getProfileStore().storeProfile(recipientId, profile);
return profile;
account.getGroupStore().updateGroup(groupInfoV1);
} else {
final var groupInfoV2 = (GroupInfoV2) g;
- final var groupGroupChangePair = groupHelper.leaveGroup(groupInfoV2);
+ final var groupGroupChangePair = groupV2Helper.leaveGroup(groupInfoV2);
groupInfoV2.setGroup(groupGroupChangePair.first(), this::resolveRecipient);
messageBuilder = getGroupUpdateMessageBuilder(groupInfoV2, groupGroupChangePair.second().toByteArray());
account.getGroupStore().updateGroup(groupInfoV2);
return sendMessage(messageBuilder, g.getMembersWithout(account.getSelfRecipientId()));
}
- public Pair<GroupId, List<SendMessageResult>> updateGroup(
- GroupId groupId, String name, List<String> members, File avatarFile
+ public Pair<GroupId, List<SendMessageResult>> createGroup(
+ String name, List<String> members, File avatarFile
+ ) throws IOException, AttachmentInvalidException, InvalidNumberException {
+ return createGroup(name, members == null ? null : getSignalServiceAddresses(members), avatarFile);
+ }
+
+ private Pair<GroupId, List<SendMessageResult>> createGroup(
+ String name, Set<RecipientId> members, File avatarFile
+ ) throws IOException, AttachmentInvalidException {
+ final var selfRecipientId = account.getSelfRecipientId();
+ if (members != null && members.contains(selfRecipientId)) {
+ members = new HashSet<>(members);
+ members.remove(selfRecipientId);
+ }
+
+ var gv2Pair = groupV2Helper.createGroup(name == null ? "" : name,
+ members == null ? Set.of() : members,
+ avatarFile);
+
+ SignalServiceDataMessage.Builder messageBuilder;
+ if (gv2Pair == null) {
+ // Failed to create v2 group, creating v1 group instead
+ var gv1 = new GroupInfoV1(GroupIdV1.createRandom());
+ gv1.addMembers(List.of(selfRecipientId));
+ final var result = updateGroupV1(gv1, name, members, avatarFile);
+ return new Pair<>(gv1.getGroupId(), result.second());
+ }
+
+ final var gv2 = gv2Pair.first();
+ final var decryptedGroup = gv2Pair.second();
+
+ gv2.setGroup(decryptedGroup, this::resolveRecipient);
+ if (avatarFile != null) {
+ avatarStore.storeGroupAvatar(gv2.getGroupId(),
+ outputStream -> IOUtils.copyFileToStream(avatarFile, outputStream));
+ }
+ messageBuilder = getGroupUpdateMessageBuilder(gv2, null);
+ account.getGroupStore().updateGroup(gv2);
+
+ final var result = sendMessage(messageBuilder, gv2.getMembersIncludingPendingWithout(selfRecipientId));
+ return new Pair<>(gv2.getGroupId(), result.second());
+ }
+
+ public Pair<Long, List<SendMessageResult>> updateGroup(
+ GroupId groupId,
+ String name,
+ String description,
+ List<String> members,
+ List<String> removeMembers,
+ File avatarFile
) throws IOException, GroupNotFoundException, AttachmentInvalidException, InvalidNumberException, NotAGroupMemberException {
- return sendUpdateGroupMessage(groupId,
+ return updateGroup(groupId,
name,
+ description,
members == null ? null : getSignalServiceAddresses(members),
+ removeMembers == null ? null : getSignalServiceAddresses(removeMembers),
avatarFile);
}
- private Pair<GroupId, List<SendMessageResult>> sendUpdateGroupMessage(
- GroupId groupId, String name, Set<RecipientId> members, File avatarFile
+ private Pair<Long, List<SendMessageResult>> updateGroup(
+ GroupId groupId,
+ String name,
+ String description,
+ Set<RecipientId> members,
+ final Set<RecipientId> removeMembers,
+ File avatarFile
) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException {
- GroupInfo g;
- SignalServiceDataMessage.Builder messageBuilder;
- if (groupId == null) {
- // Create new group
- var gv2Pair = groupHelper.createGroupV2(name == null ? "" : name,
- members == null ? Set.of() : members,
- avatarFile);
- if (gv2Pair == null) {
- var gv1 = new GroupInfoV1(GroupIdV1.createRandom());
- gv1.addMembers(List.of(account.getSelfRecipientId()));
- updateGroupV1(gv1, name, members, avatarFile);
- messageBuilder = getGroupUpdateMessageBuilder(gv1);
- g = gv1;
- } else {
- final var gv2 = gv2Pair.first();
- final var decryptedGroup = gv2Pair.second();
+ var group = getGroupForUpdating(groupId);
- gv2.setGroup(decryptedGroup, this::resolveRecipient);
- if (avatarFile != null) {
- avatarStore.storeGroupAvatar(gv2.getGroupId(),
- outputStream -> IOUtils.copyFileToStream(avatarFile, outputStream));
- }
- messageBuilder = getGroupUpdateMessageBuilder(gv2, null);
- g = gv2;
- }
- } else {
- var group = getGroupForUpdating(groupId);
- if (group instanceof GroupInfoV2) {
- final var groupInfoV2 = (GroupInfoV2) group;
-
- Pair<Long, List<SendMessageResult>> result = null;
- if (groupInfoV2.isPendingMember(account.getSelfRecipientId())) {
- var groupGroupChangePair = groupHelper.acceptInvite(groupInfoV2);
- result = sendUpdateGroupMessage(groupInfoV2,
- groupGroupChangePair.first(),
- groupGroupChangePair.second());
- }
+ if (group instanceof GroupInfoV2) {
+ return updateGroupV2((GroupInfoV2) group, name, description, members, removeMembers, avatarFile);
+ }
- if (members != null) {
- final var newMembers = new HashSet<>(members);
- newMembers.removeAll(group.getMembers());
- if (newMembers.size() > 0) {
- var groupGroupChangePair = groupHelper.updateGroupV2(groupInfoV2, newMembers);
- result = sendUpdateGroupMessage(groupInfoV2,
- groupGroupChangePair.first(),
- groupGroupChangePair.second());
- }
- }
- if (result == null || name != null || avatarFile != null) {
- var groupGroupChangePair = groupHelper.updateGroupV2(groupInfoV2, name, avatarFile);
- if (avatarFile != null) {
- avatarStore.storeGroupAvatar(groupInfoV2.getGroupId(),
- outputStream -> IOUtils.copyFileToStream(avatarFile, outputStream));
- }
- result = sendUpdateGroupMessage(groupInfoV2,
- groupGroupChangePair.first(),
- groupGroupChangePair.second());
- }
+ return updateGroupV1((GroupInfoV1) group, name, members, avatarFile);
+ }
- return new Pair<>(group.getGroupId(), result.second());
- } else {
- var gv1 = (GroupInfoV1) group;
- updateGroupV1(gv1, name, members, avatarFile);
- messageBuilder = getGroupUpdateMessageBuilder(gv1);
- g = gv1;
- }
- }
+ private Pair<Long, List<SendMessageResult>> updateGroupV1(
+ final GroupInfoV1 gv1, final String name, final Set<RecipientId> members, final File avatarFile
+ ) throws IOException, AttachmentInvalidException {
+ updateGroupV1Details(gv1, name, members, avatarFile);
+ var messageBuilder = getGroupUpdateMessageBuilder(gv1);
- account.getGroupStore().updateGroup(g);
+ account.getGroupStore().updateGroup(gv1);
- final var result = sendMessage(messageBuilder,
- g.getMembersIncludingPendingWithout(account.getSelfRecipientId()));
- return new Pair<>(g.getGroupId(), result.second());
+ return sendMessage(messageBuilder, gv1.getMembersIncludingPendingWithout(account.getSelfRecipientId()));
}
- private void updateGroupV1(
+ private void updateGroupV1Details(
final GroupInfoV1 g, final String name, final Collection<RecipientId> members, final File avatarFile
) throws IOException {
if (name != null) {
}
}
- public Pair<GroupId, List<SendMessageResult>> joinGroup(
- GroupInviteLinkUrl inviteLinkUrl
- ) throws IOException, GroupLinkNotActiveException {
- return sendJoinGroupMessage(inviteLinkUrl);
+ private Pair<Long, List<SendMessageResult>> updateGroupV2(
+ final GroupInfoV2 group,
+ final String name,
+ final String description,
+ final Set<RecipientId> members,
+ final Set<RecipientId> removeMembers,
+ final File avatarFile
+ ) throws IOException {
+ Pair<Long, List<SendMessageResult>> result = null;
+ if (group.isPendingMember(account.getSelfRecipientId())) {
+ var groupGroupChangePair = groupV2Helper.acceptInvite(group);
+ result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
+ }
+
+ if (members != null) {
+ final var newMembers = new HashSet<>(members);
+ newMembers.removeAll(group.getMembers());
+ if (newMembers.size() > 0) {
+ var groupGroupChangePair = groupV2Helper.addMembers(group, newMembers);
+ result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
+ }
+ }
+
+ if (removeMembers != null) {
+ var existingRemoveMembers = new HashSet<>(removeMembers);
+ existingRemoveMembers.retainAll(group.getMembers());
+ existingRemoveMembers.remove(getSelfRecipientId());// self can be removed with sendQuitGroupMessage
+ if (existingRemoveMembers.size() > 0) {
+ var groupGroupChangePair = groupV2Helper.removeMembers(group, existingRemoveMembers);
+ result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
+ }
+
+ var pendingRemoveMembers = new HashSet<>(removeMembers);
+ pendingRemoveMembers.retainAll(group.getPendingMembers());
+ if (pendingRemoveMembers.size() > 0) {
+ var groupGroupChangePair = groupV2Helper.revokeInvitedMembers(group, pendingRemoveMembers);
+ result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
+ }
+ }
+
+ if (result == null || name != null || description != null || avatarFile != null) {
+ var groupGroupChangePair = groupV2Helper.updateGroup(group, name, description, avatarFile);
+ if (avatarFile != null) {
+ avatarStore.storeGroupAvatar(group.getGroupId(),
+ outputStream -> IOUtils.copyFileToStream(avatarFile, outputStream));
+ }
+ result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second());
+ }
+
+ return result;
}
- private Pair<GroupId, List<SendMessageResult>> sendJoinGroupMessage(
+ public Pair<GroupId, List<SendMessageResult>> joinGroup(
GroupInviteLinkUrl inviteLinkUrl
) throws IOException, GroupLinkNotActiveException {
- final var groupJoinInfo = groupHelper.getDecryptedGroupJoinInfo(inviteLinkUrl.getGroupMasterKey(),
+ final var groupJoinInfo = groupV2Helper.getDecryptedGroupJoinInfo(inviteLinkUrl.getGroupMasterKey(),
inviteLinkUrl.getPassword());
- final var groupChange = groupHelper.joinGroup(inviteLinkUrl.getGroupMasterKey(),
+ final var groupChange = groupV2Helper.joinGroup(inviteLinkUrl.getGroupMasterKey(),
inviteLinkUrl.getPassword(),
groupJoinInfo);
final var group = getOrMigrateGroup(inviteLinkUrl.getGroupMasterKey(),
return new Pair<>(group.getGroupId(), List.of());
}
- final var result = sendUpdateGroupMessage(group, group.getGroup(), groupChange);
+ final var result = sendUpdateGroupV2Message(group, group.getGroup(), groupChange);
return new Pair<>(group.getGroupId(), result.second());
}
+ private Pair<Long, List<SendMessageResult>> sendUpdateGroupV2Message(
+ GroupInfoV2 group, DecryptedGroup newDecryptedGroup, GroupChange groupChange
+ ) throws IOException {
+ final var selfRecipientId = account.getSelfRecipientId();
+ final var members = group.getMembersIncludingPendingWithout(selfRecipientId);
+ group.setGroup(newDecryptedGroup, this::resolveRecipient);
+ members.addAll(group.getMembersIncludingPendingWithout(selfRecipientId));
+
+ final var messageBuilder = getGroupUpdateMessageBuilder(group, groupChange.toByteArray());
+ account.getGroupStore().updateGroup(group);
+ return sendMessage(messageBuilder, members);
+ }
+
private static int currentTimeDays() {
return (int) TimeUnit.MILLISECONDS.toDays(System.currentTimeMillis());
}
}
}
- private Pair<Long, List<SendMessageResult>> sendUpdateGroupMessage(
- GroupInfoV2 group, DecryptedGroup newDecryptedGroup, GroupChange groupChange
- ) throws IOException {
- group.setGroup(newDecryptedGroup, this::resolveRecipient);
- final var messageBuilder = getGroupUpdateMessageBuilder(group, groupChange.toByteArray());
- account.getGroupStore().updateGroup(group);
- return sendMessage(messageBuilder, group.getMembersIncludingPendingWithout(account.getSelfRecipientId()));
- }
-
Pair<Long, List<SendMessageResult>> sendGroupInfoMessage(
GroupIdV1 groupId, SignalServiceAddress recipient
) throws IOException, NotAGroupMemberException, GroupNotFoundException, AttachmentInvalidException {
}
}
- SendMessageResult renewSession(RecipientId recipientId) throws IOException {
+ void renewSession(RecipientId recipientId) throws IOException {
account.getSessionStore().archiveSessions(recipientId);
- return sendNullMessage(recipientId);
+ if (!recipientId.equals(getSelfRecipientId())) {
+ sendNullMessage(recipientId);
+ }
}
public String getContactName(String number) throws InvalidNumberException {
}
private byte[] getSenderCertificate() {
- // TODO support UUID capable sender certificates
- // byte[] certificate = accountManager.getSenderCertificateForPhoneNumberPrivacy();
byte[] certificate;
try {
- certificate = accountManager.getSenderCertificate();
+ if (account.isPhoneNumberShared()) {
+ certificate = accountManager.getSenderCertificate();
+ } else {
+ certificate = accountManager.getSenderCertificateForPhoneNumberPrivacy();
+ }
} catch (IOException e) {
logger.warn("Failed to get sender certificate, ignoring: {}", e.getMessage());
return null;
}
}
- private SignalServiceContent decryptMessage(SignalServiceEnvelope envelope) throws InvalidMetadataMessageException, ProtocolInvalidMessageException, ProtocolDuplicateMessageException, ProtocolLegacyMessageException, ProtocolInvalidKeyIdException, InvalidMetadataVersionException, ProtocolInvalidVersionException, ProtocolNoSessionException, ProtocolInvalidKeyException, SelfSendException, UnsupportedDataMessageException, org.whispersystems.libsignal.UntrustedIdentityException {
+ private SignalServiceContent decryptMessage(SignalServiceEnvelope envelope) throws InvalidMetadataMessageException, ProtocolInvalidMessageException, ProtocolDuplicateMessageException, ProtocolLegacyMessageException, ProtocolInvalidKeyIdException, InvalidMetadataVersionException, ProtocolInvalidVersionException, ProtocolNoSessionException, ProtocolInvalidKeyException, SelfSendException, UnsupportedDataMessageException, ProtocolUntrustedIdentityException {
var cipher = new SignalServiceCipher(account.getSelfAddress(),
account.getSignalProtocolStore(),
+ sessionLock,
certificateValidator);
- try {
- return cipher.decrypt(envelope);
- } catch (ProtocolUntrustedIdentityException e) {
- if (e.getCause() instanceof org.whispersystems.libsignal.UntrustedIdentityException) {
- throw (org.whispersystems.libsignal.UntrustedIdentityException) e.getCause();
- }
- throw new AssertionError(e);
- }
+ return cipher.decrypt(envelope);
}
private void handleEndSession(RecipientId recipientId) {
if (signedGroupChange != null
&& groupInfoV2.getGroup() != null
&& groupInfoV2.getGroup().getRevision() + 1 == revision) {
- group = groupHelper.getUpdatedDecryptedGroup(groupInfoV2.getGroup(), signedGroupChange, groupMasterKey);
+ group = groupV2Helper.getUpdatedDecryptedGroup(groupInfoV2.getGroup(),
+ signedGroupChange,
+ groupMasterKey);
}
if (group == null) {
- group = groupHelper.getDecryptedGroup(groupSecretParams);
+ group = groupV2Helper.getDecryptedGroup(groupSecretParams);
}
if (group != null) {
storeProfileKeysFromMembers(group);
if (!envelope.isReceipt()) {
try {
content = decryptMessage(envelope);
- } catch (org.whispersystems.libsignal.UntrustedIdentityException e) {
+ } catch (ProtocolUntrustedIdentityException e) {
if (!envelope.hasSource()) {
- final var identifier = ((org.whispersystems.libsignal.UntrustedIdentityException) e).getName();
+ final var identifier = e.getSender();
final var recipientId = resolveRecipient(identifier);
try {
account.getMessageCache().replaceSender(cachedMessage, recipientId);
if (envelope.hasSource()) {
// Store uuid if we don't have it already
+ // address/uuid in envelope is sent by server
resolveRecipientTrusted(envelope.getSourceAddress());
}
final var notAGroupMember = isNotAGroupMember(envelope, content);
} catch (Exception e) {
exception = e;
}
+ if (!envelope.hasSource() && content != null) {
+ // Store uuid if we don't have it already
+ // address/uuid is validated by unidentified sender certificate
+ resolveRecipientTrusted(content.getSender());
+ }
var actions = handleMessage(envelope, content, ignoreAttachments);
if (exception instanceof ProtocolInvalidMessageException) {
final var sender = resolveRecipient(((ProtocolInvalidMessageException) exception).getSender());
handler.handleMessage(envelope, content, exception);
}
if (cachedMessage[0] != null) {
- if (exception instanceof org.whispersystems.libsignal.UntrustedIdentityException) {
- final var identifier = ((org.whispersystems.libsignal.UntrustedIdentityException) exception).getName();
+ if (exception instanceof ProtocolUntrustedIdentityException) {
+ final var identifier = ((ProtocolUntrustedIdentityException) exception).getSender();
final var recipientId = resolveRecipient(identifier);
queuedActions.add(new RetrieveProfileAction(recipientId));
if (!envelope.hasSource()) {
final var group = account.getGroupStore().getGroup(groupId);
if (group instanceof GroupInfoV2 && ((GroupInfoV2) group).getGroup() == null) {
final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(((GroupInfoV2) group).getMasterKey());
- ((GroupInfoV2) group).setGroup(groupHelper.getDecryptedGroup(groupSecretParams), this::resolveRecipient);
+ ((GroupInfoV2) group).setGroup(groupV2Helper.getDecryptedGroup(groupSecretParams), this::resolveRecipient);
account.getGroupStore().updateGroup(group);
}
return group;