1 package org
.asamk
.signal
.manager
.helper
;
3 import org
.asamk
.signal
.manager
.api
.RecipientIdentifier
;
4 import org
.asamk
.signal
.manager
.api
.UnregisteredRecipientException
;
5 import org
.asamk
.signal
.manager
.config
.ServiceEnvironmentConfig
;
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
.CdsiInvalidTokenException
;
18 import org
.whispersystems
.signalservice
.api
.services
.CdsiV2Service
;
19 import org
.whispersystems
.util
.Base64UrlSafe
;
21 import java
.io
.IOException
;
22 import java
.util
.Collection
;
23 import java
.util
.HashMap
;
24 import java
.util
.HashSet
;
26 import java
.util
.Optional
;
29 public class RecipientHelper
{
31 private final static Logger logger
= LoggerFactory
.getLogger(RecipientHelper
.class);
33 private final SignalAccount account
;
34 private final SignalDependencies dependencies
;
35 private final ServiceEnvironmentConfig serviceEnvironmentConfig
;
37 public RecipientHelper(final Context context
) {
38 this.account
= context
.getAccount();
39 this.dependencies
= context
.getDependencies();
40 this.serviceEnvironmentConfig
= dependencies
.getServiceEnvironmentConfig();
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();
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
;
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();
64 return account
.getRecipientAddressResolver()
65 .resolveRecipientAddress(account
.getRecipientResolver().resolveRecipient(serviceId
))
66 .toSignalServiceAddress();
69 public RecipientId
resolveRecipient(final SignalServiceAddress address
) {
70 return account
.getRecipientResolver().resolveRecipient(address
);
73 public Set
<RecipientId
> resolveRecipients(Collection
<RecipientIdentifier
.Single
> recipients
) throws UnregisteredRecipientException
{
74 final var recipientIds
= new HashSet
<RecipientId
>(recipients
.size());
75 for (var number
: recipients
) {
76 final var recipientId
= resolveRecipient(number
);
77 recipientIds
.add(recipientId
);
82 public RecipientId
resolveRecipient(final RecipientIdentifier
.Single recipient
) throws UnregisteredRecipientException
{
83 if (recipient
instanceof RecipientIdentifier
.Uuid uuidRecipient
) {
84 return account
.getRecipientResolver().resolveRecipient(ACI
.from(uuidRecipient
.uuid()));
85 } else if (recipient
instanceof RecipientIdentifier
.Number numberRecipient
) {
86 final var number
= numberRecipient
.number();
87 return account
.getRecipientStore().resolveRecipientByNumber(number
, () -> {
89 return getRegisteredUserByNumber(number
);
90 } catch (Exception e
) {
94 } else if (recipient
instanceof RecipientIdentifier
.Username usernameRecipient
) {
95 final var username
= usernameRecipient
.username();
96 return account
.getRecipientStore().resolveRecipientByUsername(username
, () -> {
98 return getRegisteredUserByUsername(username
);
99 } catch (Exception e
) {
104 throw new AssertionError("Unexpected RecipientIdentifier: " + recipient
);
107 public Optional
<RecipientId
> resolveRecipientOptional(final RecipientIdentifier
.Single recipient
) {
109 return Optional
.of(resolveRecipient(recipient
));
110 } catch (UnregisteredRecipientException e
) {
111 if (recipient
instanceof RecipientIdentifier
.Number r
) {
112 return account
.getRecipientStore().resolveRecipientByNumberOptional(r
.number());
114 return Optional
.empty();
119 public void refreshUsers() throws IOException
{
120 getRegisteredUsers(account
.getRecipientStore().getAllNumbers(), false);
123 public RecipientId
refreshRegisteredUser(RecipientId recipientId
) throws IOException
, UnregisteredRecipientException
{
124 final var address
= resolveSignalServiceAddress(recipientId
);
125 if (address
.getNumber().isEmpty()) {
128 final var number
= address
.getNumber().get();
129 final var serviceId
= getRegisteredUserByNumber(number
);
130 return account
.getRecipientTrustedResolver()
131 .resolveRecipientTrusted(new SignalServiceAddress(serviceId
, number
));
134 public Map
<String
, RegisteredUser
> getRegisteredUsers(
135 final Set
<String
> numbers
136 ) throws IOException
{
137 return getRegisteredUsers(numbers
, true);
140 private Map
<String
, RegisteredUser
> getRegisteredUsers(
141 final Set
<String
> numbers
, final boolean isPartialRefresh
142 ) throws IOException
{
143 Map
<String
, RegisteredUser
> registeredUsers
= getRegisteredUsersV2(numbers
, isPartialRefresh
, true);
145 // Store numbers as recipients, so we have the number/uuid association
146 registeredUsers
.forEach((number
, u
) -> account
.getRecipientTrustedResolver()
147 .resolveRecipientTrusted(u
.aci
, u
.pni
, Optional
.of(number
)));
149 return registeredUsers
;
152 private ServiceId
getRegisteredUserByNumber(final String number
) throws IOException
, UnregisteredRecipientException
{
153 final Map
<String
, RegisteredUser
> aciMap
;
155 aciMap
= getRegisteredUsers(Set
.of(number
), true);
156 } catch (NumberFormatException e
) {
157 throw new UnregisteredRecipientException(new org
.asamk
.signal
.manager
.api
.RecipientAddress(null, number
));
159 final var user
= aciMap
.get(number
);
161 throw new UnregisteredRecipientException(new org
.asamk
.signal
.manager
.api
.RecipientAddress(null, number
));
163 return user
.getServiceId();
166 private Map
<String
, RegisteredUser
> getRegisteredUsersV2(
167 final Set
<String
> numbers
, boolean isPartialRefresh
, boolean useCompat
168 ) throws IOException
{
169 final var previousNumbers
= isPartialRefresh ? Set
.<String
>of() : account
.getCdsiStore().getAllNumbers();
170 final var newNumbers
= new HashSet
<>(numbers
) {{
171 removeAll(previousNumbers
);
173 if (newNumbers
.isEmpty() && previousNumbers
.isEmpty()) {
174 logger
.debug("No new numbers to query.");
177 logger
.trace("Querying CDSI for {} new numbers ({} previous)", newNumbers
.size(), previousNumbers
.size());
178 final var token
= previousNumbers
.isEmpty()
179 ? Optional
.<byte[]>empty()
180 : Optional
.ofNullable(account
.getCdsiToken());
182 final CdsiV2Service
.Response response
;
184 response
= dependencies
.getAccountManager()
185 .getRegisteredUsersWithCdsi(previousNumbers
,
187 account
.getRecipientStore().getServiceIdToProfileKeyMap(),
190 serviceEnvironmentConfig
.cdsiMrenclave(),
193 if (isPartialRefresh
) {
194 account
.getCdsiStore().updateAfterPartialCdsQuery(newNumbers
);
195 // Not storing newToken for partial refresh
197 final var fullNumbers
= new HashSet
<>(previousNumbers
) {{
200 final var seenNumbers
= new HashSet
<>(numbers
) {{
203 account
.getCdsiStore().updateAfterFullCdsQuery(fullNumbers
, seenNumbers
);
204 account
.setCdsiToken(newToken
);
207 } catch (CdsiInvalidTokenException e
) {
208 account
.setCdsiToken(null);
209 account
.getCdsiStore().clearAll();
211 } catch (NumberFormatException e
) {
212 throw new IOException(e
);
214 logger
.debug("CDSI request successful, quota used by this request: {}", response
.getQuotaUsedDebugOnly());
216 final var registeredUsers
= new HashMap
<String
, RegisteredUser
>();
217 response
.getResults()
218 .forEach((key
, value
) -> registeredUsers
.put(key
,
219 new RegisteredUser(value
.getAci(), Optional
.of(value
.getPni()))));
220 return registeredUsers
;
223 private ACI
getRegisteredUserByUsername(String username
) throws IOException
, BaseUsernameException
{
224 return dependencies
.getAccountManager()
225 .getAciByUsernameHash(Base64UrlSafe
.encodeBytesWithoutPadding(new Username(username
).getHash()));
228 public record RegisteredUser(Optional
<ACI
> aci
, Optional
<PNI
> pni
) {
230 public RegisteredUser
{
231 aci
= aci
.isPresent() && aci
.get().isUnknown() ? Optional
.empty() : aci
;
232 pni
= pni
.isPresent() && pni
.get().isUnknown() ? Optional
.empty() : pni
;
233 if (aci
.isEmpty() && pni
.isEmpty()) {
234 throw new AssertionError("Must have either a ACI or PNI!");
238 public ServiceId
getServiceId() {
239 return aci
.map(a
-> (ServiceId
) a
).or(this::pni
).orElse(null);