]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/helper/RecipientHelper.java
Update libsignal-service
[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.config.ServiceEnvironmentConfig;
7 import org.asamk.signal.manager.internal.SignalDependencies;
8 import org.asamk.signal.manager.storage.SignalAccount;
9 import org.asamk.signal.manager.storage.recipients.RecipientId;
10 import org.signal.libsignal.usernames.BaseUsernameException;
11 import org.signal.libsignal.usernames.Username;
12 import org.slf4j.Logger;
13 import org.slf4j.LoggerFactory;
14 import org.whispersystems.signalservice.api.push.ServiceId;
15 import org.whispersystems.signalservice.api.push.ServiceId.ACI;
16 import org.whispersystems.signalservice.api.push.ServiceId.PNI;
17 import org.whispersystems.signalservice.api.push.SignalServiceAddress;
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 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.serviceId().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 ServiceId serviceId;
55 try {
56 serviceId = getRegisteredUserByNumber(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(serviceId))
68 .toSignalServiceAddress();
69 }
70
71 public Set<RecipientId> resolveRecipients(Collection<RecipientIdentifier.Single> recipients) throws UnregisteredRecipientException {
72 final var recipientIds = new HashSet<RecipientId>(recipients.size());
73 for (var number : recipients) {
74 final var recipientId = resolveRecipient(number);
75 recipientIds.add(recipientId);
76 }
77 return recipientIds;
78 }
79
80 public RecipientId resolveRecipient(final RecipientIdentifier.Single recipient) throws UnregisteredRecipientException {
81 if (recipient instanceof RecipientIdentifier.Uuid uuidRecipient) {
82 return account.getRecipientResolver().resolveRecipient(ACI.from(uuidRecipient.uuid()));
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 try {
95 UsernameLinkUrl usernameLinkUrl = UsernameLinkUrl.fromUri(username);
96 final var components = usernameLinkUrl.getComponents();
97 final var encryptedUsername = dependencies.getAccountManager()
98 .getEncryptedUsernameFromLinkServerId(components.getServerId());
99 final var link = new Username.UsernameLink(components.getEntropy(), encryptedUsername);
100
101 username = Username.fromLink(link).getUsername();
102 } catch (UsernameLinkUrl.InvalidUsernameLinkException e) {
103 } catch (IOException | BaseUsernameException e) {
104 throw new RuntimeException(e);
105 }
106 final String finalUsername = username;
107 return account.getRecipientStore().resolveRecipientByUsername(finalUsername, () -> {
108 try {
109 return getRegisteredUserByUsername(finalUsername);
110 } catch (Exception e) {
111 return null;
112 }
113 });
114 }
115 throw new AssertionError("Unexpected RecipientIdentifier: " + recipient);
116 }
117
118 public Optional<RecipientId> resolveRecipientOptional(final RecipientIdentifier.Single recipient) {
119 try {
120 return Optional.of(resolveRecipient(recipient));
121 } catch (UnregisteredRecipientException e) {
122 if (recipient instanceof RecipientIdentifier.Number r) {
123 return account.getRecipientStore().resolveRecipientByNumberOptional(r.number());
124 } else {
125 return Optional.empty();
126 }
127 }
128 }
129
130 public void refreshUsers() throws IOException {
131 getRegisteredUsers(account.getRecipientStore().getAllNumbers(), false);
132 }
133
134 public RecipientId refreshRegisteredUser(RecipientId recipientId) throws IOException, UnregisteredRecipientException {
135 final var address = resolveSignalServiceAddress(recipientId);
136 if (address.getNumber().isEmpty()) {
137 return recipientId;
138 }
139 final var number = address.getNumber().get();
140 final var serviceId = getRegisteredUserByNumber(number);
141 return account.getRecipientTrustedResolver()
142 .resolveRecipientTrusted(new SignalServiceAddress(serviceId, number));
143 }
144
145 public Map<String, RegisteredUser> getRegisteredUsers(
146 final Set<String> numbers
147 ) throws IOException {
148 if (numbers.size() > MAXIMUM_ONE_OFF_REQUEST_SIZE) {
149 final var allNumbers = new HashSet<>(account.getRecipientStore().getAllNumbers()) {{
150 addAll(numbers);
151 }};
152 return getRegisteredUsers(allNumbers, false);
153 } else {
154 return getRegisteredUsers(numbers, true);
155 }
156 }
157
158 private Map<String, RegisteredUser> getRegisteredUsers(
159 final Set<String> numbers, final boolean isPartialRefresh
160 ) throws IOException {
161 Map<String, RegisteredUser> registeredUsers = getRegisteredUsersV2(numbers, isPartialRefresh);
162
163 // Store numbers as recipients, so we have the number/uuid association
164 registeredUsers.forEach((number, u) -> account.getRecipientTrustedResolver()
165 .resolveRecipientTrusted(u.aci, u.pni, Optional.of(number)));
166
167 final var unregisteredUsers = new HashSet<>(numbers);
168 unregisteredUsers.removeAll(registeredUsers.keySet());
169 account.getRecipientStore().markUnregistered(unregisteredUsers);
170
171 return registeredUsers;
172 }
173
174 private ServiceId getRegisteredUserByNumber(final String number) throws IOException, UnregisteredRecipientException {
175 final Map<String, RegisteredUser> aciMap;
176 try {
177 aciMap = getRegisteredUsers(Set.of(number), true);
178 } catch (NumberFormatException e) {
179 throw new UnregisteredRecipientException(new org.asamk.signal.manager.api.RecipientAddress(null, number));
180 }
181 final var user = aciMap.get(number);
182 if (user == null) {
183 throw new UnregisteredRecipientException(new org.asamk.signal.manager.api.RecipientAddress(null, number));
184 }
185 return user.getServiceId();
186 }
187
188 private Map<String, RegisteredUser> getRegisteredUsersV2(
189 final Set<String> numbers, boolean isPartialRefresh
190 ) throws IOException {
191 final var previousNumbers = isPartialRefresh ? Set.<String>of() : account.getCdsiStore().getAllNumbers();
192 final var newNumbers = new HashSet<>(numbers) {{
193 removeAll(previousNumbers);
194 }};
195 if (newNumbers.isEmpty() && previousNumbers.isEmpty()) {
196 logger.debug("No new numbers to query.");
197 return Map.of();
198 }
199 logger.trace("Querying CDSI for {} new numbers ({} previous), isPartialRefresh={}",
200 newNumbers.size(),
201 previousNumbers.size(),
202 isPartialRefresh);
203 final var token = previousNumbers.isEmpty()
204 ? Optional.<byte[]>empty()
205 : Optional.ofNullable(account.getCdsiToken());
206
207 final CdsiV2Service.Response response;
208 try {
209 response = dependencies.getAccountManager()
210 .getRegisteredUsersWithCdsi(previousNumbers,
211 newNumbers,
212 account.getRecipientStore().getServiceIdToProfileKeyMap(),
213 token,
214 serviceEnvironmentConfig.cdsiMrenclave(),
215 null,
216 newToken -> {
217 if (isPartialRefresh) {
218 account.getCdsiStore().updateAfterPartialCdsQuery(newNumbers);
219 // Not storing newToken for partial refresh
220 } else {
221 final var fullNumbers = new HashSet<>(previousNumbers) {{
222 addAll(newNumbers);
223 }};
224 final var seenNumbers = new HashSet<>(numbers) {{
225 addAll(newNumbers);
226 }};
227 account.getCdsiStore().updateAfterFullCdsQuery(fullNumbers, seenNumbers);
228 account.setCdsiToken(newToken);
229 account.setLastRecipientsRefresh(System.currentTimeMillis());
230 }
231 });
232 } catch (CdsiInvalidTokenException e) {
233 account.setCdsiToken(null);
234 account.getCdsiStore().clearAll();
235 throw e;
236 } catch (NumberFormatException e) {
237 throw new IOException(e);
238 }
239 logger.debug("CDSI request successful, quota used by this request: {}", response.getQuotaUsedDebugOnly());
240
241 final var registeredUsers = new HashMap<String, RegisteredUser>();
242 response.getResults()
243 .forEach((key, value) -> registeredUsers.put(key,
244 new RegisteredUser(value.getAci(), Optional.of(value.getPni()))));
245 return registeredUsers;
246 }
247
248 private ACI getRegisteredUserByUsername(String username) throws IOException, BaseUsernameException {
249 return dependencies.getAccountManager().getAciByUsername(new Username(username));
250 }
251
252 public record RegisteredUser(Optional<ACI> aci, Optional<PNI> pni) {
253
254 public RegisteredUser {
255 aci = aci.isPresent() && aci.get().isUnknown() ? Optional.empty() : aci;
256 pni = pni.isPresent() && pni.get().isUnknown() ? Optional.empty() : pni;
257 if (aci.isEmpty() && pni.isEmpty()) {
258 throw new AssertionError("Must have either a ACI or PNI!");
259 }
260 }
261
262 public ServiceId getServiceId() {
263 return aci.map(a -> (ServiceId) a).or(this::pni).orElse(null);
264 }
265 }
266 }