From: AsamK Date: Wed, 15 Sep 2021 19:40:47 +0000 (+0200) Subject: Extract Manager interface X-Git-Tag: v0.9.1~40 X-Git-Url: https://git.nmode.ca/signal-cli/commitdiff_plain/b91c162159c7c28d049ceb8889c419791573d3bb?ds=inline Extract Manager interface --- 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 05700379..d2eb0f8f 100644 --- a/lib/src/main/java/org/asamk/signal/manager/Manager.java +++ b/lib/src/main/java/org/asamk/signal/manager/Manager.java @@ -1,22 +1,5 @@ -/* - 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.actions.HandleAction; import org.asamk.signal.manager.api.Device; import org.asamk.signal.manager.api.Message; import org.asamk.signal.manager.api.RecipientIdentifier; @@ -25,7 +8,6 @@ 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.GroupInviteLinkUrl; import org.asamk.signal.manager.groups.GroupLinkState; @@ -34,233 +16,46 @@ import org.asamk.signal.manager.groups.GroupPermission; import org.asamk.signal.manager.groups.GroupSendingNotAllowedException; import org.asamk.signal.manager.groups.LastGroupAdminException; import org.asamk.signal.manager.groups.NotAGroupMemberException; -import org.asamk.signal.manager.helper.AttachmentHelper; -import org.asamk.signal.manager.helper.ContactHelper; -import org.asamk.signal.manager.helper.GroupHelper; -import org.asamk.signal.manager.helper.GroupV2Helper; -import org.asamk.signal.manager.helper.IncomingMessageHandler; -import org.asamk.signal.manager.helper.PinHelper; -import org.asamk.signal.manager.helper.PreKeyHelper; -import org.asamk.signal.manager.helper.ProfileHelper; -import org.asamk.signal.manager.helper.SendHelper; -import org.asamk.signal.manager.helper.StorageHelper; -import org.asamk.signal.manager.helper.SyncHelper; -import org.asamk.signal.manager.helper.UnidentifiedAccessHelper; -import org.asamk.signal.manager.jobs.Context; import org.asamk.signal.manager.storage.SignalAccount; import org.asamk.signal.manager.storage.groups.GroupInfo; 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.KeyUtils; -import org.asamk.signal.manager.util.StickerUtils; -import org.asamk.signal.manager.util.Utils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.InvalidKeyException; -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.util.Pair; import org.whispersystems.libsignal.util.guava.Optional; -import org.whispersystems.signalservice.api.SignalSessionLock; import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException; -import org.whispersystems.signalservice.api.messages.SendMessageResult; import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId; 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.SignalServiceReceiptMessage; -import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException; -import org.whispersystems.signalservice.api.util.DeviceNameUtil; -import org.whispersystems.signalservice.api.util.InvalidNumberException; import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; -import org.whispersystems.signalservice.api.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.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.IOException; import java.net.URI; -import java.net.URISyntaxException; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; -import java.security.SignatureException; import java.util.Arrays; -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 StorageHelper storageHelper; - private final SendHelper sendHelper; - private final SyncHelper syncHelper; - private final AttachmentHelper attachmentHelper; - private final GroupHelper groupHelper; - private final ContactHelper contactHelper; - private final IncomingMessageHandler incomingMessageHandler; - private final PreKeyHelper preKeyHelper; - - private final Context context; - private boolean hasCaughtUpWithOldMessages = false; - - 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()); - final var sessionLock = new SignalSessionLock() { - private final ReentrantLock LEGACY_LOCK = new ReentrantLock(); - - @Override - public Lock acquire() { - LEGACY_LOCK.lock(); - return LEGACY_LOCK::unlock; - } - }; - this.dependencies = new SignalDependencies(serviceEnvironmentConfig, - userAgent, - credentialsProvider, - account.getSignalProtocolStore(), - executor, - sessionLock); - final var avatarStore = new AvatarStore(pathConfig.getAvatarsPath()); - final var attachmentStore = new AttachmentStore(pathConfig.getAttachmentsPath()); - final var stickerPackStore = new StickerPackStore(pathConfig.getStickerPacksPath()); - - this.attachmentHelper = new AttachmentHelper(dependencies, attachmentStore); - 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, - dependencies, - avatarStore, - account.getProfileStore()::getProfileKey, - unidentifiedAccessHelper::getAccessFor, - this::resolveSignalServiceAddress); - final GroupV2Helper groupV2Helper = new GroupV2Helper(profileHelper::getRecipientProfileKeyCredential, - this::getRecipientProfile, - account::getSelfRecipientId, - dependencies.getGroupsV2Operations(), - dependencies.getGroupsV2Api(), - this::resolveSignalServiceAddress); - this.sendHelper = new SendHelper(account, - dependencies, - unidentifiedAccessHelper, - this::resolveSignalServiceAddress, - account.getRecipientStore(), - this::handleIdentityFailure, - this::getGroup, - this::refreshRegisteredUser); - this.groupHelper = new GroupHelper(account, - dependencies, - attachmentHelper, - sendHelper, - groupV2Helper, - avatarStore, - this::resolveSignalServiceAddress, - account.getRecipientStore()); - this.storageHelper = new StorageHelper(account, dependencies, groupHelper); - this.contactHelper = new ContactHelper(account); - this.syncHelper = new SyncHelper(account, - attachmentHelper, - sendHelper, - groupHelper, - avatarStore, - this::resolveSignalServiceAddress); - preKeyHelper = new PreKeyHelper(account, dependencies); - - this.context = new Context(account, - dependencies, - stickerPackStore, - sendHelper, - groupHelper, - syncHelper, - profileHelper, - storageHelper, - preKeyHelper); - var jobExecutor = new JobExecutor(context); - - this.incomingMessageHandler = new IncomingMessageHandler(account, - dependencies, - account.getRecipientStore(), - this::resolveSignalServiceAddress, - groupHelper, - contactHelper, - attachmentHelper, - syncHelper, - this::getRecipientProfile, - jobExecutor); - } - - public String getUsername() { - return account.getUsername(); - } +public interface Manager extends Closeable { - public RecipientId getSelfRecipientId() { - return account.getSelfRecipientId(); - } - - public int getDeviceId() { - return account.getDeviceId(); - } - - public static Manager init( + static Manager init( String username, File settingsPath, ServiceEnvironment serviceEnvironment, String userAgent, - final TrustNewIdentity trustNewIdentity + TrustNewIdentity trustNewIdentity ) throws IOException, NotRegisteredException { var pathConfig = PathConfig.createDefault(settingsPath); @@ -276,10 +71,10 @@ public class Manager implements Closeable { final var serviceEnvironmentConfig = ServiceConfig.getServiceEnvironmentConfig(serviceEnvironment, userAgent); - return new Manager(account, pathConfig, serviceEnvironmentConfig, userAgent); + return new ManagerImpl(account, pathConfig, serviceEnvironmentConfig, userAgent); } - public static List getAllLocalUsernames(File settingsPath) { + static List getAllLocalUsernames(File settingsPath) { var pathConfig = PathConfig.createDefault(settingsPath); final var dataPath = pathConfig.getDataPath(); final var files = dataPath.listFiles(); @@ -295,208 +90,51 @@ public class Manager implements Closeable { .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); - } - } - preKeyHelper.refreshPreKeysIfNecessary(); - if (account.getUuid() == null) { - account.setUuid(dependencies.getAccountManager().getOwnUuid()); - } - updateAccountAttributes(null); - } - - /** - * 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 PhoneNumberFormatter.formatNumber(n, account.getUsername()); - } catch (InvalidNumberException e) { - return ""; - } - })); - - // Note "registeredUsers" has no optionals. It only gives us info on users who are registered - var registeredUsers = 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 = registeredUsers.get(number); - return new Pair<>(number.isEmpty() ? null : number, uuid); - })); - } - - public void updateAccountAttributes(String deviceName) throws IOException { - final String encryptedDeviceName; - if (deviceName == null) { - encryptedDeviceName = account.getEncryptedDeviceName(); - } else { - final var privateKey = account.getIdentityKeyPair().getPrivateKey(); - encryptedDeviceName = DeviceNameUtil.encryptDeviceName(deviceName, privateKey); - account.setEncryptedDeviceName(encryptedDeviceName); - } - dependencies.getAccountManager() - .setAccountAttributes(encryptedDeviceName, - null, - account.getLocalRegistrationId(), - true, - 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 { - profileHelper.setProfile(givenName, familyName, about, aboutEmoji, avatar); - syncHelper.sendSyncFetchProfileMessage(); - } - - 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 { - try { - pinHelper.removeRegistrationLockPin(); - } catch (UnauthenticatedResponseException e) { - logger.warn("Failed to remove registration lock pin"); - } - account.setRegistrationLockPin(null, null); + String getUsername(); - dependencies.getAccountManager().deleteAccount(); + RecipientId getSelfRecipientId(); - account.setRegistered(false); - } + int getDeviceId(); - public void submitRateLimitRecaptchaChallenge(String challenge, String captcha) throws IOException { - dependencies.getAccountManager().submitRateLimitRecaptchaChallenge(challenge, captcha); - } + void checkAccountState() throws IOException; - 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()); - } + Map> areUsersRegistered(Set numbers) throws IOException; - public void removeLinkedDevices(int deviceId) throws IOException { - dependencies.getAccountManager().removeDevice(deviceId); - var devices = dependencies.getAccountManager().getDevices(); - account.setMultiDevice(devices.size() > 1); - } + void updateAccountAttributes(String deviceName) throws IOException; - public void addDeviceLink(URI linkUri) throws IOException, InvalidKeyException { - var info = DeviceLinkInfo.parseDeviceLinkUri(linkUri); + void setProfile( + String givenName, String familyName, String about, String aboutEmoji, Optional avatar + ) throws IOException; - addDevice(info.deviceIdentifier, info.deviceKey); - } + void unregister() throws IOException; - private void addDevice(String deviceIdentifier, ECPublicKey deviceKey) throws IOException, InvalidKeyException { - var identityKeyPair = account.getIdentityKeyPair(); - var verificationCode = dependencies.getAccountManager().getNewDeviceVerificationCode(); - - dependencies.getAccountManager() - .addDevice(deviceIdentifier, - deviceKey, - identityKeyPair, - Optional.of(account.getProfileKey().serialize()), - verificationCode); - account.setMultiDevice(true); - } + void deleteAccount() throws IOException; - 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(); + void submitRateLimitRecaptchaChallenge(String challenge, String captcha) throws IOException; - pinHelper.setRegistrationLockPin(pin.get(), masterKey); + List getLinkedDevices() throws IOException; - account.setRegistrationLockPin(pin.get(), masterKey); - } else { - // Remove KBS Pin - pinHelper.removeRegistrationLockPin(); + void removeLinkedDevices(int deviceId) throws IOException; - account.setRegistrationLockPin(null, null); - } - } + void addDeviceLink(URI linkUri) throws IOException, InvalidKeyException; - void refreshPreKeys() throws IOException { - preKeyHelper.refreshPreKeys(); - } + void setRegistrationLockPin(Optional pin) throws IOException, UnauthenticatedResponseException; - public Profile getRecipientProfile(RecipientId recipientId) { - return profileHelper.getRecipientProfile(recipientId); - } + Profile getRecipientProfile(RecipientId recipientId); - public List getGroups() { - return account.getGroupStore().getGroups(); - } + List getGroups(); - public SendGroupMessageResults quitGroup( + SendGroupMessageResults quitGroup( GroupId groupId, Set groupAdmins - ) throws GroupNotFoundException, IOException, NotAGroupMemberException, LastGroupAdminException { - final var newAdmins = resolveRecipients(groupAdmins); - return groupHelper.quitGroup(groupId, newAdmins); - } + ) throws GroupNotFoundException, IOException, NotAGroupMemberException, LastGroupAdminException; - public void deleteGroup(GroupId groupId) throws IOException { - groupHelper.deleteGroup(groupId); - } + void deleteGroup(GroupId groupId) throws IOException; - public Pair createGroup( + Pair createGroup( String name, Set members, File avatarFile - ) throws IOException, AttachmentInvalidException { - return groupHelper.createGroup(name, members == null ? null : resolveRecipients(members), avatarFile); - } + ) throws IOException, AttachmentInvalidException; - public SendGroupMessageResults updateGroup( + SendGroupMessageResults updateGroup( GroupId groupId, String name, String description, @@ -511,724 +149,110 @@ public class Manager implements Closeable { File avatarFile, Integer expirationTimer, Boolean isAnnouncementGroup - ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException, GroupSendingNotAllowedException { - return groupHelper.updateGroup(groupId, - name, - description, - members == null ? null : resolveRecipients(members), - removeMembers == null ? null : resolveRecipients(removeMembers), - admins == null ? null : resolveRecipients(admins), - removeAdmins == null ? null : resolveRecipients(removeAdmins), - resetGroupLink, - groupLinkState, - addMemberPermission, - editDetailsPermission, - avatarFile, - expirationTimer, - isAnnouncementGroup); - } + ) throws IOException, GroupNotFoundException, AttachmentInvalidException, NotAGroupMemberException, GroupSendingNotAllowedException; - public Pair joinGroup( + 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); - } + ) throws IOException, GroupLinkNotActiveException; - 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); - } - } - } + void sendTypingMessage( + TypingAction action, Set recipients + ) throws IOException, UntrustedIdentityException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException; - public void sendReadReceipt( + void sendReadReceipt( RecipientIdentifier.Single sender, List messageIds - ) throws IOException, UntrustedIdentityException { - var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.READ, - messageIds, - System.currentTimeMillis()); + ) throws IOException, UntrustedIdentityException; - sendHelper.sendReceiptMessage(receiptMessage, resolveRecipient(sender)); - } - - public void sendViewedReceipt( + void sendViewedReceipt( RecipientIdentifier.Single sender, List messageIds - ) throws IOException, UntrustedIdentityException { - var receiptMessage = new SignalServiceReceiptMessage(SignalServiceReceiptMessage.Type.VIEWED, - messageIds, - System.currentTimeMillis()); + ) throws IOException, UntrustedIdentityException; - sendHelper.sendReceiptMessage(receiptMessage, resolveRecipient(sender)); - } - - public SendMessageResults sendMessage( + 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()); - final var attachments = message.getAttachments(); - if (attachments != null) { - messageBuilder.withAttachments(attachmentHelper.uploadAttachments(attachments)); - } - } + ) throws IOException, AttachmentInvalidException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException; - public SendMessageResults sendRemoteDeleteMessage( + 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); - } + ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException; - public SendMessageResults sendMessageReaction( + 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); - } + ) throws IOException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException; - 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(recipient); - account.getSessionStore().deleteAllSessions(recipientId); - } - } - } + SendMessageResults sendEndSessionMessage(Set recipients) throws IOException; - public void setContactName( + void setContactName( RecipientIdentifier.Single recipient, String name - ) throws NotMasterDeviceException, UnregisteredUserException { - if (!account.isMasterDevice()) { - throw new NotMasterDeviceException(); - } - contactHelper.setContactName(resolveRecipient(recipient), name); - } + ) throws NotMasterDeviceException, UnregisteredUserException; - public void setContactBlocked( + void setContactBlocked( RecipientIdentifier.Single recipient, boolean blocked - ) throws NotMasterDeviceException, IOException { - if (!account.isMasterDevice()) { - throw new NotMasterDeviceException(); - } - contactHelper.setContactBlocked(resolveRecipient(recipient), blocked); - // TODO cycle our profile key - syncHelper.sendBlockedList(); - } + ) throws NotMasterDeviceException, IOException; - public void setGroupBlocked( - final GroupId groupId, final boolean blocked - ) throws GroupNotFoundException, IOException { - groupHelper.setGroupBlocked(groupId, blocked); - // TODO cycle our profile key - syncHelper.sendBlockedList(); - } + void setGroupBlocked( + GroupId groupId, boolean blocked + ) throws GroupNotFoundException, IOException; - /** - * Change the expiration timer for a contact - */ - public void setExpirationTimer( + void setExpirationTimer( RecipientIdentifier.Single recipient, int messageExpirationTimer - ) throws IOException { - var recipientId = resolveRecipient(recipient); - contactHelper.setExpirationTimer(recipientId, messageExpirationTimer); - final var messageBuilder = SignalServiceDataMessage.newBuilder().asExpirationUpdate(); - try { - sendMessage(messageBuilder, Set.of(recipient)); - } catch (NotAGroupMemberException | GroupNotFoundException | GroupSendingNotAllowedException e) { - throw new AssertionError(e); - } - } - - /** - * 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 { - syncHelper.requestAllSyncData(); - retrieveRemoteStorage(); - } - - void retrieveRemoteStorage() throws IOException { - if (account.getStorageKey() != null) { - storageHelper.readDataFromStorage(); - } - } - - 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 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 uuid = getRegisteredUser(number); - return resolveRecipientTrusted(new SignalServiceAddress(uuid, number)); - } + ) throws IOException; - private UUID getRegisteredUser(final String number) throws IOException { - final Map uuidMap; - try { - uuidMap = getRegisteredUsers(Set.of(number)); - } catch (NumberFormatException e) { - throw new UnregisteredUserException(number, e); - } - final var uuid = uuidMap.get(number); - if (uuid == null) { - throw new UnregisteredUserException(number, null); - } - return uuid; - } - - 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); - } + URI uploadStickerPack(File path) throws IOException, StickerPackInvalidException; - // Store numbers as recipients so we have the number/uuid association - registeredUsers.forEach((number, uuid) -> resolveRecipientTrusted(new SignalServiceAddress(uuid, number))); + void requestAllSyncData() throws IOException; - return registeredUsers; - } - - public void sendTypingMessage( - TypingAction action, Set recipients - ) throws IOException, UntrustedIdentityException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { - sendTypingMessage(action.toSignalService(), recipients); - } - - 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) { - cachedMessage.delete(); - return null; - } - - final var result = incomingMessageHandler.handleRetryEnvelope(envelope, ignoreAttachments, handler); - final var actions = result.first(); - final var exception = result.second(); - - if (exception instanceof UntrustedIdentityException) { - if (System.currentTimeMillis() - envelope.getServerDeliveredTimestamp() > 1000L * 60 * 60 * 24 * 30) { - // Envelope is more than a month old, cleaning up. - cachedMessage.delete(); - return null; - } - if (!envelope.hasSourceUuid()) { - final var identifier = ((UntrustedIdentityException) exception).getSender(); - final var recipientId = account.getRecipientStore().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; - } - - // If successful and for all other errors that are not recoverable, delete the cached message - cachedMessage.delete(); - return actions; - } - - public void receiveMessages( + 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(); - - hasCaughtUpWithOldMessages = false; - - while (!Thread.interrupted()) { - SignalServiceEnvelope envelope; - 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.hasSourceUuid() - ? resolveRecipient(envelope1.getSourceAddress()) - : null; - // store message on disk, before acknowledging receipt to the server - cachedMessage[0] = account.getMessageCache().cacheMessage(envelope1, recipientId); - }); - if (result.isPresent()) { - envelope = result.get(); - logger.debug("New message received from server"); - } else { - logger.debug("Received indicator that server queue is empty"); - handleQueuedActions(queuedActions); - queuedActions.clear(); - - hasCaughtUpWithOldMessages = true; - synchronized (this) { - this.notifyAll(); - } - - // 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; - } - - final var result = incomingMessageHandler.handleEnvelope(envelope, ignoreAttachments, handler); - queuedActions.addAll(result.first()); - final var exception = result.second(); - - if (hasCaughtUpWithOldMessages) { - handleQueuedActions(queuedActions); - } - if (cachedMessage[0] != null) { - if (exception instanceof UntrustedIdentityException) { - final var address = ((UntrustedIdentityException) exception).getSender(); - final var recipientId = resolveRecipient(address); - if (!envelope.hasSourceUuid()) { - 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); - } - - public boolean hasCaughtUpWithOldMessages() { - return hasCaughtUpWithOldMessages; - } - - private void handleQueuedActions(final Collection queuedActions) { - var interrupted = false; - for (var action : queuedActions) { - try { - action.execute(context); - } catch (Throwable e) { - if ((e instanceof AssertionError || e instanceof RuntimeException) - && e.getCause() instanceof InterruptedException) { - interrupted = true; - continue; - } - logger.warn("Message action failed.", e); - } - } - if (interrupted) { - Thread.currentThread().interrupt(); - } - } - - public boolean isContactBlocked(final RecipientIdentifier.Single recipient) { - final RecipientId recipientId; - try { - recipientId = resolveRecipient(recipient); - } catch (UnregisteredUserException e) { - return false; - } - return contactHelper.isContactBlocked(recipientId); - } - - public File getAttachmentFile(SignalServiceAttachmentRemoteId attachmentId) { - return attachmentHelper.getAttachmentFile(attachmentId); - } - - public void sendContacts() throws IOException { - syncHelper.sendContacts(); - } - - public List> getContacts() { - return account.getContactStore().getContacts(); - } - - public String getContactOrProfileName(RecipientIdentifier.Single recipientIdentifier) { - final RecipientId recipientId; - try { - recipientId = resolveRecipient(recipientIdentifier); - } catch (UnregisteredUserException e) { - return null; - } - - final var contact = account.getContactStore().getContact(recipientId); - if (contact != null && !Util.isEmpty(contact.getName())) { - return contact.getName(); - } - - final var profile = getRecipientProfile(recipientId); - if (profile != null) { - return profile.getDisplayName(); - } - - return null; - } - - public GroupInfo getGroup(GroupId groupId) { - return groupHelper.getGroup(groupId); - } + ) throws IOException; - public List getIdentities() { - return account.getIdentityKeyStore().getIdentities(); - } + boolean hasCaughtUpWithOldMessages(); - public List getIdentities(RecipientIdentifier.Single recipient) { - IdentityInfo identity; - try { - identity = account.getIdentityKeyStore().getIdentity(resolveRecipient(recipient)); - } catch (UnregisteredUserException e) { - identity = null; - } - return identity == null ? List.of() : List.of(identity); - } + boolean isContactBlocked(RecipientIdentifier.Single recipient); - /** - * Trust this the identity with this fingerprint - * - * @param recipient username of the identity - * @param fingerprint Fingerprint - */ - public boolean trustIdentityVerified(RecipientIdentifier.Single recipient, byte[] fingerprint) { - RecipientId recipientId; - try { - recipientId = resolveRecipient(recipient); - } catch (UnregisteredUserException e) { - return false; - } - return trustIdentity(recipientId, - identityKey -> Arrays.equals(identityKey.serialize(), fingerprint), - TrustLevel.TRUSTED_VERIFIED); - } + File getAttachmentFile(SignalServiceAttachmentRemoteId attachmentId); - /** - * 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) { - RecipientId recipientId; - try { - recipientId = resolveRecipient(recipient); - } catch (UnregisteredUserException e) { - return false; - } - var address = resolveSignalServiceAddress(recipientId); - return trustIdentity(recipientId, - identityKey -> safetyNumber.equals(computeSafetyNumber(address, identityKey)), - TrustLevel.TRUSTED_VERIFIED); - } + void sendContacts() throws IOException; - /** - * 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) { - RecipientId recipientId; - try { - recipientId = resolveRecipient(recipient); - } catch (UnregisteredUserException e) { - return false; - } - var address = resolveSignalServiceAddress(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); - } + List> getContacts(); - /** - * Trust all keys of this identity without verification - * - * @param recipient username of the identity - */ - public boolean trustIdentityAllKeys(RecipientIdentifier.Single recipient) { - RecipientId recipientId; - try { - recipientId = resolveRecipient(recipient); - } catch (UnregisteredUserException e) { - return false; - } - return trustIdentity(recipientId, identityKey -> true, TrustLevel.TRUSTED_UNVERIFIED); - } + String getContactOrProfileName(RecipientIdentifier.Single recipientIdentifier); - private boolean trustIdentity( - RecipientId recipientId, Function verifier, TrustLevel trustLevel - ) { - var identity = account.getIdentityKeyStore().getIdentity(recipientId); - if (identity == null) { - return false; - } + GroupInfo getGroup(GroupId groupId); - if (!verifier.apply(identity.getIdentityKey())) { - return false; - } + List getIdentities(); - account.getIdentityKeyStore().setIdentityTrustLevel(recipientId, identity.getIdentityKey(), trustLevel); - try { - var address = resolveSignalServiceAddress(recipientId); - syncHelper.sendVerifiedMessage(address, identity.getIdentityKey(), trustLevel); - } catch (IOException e) { - logger.warn("Failed to send verification sync message: {}", e.getMessage()); - } + List getIdentities(RecipientIdentifier.Single recipient); - return true; - } + boolean trustIdentityVerified(RecipientIdentifier.Single recipient, byte[] fingerprint); - 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 - profileHelper.refreshRecipientProfile(recipientId); - } - } + boolean trustIdentityVerifiedSafetyNumber(RecipientIdentifier.Single recipient, String safetyNumber); - public String computeSafetyNumber(SignalServiceAddress theirAddress, IdentityKey theirIdentityKey) { - final Fingerprint fingerprint = computeSafetyNumberFingerprint(theirAddress, theirIdentityKey); - return fingerprint == null ? null : fingerprint.getDisplayableFingerprint().getDisplayText(); - } + boolean trustIdentityVerifiedSafetyNumber(RecipientIdentifier.Single recipient, byte[] safetyNumber); - public byte[] computeSafetyNumberForScanning(SignalServiceAddress theirAddress, IdentityKey theirIdentityKey) { - final Fingerprint fingerprint = computeSafetyNumberFingerprint(theirAddress, theirIdentityKey); - return fingerprint == null ? null : fingerprint.getScannableFingerprint().getSerialized(); - } + boolean trustIdentityAllKeys(RecipientIdentifier.Single recipient); - private Fingerprint computeSafetyNumberFingerprint( - final SignalServiceAddress theirAddress, final IdentityKey theirIdentityKey - ) { - return Utils.computeSafetyNumber(capabilities.isUuid(), - account.getSelfAddress(), - account.getIdentityKeyPair().getPublicKey(), - theirAddress, - theirIdentityKey); - } + String computeSafetyNumber(SignalServiceAddress theirAddress, IdentityKey theirIdentityKey); - public SignalServiceAddress resolveSignalServiceAddress(SignalServiceAddress address) { - return resolveSignalServiceAddress(resolveRecipient(address)); - } + byte[] computeSafetyNumberForScanning(SignalServiceAddress theirAddress, IdentityKey theirIdentityKey); - public SignalServiceAddress resolveSignalServiceAddress(UUID uuid) { - return resolveSignalServiceAddress(account.getRecipientStore().resolveRecipient(uuid)); - } - - public SignalServiceAddress resolveSignalServiceAddress(RecipientId recipientId) { - final var address = account.getRecipientStore().resolveRecipientAddress(recipientId); - if (address.getUuid().isPresent()) { - return address.toSignalServiceAddress(); - } - - // Address in recipient store doesn't have a uuid, this shouldn't happen - // Try to retrieve the uuid from the server - final var number = address.getNumber().get(); - try { - return resolveSignalServiceAddress(getRegisteredUser(number)); - } catch (IOException e) { - logger.warn("Failed to get uuid for e164 number: {}", number, e); - // Return SignalServiceAddress with unknown UUID - return address.toSignalServiceAddress(); - } - } - - private Set resolveRecipients(Collection recipients) throws UnregisteredUserException { - final var recipientIds = new HashSet(recipients.size()); - for (var number : recipients) { - final var recipientId = resolveRecipient(number); - recipientIds.add(recipientId); - } - return recipientIds; - } - - private RecipientId resolveRecipient(final RecipientIdentifier.Single recipient) throws UnregisteredUserException { - if (recipient instanceof RecipientIdentifier.Uuid) { - return account.getRecipientStore().resolveRecipient(((RecipientIdentifier.Uuid) recipient).uuid); - } else { - final var number = ((RecipientIdentifier.Number) recipient).number; - return account.getRecipientStore().resolveRecipient(number, () -> { - try { - return getRegisteredUser(number); - } catch (IOException e) { - return null; - } - }); - } - } + SignalServiceAddress resolveSignalServiceAddress(SignalServiceAddress address); - private RecipientId resolveRecipient(SignalServiceAddress address) { - return account.getRecipientStore().resolveRecipient(address); - } + SignalServiceAddress resolveSignalServiceAddress(UUID uuid); - private RecipientId resolveRecipientTrusted(SignalServiceAddress address) { - return account.getRecipientStore().resolveRecipientTrusted(address); - } + SignalServiceAddress resolveSignalServiceAddress(RecipientId recipientId); @Override - public void close() throws IOException { - close(true); - } - - private void close(boolean closeAccount) throws IOException { - executor.shutdown(); - - dependencies.getSignalWebSocket().disconnect(); - - if (closeAccount && account != null) { - account.close(); - } - account = null; - } + void close() throws IOException; - public interface ReceiveMessageHandler { + interface ReceiveMessageHandler { void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent decryptedContent, Throwable e); } diff --git a/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java new file mode 100644 index 00000000..d0fab350 --- /dev/null +++ b/lib/src/main/java/org/asamk/signal/manager/ManagerImpl.java @@ -0,0 +1,1240 @@ +/* + 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.actions.HandleAction; +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.ServiceEnvironmentConfig; +import org.asamk.signal.manager.groups.GroupId; +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.LastGroupAdminException; +import org.asamk.signal.manager.groups.NotAGroupMemberException; +import org.asamk.signal.manager.helper.AttachmentHelper; +import org.asamk.signal.manager.helper.ContactHelper; +import org.asamk.signal.manager.helper.GroupHelper; +import org.asamk.signal.manager.helper.GroupV2Helper; +import org.asamk.signal.manager.helper.IncomingMessageHandler; +import org.asamk.signal.manager.helper.PinHelper; +import org.asamk.signal.manager.helper.PreKeyHelper; +import org.asamk.signal.manager.helper.ProfileHelper; +import org.asamk.signal.manager.helper.SendHelper; +import org.asamk.signal.manager.helper.StorageHelper; +import org.asamk.signal.manager.helper.SyncHelper; +import org.asamk.signal.manager.helper.UnidentifiedAccessHelper; +import org.asamk.signal.manager.jobs.Context; +import org.asamk.signal.manager.storage.SignalAccount; +import org.asamk.signal.manager.storage.groups.GroupInfo; +import org.asamk.signal.manager.storage.identities.IdentityInfo; +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.KeyUtils; +import org.asamk.signal.manager.util.StickerUtils; +import org.asamk.signal.manager.util.Utils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.whispersystems.libsignal.IdentityKey; +import org.whispersystems.libsignal.InvalidKeyException; +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.util.Pair; +import org.whispersystems.libsignal.util.guava.Optional; +import org.whispersystems.signalservice.api.SignalSessionLock; +import org.whispersystems.signalservice.api.groupsv2.GroupLinkNotActiveException; +import org.whispersystems.signalservice.api.messages.SendMessageResult; +import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId; +import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; +import org.whispersystems.signalservice.api.messages.SignalServiceEnvelope; +import org.whispersystems.signalservice.api.messages.SignalServiceReceiptMessage; +import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage; +import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException; +import org.whispersystems.signalservice.api.util.DeviceNameUtil; +import org.whispersystems.signalservice.api.util.InvalidNumberException; +import org.whispersystems.signalservice.api.util.PhoneNumberFormatter; +import org.whispersystems.signalservice.api.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.util.DynamicCredentialsProvider; +import org.whispersystems.signalservice.internal.util.Hex; +import org.whispersystems.signalservice.internal.util.Util; + +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.security.SignatureException; +import java.util.Arrays; +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 ManagerImpl implements Manager { + + private final static Logger logger = LoggerFactory.getLogger(ManagerImpl.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 StorageHelper storageHelper; + private final SendHelper sendHelper; + private final SyncHelper syncHelper; + private final AttachmentHelper attachmentHelper; + private final GroupHelper groupHelper; + private final ContactHelper contactHelper; + private final IncomingMessageHandler incomingMessageHandler; + private final PreKeyHelper preKeyHelper; + + private final Context context; + private boolean hasCaughtUpWithOldMessages = false; + + ManagerImpl( + 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()); + final var sessionLock = new SignalSessionLock() { + private final ReentrantLock LEGACY_LOCK = new ReentrantLock(); + + @Override + public Lock acquire() { + LEGACY_LOCK.lock(); + return LEGACY_LOCK::unlock; + } + }; + this.dependencies = new SignalDependencies(serviceEnvironmentConfig, + userAgent, + credentialsProvider, + account.getSignalProtocolStore(), + executor, + sessionLock); + final var avatarStore = new AvatarStore(pathConfig.getAvatarsPath()); + final var attachmentStore = new AttachmentStore(pathConfig.getAttachmentsPath()); + final var stickerPackStore = new StickerPackStore(pathConfig.getStickerPacksPath()); + + this.attachmentHelper = new AttachmentHelper(dependencies, attachmentStore); + 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, + dependencies, + avatarStore, + account.getProfileStore()::getProfileKey, + unidentifiedAccessHelper::getAccessFor, + this::resolveSignalServiceAddress); + final GroupV2Helper groupV2Helper = new GroupV2Helper(profileHelper::getRecipientProfileKeyCredential, + this::getRecipientProfile, + account::getSelfRecipientId, + dependencies.getGroupsV2Operations(), + dependencies.getGroupsV2Api(), + this::resolveSignalServiceAddress); + this.sendHelper = new SendHelper(account, + dependencies, + unidentifiedAccessHelper, + this::resolveSignalServiceAddress, + account.getRecipientStore(), + this::handleIdentityFailure, + this::getGroup, + this::refreshRegisteredUser); + this.groupHelper = new GroupHelper(account, + dependencies, + attachmentHelper, + sendHelper, + groupV2Helper, + avatarStore, + this::resolveSignalServiceAddress, + account.getRecipientStore()); + this.storageHelper = new StorageHelper(account, dependencies, groupHelper); + this.contactHelper = new ContactHelper(account); + this.syncHelper = new SyncHelper(account, + attachmentHelper, + sendHelper, + groupHelper, + avatarStore, + this::resolveSignalServiceAddress); + preKeyHelper = new PreKeyHelper(account, dependencies); + + this.context = new Context(account, + dependencies, + stickerPackStore, + sendHelper, + groupHelper, + syncHelper, + profileHelper, + storageHelper, + preKeyHelper); + var jobExecutor = new JobExecutor(context); + + this.incomingMessageHandler = new IncomingMessageHandler(account, + dependencies, + account.getRecipientStore(), + this::resolveSignalServiceAddress, + groupHelper, + contactHelper, + attachmentHelper, + syncHelper, + this::getRecipientProfile, + jobExecutor); + } + + @Override + public String getUsername() { + return account.getUsername(); + } + + @Override + public RecipientId getSelfRecipientId() { + return account.getSelfRecipientId(); + } + + @Override + public int getDeviceId() { + return account.getDeviceId(); + } + + @Override + 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); + } + } + preKeyHelper.refreshPreKeysIfNecessary(); + if (account.getUuid() == null) { + account.setUuid(dependencies.getAccountManager().getOwnUuid()); + } + updateAccountAttributes(null); + } + + /** + * 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 + */ + @Override + public Map> areUsersRegistered(Set numbers) throws IOException { + Map canonicalizedNumbers = numbers.stream().collect(Collectors.toMap(n -> n, n -> { + try { + return PhoneNumberFormatter.formatNumber(n, account.getUsername()); + } catch (InvalidNumberException e) { + return ""; + } + })); + + // Note "registeredUsers" has no optionals. It only gives us info on users who are registered + var registeredUsers = 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 = registeredUsers.get(number); + return new Pair<>(number.isEmpty() ? null : number, uuid); + })); + } + + @Override + public void updateAccountAttributes(String deviceName) throws IOException { + final String encryptedDeviceName; + if (deviceName == null) { + encryptedDeviceName = account.getEncryptedDeviceName(); + } else { + final var privateKey = account.getIdentityKeyPair().getPrivateKey(); + encryptedDeviceName = DeviceNameUtil.encryptDeviceName(deviceName, privateKey); + account.setEncryptedDeviceName(encryptedDeviceName); + } + dependencies.getAccountManager() + .setAccountAttributes(encryptedDeviceName, + null, + account.getLocalRegistrationId(), + true, + 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), + */ + @Override + public void setProfile( + String givenName, final String familyName, String about, String aboutEmoji, Optional avatar + ) throws IOException { + profileHelper.setProfile(givenName, familyName, about, aboutEmoji, avatar); + syncHelper.sendSyncFetchProfileMessage(); + } + + @Override + 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); + } + + @Override + public void deleteAccount() throws IOException { + try { + pinHelper.removeRegistrationLockPin(); + } catch (UnauthenticatedResponseException e) { + logger.warn("Failed to remove registration lock pin"); + } + account.setRegistrationLockPin(null, null); + + dependencies.getAccountManager().deleteAccount(); + + account.setRegistered(false); + } + + @Override + public void submitRateLimitRecaptchaChallenge(String challenge, String captcha) throws IOException { + dependencies.getAccountManager().submitRateLimitRecaptchaChallenge(challenge, captcha); + } + + @Override + 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()); + } + + @Override + public void removeLinkedDevices(int deviceId) throws IOException { + dependencies.getAccountManager().removeDevice(deviceId); + var devices = dependencies.getAccountManager().getDevices(); + account.setMultiDevice(devices.size() > 1); + } + + @Override + 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 = account.getIdentityKeyPair(); + var verificationCode = dependencies.getAccountManager().getNewDeviceVerificationCode(); + + dependencies.getAccountManager() + .addDevice(deviceIdentifier, + deviceKey, + identityKeyPair, + Optional.of(account.getProfileKey().serialize()), + verificationCode); + account.setMultiDevice(true); + } + + @Override + 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 { + preKeyHelper.refreshPreKeys(); + } + + @Override + public Profile getRecipientProfile(RecipientId recipientId) { + return profileHelper.getRecipientProfile(recipientId); + } + + @Override + public List getGroups() { + return account.getGroupStore().getGroups(); + } + + @Override + public SendGroupMessageResults quitGroup( + GroupId groupId, Set groupAdmins + ) throws GroupNotFoundException, IOException, NotAGroupMemberException, LastGroupAdminException { + final var newAdmins = resolveRecipients(groupAdmins); + return groupHelper.quitGroup(groupId, newAdmins); + } + + @Override + public void deleteGroup(GroupId groupId) throws IOException { + groupHelper.deleteGroup(groupId); + } + + @Override + public Pair createGroup( + String name, Set members, File avatarFile + ) throws IOException, AttachmentInvalidException { + return groupHelper.createGroup(name, members == null ? null : resolveRecipients(members), avatarFile); + } + + @Override + 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 : resolveRecipients(members), + removeMembers == null ? null : resolveRecipients(removeMembers), + admins == null ? null : resolveRecipients(admins), + removeAdmins == null ? null : resolveRecipients(removeAdmins), + resetGroupLink, + groupLinkState, + addMemberPermission, + editDetailsPermission, + avatarFile, + expirationTimer, + isAnnouncementGroup); + } + + @Override + public Pair joinGroup( + GroupInviteLinkUrl inviteLinkUrl + ) throws IOException, GroupLinkNotActiveException { + return groupHelper.joinGroup(inviteLinkUrl); + } + + private 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); + } + + private 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); + } + } + } + + @Override + public void sendTypingMessage( + TypingAction action, Set recipients + ) throws IOException, UntrustedIdentityException, NotAGroupMemberException, GroupNotFoundException, GroupSendingNotAllowedException { + sendTypingMessage(action.toSignalService(), recipients); + } + + @Override + 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)); + } + + @Override + 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)); + } + + @Override + 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()); + final var attachments = message.getAttachments(); + if (attachments != null) { + messageBuilder.withAttachments(attachmentHelper.uploadAttachments(attachments)); + } + } + + @Override + 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); + } + + @Override + 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); + } + + @Override + 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(recipient); + account.getSessionStore().deleteAllSessions(recipientId); + } + } + } + + @Override + public void setContactName( + RecipientIdentifier.Single recipient, String name + ) throws NotMasterDeviceException, UnregisteredUserException { + if (!account.isMasterDevice()) { + throw new NotMasterDeviceException(); + } + contactHelper.setContactName(resolveRecipient(recipient), name); + } + + @Override + public void setContactBlocked( + RecipientIdentifier.Single recipient, boolean blocked + ) throws NotMasterDeviceException, IOException { + if (!account.isMasterDevice()) { + throw new NotMasterDeviceException(); + } + contactHelper.setContactBlocked(resolveRecipient(recipient), blocked); + // TODO cycle our profile key + syncHelper.sendBlockedList(); + } + + @Override + public void setGroupBlocked( + final GroupId groupId, final boolean blocked + ) throws GroupNotFoundException, IOException { + groupHelper.setGroupBlocked(groupId, blocked); + // TODO cycle our profile key + syncHelper.sendBlockedList(); + } + + /** + * Change the expiration timer for a contact + */ + @Override + public void setExpirationTimer( + RecipientIdentifier.Single recipient, int messageExpirationTimer + ) throws IOException { + var recipientId = resolveRecipient(recipient); + contactHelper.setExpirationTimer(recipientId, messageExpirationTimer); + final var messageBuilder = SignalServiceDataMessage.newBuilder().asExpirationUpdate(); + try { + sendMessage(messageBuilder, Set.of(recipient)); + } catch (NotAGroupMemberException | GroupNotFoundException | GroupSendingNotAllowedException e) { + throw new AssertionError(e); + } + } + + /** + * 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 + */ + @Override + 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); + } + } + + @Override + public void requestAllSyncData() throws IOException { + syncHelper.requestAllSyncData(); + retrieveRemoteStorage(); + } + + void retrieveRemoteStorage() throws IOException { + if (account.getStorageKey() != null) { + storageHelper.readDataFromStorage(); + } + } + + 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 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 uuid = getRegisteredUser(number); + return resolveRecipientTrusted(new SignalServiceAddress(uuid, number)); + } + + private UUID getRegisteredUser(final String number) throws IOException { + final Map uuidMap; + try { + uuidMap = getRegisteredUsers(Set.of(number)); + } catch (NumberFormatException e) { + throw new UnregisteredUserException(number, e); + } + final var uuid = uuidMap.get(number); + if (uuid == null) { + throw new UnregisteredUserException(number, null); + } + return uuid; + } + + 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; + } + + 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) { + cachedMessage.delete(); + return null; + } + + final var result = incomingMessageHandler.handleRetryEnvelope(envelope, ignoreAttachments, handler); + final var actions = result.first(); + final var exception = result.second(); + + if (exception instanceof UntrustedIdentityException) { + if (System.currentTimeMillis() - envelope.getServerDeliveredTimestamp() > 1000L * 60 * 60 * 24 * 30) { + // Envelope is more than a month old, cleaning up. + cachedMessage.delete(); + return null; + } + if (!envelope.hasSourceUuid()) { + final var identifier = ((UntrustedIdentityException) exception).getSender(); + final var recipientId = account.getRecipientStore().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; + } + + // If successful and for all other errors that are not recoverable, delete the cached message + cachedMessage.delete(); + return actions; + } + + @Override + 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(); + + hasCaughtUpWithOldMessages = false; + + while (!Thread.interrupted()) { + SignalServiceEnvelope envelope; + 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.hasSourceUuid() + ? resolveRecipient(envelope1.getSourceAddress()) + : null; + // store message on disk, before acknowledging receipt to the server + cachedMessage[0] = account.getMessageCache().cacheMessage(envelope1, recipientId); + }); + if (result.isPresent()) { + envelope = result.get(); + logger.debug("New message received from server"); + } else { + logger.debug("Received indicator that server queue is empty"); + handleQueuedActions(queuedActions); + queuedActions.clear(); + + hasCaughtUpWithOldMessages = true; + synchronized (this) { + this.notifyAll(); + } + + // 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; + } + + final var result = incomingMessageHandler.handleEnvelope(envelope, ignoreAttachments, handler); + queuedActions.addAll(result.first()); + final var exception = result.second(); + + if (hasCaughtUpWithOldMessages) { + handleQueuedActions(queuedActions); + } + if (cachedMessage[0] != null) { + if (exception instanceof UntrustedIdentityException) { + final var address = ((UntrustedIdentityException) exception).getSender(); + final var recipientId = resolveRecipient(address); + if (!envelope.hasSourceUuid()) { + 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); + } + + @Override + public boolean hasCaughtUpWithOldMessages() { + return hasCaughtUpWithOldMessages; + } + + private void handleQueuedActions(final Collection queuedActions) { + var interrupted = false; + for (var action : queuedActions) { + try { + action.execute(context); + } catch (Throwable e) { + if ((e instanceof AssertionError || e instanceof RuntimeException) + && e.getCause() instanceof InterruptedException) { + interrupted = true; + continue; + } + logger.warn("Message action failed.", e); + } + } + if (interrupted) { + Thread.currentThread().interrupt(); + } + } + + @Override + public boolean isContactBlocked(final RecipientIdentifier.Single recipient) { + final RecipientId recipientId; + try { + recipientId = resolveRecipient(recipient); + } catch (UnregisteredUserException e) { + return false; + } + return contactHelper.isContactBlocked(recipientId); + } + + @Override + public File getAttachmentFile(SignalServiceAttachmentRemoteId attachmentId) { + return attachmentHelper.getAttachmentFile(attachmentId); + } + + @Override + public void sendContacts() throws IOException { + syncHelper.sendContacts(); + } + + @Override + public List> getContacts() { + return account.getContactStore().getContacts(); + } + + @Override + public String getContactOrProfileName(RecipientIdentifier.Single recipientIdentifier) { + final RecipientId recipientId; + try { + recipientId = resolveRecipient(recipientIdentifier); + } catch (UnregisteredUserException e) { + return null; + } + + final var contact = account.getContactStore().getContact(recipientId); + if (contact != null && !Util.isEmpty(contact.getName())) { + return contact.getName(); + } + + final var profile = getRecipientProfile(recipientId); + if (profile != null) { + return profile.getDisplayName(); + } + + return null; + } + + @Override + public GroupInfo getGroup(GroupId groupId) { + return groupHelper.getGroup(groupId); + } + + @Override + public List getIdentities() { + return account.getIdentityKeyStore().getIdentities(); + } + + @Override + public List getIdentities(RecipientIdentifier.Single recipient) { + IdentityInfo identity; + try { + identity = account.getIdentityKeyStore().getIdentity(resolveRecipient(recipient)); + } catch (UnregisteredUserException e) { + identity = null; + } + return identity == null ? List.of() : List.of(identity); + } + + /** + * Trust this the identity with this fingerprint + * + * @param recipient username of the identity + * @param fingerprint Fingerprint + */ + @Override + public boolean trustIdentityVerified(RecipientIdentifier.Single recipient, byte[] fingerprint) { + RecipientId recipientId; + try { + recipientId = resolveRecipient(recipient); + } catch (UnregisteredUserException e) { + return false; + } + 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 + */ + @Override + public boolean trustIdentityVerifiedSafetyNumber(RecipientIdentifier.Single recipient, String safetyNumber) { + RecipientId recipientId; + try { + recipientId = resolveRecipient(recipient); + } catch (UnregisteredUserException e) { + return false; + } + var address = resolveSignalServiceAddress(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 + */ + @Override + public boolean trustIdentityVerifiedSafetyNumber(RecipientIdentifier.Single recipient, byte[] safetyNumber) { + RecipientId recipientId; + try { + recipientId = resolveRecipient(recipient); + } catch (UnregisteredUserException e) { + return false; + } + var address = resolveSignalServiceAddress(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 + */ + @Override + public boolean trustIdentityAllKeys(RecipientIdentifier.Single recipient) { + RecipientId recipientId; + try { + recipientId = resolveRecipient(recipient); + } catch (UnregisteredUserException e) { + return false; + } + 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 = resolveSignalServiceAddress(recipientId); + syncHelper.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 + profileHelper.refreshRecipientProfile(recipientId); + } + } + + @Override + public String computeSafetyNumber(SignalServiceAddress theirAddress, IdentityKey theirIdentityKey) { + final Fingerprint fingerprint = computeSafetyNumberFingerprint(theirAddress, theirIdentityKey); + return fingerprint == null ? null : fingerprint.getDisplayableFingerprint().getDisplayText(); + } + + @Override + 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(), + account.getIdentityKeyPair().getPublicKey(), + theirAddress, + theirIdentityKey); + } + + @Override + public SignalServiceAddress resolveSignalServiceAddress(SignalServiceAddress address) { + return resolveSignalServiceAddress(resolveRecipient(address)); + } + + @Override + public SignalServiceAddress resolveSignalServiceAddress(UUID uuid) { + return resolveSignalServiceAddress(account.getRecipientStore().resolveRecipient(uuid)); + } + + @Override + public SignalServiceAddress resolveSignalServiceAddress(RecipientId recipientId) { + final var address = account.getRecipientStore().resolveRecipientAddress(recipientId); + if (address.getUuid().isPresent()) { + return address.toSignalServiceAddress(); + } + + // Address in recipient store doesn't have a uuid, this shouldn't happen + // Try to retrieve the uuid from the server + final var number = address.getNumber().get(); + try { + return resolveSignalServiceAddress(getRegisteredUser(number)); + } catch (IOException e) { + logger.warn("Failed to get uuid for e164 number: {}", number, e); + // Return SignalServiceAddress with unknown UUID + return address.toSignalServiceAddress(); + } + } + + private Set resolveRecipients(Collection recipients) throws UnregisteredUserException { + final var recipientIds = new HashSet(recipients.size()); + for (var number : recipients) { + final var recipientId = resolveRecipient(number); + recipientIds.add(recipientId); + } + return recipientIds; + } + + private RecipientId resolveRecipient(final RecipientIdentifier.Single recipient) throws UnregisteredUserException { + if (recipient instanceof RecipientIdentifier.Uuid) { + return account.getRecipientStore().resolveRecipient(((RecipientIdentifier.Uuid) recipient).uuid); + } else { + final var number = ((RecipientIdentifier.Number) recipient).number; + return account.getRecipientStore().resolveRecipient(number, () -> { + try { + return getRegisteredUser(number); + } catch (IOException e) { + return null; + } + }); + } + } + + private RecipientId resolveRecipient(SignalServiceAddress address) { + return account.getRecipientStore().resolveRecipient(address); + } + + private RecipientId resolveRecipientTrusted(SignalServiceAddress address) { + return account.getRecipientStore().resolveRecipientTrusted(address); + } + + @Override + public void close() throws IOException { + close(true); + } + + private void close(boolean closeAccount) throws IOException { + executor.shutdown(); + + dependencies.getSignalWebSocket().disconnect(); + + if (closeAccount && account != null) { + account.close(); + } + account = null; + } + +} diff --git a/lib/src/main/java/org/asamk/signal/manager/ProvisioningManager.java b/lib/src/main/java/org/asamk/signal/manager/ProvisioningManager.java index 90dc6c66..226de9be 100644 --- a/lib/src/main/java/org/asamk/signal/manager/ProvisioningManager.java +++ b/lib/src/main/java/org/asamk/signal/manager/ProvisioningManager.java @@ -126,9 +126,9 @@ public class ProvisioningManager { profileKey, TrustNewIdentity.ON_FIRST_USE); - Manager m = null; + ManagerImpl m = null; try { - m = new Manager(account, pathConfig, serviceEnvironmentConfig, userAgent); + m = new ManagerImpl(account, pathConfig, serviceEnvironmentConfig, userAgent); logger.debug("Refreshing pre keys"); try { @@ -178,7 +178,7 @@ public class ProvisioningManager { return false; } - final var m = new Manager(signalAccount, pathConfig, serviceEnvironmentConfig, userAgent); + final var m = new ManagerImpl(signalAccount, pathConfig, serviceEnvironmentConfig, userAgent); try (m) { m.checkAccountState(); } catch (AuthorizationFailedException ignored) { diff --git a/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java b/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java index 443a7969..978f1fd5 100644 --- a/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java +++ b/lib/src/main/java/org/asamk/signal/manager/RegistrationManager.java @@ -177,9 +177,9 @@ public class RegistrationManager implements Closeable { //accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID))); account.finishRegistration(UuidUtil.parseOrNull(response.getUuid()), masterKey, pin); - Manager m = null; + ManagerImpl m = null; try { - m = new Manager(account, pathConfig, serviceEnvironmentConfig, userAgent); + m = new ManagerImpl(account, pathConfig, serviceEnvironmentConfig, userAgent); account = null; m.refreshPreKeys();