/* Copyright (C) 2015-2021 AsamK and contributors This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package org.asamk.signal.manager; import org.asamk.signal.manager.api.Device; import org.asamk.signal.manager.api.Message; import org.asamk.signal.manager.api.RecipientIdentifier; import org.asamk.signal.manager.api.SendGroupMessageResults; import org.asamk.signal.manager.api.SendMessageResults; 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.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.GroupSendingNotAllowedException; 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.groups.GroupInfo; import org.asamk.signal.manager.storage.groups.GroupInfoV1; import org.asamk.signal.manager.storage.identities.IdentityInfo; import org.asamk.signal.manager.storage.identities.TrustNewIdentity; import org.asamk.signal.manager.storage.messageCache.CachedMessage; 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; import org.asamk.signal.manager.util.ProfileUtils; import org.asamk.signal.manager.util.StickerUtils; import org.asamk.signal.manager.util.Utils; import org.signal.libsignal.metadata.ProtocolInvalidMessageException; import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException; import org.signal.zkgroup.InvalidInputException; import org.signal.zkgroup.profiles.ProfileKey; import org.signal.zkgroup.profiles.ProfileKeyCredential; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.IdentityKeyPair; import org.whispersystems.libsignal.InvalidKeyException; import org.whispersystems.libsignal.InvalidMessageException; import org.whispersystems.libsignal.ecc.ECPublicKey; import org.whispersystems.libsignal.fingerprint.Fingerprint; import org.whispersystems.libsignal.fingerprint.FingerprintParsingException; import org.whispersystems.libsignal.fingerprint.FingerprintVersionMismatchException; 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.SignalSessionLock; import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException; import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException; import org.whispersystems.signalservice.api.messages.SendMessageResult; import org.whispersystems.signalservice.api.messages.SignalServiceAttachment; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream; import org.whispersystems.signalservice.api.messages.SignalServiceContent; import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; import org.whispersystems.signalservice.api.messages.SignalServiceGroup; import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage; 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; import org.whispersystems.signalservice.api.messages.multidevice.DeviceContactsInputStream; import org.whispersystems.signalservice.api.messages.multidevice.DeviceContactsOutputStream; 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.RequestMessage; 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.MissingConfigurationException; 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.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; import org.whispersystems.signalservice.internal.push.SignalServiceProtos; import org.whispersystems.signalservice.internal.util.DynamicCredentialsProvider; import org.whispersystems.signalservice.internal.util.Hex; import org.whispersystems.signalservice.internal.util.Util; import java.io.Closeable; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.URI; import java.net.URISyntaxException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; 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; public class Manager implements Closeable { private final static Logger logger = LoggerFactory.getLogger(Manager.class); private final ServiceEnvironmentConfig serviceEnvironmentConfig; private final SignalDependencies dependencies; private SignalAccount account; private final ExecutorService executor = Executors.newCachedThreadPool(); private final ProfileHelper profileHelper; private final PinHelper pinHelper; private final SendHelper sendHelper; private final GroupHelper groupHelper; 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, PathConfig pathConfig, ServiceEnvironmentConfig serviceEnvironmentConfig, String userAgent ) { this.account = account; this.serviceEnvironmentConfig = serviceEnvironmentConfig; final var credentialsProvider = new DynamicCredentialsProvider(account.getUuid(), account.getUsername(), account.getPassword(), account.getDeviceId()); this.dependencies = new SignalDependencies(account.getSelfAddress(), serviceEnvironmentConfig, userAgent, credentialsProvider, account.getSignalProtocolStore(), executor, sessionLock); this.pinHelper = new PinHelper(dependencies.getKeyBackupService()); final var unidentifiedAccessHelper = new UnidentifiedAccessHelper(account::getProfileKey, account.getProfileStore()::getProfileKey, this::getRecipientProfile, this::getSenderCertificate); this.profileHelper = new ProfileHelper(account.getProfileStore()::getProfileKey, unidentifiedAccessHelper::getAccessFor, dependencies::getProfileService, dependencies::getMessageReceiver, this::resolveSignalServiceAddress); final GroupV2Helper groupV2Helper = new GroupV2Helper(this::getRecipientProfileKeyCredential, this::getRecipientProfile, account::getSelfRecipientId, dependencies.getGroupsV2Operations(), dependencies.getGroupsV2Api(), 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); this.groupHelper = new GroupHelper(account, dependencies, sendHelper, groupV2Helper, avatarStore, this::resolveSignalServiceAddress, this::resolveRecipient); } public String getUsername() { return account.getUsername(); } public SignalServiceAddress getSelfAddress() { return account.getSelfAddress(); } public RecipientId getSelfRecipientId() { return account.getSelfRecipientId(); } private IdentityKeyPair getIdentityKeyPair() { return account.getIdentityKeyPair(); } public int getDeviceId() { return account.getDeviceId(); } public static Manager init( String username, File settingsPath, ServiceEnvironment serviceEnvironment, String userAgent, final TrustNewIdentity trustNewIdentity ) throws IOException, NotRegisteredException { var pathConfig = PathConfig.createDefault(settingsPath); if (!SignalAccount.userExists(pathConfig.getDataPath(), username)) { throw new NotRegisteredException(); } var account = SignalAccount.load(pathConfig.getDataPath(), username, true, trustNewIdentity); if (!account.isRegistered()) { throw new NotRegisteredException(); } final var serviceEnvironmentConfig = ServiceConfig.getServiceEnvironmentConfig(serviceEnvironment, userAgent); return new Manager(account, pathConfig, serviceEnvironmentConfig, userAgent); } public static List getAllLocalUsernames(File settingsPath) { var pathConfig = PathConfig.createDefault(settingsPath); final var dataPath = pathConfig.getDataPath(); final var files = dataPath.listFiles(); if (files == null) { return List.of(); } return Arrays.stream(files) .filter(File::isFile) .map(File::getName) .filter(file -> PhoneNumberFormatter.isValidNumber(file, null)) .collect(Collectors.toList()); } public void checkAccountState() throws IOException { if (account.getLastReceiveTimestamp() == 0) { logger.info("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(); } if (account.getUuid() == null) { account.setUuid(dependencies.getAccountManager().getOwnUuid()); } updateAccountAttributes(); } /** * 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 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 { 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 var contactDetails = getRegisteredUsers(canonicalizedNumbers.values() .stream() .filter(s -> !s.isEmpty()) .collect(Collectors.toSet())); 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 { 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 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), */ 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) { 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) { if (avatar.isPresent()) { avatarStore.storeProfileAvatar(getSelfAddress(), outputStream -> IOUtils.copyFileToStream(avatar.get(), outputStream)); } else { avatarStore.deleteProfileAvatar(getSelfAddress()); } } account.getProfileStore().storeProfile(account.getSelfRecipientId(), newProfile); 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. dependencies.getAccountManager().setGcmId(Optional.absent()); account.setRegistered(false); } 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); 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 { dependencies.getAccountManager().removeDevice(deviceId); var devices = dependencies.getAccountManager().getDevices(); account.setMultiDevice(devices.size() > 1); } public void addDeviceLink(URI linkUri) throws IOException, InvalidKeyException { var info = DeviceLinkInfo.parseDeviceLinkUri(linkUri); addDevice(info.deviceIdentifier, info.deviceKey); } private void addDevice(String deviceIdentifier, ECPublicKey deviceKey) throws IOException, InvalidKeyException { var identityKeyPair = getIdentityKeyPair(); var verificationCode = dependencies.getAccountManager().getNewDeviceVerificationCode(); dependencies.getAccountManager() .addDevice(deviceIdentifier, deviceKey, identityKeyPair, Optional.of(account.getProfileKey().serialize()), verificationCode); account.setMultiDevice(true); } public void setRegistrationLockPin(Optional pin) throws IOException, UnauthenticatedResponseException { if (!account.isMasterDevice()) { throw new RuntimeException("Only master device can set a PIN"); } if (pin.isPresent()) { final var masterKey = account.getPinMasterKey() != null ? account.getPinMasterKey() : KeyUtils.createMasterKey(); pinHelper.setRegistrationLockPin(pin.get(), masterKey); account.setRegistrationLockPin(pin.get(), masterKey); } else { // Remove KBS Pin pinHelper.removeRegistrationLockPin(); account.setRegistrationLockPin(null, null); } } void refreshPreKeys() throws IOException { var oneTimePreKeys = generatePreKeys(); final var identityKeyPair = getIdentityKeyPair(); var signedPreKeyRecord = generateSignedPreKey(identityKeyPair); dependencies.getAccountManager().setPreKeys(identityKeyPair.getPublicKey(), signedPreKeyRecord, oneTimePreKeys); } private List generatePreKeys() { final var offset = account.getPreKeyIdOffset(); var records = KeyUtils.generatePreKeyRecords(offset, ServiceConfig.PREKEY_BATCH_SIZE); account.addPreKeys(records); return records; } private SignedPreKeyRecord generateSignedPreKey(IdentityKeyPair identityKeyPair) { final var signedPreKeyId = account.getNextSignedPreKeyId(); var record = KeyUtils.generateSignedPreKeyRecord(identityKeyPair, signedPreKeyId); account.addSignedPreKey(record); return record; } public Profile getRecipientProfile( RecipientId recipientId ) { return getRecipientProfile(recipientId, false); } 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; } 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 Profile decryptProfileIfKeyKnown( final RecipientId recipientId, final SignalServiceProfile encryptedProfile ) { 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 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; } } private ProfileAndCredential retrieveProfileAndCredential( final RecipientId recipientId, final SignalServiceProfile.RequestType requestType ) throws IOException { final var profileAndCredential = profileHelper.retrieveProfileSync(recipientId, requestType); final var profile = profileAndCredential.getProfile(); try { var newIdentity = account.getIdentityKeyStore() .saveIdentity(recipientId, new IdentityKey(Base64.getDecoder().decode(profile.getIdentityKey())), new Date()); if (newIdentity) { account.getSessionStore().archiveSessions(recipientId); } } catch (InvalidKeyException ignored) { logger.warn("Got invalid identity key in profile for {}", resolveSignalServiceAddress(recipientId).getIdentifier()); } return profileAndCredential; } private ProfileKeyCredential getRecipientProfileKeyCredential(RecipientId recipientId) { var profileKeyCredential = account.getProfileStore().getProfileKeyCredential(recipientId); if (profileKeyCredential != null) { return profileKeyCredential; } ProfileAndCredential profileAndCredential; try { profileAndCredential = retrieveProfileAndCredential(recipientId, SignalServiceProfile.RequestType.PROFILE_AND_CREDENTIAL); } catch (IOException e) { logger.warn("Failed to retrieve profile key credential, ignoring: {}", e.getMessage()); return null; } 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().storeProfile(recipientId, profile); } return profileKeyCredential; } private Profile decryptProfileAndDownloadAvatar( final RecipientId recipientId, final ProfileKey profileKey, final SignalServiceProfile encryptedProfile ) { if (encryptedProfile.getAvatar() != null) { downloadProfileAvatar(resolveSignalServiceAddress(recipientId), encryptedProfile.getAvatar(), profileKey); } return ProfileUtils.decryptProfile(profileKey, encryptedProfile); } private Optional createContactAvatarAttachment(SignalServiceAddress address) throws IOException { final var streamDetails = avatarStore.retrieveContactAvatar(address); if (streamDetails == null) { return Optional.absent(); } return Optional.of(AttachmentUtils.createAttachment(streamDetails, Optional.absent())); } public List getGroups() { return account.getGroupStore().getGroups(); } public SendGroupMessageResults quitGroup( GroupId groupId, Set groupAdmins ) throws GroupNotFoundException, IOException, NotAGroupMemberException, LastGroupAdminException { final var newAdmins = getRecipientIds(groupAdmins); return groupHelper.quitGroup(groupId, newAdmins); } public void deleteGroup(GroupId groupId) throws IOException { account.getGroupStore().deleteGroup(groupId); avatarStore.deleteGroupAvatar(groupId); } public Pair createGroup( String name, Set members, File avatarFile ) throws IOException, AttachmentInvalidException { return groupHelper.createGroup(name, members == null ? null : getRecipientIds(members), avatarFile); } public SendGroupMessageResults updateGroup( GroupId groupId, String name, String description, Set members, Set removeMembers, Set admins, Set removeAdmins, boolean resetGroupLink, GroupLinkState groupLinkState, GroupPermission addMemberPermission, GroupPermission editDetailsPermission, File avatarFile, Integer expirationTimer, Boolean isAnnouncementGroup ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException, GroupSendingNotAllowedException { return groupHelper.updateGroup(groupId, name, 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); } public Pair joinGroup( GroupInviteLinkUrl inviteLinkUrl ) throws IOException, GroupLinkNotActiveException { return groupHelper.joinGroup(inviteLinkUrl); } public SendMessageResults sendMessage( SignalServiceDataMessage.Builder messageBuilder, Set recipients ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { var results = new HashMap>(); long timestamp = System.currentTimeMillis(); messageBuilder.withTimestamp(timestamp); for (final var recipient : recipients) { if (recipient instanceof RecipientIdentifier.Single) { final var recipientId = resolveRecipient((RecipientIdentifier.Single) recipient); final var result = sendHelper.sendMessage(messageBuilder, recipientId); results.put(recipient, List.of(result)); } else if (recipient instanceof RecipientIdentifier.NoteToSelf) { final var result = sendHelper.sendSelfMessage(messageBuilder); results.put(recipient, List.of(result)); } else if (recipient instanceof RecipientIdentifier.Group) { final var groupId = ((RecipientIdentifier.Group) recipient).groupId; final var result = sendHelper.sendAsGroupMessage(messageBuilder, groupId); results.put(recipient, result); } } return new SendMessageResults(timestamp, results); } public void sendTypingMessage( SignalServiceTypingMessage.Action action, Set recipients ) throws IOException, UntrustedIdentityException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { final var timestamp = System.currentTimeMillis(); for (var recipient : recipients) { if (recipient instanceof RecipientIdentifier.Single) { final var message = new SignalServiceTypingMessage(action, timestamp, Optional.absent()); final var recipientId = resolveRecipient((RecipientIdentifier.Single) recipient); sendHelper.sendTypingMessage(message, recipientId); } else if (recipient instanceof RecipientIdentifier.Group) { final var groupId = ((RecipientIdentifier.Group) recipient).groupId; final var message = new SignalServiceTypingMessage(action, timestamp, Optional.of(groupId.serialize())); sendHelper.sendGroupTypingMessage(message, groupId); } } } SendGroupMessageResults sendGroupInfoMessage( GroupIdV1 groupId, SignalServiceAddress recipient ) throws IOException, NotAGroupMemberException, GroupNotFoundException, AttachmentInvalidException { final var recipientId = resolveRecipient(recipient); return groupHelper.sendGroupInfoMessage(groupId, recipientId); } SendGroupMessageResults sendGroupInfoRequest( GroupIdV1 groupId, SignalServiceAddress recipient ) throws IOException { final var recipientId = resolveRecipient(recipient); return groupHelper.sendGroupInfoRequest(groupId, recipientId); } public void sendReadReceipt( RecipientIdentifier.Single sender, List messageIds ) throws IOException, UntrustedIdentityException { var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.READ, messageIds, System.currentTimeMillis()); sendHelper.sendReceiptMessage(receiptMessage, resolveRecipient(sender)); } public void sendViewedReceipt( RecipientIdentifier.Single sender, List messageIds ) throws IOException, UntrustedIdentityException { var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.VIEWED, messageIds, System.currentTimeMillis()); sendHelper.sendReceiptMessage(receiptMessage, resolveRecipient(sender)); } void sendDeliveryReceipt( SignalServiceAddress remoteAddress, List messageIds ) throws IOException, UntrustedIdentityException { var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.DELIVERY, messageIds, System.currentTimeMillis()); sendHelper.sendReceiptMessage(receiptMessage, resolveRecipient(remoteAddress)); } public SendMessageResults sendMessage( Message message, Set recipients ) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { final var messageBuilder = SignalServiceDataMessage.newBuilder(); applyMessage(messageBuilder, message); return sendMessage(messageBuilder, recipients); } private void applyMessage( final SignalServiceDataMessage.Builder messageBuilder, final Message message ) throws AttachmentInvalidException, IOException { messageBuilder.withBody(message.getMessageText()); if (message.getAttachments() != null) { var attachmentStreams = AttachmentUtils.getSignalServiceAttachments(message.getAttachments()); // Upload attachments here, so we only upload once even for multiple recipients 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()) { attachmentPointers.add(attachment.asPointer()); } } messageBuilder.withAttachments(attachmentPointers); } } public SendMessageResults sendRemoteDeleteMessage( long targetSentTimestamp, Set recipients ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { var delete = new SignalServiceDataMessage.RemoteDelete(targetSentTimestamp); final var messageBuilder = SignalServiceDataMessage.newBuilder().withRemoteDelete(delete); return sendMessage(messageBuilder, recipients); } public SendMessageResults sendMessageReaction( String emoji, boolean remove, RecipientIdentifier.Single targetAuthor, long targetSentTimestamp, Set recipients ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { var targetAuthorRecipientId = resolveRecipient(targetAuthor); var reaction = new SignalServiceDataMessage.Reaction(emoji, remove, resolveSignalServiceAddress(targetAuthorRecipientId), targetSentTimestamp); final var messageBuilder = SignalServiceDataMessage.newBuilder().withReaction(reaction); return sendMessage(messageBuilder, recipients); } public SendMessageResults sendEndSessionMessage(Set recipients) throws IOException { var messageBuilder = SignalServiceDataMessage.newBuilder().asEndSessionMessage(); try { return sendMessage(messageBuilder, recipients.stream().map(RecipientIdentifier.class::cast).collect(Collectors.toSet())); } catch (GroupNotFoundException | NotAGroupMemberException | GroupSendingNotAllowedException e) { throw new AssertionError(e); } finally { for (var recipient : recipients) { final var recipientId = resolveRecipient((RecipientIdentifier.Single) recipient); handleEndSession(recipientId); } } } void renewSession(RecipientId recipientId) throws IOException { account.getSessionStore().archiveSessions(recipientId); if (!recipientId.equals(getSelfRecipientId())) { sendHelper.sendNullMessage(recipientId); } } public void setContactName( RecipientIdentifier.Single recipient, String name ) throws NotMasterDeviceException { if (!account.isMasterDevice()) { throw new NotMasterDeviceException(); } final var recipientId = resolveRecipient(recipient); 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( RecipientIdentifier.Single recipient, boolean blocked ) throws NotMasterDeviceException { if (!account.isMasterDevice()) { throw new NotMasterDeviceException(); } setContactBlocked(resolveRecipient(recipient), blocked); } private void setContactBlocked(RecipientId recipientId, boolean blocked) { var contact = account.getContactStore().getContact(recipientId); final var builder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact); // TODO cycle our profile key account.getContactStore().storeContact(recipientId, builder.withBlocked(blocked).build()); } public void setGroupBlocked(final GroupId groupId, final boolean blocked) throws GroupNotFoundException { var group = getGroup(groupId); if (group == null) { throw new GroupNotFoundException(groupId); } group.setBlocked(blocked); // TODO cycle our profile key account.getGroupStore().updateGroup(group); } /** * Change the expiration timer for a contact */ public void setExpirationTimer( RecipientIdentifier.Single recipient, int messageExpirationTimer ) throws IOException { var recipientId = resolveRecipient(recipient); setExpirationTimer(recipientId, messageExpirationTimer); final var messageBuilder = SignalServiceDataMessage.newBuilder().asExpirationUpdate(); try { sendMessage(messageBuilder, Set.of(recipient)); } catch (NotAGroupMemberException | GroupNotFoundException | GroupSendingNotAllowedException e) { throw new AssertionError(e); } } 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()); } /** * Upload the sticker pack from path. * * @param path Path can be a path to a manifest.json file or to a zip file that contains a manifest.json file * @return if successful, returns the URL to install the sticker pack in the signal app */ public URI uploadStickerPack(File path) throws IOException, StickerPackInvalidException { var manifest = StickerUtils.getSignalServiceStickerManifestUpload(path); var messageSender = dependencies.getMessageSender(); var packKey = KeyUtils.createStickerUploadKey(); var packIdString = messageSender.uploadStickerManifest(manifest, packKey); var packId = StickerPackId.deserialize(Hex.fromStringCondensed(packIdString)); var sticker = new Sticker(packId, packKey); account.getStickerStore().updateSticker(sticker); try { return new URI("https", "signal.art", "/addstickers/", "pack_id=" + URLEncoder.encode(Hex.toStringCondensed(packId.serialize()), StandardCharsets.UTF_8) + "&pack_key=" + URLEncoder.encode(Hex.toStringCondensed(packKey), StandardCharsets.UTF_8)); } catch (URISyntaxException e) { throw new AssertionError(e); } } 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(); var message = SignalServiceSyncMessage.forRequest(new RequestMessage(r)); sendHelper.sendSyncMessage(message); } private void requestSyncContacts() throws IOException { var r = SignalServiceProtos.SyncMessage.Request.newBuilder() .setType(SignalServiceProtos.SyncMessage.Request.Type.CONTACTS) .build(); var message = SignalServiceSyncMessage.forRequest(new RequestMessage(r)); sendHelper.sendSyncMessage(message); } private void requestSyncBlocked() throws IOException { var r = SignalServiceProtos.SyncMessage.Request.newBuilder() .setType(SignalServiceProtos.SyncMessage.Request.Type.BLOCKED) .build(); var message = SignalServiceSyncMessage.forRequest(new RequestMessage(r)); sendHelper.sendSyncMessage(message); } private void requestSyncConfiguration() throws IOException { var r = SignalServiceProtos.SyncMessage.Request.newBuilder() .setType(SignalServiceProtos.SyncMessage.Request.Type.CONFIGURATION) .build(); var message = SignalServiceSyncMessage.forRequest(new RequestMessage(r)); sendHelper.sendSyncMessage(message); } private void requestSyncKeys() throws IOException { var r = SignalServiceProtos.SyncMessage.Request.newBuilder() .setType(SignalServiceProtos.SyncMessage.Request.Type.KEYS) .build(); var message = SignalServiceSyncMessage.forRequest(new RequestMessage(r)); sendHelper.sendSyncMessage(message); } private byte[] getSenderCertificate() { byte[] certificate; try { 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; } // TODO cache for a day return certificate; } private Set getRecipientIds(Collection recipients) { final var signalServiceAddresses = new HashSet(recipients.size()); final var addressesMissingUuid = new HashSet(); for (var number : recipients) { final var resolvedAddress = resolveSignalServiceAddress(resolveRecipient(number)); if (resolvedAddress.getUuid().isPresent()) { signalServiceAddresses.add(resolvedAddress); } else { addressesMissingUuid.add(resolvedAddress); } } final var numbersMissingUuid = addressesMissingUuid.stream() .map(a -> a.getNumber().get()) .collect(Collectors.toSet()); Map registeredUsers; try { registeredUsers = getRegisteredUsers(numbersMissingUuid); } catch (IOException e) { logger.warn("Failed to resolve uuids from server, ignoring: {}", e.getMessage()); registeredUsers = Map.of(); } for (var address : addressesMissingUuid) { final var number = address.getNumber().get(); if (registeredUsers.containsKey(number)) { final var newAddress = resolveSignalServiceAddress(resolveRecipientTrusted(new SignalServiceAddress( registeredUsers.get(number), number))); signalServiceAddresses.add(newAddress); } else { signalServiceAddresses.add(address); } } return signalServiceAddresses.stream().map(this::resolveRecipient).collect(Collectors.toSet()); } private RecipientId refreshRegisteredUser(RecipientId recipientId) throws IOException { final var address = resolveSignalServiceAddress(recipientId); if (!address.getNumber().isPresent()) { return recipientId; } final var number = address.getNumber().get(); final var uuidMap = getRegisteredUsers(Set.of(number)); return resolveRecipientTrusted(new SignalServiceAddress(uuidMap.getOrDefault(number, null), number)); } private Map getRegisteredUsers(final Set numbers) throws IOException { final Map registeredUsers; try { registeredUsers = dependencies.getAccountManager() .getRegisteredUsers(ServiceConfig.getIasKeyStore(), numbers, serviceEnvironmentConfig.getCdsMrenclave()); } catch (Quote.InvalidQuoteFormatException | UnauthenticatedQuoteException | SignatureException | UnauthenticatedResponseException | InvalidKeyException e) { throw new IOException(e); } // Store numbers as recipients so we have the number/uuid association registeredUsers.forEach((number, uuid) -> resolveRecipientTrusted(new SignalServiceAddress(uuid, number))); return registeredUsers; } public void sendTypingMessage( TypingAction action, Set recipients ) throws IOException, UntrustedIdentityException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { sendTypingMessage(action.toSignalService(), recipients); } private void handleEndSession(RecipientId recipientId) { account.getSessionStore().deleteAllSessions(recipientId); } private List handleSignalServiceDataMessage( SignalServiceDataMessage message, boolean isSync, SignalServiceAddress source, SignalServiceAddress destination, boolean ignoreAttachments ) { var actions = new ArrayList(); if (message.getGroupContext().isPresent()) { if (message.getGroupContext().get().getGroupV1().isPresent()) { var groupInfo = message.getGroupContext().get().getGroupV1().get(); var groupId = GroupId.v1(groupInfo.getGroupId()); var group = getGroup(groupId); if (group == null || group instanceof GroupInfoV1) { var groupV1 = (GroupInfoV1) group; switch (groupInfo.getType()) { case UPDATE: { if (groupV1 == null) { groupV1 = new GroupInfoV1(groupId); } if (groupInfo.getAvatar().isPresent()) { var avatar = groupInfo.getAvatar().get(); downloadGroupAvatar(groupV1.getGroupId(), avatar); } if (groupInfo.getName().isPresent()) { groupV1.name = groupInfo.getName().get(); } if (groupInfo.getMembers().isPresent()) { groupV1.addMembers(groupInfo.getMembers() .get() .stream() .map(this::resolveRecipient) .collect(Collectors.toSet())); } account.getGroupStore().updateGroup(groupV1); break; } case DELIVER: if (groupV1 == null && !isSync) { actions.add(new SendGroupInfoRequestAction(source, groupId)); } break; case QUIT: { if (groupV1 != null) { groupV1.removeMember(resolveRecipient(source)); account.getGroupStore().updateGroup(groupV1); } break; } case REQUEST_INFO: if (groupV1 != null && !isSync) { actions.add(new SendGroupInfoAction(source, groupV1.getGroupId())); } break; } } else { // Received a group v1 message for a v2 group } } if (message.getGroupContext().get().getGroupV2().isPresent()) { final var groupContext = message.getGroupContext().get().getGroupV2().get(); final var groupMasterKey = groupContext.getMasterKey(); groupHelper.getOrMigrateGroup(groupMasterKey, groupContext.getRevision(), groupContext.hasSignedGroupChange() ? groupContext.getSignedGroupChange() : null); } } final var conversationPartnerAddress = isSync ? destination : source; if (conversationPartnerAddress != null && message.isEndSession()) { handleEndSession(resolveRecipient(conversationPartnerAddress)); } if (message.isExpirationUpdate() || message.getBody().isPresent()) { if (message.getGroupContext().isPresent()) { if (message.getGroupContext().get().getGroupV1().isPresent()) { 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(); account.getGroupStore().updateGroup(group); } } } else if (message.getGroupContext().get().getGroupV2().isPresent()) { // disappearing message timer already stored in the DecryptedGroup } } else if (conversationPartnerAddress != null) { setExpirationTimer(resolveRecipient(conversationPartnerAddress), message.getExpiresInSeconds()); } } 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) { final ProfileKey profileKey; try { profileKey = new ProfileKey(message.getProfileKey().get()); } catch (InvalidInputException e) { throw new AssertionError(e); } if (source.matches(account.getSelfAddress())) { this.account.setProfileKey(profileKey); } this.account.getProfileStore().storeProfileKey(resolveRecipient(source), profileKey); } if (message.getPreviews().isPresent()) { final var previews = message.getPreviews().get(); for (var preview : previews) { if (preview.getImage().isPresent()) { downloadAttachment(preview.getImage().get()); } } } if (message.getQuote().isPresent()) { final var quote = message.getQuote().get(); for (var quotedAttachment : quote.getAttachments()) { final var thumbnail = quotedAttachment.getThumbnail(); if (thumbnail != null) { downloadAttachment(thumbnail); } } } if (message.getSticker().isPresent()) { 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(stickerPackId, messageSticker.getPackKey()); account.getStickerStore().updateSticker(sticker); } enqueueJob(new RetrieveStickerPackJob(stickerPackId, messageSticker.getPackKey())); } return actions; } private void retryFailedReceivedMessages(ReceiveMessageHandler handler, boolean ignoreAttachments) { Set queuedActions = new HashSet<>(); for (var cachedMessage : account.getMessageCache().getCachedMessages()) { var actions = retryFailedReceivedMessage(handler, ignoreAttachments, cachedMessage); if (actions != null) { queuedActions.addAll(actions); } } handleQueuedActions(queuedActions); } private List retryFailedReceivedMessage( final ReceiveMessageHandler handler, final boolean ignoreAttachments, final CachedMessage cachedMessage ) { var envelope = cachedMessage.loadEnvelope(); if (envelope == null) { return null; } SignalServiceContent content = null; List actions = null; if (!envelope.isReceipt()) { try { content = dependencies.getCipher().decrypt(envelope); } 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 null; } actions = handleMessage(envelope, content, ignoreAttachments); } handler.handleMessage(envelope, content, null); cachedMessage.delete(); return actions; } public void receiveMessages( long timeout, TimeUnit unit, boolean returnOnTimeout, boolean ignoreAttachments, ReceiveMessageHandler handler ) throws IOException { retryFailedReceivedMessages(handler, ignoreAttachments); Set queuedActions = new HashSet<>(); final var signalWebSocket = dependencies.getSignalWebSocket(); signalWebSocket.connect(); var hasCaughtUpWithOldMessages = false; 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 { 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, recipientId); }); logger.debug("New message received from server"); if (result.isPresent()) { envelope = result.get(); } else { // Received indicator that server queue is empty hasCaughtUpWithOldMessages = true; handleQueuedActions(queuedActions); queuedActions.clear(); // Continue to wait another timeout for new messages continue; } } catch (AssertionError e) { if (e.getCause() instanceof InterruptedException) { Thread.currentThread().interrupt(); break; } else { throw e; } } catch (WebSocketUnavailableException e) { logger.debug("Pipe unexpectedly unavailable, connecting"); signalWebSocket.connect(); continue; } catch (TimeoutException e) { if (returnOnTimeout) return; continue; } if (envelope.hasSource()) { // Store uuid if we don't have it already // address/uuid in envelope is sent by server resolveRecipientTrusted(envelope.getSourceAddress()); } if (!envelope.isReceipt()) { try { content = dependencies.getCipher().decrypt(envelope); } catch (Exception e) { exception = e; } if (!envelope.hasSource() && content != null) { // Store uuid if we don't have it already // address/uuid is validated by unidentified sender certificate resolveRecipientTrusted(content.getSender()); } var actions = handleMessage(envelope, content, ignoreAttachments); if (exception instanceof ProtocolInvalidMessageException) { final var sender = resolveRecipient(((ProtocolInvalidMessageException) exception).getSender()); logger.debug("Received invalid message, queuing renew session action."); actions.add(new RenewSessionAction(sender)); } if (hasCaughtUpWithOldMessages) { for (var action : actions) { try { action.execute(this); } catch (Throwable e) { if (e instanceof AssertionError && e.getCause() instanceof InterruptedException) { Thread.currentThread().interrupt(); } logger.warn("Message action failed.", e); } } } else { queuedActions.addAll(actions); } } final var notAllowedToSendToGroup = isNotAllowedToSendToGroup(envelope, content); if (isMessageBlocked(envelope, content)) { logger.info("Ignoring a message from blocked user/group: {}", 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 (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(); } } } handleQueuedActions(queuedActions); } private void handleQueuedActions(final Set 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); } } } private boolean isMessageBlocked( SignalServiceEnvelope envelope, SignalServiceContent content ) { SignalServiceAddress source; if (!envelope.isUnidentifiedSender() && envelope.hasSource()) { source = envelope.getSourceAddress(); } else if (content != null) { source = content.getSender(); } else { return false; } final var recipientId = resolveRecipient(source); if (isContactBlocked(recipientId)) { return true; } if (content != null && content.getDataMessage().isPresent()) { var message = content.getDataMessage().get(); if (message.getGroupContext().isPresent()) { var groupId = GroupUtils.getGroupId(message.getGroupContext().get()); var group = getGroup(groupId); if (group != null && group.isBlocked()) { return true; } } } return false; } public boolean isContactBlocked(final RecipientIdentifier.Single recipient) { final var recipientId = resolveRecipient(recipient); 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; if (!envelope.isUnidentifiedSender() && envelope.hasSource()) { source = envelope.getSourceAddress(); } else if (content != null) { source = content.getSender(); } else { return false; } 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; } } var groupId = GroupUtils.getGroupId(message.getGroupContext().get()); var group = getGroup(groupId); if (group == null) { return false; } final var recipientId = resolveRecipient(source); if (!group.isMember(recipientId)) { return true; } if (group.isAnnouncementGroup() && !group.isAdmin(recipientId)) { return message.getBody().isPresent() || message.getAttachments().isPresent() || message.getQuote() .isPresent() || message.getPreviews().isPresent() || message.getMentions().isPresent() || message.getSticker().isPresent(); } return false; } private List handleMessage( SignalServiceEnvelope envelope, SignalServiceContent content, boolean ignoreAttachments ) { var actions = new ArrayList(); if (content != null) { final SignalServiceAddress sender; if (!envelope.isUnidentifiedSender() && envelope.hasSource()) { sender = envelope.getSourceAddress(); } else { sender = content.getSender(); } if (content.getDataMessage().isPresent()) { var message = content.getDataMessage().get(); if (content.isNeedsReceipt()) { actions.add(new SendReceiptAction(sender, message.getTimestamp())); } actions.addAll(handleSignalServiceDataMessage(message, false, sender, account.getSelfAddress(), ignoreAttachments)); } if (content.getSyncMessage().isPresent()) { account.setMultiDevice(true); var syncMessage = content.getSyncMessage().get(); if (syncMessage.getSent().isPresent()) { var message = syncMessage.getSent().get(); final var destination = message.getDestination().orNull(); actions.addAll(handleSignalServiceDataMessage(message.getMessage(), true, sender, destination, ignoreAttachments)); } if (syncMessage.getRequest().isPresent() && account.isMasterDevice()) { var rm = syncMessage.getRequest().get(); if (rm.isContactsRequest()) { actions.add(SendSyncContactsAction.create()); } if (rm.isGroupsRequest()) { actions.add(SendSyncGroupsAction.create()); } if (rm.isBlockedListRequest()) { actions.add(SendSyncBlockedListAction.create()); } // TODO Handle rm.isConfigurationRequest(); rm.isKeysRequest(); } if (syncMessage.getGroups().isPresent()) { File tmpFile = null; try { tmpFile = IOUtils.createTempFile(); final var groupsMessage = syncMessage.getGroups().get(); try (var attachmentAsStream = retrieveAttachmentAsStream(groupsMessage.asPointer(), tmpFile)) { var s = new DeviceGroupsInputStream(attachmentAsStream); DeviceGroup g; 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::resolveRecipient) .collect(Collectors.toSet())); if (!g.isActive()) { syncGroup.removeMember(account.getSelfRecipientId()); } else { // Add ourself to the member set as it's marked as active syncGroup.addMembers(List.of(account.getSelfRecipientId())); } syncGroup.blocked = g.isBlocked(); if (g.getColor().isPresent()) { syncGroup.color = g.getColor().get(); } if (g.getAvatar().isPresent()) { downloadGroupAvatar(syncGroup.getGroupId(), g.getAvatar().get()); } syncGroup.archived = g.isArchived(); account.getGroupStore().updateGroup(syncGroup); } } } } catch (Exception e) { logger.warn("Failed to handle received sync groups “{}”, ignoring: {}", tmpFile, e.getMessage()); } finally { if (tmpFile != null) { try { Files.delete(tmpFile.toPath()); } catch (IOException e) { logger.warn("Failed to delete received groups temp file “{}”, ignoring: {}", tmpFile, e.getMessage()); } } } } if (syncMessage.getBlockedList().isPresent()) { final var blockedListMessage = syncMessage.getBlockedList().get(); for (var address : blockedListMessage.getAddresses()) { setContactBlocked(resolveRecipient(address), true); } for (var groupId : blockedListMessage.getGroupIds() .stream() .map(GroupId::unknownVersion) .collect(Collectors.toSet())) { try { setGroupBlocked(groupId, true); } catch (GroupNotFoundException e) { logger.warn("BlockedListMessage contained groupID that was not found in GroupStore: {}", groupId.toBase64()); } } } if (syncMessage.getContacts().isPresent()) { File tmpFile = null; try { tmpFile = IOUtils.createTempFile(); final var contactsMessage = syncMessage.getContacts().get(); try (var attachmentAsStream = retrieveAttachmentAsStream(contactsMessage.getContactsStream() .asPointer(), tmpFile)) { var s = new DeviceContactsInputStream(attachmentAsStream); DeviceContact c; 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 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()) { builder.withName(c.getName().get()); } if (c.getColor().isPresent()) { builder.withColor(c.getColor().get()); } if (c.getProfileKey().isPresent()) { account.getProfileStore().storeProfileKey(recipientId, c.getProfileKey().get()); } if (c.getVerified().isPresent()) { final var verifiedMessage = c.getVerified().get(); account.getIdentityKeyStore() .setIdentityTrustLevel(resolveRecipientTrusted(verifiedMessage.getDestination()), verifiedMessage.getIdentityKey(), TrustLevel.fromVerifiedState(verifiedMessage.getVerified())); } if (c.getExpirationTimer().isPresent()) { builder.withMessageExpirationTime(c.getExpirationTimer().get()); } builder.withBlocked(c.isBlocked()); builder.withArchived(c.isArchived()); account.getContactStore().storeContact(recipientId, builder.build()); if (c.getAvatar().isPresent()) { downloadContactAvatar(c.getAvatar().get(), c.getAddress()); } } } } catch (Exception e) { logger.warn("Failed to handle received sync contacts “{}”, ignoring: {}", tmpFile, e.getMessage()); } finally { if (tmpFile != null) { try { Files.delete(tmpFile.toPath()); } catch (IOException e) { logger.warn("Failed to delete received contacts temp file “{}”, ignoring: {}", tmpFile, e.getMessage()); } } } } if (syncMessage.getVerified().isPresent()) { final var verifiedMessage = syncMessage.getVerified().get(); account.getIdentityKeyStore() .setIdentityTrustLevel(resolveRecipientTrusted(verifiedMessage.getDestination()), verifiedMessage.getIdentityKey(), TrustLevel.fromVerifiedState(verifiedMessage.getVerified())); } if (syncMessage.getStickerPackOperations().isPresent()) { final var stickerPackOperationMessages = syncMessage.getStickerPackOperations().get(); for (var m : stickerPackOperationMessages) { if (!m.getPackId().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())); } } if (sticker != null) { sticker.setInstalled(installed); account.getStickerStore().updateSticker(sticker); } } } if (syncMessage.getFetchType().isPresent()) { switch (syncMessage.getFetchType().get()) { case LOCAL_PROFILE: getRecipientProfile(account.getSelfRecipientId(), true); case STORAGE_MANIFEST: // TODO } } if (syncMessage.getKeys().isPresent()) { final var keysMessage = syncMessage.getKeys().get(); if (keysMessage.getStorageService().isPresent()) { final var storageKey = keysMessage.getStorageService().get(); account.setStorageKey(storageKey); } } if (syncMessage.getConfiguration().isPresent()) { // TODO } } } return actions; } private void downloadContactAvatar(SignalServiceAttachment avatar, SignalServiceAddress address) { try { avatarStore.storeContactAvatar(address, outputStream -> retrieveAttachment(avatar, outputStream)); } catch (IOException e) { logger.warn("Failed to download avatar for contact {}, ignoring: {}", address, e.getMessage()); } } private void downloadGroupAvatar(GroupIdV1 groupId, SignalServiceAttachment avatar) { try { avatarStore.storeGroupAvatar(groupId, outputStream -> retrieveAttachment(avatar, outputStream)); } catch (IOException e) { logger.warn("Failed to download avatar for group {}, ignoring: {}", groupId.toBase64(), e.getMessage()); } } private void downloadProfileAvatar( SignalServiceAddress address, String avatarPath, ProfileKey profileKey ) { try { 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()); } } public File getAttachmentFile(SignalServiceAttachmentRemoteId attachmentId) { return attachmentStore.getAttachmentFile(attachmentId); } private void downloadAttachment(final SignalServiceAttachment attachment) { if (!attachment.isPointer()) { logger.warn("Invalid state, can't store an attachment stream."); } var pointer = attachment.asPointer(); if (pointer.getPreview().isPresent()) { final var preview = pointer.getPreview().get(); try { attachmentStore.storeAttachmentPreview(pointer.getRemoteId(), outputStream -> outputStream.write(preview, 0, preview.length)); } catch (IOException e) { logger.warn("Failed to download attachment preview, ignoring: {}", e.getMessage()); } } try { attachmentStore.storeAttachment(pointer.getRemoteId(), outputStream -> retrieveAttachmentPointer(pointer, outputStream)); } catch (IOException e) { logger.warn("Failed to download attachment ({}), ignoring: {}", pointer.getRemoteId(), e.getMessage()); } } private void retrieveProfileAvatar( String avatarPath, ProfileKey profileKey, OutputStream outputStream ) throws IOException { 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 { try { Files.delete(tmpFile.toPath()); } catch (IOException e) { logger.warn("Failed to delete received profile avatar temp file “{}”, ignoring: {}", tmpFile, e.getMessage()); } } } private void retrieveAttachment( final SignalServiceAttachment attachment, final OutputStream outputStream ) throws IOException { if (attachment.isPointer()) { var pointer = attachment.asPointer(); retrieveAttachmentPointer(pointer, outputStream); } else { var stream = attachment.asStream(); IOUtils.copyStream(stream.getInputStream(), outputStream); } } private void retrieveAttachmentPointer( SignalServiceAttachmentPointer pointer, OutputStream outputStream ) throws IOException { var tmpFile = IOUtils.createTempFile(); try (var input = retrieveAttachmentAsStream(pointer, tmpFile)) { IOUtils.copyStream(input, outputStream); } catch (MissingConfigurationException | InvalidMessageException e) { throw new IOException(e); } finally { try { Files.delete(tmpFile.toPath()); } catch (IOException e) { logger.warn("Failed to delete received attachment temp file “{}”, ignoring: {}", tmpFile, e.getMessage()); } } } private InputStream retrieveAttachmentAsStream( SignalServiceAttachmentPointer pointer, File tmpFile ) throws IOException, InvalidMessageException, MissingConfigurationException { return dependencies.getMessageReceiver() .retrieveAttachment(pointer, tmpFile, ServiceConfig.MAX_ATTACHMENT_SIZE); } void sendGroups() throws IOException { var groupsFile = IOUtils.createTempFile(); try { try (OutputStream fos = new FileOutputStream(groupsFile)) { var out = new DeviceGroupsOutputStream(fos); for (var record : getGroups()) { if (record instanceof GroupInfoV1) { var groupInfo = (GroupInfoV1) record; out.write(new DeviceGroup(groupInfo.getGroupId().serialize(), Optional.fromNullable(groupInfo.name), groupInfo.getMembers() .stream() .map(this::resolveSignalServiceAddress) .collect(Collectors.toList()), groupHelper.createGroupAvatarAttachment(groupInfo.getGroupId()), groupInfo.isMember(account.getSelfRecipientId()), Optional.of(groupInfo.messageExpirationTime), Optional.fromNullable(groupInfo.color), groupInfo.blocked, Optional.absent(), groupInfo.archived)); } } } if (groupsFile.exists() && groupsFile.length() > 0) { try (var groupsFileStream = new FileInputStream(groupsFile)) { var attachmentStream = SignalServiceAttachment.newStreamBuilder() .withStream(groupsFileStream) .withContentType("application/octet-stream") .withLength(groupsFile.length()) .build(); sendHelper.sendSyncMessage(SignalServiceSyncMessage.forGroups(attachmentStream)); } } } finally { try { Files.delete(groupsFile.toPath()); } catch (IOException e) { logger.warn("Failed to delete groups temp file “{}”, ignoring: {}", groupsFile, e.getMessage()); } } } public void sendContacts() throws IOException { var contactsFile = IOUtils.createTempFile(); try { try (OutputStream fos = new FileOutputStream(contactsFile)) { 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; if (currentIdentity != null) { verifiedMessage = new VerifiedMessage(address, currentIdentity.getIdentityKey(), currentIdentity.getTrustLevel().toVerifiedState(), currentIdentity.getDateAdded().getTime()); } 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), contact.isBlocked(), Optional.of(contact.getMessageExpirationTime()), Optional.absent(), contact.isArchived())); } if (account.getProfileKey() != null) { // Send our own profile key as well out.write(new DeviceContact(account.getSelfAddress(), Optional.absent(), Optional.absent(), Optional.absent(), Optional.absent(), Optional.of(account.getProfileKey()), false, Optional.absent(), Optional.absent(), false)); } } if (contactsFile.exists() && contactsFile.length() > 0) { try (var contactsFileStream = new FileInputStream(contactsFile)) { var attachmentStream = SignalServiceAttachment.newStreamBuilder() .withStream(contactsFileStream) .withContentType("application/octet-stream") .withLength(contactsFile.length()) .build(); sendHelper.sendSyncMessage(SignalServiceSyncMessage.forContacts(new ContactsMessage(attachmentStream, true))); } } } finally { try { Files.delete(contactsFile.toPath()); } catch (IOException e) { logger.warn("Failed to delete contacts temp file “{}”, ignoring: {}", contactsFile, e.getMessage()); } } } void sendBlockedList() throws IOException { var addresses = new ArrayList(); for (var record : account.getContactStore().getContacts()) { if (record.second().isBlocked()) { addresses.add(resolveSignalServiceAddress(record.first())); } } var groupIds = new ArrayList(); for (var record : getGroups()) { if (record.isBlocked()) { groupIds.add(record.getGroupId().serialize()); } } sendHelper.sendSyncMessage(SignalServiceSyncMessage.forBlocked(new BlockedListMessage(addresses, groupIds))); } private void sendVerifiedMessage( SignalServiceAddress destination, IdentityKey identityKey, TrustLevel trustLevel ) throws IOException { var verifiedMessage = new VerifiedMessage(destination, identityKey, trustLevel.toVerifiedState(), System.currentTimeMillis()); sendHelper.sendSyncMessage(SignalServiceSyncMessage.forVerified(verifiedMessage)); } public List> getContacts() { return account.getContactStore().getContacts(); } public String getContactOrProfileName(RecipientIdentifier.Single recipientIdentifier) { final var recipientId = resolveRecipient(recipientIdentifier); final var recipient = account.getRecipientStore().getRecipient(recipientId); if (recipient == null) { return null; } if (recipient.getContact() != null && !Util.isEmpty(recipient.getContact().getName())) { return recipient.getContact().getName(); } if (recipient.getProfile() != null && recipient.getProfile() != null) { return recipient.getProfile().getDisplayName(); } return null; } public GroupInfo getGroup(GroupId groupId) { return groupHelper.getGroup(groupId); } public List getIdentities() { return account.getIdentityKeyStore().getIdentities(); } public List getIdentities(RecipientIdentifier.Single recipient) { final var identity = account.getIdentityKeyStore().getIdentity(resolveRecipient(recipient)); return identity == null ? List.of() : List.of(identity); } /** * Trust this the identity with this fingerprint * * @param recipient username of the identity * @param fingerprint Fingerprint */ public boolean trustIdentityVerified(RecipientIdentifier.Single recipient, byte[] fingerprint) { var recipientId = resolveRecipient(recipient); return trustIdentity(recipientId, identityKey -> Arrays.equals(identityKey.serialize(), fingerprint), TrustLevel.TRUSTED_VERIFIED); } /** * Trust this the identity with this safety number * * @param recipient username of the identity * @param safetyNumber Safety number */ public boolean trustIdentityVerifiedSafetyNumber(RecipientIdentifier.Single recipient, String safetyNumber) { var recipientId = resolveRecipient(recipient); var address = account.getRecipientStore().resolveServiceAddress(recipientId); return trustIdentity(recipientId, identityKey -> safetyNumber.equals(computeSafetyNumber(address, identityKey)), TrustLevel.TRUSTED_VERIFIED); } /** * Trust this the identity with this scannable safety number * * @param recipient username of the identity * @param safetyNumber Scannable safety number */ public boolean trustIdentityVerifiedSafetyNumber(RecipientIdentifier.Single recipient, byte[] safetyNumber) { var recipientId = resolveRecipient(recipient); var address = account.getRecipientStore().resolveServiceAddress(recipientId); return trustIdentity(recipientId, identityKey -> { final var fingerprint = computeSafetyNumberFingerprint(address, identityKey); try { return fingerprint != null && fingerprint.getScannableFingerprint().compareTo(safetyNumber); } catch (FingerprintVersionMismatchException | FingerprintParsingException e) { return false; } }, TrustLevel.TRUSTED_VERIFIED); } /** * Trust all keys of this identity without verification * * @param recipient username of the identity */ public boolean trustIdentityAllKeys(RecipientIdentifier.Single recipient) { var recipientId = resolveRecipient(recipient); 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; } if (!verifier.apply(identity.getIdentityKey())) { return false; } 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; } private void handleIdentityFailure( final RecipientId recipientId, final SendMessageResult.IdentityFailure identityFailure ) { 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 Fingerprint fingerprint = computeSafetyNumberFingerprint(theirAddress, theirIdentityKey); return fingerprint == null ? null : fingerprint.getDisplayableFingerprint().getDisplayText(); } public byte[] computeSafetyNumberForScanning(SignalServiceAddress theirAddress, IdentityKey theirIdentityKey) { final Fingerprint fingerprint = computeSafetyNumberFingerprint(theirAddress, theirIdentityKey); return fingerprint == null ? null : fingerprint.getScannableFingerprint().getSerialized(); } private Fingerprint computeSafetyNumberFingerprint( final SignalServiceAddress theirAddress, final IdentityKey theirIdentityKey ) { return Utils.computeSafetyNumber(capabilities.isUuid(), account.getSelfAddress(), getIdentityKeyPair().getPublicKey(), theirAddress, theirIdentityKey); } @Deprecated public SignalServiceAddress resolveSignalServiceAddress(String identifier) { var address = Utils.getSignalServiceAddressFromIdentifier(identifier); return resolveSignalServiceAddress(address); } @Deprecated public SignalServiceAddress resolveSignalServiceAddress(SignalServiceAddress address) { if (address.matches(account.getSelfAddress())) { return account.getSelfAddress(); } return account.getRecipientStore().resolveServiceAddress(address); } public SignalServiceAddress resolveSignalServiceAddress(RecipientId recipientId) { return account.getRecipientStore().resolveServiceAddress(recipientId); } 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); } private RecipientId resolveRecipient(final RecipientIdentifier.Single recipient) { final SignalServiceAddress address; if (recipient instanceof RecipientIdentifier.Uuid) { address = new SignalServiceAddress(((RecipientIdentifier.Uuid) recipient).uuid, null); } else { address = new SignalServiceAddress(null, ((RecipientIdentifier.Number) recipient).number); } 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 { executor.shutdown(); dependencies.getSignalWebSocket().disconnect(); if (closeAccount && account != null) { account.close(); } account = null; } public interface ReceiveMessageHandler { void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent decryptedContent, Throwable e); } }