]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/helper/RecipientHelper.java
Fix truncating cdsi table
[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.api.RecipientIdentifier;
4 import org.asamk.signal.manager.api.UnregisteredRecipientException;
5 import org.asamk.signal.manager.api.UsernameLinkUrl;
6 import org.asamk.signal.manager.internal.SignalDependencies;
7 import org.asamk.signal.manager.storage.SignalAccount;
8 import org.asamk.signal.manager.storage.recipients.RecipientId;
9 import org.signal.libsignal.usernames.BaseUsernameException;
10 import org.signal.libsignal.usernames.Username;
11 import org.slf4j.Logger;
12 import org.slf4j.LoggerFactory;
13 import org.whispersystems.signalservice.api.push.ServiceId;
14 import org.whispersystems.signalservice.api.push.ServiceId.ACI;
15 import org.whispersystems.signalservice.api.push.ServiceId.PNI;
16 import org.whispersystems.signalservice.api.push.SignalServiceAddress;
17 import org.whispersystems.signalservice.api.push.exceptions.CdsiInvalidArgumentException;
18 import org.whispersystems.signalservice.api.push.exceptions.CdsiInvalidTokenException;
19 import org.whispersystems.signalservice.api.services.CdsiV2Service;
20
21 import java.io.IOException;
22 import java.util.Collection;
23 import java.util.HashMap;
24 import java.util.HashSet;
25 import java.util.Map;
26 import java.util.Optional;
27 import java.util.Set;
28
29 import static org.asamk.signal.manager.config.ServiceConfig.MAXIMUM_ONE_OFF_REQUEST_SIZE;
30
31 public class RecipientHelper {
32
33 private static final Logger logger = LoggerFactory.getLogger(RecipientHelper.class);
34
35 private final SignalAccount account;
36 private final SignalDependencies dependencies;
37
38 public RecipientHelper(final Context context) {
39 this.account = context.getAccount();
40 this.dependencies = context.getDependencies();
41 }
42
43 public SignalServiceAddress resolveSignalServiceAddress(RecipientId recipientId) {
44 final var address = account.getRecipientAddressResolver().resolveRecipientAddress(recipientId);
45 if (address.number().isEmpty() || address.serviceId().isPresent()) {
46 return address.toSignalServiceAddress();
47 }
48
49 // Address in recipient store doesn't have a uuid, this shouldn't happen
50 // Try to retrieve the uuid from the server
51 final var number = address.number().get();
52 final ServiceId serviceId;
53 try {
54 serviceId = getRegisteredUserByNumber(number);
55 } catch (UnregisteredRecipientException e) {
56 logger.warn("Failed to get uuid for e164 number: {}", number);
57 // Return SignalServiceAddress with unknown UUID
58 return address.toSignalServiceAddress();
59 } catch (IOException e) {
60 logger.warn("Failed to get uuid for e164 number: {}", number, e);
61 // Return SignalServiceAddress with unknown UUID
62 return address.toSignalServiceAddress();
63 }
64 return account.getRecipientAddressResolver()
65 .resolveRecipientAddress(account.getRecipientResolver().resolveRecipient(serviceId))
66 .toSignalServiceAddress();
67 }
68
69 public Set<RecipientId> resolveRecipients(Collection<RecipientIdentifier.Single> recipients) throws UnregisteredRecipientException {
70 final var recipientIds = new HashSet<RecipientId>(recipients.size());
71 for (var number : recipients) {
72 final var recipientId = resolveRecipient(number);
73 recipientIds.add(recipientId);
74 }
75 return recipientIds;
76 }
77
78 public RecipientId resolveRecipient(final RecipientIdentifier.Single recipient) throws UnregisteredRecipientException {
79 if (recipient instanceof RecipientIdentifier.Uuid uuidRecipient) {
80 return account.getRecipientResolver().resolveRecipient(ACI.from(uuidRecipient.uuid()));
81 } else if (recipient instanceof RecipientIdentifier.Pni pniRecipient) {
82 return account.getRecipientResolver().resolveRecipient(PNI.parseOrThrow(pniRecipient.pni()));
83 } else if (recipient instanceof RecipientIdentifier.Number numberRecipient) {
84 final var number = numberRecipient.number();
85 return account.getRecipientStore().resolveRecipientByNumber(number, () -> {
86 try {
87 return getRegisteredUserByNumber(number);
88 } catch (Exception e) {
89 return null;
90 }
91 });
92 } else if (recipient instanceof RecipientIdentifier.Username usernameRecipient) {
93 var username = usernameRecipient.username();
94 return resolveRecipientByUsernameOrLink(username, false);
95 }
96 throw new AssertionError("Unexpected RecipientIdentifier: " + recipient);
97 }
98
99 public RecipientId resolveRecipientByUsernameOrLink(
100 String username, boolean forceRefresh
101 ) throws UnregisteredRecipientException {
102 final Username finalUsername;
103 try {
104 finalUsername = getUsernameFromUsernameOrLink(username);
105 } catch (IOException | BaseUsernameException e) {
106 throw new RuntimeException(e);
107 }
108 if (forceRefresh) {
109 try {
110 final var aci = dependencies.getAccountManager().getAciByUsername(finalUsername);
111 return account.getRecipientStore().resolveRecipientTrusted(aci, finalUsername.getUsername());
112 } catch (IOException e) {
113 throw new UnregisteredRecipientException(new org.asamk.signal.manager.api.RecipientAddress(null,
114 null,
115 null,
116 username));
117 }
118 }
119 return account.getRecipientStore().resolveRecipientByUsername(finalUsername.getUsername(), () -> {
120 try {
121 return dependencies.getAccountManager().getAciByUsername(finalUsername);
122 } catch (Exception e) {
123 return null;
124 }
125 });
126 }
127
128 private Username getUsernameFromUsernameOrLink(String username) throws BaseUsernameException, IOException {
129 try {
130 final var usernameLinkUrl = UsernameLinkUrl.fromUri(username);
131 final var components = usernameLinkUrl.getComponents();
132 final var encryptedUsername = dependencies.getAccountManager()
133 .getEncryptedUsernameFromLinkServerId(components.getServerId());
134 final var link = new Username.UsernameLink(components.getEntropy(), encryptedUsername);
135
136 return Username.fromLink(link);
137 } catch (UsernameLinkUrl.InvalidUsernameLinkException e) {
138 return new Username(username);
139 }
140 }
141
142 public Optional<RecipientId> resolveRecipientOptional(final RecipientIdentifier.Single recipient) {
143 try {
144 return Optional.of(resolveRecipient(recipient));
145 } catch (UnregisteredRecipientException e) {
146 if (recipient instanceof RecipientIdentifier.Number r) {
147 return account.getRecipientStore().resolveRecipientByNumberOptional(r.number());
148 } else {
149 return Optional.empty();
150 }
151 }
152 }
153
154 public void refreshUsers() throws IOException {
155 getRegisteredUsers(account.getRecipientStore().getAllNumbers(), false);
156 }
157
158 public RecipientId refreshRegisteredUser(RecipientId recipientId) throws IOException, UnregisteredRecipientException {
159 final var address = resolveSignalServiceAddress(recipientId);
160 if (address.getNumber().isEmpty()) {
161 return recipientId;
162 }
163 final var number = address.getNumber().get();
164 final var serviceId = getRegisteredUserByNumber(number);
165 return account.getRecipientTrustedResolver()
166 .resolveRecipientTrusted(new SignalServiceAddress(serviceId, number));
167 }
168
169 public Map<String, RegisteredUser> getRegisteredUsers(
170 final Set<String> numbers
171 ) throws IOException {
172 if (numbers.size() > MAXIMUM_ONE_OFF_REQUEST_SIZE) {
173 final var allNumbers = new HashSet<>(account.getRecipientStore().getAllNumbers()) {{
174 addAll(numbers);
175 }};
176 return getRegisteredUsers(allNumbers, false);
177 } else {
178 return getRegisteredUsers(numbers, true);
179 }
180 }
181
182 private Map<String, RegisteredUser> getRegisteredUsers(
183 final Set<String> numbers, final boolean isPartialRefresh
184 ) throws IOException {
185 Map<String, RegisteredUser> registeredUsers = getRegisteredUsersV2(numbers, isPartialRefresh);
186
187 // Store numbers as recipients, so we have the number/uuid association
188 registeredUsers.forEach((number, u) -> account.getRecipientTrustedResolver()
189 .resolveRecipientTrusted(u.aci, u.pni, Optional.of(number)));
190
191 final var unregisteredUsers = new HashSet<>(numbers);
192 unregisteredUsers.removeAll(registeredUsers.keySet());
193 account.getRecipientStore().markUndiscoverablePossiblyUnregistered(unregisteredUsers);
194 account.getRecipientStore().markDiscoverable(registeredUsers.keySet());
195
196 return registeredUsers;
197 }
198
199 private ServiceId getRegisteredUserByNumber(final String number) throws IOException, UnregisteredRecipientException {
200 final Map<String, RegisteredUser> aciMap;
201 try {
202 aciMap = getRegisteredUsers(Set.of(number), true);
203 } catch (NumberFormatException e) {
204 throw new UnregisteredRecipientException(new org.asamk.signal.manager.api.RecipientAddress(number));
205 }
206 final var user = aciMap.get(number);
207 if (user == null) {
208 throw new UnregisteredRecipientException(new org.asamk.signal.manager.api.RecipientAddress(number));
209 }
210 return user.getServiceId();
211 }
212
213 private Map<String, RegisteredUser> getRegisteredUsersV2(
214 final Set<String> numbers, boolean isPartialRefresh
215 ) throws IOException {
216 final var previousNumbers = isPartialRefresh ? Set.<String>of() : account.getCdsiStore().getAllNumbers();
217 final var newNumbers = new HashSet<>(numbers) {{
218 removeAll(previousNumbers);
219 }};
220 if (newNumbers.isEmpty() && previousNumbers.isEmpty()) {
221 logger.debug("No new numbers to query.");
222 return Map.of();
223 }
224 logger.trace("Querying CDSI for {} new numbers ({} previous), isPartialRefresh={}",
225 newNumbers.size(),
226 previousNumbers.size(),
227 isPartialRefresh);
228 final var token = previousNumbers.isEmpty()
229 ? Optional.<byte[]>empty()
230 : Optional.ofNullable(account.getCdsiToken());
231
232 final CdsiV2Service.Response response;
233 try {
234 response = dependencies.getAccountManager()
235 .getRegisteredUsersWithCdsi(token.isEmpty() ? Set.of() : previousNumbers,
236 newNumbers,
237 account.getRecipientStore().getServiceIdToProfileKeyMap(),
238 token,
239 dependencies.getServiceEnvironmentConfig().cdsiMrenclave(),
240 null,
241 dependencies.getLibSignalNetwork(),
242 newToken -> {
243 if (isPartialRefresh) {
244 account.getCdsiStore().updateAfterPartialCdsQuery(newNumbers);
245 // Not storing newToken for partial refresh
246 } else {
247 final var fullNumbers = new HashSet<>(previousNumbers) {{
248 addAll(newNumbers);
249 }};
250 final var seenNumbers = new HashSet<>(numbers) {{
251 addAll(newNumbers);
252 }};
253 account.getCdsiStore().updateAfterFullCdsQuery(fullNumbers, seenNumbers);
254 account.setCdsiToken(newToken);
255 account.setLastRecipientsRefresh(System.currentTimeMillis());
256 }
257 });
258 } catch (CdsiInvalidTokenException | CdsiInvalidArgumentException e) {
259 account.setCdsiToken(null);
260 account.getCdsiStore().clearAll();
261 throw e;
262 } catch (NumberFormatException e) {
263 throw new IOException(e);
264 }
265 logger.debug("CDSI request successful, quota used by this request: {}", response.getQuotaUsedDebugOnly());
266
267 final var registeredUsers = new HashMap<String, RegisteredUser>();
268 response.getResults()
269 .forEach((key, value) -> registeredUsers.put(key,
270 new RegisteredUser(value.getAci(), Optional.of(value.getPni()))));
271 return registeredUsers;
272 }
273
274 public record RegisteredUser(Optional<ACI> aci, Optional<PNI> pni) {
275
276 public RegisteredUser {
277 aci = aci.isPresent() && aci.get().isUnknown() ? Optional.empty() : aci;
278 pni = pni.isPresent() && pni.get().isUnknown() ? Optional.empty() : pni;
279 if (aci.isEmpty() && pni.isEmpty()) {
280 throw new AssertionError("Must have either a ACI or PNI!");
281 }
282 }
283
284 public ServiceId getServiceId() {
285 return aci.map(a -> (ServiceId) a).or(this::pni).orElse(null);
286 }
287 }
288 }