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(SignalContactRecord remote
, SignalContactRecord local
) {
95 String profileGivenName
;
96 String profileFamilyName
;
97 if (remote
.getProfileGivenName().isPresent() || remote
.getProfileFamilyName().isPresent()) {
98 profileGivenName
= remote
.getProfileGivenName().orElse("");
99 profileFamilyName
= remote
.getProfileFamilyName().orElse("");
101 profileGivenName
= local
.getProfileGivenName().orElse("");
102 profileFamilyName
= local
.getProfileFamilyName().orElse("");
105 IdentityState identityState
;
107 if (remote
.getIdentityKey().isPresent() && (
108 remote
.getIdentityState() != local
.getIdentityState()
109 || local
.getIdentityKey().isEmpty()
110 || !account
.isPrimaryDevice()
113 identityState
= remote
.getIdentityState();
114 identityKey
= remote
.getIdentityKey().get();
116 identityState
= local
.getIdentityState();
117 identityKey
= local
.getIdentityKey().orElse(null);
120 if (local
.getAci().isPresent()
121 && local
.getIdentityKey().isPresent()
122 && remote
.getIdentityKey().isPresent()
123 && !Arrays
.equals(local
.getIdentityKey().get(), remote
.getIdentityKey().get())) {
124 logger
.debug("The local and remote identity keys do not match for {}. Enqueueing a profile fetch.",
125 local
.getAci().orElse(null));
126 final var address
= getRecipientAddress(local
);
127 jobExecutor
.enqueueJob(new DownloadProfileJob(address
));
130 final var e164sMatchButPnisDont
= local
.getNumber().isPresent()
133 .equals(remote
.getNumber().orElse(null))
134 && local
.getPni().isPresent()
135 && remote
.getPni().isPresent()
136 && !local
.getPni().get().equals(remote
.getPni().get());
138 final var pnisMatchButE164sDont
= local
.getPni().isPresent()
141 .equals(remote
.getPni().orElse(null))
142 && local
.getNumber().isPresent()
143 && remote
.getNumber().isPresent()
144 && !local
.getNumber().get().equals(remote
.getNumber().get());
148 if (!account
.isPrimaryDevice() && (e164sMatchButPnisDont
|| pnisMatchButE164sDont
)) {
149 if (e164sMatchButPnisDont
) {
150 logger
.debug("Matching E164s, but the PNIs differ! Trusting our local pair.");
151 } else if (pnisMatchButE164sDont
) {
152 logger
.debug("Matching PNIs, but the E164s differ! Trusting our local pair.");
154 jobExecutor
.enqueueJob(new RefreshRecipientsJob());
155 pni
= local
.getPni().get();
156 e164
= local
.getNumber().get();
158 pni
= OptionalUtil
.or(remote
.getPni(), local
.getPni()).orElse(null);
159 e164
= OptionalUtil
.or(remote
.getNumber(), local
.getNumber()).orElse(null);
162 final var unknownFields
= remote
.serializeUnknownFields();
163 final var aci
= local
.getAci().isEmpty() ? remote
.getAci().orElse(null) : local
.getAci().get();
164 final var profileKey
= OptionalUtil
.or(remote
.getProfileKey(), local
.getProfileKey()).orElse(null);
165 final var username
= OptionalUtil
.or(remote
.getUsername(), local
.getUsername()).orElse("");
166 final var blocked
= remote
.isBlocked();
167 final var profileSharing
= remote
.isProfileSharingEnabled();
168 final var archived
= remote
.isArchived();
169 final var forcedUnread
= remote
.isForcedUnread();
170 final var muteUntil
= remote
.getMuteUntil();
171 final var hideStory
= remote
.shouldHideStory();
172 final var unregisteredTimestamp
= remote
.getUnregisteredTimestamp();
173 final var hidden
= remote
.isHidden();
174 final var systemGivenName
= account
.isPrimaryDevice()
175 ? local
.getSystemGivenName().orElse("")
176 : remote
.getSystemGivenName().orElse("");
177 final var systemFamilyName
= account
.isPrimaryDevice()
178 ? local
.getSystemFamilyName().orElse("")
179 : remote
.getSystemFamilyName().orElse("");
180 final var systemNickname
= remote
.getSystemNickname().orElse("");
181 final var nicknameGivenName
= remote
.getNicknameGivenName().orElse("");
182 final var nicknameFamilyName
= remote
.getNicknameFamilyName().orElse("");
183 final var pniSignatureVerified
= remote
.isPniSignatureVerified() || local
.isPniSignatureVerified();
184 final var note
= remote
.getNote().or(local
::getNote
).orElse("");
186 final var mergedBuilder
= new SignalContactRecord
.Builder(remote
.getId().getRaw(), aci
, unknownFields
).setE164(
189 .setProfileGivenName(profileGivenName
)
190 .setProfileFamilyName(profileFamilyName
)
191 .setSystemGivenName(systemGivenName
)
192 .setSystemFamilyName(systemFamilyName
)
193 .setSystemNickname(systemNickname
)
194 .setProfileKey(profileKey
)
195 .setUsername(username
)
196 .setIdentityState(identityState
)
197 .setIdentityKey(identityKey
)
199 .setProfileSharingEnabled(profileSharing
)
200 .setArchived(archived
)
201 .setForcedUnread(forcedUnread
)
202 .setMuteUntil(muteUntil
)
203 .setHideStory(hideStory
)
204 .setUnregisteredTimestamp(unregisteredTimestamp
)
206 .setPniSignatureVerified(pniSignatureVerified
)
207 .setNicknameGivenName(nicknameGivenName
)
208 .setNicknameFamilyName(nicknameFamilyName
)
210 final var merged
= mergedBuilder
.build();
212 final var matchesRemote
= doProtosMatch(merged
, remote
);
217 final var matchesLocal
= doProtosMatch(merged
, local
);
222 return mergedBuilder
.setId(KeyUtils
.createRawStorageId()).build();
226 protected void insertLocal(SignalContactRecord
record) throws SQLException
{
227 StorageRecordUpdate
<SignalContactRecord
> update
= new StorageRecordUpdate
<>(null, record);
232 protected void updateLocal(StorageRecordUpdate
<SignalContactRecord
> update
) throws SQLException
{
233 final var contactRecord
= update
.newRecord();
234 final var address
= getRecipientAddress(contactRecord
);
235 final var recipientId
= account
.getRecipientStore().resolveRecipientTrusted(connection
, address
);
236 final var recipient
= account
.getRecipientStore().getRecipient(connection
, recipientId
);
238 final var contact
= recipient
.getContact();
239 final var blocked
= contact
!= null && contact
.isBlocked();
240 final var profileShared
= contact
!= null && contact
.isProfileSharingEnabled();
241 final var archived
= contact
!= null && contact
.isArchived();
242 final var hidden
= contact
!= null && contact
.isHidden();
243 final var hideStory
= contact
!= null && contact
.hideStory();
244 final var muteUntil
= contact
== null ?
0 : contact
.muteUntil();
245 final var unregisteredTimestamp
= contact
== null || contact
.unregisteredTimestamp() == null
247 : contact
.unregisteredTimestamp();
248 final var contactGivenName
= contact
== null ?
null : contact
.givenName();
249 final var contactFamilyName
= contact
== null ?
null : contact
.familyName();
250 final var contactNickName
= contact
== null ?
null : contact
.nickName();
251 final var contactNickGivenName
= contact
== null ?
null : contact
.nickNameGivenName();
252 final var contactNickFamilyName
= contact
== null ?
null : contact
.nickNameFamilyName();
253 final var contactNote
= contact
== null ?
null : contact
.note();
254 if (blocked
!= contactRecord
.isBlocked()
255 || profileShared
!= contactRecord
.isProfileSharingEnabled()
256 || archived
!= contactRecord
.isArchived()
257 || hidden
!= contactRecord
.isHidden()
258 || hideStory
!= contactRecord
.shouldHideStory()
259 || muteUntil
!= contactRecord
.getMuteUntil()
260 || unregisteredTimestamp
!= contactRecord
.getUnregisteredTimestamp()
261 || !Objects
.equals(contactRecord
.getSystemGivenName().orElse(null), contactGivenName
)
262 || !Objects
.equals(contactRecord
.getSystemFamilyName().orElse(null), contactFamilyName
)
263 || !Objects
.equals(contactRecord
.getSystemNickname().orElse(null), contactNickName
)
264 || !Objects
.equals(contactRecord
.getNicknameGivenName().orElse(null), contactNickGivenName
)
265 || !Objects
.equals(contactRecord
.getNicknameFamilyName().orElse(null), contactNickFamilyName
)
266 || !Objects
.equals(contactRecord
.getNote().orElse(null), contactNote
)) {
267 logger
.debug("Storing new or updated contact {}", recipientId
);
268 final var contactBuilder
= contact
== null ? Contact
.newBuilder() : Contact
.newBuilder(contact
);
269 final var newContact
= contactBuilder
.withIsBlocked(contactRecord
.isBlocked())
270 .withIsProfileSharingEnabled(contactRecord
.isProfileSharingEnabled())
271 .withIsArchived(contactRecord
.isArchived())
272 .withIsHidden(contactRecord
.isHidden())
273 .withMuteUntil(contactRecord
.getMuteUntil())
274 .withHideStory(contactRecord
.shouldHideStory())
275 .withGivenName(contactRecord
.getSystemGivenName().orElse(null))
276 .withFamilyName(contactRecord
.getSystemFamilyName().orElse(null))
277 .withNickName(contactRecord
.getSystemNickname().orElse(null))
278 .withNickNameGivenName(contactRecord
.getNicknameGivenName().orElse(null))
279 .withNickNameFamilyName(contactRecord
.getNicknameFamilyName().orElse(null))
280 .withNote(contactRecord
.getNote().orElse(null))
281 .withUnregisteredTimestamp(contactRecord
.getUnregisteredTimestamp() == 0
283 : contactRecord
.getUnregisteredTimestamp());
284 account
.getRecipientStore().storeContact(connection
, recipientId
, newContact
.build());
287 final var profile
= recipient
.getProfile();
288 final var profileGivenName
= profile
== null ?
null : profile
.getGivenName();
289 final var profileFamilyName
= profile
== null ?
null : profile
.getFamilyName();
290 if (!Objects
.equals(contactRecord
.getProfileGivenName().orElse(null), profileGivenName
) || !Objects
.equals(
291 contactRecord
.getProfileFamilyName().orElse(null),
292 profileFamilyName
)) {
293 final var profileBuilder
= profile
== null ? Profile
.newBuilder() : Profile
.newBuilder(profile
);
294 final var newProfile
= profileBuilder
.withGivenName(contactRecord
.getProfileGivenName().orElse(null))
295 .withFamilyName(contactRecord
.getProfileFamilyName().orElse(null))
297 account
.getRecipientStore().storeProfile(connection
, recipientId
, newProfile
);
299 if (contactRecord
.getProfileKey().isPresent()) {
301 logger
.trace("Storing profile key {}", recipientId
);
302 final var profileKey
= new ProfileKey(contactRecord
.getProfileKey().get());
303 account
.getRecipientStore().storeProfileKey(connection
, recipientId
, profileKey
);
304 } catch (InvalidInputException e
) {
305 logger
.warn("Received invalid contact profile key from storage");
308 if (contactRecord
.getIdentityKey().isPresent() && contactRecord
.getAci().isPresent()) {
310 logger
.trace("Storing identity key {}", recipientId
);
311 final var identityKey
= new IdentityKey(contactRecord
.getIdentityKey().get());
312 account
.getIdentityKeyStore()
313 .saveIdentity(connection
, contactRecord
.getAci().orElse(null), identityKey
);
315 final var trustLevel
= StorageSyncModels
.remoteToLocal(contactRecord
.getIdentityState());
316 if (trustLevel
!= null) {
317 account
.getIdentityKeyStore()
318 .setIdentityTrustLevel(connection
,
319 contactRecord
.getAci().orElse(null),
323 } catch (InvalidKeyException e
) {
324 logger
.warn("Received invalid contact identity key from storage");
327 account
.getRecipientStore()
328 .storeStorageRecord(connection
, recipientId
, contactRecord
.getId(), contactRecord
.toProto().encode());
331 private static RecipientAddress
getRecipientAddress(final SignalContactRecord contactRecord
) {
332 return new RecipientAddress(contactRecord
.getAci().orElse(null),
333 contactRecord
.getPni().orElse(null),
334 contactRecord
.getNumber().orElse(null),
335 contactRecord
.getUsername().orElse(null));
339 public int compare(SignalContactRecord lhs
, SignalContactRecord rhs
) {
340 if ((lhs
.getAci().isPresent() && Objects
.equals(lhs
.getAci(), rhs
.getAci())) || (
341 lhs
.getNumber().isPresent() && Objects
.equals(lhs
.getNumber(), rhs
.getNumber())
342 ) || (lhs
.getPni().isPresent() && Objects
.equals(lhs
.getPni(), rhs
.getPni()))) {
349 private static boolean isValidE164(String value
) {
350 return E164_PATTERN
.matcher(value
).matches();
353 private static boolean doProtosMatch(SignalContactRecord merged
, SignalContactRecord other
) {
354 return Arrays
.equals(merged
.toProto().encode(), other
.toProto().encode());