1 package org
.asamk
.signal
.manager
.syncStorage
;
3 import org
.asamk
.signal
.manager
.api
.Contact
;
4 import org
.asamk
.signal
.manager
.api
.Profile
;
5 import org
.asamk
.signal
.manager
.internal
.JobExecutor
;
6 import org
.asamk
.signal
.manager
.jobs
.DownloadProfileJob
;
7 import org
.asamk
.signal
.manager
.storage
.SignalAccount
;
8 import org
.asamk
.signal
.manager
.storage
.recipients
.RecipientAddress
;
9 import org
.asamk
.signal
.manager
.util
.KeyUtils
;
10 import org
.signal
.libsignal
.protocol
.IdentityKey
;
11 import org
.signal
.libsignal
.protocol
.InvalidKeyException
;
12 import org
.signal
.libsignal
.zkgroup
.InvalidInputException
;
13 import org
.signal
.libsignal
.zkgroup
.profiles
.ProfileKey
;
14 import org
.slf4j
.Logger
;
15 import org
.slf4j
.LoggerFactory
;
16 import org
.whispersystems
.signalservice
.api
.push
.ServiceId
.ACI
;
17 import org
.whispersystems
.signalservice
.api
.push
.ServiceId
.PNI
;
18 import org
.whispersystems
.signalservice
.api
.storage
.SignalContactRecord
;
19 import org
.whispersystems
.signalservice
.api
.util
.OptionalUtil
;
20 import org
.whispersystems
.signalservice
.internal
.storage
.protos
.ContactRecord
.IdentityState
;
22 import java
.sql
.Connection
;
23 import java
.sql
.SQLException
;
24 import java
.util
.Arrays
;
25 import java
.util
.Objects
;
26 import java
.util
.Optional
;
27 import java
.util
.regex
.Pattern
;
29 public class ContactRecordProcessor
extends DefaultStorageRecordProcessor
<SignalContactRecord
> {
31 private static final Logger logger
= LoggerFactory
.getLogger(ContactRecordProcessor
.class);
33 private static final Pattern E164_PATTERN
= Pattern
.compile("^\\+[1-9]\\d{0,18}$");
35 private final ACI selfAci
;
36 private final PNI selfPni
;
37 private final String selfNumber
;
38 private final SignalAccount account
;
39 private final Connection connection
;
40 private final JobExecutor jobExecutor
;
42 public ContactRecordProcessor(SignalAccount account
, Connection connection
, final JobExecutor jobExecutor
) {
43 this.account
= account
;
44 this.connection
= connection
;
45 this.jobExecutor
= jobExecutor
;
46 this.selfAci
= account
.getAci();
47 this.selfPni
= account
.getPni();
48 this.selfNumber
= account
.getNumber();
53 * - You can't have a contact record without an ACI or PNI.
54 * - You can't have a contact record for yourself. That should be an account record.
57 protected boolean isInvalid(SignalContactRecord remote
) {
58 boolean hasAci
= remote
.getAci().isPresent() && remote
.getAci().get().isValid();
59 boolean hasPni
= remote
.getPni().isPresent() && remote
.getPni().get().isValid();
61 if (!hasAci
&& !hasPni
) {
62 logger
.debug("Found a ContactRecord with neither an ACI nor a PNI -- marking as invalid.");
64 } else if (selfAci
!= null && selfAci
.equals(remote
.getAci().orElse(null)) || (
65 selfPni
!= null && selfPni
.equals(remote
.getPni().orElse(null))
66 ) || (selfNumber
!= null && selfNumber
.equals(remote
.getNumber().orElse(null)))) {
67 logger
.debug("Found a ContactRecord for ourselves -- marking as invalid.");
69 } else if (remote
.getNumber().isPresent() && !isValidE164(remote
.getNumber().get())) {
70 logger
.debug("Found a record with an invalid E164. Marking as invalid.");
78 protected Optional
<SignalContactRecord
> getMatching(SignalContactRecord remote
) throws SQLException
{
79 final var address
= getRecipientAddress(remote
);
80 final var recipientId
= account
.getRecipientStore().resolveRecipient(connection
, address
);
81 final var recipient
= account
.getRecipientStore().getRecipient(connection
, recipientId
);
83 final var identifier
= recipient
.getAddress().getIdentifier();
84 final var identity
= account
.getIdentityKeyStore().getIdentityInfo(connection
, identifier
);
85 final var storageId
= account
.getRecipientStore().getStorageId(connection
, recipientId
);
87 return Optional
.of(StorageSyncModels
.localToRemoteRecord(recipient
, identity
, storageId
.getRaw())
93 protected SignalContactRecord
merge(
94 SignalContactRecord remote
, SignalContactRecord local
96 String profileGivenName
;
97 String profileFamilyName
;
98 if (remote
.getProfileGivenName().isPresent() || remote
.getProfileFamilyName().isPresent()) {
99 profileGivenName
= remote
.getProfileGivenName().orElse("");
100 profileFamilyName
= remote
.getProfileFamilyName().orElse("");
102 profileGivenName
= local
.getProfileGivenName().orElse("");
103 profileFamilyName
= local
.getProfileFamilyName().orElse("");
106 IdentityState identityState
;
108 if ((remote
.getIdentityState() != local
.getIdentityState() && remote
.getIdentityKey().isPresent())
109 || (remote
.getIdentityKey().isPresent() && local
.getIdentityKey().isEmpty())) {
110 identityState
= remote
.getIdentityState();
111 identityKey
= remote
.getIdentityKey().get();
113 identityState
= local
.getIdentityState();
114 identityKey
= local
.getIdentityKey().orElse(null);
117 if (local
.getAci().isPresent() && identityKey
!= null && remote
.getIdentityKey().isPresent() && !Arrays
.equals(
119 remote
.getIdentityKey().get())) {
120 logger
.debug("The local and remote identity keys do not match for {}. Enqueueing a profile fetch.",
121 local
.getAci().orElse(null));
122 final var address
= getRecipientAddress(local
);
123 jobExecutor
.enqueueJob(new DownloadProfileJob(address
));
126 final var e164sMatchButPnisDont
= local
.getNumber().isPresent()
129 .equals(remote
.getNumber().orElse(null))
130 && local
.getPni().isPresent()
131 && remote
.getPni().isPresent()
132 && !local
.getPni().get().equals(remote
.getPni().get());
134 final var pnisMatchButE164sDont
= local
.getPni().isPresent()
137 .equals(remote
.getPni().orElse(null))
138 && local
.getNumber().isPresent()
139 && remote
.getNumber().isPresent()
140 && !local
.getNumber().get().equals(remote
.getNumber().get());
144 if (e164sMatchButPnisDont
) {
145 logger
.debug("Matching E164s, but the PNIs differ! Trusting our local pair.");
146 // TODO [pnp] Schedule CDS fetch?
147 pni
= local
.getPni().get();
148 e164
= local
.getNumber().get();
149 } else if (pnisMatchButE164sDont
) {
150 logger
.debug("Matching PNIs, but the E164s differ! Trusting our local pair.");
151 // TODO [pnp] Schedule CDS fetch?
152 pni
= local
.getPni().get();
153 e164
= local
.getNumber().get();
155 pni
= OptionalUtil
.or(remote
.getPni(), local
.getPni()).orElse(null);
156 e164
= OptionalUtil
.or(remote
.getNumber(), local
.getNumber()).orElse(null);
159 final var unknownFields
= remote
.serializeUnknownFields();
160 final var aci
= local
.getAci().isEmpty() ? remote
.getAci().orElse(null) : local
.getAci().get();
161 final var profileKey
= OptionalUtil
.or(remote
.getProfileKey(), local
.getProfileKey()).orElse(null);
162 final var username
= OptionalUtil
.or(remote
.getUsername(), local
.getUsername()).orElse("");
163 final var blocked
= remote
.isBlocked();
164 final var profileSharing
= remote
.isProfileSharingEnabled();
165 final var archived
= remote
.isArchived();
166 final var forcedUnread
= remote
.isForcedUnread();
167 final var muteUntil
= remote
.getMuteUntil();
168 final var hideStory
= remote
.shouldHideStory();
169 final var unregisteredTimestamp
= remote
.getUnregisteredTimestamp();
170 final var hidden
= remote
.isHidden();
171 final var systemGivenName
= account
.isPrimaryDevice()
172 ? local
.getSystemGivenName().orElse("")
173 : remote
.getSystemGivenName().orElse("");
174 final var systemFamilyName
= account
.isPrimaryDevice()
175 ? local
.getSystemFamilyName().orElse("")
176 : remote
.getSystemFamilyName().orElse("");
177 final var systemNickname
= remote
.getSystemNickname().orElse("");
179 final var mergedBuilder
= new SignalContactRecord
.Builder(remote
.getId().getRaw(), aci
, unknownFields
).setE164(
182 .setProfileGivenName(profileGivenName
)
183 .setProfileFamilyName(profileFamilyName
)
184 .setSystemGivenName(systemGivenName
)
185 .setSystemFamilyName(systemFamilyName
)
186 .setSystemNickname(systemNickname
)
187 .setProfileKey(profileKey
)
188 .setUsername(username
)
189 .setIdentityState(identityState
)
190 .setIdentityKey(identityKey
)
192 .setProfileSharingEnabled(profileSharing
)
193 .setArchived(archived
)
194 .setForcedUnread(forcedUnread
)
195 .setMuteUntil(muteUntil
)
196 .setHideStory(hideStory
)
197 .setUnregisteredTimestamp(unregisteredTimestamp
)
199 final var merged
= mergedBuilder
.build();
201 final var matchesRemote
= doProtosMatch(merged
, remote
);
206 final var matchesLocal
= doProtosMatch(merged
, local
);
211 return mergedBuilder
.setId(KeyUtils
.createRawStorageId()).build();
215 protected void insertLocal(SignalContactRecord
record) throws SQLException
{
216 StorageRecordUpdate
<SignalContactRecord
> update
= new StorageRecordUpdate
<>(null, record);
221 protected void updateLocal(StorageRecordUpdate
<SignalContactRecord
> update
) throws SQLException
{
222 final var contactRecord
= update
.newRecord();
223 final var address
= getRecipientAddress(contactRecord
);
224 final var recipientId
= account
.getRecipientStore().resolveRecipientTrusted(connection
, address
);
225 final var recipient
= account
.getRecipientStore().getRecipient(connection
, recipientId
);
227 final var contact
= recipient
.getContact();
228 final var blocked
= contact
!= null && contact
.isBlocked();
229 final var profileShared
= contact
!= null && contact
.isProfileSharingEnabled();
230 final var archived
= contact
!= null && contact
.isArchived();
231 final var hidden
= contact
!= null && contact
.isHidden();
232 final var contactGivenName
= contact
== null ?
null : contact
.givenName();
233 final var contactFamilyName
= contact
== null ?
null : contact
.familyName();
234 if (blocked
!= contactRecord
.isBlocked()
235 || profileShared
!= contactRecord
.isProfileSharingEnabled()
236 || archived
!= contactRecord
.isArchived()
237 || hidden
!= contactRecord
.isHidden()
239 contactRecord
.getSystemGivenName().isPresent() && !contactRecord
.getSystemGivenName()
241 .equals(contactGivenName
)
244 contactRecord
.getSystemFamilyName().isPresent() && !contactRecord
.getSystemFamilyName()
246 .equals(contactFamilyName
)
248 logger
.debug("Storing new or updated contact {}", recipientId
);
249 final var contactBuilder
= contact
== null ? Contact
.newBuilder() : Contact
.newBuilder(contact
);
250 final var newContact
= contactBuilder
.withIsBlocked(contactRecord
.isBlocked())
251 .withIsProfileSharingEnabled(contactRecord
.isProfileSharingEnabled())
252 .withIsArchived(contactRecord
.isArchived())
253 .withIsHidden(contactRecord
.isHidden());
254 if (contactRecord
.getSystemGivenName().isPresent() || contactRecord
.getSystemFamilyName().isPresent()) {
255 newContact
.withGivenName(contactRecord
.getSystemGivenName().orElse(null))
256 .withFamilyName(contactRecord
.getSystemFamilyName().orElse(null));
258 account
.getRecipientStore().storeContact(connection
, recipientId
, newContact
.build());
261 final var profile
= recipient
.getProfile();
262 final var profileGivenName
= profile
== null ?
null : profile
.getGivenName();
263 final var profileFamilyName
= profile
== null ?
null : profile
.getFamilyName();
265 contactRecord
.getProfileGivenName().isPresent() && !contactRecord
.getProfileGivenName()
267 .equals(profileGivenName
)
269 contactRecord
.getProfileFamilyName().isPresent() && !contactRecord
.getProfileFamilyName()
271 .equals(profileFamilyName
)
273 final var profileBuilder
= profile
== null ? Profile
.newBuilder() : Profile
.newBuilder(profile
);
274 final var newProfile
= profileBuilder
.withGivenName(contactRecord
.getProfileGivenName().orElse(null))
275 .withFamilyName(contactRecord
.getProfileFamilyName().orElse(null))
277 account
.getRecipientStore().storeProfile(connection
, recipientId
, newProfile
);
279 if (contactRecord
.getProfileKey().isPresent()) {
281 logger
.trace("Storing profile key {}", recipientId
);
282 final var profileKey
= new ProfileKey(contactRecord
.getProfileKey().get());
283 account
.getRecipientStore().storeProfileKey(connection
, recipientId
, profileKey
);
284 } catch (InvalidInputException e
) {
285 logger
.warn("Received invalid contact profile key from storage");
288 if (contactRecord
.getIdentityKey().isPresent() && contactRecord
.getAci().orElse(null) != null) {
290 logger
.trace("Storing identity key {}", recipientId
);
291 final var identityKey
= new IdentityKey(contactRecord
.getIdentityKey().get());
292 account
.getIdentityKeyStore()
293 .saveIdentity(connection
, contactRecord
.getAci().orElse(null), identityKey
);
295 final var trustLevel
= StorageSyncModels
.remoteToLocal(contactRecord
.getIdentityState());
296 if (trustLevel
!= null) {
297 account
.getIdentityKeyStore()
298 .setIdentityTrustLevel(connection
,
299 contactRecord
.getAci().orElse(null),
303 } catch (InvalidKeyException e
) {
304 logger
.warn("Received invalid contact identity key from storage");
307 account
.getRecipientStore()
308 .storeStorageRecord(connection
, recipientId
, contactRecord
.getId(), contactRecord
.toProto().encode());
311 private static RecipientAddress
getRecipientAddress(final SignalContactRecord contactRecord
) {
312 return new RecipientAddress(contactRecord
.getAci().orElse(null),
313 contactRecord
.getPni().orElse(null),
314 contactRecord
.getNumber().orElse(null),
315 contactRecord
.getUsername().orElse(null));
319 public int compare(SignalContactRecord lhs
, SignalContactRecord rhs
) {
320 if ((lhs
.getAci().isPresent() && Objects
.equals(lhs
.getAci(), rhs
.getAci())) || (
321 lhs
.getNumber().isPresent() && Objects
.equals(lhs
.getNumber(), rhs
.getNumber())
322 ) || (lhs
.getPni().isPresent() && Objects
.equals(lhs
.getPni(), rhs
.getPni()))) {
329 private static boolean isValidE164(String value
) {
330 return E164_PATTERN
.matcher(value
).matches();
333 private static boolean doProtosMatch(SignalContactRecord merged
, SignalContactRecord other
) {
334 return Arrays
.equals(merged
.toProto().encode(), other
.toProto().encode());