X-Git-Url: https://git.nmode.ca/signal-cli/blobdiff_plain/a811d1a05a28d332705f30528db882a1031e8257..5bbfd3259891e18a11cb878e14a9c17990b13d79:/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 be31493d..8aa7ed18 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -16,30 +16,39 @@ */ package org.asamk.signal.manager; +import org.asamk.signal.manager.api.Device; +import org.asamk.signal.manager.api.TypingAction; 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.GroupIdV2; 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.SendHelper; import org.asamk.signal.manager.helper.UnidentifiedAccessHelper; +import org.asamk.signal.manager.jobs.Context; +import org.asamk.signal.manager.jobs.Job; +import org.asamk.signal.manager.jobs.RetrieveStickerPackJob; 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.profiles.SignalProfileEntry; -import org.asamk.signal.manager.storage.protocol.IdentityInfo; +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; @@ -57,17 +66,12 @@ import org.signal.libsignal.metadata.ProtocolLegacyMessageException; import org.signal.libsignal.metadata.ProtocolNoSessionException; import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException; import org.signal.libsignal.metadata.SelfSendException; -import org.signal.libsignal.metadata.certificate.CertificateValidator; import org.signal.storageservice.protos.groups.GroupChange; import org.signal.storageservice.protos.groups.local.DecryptedGroup; -import org.signal.storageservice.protos.groups.local.DecryptedGroupJoinInfo; -import org.signal.storageservice.protos.groups.local.DecryptedMember; import org.signal.zkgroup.InvalidInputException; import org.signal.zkgroup.VerificationFailedException; -import org.signal.zkgroup.auth.AuthCredentialResponse; import org.signal.zkgroup.groups.GroupMasterKey; import org.signal.zkgroup.groups.GroupSecretParams; -import org.signal.zkgroup.profiles.ClientZkProfileOperations; import org.signal.zkgroup.profiles.ProfileKey; import org.signal.zkgroup.profiles.ProfileKeyCredential; import org.slf4j.Logger; @@ -76,26 +80,16 @@ import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.IdentityKeyPair; import org.whispersystems.libsignal.InvalidKeyException; import org.whispersystems.libsignal.InvalidMessageException; -import org.whispersystems.libsignal.InvalidVersionException; import org.whispersystems.libsignal.ecc.ECPublicKey; import org.whispersystems.libsignal.state.PreKeyRecord; import org.whispersystems.libsignal.state.SignedPreKeyRecord; import org.whispersystems.libsignal.util.Pair; import org.whispersystems.libsignal.util.guava.Optional; -import org.whispersystems.signalservice.api.KeyBackupService; -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.crypto.SignalServiceCipher; -import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair; +import org.whispersystems.signalservice.api.InvalidMessageStructureException; +import org.whispersystems.signalservice.api.SignalSessionLock; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; -import org.whispersystems.signalservice.api.groupsv2.ClientZkOperations; import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException; -import org.whispersystems.signalservice.api.groupsv2.GroupsV2Api; import org.whispersystems.signalservice.api.groupsv2.GroupsV2AuthorizationString; -import org.whispersystems.signalservice.api.groupsv2.GroupsV2Operations; -import org.whispersystems.signalservice.api.kbs.MasterKey; import org.whispersystems.signalservice.api.messages.SendMessageResult; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; @@ -107,7 +101,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; import org.whispersystems.signalservice.api.messages.SignalServiceGroup; import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2; import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage; -import org.whispersystems.signalservice.api.messages.SignalServiceStickerManifestUpload; +import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage; import org.whispersystems.signalservice.api.messages.multidevice.BlockedListMessage; import org.whispersystems.signalservice.api.messages.multidevice.ContactsMessage; import org.whispersystems.signalservice.api.messages.multidevice.DeviceContact; @@ -116,24 +110,20 @@ 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.KeysMessage; import org.whispersystems.signalservice.api.messages.multidevice.RequestMessage; -import org.whispersystems.signalservice.api.messages.multidevice.SentTranscriptMessage; import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage; import org.whispersystems.signalservice.api.messages.multidevice.StickerPackOperationMessage; 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.storage.StorageKey; +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; -import org.whispersystems.signalservice.api.util.StreamDetails; -import org.whispersystems.signalservice.api.util.UptimeSleepTimer; import org.whispersystems.signalservice.api.util.UuidUtil; +import org.whispersystems.signalservice.api.websocket.WebSocketUnavailableException; import org.whispersystems.signalservice.internal.contacts.crypto.Quote; import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedQuoteException; import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException; @@ -158,17 +148,20 @@ import java.nio.file.Files; import java.security.SignatureException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Base64; import java.util.Collection; import java.util.Date; -import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; 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; import static org.asamk.signal.manager.config.ServiceConfig.capabilities; @@ -177,27 +170,30 @@ public class Manager implements Closeable { private final static Logger logger = LoggerFactory.getLogger(Manager.class); - private final CertificateValidator certificateValidator; - private final ServiceEnvironmentConfig serviceEnvironmentConfig; - private final String userAgent; + private final SignalDependencies dependencies; private SignalAccount account; - private final SignalServiceAccountManager accountManager; - private final GroupsV2Api groupsV2Api; - private final GroupsV2Operations groupsV2Operations; - private final SignalServiceMessageReceiver messageReceiver; - private final ClientZkProfileOperations clientZkProfileOperations; - private SignalServiceMessagePipe messagePipe = null; - private SignalServiceMessagePipe unidentifiedMessagePipe = null; + private final ExecutorService executor = Executors.newCachedThreadPool(); - private final UnidentifiedAccessHelper unidentifiedAccessHelper; private final ProfileHelper profileHelper; - private final GroupHelper groupHelper; + private final GroupV2Helper groupV2Helper; private final PinHelper pinHelper; + private final SendHelper sendHelper; + private final AvatarStore avatarStore; private final AttachmentStore attachmentStore; + private final StickerPackStore stickerPackStore; + 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, @@ -207,63 +203,47 @@ public class Manager implements Closeable { ) { this.account = account; this.serviceEnvironmentConfig = serviceEnvironmentConfig; - this.certificateValidator = new CertificateValidator(serviceEnvironmentConfig.getUnidentifiedSenderTrustRoot()); - this.userAgent = userAgent; - this.groupsV2Operations = capabilities.isGv2() ? new GroupsV2Operations(ClientZkOperations.create( - serviceEnvironmentConfig.getSignalServiceConfiguration())) : null; - final SleepTimer timer = new UptimeSleepTimer(); - this.accountManager = new SignalServiceAccountManager(serviceEnvironmentConfig.getSignalServiceConfiguration(), - new DynamicCredentialsProvider(account.getUuid(), - account.getUsername(), - account.getPassword(), - account.getSignalingKey(), - account.getDeviceId()), - userAgent, - groupsV2Operations, - ServiceConfig.AUTOMATIC_NETWORK_RETRY, - timer); - this.groupsV2Api = accountManager.getGroupsV2Api(); - final KeyBackupService keyBackupService = accountManager.getKeyBackupService(ServiceConfig.getIasKeyStore(), - serviceEnvironmentConfig.getKeyBackupConfig().getEnclaveName(), - serviceEnvironmentConfig.getKeyBackupConfig().getServiceId(), - serviceEnvironmentConfig.getKeyBackupConfig().getMrenclave(), - 10); - - this.pinHelper = new PinHelper(keyBackupService); - this.clientZkProfileOperations = capabilities.isGv2() - ? ClientZkOperations.create(serviceEnvironmentConfig.getSignalServiceConfiguration()) - .getProfileOperations() - : null; - this.messageReceiver = new SignalServiceMessageReceiver(serviceEnvironmentConfig.getSignalServiceConfiguration(), - account.getUuid(), + + final var credentialsProvider = new DynamicCredentialsProvider(account.getUuid(), account.getUsername(), account.getPassword(), - account.getDeviceId(), - account.getSignalingKey(), + account.getDeviceId()); + this.dependencies = new SignalDependencies(account.getSelfAddress(), + serviceEnvironmentConfig, userAgent, - null, - timer, - clientZkProfileOperations, - ServiceConfig.AUTOMATIC_NETWORK_RETRY); - - this.account.setResolver(this::resolveSignalServiceAddress); + credentialsProvider, + account.getSignalProtocolStore(), + executor, + sessionLock); + this.pinHelper = new PinHelper(dependencies.getKeyBackupService()); - this.unidentifiedAccessHelper = new UnidentifiedAccessHelper(account::getProfileKey, + final var unidentifiedAccessHelper = new UnidentifiedAccessHelper(account::getProfileKey, account.getProfileStore()::getProfileKey, this::getRecipientProfile, this::getSenderCertificate); this.profileHelper = new ProfileHelper(account.getProfileStore()::getProfileKey, unidentifiedAccessHelper::getAccessFor, - unidentified -> unidentified ? getOrCreateUnidentifiedMessagePipe() : getOrCreateMessagePipe(), - () -> messageReceiver); - this.groupHelper = new GroupHelper(this::getRecipientProfileKeyCredential, + dependencies::getProfileService, + dependencies::getMessageReceiver, + this::resolveSignalServiceAddress); + this.groupV2Helper = new GroupV2Helper(this::getRecipientProfileKeyCredential, this::getRecipientProfile, - account::getSelfAddress, - groupsV2Operations, - groupsV2Api, - this::getGroupAuthForToday); + account::getSelfRecipientId, + dependencies.getGroupsV2Operations(), + dependencies.getGroupsV2Api(), + this::getGroupAuthForToday, + this::resolveSignalServiceAddress); this.avatarStore = new AvatarStore(pathConfig.getAvatarsPath()); this.attachmentStore = new AttachmentStore(pathConfig.getAttachmentsPath()); + this.stickerPackStore = new StickerPackStore(pathConfig.getStickerPacksPath()); + this.sendHelper = new SendHelper(account, + dependencies, + unidentifiedAccessHelper, + this::resolveSignalServiceAddress, + this::resolveRecipient, + this::handleIdentityFailure, + this::getGroup, + this::refreshRegisteredUser); } public String getUsername() { @@ -274,8 +254,12 @@ public class Manager implements Closeable { return account.getSelfAddress(); } + public RecipientId getSelfRecipientId() { + return account.getSelfRecipientId(); + } + private IdentityKeyPair getIdentityKeyPair() { - return account.getSignalProtocolStore().getIdentityKeyPair(); + return account.getIdentityKeyPair(); } public int getDeviceId() { @@ -285,29 +269,27 @@ public class Manager implements Closeable { public static Manager init( String username, File settingsPath, ServiceEnvironment serviceEnvironment, String userAgent ) throws IOException, NotRegisteredException { - PathConfig pathConfig = PathConfig.createDefault(settingsPath); + var pathConfig = PathConfig.createDefault(settingsPath); if (!SignalAccount.userExists(pathConfig.getDataPath(), username)) { throw new NotRegisteredException(); } - SignalAccount account = SignalAccount.load(pathConfig.getDataPath(), username); + var account = SignalAccount.load(pathConfig.getDataPath(), username, true); if (!account.isRegistered()) { throw new NotRegisteredException(); } - final ServiceEnvironmentConfig serviceEnvironmentConfig = ServiceConfig.getServiceEnvironmentConfig( - serviceEnvironment, - userAgent); + final var serviceEnvironmentConfig = ServiceConfig.getServiceEnvironmentConfig(serviceEnvironment, userAgent); return new Manager(account, pathConfig, serviceEnvironmentConfig, userAgent); } public static List getAllLocalUsernames(File settingsPath) { - PathConfig pathConfig = PathConfig.createDefault(settingsPath); - final File dataPath = pathConfig.getDataPath(); - final File[] files = dataPath.listFiles(); + var pathConfig = PathConfig.createDefault(settingsPath); + final var dataPath = pathConfig.getDataPath(); + final var files = dataPath.listFiles(); if (files == null) { return List.of(); @@ -321,13 +303,22 @@ public class Manager implements Closeable { } public void checkAccountState() throws IOException { - if (accountManager.getPreKeysCount() < ServiceConfig.PREKEY_MINIMUM_COUNT) { + 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 (dependencies.getAccountManager().getPreKeysCount() < ServiceConfig.PREKEY_MINIMUM_COUNT) { refreshPreKeys(); - account.save(); } if (account.getUuid() == null) { - account.setUuid(accountManager.getOwnUuid()); - account.save(); + account.setUuid(dependencies.getAccountManager().getOwnUuid()); } updateAccountAttributes(); } @@ -336,60 +327,86 @@ public class Manager implements Closeable { * This is used for checking a set of phone numbers for registration on Signal * * @param numbers The set of phone number in question - * @return A map of numbers to booleans. True if registered, false otherwise. Should never be null + * @return A map of numbers to canonicalized number and uuid. If a number is not registered the uuid is null. * @throws IOException if its unable to get the contacts to check if they're registered */ - public Map areUsersRegistered(Set numbers) throws IOException { + public Map> areUsersRegistered(Set numbers) throws IOException { + Map canonicalizedNumbers = numbers.stream().collect(Collectors.toMap(n -> n, n -> { + try { + return canonicalizePhoneNumber(n); + } catch (InvalidNumberException e) { + return ""; + } + })); + // Note "contactDetails" has no optionals. It only gives us info on users who are registered - Map contactDetails = getRegisteredUsers(numbers); + var contactDetails = getRegisteredUsers(canonicalizedNumbers.values() + .stream() + .filter(s -> !s.isEmpty()) + .collect(Collectors.toSet())); - Set registeredUsers = contactDetails.keySet(); + // Store numbers as recipients so we have the number/uuid association + contactDetails.forEach((number, uuid) -> resolveRecipientTrusted(new SignalServiceAddress(uuid, number))); - return numbers.stream().collect(Collectors.toMap(x -> x, registeredUsers::contains)); + return numbers.stream().collect(Collectors.toMap(n -> n, n -> { + final var number = canonicalizedNumbers.get(n); + final var uuid = contactDetails.get(number); + return new Pair<>(number.isEmpty() ? null : number, uuid); + })); } public void updateAccountAttributes() throws IOException { - accountManager.setAccountAttributes(account.getSignalingKey(), - account.getSignalProtocolStore().getLocalRegistrationId(), - true, - // set legacy pin only if no KBS master key is set - account.getPinMasterKey() == null ? account.getRegistrationLockPin() : null, - account.getPinMasterKey() == null ? null : account.getPinMasterKey().deriveRegistrationLock(), - account.getSelfUnidentifiedAccessKey(), - account.isUnrestrictedUnidentifiedAccess(), - capabilities, - account.isDiscoverableByPhoneNumber()); + dependencies.getAccountManager() + .setAccountAttributes(account.getEncryptedDeviceName(), + null, + account.getLocalRegistrationId(), + true, + // set legacy pin only if no KBS master key is set + account.getPinMasterKey() == null ? account.getRegistrationLockPin() : null, + account.getPinMasterKey() == null ? null : account.getPinMasterKey().deriveRegistrationLock(), + account.getSelfUnidentifiedAccessKey(), + account.isUnrestrictedUnidentifiedAccess(), + capabilities, + account.isDiscoverableByPhoneNumber()); } /** - * @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 { - SignalProfileEntry profileEntry = account.getProfileStore().getProfileEntry(getSelfAddress()); - SignalProfile profile = profileEntry == null ? null : profileEntry.getProfile(); - SignalProfile 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()); - - try (final StreamDetails streamDetails = avatar == null + 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(), - streamDetails); + dependencies.getAccountManager() + .setVersionedProfile(account.getUuid(), + account.getProfileKey(), + newProfile.getInternalServiceName(), + newProfile.getAbout() == null ? "" : newProfile.getAbout(), + newProfile.getAboutEmoji() == null ? "" : newProfile.getAboutEmoji(), + Optional.absent(), + streamDetails); } if (avatar != null) { @@ -400,61 +417,66 @@ 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)); - } catch (UntrustedIdentityException ignored) { - } + sendHelper.sendSyncMessage(SignalServiceSyncMessage.forFetchLatest(SignalServiceSyncMessage.FetchType.LOCAL_PROFILE)); } public void unregister() throws IOException { // When setting an empty GCM id, the Signal-Server also sets the fetchesMessages property to false. // 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()); - accountManager.deleteAccount(); + dependencies.getAccountManager().setGcmId(Optional.absent()); account.setRegistered(false); - account.save(); } - public List getLinkedDevices() throws IOException { - List devices = accountManager.getDevices(); + public void deleteAccount() throws IOException { + dependencies.getAccountManager().deleteAccount(); + + account.setRegistered(false); + } + + public List getLinkedDevices() throws IOException { + var devices = dependencies.getAccountManager().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); - List devices = accountManager.getDevices(); + dependencies.getAccountManager().removeDevice(deviceId); + var devices = dependencies.getAccountManager().getDevices(); account.setMultiDevice(devices.size() > 1); - account.save(); } public void addDeviceLink(URI linkUri) throws IOException, InvalidKeyException { - DeviceLinkInfo info = DeviceLinkInfo.parseDeviceLinkUri(linkUri); + var info = DeviceLinkInfo.parseDeviceLinkUri(linkUri); addDevice(info.deviceIdentifier, info.deviceKey); } private void addDevice(String deviceIdentifier, ECPublicKey deviceKey) throws IOException, InvalidKeyException { - IdentityKeyPair identityKeyPair = getIdentityKeyPair(); - String verificationCode = accountManager.getNewDeviceVerificationCode(); - - accountManager.addDevice(deviceIdentifier, - deviceKey, - identityKeyPair, - Optional.of(account.getProfileKey().serialize()), - verificationCode); + var identityKeyPair = getIdentityKeyPair(); + var verificationCode = dependencies.getAccountManager().getNewDeviceVerificationCode(); + + dependencies.getAccountManager() + .addDevice(deviceIdentifier, + deviceKey, + identityKeyPair, + Optional.of(account.getProfileKey().serialize()), + verificationCode); account.setMultiDevice(true); - account.save(); } public void setRegistrationLockPin(Optional pin) throws IOException, UnauthenticatedResponseException { @@ -462,168 +484,179 @@ public class Manager implements Closeable { throw new RuntimeException("Only master device can set a PIN"); } if (pin.isPresent()) { - final MasterKey masterKey = account.getPinMasterKey() != null + final var masterKey = account.getPinMasterKey() != null ? account.getPinMasterKey() : KeyUtils.createMasterKey(); pinHelper.setRegistrationLockPin(pin.get(), masterKey); - account.setRegistrationLockPin(pin.get()); - account.setPinMasterKey(masterKey); + account.setRegistrationLockPin(pin.get(), masterKey); } else { - // Remove legacy registration lock - accountManager.removeRegistrationLockV1(); - // Remove KBS Pin pinHelper.removeRegistrationLockPin(); - account.setRegistrationLockPin(null); - account.setPinMasterKey(null); + account.setRegistrationLockPin(null, null); } - account.save(); } void refreshPreKeys() throws IOException { - List oneTimePreKeys = generatePreKeys(); - final IdentityKeyPair identityKeyPair = getIdentityKeyPair(); - SignedPreKeyRecord signedPreKeyRecord = generateSignedPreKey(identityKeyPair); + var oneTimePreKeys = generatePreKeys(); + final var identityKeyPair = getIdentityKeyPair(); + var signedPreKeyRecord = generateSignedPreKey(identityKeyPair); - accountManager.setPreKeys(identityKeyPair.getPublicKey(), signedPreKeyRecord, oneTimePreKeys); + dependencies.getAccountManager().setPreKeys(identityKeyPair.getPublicKey(), signedPreKeyRecord, oneTimePreKeys); } private List generatePreKeys() { - final int offset = account.getPreKeyIdOffset(); + final var offset = account.getPreKeyIdOffset(); - List records = KeyUtils.generatePreKeyRecords(offset, ServiceConfig.PREKEY_BATCH_SIZE); + var records = KeyUtils.generatePreKeyRecords(offset, ServiceConfig.PREKEY_BATCH_SIZE); account.addPreKeys(records); - account.save(); return records; } private SignedPreKeyRecord generateSignedPreKey(IdentityKeyPair identityKeyPair) { - final int signedPreKeyId = account.getNextSignedPreKeyId(); + final var signedPreKeyId = account.getNextSignedPreKeyId(); - SignedPreKeyRecord record = KeyUtils.generateSignedPreKeyRecord(identityKeyPair, signedPreKeyId); + var record = KeyUtils.generateSignedPreKeyRecord(identityKeyPair, signedPreKeyId); account.addSignedPreKey(record); - account.save(); return record; } - private SignalServiceMessagePipe getOrCreateMessagePipe() { - if (messagePipe == null) { - messagePipe = messageReceiver.createMessagePipe(); - } - return messagePipe; + public Profile getRecipientProfile( + RecipientId recipientId + ) { + return getRecipientProfile(recipientId, false); } - private SignalServiceMessagePipe getOrCreateUnidentifiedMessagePipe() { - if (unidentifiedMessagePipe == null) { - unidentifiedMessagePipe = messageReceiver.createUnidentifiedMessagePipe(); + private final Set pendingProfileRequest = new HashSet<>(); + + Profile getRecipientProfile( + RecipientId recipientId, boolean force + ) { + 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; } - return unidentifiedMessagePipe; - } - private SignalServiceMessageSender createMessageSender() { - final ExecutorService executor = null; - return new SignalServiceMessageSender(serviceEnvironmentConfig.getSignalServiceConfiguration(), - account.getUuid(), - account.getUsername(), - account.getPassword(), - account.getDeviceId(), - account.getSignalProtocolStore(), - userAgent, - account.isMultiDevice(), - Optional.fromNullable(messagePipe), - Optional.fromNullable(unidentifiedMessagePipe), - Optional.absent(), - clientZkProfileOperations, - executor, - ServiceConfig.MAX_ENVELOPE_SIZE, - ServiceConfig.AUTOMATIC_NETWORK_RETRY); + synchronized (pendingProfileRequest) { + if (pendingProfileRequest.contains(recipientId)) { + return profile; + } + pendingProfileRequest.add(recipientId); + } + final SignalServiceProfile encryptedProfile; + try { + encryptedProfile = retrieveEncryptedProfile(recipientId); + } finally { + synchronized (pendingProfileRequest) { + pendingProfileRequest.remove(recipientId); + } + } + if (encryptedProfile == null) { + return null; + } + + profile = decryptProfileIfKeyKnown(recipientId, encryptedProfile); + account.getProfileStore().storeProfile(recipientId, profile); + + return profile; } - private SignalProfile getRecipientProfile( - SignalServiceAddress address + private Profile decryptProfileIfKeyKnown( + final RecipientId recipientId, final SignalServiceProfile encryptedProfile ) { - return getRecipientProfile(address, false); + var profileKey = account.getProfileStore().getProfileKey(recipientId); + if (profileKey == null) { + return new Profile(System.currentTimeMillis(), + null, + null, + null, + null, + ProfileUtils.getUnidentifiedAccessMode(encryptedProfile, null), + ProfileUtils.getCapabilities(encryptedProfile)); + } + + return decryptProfileAndDownloadAvatar(recipientId, profileKey, encryptedProfile); } - private SignalProfile getRecipientProfile( - SignalServiceAddress address, boolean force - ) { - SignalProfileEntry profileEntry = account.getProfileStore().getProfileEntry(address); - if (profileEntry == null) { + private SignalServiceProfile retrieveEncryptedProfile(RecipientId recipientId) { + try { + return retrieveProfileAndCredential(recipientId, SignalServiceProfile.RequestType.PROFILE).getProfile(); + } catch (IOException e) { + logger.warn("Failed to retrieve profile, ignoring: {}", e.getMessage()); return null; } - long 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 = profileHelper.retrieveProfileSync(address, SignalServiceProfile.RequestType.PROFILE) - .getProfile(); - } catch (IOException e) { - logger.warn("Failed to retrieve profile, ignoring: {}", e.getMessage()); - return null; - } finally { - profileEntry.setRequestPending(false); - } + } - final ProfileKey profileKey = profileEntry.getProfileKey(); - final SignalProfile profile = decryptProfileAndDownloadAvatar(address, profileKey, encryptedProfile); - account.getProfileStore() - .updateProfile(address, profileKey, now, profile, profileEntry.getProfileKeyCredential()); - return profile; + 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 profileEntry.getProfile(); + return profileAndCredential; } - private ProfileKeyCredential getRecipientProfileKeyCredential(SignalServiceAddress address) { - SignalProfileEntry profileEntry = account.getProfileStore().getProfileEntry(address); - if (profileEntry == null) { + 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; - } - long now = new Date().getTime(); - final ProfileKeyCredential profileKeyCredential = profileAndCredential.getProfileKeyCredential().orNull(); - final SignalProfile 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); } private Optional createGroupAvatarAttachment(GroupId groupId) throws IOException { - final StreamDetails streamDetails = avatarStore.retrieveGroupAvatar(groupId); + final var streamDetails = avatarStore.retrieveGroupAvatar(groupId); if (streamDetails == null) { return Optional.absent(); } @@ -632,7 +665,7 @@ public class Manager implements Closeable { } private Optional createContactAvatarAttachment(SignalServiceAddress address) throws IOException { - final StreamDetails streamDetails = avatarStore.retrieveContactAvatar(address); + final var streamDetails = avatarStore.retrieveContactAvatar(address); if (streamDetails == null) { return Optional.absent(); } @@ -640,23 +673,12 @@ public class Manager implements Closeable { return Optional.of(AttachmentUtils.createAttachment(streamDetails, Optional.absent())); } - private GroupInfo getGroupForSending(GroupId groupId) throws GroupNotFoundException, NotAGroupMemberException { - GroupInfo g = getGroup(groupId); - if (g == null) { - throw new GroupNotFoundException(groupId); - } - if (!g.isMember(account.getSelfAddress())) { - throw new NotAGroupMemberException(groupId, g.getTitle()); - } - return g; - } - private GroupInfo getGroupForUpdating(GroupId groupId) throws GroupNotFoundException, NotAGroupMemberException { - GroupInfo g = getGroup(groupId); + var g = getGroup(groupId); 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; @@ -669,171 +691,254 @@ public class Manager implements Closeable { public Pair> sendGroupMessage( String messageText, List attachments, GroupId groupId ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException { - final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder() - .withBody(messageText); + final var messageBuilder = createMessageBuilder().withBody(messageText); if (attachments != null) { messageBuilder.withAttachments(AttachmentUtils.getSignalServiceAttachments(attachments)); } - return sendGroupMessage(messageBuilder, groupId); + return sendHelper.sendAsGroupMessage(messageBuilder, groupId); } public Pair> sendGroupMessageReaction( String emoji, boolean remove, String targetAuthor, long targetSentTimestamp, GroupId groupId ) throws IOException, InvalidNumberException, NotAGroupMemberException, GroupNotFoundException { - SignalServiceDataMessage.Reaction reaction = new SignalServiceDataMessage.Reaction(emoji, + var targetAuthorRecipientId = canonicalizeAndResolveRecipient(targetAuthor); + var reaction = new SignalServiceDataMessage.Reaction(emoji, remove, - canonicalizeAndResolveSignalServiceAddress(targetAuthor), + resolveSignalServiceAddress(targetAuthorRecipientId), targetSentTimestamp); - final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder() - .withReaction(reaction); + final var messageBuilder = createMessageBuilder().withReaction(reaction); - return sendGroupMessage(messageBuilder, groupId); + return sendHelper.sendAsGroupMessage(messageBuilder, groupId); } - public Pair> sendGroupMessage( - SignalServiceDataMessage.Builder messageBuilder, GroupId groupId - ) throws IOException, GroupNotFoundException, NotAGroupMemberException { - final GroupInfo g = getGroupForSending(groupId); + public Pair> sendQuitGroupMessage( + GroupId groupId, Set groupAdmins + ) throws GroupNotFoundException, IOException, NotAGroupMemberException, InvalidNumberException, LastGroupAdminException { + var group = getGroupForUpdating(groupId); + if (group instanceof GroupInfoV1) { + return quitGroupV1((GroupInfoV1) group); + } - GroupUtils.setGroupContext(messageBuilder, g); - messageBuilder.withExpiration(g.getMessageExpirationTime()); + final var newAdmins = getRecipientIds(groupAdmins); + try { + return quitGroupV2((GroupInfoV2) group, newAdmins); + } catch (ConflictException e) { + // Detected conflicting update, refreshing group and trying again + group = getGroup(groupId, true); + return quitGroupV2((GroupInfoV2) group, newAdmins); + } + } + + private Pair> quitGroupV1(final GroupInfoV1 groupInfoV1) throws IOException { + var group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.QUIT) + .withId(groupInfoV1.getGroupId().serialize()) + .build(); - return sendMessage(messageBuilder, g.getMembersWithout(account.getSelfAddress())); + var messageBuilder = createMessageBuilder().asGroupMessage(group); + groupInfoV1.removeMember(account.getSelfRecipientId()); + account.getGroupStore().updateGroup(groupInfoV1); + return sendHelper.sendGroupMessage(messageBuilder.build(), + groupInfoV1.getMembersIncludingPendingWithout(account.getSelfRecipientId())); + } + + private Pair> quitGroupV2( + final GroupInfoV2 groupInfoV2, final Set newAdmins + ) throws LastGroupAdminException, IOException { + final var currentAdmins = groupInfoV2.getAdminMembers(); + newAdmins.removeAll(currentAdmins); + newAdmins.retainAll(groupInfoV2.getMembers()); + if (currentAdmins.contains(getSelfRecipientId()) + && currentAdmins.size() == 1 + && groupInfoV2.getMembers().size() > 1 + && newAdmins.size() == 0) { + // Last admin can't leave the group, unless she's also the last member + throw new LastGroupAdminException(groupInfoV2.getGroupId(), groupInfoV2.getTitle()); + } + final var groupGroupChangePair = groupV2Helper.leaveGroup(groupInfoV2, newAdmins); + groupInfoV2.setGroup(groupGroupChangePair.first(), this::resolveRecipient); + var messageBuilder = getGroupUpdateMessageBuilder(groupInfoV2, groupGroupChangePair.second().toByteArray()); + account.getGroupStore().updateGroup(groupInfoV2); + return sendHelper.sendGroupMessage(messageBuilder.build(), + groupInfoV2.getMembersIncludingPendingWithout(account.getSelfRecipientId())); + } + + public void deleteGroup(GroupId groupId) throws IOException { + account.getGroupStore().deleteGroup(groupId); + avatarStore.deleteGroupAvatar(groupId); + } + + public Pair> createGroup( + String name, List members, File avatarFile + ) throws IOException, AttachmentInvalidException, InvalidNumberException { + return createGroup(name, members == null ? null : getRecipientIds(members), avatarFile); } - public Pair> sendQuitGroupMessage(GroupId groupId) throws GroupNotFoundException, IOException, NotAGroupMemberException { - SignalServiceDataMessage.Builder messageBuilder; + 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); + } - final GroupInfo g = getGroupForUpdating(groupId); - if (g instanceof GroupInfoV1) { - GroupInfoV1 groupInfoV1 = (GroupInfoV1) g; - SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.QUIT) - .withId(groupId.serialize()) - .build(); - messageBuilder = SignalServiceDataMessage.newBuilder().asGroupMessage(group); - groupInfoV1.removeMember(account.getSelfAddress()); - account.getGroupStore().updateGroup(groupInfoV1); - } else { - final GroupInfoV2 groupInfoV2 = (GroupInfoV2) g; - final Pair groupGroupChangePair = groupHelper.leaveGroup(groupInfoV2); - groupInfoV2.setGroup(groupGroupChangePair.first()); - messageBuilder = getGroupUpdateMessageBuilder(groupInfoV2, groupGroupChangePair.second().toByteArray()); - account.getGroupStore().updateGroup(groupInfoV2); + 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()); } - return sendMessage(messageBuilder, g.getMembersWithout(account.getSelfAddress())); - } + final var gv2 = gv2Pair.first(); + final var decryptedGroup = gv2Pair.second(); - public Pair> updateGroup( - GroupId groupId, String name, List members, File avatarFile + 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 = sendHelper.sendGroupMessage(messageBuilder.build(), + 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, + Boolean isAnnouncementGroup ) throws IOException, GroupNotFoundException, AttachmentInvalidException, InvalidNumberException, NotAGroupMemberException { - return sendUpdateGroupMessage(groupId, + return updateGroup(groupId, name, - members == null ? null : getSignalServiceAddresses(members), - avatarFile); - } - - private Pair> sendUpdateGroupMessage( - GroupId groupId, String name, Collection members, File avatarFile + description, + members == null ? null : getRecipientIds(members), + removeMembers == null ? null : getRecipientIds(removeMembers), + admins == null ? null : getRecipientIds(admins), + removeAdmins == null ? null : getRecipientIds(removeAdmins), + resetGroupLink, + groupLinkState, + addMemberPermission, + editDetailsPermission, + avatarFile, + expirationTimer, + isAnnouncementGroup); + } + + 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, + final Boolean isAnnouncementGroup ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException { - GroupInfo g; - SignalServiceDataMessage.Builder messageBuilder; - if (groupId == null) { - // Create new group - GroupInfoV2 gv2 = groupHelper.createGroupV2(name == null ? "" : name, - members == null ? List.of() : members, - avatarFile); - if (gv2 == null) { - GroupInfoV1 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 { - GroupInfo group = getGroupForUpdating(groupId); - if (group instanceof GroupInfoV2) { - final GroupInfoV2 groupInfoV2 = (GroupInfoV2) group; - - Pair> result = null; - if (groupInfoV2.isPendingMember(getSelfAddress())) { - Pair groupGroupChangePair = groupHelper.acceptInvite(groupInfoV2); - result = sendUpdateGroupMessage(groupInfoV2, - groupGroupChangePair.first(), - groupGroupChangePair.second()); - } - - if (members != null) { - final Set newMembers = new HashSet<>(members); - newMembers.removeAll(group.getMembers() - .stream() - .map(this::resolveSignalServiceAddress) - .collect(Collectors.toSet())); - if (newMembers.size() > 0) { - Pair groupGroupChangePair = groupHelper.updateGroupV2(groupInfoV2, - newMembers); - result = sendUpdateGroupMessage(groupInfoV2, - groupGroupChangePair.first(), - groupGroupChangePair.second()); - } - } - if (result == null || name != null || avatarFile != null) { - Pair 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()); - } + var group = getGroupForUpdating(groupId); - return new Pair<>(group.getGroupId(), result.second()); - } else { - GroupInfoV1 gv1 = (GroupInfoV1) group; - updateGroupV1(gv1, name, members, avatarFile); - messageBuilder = getGroupUpdateMessageBuilder(gv1); - g = gv1; + if (group instanceof GroupInfoV2) { + try { + return updateGroupV2((GroupInfoV2) group, + name, + description, + members, + removeMembers, + admins, + removeAdmins, + resetGroupLink, + groupLinkState, + addMemberPermission, + editDetailsPermission, + avatarFile, + expirationTimer, + isAnnouncementGroup); + } 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, + isAnnouncementGroup); } } - 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; + } + + 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); - final Pair> result = sendMessage(messageBuilder, - g.getMembersIncludingPendingWithout(account.getSelfAddress())); - return new Pair<>(g.getGroupId(), result.second()); + account.getGroupStore().updateGroup(gv1); + + return sendHelper.sendGroupMessage(messageBuilder.build(), + 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 Set newE164Members = new HashSet<>(); - for (SignalServiceAddress member : members) { - if (g.isMember(member) || !member.getNumber().isPresent()) { + final var newMemberAddresses = members.stream() + .filter(member -> !g.isMember(member)) + .map(this::resolveSignalServiceAddress) + .collect(Collectors.toList()); + final var newE164Members = new HashSet(); + for (var member : newMemberAddresses) { + if (!member.getNumber().isPresent()) { continue; } newE164Members.add(member.getNumber().get()); } - final Map registeredUsers = getRegisteredUsers(newE164Members); + final var registeredUsers = getRegisteredUsers(newE164Members); if (registeredUsers.size() != newE164Members.size()) { // Some of the new members are not registered on Signal newE164Members.removeAll(registeredUsers.keySet()); @@ -851,21 +956,132 @@ 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, + final Integer expirationTimer, + final Boolean isAnnouncementGroup + ) 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 (isAnnouncementGroup != null) { + var groupGroupChangePair = groupV2Helper.setIsAnnouncementGroup(group, isAnnouncementGroup); + 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 DecryptedGroupJoinInfo groupJoinInfo = groupHelper.getDecryptedGroupJoinInfo(inviteLinkUrl.getGroupMasterKey(), + final var groupJoinInfo = groupV2Helper.getDecryptedGroupJoinInfo(inviteLinkUrl.getGroupMasterKey(), inviteLinkUrl.getPassword()); - final GroupChange groupChange = groupHelper.joinGroup(inviteLinkUrl.getGroupMasterKey(), + final var groupChange = groupV2Helper.joinGroup(inviteLinkUrl.getGroupMasterKey(), inviteLinkUrl.getPassword(), groupJoinInfo); - final GroupInfoV2 group = getOrMigrateGroup(inviteLinkUrl.getGroupMasterKey(), + final var group = getOrMigrateGroup(inviteLinkUrl.getGroupMasterKey(), groupJoinInfo.getRevision() + 1, groupChange.toByteArray()); @@ -874,11 +1090,24 @@ public class Manager implements Closeable { return new Pair<>(group.getGroupId(), List.of()); } - final Pair> 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 sendHelper.sendGroupMessage(messageBuilder.build(), members); + } + private static int currentTimeDays() { return (int) TimeUnit.MILLISECONDS.toDays(System.currentTimeMillis()); } @@ -886,59 +1115,54 @@ public class Manager implements Closeable { private GroupsV2AuthorizationString getGroupAuthForToday( final GroupSecretParams groupSecretParams ) throws IOException { - final int today = currentTimeDays(); + final var today = currentTimeDays(); // Returns credentials for the next 7 days - final HashMap credentials = groupsV2Api.getCredentials(today); + final var credentials = dependencies.getGroupsV2Api().getCredentials(today); // TODO cache credentials until they expire - AuthCredentialResponse authCredentialResponse = credentials.get(today); + var authCredentialResponse = credentials.get(today); try { - return groupsV2Api.getGroupsV2AuthorizationString(account.getUuid(), - today, - groupSecretParams, - authCredentialResponse); + return dependencies.getGroupsV2Api() + .getGroupsV2AuthorizationString(account.getUuid(), + today, + groupSecretParams, + authCredentialResponse); } catch (VerificationFailedException e) { throw new IOException(e); } } - private Pair> sendUpdateGroupMessage( - GroupInfoV2 group, DecryptedGroup newDecryptedGroup, GroupChange groupChange - ) throws IOException { - group.setGroup(newDecryptedGroup); - final SignalServiceDataMessage.Builder 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 { GroupInfoV1 g; - GroupInfo group = getGroupForSending(groupId); + var group = getGroupForUpdating(groupId); if (!(group instanceof GroupInfoV1)) { - throw new RuntimeException("Received an invalid group request for a v2 group!"); + throw new IOException("Received an invalid group request for a v2 group!"); } g = (GroupInfoV1) group; - if (!g.isMember(recipient)) { + final var recipientId = resolveRecipient(recipient); + if (!g.isMember(recipientId)) { throw new NotAGroupMemberException(groupId, g.name); } - SignalServiceDataMessage.Builder messageBuilder = getGroupUpdateMessageBuilder(g); + var messageBuilder = getGroupUpdateMessageBuilder(g); // Send group message only to the recipient who requested it - return sendMessage(messageBuilder, List.of(recipient)); + return sendHelper.sendGroupMessage(messageBuilder.build(), Set.of(recipientId)); } private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfoV1 g) throws AttachmentInvalidException { - SignalServiceGroup.Builder group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.UPDATE) + 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 Optional attachment = createGroupAvatarAttachment(g.getGroupId()); + final var attachment = createGroupAvatarAttachment(g.getGroupId()); if (attachment.isPresent()) { group.withAvatar(attachment.get()); } @@ -946,57 +1170,48 @@ public class Manager implements Closeable { throw new AttachmentInvalidException(g.getGroupId().toBase64(), e); } - return SignalServiceDataMessage.newBuilder() - .asGroupMessage(group.build()) - .withExpiration(g.getMessageExpirationTime()); + return createMessageBuilder().asGroupMessage(group.build()).withExpiration(g.getMessageExpirationTime()); } private SignalServiceDataMessage.Builder getGroupUpdateMessageBuilder(GroupInfoV2 g, byte[] signedGroupChange) { - SignalServiceGroupV2.Builder group = SignalServiceGroupV2.newBuilder(g.getMasterKey()) + var group = SignalServiceGroupV2.newBuilder(g.getMasterKey()) .withRevision(g.getGroup().getRevision()) .withSignedGroupChange(signedGroupChange); - return SignalServiceDataMessage.newBuilder() - .asGroupMessage(group.build()) - .withExpiration(g.getMessageExpirationTime()); + return createMessageBuilder().asGroupMessage(group.build()).withExpiration(g.getMessageExpirationTime()); } Pair> sendGroupInfoRequest( GroupIdV1 groupId, SignalServiceAddress recipient ) throws IOException { - SignalServiceGroup.Builder group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.REQUEST_INFO) - .withId(groupId.serialize()); + var group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.REQUEST_INFO).withId(groupId.serialize()); - SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder() - .asGroupMessage(group.build()); + var messageBuilder = createMessageBuilder().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 sendHelper.sendGroupMessage(messageBuilder.build(), Set.of(resolveRecipient(recipient))); } void sendReceipt( SignalServiceAddress remoteAddress, long messageId ) throws IOException, UntrustedIdentityException { - SignalServiceReceiptMessage receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.DELIVERY, + var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.DELIVERY, List.of(messageId), System.currentTimeMillis()); - createMessageSender().sendReceipt(remoteAddress, - unidentifiedAccessHelper.getAccessFor(remoteAddress), - receiptMessage); + sendHelper.sendReceiptMessage(receiptMessage, resolveRecipient(remoteAddress)); } public Pair> sendMessage( String messageText, List attachments, List recipients ) throws IOException, AttachmentInvalidException, InvalidNumberException { - final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder() - .withBody(messageText); + final var messageBuilder = createMessageBuilder().withBody(messageText); if (attachments != null) { - List attachmentStreams = AttachmentUtils.getSignalServiceAttachments(attachments); + var attachmentStreams = AttachmentUtils.getSignalServiceAttachments(attachments); // Upload attachments here, so we only upload once even for multiple recipients - SignalServiceMessageSender messageSender = createMessageSender(); - List attachmentPointers = new ArrayList<>(attachmentStreams.size()); - for (SignalServiceAttachment attachment : attachmentStreams) { + var messageSender = dependencies.getMessageSender(); + var attachmentPointers = new ArrayList(attachmentStreams.size()); + for (var attachment : attachmentStreams) { if (attachment.isStream()) { attachmentPointers.add(messageSender.uploadAttachment(attachment.asStream())); } else if (attachment.isPointer()) { @@ -1006,107 +1221,115 @@ public class Manager implements Closeable { messageBuilder.withAttachments(attachmentPointers); } - return sendMessage(messageBuilder, getSignalServiceAddresses(recipients)); + return sendHelper.sendMessage(messageBuilder, getRecipientIds(recipients)); } public Pair sendSelfMessage( String messageText, List attachments ) throws IOException, AttachmentInvalidException { - final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder() - .withBody(messageText); + final var messageBuilder = createMessageBuilder().withBody(messageText); if (attachments != null) { messageBuilder.withAttachments(AttachmentUtils.getSignalServiceAttachments(attachments)); } - return sendSelfMessage(messageBuilder); + return sendHelper.sendSelfMessage(messageBuilder); + } + + public Pair> sendRemoteDeleteMessage( + long targetSentTimestamp, List recipients + ) throws IOException, InvalidNumberException { + var delete = new SignalServiceDataMessage.RemoteDelete(targetSentTimestamp); + final var messageBuilder = createMessageBuilder().withRemoteDelete(delete); + return sendHelper.sendMessage(messageBuilder, getRecipientIds(recipients)); + } + + public Pair> sendGroupRemoteDeleteMessage( + long targetSentTimestamp, GroupId groupId + ) throws IOException, NotAGroupMemberException, GroupNotFoundException { + var delete = new SignalServiceDataMessage.RemoteDelete(targetSentTimestamp); + final var messageBuilder = createMessageBuilder().withRemoteDelete(delete); + return sendHelper.sendAsGroupMessage(messageBuilder, groupId); } public Pair> sendMessageReaction( String emoji, boolean remove, String targetAuthor, long targetSentTimestamp, List recipients ) throws IOException, InvalidNumberException { - SignalServiceDataMessage.Reaction reaction = new SignalServiceDataMessage.Reaction(emoji, + var targetAuthorRecipientId = canonicalizeAndResolveRecipient(targetAuthor); + var reaction = new SignalServiceDataMessage.Reaction(emoji, remove, - canonicalizeAndResolveSignalServiceAddress(targetAuthor), + resolveSignalServiceAddress(targetAuthorRecipientId), targetSentTimestamp); - final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder() - .withReaction(reaction); - return sendMessage(messageBuilder, getSignalServiceAddresses(recipients)); + final var messageBuilder = createMessageBuilder().withReaction(reaction); + return sendHelper.sendMessage(messageBuilder, getRecipientIds(recipients)); } public Pair> sendEndSessionMessage(List recipients) throws IOException, InvalidNumberException { - SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder().asEndSessionMessage(); + var messageBuilder = createMessageBuilder().asEndSessionMessage(); - final Collection signalServiceAddresses = getSignalServiceAddresses(recipients); + final var recipientIds = getRecipientIds(recipients); try { - return sendMessage(messageBuilder, signalServiceAddresses); - } catch (Exception e) { - for (SignalServiceAddress address : signalServiceAddresses) { - handleEndSession(address); + return sendHelper.sendMessage(messageBuilder, recipientIds); + } finally { + for (var recipientId : recipientIds) { + handleEndSession(recipientId); } - account.save(); - throw e; } } - public String getContactName(String number) throws InvalidNumberException { - ContactInfo 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())) { + sendHelper.sendNullMessage(recipientId); } } - public void setContactName(String number, String name) throws InvalidNumberException { - final SignalServiceAddress address = canonicalizeAndResolveSignalServiceAddress(number); - ContactInfo contact = account.getContactStore().getContact(address); - if (contact == null) { - contact = new ContactInfo(address); + public void setContactName(String number, String name) throws InvalidNumberException, NotMasterDeviceException { + if (!account.isMasterDevice()) { + throw new NotMasterDeviceException(); } - contact.name = name; - account.getContactStore().updateContact(contact); - account.save(); + 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()); } - public void setContactBlocked(String number, boolean blocked) throws InvalidNumberException { - setContactBlocked(canonicalizeAndResolveSignalServiceAddress(number), blocked); + public void setContactBlocked( + String number, boolean blocked + ) throws InvalidNumberException, NotMasterDeviceException { + if (!account.isMasterDevice()) { + throw new NotMasterDeviceException(); + } + setContactBlocked(canonicalizeAndResolveRecipient(number), blocked); } - private void setContactBlocked(SignalServiceAddress address, boolean blocked) { - ContactInfo contact = account.getContactStore().getContact(address); - if (contact == null) { - contact = new ContactInfo(address); - } - contact.blocked = blocked; - account.getContactStore().updateContact(contact); - account.save(); + 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 { - GroupInfo group = getGroup(groupId); + var group = getGroup(groupId); if (group == null) { throw new GroupNotFoundException(groupId); } 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 { - ContactInfo 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 { - final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder() - .asExpirationUpdate(); - sendMessage(messageBuilder, List.of(address)); + private void sendExpirationTimerUpdate(RecipientId recipientId) throws IOException { + final var messageBuilder = createMessageBuilder().asExpirationUpdate(); + sendHelper.sendMessage(messageBuilder, Set.of(recipientId)); } /** @@ -1115,22 +1338,25 @@ public class Manager implements Closeable { public void setExpirationTimer( String number, int messageExpirationTimer ) throws IOException, InvalidNumberException { - SignalServiceAddress 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) { - GroupInfo g = getGroup(groupId); - if (g instanceof GroupInfoV1) { - GroupInfoV1 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 = createMessageBuilder().asExpirationUpdate(); + sendHelper.sendAsGroupMessage(messageBuilder, groupId); } /** @@ -1140,95 +1366,86 @@ public class Manager implements Closeable { * @return if successful, returns the URL to install the sticker pack in the signal app */ public String uploadStickerPack(File path) throws IOException, StickerPackInvalidException { - SignalServiceStickerManifestUpload manifest = StickerUtils.getSignalServiceStickerManifestUpload(path); + var manifest = StickerUtils.getSignalServiceStickerManifestUpload(path); - SignalServiceMessageSender messageSender = createMessageSender(); + var messageSender = dependencies.getMessageSender(); - byte[] packKey = KeyUtils.createStickerUploadKey(); - String packId = messageSender.uploadStickerManifest(manifest, packKey); + var packKey = KeyUtils.createStickerUploadKey(); + var packIdString = messageSender.uploadStickerManifest(manifest, packKey); + var packId = StickerPackId.deserialize(Hex.fromStringCondensed(packIdString)); - Sticker sticker = new Sticker(Hex.fromStringCondensed(packId), packKey); + var sticker = new Sticker(packId, packKey); account.getStickerStore().updateSticker(sticker); - account.save(); try { return new URI("https", "signal.art", "/addstickers/", - "pack_id=" + URLEncoder.encode(packId, StandardCharsets.UTF_8) + "&pack_key=" + URLEncoder.encode( - Hex.toStringCondensed(packKey), - StandardCharsets.UTF_8)).toString(); + "pack_id=" + + URLEncoder.encode(Hex.toStringCondensed(packId.serialize()), StandardCharsets.UTF_8) + + "&pack_key=" + + URLEncoder.encode(Hex.toStringCondensed(packKey), StandardCharsets.UTF_8)).toString(); } catch (URISyntaxException e) { throw new AssertionError(e); } } - void requestSyncGroups() throws IOException { - SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder() + 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(); - SignalServiceSyncMessage message = SignalServiceSyncMessage.forRequest(new RequestMessage(r)); - try { - sendSyncMessage(message); - } catch (UntrustedIdentityException e) { - throw new AssertionError(e); - } + var message = SignalServiceSyncMessage.forRequest(new RequestMessage(r)); + sendHelper.sendSyncMessage(message); } - void requestSyncContacts() throws IOException { - SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder() + private void requestSyncContacts() throws IOException { + var r = SignalServiceProtos.SyncMessage.Request.newBuilder() .setType(SignalServiceProtos.SyncMessage.Request.Type.CONTACTS) .build(); - SignalServiceSyncMessage message = SignalServiceSyncMessage.forRequest(new RequestMessage(r)); - try { - sendSyncMessage(message); - } catch (UntrustedIdentityException e) { - throw new AssertionError(e); - } + var message = SignalServiceSyncMessage.forRequest(new RequestMessage(r)); + sendHelper.sendSyncMessage(message); } - void requestSyncBlocked() throws IOException { - SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder() + private void requestSyncBlocked() throws IOException { + var r = SignalServiceProtos.SyncMessage.Request.newBuilder() .setType(SignalServiceProtos.SyncMessage.Request.Type.BLOCKED) .build(); - SignalServiceSyncMessage message = SignalServiceSyncMessage.forRequest(new RequestMessage(r)); - try { - sendSyncMessage(message); - } catch (UntrustedIdentityException e) { - throw new AssertionError(e); - } + var message = SignalServiceSyncMessage.forRequest(new RequestMessage(r)); + sendHelper.sendSyncMessage(message); } - void requestSyncConfiguration() throws IOException { - SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder() + private void requestSyncConfiguration() throws IOException { + var r = SignalServiceProtos.SyncMessage.Request.newBuilder() .setType(SignalServiceProtos.SyncMessage.Request.Type.CONFIGURATION) .build(); - SignalServiceSyncMessage message = SignalServiceSyncMessage.forRequest(new RequestMessage(r)); - try { - sendSyncMessage(message); - } catch (UntrustedIdentityException e) { - throw new AssertionError(e); - } + var message = SignalServiceSyncMessage.forRequest(new RequestMessage(r)); + sendHelper.sendSyncMessage(message); } - void requestSyncKeys() throws IOException { - SignalServiceProtos.SyncMessage.Request r = SignalServiceProtos.SyncMessage.Request.newBuilder() + private void requestSyncKeys() throws IOException { + var r = SignalServiceProtos.SyncMessage.Request.newBuilder() .setType(SignalServiceProtos.SyncMessage.Request.Type.KEYS) .build(); - SignalServiceSyncMessage message = SignalServiceSyncMessage.forRequest(new RequestMessage(r)); - try { - sendSyncMessage(message); - } catch (UntrustedIdentityException e) { - throw new AssertionError(e); - } + var message = SignalServiceSyncMessage.forRequest(new RequestMessage(r)); + sendHelper.sendSyncMessage(message); } private byte[] getSenderCertificate() { - // TODO support UUID capable sender certificates - // byte[] certificate = accountManager.getSenderCertificateForPhoneNumberPrivacy(); byte[] certificate; try { - certificate = accountManager.getSenderCertificate(); + if (account.isPhoneNumberShared()) { + certificate = dependencies.getAccountManager().getSenderCertificate(); + } else { + certificate = dependencies.getAccountManager().getSenderCertificateForPhoneNumberPrivacy(); + } } catch (IOException e) { logger.warn("Failed to get sender certificate, ignoring: {}", e.getMessage()); return null; @@ -1237,25 +1454,12 @@ public class Manager implements Closeable { return certificate; } - private void sendSyncMessage(SignalServiceSyncMessage message) throws IOException, UntrustedIdentityException { - SignalServiceMessageSender messageSender = createMessageSender(); - try { - messageSender.sendMessage(message, unidentifiedAccessHelper.getAccessForSync()); - } catch (UntrustedIdentityException e) { - account.getSignalProtocolStore() - .saveIdentity(resolveSignalServiceAddress(e.getIdentifier()), - e.getIdentityKey(), - TrustLevel.UNTRUSTED); - throw e; - } - } + private Set getRecipientIds(Collection numbers) throws InvalidNumberException { + final var signalServiceAddresses = new HashSet(numbers.size()); + final var addressesMissingUuid = new HashSet(); - private Collection getSignalServiceAddresses(Collection numbers) throws InvalidNumberException { - final Set signalServiceAddresses = new HashSet<>(numbers.size()); - final Set addressesMissingUuid = new HashSet<>(); - - for (String number : numbers) { - final SignalServiceAddress resolvedAddress = canonicalizeAndResolveSignalServiceAddress(number); + for (var number : numbers) { + final var resolvedAddress = resolveSignalServiceAddress(canonicalizeAndResolveRecipient(number)); if (resolvedAddress.getUuid().isPresent()) { signalServiceAddresses.add(resolvedAddress); } else { @@ -1263,7 +1467,7 @@ public class Manager implements Closeable { } } - final Set numbersMissingUuid = addressesMissingUuid.stream() + final var numbersMissingUuid = addressesMissingUuid.stream() .map(a -> a.getNumber().get()) .collect(Collectors.toSet()); Map registeredUsers; @@ -1274,182 +1478,74 @@ public class Manager implements Closeable { registeredUsers = Map.of(); } - for (SignalServiceAddress address : addressesMissingUuid) { - final String number = address.getNumber().get(); + for (var address : addressesMissingUuid) { + final var number = address.getNumber().get(); if (registeredUsers.containsKey(number)) { - final SignalServiceAddress newAddress = resolveSignalServiceAddress(new SignalServiceAddress( + final var newAddress = resolveSignalServiceAddress(resolveRecipientTrusted(new SignalServiceAddress( registeredUsers.get(number), - 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 { - try { - return accountManager.getRegisteredUsers(ServiceConfig.getIasKeyStore(), - numbersMissingUuid, - serviceEnvironmentConfig.getCdsMrenclave()); - } catch (Quote.InvalidQuoteFormatException | UnauthenticatedQuoteException | SignatureException | UnauthenticatedResponseException | InvalidKeyException e) { - throw new IOException(e); + 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 Pair> sendMessage( - SignalServiceDataMessage.Builder messageBuilder, Collection recipients - ) throws IOException { - recipients = recipients.stream().map(this::resolveSignalServiceAddress).collect(Collectors.toSet()); - final long timestamp = System.currentTimeMillis(); - messageBuilder.withTimestamp(timestamp); - getOrCreateMessagePipe(); - getOrCreateUnidentifiedMessagePipe(); - SignalServiceDataMessage message = null; + private Map getRegisteredUsers(final Set numbers) throws IOException { try { - message = messageBuilder.build(); - if (message.getGroupContext().isPresent()) { - try { - SignalServiceMessageSender messageSender = createMessageSender(); - final boolean isRecipientUpdate = false; - List result = messageSender.sendMessage(new ArrayList<>(recipients), - unidentifiedAccessHelper.getAccessFor(recipients), - isRecipientUpdate, - message); - for (SendMessageResult r : result) { - if (r.getIdentityFailure() != null) { - account.getSignalProtocolStore() - .saveIdentity(r.getAddress(), - r.getIdentityFailure().getIdentityKey(), - TrustLevel.UNTRUSTED); - } - } - return new Pair<>(timestamp, result); - } catch (UntrustedIdentityException e) { - account.getSignalProtocolStore() - .saveIdentity(resolveSignalServiceAddress(e.getIdentifier()), - e.getIdentityKey(), - TrustLevel.UNTRUSTED); - return new Pair<>(timestamp, List.of()); - } - } else { - // Send to all individually, so sync messages are sent correctly - messageBuilder.withProfileKey(account.getProfileKey().serialize()); - List results = new ArrayList<>(recipients.size()); - for (SignalServiceAddress address : recipients) { - final ContactInfo contact = account.getContactStore().getContact(address); - final int expirationTime = contact != null ? contact.messageExpirationTime : 0; - messageBuilder.withExpiration(expirationTime); - message = messageBuilder.build(); - results.add(sendMessage(address, message)); - } - return new Pair<>(timestamp, results); - } - } finally { - if (message != null && message.isEndSession()) { - for (SignalServiceAddress recipient : recipients) { - handleEndSession(recipient); - } - } - account.save(); + return dependencies.getAccountManager() + .getRegisteredUsers(ServiceConfig.getIasKeyStore(), + numbers, + serviceEnvironmentConfig.getCdsMrenclave()); + } catch (Quote.InvalidQuoteFormatException | UnauthenticatedQuoteException | SignatureException | UnauthenticatedResponseException | InvalidKeyException e) { + throw new IOException(e); } } - private Pair sendSelfMessage( - SignalServiceDataMessage.Builder messageBuilder - ) throws IOException { - final long timestamp = System.currentTimeMillis(); - messageBuilder.withTimestamp(timestamp); - getOrCreateMessagePipe(); - getOrCreateUnidentifiedMessagePipe(); - try { - final SignalServiceAddress address = getSelfAddress(); - - final ContactInfo contact = account.getContactStore().getContact(address); - final int expirationTime = contact != null ? contact.messageExpirationTime : 0; - messageBuilder.withExpiration(expirationTime); - - SignalServiceDataMessage message = messageBuilder.build(); - final SendMessageResult result = sendSelfMessage(message); - return new Pair<>(timestamp, result); - } finally { - account.save(); - } + public void sendTypingMessage( + TypingAction action, Set recipients + ) throws IOException, UntrustedIdentityException, InvalidNumberException { + final var timestamp = System.currentTimeMillis(); + var message = new SignalServiceTypingMessage(action.toSignalService(), timestamp, Optional.absent()); + sendHelper.sendTypingMessage(message, getRecipientIds(recipients)); } - private SendMessageResult sendSelfMessage(SignalServiceDataMessage message) throws IOException { - SignalServiceMessageSender messageSender = createMessageSender(); - - SignalServiceAddress recipient = account.getSelfAddress(); - - final Optional unidentifiedAccess = unidentifiedAccessHelper.getAccessFor(recipient); - SentTranscriptMessage transcript = new SentTranscriptMessage(Optional.of(recipient), - message.getTimestamp(), - message, - message.getExpiresInSeconds(), - Map.of(recipient, unidentifiedAccess.isPresent()), - false); - SignalServiceSyncMessage syncMessage = SignalServiceSyncMessage.forSentTranscript(transcript); + public void sendGroupTypingMessage( + TypingAction action, GroupId groupId + ) throws IOException, NotAGroupMemberException, GroupNotFoundException { + final var timestamp = System.currentTimeMillis(); + final var message = new SignalServiceTypingMessage(action.toSignalService(), + timestamp, + Optional.of(groupId.serialize())); + sendHelper.sendGroupTypingMessage(message, groupId); + } - try { - long startTime = System.currentTimeMillis(); - messageSender.sendMessage(syncMessage, unidentifiedAccess); - return SendMessageResult.success(recipient, - unidentifiedAccess.isPresent(), - false, - System.currentTimeMillis() - startTime); - } catch (UntrustedIdentityException e) { - account.getSignalProtocolStore() - .saveIdentity(resolveSignalServiceAddress(e.getIdentifier()), - e.getIdentityKey(), - TrustLevel.UNTRUSTED); - return SendMessageResult.identityFailure(recipient, e.getIdentityKey()); - } - } - - private SendMessageResult sendMessage( - SignalServiceAddress address, SignalServiceDataMessage message - ) throws IOException { - SignalServiceMessageSender messageSender = createMessageSender(); + private SignalServiceDataMessage.Builder createMessageBuilder() { + final var timestamp = System.currentTimeMillis(); - try { - return messageSender.sendMessage(address, unidentifiedAccessHelper.getAccessFor(address), message); - } catch (UntrustedIdentityException e) { - account.getSignalProtocolStore() - .saveIdentity(resolveSignalServiceAddress(e.getIdentifier()), - e.getIdentityKey(), - TrustLevel.UNTRUSTED); - return SendMessageResult.identityFailure(address, e.getIdentityKey()); - } + var messageBuilder = SignalServiceDataMessage.newBuilder(); + messageBuilder.withTimestamp(timestamp); + return messageBuilder; } - private SignalServiceContent decryptMessage(SignalServiceEnvelope envelope) throws InvalidMetadataMessageException, ProtocolInvalidMessageException, ProtocolDuplicateMessageException, ProtocolLegacyMessageException, ProtocolInvalidKeyIdException, InvalidMetadataVersionException, ProtocolInvalidVersionException, ProtocolNoSessionException, ProtocolInvalidKeyException, SelfSendException, UnsupportedDataMessageException, org.whispersystems.libsignal.UntrustedIdentityException { - SignalServiceCipher cipher = new SignalServiceCipher(account.getSelfAddress(), - account.getSignalProtocolStore(), - certificateValidator); - try { - return cipher.decrypt(envelope); - } catch (ProtocolUntrustedIdentityException e) { - if (e.getCause() instanceof org.whispersystems.libsignal.UntrustedIdentityException) { - org.whispersystems.libsignal.UntrustedIdentityException identityException = (org.whispersystems.libsignal.UntrustedIdentityException) e - .getCause(); - final IdentityKey untrustedIdentity = identityException.getUntrustedIdentity(); - if (untrustedIdentity != null) { - account.getSignalProtocolStore() - .saveIdentity(resolveSignalServiceAddress(identityException.getName()), - untrustedIdentity, - TrustLevel.UNTRUSTED); - } - throw identityException; - } - throw new AssertionError(e); - } + private SignalServiceContent decryptMessage(SignalServiceEnvelope envelope) throws InvalidMetadataMessageException, ProtocolInvalidMessageException, ProtocolDuplicateMessageException, ProtocolLegacyMessageException, ProtocolInvalidKeyIdException, InvalidMetadataVersionException, ProtocolInvalidVersionException, ProtocolNoSessionException, ProtocolInvalidKeyException, SelfSendException, UnsupportedDataMessageException, ProtocolUntrustedIdentityException, InvalidMessageStructureException { + return dependencies.getCipher().decrypt(envelope); } - private void handleEndSession(SignalServiceAddress source) { - account.getSignalProtocolStore().deleteAllSessions(source); + private void handleEndSession(RecipientId recipientId) { + account.getSessionStore().deleteAllSessions(recipientId); } private List handleSignalServiceDataMessage( @@ -1459,14 +1555,14 @@ public class Manager implements Closeable { SignalServiceAddress destination, boolean ignoreAttachments ) { - List actions = new ArrayList<>(); + var actions = new ArrayList(); if (message.getGroupContext().isPresent()) { if (message.getGroupContext().get().getGroupV1().isPresent()) { - SignalServiceGroup groupInfo = message.getGroupContext().get().getGroupV1().get(); - GroupIdV1 groupId = GroupId.v1(groupInfo.getGroupId()); - GroupInfo group = getGroup(groupId); + var groupInfo = message.getGroupContext().get().getGroupV1().get(); + var groupId = GroupId.v1(groupInfo.getGroupId()); + var group = getGroup(groupId); if (group == null || group instanceof GroupInfoV1) { - GroupInfoV1 groupV1 = (GroupInfoV1) group; + var groupV1 = (GroupInfoV1) group; switch (groupInfo.getType()) { case UPDATE: { if (groupV1 == null) { @@ -1474,7 +1570,7 @@ public class Manager implements Closeable { } if (groupInfo.getAvatar().isPresent()) { - SignalServiceAttachment avatar = groupInfo.getAvatar().get(); + var avatar = groupInfo.getAvatar().get(); downloadGroupAvatar(avatar, groupV1.getGroupId()); } @@ -1486,7 +1582,7 @@ public class Manager implements Closeable { groupV1.addMembers(groupInfo.getMembers() .get() .stream() - .map(this::resolveSignalServiceAddress) + .map(this::resolveRecipient) .collect(Collectors.toSet())); } @@ -1500,7 +1596,7 @@ public class Manager implements Closeable { break; case QUIT: { if (groupV1 != null) { - groupV1.removeMember(source); + groupV1.removeMember(resolveRecipient(source)); account.getGroupStore().updateGroup(groupV1); } break; @@ -1516,8 +1612,8 @@ public class Manager implements Closeable { } } if (message.getGroupContext().get().getGroupV2().isPresent()) { - final SignalServiceGroupV2 groupContext = message.getGroupContext().get().getGroupV2().get(); - final GroupMasterKey groupMasterKey = groupContext.getMasterKey(); + final var groupContext = message.getGroupContext().get().getGroupV2().get(); + final var groupMasterKey = groupContext.getMasterKey(); getOrMigrateGroup(groupMasterKey, groupContext.getRevision(), @@ -1525,15 +1621,15 @@ public class Manager implements Closeable { } } - final SignalServiceAddress conversationPartnerAddress = isSync ? destination : source; + 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()) { if (message.getGroupContext().get().getGroupV1().isPresent()) { - SignalServiceGroup groupInfo = message.getGroupContext().get().getGroupV1().get(); - GroupInfoV1 group = account.getGroupStore().getOrCreateGroupV1(GroupId.v1(groupInfo.getGroupId())); + var groupInfo = message.getGroupContext().get().getGroupV1().get(); + var group = account.getGroupStore().getOrCreateGroupV1(GroupId.v1(groupInfo.getGroupId())); if (group != null) { if (group.messageExpirationTime != message.getExpiresInSeconds()) { group.messageExpirationTime = message.getExpiresInSeconds(); @@ -1544,19 +1640,21 @@ public class Manager implements Closeable { // disappearing message timer already stored in the DecryptedGroup } } else if (conversationPartnerAddress != null) { - ContactInfo 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 (message.getAttachments().isPresent() && !ignoreAttachments) { - for (SignalServiceAttachment attachment : message.getAttachments().get()) { - downloadAttachment(attachment); + if (!ignoreAttachments) { + if (message.getAttachments().isPresent()) { + for (var attachment : message.getAttachments().get()) { + downloadAttachment(attachment); + } + } + if (message.getSharedContacts().isPresent()) { + for (var contact : message.getSharedContacts().get()) { + if (contact.getAvatar().isPresent()) { + downloadAttachment(contact.getAvatar().get().getAttachment()); + } + } } } if (message.getProfileKey().isPresent() && message.getProfileKey().get().length == 32) { @@ -1569,33 +1667,35 @@ 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 List previews = message.getPreviews().get(); - for (SignalServiceDataMessage.Preview preview : previews) { + final var previews = message.getPreviews().get(); + for (var preview : previews) { if (preview.getImage().isPresent()) { downloadAttachment(preview.getImage().get()); } } } if (message.getQuote().isPresent()) { - final SignalServiceDataMessage.Quote quote = message.getQuote().get(); + final var quote = message.getQuote().get(); - for (SignalServiceDataMessage.Quote.QuotedAttachment quotedAttachment : quote.getAttachments()) { - final SignalServiceAttachment thumbnail = quotedAttachment.getThumbnail(); + for (var quotedAttachment : quote.getAttachments()) { + final var thumbnail = quotedAttachment.getThumbnail(); if (thumbnail != null) { downloadAttachment(thumbnail); } } } if (message.getSticker().isPresent()) { - final SignalServiceDataMessage.Sticker messageSticker = message.getSticker().get(); - Sticker sticker = account.getStickerStore().getSticker(messageSticker.getPackId()); + final var messageSticker = message.getSticker().get(); + 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); } + enqueueJob(new RetrieveStickerPackJob(stickerPackId, messageSticker.getPackKey())); } return actions; } @@ -1603,14 +1703,14 @@ public class Manager implements Closeable { private GroupInfoV2 getOrMigrateGroup( final GroupMasterKey groupMasterKey, final int revision, final byte[] signedGroupChange ) { - final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey); + final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(groupMasterKey); - GroupIdV2 groupId = GroupUtils.getGroupIdV2(groupSecretParams); - GroupInfo groupInfo = getGroup(groupId); + var groupId = GroupUtils.getGroupIdV2(groupSecretParams); + var groupInfo = getGroup(groupId); 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(), @@ -1626,19 +1726,21 @@ 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); - final String avatar = group.getAvatar(); + final var avatar = group.getAvatar(); if (avatar != null && !avatar.isEmpty()) { downloadGroupAvatar(groupId, groupSecretParams, avatar); } } - groupInfoV2.setGroup(group); + groupInfoV2.setGroup(group, this::resolveRecipient); account.getGroupStore().updateGroup(groupInfoV2); } @@ -1646,53 +1748,70 @@ public class Manager implements Closeable { } private void storeProfileKeysFromMembers(final DecryptedGroup group) { - for (DecryptedMember member : group.getMembersList()) { - final SignalServiceAddress address = resolveSignalServiceAddress(new SignalServiceAddress(UuidUtil.parseOrThrow( - member.getUuid().toByteArray()), null)); + for (var member : group.getMembersList()) { + 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) { } } } private void retryFailedReceivedMessages(ReceiveMessageHandler handler, boolean ignoreAttachments) { - for (CachedMessage cachedMessage : account.getMessageCache().getCachedMessages()) { - retryFailedReceivedMessage(handler, ignoreAttachments, cachedMessage); + Set queuedActions = new HashSet<>(); + for (var cachedMessage : account.getMessageCache().getCachedMessages()) { + var actions = retryFailedReceivedMessage(handler, ignoreAttachments, cachedMessage); + if (actions != null) { + queuedActions.addAll(actions); + } + } + for (var action : queuedActions) { + try { + action.execute(this); + } catch (Throwable e) { + if (e instanceof AssertionError && e.getCause() instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } + logger.warn("Message action failed.", e); + } } } - private void retryFailedReceivedMessage( + private List retryFailedReceivedMessage( final ReceiveMessageHandler handler, final boolean ignoreAttachments, final CachedMessage cachedMessage ) { - SignalServiceEnvelope envelope = cachedMessage.loadEnvelope(); + var envelope = cachedMessage.loadEnvelope(); if (envelope == null) { - return; + return null; } SignalServiceContent content = null; + List actions = null; if (!envelope.isReceipt()) { try { content = decryptMessage(envelope); - } catch (org.whispersystems.libsignal.UntrustedIdentityException e) { - return; + } catch (ProtocolUntrustedIdentityException e) { + if (!envelope.hasSource()) { + final var identifier = e.getSender(); + final var recipientId = resolveRecipient(identifier); + try { + account.getMessageCache().replaceSender(cachedMessage, recipientId); + } catch (IOException ioException) { + logger.warn("Failed to move cached message to recipient folder: {}", ioException.getMessage()); + } + } + return null; } catch (Exception er) { // All other errors are not recoverable, so delete the cached message cachedMessage.delete(); - return; - } - List actions = handleMessage(envelope, content, ignoreAttachments); - for (HandleAction action : actions) { - try { - action.execute(this); - } catch (Throwable e) { - logger.warn("Message action failed.", e); - } + return null; } + actions = handleMessage(envelope, content, ignoreAttachments); } - account.save(); handler.handleMessage(envelope, content, null); cachedMessage.delete(); + return actions; } public void receiveMessages( @@ -1701,25 +1820,32 @@ public class Manager implements Closeable { boolean returnOnTimeout, boolean ignoreAttachments, ReceiveMessageHandler handler - ) throws IOException { + ) throws IOException, InterruptedException { retryFailedReceivedMessages(handler, ignoreAttachments); Set queuedActions = null; - final SignalServiceMessagePipe messagePipe = getOrCreateMessagePipe(); + final var signalWebSocket = dependencies.getSignalWebSocket(); + signalWebSocket.connect(); - boolean hasCaughtUpWithOldMessages = false; + var hasCaughtUpWithOldMessages = false; - while (true) { + while (!Thread.interrupted()) { SignalServiceEnvelope envelope; SignalServiceContent content = null; Exception exception = null; final CachedMessage[] cachedMessage = {null}; + account.setLastReceiveTimestamp(System.currentTimeMillis()); + logger.debug("Checking for new message from server"); try { - Optional result = messagePipe.readOrEmpty(timeout, unit, envelope1 -> { + var result = signalWebSocket.readOrEmpty(unit.toMillis(timeout), envelope1 -> { + final var recipientId = envelope1.hasSource() + ? resolveRecipient(envelope1.getSourceIdentifier()) + : null; // store message on disk, before acknowledging receipt to the server - cachedMessage[0] = account.getMessageCache().cacheMessage(envelope1); + cachedMessage[0] = account.getMessageCache().cacheMessage(envelope1, recipientId); }); + logger.debug("New message received from server"); if (result.isPresent()) { envelope = result.get(); } else { @@ -1727,14 +1853,16 @@ public class Manager implements Closeable { hasCaughtUpWithOldMessages = true; if (queuedActions != null) { - for (HandleAction action : queuedActions) { + for (var action : queuedActions) { try { action.execute(this); } catch (Throwable e) { + if (e instanceof AssertionError && e.getCause() instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } logger.warn("Message action failed.", e); } } - account.save(); queuedActions.clear(); queuedActions = null; } @@ -1742,18 +1870,25 @@ public class Manager implements Closeable { // Continue to wait another timeout for new messages continue; } + } catch (AssertionError e) { + if (e.getCause() instanceof InterruptedException) { + throw (InterruptedException) e.getCause(); + } else { + throw e; + } + } catch (WebSocketUnavailableException e) { + logger.debug("Pipe unexpectedly unavailable, connecting"); + signalWebSocket.connect(); + continue; } catch (TimeoutException e) { if (returnOnTimeout) return; continue; - } catch (InvalidVersionException e) { - logger.warn("Error while receiving messages, ignoring: {}", e.getMessage()); - continue; } if (envelope.hasSource()) { // Store uuid if we don't have it already - SignalServiceAddress source = envelope.getSourceAddress(); - resolveSignalServiceAddress(source); + // address/uuid in envelope is sent by server + resolveRecipientTrusted(envelope.getSourceAddress()); } if (!envelope.isReceipt()) { try { @@ -1761,12 +1896,25 @@ public class Manager implements Closeable { } catch (Exception e) { exception = e; } - List actions = handleMessage(envelope, content, ignoreAttachments); + 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 (HandleAction action : actions) { + for (var action : actions) { try { action.execute(this); } catch (Throwable e) { + if (e instanceof AssertionError && e.getCause() instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } logger.warn("Message action failed.", e); } } @@ -1777,16 +1925,30 @@ public class Manager implements Closeable { queuedActions.addAll(actions); } } - account.save(); + final var notAllowedToSendToGroup = isNotAllowedToSendToGroup(envelope, content); if (isMessageBlocked(envelope, content)) { logger.info("Ignoring a message from blocked user/group: {}", envelope.getTimestamp()); - } else if (isNotAGroupMember(envelope, content)) { - logger.info("Ignoring a message from a non group member: {}", envelope.getTimestamp()); + } else if (notAllowedToSendToGroup) { + logger.info("Ignoring a group message from an unauthorized sender (no member or admin): {} {}", + (envelope.hasSource() ? envelope.getSourceAddress() : content.getSender()).getIdentifier(), + envelope.getTimestamp()); } else { handler.handleMessage(envelope, content, exception); } - if (!(exception instanceof org.whispersystems.libsignal.UntrustedIdentityException)) { - if (cachedMessage[0] != null) { + if (cachedMessage[0] != null) { + 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); + } catch (IOException ioException) { + logger.warn("Failed to move cached message to recipient folder: {}", + ioException.getMessage()); + } + } + } else { cachedMessage[0].delete(); } } @@ -1804,16 +1966,16 @@ public class Manager implements Closeable { } else { return false; } - ContactInfo sourceContact = account.getContactStore().getContact(source); - if (sourceContact != null && sourceContact.blocked) { + final var recipientId = resolveRecipient(source); + if (isContactBlocked(recipientId)) { return true; } if (content != null && content.getDataMessage().isPresent()) { - SignalServiceDataMessage message = content.getDataMessage().get(); + var message = content.getDataMessage().get(); if (message.getGroupContext().isPresent()) { - GroupId groupId = GroupUtils.getGroupId(message.getGroupContext().get()); - GroupInfo group = getGroup(groupId); + var groupId = GroupUtils.getGroupId(message.getGroupContext().get()); + var group = getGroup(groupId); if (group != null && group.isBlocked()) { return true; } @@ -1822,7 +1984,17 @@ public class Manager implements Closeable { return false; } - private boolean isNotAGroupMember( + 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 isNotAllowedToSendToGroup( SignalServiceEnvelope envelope, SignalServiceContent content ) { SignalServiceAddress source; @@ -1834,29 +2006,38 @@ public class Manager implements Closeable { return false; } - if (content != null && content.getDataMessage().isPresent()) { - SignalServiceDataMessage message = content.getDataMessage().get(); - if (message.getGroupContext().isPresent()) { - if (message.getGroupContext().get().getGroupV1().isPresent()) { - SignalServiceGroup groupInfo = message.getGroupContext().get().getGroupV1().get(); - if (groupInfo.getType() == SignalServiceGroup.Type.QUIT) { - return false; - } - } - GroupId groupId = GroupUtils.getGroupId(message.getGroupContext().get()); - GroupInfo group = getGroup(groupId); - if (group != null && !group.isMember(source)) { - return true; - } + if (content == null || !content.getDataMessage().isPresent()) { + return false; + } + + var message = content.getDataMessage().get(); + if (!message.getGroupContext().isPresent()) { + return false; + } + + if (message.getGroupContext().get().getGroupV1().isPresent()) { + var groupInfo = message.getGroupContext().get().getGroupV1().get(); + if (groupInfo.getType() == SignalServiceGroup.Type.QUIT) { + return false; } } - return false; + + var groupId = GroupUtils.getGroupId(message.getGroupContext().get()); + var group = getGroup(groupId); + if (group == null) { + return false; + } + + final var recipientId = resolveRecipient(source); + return !group.isMember(recipientId) || ( + group.isAnnouncementGroup() && !group.isAdmin(recipientId) + ); } private List handleMessage( SignalServiceEnvelope envelope, SignalServiceContent content, boolean ignoreAttachments ) { - List actions = new ArrayList<>(); + var actions = new ArrayList(); if (content != null) { final SignalServiceAddress sender; if (!envelope.isUnidentifiedSender() && envelope.hasSource()) { @@ -1864,11 +2045,9 @@ public class Manager implements Closeable { } else { sender = content.getSender(); } - // Store uuid if we don't have it already - resolveSignalServiceAddress(sender); if (content.getDataMessage().isPresent()) { - SignalServiceDataMessage message = content.getDataMessage().get(); + var message = content.getDataMessage().get(); if (content.isNeedsReceipt()) { actions.add(new SendReceiptAction(sender, message.getTimestamp())); @@ -1882,18 +2061,18 @@ public class Manager implements Closeable { } if (content.getSyncMessage().isPresent()) { account.setMultiDevice(true); - SignalServiceSyncMessage syncMessage = content.getSyncMessage().get(); + var syncMessage = content.getSyncMessage().get(); if (syncMessage.getSent().isPresent()) { - SentTranscriptMessage message = syncMessage.getSent().get(); - final SignalServiceAddress destination = message.getDestination().orNull(); + var message = syncMessage.getSent().get(); + final var destination = message.getDestination().orNull(); actions.addAll(handleSignalServiceDataMessage(message.getMessage(), true, sender, destination, ignoreAttachments)); } - if (syncMessage.getRequest().isPresent()) { - RequestMessage rm = syncMessage.getRequest().get(); + if (syncMessage.getRequest().isPresent() && account.isMasterDevice()) { + var rm = syncMessage.getRequest().get(); if (rm.isContactsRequest()) { actions.add(SendSyncContactsAction.create()); } @@ -1909,27 +2088,34 @@ public class Manager implements Closeable { File tmpFile = null; try { tmpFile = IOUtils.createTempFile(); - final SignalServiceAttachment groupsMessage = syncMessage.getGroups().get(); - try (InputStream attachmentAsStream = retrieveAttachmentAsStream(groupsMessage.asPointer(), - tmpFile)) { - DeviceGroupsInputStream s = new DeviceGroupsInputStream(attachmentAsStream); + final var groupsMessage = syncMessage.getGroups().get(); + try (var attachmentAsStream = retrieveAttachmentAsStream(groupsMessage.asPointer(), tmpFile)) { + var s = new DeviceGroupsInputStream(attachmentAsStream); DeviceGroup g; - while ((g = s.read()) != null) { - GroupInfoV1 syncGroup = account.getGroupStore() - .getOrCreateGroupV1(GroupId.v1(g.getId())); + 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()) { syncGroup.name = g.getName().get(); } 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()) { @@ -1939,7 +2125,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); } @@ -1962,11 +2147,11 @@ public class Manager implements Closeable { } } if (syncMessage.getBlockedList().isPresent()) { - final BlockedListMessage blockedListMessage = syncMessage.getBlockedList().get(); - for (SignalServiceAddress address : blockedListMessage.getAddresses()) { - setContactBlocked(resolveSignalServiceAddress(address), true); + final var blockedListMessage = syncMessage.getBlockedList().get(); + for (var address : blockedListMessage.getAddresses()) { + setContactBlocked(resolveRecipient(address), true); } - for (GroupId groupId : blockedListMessage.getGroupIds() + for (var groupId : blockedListMessage.getGroupIds() .stream() .map(GroupId::unknownVersion) .collect(Collectors.toSet())) { @@ -1982,49 +2167,55 @@ public class Manager implements Closeable { File tmpFile = null; try { tmpFile = IOUtils.createTempFile(); - final ContactsMessage contactsMessage = syncMessage.getContacts().get(); - try (InputStream attachmentAsStream = retrieveAttachmentAsStream(contactsMessage.getContactsStream() + final var contactsMessage = syncMessage.getContacts().get(); + try (var attachmentAsStream = retrieveAttachmentAsStream(contactsMessage.getContactsStream() .asPointer(), tmpFile)) { - DeviceContactsInputStream s = new DeviceContactsInputStream(attachmentAsStream); - if (contactsMessage.isComplete()) { - account.getContactStore().clear(); - } + var s = new DeviceContactsInputStream(attachmentAsStream); DeviceContact c; - while ((c = s.read()) != null) { + while (true) { + try { + c = s.read(); + } catch (IOException e) { + logger.warn("Sync contacts contained invalid contact, ignoring: {}", + e.getMessage()); + continue; + } + if (c == null) { + break; + } if (c.getAddress().matches(account.getSelfAddress()) && c.getProfileKey().isPresent()) { account.setProfileKey(c.getProfileKey().get()); } - final SignalServiceAddress address = resolveSignalServiceAddress(c.getAddress()); - ContactInfo 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 VerifiedMessage verifiedMessage = c.getVerified().get(); - account.getSignalProtocolStore() - .setIdentityTrustLevel(verifiedMessage.getDestination(), + final var verifiedMessage = c.getVerified().get(); + account.getIdentityKeyStore() + .setIdentityTrustLevel(resolveRecipientTrusted(verifiedMessage.getDestination()), verifiedMessage.getIdentityKey(), 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()); } } } @@ -2045,43 +2236,50 @@ public class Manager implements Closeable { } } if (syncMessage.getVerified().isPresent()) { - final VerifiedMessage verifiedMessage = syncMessage.getVerified().get(); - account.getSignalProtocolStore() - .setIdentityTrustLevel(resolveSignalServiceAddress(verifiedMessage.getDestination()), + final var verifiedMessage = syncMessage.getVerified().get(); + account.getIdentityKeyStore() + .setIdentityTrustLevel(resolveRecipientTrusted(verifiedMessage.getDestination()), verifiedMessage.getIdentityKey(), TrustLevel.fromVerifiedState(verifiedMessage.getVerified())); } if (syncMessage.getStickerPackOperations().isPresent()) { - final List stickerPackOperationMessages = syncMessage.getStickerPackOperations() - .get(); - for (StickerPackOperationMessage m : stickerPackOperationMessages) { + final var stickerPackOperationMessages = syncMessage.getStickerPackOperations().get(); + for (var m : stickerPackOperationMessages) { if (!m.getPackId().isPresent()) { continue; } - Sticker sticker = account.getStickerStore().getSticker(m.getPackId().get()); - if (sticker == null) { - if (!m.getPackKey().isPresent()) { - continue; + final var stickerPackId = StickerPackId.deserialize(m.getPackId().get()); + final var installed = !m.getType().isPresent() + || m.getType().get() == StickerPackOperationMessage.Type.INSTALL; + + var sticker = account.getStickerStore().getSticker(stickerPackId); + if (m.getPackKey().isPresent()) { + if (sticker == null) { + sticker = new Sticker(stickerPackId, m.getPackKey().get()); + } + if (installed) { + enqueueJob(new RetrieveStickerPackJob(stickerPackId, m.getPackKey().get())); } - sticker = new Sticker(m.getPackId().get(), m.getPackKey().get()); } - sticker.setInstalled(!m.getType().isPresent() - || m.getType().get() == StickerPackOperationMessage.Type.INSTALL); - account.getStickerStore().updateSticker(sticker); + + if (sticker != null) { + sticker.setInstalled(installed); + account.getStickerStore().updateSticker(sticker); + } } } if (syncMessage.getFetchType().isPresent()) { switch (syncMessage.getFetchType().get()) { case LOCAL_PROFILE: - getRecipientProfile(getSelfAddress(), true); + getRecipientProfile(account.getSelfRecipientId(), true); case STORAGE_MANIFEST: // TODO } } if (syncMessage.getKeys().isPresent()) { - final KeysMessage keysMessage = syncMessage.getKeys().get(); + final var keysMessage = syncMessage.getKeys().get(); if (keysMessage.getStorageService().isPresent()) { - final StorageKey storageKey = keysMessage.getStorageService().get(); + final var storageKey = keysMessage.getStorageService().get(); account.setStorageKey(storageKey); } } @@ -2125,6 +2323,9 @@ public class Manager implements Closeable { avatarStore.storeProfileAvatar(address, outputStream -> retrieveProfileAvatar(avatarPath, profileKey, outputStream)); } catch (Throwable e) { + if (e instanceof AssertionError && e.getCause() instanceof InterruptedException) { + Thread.currentThread().interrupt(); + } logger.warn("Failed to download profile avatar, ignoring: {}", e.getMessage()); } } @@ -2138,9 +2339,9 @@ public class Manager implements Closeable { logger.warn("Invalid state, can't store an attachment stream."); } - SignalServiceAttachmentPointer pointer = attachment.asPointer(); + var pointer = attachment.asPointer(); if (pointer.getPreview().isPresent()) { - final byte[] preview = pointer.getPreview().get(); + final var preview = pointer.getPreview().get(); try { attachmentStore.storeAttachmentPreview(pointer.getRemoteId(), outputStream -> outputStream.write(preview, 0, preview.length)); @@ -2160,15 +2361,14 @@ public class Manager implements Closeable { private void retrieveGroupV2Avatar( GroupSecretParams groupSecretParams, String cdnKey, OutputStream outputStream ) throws IOException { - GroupsV2Operations.GroupOperations groupOperations = groupsV2Operations.forGroup(groupSecretParams); + var groupOperations = dependencies.getGroupsV2Operations().forGroup(groupSecretParams); - File tmpFile = IOUtils.createTempFile(); - try (InputStream input = messageReceiver.retrieveGroupsV2ProfileAvatar(cdnKey, - tmpFile, - ServiceConfig.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE)) { - byte[] encryptedData = IOUtils.readFully(input); + var tmpFile = IOUtils.createTempFile(); + try (InputStream input = dependencies.getMessageReceiver() + .retrieveGroupsV2ProfileAvatar(cdnKey, tmpFile, ServiceConfig.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE)) { + var encryptedData = IOUtils.readFully(input); - byte[] decryptedData = groupOperations.decryptAvatar(encryptedData); + var decryptedData = groupOperations.decryptAvatar(encryptedData); outputStream.write(decryptedData); } finally { try { @@ -2184,11 +2384,12 @@ public class Manager implements Closeable { private void retrieveProfileAvatar( String avatarPath, ProfileKey profileKey, OutputStream outputStream ) throws IOException { - File tmpFile = IOUtils.createTempFile(); - try (InputStream input = messageReceiver.retrieveProfileAvatar(avatarPath, - tmpFile, - profileKey, - ServiceConfig.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE)) { + var tmpFile = IOUtils.createTempFile(); + try (var input = dependencies.getMessageReceiver() + .retrieveProfileAvatar(avatarPath, + tmpFile, + profileKey, + ServiceConfig.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE)) { // Use larger buffer size to prevent AssertionError: Need: 12272 but only have: 8192 ... IOUtils.copyStream(input, outputStream, (int) ServiceConfig.AVATAR_DOWNLOAD_FAILSAFE_MAX_SIZE); } finally { @@ -2206,10 +2407,10 @@ public class Manager implements Closeable { final SignalServiceAttachment attachment, final OutputStream outputStream ) throws IOException { if (attachment.isPointer()) { - SignalServiceAttachmentPointer pointer = attachment.asPointer(); + var pointer = attachment.asPointer(); retrieveAttachmentPointer(pointer, outputStream); } else { - SignalServiceAttachmentStream stream = attachment.asStream(); + var stream = attachment.asStream(); IOUtils.copyStream(stream.getInputStream(), outputStream); } } @@ -2217,8 +2418,8 @@ public class Manager implements Closeable { private void retrieveAttachmentPointer( SignalServiceAttachmentPointer pointer, OutputStream outputStream ) throws IOException { - File tmpFile = IOUtils.createTempFile(); - try (InputStream input = retrieveAttachmentAsStream(pointer, tmpFile)) { + var tmpFile = IOUtils.createTempFile(); + try (var input = retrieveAttachmentAsStream(pointer, tmpFile)) { IOUtils.copyStream(input, outputStream); } catch (MissingConfigurationException | InvalidMessageException e) { throw new IOException(e); @@ -2236,41 +2437,45 @@ public class Manager implements Closeable { private InputStream retrieveAttachmentAsStream( SignalServiceAttachmentPointer pointer, File tmpFile ) throws IOException, InvalidMessageException, MissingConfigurationException { - return messageReceiver.retrieveAttachment(pointer, tmpFile, ServiceConfig.MAX_ATTACHMENT_SIZE); + return dependencies.getMessageReceiver() + .retrieveAttachment(pointer, tmpFile, ServiceConfig.MAX_ATTACHMENT_SIZE); } - void sendGroups() throws IOException, UntrustedIdentityException { - File groupsFile = IOUtils.createTempFile(); + void sendGroups() throws IOException { + var groupsFile = IOUtils.createTempFile(); try { try (OutputStream fos = new FileOutputStream(groupsFile)) { - DeviceGroupsOutputStream out = new DeviceGroupsOutputStream(fos); - for (GroupInfo record : getGroups()) { + var out = new DeviceGroupsOutputStream(fos); + for (var record : getGroups()) { if (record instanceof GroupInfoV1) { - GroupInfoV1 groupInfo = (GroupInfoV1) record; + 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)); } } } if (groupsFile.exists() && groupsFile.length() > 0) { - try (FileInputStream groupsFileStream = new FileInputStream(groupsFile)) { - SignalServiceAttachmentStream attachmentStream = SignalServiceAttachment.newStreamBuilder() + try (var groupsFileStream = new FileInputStream(groupsFile)) { + var attachmentStream = SignalServiceAttachment.newStreamBuilder() .withStream(groupsFileStream) .withContentType("application/octet-stream") .withLength(groupsFile.length()) .build(); - sendSyncMessage(SignalServiceSyncMessage.forGroups(attachmentStream)); + sendHelper.sendSyncMessage(SignalServiceSyncMessage.forGroups(attachmentStream)); } } } finally { @@ -2282,33 +2487,37 @@ public class Manager implements Closeable { } } - public void sendContacts() throws IOException, UntrustedIdentityException { - File contactsFile = IOUtils.createTempFile(); + public void sendContacts() throws IOException { + var contactsFile = IOUtils.createTempFile(); try { try (OutputStream fos = new FileOutputStream(contactsFile)) { - DeviceContactsOutputStream out = new DeviceContactsOutputStream(fos); - for (ContactInfo record : account.getContactStore().getContacts()) { + var out = new DeviceContactsOutputStream(fos); + 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; - IdentityInfo currentIdentity = account.getSignalProtocolStore().getIdentity(record.getAddress()); if (currentIdentity != null) { - verifiedMessage = new VerifiedMessage(record.getAddress(), + verifiedMessage = new VerifiedMessage(address, currentIdentity.getIdentityKey(), currentIdentity.getTrustLevel().toVerifiedState(), currentIdentity.getDateAdded().getTime()); } - ProfileKey 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) { @@ -2327,14 +2536,15 @@ public class Manager implements Closeable { } if (contactsFile.exists() && contactsFile.length() > 0) { - try (FileInputStream contactsFileStream = new FileInputStream(contactsFile)) { - SignalServiceAttachmentStream attachmentStream = SignalServiceAttachment.newStreamBuilder() + try (var contactsFileStream = new FileInputStream(contactsFile)) { + var attachmentStream = SignalServiceAttachment.newStreamBuilder() .withStream(contactsFileStream) .withContentType("application/octet-stream") .withLength(contactsFile.length()) .build(); - sendSyncMessage(SignalServiceSyncMessage.forContacts(new ContactsMessage(attachmentStream, true))); + sendHelper.sendSyncMessage(SignalServiceSyncMessage.forContacts(new ContactsMessage(attachmentStream, + true))); } } } finally { @@ -2346,68 +2556,75 @@ public class Manager implements Closeable { } } - void sendBlockedList() throws IOException, UntrustedIdentityException { - List addresses = new ArrayList<>(); - for (ContactInfo record : account.getContactStore().getContacts()) { - if (record.blocked) { - addresses.add(record.getAddress()); + void sendBlockedList() throws IOException { + var addresses = new ArrayList(); + for (var record : account.getContactStore().getContacts()) { + if (record.second().isBlocked()) { + addresses.add(resolveSignalServiceAddress(record.first())); } } - List groupIds = new ArrayList<>(); - for (GroupInfo record : getGroups()) { + var groupIds = new ArrayList(); + for (var record : getGroups()) { if (record.isBlocked()) { groupIds.add(record.getGroupId().serialize()); } } - sendSyncMessage(SignalServiceSyncMessage.forBlocked(new BlockedListMessage(addresses, groupIds))); + sendHelper.sendSyncMessage(SignalServiceSyncMessage.forBlocked(new BlockedListMessage(addresses, groupIds))); } private void sendVerifiedMessage( SignalServiceAddress destination, IdentityKey identityKey, TrustLevel trustLevel - ) throws IOException, UntrustedIdentityException { - VerifiedMessage verifiedMessage = new VerifiedMessage(destination, + ) throws IOException { + var verifiedMessage = new VerifiedMessage(destination, identityKey, trustLevel.toVerifiedState(), System.currentTimeMillis()); - sendSyncMessage(SignalServiceSyncMessage.forVerified(verifiedMessage)); + sendHelper.sendSyncMessage(SignalServiceSyncMessage.forVerified(verifiedMessage)); } - public List getContacts() { + public List> getContacts() { return account.getContactStore().getContacts(); } - public String getContactOrProfileName(String number) { - final SignalServiceAddress 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 ContactInfo 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 SignalProfileEntry profileEntry = account.getProfileStore().getProfileEntry(address); - if (profileEntry != null && profileEntry.getProfile() != null) { - return profileEntry.getProfile().getName(); + if (recipient.getProfile() != null && recipient.getProfile() != null) { + return recipient.getProfile().getDisplayName(); } return null; } public GroupInfo getGroup(GroupId groupId) { - final GroupInfo group = account.getGroupStore().getGroup(groupId); - if (group instanceof GroupInfoV2 && ((GroupInfoV2) group).getGroup() == null) { - final GroupSecretParams groupSecretParams = GroupSecretParams.deriveFromMasterKey(((GroupInfoV2) group).getMasterKey()); - ((GroupInfoV2) group).setGroup(groupHelper.getDecryptedGroup(groupSecretParams)); + return getGroup(groupId, false); + } + + public GroupInfo getGroup(GroupId groupId, boolean forceUpdate) { + final var group = account.getGroupStore().getGroup(groupId); + if (group instanceof GroupInfoV2 && (forceUpdate || ((GroupInfoV2) group).getGroup() == null)) { + final var groupSecretParams = GroupSecretParams.deriveFromMasterKey(((GroupInfoV2) group).getMasterKey()); + ((GroupInfoV2) group).setGroup(groupV2Helper.getDecryptedGroup(groupSecretParams), this::resolveRecipient); account.getGroupStore().updateGroup(group); } return group; } public List getIdentities() { - return account.getSignalProtocolStore().getIdentities(); + return account.getIdentityKeyStore().getIdentities(); } public List getIdentities(String number) throws InvalidNumberException { - return account.getSignalProtocolStore().getIdentities(canonicalizeAndResolveSignalServiceAddress(number)); + final var identity = account.getIdentityKeyStore().getIdentity(canonicalizeAndResolveRecipient(number)); + return identity == null ? List.of() : List.of(identity); } /** @@ -2417,27 +2634,10 @@ public class Manager implements Closeable { * @param fingerprint Fingerprint */ public boolean trustIdentityVerified(String name, byte[] fingerprint) throws InvalidNumberException { - SignalServiceAddress address = canonicalizeAndResolveSignalServiceAddress(name); - List ids = account.getSignalProtocolStore().getIdentities(address); - if (ids == null) { - return false; - } - for (IdentityInfo id : ids) { - if (!Arrays.equals(id.getIdentityKey().serialize(), fingerprint)) { - continue; - } - - account.getSignalProtocolStore() - .setIdentityTrustLevel(address, id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED); - try { - sendVerifiedMessage(address, id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED); - } catch (IOException | UntrustedIdentityException e) { - logger.warn("Failed to send verification sync message: {}", e.getMessage()); - } - account.save(); - return true; - } - return false; + var recipientId = canonicalizeAndResolveRecipient(name); + return trustIdentity(recipientId, + identityKey -> Arrays.equals(identityKey.serialize(), fingerprint), + TrustLevel.TRUSTED_VERIFIED); } /** @@ -2447,27 +2647,11 @@ public class Manager implements Closeable { * @param safetyNumber Safety number */ public boolean trustIdentityVerifiedSafetyNumber(String name, String safetyNumber) throws InvalidNumberException { - SignalServiceAddress address = canonicalizeAndResolveSignalServiceAddress(name); - List ids = account.getSignalProtocolStore().getIdentities(address); - if (ids == null) { - return false; - } - for (IdentityInfo id : ids) { - if (!safetyNumber.equals(computeSafetyNumber(address, id.getIdentityKey()))) { - continue; - } - - account.getSignalProtocolStore() - .setIdentityTrustLevel(address, id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED); - try { - sendVerifiedMessage(address, id.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED); - } catch (IOException | UntrustedIdentityException e) { - logger.warn("Failed to send verification sync message: {}", e.getMessage()); - } - account.save(); - return true; - } - return false; + var recipientId = canonicalizeAndResolveRecipient(name); + var address = account.getRecipientStore().resolveServiceAddress(recipientId); + return trustIdentity(recipientId, + identityKey -> safetyNumber.equals(computeSafetyNumber(address, identityKey)), + TrustLevel.TRUSTED_VERIFIED); } /** @@ -2475,50 +2659,75 @@ public class Manager implements Closeable { * * @param name username of the identity */ - public boolean trustIdentityAllKeys(String name) { - SignalServiceAddress address = resolveSignalServiceAddress(name); - List ids = account.getSignalProtocolStore().getIdentities(address); - if (ids == null) { + public boolean trustIdentityAllKeys(String name) throws InvalidNumberException { + var recipientId = canonicalizeAndResolveRecipient(name); + return trustIdentity(recipientId, identityKey -> true, TrustLevel.TRUSTED_UNVERIFIED); + } + + private boolean trustIdentity( + RecipientId recipientId, Function verifier, TrustLevel trustLevel + ) { + var identity = account.getIdentityKeyStore().getIdentity(recipientId); + if (identity == null) { return false; } - for (IdentityInfo id : ids) { - if (id.getTrustLevel() == TrustLevel.UNTRUSTED) { - account.getSignalProtocolStore() - .setIdentityTrustLevel(address, id.getIdentityKey(), TrustLevel.TRUSTED_UNVERIFIED); - try { - sendVerifiedMessage(address, id.getIdentityKey(), TrustLevel.TRUSTED_UNVERIFIED); - } catch (IOException | UntrustedIdentityException e) { - logger.warn("Failed to send verification sync message: {}", e.getMessage()); - } - } + + if (!verifier.apply(identity.getIdentityKey())) { + return false; } - account.save(); + + account.getIdentityKeyStore().setIdentityTrustLevel(recipientId, identity.getIdentityKey(), trustLevel); + try { + var address = account.getRecipientStore().resolveServiceAddress(recipientId); + sendVerifiedMessage(address, identity.getIdentityKey(), trustLevel); + } catch (IOException e) { + logger.warn("Failed to send verification sync message: {}", e.getMessage()); + } + return true; } - public String computeSafetyNumber( - SignalServiceAddress theirAddress, IdentityKey theirIdentityKey + private void handleIdentityFailure( + final RecipientId recipientId, final SendMessageResult.IdentityFailure identityFailure ) { - return Utils.computeSafetyNumber(ServiceConfig.capabilities.isUuid(), + final var identityKey = identityFailure.getIdentityKey(); + if (identityKey != null) { + final var newIdentity = account.getIdentityKeyStore().saveIdentity(recipientId, identityKey, new Date()); + if (newIdentity) { + account.getSessionStore().archiveSessions(recipientId); + } + } else { + // Retrieve profile to get the current identity key from the server + retrieveEncryptedProfile(recipientId); + } + } + + public String computeSafetyNumber(SignalServiceAddress theirAddress, IdentityKey theirIdentityKey) { + final var fingerprint = Utils.computeSafetyNumber(capabilities.isUuid(), account.getSelfAddress(), getIdentityKeyPair().getPublicKey(), theirAddress, theirIdentityKey); + return fingerprint == null ? null : fingerprint.getDisplayableFingerprint().getDisplayText(); } - public SignalServiceAddress canonicalizeAndResolveSignalServiceAddress(String identifier) throws InvalidNumberException { - String canonicalizedNumber = UuidUtil.isUuid(identifier) - ? identifier - : PhoneNumberFormatter.formatNumber(identifier, account.getUsername()); - return resolveSignalServiceAddress(canonicalizedNumber); + public byte[] computeSafetyNumberForScanning(SignalServiceAddress theirAddress, IdentityKey theirIdentityKey) { + final var fingerprint = Utils.computeSafetyNumber(capabilities.isUuid(), + account.getSelfAddress(), + getIdentityKeyPair().getPublicKey(), + theirAddress, + theirIdentityKey); + return fingerprint == null ? null : fingerprint.getScannableFingerprint().getSerialized(); } + @Deprecated public SignalServiceAddress resolveSignalServiceAddress(String identifier) { - SignalServiceAddress address = Utils.getSignalServiceAddressFromIdentifier(identifier); + var address = Utils.getSignalServiceAddressFromIdentifier(identifier); return resolveSignalServiceAddress(address); } + @Deprecated public SignalServiceAddress resolveSignalServiceAddress(SignalServiceAddress address) { if (address.matches(account.getSelfAddress())) { return account.getSelfAddress(); @@ -2527,21 +2736,51 @@ public class Manager implements Closeable { return account.getRecipientStore().resolveServiceAddress(address); } + public SignalServiceAddress resolveSignalServiceAddress(RecipientId recipientId) { + return account.getRecipientStore().resolveServiceAddress(recipientId); + } + + private RecipientId canonicalizeAndResolveRecipient(String identifier) throws InvalidNumberException { + var canonicalizedNumber = UuidUtil.isUuid(identifier) ? identifier : canonicalizePhoneNumber(identifier); + + return resolveRecipient(canonicalizedNumber); + } + + private String canonicalizePhoneNumber(final String number) throws InvalidNumberException { + return PhoneNumberFormatter.formatNumber(number, account.getUsername()); + } + + private RecipientId resolveRecipient(final String identifier) { + var address = Utils.getSignalServiceAddressFromIdentifier(identifier); + + return resolveRecipient(address); + } + + public RecipientId resolveRecipient(SignalServiceAddress address) { + return account.getRecipientStore().resolveRecipient(address); + } + + private RecipientId resolveRecipientTrusted(SignalServiceAddress address) { + return account.getRecipientStore().resolveRecipientTrusted(address); + } + + private void enqueueJob(Job job) { + var context = new Context(account, + dependencies.getAccountManager(), + dependencies.getMessageReceiver(), + stickerPackStore); + job.run(context); + } + @Override public void close() throws IOException { close(true); } void close(boolean closeAccount) throws IOException { - if (messagePipe != null) { - messagePipe.shutdown(); - messagePipe = null; - } + executor.shutdown(); - if (unidentifiedMessagePipe != null) { - unidentifiedMessagePipe.shutdown(); - unidentifiedMessagePipe = null; - } + dependencies.getSignalWebSocket().disconnect(); if (closeAccount && account != null) { account.close();