X-Git-Url: https://git.nmode.ca/signal-cli/blobdiff_plain/08962fd1dc39d5237e39e5d787b6ba07d4ad2de1..0001a9b3005ae2d20400296ea94be38f850c8dc7:/lib/src/main/java/org/asamk/signal/manager/Manager.java diff --git a/lib/src/main/java/org/asamk/signal/manager/Manager.java b/lib/src/main/java/org/asamk/signal/manager/Manager.java index cd91230d..99a42a41 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -16,29 +16,34 @@ */ package org.asamk.signal.manager; +import org.asamk.signal.manager.api.Device; import org.asamk.signal.manager.config.ServiceConfig; import org.asamk.signal.manager.config.ServiceEnvironment; import org.asamk.signal.manager.config.ServiceEnvironmentConfig; import org.asamk.signal.manager.groups.GroupId; import org.asamk.signal.manager.groups.GroupIdV1; import org.asamk.signal.manager.groups.GroupInviteLinkUrl; +import org.asamk.signal.manager.groups.GroupLinkState; import org.asamk.signal.manager.groups.GroupNotFoundException; +import org.asamk.signal.manager.groups.GroupPermission; import org.asamk.signal.manager.groups.GroupUtils; +import org.asamk.signal.manager.groups.LastGroupAdminException; 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.asamk.signal.manager.storage.SignalAccount; -import org.asamk.signal.manager.storage.contacts.ContactInfo; import org.asamk.signal.manager.storage.groups.GroupInfo; import org.asamk.signal.manager.storage.groups.GroupInfoV1; import org.asamk.signal.manager.storage.groups.GroupInfoV2; import org.asamk.signal.manager.storage.identities.IdentityInfo; import org.asamk.signal.manager.storage.messageCache.CachedMessage; -import org.asamk.signal.manager.storage.profiles.SignalProfile; +import org.asamk.signal.manager.storage.recipients.Contact; +import org.asamk.signal.manager.storage.recipients.Profile; import org.asamk.signal.manager.storage.recipients.RecipientId; import org.asamk.signal.manager.storage.stickers.Sticker; +import org.asamk.signal.manager.storage.stickers.StickerPackId; import org.asamk.signal.manager.util.AttachmentUtils; import org.asamk.signal.manager.util.IOUtils; import org.asamk.signal.manager.util.KeyUtils; @@ -81,6 +86,7 @@ import org.whispersystems.signalservice.api.SignalServiceAccountManager; 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; @@ -107,7 +113,6 @@ import org.whispersystems.signalservice.api.messages.multidevice.DeviceContactsO import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroup; import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroupsInputStream; import org.whispersystems.signalservice.api.messages.multidevice.DeviceGroupsOutputStream; -import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo; import org.whispersystems.signalservice.api.messages.multidevice.RequestMessage; import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage; import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage; @@ -116,7 +121,10 @@ import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage import org.whispersystems.signalservice.api.profiles.ProfileAndCredential; import org.whispersystems.signalservice.api.profiles.SignalServiceProfile; import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import org.whispersystems.signalservice.api.push.exceptions.ConflictException; import org.whispersystems.signalservice.api.push.exceptions.MissingConfigurationException; +import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException; +import org.whispersystems.signalservice.api.util.DeviceNameUtil; import org.whispersystems.signalservice.api.util.InvalidNumberException; import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; import org.whispersystems.signalservice.api.util.SleepTimer; @@ -158,6 +166,7 @@ import java.util.concurrent.ExecutorService; 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; @@ -186,10 +195,19 @@ public class Manager implements Closeable { 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, @@ -243,13 +261,15 @@ public class Manager implements Closeable { this.profileHelper = new ProfileHelper(account.getProfileStore()::getProfileKey, unidentifiedAccessHelper::getAccessFor, unidentified -> unidentified ? getOrCreateUnidentifiedMessagePipe() : getOrCreateMessagePipe(), - () -> messageReceiver); - this.groupHelper = new GroupHelper(this::getRecipientProfileKeyCredential, + () -> messageReceiver, + this::resolveSignalServiceAddress); + this.groupV2Helper = new GroupV2Helper(this::getRecipientProfileKeyCredential, this::getRecipientProfile, - account::getSelfAddress, + account::getSelfRecipientId, groupsV2Operations, groupsV2Api, - this::getGroupAuthForToday); + this::getGroupAuthForToday, + this::resolveSignalServiceAddress); this.avatarStore = new AvatarStore(pathConfig.getAvatarsPath()); this.attachmentStore = new AttachmentStore(pathConfig.getAttachmentsPath()); } @@ -262,6 +282,10 @@ public class Manager implements Closeable { return account.getSelfAddress(); } + public RecipientId getSelfRecipientId() { + return account.getSelfRecipientId(); + } + private IdentityKeyPair getIdentityKeyPair() { return account.getIdentityKeyPair(); } @@ -279,7 +303,7 @@ public class Manager implements Closeable { throw new NotRegisteredException(); } - var account = SignalAccount.load(pathConfig.getDataPath(), username); + var account = SignalAccount.load(pathConfig.getDataPath(), username, true); if (!account.isRegistered()) { throw new NotRegisteredException(); @@ -307,13 +331,22 @@ public class Manager implements Closeable { } public void checkAccountState() throws IOException { + if (account.getLastReceiveTimestamp() == 0) { + logger.warn("The Signal protocol expects that incoming messages are regularly received."); + } else { + var diffInMilliseconds = System.currentTimeMillis() - account.getLastReceiveTimestamp(); + long days = TimeUnit.DAYS.convert(diffInMilliseconds, TimeUnit.MILLISECONDS); + if (days > 7) { + logger.warn( + "Messages have been last received {} days ago. The Signal protocol expects that incoming messages are regularly received.", + days); + } + } if (accountManager.getPreKeysCount() < ServiceConfig.PREKEY_MINIMUM_COUNT) { refreshPreKeys(); - account.save(); } if (account.getUuid() == null) { account.setUuid(accountManager.getOwnUuid()); - account.save(); } updateAccountAttributes(); } @@ -335,7 +368,8 @@ public class Manager implements Closeable { } public void updateAccountAttributes() throws IOException { - accountManager.setAccountAttributes(null, + accountManager.setAccountAttributes(account.getEncryptedDeviceName(), + null, account.getLocalRegistrationId(), true, // set legacy pin only if no KBS master key is set @@ -348,33 +382,40 @@ public class Manager implements Closeable { } /** - * @param name if null, the previous name will be kept + * @param givenName if null, the previous givenName will be kept + * @param familyName if null, the previous familyName will be kept * @param about if null, the previous about text will be kept * @param aboutEmoji if null, the previous about emoji will be kept * @param avatar if avatar is null the image from the local avatar store is used (if present), - * if it's Optional.absent(), the avatar will be removed */ - public void setProfile(String name, String about, String aboutEmoji, Optional avatar) throws IOException { - var profileEntry = account.getProfileStore().getProfileEntry(getSelfAddress()); - var profile = profileEntry == null ? null : profileEntry.getProfile(); - var newProfile = new SignalProfile(profile == null ? null : profile.getIdentityKey(), - name != null ? name : profile == null || profile.getName() == null ? "" : profile.getName(), - about != null ? about : profile == null || profile.getAbout() == null ? "" : profile.getAbout(), - aboutEmoji != null - ? aboutEmoji - : profile == null || profile.getAboutEmoji() == null ? "" : profile.getAboutEmoji(), - profile == null ? null : profile.getUnidentifiedAccess(), - account.isUnrestrictedUnidentifiedAccess(), - profile == null ? null : profile.getCapabilities()); + public void setProfile( + String givenName, final String familyName, String about, String aboutEmoji, Optional avatar + ) throws IOException { + var profile = getRecipientProfile(account.getSelfRecipientId()); + var builder = profile == null ? Profile.newBuilder() : Profile.newBuilder(profile); + if (givenName != null) { + builder.withGivenName(givenName); + } + if (familyName != null) { + builder.withFamilyName(familyName); + } + if (about != null) { + builder.withAbout(about); + } + if (aboutEmoji != null) { + builder.withAboutEmoji(aboutEmoji); + } + var newProfile = builder.build(); try (final var streamDetails = avatar == null ? avatarStore.retrieveProfileAvatar(getSelfAddress()) : avatar.isPresent() ? Utils.createStreamDetailsFromFile(avatar.get()) : null) { accountManager.setVersionedProfile(account.getUuid(), account.getProfileKey(), - newProfile.getName(), - newProfile.getAbout(), - newProfile.getAboutEmoji(), + newProfile.getInternalServiceName(), + newProfile.getAbout() == null ? "" : newProfile.getAbout(), + newProfile.getAboutEmoji() == null ? "" : newProfile.getAboutEmoji(), + Optional.absent(), streamDetails); } @@ -386,12 +427,7 @@ public class Manager implements Closeable { avatarStore.deleteProfileAvatar(getSelfAddress()); } } - account.getProfileStore() - .updateProfile(getSelfAddress(), - account.getProfileKey(), - System.currentTimeMillis(), - newProfile, - profileEntry == null ? null : profileEntry.getProfileKeyCredential()); + account.getProfileStore().storeProfile(account.getSelfRecipientId(), newProfile); try { sendSyncMessage(SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.LOCAL_PROFILE)); @@ -404,24 +440,37 @@ public class Manager implements Closeable { // If this is the master device, other users can't send messages to this number anymore. // If this is a linked device, other users can still send messages, but this device doesn't receive them anymore. accountManager.setGcmId(Optional.absent()); + + account.setRegistered(false); + } + + public void deleteAccount() throws IOException { accountManager.deleteAccount(); account.setRegistered(false); - account.save(); } - public List getLinkedDevices() throws IOException { + public List getLinkedDevices() throws IOException { var devices = accountManager.getDevices(); account.setMultiDevice(devices.size() > 1); - account.save(); - return devices; + var identityKey = account.getIdentityKeyPair().getPrivateKey(); + return devices.stream().map(d -> { + String deviceName = d.getName(); + if (deviceName != null) { + try { + deviceName = DeviceNameUtil.decryptDeviceName(deviceName, identityKey); + } catch (IOException e) { + logger.debug("Failed to decrypt device name, maybe plain text?", e); + } + } + return new Device(d.getId(), deviceName, d.getCreated(), d.getLastSeen()); + }).collect(Collectors.toList()); } public void removeLinkedDevices(int deviceId) throws IOException { accountManager.removeDevice(deviceId); var devices = accountManager.getDevices(); account.setMultiDevice(devices.size() > 1); - account.save(); } public void addDeviceLink(URI linkUri) throws IOException, InvalidKeyException { @@ -440,7 +489,6 @@ public class Manager implements Closeable { Optional.of(account.getProfileKey().serialize()), verificationCode); account.setMultiDevice(true); - account.save(); } public void setRegistrationLockPin(Optional pin) throws IOException, UnauthenticatedResponseException { @@ -454,8 +502,7 @@ public class Manager implements Closeable { pinHelper.setRegistrationLockPin(pin.get(), masterKey); - account.setRegistrationLockPin(pin.get()); - account.setPinMasterKey(masterKey); + account.setRegistrationLockPin(pin.get(), masterKey); } else { // Remove legacy registration lock accountManager.removeRegistrationLockV1(); @@ -463,10 +510,8 @@ public class Manager implements Closeable { // Remove KBS Pin pinHelper.removeRegistrationLockPin(); - account.setRegistrationLockPin(null); - account.setPinMasterKey(null); + account.setRegistrationLockPin(null, null); } - account.save(); } void refreshPreKeys() throws IOException { @@ -516,6 +561,7 @@ public class Manager implements Closeable { account.getPassword(), account.getDeviceId(), account.getSignalProtocolStore(), + sessionLock, userAgent, account.isMultiDevice(), Optional.fromNullable(messagePipe), @@ -527,99 +573,125 @@ public class Manager implements Closeable { ServiceConfig.AUTOMATIC_NETWORK_RETRY); } - public SignalProfile getRecipientProfile( - SignalServiceAddress address + public Profile getRecipientProfile( + RecipientId recipientId ) { - return getRecipientProfile(address, false); + return getRecipientProfile(recipientId, false); } - SignalProfile getRecipientProfile( - SignalServiceAddress address, boolean force + private final Set pendingProfileRequest = new HashSet<>(); + + Profile getRecipientProfile( + RecipientId recipientId, boolean force ) { - var profileEntry = account.getProfileStore().getProfileEntry(address); - if (profileEntry == null) { - // retrieve profile to get identity key - retrieveEncryptedProfile(address); - return null; + var profile = account.getProfileStore().getProfile(recipientId); + + var now = System.currentTimeMillis(); + // Profiles are cached for 24h before retrieving them again, unless forced + if (!force && profile != null && now - profile.getLastUpdateTimestamp() < 24 * 60 * 60 * 1000) { + return profile; } - var now = new Date().getTime(); - // Profiles are cached for 24h before retrieving them again - if (!profileEntry.isRequestPending() && ( - force - || profileEntry.getProfile() == null - || now - profileEntry.getLastUpdateTimestamp() > 24 * 60 * 60 * 1000 - )) { - profileEntry.setRequestPending(true); - final SignalServiceProfile encryptedProfile; - try { - encryptedProfile = retrieveEncryptedProfile(address); - } finally { - profileEntry.setRequestPending(false); + + synchronized (pendingProfileRequest) { + if (pendingProfileRequest.contains(recipientId)) { + return profile; } - if (encryptedProfile == null) { - return null; + pendingProfileRequest.add(recipientId); + } + final SignalServiceProfile encryptedProfile; + try { + encryptedProfile = retrieveEncryptedProfile(recipientId); + } finally { + synchronized (pendingProfileRequest) { + pendingProfileRequest.remove(recipientId); } + } + if (encryptedProfile == null) { + return null; + } - final var profileKey = profileEntry.getProfileKey(); - final var profile = decryptProfileAndDownloadAvatar(address, profileKey, encryptedProfile); - account.getProfileStore() - .updateProfile(address, profileKey, now, profile, profileEntry.getProfileKeyCredential()); - return profile; + var profileKey = account.getProfileStore().getProfileKey(recipientId); + if (profileKey == null) { + profile = new Profile(System.currentTimeMillis(), + null, + null, + null, + null, + ProfileUtils.getUnidentifiedAccessMode(encryptedProfile, null), + ProfileUtils.getCapabilities(encryptedProfile)); + } else { + profile = decryptProfileAndDownloadAvatar(recipientId, profileKey, encryptedProfile); } - return profileEntry.getProfile(); + account.getProfileStore().storeProfile(recipientId, profile); + + return profile; } - private SignalServiceProfile retrieveEncryptedProfile(SignalServiceAddress address) { + private SignalServiceProfile retrieveEncryptedProfile(RecipientId recipientId) { try { - final var profile = profileHelper.retrieveProfileSync(address, SignalServiceProfile.RequestType.PROFILE) - .getProfile(); - try { - account.getIdentityKeyStore() - .saveIdentity(resolveRecipient(address), - new IdentityKey(Base64.getDecoder().decode(profile.getIdentityKey())), - new Date()); - } catch (InvalidKeyException ignored) { - logger.warn("Got invalid identity key in profile for {}", address.getLegacyIdentifier()); - } - return profile; + return retrieveProfileAndCredential(recipientId, SignalServiceProfile.RequestType.PROFILE).getProfile(); } catch (IOException e) { logger.warn("Failed to retrieve profile, ignoring: {}", e.getMessage()); return null; } } - private ProfileKeyCredential getRecipientProfileKeyCredential(SignalServiceAddress address) { - var profileEntry = account.getProfileStore().getProfileEntry(address); - if (profileEntry == null) { + private ProfileAndCredential retrieveProfileAndCredential( + final RecipientId recipientId, final SignalServiceProfile.RequestType requestType + ) throws IOException { + final var profileAndCredential = profileHelper.retrieveProfileSync(recipientId, requestType); + final var profile = profileAndCredential.getProfile(); + + try { + var newIdentity = account.getIdentityKeyStore() + .saveIdentity(recipientId, + new IdentityKey(Base64.getDecoder().decode(profile.getIdentityKey())), + new Date()); + + if (newIdentity) { + account.getSessionStore().archiveSessions(recipientId); + } + } catch (InvalidKeyException ignored) { + logger.warn("Got invalid identity key in profile for {}", + resolveSignalServiceAddress(recipientId).getIdentifier()); + } + return profileAndCredential; + } + + private ProfileKeyCredential getRecipientProfileKeyCredential(RecipientId recipientId) { + var profileKeyCredential = account.getProfileStore().getProfileKeyCredential(recipientId); + if (profileKeyCredential != null) { + return profileKeyCredential; + } + + ProfileAndCredential profileAndCredential; + try { + profileAndCredential = retrieveProfileAndCredential(recipientId, + SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL); + } catch (IOException e) { + logger.warn("Failed to retrieve profile key credential, ignoring: {}", e.getMessage()); return null; } - if (profileEntry.getProfileKeyCredential() == null) { - ProfileAndCredential profileAndCredential; - try { - profileAndCredential = profileHelper.retrieveProfileSync(address, - SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL); - } catch (IOException e) { - logger.warn("Failed to retrieve profile key credential, ignoring: {}", e.getMessage()); - return null; - } - var now = new Date().getTime(); - final var profileKeyCredential = profileAndCredential.getProfileKeyCredential().orNull(); - final var profile = decryptProfileAndDownloadAvatar(address, - profileEntry.getProfileKey(), + profileKeyCredential = profileAndCredential.getProfileKeyCredential().orNull(); + account.getProfileStore().storeProfileKeyCredential(recipientId, profileKeyCredential); + + var profileKey = account.getProfileStore().getProfileKey(recipientId); + if (profileKey != null) { + final var profile = decryptProfileAndDownloadAvatar(recipientId, + profileKey, profileAndCredential.getProfile()); - account.getProfileStore() - .updateProfile(address, profileEntry.getProfileKey(), now, profile, profileKeyCredential); - return profileKeyCredential; + account.getProfileStore().storeProfile(recipientId, profile); } - return profileEntry.getProfileKeyCredential(); + + return profileKeyCredential; } - private SignalProfile decryptProfileAndDownloadAvatar( - final SignalServiceAddress address, final ProfileKey profileKey, final SignalServiceProfile encryptedProfile + private Profile decryptProfileAndDownloadAvatar( + final RecipientId recipientId, final ProfileKey profileKey, final SignalServiceProfile encryptedProfile ) { if (encryptedProfile.getAvatar() != null) { - downloadProfileAvatar(address, encryptedProfile.getAvatar(), profileKey); + downloadProfileAvatar(resolveSignalServiceAddress(recipientId), encryptedProfile.getAvatar(), profileKey); } return ProfileUtils.decryptProfile(profileKey, encryptedProfile); @@ -648,7 +720,7 @@ public class Manager implements Closeable { if (g == null) { throw new GroupNotFoundException(groupId); } - if (!g.isMember(account.getSelfAddress())) { + if (!g.isMember(account.getSelfRecipientId())) { throw new NotAGroupMemberException(groupId, g.getTitle()); } return g; @@ -659,7 +731,7 @@ public class Manager implements Closeable { if (g == null) { throw new GroupNotFoundException(groupId); } - if (!g.isMember(account.getSelfAddress()) && !g.isPendingMember(account.getSelfAddress())) { + if (!g.isMember(account.getSelfRecipientId()) && !g.isPendingMember(account.getSelfRecipientId())) { throw new NotAGroupMemberException(groupId, g.getTitle()); } return g; @@ -683,9 +755,10 @@ public class Manager implements Closeable { public Pair> sendGroupMessageReaction( String emoji, boolean remove, String targetAuthor, long targetSentTimestamp, GroupId groupId ) throws IOException, InvalidNumberException, NotAGroupMemberException, GroupNotFoundException { + var targetAuthorRecipientId = canonicalizeAndResolveRecipient(targetAuthor); var reaction = new SignalServiceDataMessage.Reaction(emoji, remove, - canonicalizeAndResolveSignalServiceAddress(targetAuthor), + resolveSignalServiceAddress(targetAuthorRecipientId), targetSentTimestamp); final var messageBuilder = SignalServiceDataMessage.newBuilder().withReaction(reaction); @@ -700,10 +773,12 @@ public class Manager implements Closeable { GroupUtils.setGroupContext(messageBuilder, g); messageBuilder.withExpiration(g.getMessageExpirationTime()); - return sendMessage(messageBuilder, g.getMembersWithout(account.getSelfAddress())); + return sendMessage(messageBuilder, g.getMembersWithout(account.getSelfRecipientId())); } - public Pair> sendQuitGroupMessage(GroupId groupId) throws GroupNotFoundException, IOException, NotAGroupMemberException { + public Pair> sendQuitGroupMessage( + GroupId groupId, Set groupAdmins + ) throws GroupNotFoundException, IOException, NotAGroupMemberException, InvalidNumberException, LastGroupAdminException { SignalServiceDataMessage.Builder messageBuilder; final var g = getGroupForUpdating(groupId); @@ -711,118 +786,188 @@ public class Manager implements Closeable { var groupInfoV1 = (GroupInfoV1) g; var group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.QUIT).withId(groupId.serialize()).build(); messageBuilder = SignalServiceDataMessage.newBuilder().asGroupMessage(group); - groupInfoV1.removeMember(account.getSelfAddress()); + groupInfoV1.removeMember(account.getSelfRecipientId()); account.getGroupStore().updateGroup(groupInfoV1); } else { final var groupInfoV2 = (GroupInfoV2) g; - final var groupGroupChangePair = groupHelper.leaveGroup(groupInfoV2); - groupInfoV2.setGroup(groupGroupChangePair.first()); + final var currentAdmins = g.getAdminMembers(); + final var newAdmins = getSignalServiceAddresses(groupAdmins); + newAdmins.removeAll(currentAdmins); + newAdmins.retainAll(g.getMembers()); + if (currentAdmins.contains(getSelfRecipientId()) + && currentAdmins.size() == 1 + && g.getMembers().size() > 1 + && newAdmins.size() == 0) { + // Last admin can't leave the group, unless she's also the last member + throw new LastGroupAdminException(g.getGroupId(), g.getTitle()); + } + final var groupGroupChangePair = groupV2Helper.leaveGroup(groupInfoV2, newAdmins); + groupInfoV2.setGroup(groupGroupChangePair.first(), this::resolveRecipient); messageBuilder = getGroupUpdateMessageBuilder(groupInfoV2, groupGroupChangePair.second().toByteArray()); account.getGroupStore().updateGroup(groupInfoV2); } - return sendMessage(messageBuilder, g.getMembersWithout(account.getSelfAddress())); + return sendMessage(messageBuilder, g.getMembersWithout(account.getSelfRecipientId())); } - public Pair> updateGroup( - GroupId groupId, String name, List members, File avatarFile - ) throws IOException, GroupNotFoundException, AttachmentInvalidException, InvalidNumberException, NotAGroupMemberException { - return sendUpdateGroupMessage(groupId, - name, - members == null ? null : getSignalServiceAddresses(members), - avatarFile); + public Pair> createGroup( + String name, List members, File avatarFile + ) throws IOException, AttachmentInvalidException, InvalidNumberException { + return createGroup(name, members == null ? null : getSignalServiceAddresses(members), avatarFile); } - private Pair> sendUpdateGroupMessage( - GroupId groupId, String name, Collection members, File avatarFile - ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException { - GroupInfo g; + private Pair> createGroup( + String name, Set 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 (groupId == null) { - // Create new group - var gv2 = groupHelper.createGroupV2(name == null ? "" : name, - members == null ? List.of() : members, - avatarFile); - if (gv2 == null) { - var gv1 = new GroupInfoV1(GroupIdV1.createRandom()); - gv1.addMembers(List.of(account.getSelfAddress())); - updateGroupV1(gv1, name, members, avatarFile); - messageBuilder = getGroupUpdateMessageBuilder(gv1); - g = gv1; - } else { - 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> result = null; - if (groupInfoV2.isPendingMember(getSelfAddress())) { - var groupGroupChangePair = groupHelper.acceptInvite(groupInfoV2); - result = sendUpdateGroupMessage(groupInfoV2, - groupGroupChangePair.first(), - groupGroupChangePair.second()); - } + 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()); + } - if (members != null) { - final var newMembers = new HashSet<>(members); - newMembers.removeAll(group.getMembers() - .stream() - .map(this::resolveSignalServiceAddress) - .collect(Collectors.toSet())); - 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()); - } + final var gv2 = gv2Pair.first(); + final var decryptedGroup = gv2Pair.second(); - return new Pair<>(group.getGroupId(), result.second()); - } else { - var gv1 = (GroupInfoV1) group; - updateGroupV1(gv1, name, members, avatarFile); - messageBuilder = getGroupUpdateMessageBuilder(gv1); - g = gv1; + 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> updateGroup( + GroupId groupId, + String name, + String description, + List members, + List removeMembers, + List admins, + List removeAdmins, + boolean resetGroupLink, + GroupLinkState groupLinkState, + GroupPermission addMemberPermission, + GroupPermission editDetailsPermission, + File avatarFile, + Integer expirationTimer + ) throws IOException, GroupNotFoundException, AttachmentInvalidException, InvalidNumberException, NotAGroupMemberException { + return updateGroup(groupId, + name, + description, + members == null ? null : getSignalServiceAddresses(members), + removeMembers == null ? null : getSignalServiceAddresses(removeMembers), + admins == null ? null : getSignalServiceAddresses(admins), + removeAdmins == null ? null : getSignalServiceAddresses(removeAdmins), + resetGroupLink, + groupLinkState, + addMemberPermission, + editDetailsPermission, + avatarFile, + expirationTimer); + } + + private Pair> updateGroup( + final GroupId groupId, + final String name, + final String description, + final Set members, + final Set removeMembers, + final Set admins, + final Set removeAdmins, + final boolean resetGroupLink, + final GroupLinkState groupLinkState, + final GroupPermission addMemberPermission, + final GroupPermission editDetailsPermission, + final File avatarFile, + final Integer expirationTimer + ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException { + var group = getGroupForUpdating(groupId); + + if (group instanceof GroupInfoV2) { + try { + return updateGroupV2((GroupInfoV2) group, + name, + description, + members, + removeMembers, + admins, + removeAdmins, + resetGroupLink, + groupLinkState, + addMemberPermission, + editDetailsPermission, + avatarFile, + expirationTimer); + } catch (ConflictException e) { + // Detected conflicting update, refreshing group and trying again + group = getGroup(groupId, true); + return updateGroupV2((GroupInfoV2) group, + name, + description, + members, + removeMembers, + admins, + removeAdmins, + resetGroupLink, + groupLinkState, + addMemberPermission, + editDetailsPermission, + avatarFile, + expirationTimer); } } - account.getGroupStore().updateGroup(g); + final var gv1 = (GroupInfoV1) group; + final var result = updateGroupV1(gv1, name, members, avatarFile); + if (expirationTimer != null) { + setExpirationTimer(gv1, expirationTimer); + } + return result; + } - final var result = sendMessage(messageBuilder, g.getMembersIncludingPendingWithout(account.getSelfAddress())); - return new Pair<>(g.getGroupId(), result.second()); + private Pair> updateGroupV1( + final GroupInfoV1 gv1, final String name, final Set members, final File avatarFile + ) throws IOException, AttachmentInvalidException { + updateGroupV1Details(gv1, name, members, avatarFile); + var messageBuilder = getGroupUpdateMessageBuilder(gv1); + + account.getGroupStore().updateGroup(gv1); + + return sendMessage(messageBuilder, gv1.getMembersIncludingPendingWithout(account.getSelfRecipientId())); } - private void updateGroupV1( - final GroupInfoV1 g, - final String name, - final Collection members, - final File avatarFile + private void updateGroupV1Details( + final GroupInfoV1 g, final String name, final Collection members, final File avatarFile ) throws IOException { if (name != null) { g.name = name; } if (members != null) { + final var newMemberAddresses = members.stream() + .filter(member -> !g.isMember(member)) + .map(this::resolveSignalServiceAddress) + .collect(Collectors.toList()); final var newE164Members = new HashSet(); - for (var member : members) { - if (g.isMember(member) || !member.getNumber().isPresent()) { + for (var member : newMemberAddresses) { + if (!member.getNumber().isPresent()) { continue; } newE164Members.add(member.getNumber().get()); @@ -846,18 +991,123 @@ public class Manager implements Closeable { } } - public Pair> joinGroup( - GroupInviteLinkUrl inviteLinkUrl - ) throws IOException, GroupLinkNotActiveException { - return sendJoinGroupMessage(inviteLinkUrl); + private Pair> updateGroupV2( + final GroupInfoV2 group, + final String name, + final String description, + final Set members, + final Set removeMembers, + final Set admins, + final Set removeAdmins, + final boolean resetGroupLink, + final GroupLinkState groupLinkState, + final GroupPermission addMemberPermission, + final GroupPermission editDetailsPermission, + final File avatarFile, + Integer expirationTimer + ) throws IOException { + Pair> 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 (admins != null) { + final var newAdmins = new HashSet<>(admins); + newAdmins.retainAll(group.getMembers()); + newAdmins.removeAll(group.getAdminMembers()); + if (newAdmins.size() > 0) { + for (var admin : newAdmins) { + var groupGroupChangePair = groupV2Helper.setMemberAdmin(group, admin, true); + result = sendUpdateGroupV2Message(group, + groupGroupChangePair.first(), + groupGroupChangePair.second()); + } + } + } + + if (removeAdmins != null) { + final var existingRemoveAdmins = new HashSet<>(removeAdmins); + existingRemoveAdmins.retainAll(group.getAdminMembers()); + if (existingRemoveAdmins.size() > 0) { + for (var admin : existingRemoveAdmins) { + var groupGroupChangePair = groupV2Helper.setMemberAdmin(group, admin, false); + result = sendUpdateGroupV2Message(group, + groupGroupChangePair.first(), + groupGroupChangePair.second()); + } + } + } + + if (resetGroupLink) { + var groupGroupChangePair = groupV2Helper.resetGroupLinkPassword(group); + result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); + } + + if (groupLinkState != null) { + var groupGroupChangePair = groupV2Helper.setGroupLinkState(group, groupLinkState); + result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); + } + + if (addMemberPermission != null) { + var groupGroupChangePair = groupV2Helper.setAddMemberPermission(group, addMemberPermission); + result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); + } + + if (editDetailsPermission != null) { + var groupGroupChangePair = groupV2Helper.setEditDetailsPermission(group, editDetailsPermission); + result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); + } + + if (expirationTimer != null) { + var groupGroupChangePair = groupV2Helper.setMessageExpirationTimer(group, expirationTimer); + result = sendUpdateGroupV2Message(group, groupGroupChangePair.first(), groupGroupChangePair.second()); + } + + if (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> sendJoinGroupMessage( + public Pair> 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(), @@ -869,11 +1119,24 @@ public class Manager implements Closeable { 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> 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()); } @@ -896,15 +1159,6 @@ public class Manager implements Closeable { } } - private Pair> sendUpdateGroupMessage( - GroupInfoV2 group, DecryptedGroup newDecryptedGroup, GroupChange groupChange - ) throws IOException { - group.setGroup(newDecryptedGroup); - final var messageBuilder = getGroupUpdateMessageBuilder(group, groupChange.toByteArray()); - account.getGroupStore().updateGroup(group); - return sendMessage(messageBuilder, group.getMembersIncludingPendingWithout(account.getSelfAddress())); - } - Pair> sendGroupInfoMessage( GroupIdV1 groupId, SignalServiceAddress recipient ) throws IOException, NotAGroupMemberException, GroupNotFoundException, AttachmentInvalidException { @@ -915,21 +1169,25 @@ public class Manager implements Closeable { } g = (GroupInfoV1) group; - if (!g.isMember(recipient)) { + final var recipientId = resolveRecipient(recipient); + if (!g.isMember(recipientId)) { throw new NotAGroupMemberException(groupId, g.name); } var messageBuilder = getGroupUpdateMessageBuilder(g); // Send group message only to the recipient who requested it - return sendMessage(messageBuilder, List.of(recipient)); + return sendMessage(messageBuilder, Set.of(recipientId)); } private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfoV1 g) throws AttachmentInvalidException { var group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.UPDATE) .withId(g.getGroupId().serialize()) .withName(g.name) - .withMembers(new ArrayList<>(g.getMembers())); + .withMembers(g.getMembers() + .stream() + .map(this::resolveSignalServiceAddress) + .collect(Collectors.toList())); try { final var attachment = createGroupAvatarAttachment(g.getGroupId()); @@ -962,7 +1220,7 @@ public class Manager implements Closeable { var messageBuilder = SignalServiceDataMessage.newBuilder().asGroupMessage(group.build()); // Send group info request message to the recipient who sent us a message with this groupId - return sendMessage(messageBuilder, List.of(recipient)); + return sendMessage(messageBuilder, Set.of(resolveRecipient(recipient))); } void sendReceipt( @@ -973,7 +1231,7 @@ public class Manager implements Closeable { System.currentTimeMillis()); createMessageSender().sendReceipt(remoteAddress, - unidentifiedAccessHelper.getAccessFor(remoteAddress), + unidentifiedAccessHelper.getAccessFor(resolveRecipient(remoteAddress)), receiptMessage); } @@ -1029,9 +1287,10 @@ public class Manager implements Closeable { public Pair> sendMessageReaction( String emoji, boolean remove, String targetAuthor, long targetSentTimestamp, List recipients ) throws IOException, InvalidNumberException { + var targetAuthorRecipientId = canonicalizeAndResolveRecipient(targetAuthor); var reaction = new SignalServiceDataMessage.Reaction(emoji, remove, - canonicalizeAndResolveSignalServiceAddress(targetAuthor), + resolveSignalServiceAddress(targetAuthorRecipientId), targetSentTimestamp); final var messageBuilder = SignalServiceDataMessage.newBuilder().withReaction(reaction); return sendMessage(messageBuilder, getSignalServiceAddresses(recipients)); @@ -1047,43 +1306,45 @@ public class Manager implements Closeable { for (var address : signalServiceAddresses) { handleEndSession(address); } - account.save(); throw e; } } - public String getContactName(String number) throws InvalidNumberException { - var contact = account.getContactStore().getContact(canonicalizeAndResolveSignalServiceAddress(number)); - if (contact == null) { - return ""; - } else { - return contact.name; + void renewSession(RecipientId recipientId) throws IOException { + account.getSessionStore().archiveSessions(recipientId); + if (!recipientId.equals(getSelfRecipientId())) { + sendNullMessage(recipientId); } } - public void setContactName(String number, String name) throws InvalidNumberException { - final var address = canonicalizeAndResolveSignalServiceAddress(number); - var contact = account.getContactStore().getContact(address); - if (contact == null) { - contact = new ContactInfo(address); - } - contact.name = name; - account.getContactStore().updateContact(contact); - account.save(); + public String getContactName(String number) throws InvalidNumberException { + var contact = account.getContactStore().getContact(canonicalizeAndResolveRecipient(number)); + return contact == null || contact.getName() == null ? "" : contact.getName(); } - public void setContactBlocked(String number, boolean blocked) throws InvalidNumberException { - setContactBlocked(canonicalizeAndResolveSignalServiceAddress(number), blocked); + public void setContactName(String number, String name) throws InvalidNumberException, NotMasterDeviceException { + if (!account.isMasterDevice()) { + throw new NotMasterDeviceException(); + } + final var recipientId = canonicalizeAndResolveRecipient(number); + var contact = account.getContactStore().getContact(recipientId); + final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact); + account.getContactStore().storeContact(recipientId, builder.withName(name).build()); } - private void setContactBlocked(SignalServiceAddress address, boolean blocked) { - var contact = account.getContactStore().getContact(address); - if (contact == null) { - contact = new ContactInfo(address); + public void setContactBlocked( + String number, boolean blocked + ) throws InvalidNumberException, NotMasterDeviceException { + if (!account.isMasterDevice()) { + throw new NotMasterDeviceException(); } - contact.blocked = blocked; - account.getContactStore().updateContact(contact); - account.save(); + setContactBlocked(canonicalizeAndResolveRecipient(number), blocked); + } + + private void setContactBlocked(RecipientId recipientId, boolean blocked) { + var contact = account.getContactStore().getContact(recipientId); + final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact); + account.getContactStore().storeContact(recipientId, builder.withBlocked(blocked).build()); } public void setGroupBlocked(final GroupId groupId, final boolean blocked) throws GroupNotFoundException { @@ -1094,23 +1355,21 @@ public class Manager implements Closeable { group.setBlocked(blocked); account.getGroupStore().updateGroup(group); - account.save(); } - /** - * Change the expiration timer for a contact - */ - public void setExpirationTimer(SignalServiceAddress address, int messageExpirationTimer) throws IOException { - var contact = account.getContactStore().getContact(address); - contact.messageExpirationTime = messageExpirationTimer; - account.getContactStore().updateContact(contact); - sendExpirationTimerUpdate(address); - account.save(); + private void setExpirationTimer(RecipientId recipientId, int messageExpirationTimer) { + var contact = account.getContactStore().getContact(recipientId); + if (contact != null && contact.getMessageExpirationTime() == messageExpirationTimer) { + return; + } + final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact); + account.getContactStore() + .storeContact(recipientId, builder.withMessageExpirationTime(messageExpirationTimer).build()); } - private void sendExpirationTimerUpdate(SignalServiceAddress address) throws IOException { + private void sendExpirationTimerUpdate(RecipientId recipientId) throws IOException { final var messageBuilder = SignalServiceDataMessage.newBuilder().asExpirationUpdate(); - sendMessage(messageBuilder, List.of(address)); + sendMessage(messageBuilder, Set.of(recipientId)); } /** @@ -1119,22 +1378,25 @@ public class Manager implements Closeable { public void setExpirationTimer( String number, int messageExpirationTimer ) throws IOException, InvalidNumberException { - var address = canonicalizeAndResolveSignalServiceAddress(number); - setExpirationTimer(address, messageExpirationTimer); + var recipientId = canonicalizeAndResolveRecipient(number); + setExpirationTimer(recipientId, messageExpirationTimer); + sendExpirationTimerUpdate(recipientId); } /** * Change the expiration timer for a group */ - public void setExpirationTimer(GroupId groupId, int messageExpirationTimer) { - var g = getGroup(groupId); - if (g instanceof GroupInfoV1) { - var groupInfoV1 = (GroupInfoV1) g; - groupInfoV1.messageExpirationTime = messageExpirationTimer; - account.getGroupStore().updateGroup(groupInfoV1); - } else { - throw new RuntimeException("TODO Not implemented!"); - } + private void setExpirationTimer( + GroupInfoV1 groupInfoV1, int messageExpirationTimer + ) throws NotAGroupMemberException, GroupNotFoundException, IOException { + groupInfoV1.messageExpirationTime = messageExpirationTimer; + account.getGroupStore().updateGroup(groupInfoV1); + sendExpirationTimerUpdate(groupInfoV1.getGroupId()); + } + + private void sendExpirationTimerUpdate(GroupIdV1 groupId) throws IOException, NotAGroupMemberException, GroupNotFoundException { + final var messageBuilder = SignalServiceDataMessage.newBuilder().asExpirationUpdate(); + sendGroupMessage(messageBuilder, groupId); } /** @@ -1151,9 +1413,8 @@ public class Manager implements Closeable { var packKey = KeyUtils.createStickerUploadKey(); var packId = messageSender.uploadStickerManifest(manifest, packKey); - var sticker = new Sticker(Hex.fromStringCondensed(packId), packKey); + var sticker = new Sticker(StickerPackId.deserialize(Hex.fromStringCondensed(packId)), packKey); account.getStickerStore().updateSticker(sticker); - account.save(); try { return new URI("https", @@ -1167,7 +1428,15 @@ public class Manager implements Closeable { } } - void requestSyncGroups() throws IOException { + public void requestAllSyncData() throws IOException { + requestSyncGroups(); + requestSyncContacts(); + requestSyncBlocked(); + requestSyncConfiguration(); + requestSyncKeys(); + } + + private void requestSyncGroups() throws IOException { var r = SignalServiceProtos.SyncMessage.Request.newBuilder() .setType(SignalServiceProtos.SyncMessage.Request.Type.GROUPS) .build(); @@ -1179,7 +1448,7 @@ public class Manager implements Closeable { } } - void requestSyncContacts() throws IOException { + private void requestSyncContacts() throws IOException { var r = SignalServiceProtos.SyncMessage.Request.newBuilder() .setType(SignalServiceProtos.SyncMessage.Request.Type.CONTACTS) .build(); @@ -1191,7 +1460,7 @@ public class Manager implements Closeable { } } - void requestSyncBlocked() throws IOException { + private void requestSyncBlocked() throws IOException { var r = SignalServiceProtos.SyncMessage.Request.newBuilder() .setType(SignalServiceProtos.SyncMessage.Request.Type.BLOCKED) .build(); @@ -1203,7 +1472,7 @@ public class Manager implements Closeable { } } - void requestSyncConfiguration() throws IOException { + private void requestSyncConfiguration() throws IOException { var r = SignalServiceProtos.SyncMessage.Request.newBuilder() .setType(SignalServiceProtos.SyncMessage.Request.Type.CONFIGURATION) .build(); @@ -1215,7 +1484,7 @@ public class Manager implements Closeable { } } - void requestSyncKeys() throws IOException { + private void requestSyncKeys() throws IOException { var r = SignalServiceProtos.SyncMessage.Request.newBuilder() .setType(SignalServiceProtos.SyncMessage.Request.Type.KEYS) .build(); @@ -1228,11 +1497,13 @@ public class Manager implements Closeable { } 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; @@ -1246,12 +1517,12 @@ public class Manager implements Closeable { messageSender.sendMessage(message, unidentifiedAccessHelper.getAccessForSync()); } - private Collection getSignalServiceAddresses(Collection numbers) throws InvalidNumberException { + private Set getSignalServiceAddresses(Collection numbers) throws InvalidNumberException { final var signalServiceAddresses = new HashSet(numbers.size()); final var addressesMissingUuid = new HashSet(); for (var number : numbers) { - final var resolvedAddress = canonicalizeAndResolveSignalServiceAddress(number); + final var resolvedAddress = resolveSignalServiceAddress(canonicalizeAndResolveRecipient(number)); if (resolvedAddress.getUuid().isPresent()) { signalServiceAddresses.add(resolvedAddress); } else { @@ -1273,21 +1544,32 @@ public class Manager implements Closeable { for (var address : addressesMissingUuid) { final var number = address.getNumber().get(); if (registeredUsers.containsKey(number)) { - final var newAddress = resolveSignalServiceAddress(new SignalServiceAddress(registeredUsers.get(number), - number)); + final var newAddress = resolveSignalServiceAddress(resolveRecipientTrusted(new SignalServiceAddress( + registeredUsers.get(number), + number))); signalServiceAddresses.add(newAddress); } else { signalServiceAddresses.add(address); } } - return signalServiceAddresses; + return signalServiceAddresses.stream().map(this::resolveRecipient).collect(Collectors.toSet()); } - private Map getRegisteredUsers(final Set numbersMissingUuid) throws IOException { + private RecipientId refreshRegisteredUser(RecipientId recipientId) throws IOException { + final var address = resolveSignalServiceAddress(recipientId); + if (!address.getNumber().isPresent()) { + return recipientId; + } + final var number = address.getNumber().get(); + final var uuidMap = getRegisteredUsers(Set.of(number)); + return resolveRecipientTrusted(new SignalServiceAddress(uuidMap.getOrDefault(number, null), number)); + } + + private Map getRegisteredUsers(final Set numbers) throws IOException { try { return accountManager.getRegisteredUsers(ServiceConfig.getIasKeyStore(), - numbersMissingUuid, + numbers, serviceEnvironmentConfig.getCdsMrenclave()); } catch (Quote.InvalidQuoteFormatException | UnauthenticatedQuoteException | SignatureException | UnauthenticatedResponseException | InvalidKeyException e) { throw new IOException(e); @@ -1295,9 +1577,8 @@ public class Manager implements Closeable { } private Pair> sendMessage( - SignalServiceDataMessage.Builder messageBuilder, Collection recipients + SignalServiceDataMessage.Builder messageBuilder, Set recipientIds ) throws IOException { - recipients = recipients.stream().map(this::resolveSignalServiceAddress).collect(Collectors.toSet()); final var timestamp = System.currentTimeMillis(); messageBuilder.withTimestamp(timestamp); getOrCreateMessagePipe(); @@ -1309,17 +1590,23 @@ public class Manager implements Closeable { try { var messageSender = createMessageSender(); final var isRecipientUpdate = false; - var result = messageSender.sendMessage(new ArrayList<>(recipients), - unidentifiedAccessHelper.getAccessFor(recipients), + final var recipientIdList = new ArrayList<>(recipientIds); + final var addresses = recipientIdList.stream() + .map(this::resolveSignalServiceAddress) + .collect(Collectors.toList()); + var result = messageSender.sendMessage(addresses, + unidentifiedAccessHelper.getAccessFor(recipientIdList), isRecipientUpdate, message); for (var r : result) { if (r.getIdentityFailure() != null) { - account.getIdentityKeyStore(). - saveIdentity(resolveRecipient(r.getAddress()), - r.getIdentityFailure().getIdentityKey(), - new Date()); + final var recipientId = resolveRecipient(r.getAddress()); + final var newIdentity = account.getIdentityKeyStore() + .saveIdentity(recipientId, r.getIdentityFailure().getIdentityKey(), new Date()); + if (newIdentity) { + account.getSessionStore().archiveSessions(recipientId); + } } } @@ -1330,23 +1617,22 @@ public class Manager implements Closeable { } else { // Send to all individually, so sync messages are sent correctly messageBuilder.withProfileKey(account.getProfileKey().serialize()); - var results = new ArrayList(recipients.size()); - for (var address : recipients) { - final var contact = account.getContactStore().getContact(address); - final var expirationTime = contact != null ? contact.messageExpirationTime : 0; + var results = new ArrayList(recipientIds.size()); + for (var recipientId : recipientIds) { + final var contact = account.getContactStore().getContact(recipientId); + final var expirationTime = contact != null ? contact.getMessageExpirationTime() : 0; messageBuilder.withExpiration(expirationTime); message = messageBuilder.build(); - results.add(sendMessage(address, message)); + results.add(sendMessage(recipientId, message)); } return new Pair<>(timestamp, results); } } finally { if (message != null && message.isEndSession()) { - for (var recipient : recipients) { + for (var recipient : recipientIds) { handleEndSession(recipient); } } - account.save(); } } @@ -1357,27 +1643,24 @@ public class Manager implements Closeable { messageBuilder.withTimestamp(timestamp); getOrCreateMessagePipe(); getOrCreateUnidentifiedMessagePipe(); - try { - final var address = getSelfAddress(); + final var recipientId = account.getSelfRecipientId(); - final var contact = account.getContactStore().getContact(address); - final var expirationTime = contact != null ? contact.messageExpirationTime : 0; - messageBuilder.withExpiration(expirationTime); + final var contact = account.getContactStore().getContact(recipientId); + final var expirationTime = contact != null ? contact.getMessageExpirationTime() : 0; + messageBuilder.withExpiration(expirationTime); - var message = messageBuilder.build(); - final var result = sendSelfMessage(message); - return new Pair<>(timestamp, result); - } finally { - account.save(); - } + var message = messageBuilder.build(); + final var result = sendSelfMessage(message); + return new Pair<>(timestamp, result); } private SendMessageResult sendSelfMessage(SignalServiceDataMessage message) throws IOException { var messageSender = createMessageSender(); - var recipient = account.getSelfAddress(); + var recipientId = account.getSelfRecipientId(); - final var unidentifiedAccess = unidentifiedAccessHelper.getAccessFor(recipient); + final var unidentifiedAccess = unidentifiedAccessHelper.getAccessFor(recipientId); + var recipient = resolveSignalServiceAddress(recipientId); var transcript = new SentTranscriptMessage(Optional.of(recipient), message.getTimestamp(), message, @@ -1399,33 +1682,52 @@ public class Manager implements Closeable { } private SendMessageResult sendMessage( - SignalServiceAddress address, SignalServiceDataMessage message + RecipientId recipientId, SignalServiceDataMessage message ) throws IOException { var messageSender = createMessageSender(); + final var address = resolveSignalServiceAddress(recipientId); + try { + try { + return messageSender.sendMessage(address, unidentifiedAccessHelper.getAccessFor(recipientId), message); + } catch (UnregisteredUserException e) { + final var newRecipientId = refreshRegisteredUser(recipientId); + return messageSender.sendMessage(resolveSignalServiceAddress(newRecipientId), + unidentifiedAccessHelper.getAccessFor(newRecipientId), + message); + } + } catch (UntrustedIdentityException e) { + return SendMessageResult.identityFailure(address, e.getIdentityKey()); + } + } + + private SendMessageResult sendNullMessage(RecipientId recipientId) throws IOException { + var messageSender = createMessageSender(); + + final var address = resolveSignalServiceAddress(recipientId); try { - return messageSender.sendMessage(address, unidentifiedAccessHelper.getAccessFor(address), message); + try { + return messageSender.sendNullMessage(address, unidentifiedAccessHelper.getAccessFor(recipientId)); + } catch (UnregisteredUserException e) { + final var newRecipientId = refreshRegisteredUser(recipientId); + final var newAddress = resolveSignalServiceAddress(newRecipientId); + return messageSender.sendNullMessage(newAddress, unidentifiedAccessHelper.getAccessFor(newRecipientId)); + } } catch (UntrustedIdentityException e) { return SendMessageResult.identityFailure(address, e.getIdentityKey()); } } - 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(SignalServiceAddress source) { - account.getSessionStore().deleteAllSessions(source.getIdentifier()); + private void handleEndSession(RecipientId recipientId) { + account.getSessionStore().deleteAllSessions(recipientId); } private List handleSignalServiceDataMessage( @@ -1462,7 +1764,7 @@ public class Manager implements Closeable { groupV1.addMembers(groupInfo.getMembers() .get() .stream() - .map(this::resolveSignalServiceAddress) + .map(this::resolveRecipient) .collect(Collectors.toSet())); } @@ -1476,7 +1778,7 @@ public class Manager implements Closeable { break; case QUIT: { if (groupV1 != null) { - groupV1.removeMember(source); + groupV1.removeMember(resolveRecipient(source)); account.getGroupStore().updateGroup(groupV1); } break; @@ -1503,7 +1805,7 @@ public class Manager implements Closeable { final var conversationPartnerAddress = isSync ? destination : source; if (conversationPartnerAddress != null && message.isEndSession()) { - handleEndSession(conversationPartnerAddress); + handleEndSession(resolveRecipient(conversationPartnerAddress)); } if (message.isExpirationUpdate() || message.getBody().isPresent()) { if (message.getGroupContext().isPresent()) { @@ -1520,14 +1822,7 @@ public class Manager implements Closeable { // disappearing message timer already stored in the DecryptedGroup } } else if (conversationPartnerAddress != null) { - var contact = account.getContactStore().getContact(conversationPartnerAddress); - if (contact == null) { - contact = new ContactInfo(conversationPartnerAddress); - } - if (contact.messageExpirationTime != message.getExpiresInSeconds()) { - contact.messageExpirationTime = message.getExpiresInSeconds(); - account.getContactStore().updateContact(contact); - } + setExpirationTimer(resolveRecipient(conversationPartnerAddress), message.getExpiresInSeconds()); } } if (!ignoreAttachments) { @@ -1554,7 +1849,7 @@ public class Manager implements Closeable { if (source.matches(account.getSelfAddress())) { this.account.setProfileKey(profileKey); } - this.account.getProfileStore().storeProfileKey(source, profileKey); + this.account.getProfileStore().storeProfileKey(resolveRecipient(source), profileKey); } if (message.getPreviews().isPresent()) { final var previews = message.getPreviews().get(); @@ -1576,9 +1871,10 @@ public class Manager implements Closeable { } if (message.getSticker().isPresent()) { final var messageSticker = message.getSticker().get(); - var sticker = account.getStickerStore().getSticker(messageSticker.getPackId()); + final var stickerPackId = StickerPackId.deserialize(messageSticker.getPackId()); + var sticker = account.getStickerStore().getSticker(stickerPackId); if (sticker == null) { - sticker = new Sticker(messageSticker.getPackId(), messageSticker.getPackKey()); + sticker = new Sticker(stickerPackId, messageSticker.getPackKey()); account.getStickerStore().updateSticker(sticker); } } @@ -1595,7 +1891,7 @@ public class Manager implements Closeable { final GroupInfoV2 groupInfoV2; if (groupInfo instanceof GroupInfoV1) { // Received a v2 group message for a v1 group, we need to locally migrate the group - account.getGroupStore().deleteGroup(groupInfo.getGroupId()); + account.getGroupStore().deleteGroupV1(((GroupInfoV1) groupInfo).getGroupId()); groupInfoV2 = new GroupInfoV2(groupId, groupMasterKey); logger.info("Locally migrated group {} to group v2, id: {}", groupInfo.getGroupId().toBase64(), @@ -1611,10 +1907,12 @@ public class Manager implements Closeable { 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); @@ -1623,7 +1921,7 @@ public class Manager implements Closeable { downloadGroupAvatar(groupId, groupSecretParams, avatar); } } - groupInfoV2.setGroup(group); + groupInfoV2.setGroup(group, this::resolveRecipient); account.getGroupStore().updateGroup(groupInfoV2); } @@ -1632,11 +1930,11 @@ public class Manager implements Closeable { private void storeProfileKeysFromMembers(final DecryptedGroup group) { for (var member : group.getMembersList()) { - final var address = resolveSignalServiceAddress(new SignalServiceAddress(UuidUtil.parseOrThrow(member.getUuid() - .toByteArray()), null)); + final var uuid = UuidUtil.parseOrThrow(member.getUuid().toByteArray()); + final var recipientId = account.getRecipientStore().resolveRecipient(uuid); try { account.getProfileStore() - .storeProfileKey(address, new ProfileKey(member.getProfileKey().toByteArray())); + .storeProfileKey(recipientId, new ProfileKey(member.getProfileKey().toByteArray())); } catch (InvalidInputException ignored) { } } @@ -1671,10 +1969,10 @@ public class Manager implements Closeable { if (!envelope.isReceipt()) { try { content = decryptMessage(envelope); - } catch (org.whispersystems.libsignal.UntrustedIdentityException e) { + } catch (ProtocolUntrustedIdentityException e) { if (!envelope.hasSource()) { - final var recipientId = resolveRecipient(((org.whispersystems.libsignal.UntrustedIdentityException) e) - .getName()); + final var identifier = e.getSender(); + final var recipientId = resolveRecipient(identifier); try { account.getMessageCache().replaceSender(cachedMessage, recipientId); } catch (IOException ioException) { @@ -1689,7 +1987,6 @@ public class Manager implements Closeable { } actions = handleMessage(envelope, content, ignoreAttachments); } - account.save(); handler.handleMessage(envelope, content, null); cachedMessage.delete(); return actions; @@ -1715,6 +2012,7 @@ public class Manager implements Closeable { SignalServiceContent content = null; Exception exception = null; final CachedMessage[] cachedMessage = {null}; + account.setLastReceiveTimestamp(System.currentTimeMillis()); try { var result = messagePipe.readOrEmpty(timeout, unit, envelope1 -> { final var recipientId = envelope1.hasSource() @@ -1737,7 +2035,6 @@ public class Manager implements Closeable { logger.warn("Message action failed.", e); } } - account.save(); queuedActions.clear(); queuedActions = null; } @@ -1752,15 +2049,27 @@ public class Manager implements Closeable { 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); if (!envelope.isReceipt()) { try { content = decryptMessage(envelope); } 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()); + logger.debug("Received invalid message, queuing renew session action."); + actions.add(new RenewSessionAction(sender)); + } if (hasCaughtUpWithOldMessages) { for (var action : actions) { try { @@ -1776,19 +2085,18 @@ public class Manager implements Closeable { queuedActions.addAll(actions); } } - account.save(); if (isMessageBlocked(envelope, content)) { logger.info("Ignoring a message from blocked user/group: {}", envelope.getTimestamp()); - } else if (isNotAGroupMember(envelope, content)) { + } else if (notAGroupMember) { logger.info("Ignoring a message from a non group member: {}", envelope.getTimestamp()); } else { handler.handleMessage(envelope, content, exception); } if (cachedMessage[0] != null) { - if (exception instanceof org.whispersystems.libsignal.UntrustedIdentityException) { - final var recipientId = resolveRecipient(((org.whispersystems.libsignal.UntrustedIdentityException) exception) - .getName()); - queuedActions.add(new RetrieveProfileAction(resolveSignalServiceAddress(recipientId))); + if (exception instanceof ProtocolUntrustedIdentityException) { + final var identifier = ((ProtocolUntrustedIdentityException) exception).getSender(); + final var recipientId = resolveRecipient(identifier); + queuedActions.add(new RetrieveProfileAction(recipientId)); if (!envelope.hasSource()) { try { cachedMessage[0] = account.getMessageCache().replaceSender(cachedMessage[0], recipientId); @@ -1815,8 +2123,8 @@ public class Manager implements Closeable { } else { return false; } - var sourceContact = account.getContactStore().getContact(source); - if (sourceContact != null && sourceContact.blocked) { + final var recipientId = resolveRecipient(source); + if (isContactBlocked(recipientId)) { return true; } @@ -1833,6 +2141,16 @@ public class Manager implements Closeable { return false; } + public boolean isContactBlocked(final String identifier) throws InvalidNumberException { + final var recipientId = canonicalizeAndResolveRecipient(identifier); + return isContactBlocked(recipientId); + } + + private boolean isContactBlocked(final RecipientId recipientId) { + var sourceContact = account.getContactStore().getContact(recipientId); + return sourceContact != null && sourceContact.isBlocked(); + } + private boolean isNotAGroupMember( SignalServiceEnvelope envelope, SignalServiceContent content ) { @@ -1856,7 +2174,7 @@ public class Manager implements Closeable { } var groupId = GroupUtils.getGroupId(message.getGroupContext().get()); var group = getGroup(groupId); - if (group != null && !group.isMember(source)) { + if (group != null && !group.isMember(resolveRecipient(source))) { return true; } } @@ -1875,8 +2193,6 @@ public class Manager implements Closeable { } else { sender = content.getSender(); } - // Store uuid if we don't have it already - resolveSignalServiceAddress(sender); if (content.getDataMessage().isPresent()) { var message = content.getDataMessage().get(); @@ -1924,7 +2240,16 @@ public class Manager implements Closeable { try (var attachmentAsStream = retrieveAttachmentAsStream(groupsMessage.asPointer(), tmpFile)) { var s = new DeviceGroupsInputStream(attachmentAsStream); DeviceGroup g; - while ((g = s.read()) != null) { + while (true) { + try { + g = s.read(); + } catch (IOException e) { + logger.warn("Sync groups contained invalid group, ignoring: {}", e.getMessage()); + continue; + } + if (g == null) { + break; + } var syncGroup = account.getGroupStore().getOrCreateGroupV1(GroupId.v1(g.getId())); if (syncGroup != null) { if (g.getName().isPresent()) { @@ -1932,13 +2257,13 @@ public class Manager implements Closeable { } syncGroup.addMembers(g.getMembers() .stream() - .map(this::resolveSignalServiceAddress) + .map(this::resolveRecipient) .collect(Collectors.toSet())); if (!g.isActive()) { - syncGroup.removeMember(account.getSelfAddress()); + syncGroup.removeMember(account.getSelfRecipientId()); } else { // Add ourself to the member set as it's marked as active - syncGroup.addMembers(List.of(account.getSelfAddress())); + syncGroup.addMembers(List.of(account.getSelfRecipientId())); } syncGroup.blocked = g.isBlocked(); if (g.getColor().isPresent()) { @@ -1948,7 +2273,6 @@ public class Manager implements Closeable { if (g.getAvatar().isPresent()) { downloadGroupAvatar(g.getAvatar().get(), syncGroup.getGroupId()); } - syncGroup.inboxPosition = g.getInboxPosition().orNull(); syncGroup.archived = g.isArchived(); account.getGroupStore().updateGroup(syncGroup); } @@ -1973,7 +2297,7 @@ public class Manager implements Closeable { if (syncMessage.getBlockedList().isPresent()) { final var blockedListMessage = syncMessage.getBlockedList().get(); for (var address : blockedListMessage.getAddresses()) { - setContactBlocked(resolveSignalServiceAddress(address), true); + setContactBlocked(resolveRecipient(address), true); } for (var groupId : blockedListMessage.getGroupIds() .stream() @@ -2000,19 +2324,19 @@ public class Manager implements Closeable { if (c.getAddress().matches(account.getSelfAddress()) && c.getProfileKey().isPresent()) { account.setProfileKey(c.getProfileKey().get()); } - final var address = resolveSignalServiceAddress(c.getAddress()); - var contact = account.getContactStore().getContact(address); - if (contact == null) { - contact = new ContactInfo(address); - } + final var recipientId = resolveRecipientTrusted(c.getAddress()); + var contact = account.getContactStore().getContact(recipientId); + final var builder = contact == null + ? Contact.newBuilder() + : Contact.newBuilder(contact); if (c.getName().isPresent()) { - contact.name = c.getName().get(); + builder.withName(c.getName().get()); } if (c.getColor().isPresent()) { - contact.color = c.getColor().get(); + builder.withColor(c.getColor().get()); } if (c.getProfileKey().isPresent()) { - account.getProfileStore().storeProfileKey(address, c.getProfileKey().get()); + account.getProfileStore().storeProfileKey(recipientId, c.getProfileKey().get()); } if (c.getVerified().isPresent()) { final var verifiedMessage = c.getVerified().get(); @@ -2022,15 +2346,14 @@ public class Manager implements Closeable { TrustLevel.fromVerifiedState(verifiedMessage.getVerified())); } if (c.getExpirationTimer().isPresent()) { - contact.messageExpirationTime = c.getExpirationTimer().get(); + builder.withMessageExpirationTime(c.getExpirationTimer().get()); } - contact.blocked = c.isBlocked(); - contact.inboxPosition = c.getInboxPosition().orNull(); - contact.archived = c.isArchived(); - account.getContactStore().updateContact(contact); + builder.withBlocked(c.isBlocked()); + builder.withArchived(c.isArchived()); + account.getContactStore().storeContact(recipientId, builder.build()); if (c.getAvatar().isPresent()) { - downloadContactAvatar(c.getAvatar().get(), contact.getAddress()); + downloadContactAvatar(c.getAvatar().get(), c.getAddress()); } } } @@ -2063,12 +2386,13 @@ public class Manager implements Closeable { if (!m.getPackId().isPresent()) { continue; } - var sticker = account.getStickerStore().getSticker(m.getPackId().get()); + final var stickerPackId = StickerPackId.deserialize(m.getPackId().get()); + var sticker = account.getStickerStore().getSticker(stickerPackId); if (sticker == null) { if (!m.getPackKey().isPresent()) { continue; } - sticker = new Sticker(m.getPackId().get(), m.getPackKey().get()); + sticker = new Sticker(stickerPackId, m.getPackKey().get()); } sticker.setInstalled(!m.getType().isPresent() || m.getType().get() == StickerPackOperationMessage.Type.INSTALL); @@ -2078,7 +2402,7 @@ public class Manager implements Closeable { if (syncMessage.getFetchType().isPresent()) { switch (syncMessage.getFetchType().get()) { case LOCAL_PROFILE: - getRecipientProfile(getSelfAddress(), true); + getRecipientProfile(account.getSelfRecipientId(), true); case STORAGE_MANIFEST: // TODO } @@ -2255,13 +2579,16 @@ public class Manager implements Closeable { var groupInfo = (GroupInfoV1) record; out.write(new DeviceGroup(groupInfo.getGroupId().serialize(), Optional.fromNullable(groupInfo.name), - new ArrayList<>(groupInfo.getMembers()), + groupInfo.getMembers() + .stream() + .map(this::resolveSignalServiceAddress) + .collect(Collectors.toList()), createGroupAvatarAttachment(groupInfo.getGroupId()), - groupInfo.isMember(account.getSelfAddress()), + groupInfo.isMember(account.getSelfRecipientId()), Optional.of(groupInfo.messageExpirationTime), Optional.fromNullable(groupInfo.color), groupInfo.blocked, - Optional.fromNullable(groupInfo.inboxPosition), + Optional.absent(), groupInfo.archived)); } } @@ -2293,28 +2620,31 @@ public class Manager implements Closeable { try { try (OutputStream fos = new FileOutputStream(contactsFile)) { var out = new DeviceContactsOutputStream(fos); - for (var record : account.getContactStore().getContacts()) { + for (var contactPair : account.getContactStore().getContacts()) { + final var recipientId = contactPair.first(); + final var contact = contactPair.second(); + final var address = resolveSignalServiceAddress(recipientId); + + var currentIdentity = account.getIdentityKeyStore().getIdentity(recipientId); VerifiedMessage verifiedMessage = null; - var currentIdentity = account.getIdentityKeyStore() - .getIdentity(resolveRecipientTrusted(record.getAddress())); if (currentIdentity != null) { - verifiedMessage = new VerifiedMessage(record.getAddress(), + verifiedMessage = new VerifiedMessage(address, currentIdentity.getIdentityKey(), currentIdentity.getTrustLevel().toVerifiedState(), currentIdentity.getDateAdded().getTime()); } - var profileKey = account.getProfileStore().getProfileKey(record.getAddress()); - out.write(new DeviceContact(record.getAddress(), - Optional.fromNullable(record.name), - createContactAvatarAttachment(record.getAddress()), - Optional.fromNullable(record.color), + var profileKey = account.getProfileStore().getProfileKey(recipientId); + out.write(new DeviceContact(address, + Optional.fromNullable(contact.getName()), + createContactAvatarAttachment(address), + Optional.fromNullable(contact.getColor()), Optional.fromNullable(verifiedMessage), Optional.fromNullable(profileKey), - record.blocked, - Optional.of(record.messageExpirationTime), - Optional.fromNullable(record.inboxPosition), - record.archived)); + contact.isBlocked(), + Optional.of(contact.getMessageExpirationTime()), + Optional.absent(), + contact.isArchived())); } if (account.getProfileKey() != null) { @@ -2355,8 +2685,8 @@ public class Manager implements Closeable { void sendBlockedList() throws IOException, UntrustedIdentityException { var addresses = new ArrayList(); for (var record : account.getContactStore().getContacts()) { - if (record.blocked) { - addresses.add(record.getAddress()); + if (record.second().isBlocked()) { + addresses.add(resolveSignalServiceAddress(record.first())); } } var groupIds = new ArrayList(); @@ -2378,30 +2708,37 @@ public class Manager implements Closeable { sendSyncMessage(SignalServiceSyncMessage.forVerified(verifiedMessage)); } - public List getContacts() { + public List> getContacts() { return account.getContactStore().getContacts(); } - public String getContactOrProfileName(String number) { - final var address = Utils.getSignalServiceAddressFromIdentifier(number); + public String getContactOrProfileName(String number) throws InvalidNumberException { + final var recipientId = canonicalizeAndResolveRecipient(number); + final var recipient = account.getRecipientStore().getRecipient(recipientId); + if (recipient == null) { + return null; + } - final var contact = account.getContactStore().getContact(address); - if (contact != null && !Util.isEmpty(contact.name)) { - return contact.name; + if (recipient.getContact() != null && !Util.isEmpty(recipient.getContact().getName())) { + return recipient.getContact().getName(); } - final var profileEntry = account.getProfileStore().getProfileEntry(address); - if (profileEntry != null && profileEntry.getProfile() != null) { - return profileEntry.getProfile().getDisplayName(); + if (recipient.getProfile() != null && recipient.getProfile() != null) { + return recipient.getProfile().getDisplayName(); } + return null; } public GroupInfo getGroup(GroupId groupId) { + return getGroup(groupId, false); + } + + public GroupInfo getGroup(GroupId groupId, boolean forceUpdate) { final var group = account.getGroupStore().getGroup(groupId); - if (group instanceof GroupInfoV2 && ((GroupInfoV2) group).getGroup() == null) { + if (group instanceof GroupInfoV2 && (forceUpdate || ((GroupInfoV2) group).getGroup() == null)) { final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(((GroupInfoV2) group).getMasterKey()); - ((GroupInfoV2) group).setGroup(groupHelper.getDecryptedGroup(groupSecretParams)); + ((GroupInfoV2) group).setGroup(groupV2Helper.getDecryptedGroup(groupSecretParams), this::resolveRecipient); account.getGroupStore().updateGroup(group); } return group; @@ -2486,14 +2823,6 @@ public class Manager implements Closeable { theirIdentityKey); } - @Deprecated - public SignalServiceAddress canonicalizeAndResolveSignalServiceAddress(String identifier) throws InvalidNumberException { - var canonicalizedNumber = UuidUtil.isUuid(identifier) - ? identifier - : PhoneNumberFormatter.formatNumber(identifier, account.getUsername()); - return resolveSignalServiceAddress(canonicalizedNumber); - } - @Deprecated public SignalServiceAddress resolveSignalServiceAddress(String identifier) { var address = Utils.getSignalServiceAddressFromIdentifier(identifier); @@ -2529,11 +2858,11 @@ public class Manager implements Closeable { } public RecipientId resolveRecipient(SignalServiceAddress address) { - return account.getRecipientStore().resolveRecipientUntrusted(address); + return account.getRecipientStore().resolveRecipient(address); } private RecipientId resolveRecipientTrusted(SignalServiceAddress address) { - return account.getRecipientStore().resolveRecipient(address); + return account.getRecipientStore().resolveRecipientTrusted(address); } @Override