]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/helper/RecipientHelper.java
Use CDSI for contact discovery in compat mode
[signal-cli] / lib / src / main / java / org / asamk / signal / manager / helper / RecipientHelper.java
1 package org.asamk.signal.manager.helper;
2
3 import org.asamk.signal.manager.SignalDependencies;
4 import org.asamk.signal.manager.api.RecipientIdentifier;
5 import org.asamk.signal.manager.api.UnregisteredRecipientException;
6 import org.asamk.signal.manager.config.ServiceConfig;
7 import org.asamk.signal.manager.config.ServiceEnvironmentConfig;
8 import org.asamk.signal.manager.storage.SignalAccount;
9 import org.asamk.signal.manager.storage.recipients.RecipientAddress;
10 import org.asamk.signal.manager.storage.recipients.RecipientId;
11 import org.signal.libsignal.protocol.InvalidKeyException;
12 import org.slf4j.Logger;
13 import org.slf4j.LoggerFactory;
14 import org.whispersystems.signalservice.api.push.ACI;
15 import org.whispersystems.signalservice.api.push.ServiceId;
16 import org.whispersystems.signalservice.api.push.SignalServiceAddress;
17 import org.whispersystems.signalservice.api.services.CdsiV2Service;
18 import org.whispersystems.signalservice.internal.contacts.crypto.Quote;
19 import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedQuoteException;
20 import org.whispersystems.signalservice.internal.contacts.crypto.UnauthenticatedResponseException;
21
22 import java.io.IOException;
23 import java.security.SignatureException;
24 import java.util.Collection;
25 import java.util.HashMap;
26 import java.util.HashSet;
27 import java.util.Map;
28 import java.util.Optional;
29 import java.util.Set;
30
31 public class RecipientHelper {
32
33 private final static Logger logger = LoggerFactory.getLogger(RecipientHelper.class);
34
35 private final SignalAccount account;
36 private final SignalDependencies dependencies;
37 private final ServiceEnvironmentConfig serviceEnvironmentConfig;
38
39 public RecipientHelper(final Context context) {
40 this.account = context.getAccount();
41 this.dependencies = context.getDependencies();
42 this.serviceEnvironmentConfig = dependencies.getServiceEnvironmentConfig();
43 }
44
45 public SignalServiceAddress resolveSignalServiceAddress(RecipientId recipientId) {
46 final var address = account.getRecipientAddressResolver().resolveRecipientAddress(recipientId);
47 if (address.number().isEmpty() || address.uuid().isPresent()) {
48 return address.toSignalServiceAddress();
49 }
50
51 // Address in recipient store doesn't have a uuid, this shouldn't happen
52 // Try to retrieve the uuid from the server
53 final var number = address.number().get();
54 final ACI aci;
55 try {
56 aci = getRegisteredUser(number);
57 } catch (UnregisteredRecipientException e) {
58 logger.warn("Failed to get uuid for e164 number: {}", number);
59 // Return SignalServiceAddress with unknown UUID
60 return address.toSignalServiceAddress();
61 } catch (IOException e) {
62 logger.warn("Failed to get uuid for e164 number: {}", number, e);
63 // Return SignalServiceAddress with unknown UUID
64 return address.toSignalServiceAddress();
65 }
66 return account.getRecipientAddressResolver()
67 .resolveRecipientAddress(account.getRecipientResolver().resolveRecipient(aci))
68 .toSignalServiceAddress();
69 }
70
71 public RecipientId resolveRecipient(final SignalServiceAddress address) {
72 return account.getRecipientResolver().resolveRecipient(address);
73 }
74
75 public Set<RecipientId> resolveRecipients(Collection<RecipientIdentifier.Single> recipients) throws UnregisteredRecipientException {
76 final var recipientIds = new HashSet<RecipientId>(recipients.size());
77 for (var number : recipients) {
78 final var recipientId = resolveRecipient(number);
79 recipientIds.add(recipientId);
80 }
81 return recipientIds;
82 }
83
84 public RecipientId resolveRecipient(final RecipientIdentifier.Single recipient) throws UnregisteredRecipientException {
85 if (recipient instanceof RecipientIdentifier.Uuid uuidRecipient) {
86 return account.getRecipientResolver().resolveRecipient(ServiceId.from(uuidRecipient.uuid()));
87 } else {
88 final var number = ((RecipientIdentifier.Number) recipient).number();
89 return account.getRecipientStore().resolveRecipient(number, () -> {
90 try {
91 return getRegisteredUser(number);
92 } catch (Exception e) {
93 return null;
94 }
95 });
96 }
97 }
98
99 public RecipientId refreshRegisteredUser(RecipientId recipientId) throws IOException, UnregisteredRecipientException {
100 final var address = resolveSignalServiceAddress(recipientId);
101 if (address.getNumber().isEmpty()) {
102 return recipientId;
103 }
104 final var number = address.getNumber().get();
105 final var uuid = getRegisteredUser(number);
106 return account.getRecipientTrustedResolver().resolveRecipientTrusted(new SignalServiceAddress(uuid, number));
107 }
108
109 public Map<String, ACI> getRegisteredUsers(final Set<String> numbers) throws IOException {
110 Map<String, ACI> registeredUsers;
111 try {
112 registeredUsers = getRegisteredUsersV2(numbers, true);
113 } catch (IOException e) {
114 logger.warn("CDSI request failed, trying fallback to CDS", e);
115 registeredUsers = getRegisteredUsersV1(numbers);
116 }
117
118 // Store numbers as recipients, so we have the number/uuid association
119 registeredUsers.forEach((number, aci) -> account.getRecipientTrustedResolver()
120 .resolveRecipientTrusted(new SignalServiceAddress(aci, number)));
121
122 return registeredUsers;
123 }
124
125 private ACI getRegisteredUser(final String number) throws IOException, UnregisteredRecipientException {
126 final Map<String, ACI> aciMap;
127 try {
128 aciMap = getRegisteredUsers(Set.of(number));
129 } catch (NumberFormatException e) {
130 throw new UnregisteredRecipientException(new RecipientAddress(null, number));
131 }
132 final var uuid = aciMap.get(number);
133 if (uuid == null) {
134 throw new UnregisteredRecipientException(new RecipientAddress(null, number));
135 }
136 return uuid;
137 }
138
139 private Map<String, ACI> getRegisteredUsersV1(final Set<String> numbers) throws IOException {
140 final Map<String, ACI> registeredUsers;
141 try {
142 registeredUsers = dependencies.getAccountManager()
143 .getRegisteredUsers(ServiceConfig.getIasKeyStore(),
144 numbers,
145 serviceEnvironmentConfig.getCdsMrenclave());
146 } catch (Quote.InvalidQuoteFormatException | UnauthenticatedQuoteException | SignatureException |
147 UnauthenticatedResponseException | InvalidKeyException | NumberFormatException e) {
148 throw new IOException(e);
149 }
150 return registeredUsers;
151 }
152
153 private Map<String, ACI> getRegisteredUsersV2(final Set<String> numbers, boolean useCompat) throws IOException {
154 // Only partial refresh is implemented here
155 final CdsiV2Service.Response response;
156 try {
157 response = dependencies.getAccountManager()
158 .getRegisteredUsersWithCdsi(Set.of(),
159 numbers,
160 account.getRecipientStore().getServiceIdToProfileKeyMap(),
161 useCompat,
162 Optional.empty(),
163 serviceEnvironmentConfig.getCdsiMrenclave(),
164 token -> {
165 // Not storing for partial refresh
166 });
167 } catch (NumberFormatException e) {
168 throw new IOException(e);
169 }
170 logger.debug("CDSI request successful, quota used by this request: {}", response.getQuotaUsedDebugOnly());
171
172 final var registeredUsers = new HashMap<String, ACI>();
173 response.getResults().forEach((key, value) -> {
174 if (value.getAci().isPresent()) {
175 registeredUsers.put(key, value.getAci().get());
176 }
177 });
178 return registeredUsers;
179 }
180
181 private ACI getRegisteredUserByUsername(String username) throws IOException {
182 return dependencies.getAccountManager().getAciByUsername(username);
183 }
184 }