]> nmode's Git Repositories - signal-cli/blobdiff - lib/src/main/java/org/asamk/signal/manager/Manager.java
Fix displaying group quit messages
[signal-cli] / lib / src / main / java / org / asamk / signal / manager / Manager.java
index 99099e92535848848406dd85bb2836827451ad1e..4bced5e2304a1d50640505724d290b61c325a963 100644 (file)
@@ -34,9 +34,10 @@ import org.asamk.signal.manager.storage.contacts.ContactInfo;
 import org.asamk.signal.manager.storage.groups.GroupInfo;
 import org.asamk.signal.manager.storage.groups.GroupInfoV1;
 import org.asamk.signal.manager.storage.groups.GroupInfoV2;
+import org.asamk.signal.manager.storage.identities.IdentityInfo;
 import org.asamk.signal.manager.storage.messageCache.CachedMessage;
 import org.asamk.signal.manager.storage.profiles.SignalProfile;
-import org.asamk.signal.manager.storage.protocol.IdentityInfo;
+import org.asamk.signal.manager.storage.recipients.RecipientId;
 import org.asamk.signal.manager.storage.stickers.Sticker;
 import org.asamk.signal.manager.util.AttachmentUtils;
 import org.asamk.signal.manager.util.IOUtils;
@@ -145,6 +146,7 @@ import java.nio.file.Files;
 import java.security.SignatureException;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Base64;
 import java.util.Collection;
 import java.util.Date;
 import java.util.HashSet;
@@ -234,8 +236,6 @@ public class Manager implements Closeable {
                 clientZkProfileOperations,
                 ServiceConfig.AUTOMATIC_NETWORK_RETRY);
 
