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 IdentityState identityState
;
109 if (remote
.getIdentityKey().isPresent() && (
110 remote
.getIdentityState() != local
.getIdentityState()
111 || local
.getIdentityKey().isEmpty()
112 || !account
.isPrimaryDevice()
115 identityState
= remote
.getIdentityState();
116 identityKey
= remote
.getIdentityKey().get();
118 identityState
= local
.getIdentityState();
119 identityKey
= local
.getIdentityKey().orElse(null);
122 if (local
.getAci().isPresent()
123 && local
.getIdentityKey().isPresent()
124 && remote
.getIdentityKey().isPresent()
125 && !Arrays
.equals(local
.getIdentityKey().get(), remote
.getIdentityKey().get())) {
126 logger
.debug("The local and remote identity keys do not match for {}. Enqueueing a profile fetch.",
127 local
.getAci().orElse(null));
128 final var address
= getRecipientAddress(local
);
129 jobExecutor
.enqueueJob(new DownloadProfileJob(address
));
132 final var e164sMatchButPnisDont
= local
.getNumber().isPresent()
135 .equals(remote
.getNumber().orElse(null))
136 && local
.getPni().isPresent()
137 && remote
.getPni().isPresent()
138 && !local
.getPni().get().equals(remote
.getPni().get());
140 final var pnisMatchButE164sDont
= local
.getPni().isPresent()
143 .equals(remote
.getPni().orElse(null))
144 && local
.getNumber().isPresent()
145 && remote
.getNumber().isPresent()
146 && !local
.getNumber().get().equals(remote
.getNumber().get());
150 if (!account
.isPrimaryDevice() && (e164sMatchButPnisDont
|| pnisMatchButE164sDont
)) {
151 if (e164sMatchButPnisDont
) {
152 logger
.debug("Matching E164s, but the PNIs differ! Trusting our local pair.");
153 } else if (pnisMatchButE164sDont
) {
154 logger
.debug("Matching PNIs, but the E164s differ! Trusting our local pair.");
156 jobExecutor
.enqueueJob(new RefreshRecipientsJob());
157 pni
= local
.getPni().get();
158 e164
= local
.getNumber().get();
160 pni
= OptionalUtil
.or(remote
.getPni(), local
.getPni()).orElse(null);
161 e164
= OptionalUtil
.or(remote
.getNumber(), local
.getNumber()).orElse(null);
164 final var unknownFields
= remote
.serializeUnknownFields();
165 final var aci
= local
.getAci().isEmpty() ? remote
.getAci().orElse(null) : local
.getAci().get();
166 final var profileKey
= OptionalUtil
.or(remote
.getProfileKey(), local
.getProfileKey()).orElse(null);
167 final var username
= OptionalUtil
.or(remote
.getUsername(), local
.getUsername()).orElse("");
168 final var blocked
= remote
.isBlocked();
169 final var profileSharing
= remote
.isProfileSharingEnabled();
170 final var archived
= remote
.isArchived();
171 final var forcedUnread
= remote
.isForcedUnread();
172 final var muteUntil
= remote
.getMuteUntil();
173 final var hideStory
= remote
.shouldHideStory();
174 final var unregisteredTimestamp
= remote
.getUnregisteredTimestamp();
175 final var hidden
= remote
.isHidden();
176 final var systemGivenName
= account
.isPrimaryDevice()
177 ? local
.getSystemGivenName().orElse("")
178 : remote
.getSystemGivenName().orElse("");
179 final var systemFamilyName
= account
.isPrimaryDevice()
180 ? local
.getSystemFamilyName().orElse("")
181 : remote
.getSystemFamilyName().orElse("");
182 final var systemNickname
= remote
.getSystemNickname().orElse("");
183 final var nicknameGivenName
= remote
.getNicknameGivenName().orElse("");
184 final var nicknameFamilyName
= remote
.getNicknameFamilyName().orElse("");
185 final var pniSignatureVerified
= remote
.isPniSignatureVerified() || local
.isPniSignatureVerified();
186 final var note
= remote
.getNote().or(local
::getNote
).orElse("");
188 final var mergedBuilder
= new SignalContactRecord
.Builder(remote
.getId().getRaw(), aci
, unknownFields
).setE164(
191 .setProfileGivenName(profileGivenName
)
192 .setProfileFamilyName(profileFamilyName
)
193 .setSystemGivenName(systemGivenName
)
194 .setSystemFamilyName(systemFamilyName
)
195 .setSystemNickname(systemNickname
)
196 .setProfileKey(profileKey
)
197 .setUsername(username
)
198 .setIdentityState(identityState
)
199 .setIdentityKey(identityKey
)
201 .setProfileSharingEnabled(profileSharing
)
202 .setArchived(archived
)
203 .setForcedUnread(forcedUnread
)
204 .setMuteUntil(muteUntil
)
205 .setHideStory(hideStory
)
206 .setUnregisteredTimestamp(unregisteredTimestamp
)
208 .setPniSignatureVerified(pniSignatureVerified
)
209 .setNicknameGivenName(nicknameGivenName
)
210 .setNicknameFamilyName(nicknameFamilyName
)
212 final var merged
= mergedBuilder
.build();
214 final var matchesRemote
= doProtosMatch(merged
, remote
);
219 final var matchesLocal
= doProtosMatch(merged
, local
);
224 return mergedBuilder
.setId(KeyUtils
.createRawStorageId()).build();
228 protected void insertLocal(SignalContactRecord
record) throws SQLException
{
229 StorageRecordUpdate
<SignalContactRecord
> update
= new StorageRecordUpdate
<>(null, record);
234 protected void updateLocal(StorageRecordUpdate
<SignalContactRecord
> update
) throws SQLException
{
235 final var contactRecord
= update
.newRecord();
236 final var address
= getRecipientAddress(contactRecord
);
237 final var recipientId
= account
.getRecipientStore().resolveRecipientTrusted(connection
, address
);
238 final var recipient
= account
.getRecipientStore().getRecipient(connection
, recipientId
);
240 final var contact
= recipient
.getContact();
241 final var blocked
= contact
!= null && contact
.isBlocked();
242 final var profileShared
= contact
!= null && contact
.isProfileSharingEnabled();
243 final var archived
= contact
!= null && contact
.isArchived();
244 final var hidden
= contact
!= null && contact
.isHidden();
245 final var hideStory
= contact
!= null && contact
.hideStory();
246 final var muteUntil
= contact
== null ?
0 : contact
.muteUntil();
247 final var unregisteredTimestamp
= contact
== null || contact
.unregisteredTimestamp() == null
249 : contact
.unregisteredTimestamp();
250 final var contactGivenName
= contact
== null ?
null : contact
.givenName();
251 final var contactFamilyName
= contact
== null ?
null : contact
.familyName();
252 final var contactNickName
= contact
== null ?
null : contact
.nickName();
253 final var contactNickGivenName
= contact
== null ?
null : contact
.nickNameGivenName();
254 final var contactNickFamilyName
= contact
== null ?
null : contact
.nickNameFamilyName();
255 final var contactNote
= contact
== null ?
null : contact
.note();
256 if (blocked
!= contactRecord
.isBlocked()
257 || profileShared
!= contactRecord
.isProfileSharingEnabled()
258 || archived
!= contactRecord
.isArchived()
259 || hidden
!= contactRecord
.isHidden()
260 || hideStory
!= contactRecord
.shouldHideStory()
261 || muteUntil
!= contactRecord
.getMuteUntil()
262 || unregisteredTimestamp
!= contactRecord
.getUnregisteredTimestamp()
263 || !Objects
.equals(contactRecord
.getSystemGivenName().orElse(null), contactGivenName
)
264 || !Objects
.equals(contactRecord
.getSystemFamilyName().orElse(null), contactFamilyName
)
265 || !Objects
.equals(contactRecord
.getSystemNickname().orElse(null), contactNickName
)
266 || !Objects
.equals(contactRecord
.getNicknameGivenName().orElse(null), contactNickGivenName
)
267 || !Objects
.equals(contactRecord
.getNicknameFamilyName().orElse(null), contactNickFamilyName
)
268 || !Objects
.equals(contactRecord
.getNote().orElse(null), contactNote
)) {
269 logger
.debug("Storing new or updated contact {}", recipientId
);
270 final var contactBuilder
= contact
== null ? Contact
.newBuilder() : Contact
.newBuilder(contact
);
271 final var newContact
= contactBuilder
.withIsBlocked(contactRecord
.isBlocked())
272 .withIsProfileSharingEnabled(contactRecord
.isProfileSharingEnabled())
273 .withIsArchived(contactRecord
.isArchived())
274 .withIsHidden(contactRecord
.isHidden())
275 .withMuteUntil(contactRecord
.getMuteUntil())
276 .withHideStory(contactRecord
.shouldHideStory())
277 .withGivenName(contactRecord
.getSystemGivenName().orElse(null))
278 .withFamilyName(contactRecord
.getSystemFamilyName().orElse(null))
279 .withNickName(contactRecord
.getSystemNickname().orElse(null))
280 .withNickNameGivenName(contactRecord
.getNicknameGivenName().orElse(null))
281 .withNickNameFamilyName(contactRecord
.getNicknameFamilyName().orElse(null))
282 .withNote(contactRecord
.getNote().orElse(null))
283 .withUnregisteredTimestamp(contactRecord
.getUnregisteredTimestamp() == 0
285 : contactRecord
.getUnregisteredTimestamp());
286 account
.getRecipientStore().storeContact(connection
, recipientId
, newContact
.build());
289 final var profile
= recipient
.getProfile();
290 final var profileGivenName
= profile
== null ?
null : profile
.getGivenName();
291 final var profileFamilyName
= profile
== null ?
null : profile
.getFamilyName();
292 if (!Objects
.equals(contactRecord
.getProfileGivenName().orElse(null), profileGivenName
) || !Objects
.equals(
293 contactRecord
.getProfileFamilyName().orElse(null),
294 profileFamilyName
)) {
295 final var profileBuilder
= profile
== null ? Profile
.newBuilder() : Profile
.newBuilder(profile
);
296 final var newProfile
= profileBuilder
.withGivenName(contactRecord
.getProfileGivenName().orElse(null))
297 .withFamilyName(contactRecord
.getProfileFamilyName().orElse(null))
299 account
.getRecipientStore().storeProfile(connection
, recipientId
, newProfile
);
301 if (contactRecord
.getProfileKey().isPresent()) {
303 logger
.trace("Storing profile key {}", recipientId
);
304 final var profileKey
= new ProfileKey(contactRecord
.getProfileKey().get());
305 account
.getRecipientStore().storeProfileKey(connection
, recipientId
, profileKey
);
306 } catch (InvalidInputException e
) {
307 logger
.warn("Received invalid contact profile key from storage");
310 if (contactRecord
.getIdentityKey().isPresent() && contactRecord
.getAci().isPresent()) {
312 logger
.trace("Storing identity key {}", recipientId
);
313 final var identityKey
= new IdentityKey(contactRecord
.getIdentityKey().get());
314 account
.getIdentityKeyStore()
315 .saveIdentity(connection
, contactRecord
.getAci().orElse(null), identityKey
);
317 final var trustLevel
= StorageSyncModels
.remoteToLocal(contactRecord
.getIdentityState());
318 if (trustLevel
!= null) {
319 account
.getIdentityKeyStore()
320 .setIdentityTrustLevel(connection
,
321 contactRecord
.getAci().orElse(null),
325 } catch (InvalidKeyException e
) {
326 logger
.warn("Received invalid contact identity key from storage");
329 account
.getRecipientStore()
330 .storeStorageRecord(connection
, recipientId
, contactRecord
.getId(), contactRecord
.toProto().encode());
333 private static RecipientAddress
getRecipientAddress(final SignalContactRecord contactRecord
) {
334 return new RecipientAddress(contactRecord
.getAci().orElse(null),
335 contactRecord
.getPni().orElse(null),
336 contactRecord
.getNumber().orElse(null),
337 contactRecord
.getUsername().orElse(null));
341 public int compare(SignalContactRecord lhs
, SignalContactRecord rhs
) {
342 if ((lhs
.getAci().isPresent() && Objects
.equals(lhs
.getAci(), rhs
.getAci())) || (
343 lhs
.getNumber().isPresent() && Objects
.equals(lhs
.getNumber(), rhs
.getNumber())
344 ) || (lhs
.getPni().isPresent() && Objects
.equals(lhs
.getPni(), rhs
.getPni()))) {
351 private static boolean isValidE164(String value
) {
352 return E164_PATTERN
.matcher(value
).matches();
355 private static boolean doProtosMatch(SignalContactRecord merged
, SignalContactRecord other
) {
356 return Arrays
.equals(merged
.toProto().encode(), other
.toProto().encode());