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
.jobs
.RefreshRecipientsJob
;
8 import org
.asamk
.signal
.manager
.storage
.SignalAccount
;
9 import org
.asamk
.signal
.manager
.storage
.recipients
.RecipientAddress
;
10 import org
.asamk
.signal
.manager
.util
.KeyUtils
;
11 import org
.signal
.libsignal
.protocol
.IdentityKey
;
12 import org
.signal
.libsignal
.protocol
.InvalidKeyException
;
13 import org
.signal
.libsignal
.zkgroup
.InvalidInputException
;
14 import org
.signal
.libsignal
.zkgroup
.profiles
.ProfileKey
;
15 import org
.slf4j
.Logger
;
16 import org
.slf4j
.LoggerFactory
;
17 import org
.whispersystems
.signalservice
.api
.push
.ServiceId
.ACI
;
18 import org
.whispersystems
.signalservice
.api
.push
.ServiceId
.PNI
;
19 import org
.whispersystems
.signalservice
.api
.storage
.SignalContactRecord
;
20 import org
.whispersystems
.signalservice
.api
.util
.OptionalUtil
;
21 import org
.whispersystems
.signalservice
.internal
.storage
.protos
.ContactRecord
.IdentityState
;
23 import java
.sql
.Connection
;
24 import java
.sql
.SQLException
;
25 import java
.util
.Arrays
;
26 import java
.util
.Objects
;
27 import java
.util
.Optional
;
28 import java
.util
.regex
.Pattern
;
30 public class ContactRecordProcessor
extends DefaultStorageRecordProcessor
<SignalContactRecord
> {
32 private static final Logger logger
= LoggerFactory
.getLogger(ContactRecordProcessor
.class);
34 private static final Pattern E164_PATTERN
= Pattern
.compile("^\\+[1-9]\\d{0,18}$");
36 private final ACI selfAci
;
37 private final PNI selfPni
;
38 private final String selfNumber
;
39 private final SignalAccount account
;
40 private final Connection connection
;
41 private final JobExecutor jobExecutor
;
43 public ContactRecordProcessor(SignalAccount account
, Connection connection
, final JobExecutor jobExecutor
) {
44 this.account
= account
;
45 this.connection
= connection
;
46 this.jobExecutor
= jobExecutor
;
47 this.selfAci
= account
.getAci();
48 this.selfPni
= account
.getPni();
49 this.selfNumber
= account
.getNumber();
54 * - You can't have a contact record without an ACI or PNI.
55 * - You can't have a contact record for yourself. That should be an account record.
58 protected boolean isInvalid(SignalContactRecord remote
) {
59 boolean hasAci
= remote
.getAci().isPresent() && remote
.getAci().get().isValid();
60 boolean hasPni
= remote
.getPni().isPresent() && remote
.getPni().get().isValid();
62 if (!hasAci
&& !hasPni
) {
63 logger
.debug("Found a ContactRecord with neither an ACI nor a PNI -- marking as invalid.");
65 } else if (selfAci
!= null && selfAci
.equals(remote
.getAci().orElse(null)) || (
66 selfPni
!= null && selfPni
.equals(remote
.getPni().orElse(null))
67 ) || (selfNumber
!= null && selfNumber
.equals(remote
.getNumber().orElse(null)))) {
68 logger
.debug("Found a ContactRecord for ourselves -- marking as invalid.");
70 } else if (remote
.getNumber().isPresent() && !isValidE164(remote
.getNumber().get())) {
71 logger
.debug("Found a record with an invalid E164. Marking as invalid.");
79 protected Optional
<SignalContactRecord
> getMatching(SignalContactRecord remote
) throws SQLException
{
80 final var address
= getRecipientAddress(remote
);
81 final var recipientId
= account
.getRecipientStore().resolveRecipient(connection
, address
);
82 final var recipient
= account
.getRecipientStore().getRecipient(connection
, recipientId
);
84 final var identifier
= recipient
.getAddress().getIdentifier();
85 final var identity
= account
.getIdentityKeyStore().getIdentityInfo(connection
, identifier
);
86 final var storageId
= account
.getRecipientStore().getStorageId(connection
, recipientId
);
88 return Optional
.of(StorageSyncModels
.localToRemoteRecord(recipient
, identity
, storageId
.getRaw())
94 protected SignalContactRecord
merge(
95 SignalContactRecord remote
, SignalContactRecord local
97 String profileGivenName
;
98 String profileFamilyName
;
99 if (remote
.getProfileGivenName().isPresent() || remote
.getProfileFamilyName().isPresent()) {
100 profileGivenName
= remote
.getProfileGivenName().orElse("");
101 profileFamilyName
= remote
.getProfileFamilyName().orElse("");
103 profileGivenName
= local
.getProfileGivenName().orElse("");
104 profileFamilyName
= local
.getProfileFamilyName().orElse("");
107 String nicknameGivenName
;
108 String nicknameFamilyName
;
109 if (remote
.getNicknameGivenName().isPresent()) {
110 nicknameGivenName
= remote
.getNicknameGivenName().orElse("");
111 nicknameFamilyName
= remote
.getNicknameFamilyName().orElse("");
113 nicknameGivenName
= local
.getNicknameGivenName().orElse("");
114 nicknameFamilyName
= local
.getNicknameFamilyName().orElse("");
117 if (nicknameGivenName
.isBlank() && !nicknameFamilyName
.isBlank()) {
118 logger
.debug("Processed invalid nickname. Missing given name.");
120 nicknameGivenName
= "";
121 nicknameFamilyName
= "";
124 IdentityState identityState
;
126 if (remote
.getIdentityKey().isPresent() && (
127 remote
.getIdentityState() != local
.getIdentityState()
128 || local
.getIdentityKey().isEmpty()
129 || !account
.isPrimaryDevice()
132 identityState
= remote
.getIdentityState();
133 identityKey
= remote
.getIdentityKey().get();
135 identityState
= local
.getIdentityState();
136 identityKey
= local
.getIdentityKey().orElse(null);
139 if (local
.getAci().isPresent()
140 && local
.getIdentityKey().isPresent()
141 && remote
.getIdentityKey().isPresent()
142 && !Arrays
.equals(local
.getIdentityKey().get(), remote
.getIdentityKey().get())) {
143 logger
.debug("The local and remote identity keys do not match for {}. Enqueueing a profile fetch.",
144 local
.getAci().orElse(null));
145 final var address
= getRecipientAddress(local
);
146 jobExecutor
.enqueueJob(new DownloadProfileJob(address
));
149 final var e164sMatchButPnisDont
= local
.getNumber().isPresent()
152 .equals(remote
.getNumber().orElse(null))
153 && local
.getPni().isPresent()
154 && remote
.getPni().isPresent()
155 && !local
.getPni().get().equals(remote
.getPni().get());
157 final var pnisMatchButE164sDont
= local
.getPni().isPresent()
160 .equals(remote
.getPni().orElse(null))
161 && local
.getNumber().isPresent()
162 && remote
.getNumber().isPresent()
163 && !local
.getNumber().get().equals(remote
.getNumber().get());
167 if (!account
.isPrimaryDevice() && (e164sMatchButPnisDont
|| pnisMatchButE164sDont
)) {
168 if (e164sMatchButPnisDont
) {
169 logger
.debug("Matching E164s, but the PNIs differ! Trusting our local pair.");
170 } else if (pnisMatchButE164sDont
) {
171 logger
.debug("Matching PNIs, but the E164s differ! Trusting our local pair.");
173 jobExecutor
.enqueueJob(new RefreshRecipientsJob());
174 pni
= local
.getPni().get();
175 e164
= local
.getNumber().get();
177 pni
= OptionalUtil
.or(remote
.getPni(), local
.getPni()).orElse(null);
178 e164
= OptionalUtil
.or(remote
.getNumber(), local
.getNumber()).orElse(null);
181 final var unknownFields
= remote
.serializeUnknownFields();
182 final var aci
= local
.getAci().isEmpty() ? remote
.getAci().orElse(null) : local
.getAci().get();
183 final var profileKey
= OptionalUtil
.or(remote
.getProfileKey(), local
.getProfileKey()).orElse(null);
184 final var username
= OptionalUtil
.or(remote
.getUsername(), local
.getUsername()).orElse("");
185 final var blocked
= remote
.isBlocked();
186 final var profileSharing
= remote
.isProfileSharingEnabled();
187 final var archived
= remote
.isArchived();
188 final var forcedUnread
= remote
.isForcedUnread();
189 final var muteUntil
= remote
.getMuteUntil();
190 final var hideStory
= remote
.shouldHideStory();
191 final var unregisteredTimestamp
= remote
.getUnregisteredTimestamp();
192 final var hidden
= remote
.isHidden();
193 final var systemGivenName
= account
.isPrimaryDevice()
194 ? local
.getSystemGivenName().orElse("")
195 : remote
.getSystemGivenName().orElse("");
196 final var systemFamilyName
= account
.isPrimaryDevice()
197 ? local
.getSystemFamilyName().orElse("")
198 : remote
.getSystemFamilyName().orElse("");
199 final var systemNickname
= remote
.getSystemNickname().orElse("");
200 final var pniSignatureVerified
= remote
.isPniSignatureVerified() || local
.isPniSignatureVerified();
201 final var note
= remote
.getNote().or(local
::getNote
).orElse("");
203 final var mergedBuilder
= new SignalContactRecord
.Builder(remote
.getId().getRaw(), aci
, unknownFields
).setE164(
206 .setProfileGivenName(profileGivenName
)
207 .setProfileFamilyName(profileFamilyName
)
208 .setSystemGivenName(systemGivenName
)
209 .setSystemFamilyName(systemFamilyName
)
210 .setSystemNickname(systemNickname
)
211 .setProfileKey(profileKey
)
212 .setUsername(username
)
213 .setIdentityState(identityState
)
214 .setIdentityKey(identityKey
)
216 .setProfileSharingEnabled(profileSharing
)
217 .setArchived(archived
)
218 .setForcedUnread(forcedUnread
)
219 .setMuteUntil(muteUntil
)
220 .setHideStory(hideStory
)
221 .setUnregisteredTimestamp(unregisteredTimestamp
)
223 .setPniSignatureVerified(pniSignatureVerified
)
224 .setNicknameGivenName(nicknameGivenName
)
225 .setNicknameFamilyName(nicknameFamilyName
)
227 final var merged
= mergedBuilder
.build();
229 final var matchesRemote
= doProtosMatch(merged
, remote
);
234 final var matchesLocal
= doProtosMatch(merged
, local
);
239 return mergedBuilder
.setId(KeyUtils
.createRawStorageId()).build();
243 protected void insertLocal(SignalContactRecord
record) throws SQLException
{
244 StorageRecordUpdate
<SignalContactRecord
> update
= new StorageRecordUpdate
<>(null, record);
249 protected void updateLocal(StorageRecordUpdate
<SignalContactRecord
> update
) throws SQLException
{
250 final var contactRecord
= update
.newRecord();
251 final var address
= getRecipientAddress(contactRecord
);
252 final var recipientId
= account
.getRecipientStore().resolveRecipientTrusted(connection
, address
);
253 final var recipient
= account
.getRecipientStore().getRecipient(connection
, recipientId
);
255 final var contact
= recipient
.getContact();
256 final var blocked
= contact
!= null && contact
.isBlocked();
257 final var profileShared
= contact
!= null && contact
.isProfileSharingEnabled();
258 final var archived
= contact
!= null && contact
.isArchived();
259 final var hidden
= contact
!= null && contact
.isHidden();
260 final var hideStory
= contact
!= null && contact
.hideStory();
261 final var muteUntil
= contact
== null ?
0 : contact
.muteUntil();
262 final var unregisteredTimestamp
= contact
== null || contact
.unregisteredTimestamp() == null
264 : contact
.unregisteredTimestamp();
265 final var contactGivenName
= contact
== null ?
null : contact
.givenName();
266 final var contactFamilyName
= contact
== null ?
null : contact
.familyName();
267 final var contactNickName
= contact
== null ?
null : contact
.nickName();
268 if (blocked
!= contactRecord
.isBlocked()
269 || profileShared
!= contactRecord
.isProfileSharingEnabled()
270 || archived
!= contactRecord
.isArchived()
271 || hidden
!= contactRecord
.isHidden()
272 || hideStory
!= contactRecord
.shouldHideStory()
273 || muteUntil
!= contactRecord
.getMuteUntil()
274 || unregisteredTimestamp
!= contactRecord
.getUnregisteredTimestamp()
275 || !Objects
.equals(contactRecord
.getSystemGivenName().orElse(null), contactGivenName
)
276 || !Objects
.equals(contactRecord
.getSystemFamilyName().orElse(null), contactFamilyName
)
277 || !Objects
.equals(contactRecord
.getSystemNickname().orElse(null), contactNickName
)) {
278 logger
.debug("Storing new or updated contact {}", recipientId
);
279 final var contactBuilder
= contact
== null ? Contact
.newBuilder() : Contact
.newBuilder(contact
);
280 final var newContact
= contactBuilder
.withIsBlocked(contactRecord
.isBlocked())
281 .withIsProfileSharingEnabled(contactRecord
.isProfileSharingEnabled())
282 .withIsArchived(contactRecord
.isArchived())
283 .withIsHidden(contactRecord
.isHidden())
284 .withMuteUntil(contactRecord
.getMuteUntil())
285 .withHideStory(contactRecord
.shouldHideStory())
286 .withGivenName(contactRecord
.getSystemGivenName().orElse(null))
287 .withFamilyName(contactRecord
.getSystemFamilyName().orElse(null))
288 .withNickName(contactRecord
.getSystemNickname().orElse(null))
289 .withUnregisteredTimestamp(contactRecord
.getUnregisteredTimestamp() == 0
291 : contactRecord
.getUnregisteredTimestamp());
292 account
.getRecipientStore().storeContact(connection
, recipientId
, newContact
.build());
295 final var profile
= recipient
.getProfile();
296 final var profileGivenName
= profile
== null ?
null : profile
.getGivenName();
297 final var profileFamilyName
= profile
== null ?
null : profile
.getFamilyName();
298 if (!Objects
.equals(contactRecord
.getProfileGivenName().orElse(null), profileGivenName
) || !Objects
.equals(
299 contactRecord
.getProfileFamilyName().orElse(null),
300 profileFamilyName
)) {
301 final var profileBuilder
= profile
== null ? Profile
.newBuilder() : Profile
.newBuilder(profile
);
302 final var newProfile
= profileBuilder
.withGivenName(contactRecord
.getProfileGivenName().orElse(null))
303 .withFamilyName(contactRecord
.getProfileFamilyName().orElse(null))
305 account
.getRecipientStore().storeProfile(connection
, recipientId
, newProfile
);
307 if (contactRecord
.getProfileKey().isPresent()) {
309 logger
.trace("Storing profile key {}", recipientId
);
310 final var profileKey
= new ProfileKey(contactRecord
.getProfileKey().get());
311 account
.getRecipientStore().storeProfileKey(connection
, recipientId
, profileKey
);
312 } catch (InvalidInputException e
) {
313 logger
.warn("Received invalid contact profile key from storage");
316 if (contactRecord
.getIdentityKey().isPresent() && contactRecord
.getAci().isPresent()) {
318 logger
.trace("Storing identity key {}", recipientId
);
319 final var identityKey
= new IdentityKey(contactRecord
.getIdentityKey().get());
320 account
.getIdentityKeyStore()
321 .saveIdentity(connection
, contactRecord
.getAci().orElse(null), identityKey
);
323 final var trustLevel
= StorageSyncModels
.remoteToLocal(contactRecord
.getIdentityState());
324 if (trustLevel
!= null) {
325 account
.getIdentityKeyStore()
326 .setIdentityTrustLevel(connection
,
327 contactRecord
.getAci().orElse(null),
331 } catch (InvalidKeyException e
) {
332 logger
.warn("Received invalid contact identity key from storage");
335 account
.getRecipientStore()
336 .storeStorageRecord(connection
, recipientId
, contactRecord
.getId(), contactRecord
.toProto().encode());
339 private static RecipientAddress
getRecipientAddress(final SignalContactRecord contactRecord
) {
340 return new RecipientAddress(contactRecord
.getAci().orElse(null),
341 contactRecord
.getPni().orElse(null),
342 contactRecord
.getNumber().orElse(null),
343 contactRecord
.getUsername().orElse(null));
347 public int compare(SignalContactRecord lhs
, SignalContactRecord rhs
) {
348 if ((lhs
.getAci().isPresent() && Objects
.equals(lhs
.getAci(), rhs
.getAci())) || (
349 lhs
.getNumber().isPresent() && Objects
.equals(lhs
.getNumber(), rhs
.getNumber())
350 ) || (lhs
.getPni().isPresent() && Objects
.equals(lhs
.getPni(), rhs
.getPni()))) {
357 private static boolean isValidE164(String value
) {
358 return E164_PATTERN
.matcher(value
).matches();
361 private static boolean doProtosMatch(SignalContactRecord merged
, SignalContactRecord other
) {
362 return Arrays
.equals(merged
.toProto().encode(), other
.toProto().encode());