-        this.account.setResolver(this::resolveSignalServiceAddress);
-
         this.unidentifiedAccessHelper = new UnidentifiedAccessHelper(account::getProfileKey,
                 account.getProfileStore()::getProfileKey,
                 this::getRecipientProfile,
@@ -263,7 +263,7 @@ public class Manager implements Closeable {
     }
 
     private IdentityKeyPair getIdentityKeyPair() {
-        return account.getSignalProtocolStore().getIdentityKeyPair();
+        return account.getIdentityKeyPair();
     }
 
     public int getDeviceId() {
@@ -336,7 +336,7 @@ public class Manager implements Closeable {
 
     public void updateAccountAttributes() throws IOException {
         accountManager.setAccountAttributes(null,
-                account.getSignalProtocolStore().getLocalRegistrationId(),
+                account.getLocalRegistrationId(),
                 true,
                 // set legacy pin only if no KBS master key is set
                 account.getPinMasterKey() == null ? account.getRegistrationLockPin() : null,
@@ -482,7 +482,6 @@ public class Manager implements Closeable {
 
         var records = KeyUtils.generatePreKeyRecords(offset, ServiceConfig.PREKEY_BATCH_SIZE);
         account.addPreKeys(records);
-        account.save();
 
         return records;
     }
@@ -492,7 +491,6 @@ public class Manager implements Closeable {
 
         var record = KeyUtils.generateSignedPreKeyRecord(identityKeyPair, signedPreKeyId);
         account.addSignedPreKey(record);
-        account.save();
 
         return record;
     }
@@ -535,11 +533,13 @@ public class Manager implements Closeable {
         return getRecipientProfile(address, false);
     }
 
-    private SignalProfile getRecipientProfile(
+    SignalProfile getRecipientProfile(
             SignalServiceAddress address, boolean force
     ) {
         var profileEntry = account.getProfileStore().getProfileEntry(address);
         if (profileEntry == null) {
+            // retrieve profile to get identity key
+            retrieveEncryptedProfile(address);
             return null;
         }
         var now = new Date().getTime();
@@ -552,14 +552,13 @@ public class Manager implements Closeable {
             profileEntry.setRequestPending(true);
             final SignalServiceProfile encryptedProfile;
             try {
-                encryptedProfile = profileHelper.retrieveProfileSync(address, SignalServiceProfile.RequestType.PROFILE)
-                        .getProfile();
-            } catch (IOException e) {
-                logger.warn("Failed to retrieve profile, ignoring: {}", e.getMessage());
-                return null;
+                encryptedProfile = retrieveEncryptedProfile(address);
             } finally {
                 profileEntry.setRequestPending(false);
             }
+            if (encryptedProfile == null) {
+                return null;
+            }
 
             final var profileKey = profileEntry.getProfileKey();
             final var profile = decryptProfileAndDownloadAvatar(address, profileKey, encryptedProfile);
@@ -570,6 +569,25 @@ public class Manager implements Closeable {
         return profileEntry.getProfile();
     }
 
+    private SignalServiceProfile retrieveEncryptedProfile(SignalServiceAddress address) {
+        try {
+            final var profile = profileHelper.retrieveProfileSync(address, SignalServiceProfile.RequestType.PROFILE)
+                    .getProfile();
+            try {
+                account.getIdentityKeyStore()
+                        .saveIdentity(resolveRecipient(address),
+                                new IdentityKey(Base64.getDecoder().decode(profile.getIdentityKey())),
+                                new Date());
+            } catch (InvalidKeyException ignored) {
+                logger.warn("Got invalid identity key in profile for {}", address.getLegacyIdentifier());
+            }
+            return profile;
+        } catch (IOException e) {
+            logger.warn("Failed to retrieve profile, ignoring: {}", e.getMessage());
+            return null;
+        }
+    }
+
     private ProfileKeyCredential getRecipientProfileKeyCredential(SignalServiceAddress address) {
         var profileEntry = account.getProfileStore().getProfileEntry(address);
         if (profileEntry == null) {
@@ -992,6 +1010,22 @@ public class Manager implements Closeable {
         return sendSelfMessage(messageBuilder);
     }
 
+    public Pair<Long, List<SendMessageResult>> sendRemoteDeleteMessage(
+            long targetSentTimestamp, List<String> recipients
+    ) throws IOException, InvalidNumberException {
+        var delete = new SignalServiceDataMessage.RemoteDelete(targetSentTimestamp);
+        final var messageBuilder = SignalServiceDataMessage.newBuilder().withRemoteDelete(delete);
+        return sendMessage(messageBuilder, getSignalServiceAddresses(recipients));
+    }
+
+    public Pair<Long, List<SendMessageResult>> sendGroupRemoteDeleteMessage(
+            long targetSentTimestamp, GroupId groupId
+    ) throws IOException, NotAGroupMemberException, GroupNotFoundException {
+        var delete = new SignalServiceDataMessage.RemoteDelete(targetSentTimestamp);
+        final var messageBuilder = SignalServiceDataMessage.newBuilder().withRemoteDelete(delete);
+        return sendGroupMessage(messageBuilder, groupId);
+    }
+
     public Pair<Long, List<SendMessageResult>> sendMessageReaction(
             String emoji, boolean remove, String targetAuthor, long targetSentTimestamp, List<String> recipients
     ) throws IOException, InvalidNumberException {
@@ -1209,17 +1243,7 @@ public class Manager implements Closeable {
 
     private void sendSyncMessage(SignalServiceSyncMessage message) throws IOException, UntrustedIdentityException {
         var messageSender = createMessageSender();
-        try {
-            messageSender.sendMessage(message, unidentifiedAccessHelper.getAccessForSync());
-        } catch (UntrustedIdentityException e) {
-            if (e.getIdentityKey() != null) {
-                account.getSignalProtocolStore()
-                        .saveIdentity(resolveSignalServiceAddress(e.getIdentifier()),
-                                e.getIdentityKey(),
-                                TrustLevel.UNTRUSTED);
-            }
-            throw e;
-        }
+        messageSender.sendMessage(message, unidentifiedAccessHelper.getAccessForSync());
     }
 
     private Collection<SignalServiceAddress> getSignalServiceAddresses(Collection<String> numbers) throws InvalidNumberException {
@@ -1289,22 +1313,18 @@ public class Manager implements Closeable {
                             unidentifiedAccessHelper.getAccessFor(recipients),
                             isRecipientUpdate,
                             message);
+
                     for (var r : result) {
                         if (r.getIdentityFailure() != null) {
-                            account.getSignalProtocolStore()
-                                    .saveIdentity(r.getAddress(),
+                            account.getIdentityKeyStore().
+                                    saveIdentity(resolveRecipient(r.getAddress()),
                                             r.getIdentityFailure().getIdentityKey(),
-                                            TrustLevel.UNTRUSTED);
+                                            new Date());
                         }
                     }
+
                     return new Pair<>(timestamp, result);
                 } catch (UntrustedIdentityException e) {
-                    if (e.getIdentityKey() != null) {
-                        account.getSignalProtocolStore()
-                                .saveIdentity(resolveSignalServiceAddress(e.getIdentifier()),
-                                        e.getIdentityKey(),
-                                        TrustLevel.UNTRUSTED);
-                    }
                     return new Pair<>(timestamp, List.of());
                 }
             } else {
@@ -1374,12 +1394,6 @@ public class Manager implements Closeable {
                     false,
                     System.currentTimeMillis() - startTime);
         } catch (UntrustedIdentityException e) {
-            if (e.getIdentityKey() != null) {
-                account.getSignalProtocolStore()
-                        .saveIdentity(resolveSignalServiceAddress(e.getIdentifier()),
-                                e.getIdentityKey(),
-                                TrustLevel.UNTRUSTED);
-            }
             return SendMessageResult.identityFailure(recipient, e.getIdentityKey());
         }
     }
@@ -1392,12 +1406,6 @@ public class Manager implements Closeable {
         try {
             return messageSender.sendMessage(address, unidentifiedAccessHelper.getAccessFor(address), message);
         } catch (UntrustedIdentityException e) {
-            if (e.getIdentityKey() != null) {
-                account.getSignalProtocolStore()
-                        .saveIdentity(resolveSignalServiceAddress(e.getIdentifier()),
-                                e.getIdentityKey(),
-                                TrustLevel.UNTRUSTED);
-            }
             return SendMessageResult.identityFailure(address, e.getIdentityKey());
         }
     }
@@ -1410,22 +1418,14 @@ public class Manager implements Closeable {
             return cipher.decrypt(envelope);
         } catch (ProtocolUntrustedIdentityException e) {
             if (e.getCause() instanceof org.whispersystems.libsignal.UntrustedIdentityException) {
-                var identityException = (org.whispersystems.libsignal.UntrustedIdentityException) e.getCause();
-                final var untrustedIdentity = identityException.getUntrustedIdentity();
-                if (untrustedIdentity != null) {
-                    account.getSignalProtocolStore()
-                            .saveIdentity(resolveSignalServiceAddress(identityException.getName()),
-                                    untrustedIdentity,
-                                    TrustLevel.UNTRUSTED);
-                }
-                throw identityException;
+                throw (org.whispersystems.libsignal.UntrustedIdentityException) e.getCause();
             }
             throw new AssertionError(e);
         }
     }
 
     private void handleEndSession(SignalServiceAddress source) {
-        account.getSignalProtocolStore().deleteAllSessions(source);
+        account.getSessionStore().deleteAllSessions(source.getIdentifier());
     }
 
     private List<HandleAction> handleSignalServiceDataMessage(
@@ -1643,41 +1643,56 @@ public class Manager implements Closeable {
     }
 
     private void retryFailedReceivedMessages(ReceiveMessageHandler handler, boolean ignoreAttachments) {
+        Set<HandleAction> queuedActions = new HashSet<>();
         for (var cachedMessage : account.getMessageCache().getCachedMessages()) {
-            retryFailedReceivedMessage(handler, ignoreAttachments, cachedMessage);
+            var actions = retryFailedReceivedMessage(handler, ignoreAttachments, cachedMessage);
+            if (actions != null) {
+                queuedActions.addAll(actions);
+            }
+        }
+        for (var action : queuedActions) {
+            try {
+                action.execute(this);
+            } catch (Throwable e) {
+                logger.warn("Message action failed.", e);
+            }
         }
     }
 
-    private void retryFailedReceivedMessage(
+    private List<HandleAction> retryFailedReceivedMessage(
             final ReceiveMessageHandler handler, final boolean ignoreAttachments, final CachedMessage cachedMessage
     ) {
         var envelope = cachedMessage.loadEnvelope();
         if (envelope == null) {
-            return;
+            return null;
         }
         SignalServiceContent content = null;
+        List<HandleAction> actions = null;
         if (!envelope.isReceipt()) {
             try {
                 content = decryptMessage(envelope);
             } catch (org.whispersystems.libsignal.UntrustedIdentityException e) {
-                return;
+                if (!envelope.hasSource()) {
+                    final var recipientId = resolveRecipient(((org.whispersystems.libsignal.UntrustedIdentityException) e)
+                            .getName());
+                    try {
+                        account.getMessageCache().replaceSender(cachedMessage, recipientId);
+                    } catch (IOException ioException) {
+                        logger.warn("Failed to move cached message to recipient folder: {}", ioException.getMessage());
+                    }
+                }
+                return null;
             } catch (Exception er) {
                 // All other errors are not recoverable, so delete the cached message
                 cachedMessage.delete();
-                return;
-            }
-            var actions = handleMessage(envelope, content, ignoreAttachments);
-            for (var action : actions) {
-                try {
-                    action.execute(this);
-                } catch (Throwable e) {
-                    logger.warn("Message action failed.", e);
-                }
+                return null;
             }
+            actions = handleMessage(envelope, content, ignoreAttachments);
         }
         account.save();
         handler.handleMessage(envelope, content, null);
         cachedMessage.delete();
+        return actions;
     }
 
     public void receiveMessages(
@@ -1702,8 +1717,11 @@ public class Manager implements Closeable {
             final CachedMessage[] cachedMessage = {null};
             try {
                 var result = messagePipe.readOrEmpty(timeout, unit, envelope1 -> {
+                    final var recipientId = envelope1.hasSource()
+                            ? resolveRecipient(envelope1.getSourceIdentifier())
+                            : null;
                     // store message on disk, before acknowledging receipt to the server
-                    cachedMessage[0] = account.getMessageCache().cacheMessage(envelope1);
+                    cachedMessage[0] = account.getMessageCache().cacheMessage(envelope1, recipientId);
                 });
                 if (result.isPresent()) {
                     envelope = result.get();
@@ -1734,9 +1752,9 @@ public class Manager implements Closeable {
 
             if (envelope.hasSource()) {
                 // Store uuid if we don't have it already
-                var source = envelope.getSourceAddress();
-                resolveSignalServiceAddress(source);
+                resolveRecipientTrusted(envelope.getSourceAddress());
             }
+            final var notAGroupMember = isNotAGroupMember(envelope, content);
             if (!envelope.isReceipt()) {
                 try {
                     content = decryptMessage(envelope);
@@ -1762,13 +1780,25 @@ public class Manager implements Closeable {
             account.save();
             if (isMessageBlocked(envelope, content)) {
                 logger.info("Ignoring a message from blocked user/group: {}", envelope.getTimestamp());
-            } else if (isNotAGroupMember(envelope, content)) {
+            } else if (notAGroupMember) {
                 logger.info("Ignoring a message from a non group member: {}", envelope.getTimestamp());
             } else {
                 handler.handleMessage(envelope, content, exception);
             }
-            if (!(exception instanceof org.whispersystems.libsignal.UntrustedIdentityException)) {
-                if (cachedMessage[0] != null) {
+            if (cachedMessage[0] != null) {
+                if (exception instanceof org.whispersystems.libsignal.UntrustedIdentityException) {
+                    final var recipientId = resolveRecipient(((org.whispersystems.libsignal.UntrustedIdentityException) exception)
+                            .getName());
+                    queuedActions.add(new RetrieveProfileAction(resolveSignalServiceAddress(recipientId)));
+                    if (!envelope.hasSource()) {
+                        try {
+                            cachedMessage[0] = account.getMessageCache().replaceSender(cachedMessage[0], recipientId);
+                        } catch (IOException ioException) {
+                            logger.warn("Failed to move cached message to recipient folder: {}",
+                                    ioException.getMessage());
+                        }
+                    }
+                } else {
                     cachedMessage[0].delete();
                 }
             }
@@ -1874,7 +1904,7 @@ public class Manager implements Closeable {
                             destination,
                             ignoreAttachments));
                 }
-                if (syncMessage.getRequest().isPresent()) {
+                if (syncMessage.getRequest().isPresent() && account.isMasterDevice()) {
                     var rm = syncMessage.getRequest().get();
                     if (rm.isContactsRequest()) {
                         actions.add(SendSyncContactsAction.create());
@@ -1966,9 +1996,6 @@ public class Manager implements Closeable {
                         try (var attachmentAsStream = retrieveAttachmentAsStream(contactsMessage.getContactsStream()
                                 .asPointer(), tmpFile)) {
                             var s = new DeviceContactsInputStream(attachmentAsStream);
-                            if (contactsMessage.isComplete()) {
-                                account.getContactStore().clear();
-                            }
                             DeviceContact c;
                             while ((c = s.read()) != null) {
                                 if (c.getAddress().matches(account.getSelfAddress()) && c.getProfileKey().isPresent()) {
@@ -1990,8 +2017,8 @@ public class Manager implements Closeable {
                                 }
                                 if (c.getVerified().isPresent()) {
                                     final var verifiedMessage = c.getVerified().get();
-                                    account.getSignalProtocolStore()
-                                            .setIdentityTrustLevel(verifiedMessage.getDestination(),
+                                    account.getIdentityKeyStore()
+                                            .setIdentityTrustLevel(resolveRecipientTrusted(verifiedMessage.getDestination()),
                                                     verifiedMessage.getIdentityKey(),
                                                     TrustLevel.fromVerifiedState(verifiedMessage.getVerified()));
                                 }
@@ -2026,8 +2053,8 @@ public class Manager implements Closeable {
                 }
                 if (syncMessage.getVerified().isPresent()) {
                     final var verifiedMessage = syncMessage.getVerified().get();
-                    account.getSignalProtocolStore()
-                            .setIdentityTrustLevel(resolveSignalServiceAddress(verifiedMessage.getDestination()),
+                    account.getIdentityKeyStore()
+                            .setIdentityTrustLevel(resolveRecipientTrusted(verifiedMessage.getDestination()),
                                     verifiedMessage.getIdentityKey(),
                                     TrustLevel.fromVerifiedState(verifiedMessage.getVerified()));
                 }
@@ -2269,7 +2296,8 @@ public class Manager implements Closeable {
                 var out = new DeviceContactsOutputStream(fos);
                 for (var record : account.getContactStore().getContacts()) {
                     VerifiedMessage verifiedMessage = null;
-                    var currentIdentity = account.getSignalProtocolStore().getIdentity(record.getAddress());
+                    var currentIdentity = account.getIdentityKeyStore()
+                            .getIdentity(resolveRecipientTrusted(record.getAddress()));
                     if (currentIdentity != null) {
                         verifiedMessage = new VerifiedMessage(record.getAddress(),
                                 currentIdentity.getIdentityKey(),
@@ -2381,11 +2409,12 @@ public class Manager implements Closeable {
     }
 
     public List<IdentityInfo> getIdentities() {
-        return account.getSignalProtocolStore().getIdentities();
+        return account.getIdentityKeyStore().getIdentities();
     }
 
     public List<IdentityInfo> getIdentities(String number) throws InvalidNumberException {
-        return account.getSignalProtocolStore().getIdentities(canonicalizeAndResolveSignalServiceAddress(number));
+        final var identity = account.getIdentityKeyStore().getIdentity(canonicalizeAndResolveRecipient(number));
+        return identity == null ? List.of() : List.of(identity);
     }
 
     /**
@@ -2395,8 +2424,10 @@ public class Manager implements Closeable {
      * @param fingerprint Fingerprint
      */
     public boolean trustIdentityVerified(String name, byte[] fingerprint) throws InvalidNumberException {
-        var address = canonicalizeAndResolveSignalServiceAddress(name);
-        return trustIdentity(address, (identityKey) -> Arrays.equals(identityKey.serialize(), fingerprint));
+        var recipientId = canonicalizeAndResolveRecipient(name);
+        return trustIdentity(recipientId,
+                identityKey -> Arrays.equals(identityKey.serialize(), fingerprint),
+                TrustLevel.TRUSTED_VERIFIED);
     }
 
     /**
@@ -2406,72 +2437,43 @@ public class Manager implements Closeable {
      * @param safetyNumber Safety number
      */
     public boolean trustIdentityVerifiedSafetyNumber(String name, String safetyNumber) throws InvalidNumberException {
-        var address = canonicalizeAndResolveSignalServiceAddress(name);
-        return trustIdentity(address, (identityKey) -> safetyNumber.equals(computeSafetyNumber(address, identityKey)));
+        var recipientId = canonicalizeAndResolveRecipient(name);
+        var address = account.getRecipientStore().resolveServiceAddress(recipientId);
+        return trustIdentity(recipientId,
+                identityKey -> safetyNumber.equals(computeSafetyNumber(address, identityKey)),
+                TrustLevel.TRUSTED_VERIFIED);
     }
 
-    private boolean trustIdentity(SignalServiceAddress address, Function<IdentityKey, Boolean> verifier) {
-        var ids = account.getSignalProtocolStore().getIdentities(address);
-        if (ids == null) {
-            return false;
-        }
-
-        IdentityInfo foundIdentity = null;
+    /**
+     * Trust all keys of this identity without verification
+     *
+     * @param name username of the identity
+     */
+    public boolean trustIdentityAllKeys(String name) throws InvalidNumberException {
+        var recipientId = canonicalizeAndResolveRecipient(name);
+        return trustIdentity(recipientId, identityKey -> true, TrustLevel.TRUSTED_UNVERIFIED);
+    }
 
-        for (var id : ids) {
-            if (verifier.apply(id.getIdentityKey())) {
-                foundIdentity = id;
-                break;
-            }
+    private boolean trustIdentity(
+            RecipientId recipientId, Function<IdentityKey, Boolean> verifier, TrustLevel trustLevel
+    ) {
+        var identity = account.getIdentityKeyStore().getIdentity(recipientId);
+        if (identity == null) {
+            return false;
         }
 
-        if (foundIdentity == null) {
+        if (!verifier.apply(identity.getIdentityKey())) {
             return false;
         }
 
-        account.getSignalProtocolStore()
-                .setIdentityTrustLevel(address, foundIdentity.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED);
+        account.getIdentityKeyStore().setIdentityTrustLevel(recipientId, identity.getIdentityKey(), trustLevel);
         try {
-            sendVerifiedMessage(address, foundIdentity.getIdentityKey(), TrustLevel.TRUSTED_VERIFIED);
+            var address = account.getRecipientStore().resolveServiceAddress(recipientId);
+            sendVerifiedMessage(address, identity.getIdentityKey(), trustLevel);
         } catch (IOException | UntrustedIdentityException e) {
             logger.warn("Failed to send verification sync message: {}", e.getMessage());
         }
 
-        // Successfully trusted the new identity, now remove all other identities for that number
-        for (var id : ids) {
-            if (id == foundIdentity) {
-                continue;
-            }
-            account.getSignalProtocolStore().removeIdentity(address, id.getIdentityKey());
-        }
-
-        account.save();
-        return true;
-    }
-
-    /**
-     * Trust all keys of this identity without verification
-     *
-     * @param name username of the identity
-     */
-    public boolean trustIdentityAllKeys(String name) {
-        var address = resolveSignalServiceAddress(name);
-        var ids = account.getSignalProtocolStore().getIdentities(address);
-        if (ids == null) {
-            return false;
-        }
-        for (var id : ids) {
-            if (id.getTrustLevel() == TrustLevel.UNTRUSTED) {
-                account.getSignalProtocolStore()
-                        .setIdentityTrustLevel(address, id.getIdentityKey(), TrustLevel.TRUSTED_UNVERIFIED);
-                try {
-                    sendVerifiedMessage(address, id.getIdentityKey(), TrustLevel.TRUSTED_UNVERIFIED);
-                } catch (IOException | UntrustedIdentityException e) {
-                    logger.warn("Failed to send verification sync message: {}", e.getMessage());
-                }
-            }
-        }
-        account.save();
         return true;
     }
 
@@ -2485,6 +2487,7 @@ public class Manager implements Closeable {
                 theirIdentityKey);
     }
 
+    @Deprecated
     public SignalServiceAddress canonicalizeAndResolveSignalServiceAddress(String identifier) throws InvalidNumberException {
         var canonicalizedNumber = UuidUtil.isUuid(identifier)
                 ? identifier
@@ -2492,12 +2495,14 @@ public class Manager implements Closeable {
         return resolveSignalServiceAddress(canonicalizedNumber);
     }
 
+    @Deprecated
     public SignalServiceAddress resolveSignalServiceAddress(String identifier) {
         var address = Utils.getSignalServiceAddressFromIdentifier(identifier);
 
         return resolveSignalServiceAddress(address);
     }
 
+    @Deprecated
     public SignalServiceAddress resolveSignalServiceAddress(SignalServiceAddress address) {
         if (address.matches(account.getSelfAddress())) {
             return account.getSelfAddress();
@@ -2506,6 +2511,32 @@ public class Manager implements Closeable {
         return account.getRecipientStore().resolveServiceAddress(address);
     }
 
+    public SignalServiceAddress resolveSignalServiceAddress(RecipientId recipientId) {
+        return account.getRecipientStore().resolveServiceAddress(recipientId);
+    }
+
+    public RecipientId canonicalizeAndResolveRecipient(String identifier) throws InvalidNumberException {
+        var canonicalizedNumber = UuidUtil.isUuid(identifier)
+                ? identifier
+                : PhoneNumberFormatter.formatNumber(identifier, account.getUsername());
+
+        return resolveRecipient(canonicalizedNumber);
+    }
+
+    private RecipientId resolveRecipient(final String identifier) {
+        var address = Utils.getSignalServiceAddressFromIdentifier(identifier);
+
+        return resolveRecipient(address);
+    }
+
+    public RecipientId resolveRecipient(SignalServiceAddress address) {
+        return account.getRecipientStore().resolveRecipientUntrusted(address);
+    }
+
+    private RecipientId resolveRecipientTrusted(SignalServiceAddress address) {
+        return account.getRecipientStore().resolveRecipient(address);
+    }
+
     @Override
     public void close() throws IOException {
         close(true);