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 pniSignatureVerified
= remote
.isPniSignatureVerified() || local
.isPniSignatureVerified();
185 final var mergedBuilder
= new SignalContactRecord
.Builder(remote
.getId().getRaw(), aci
, unknownFields
).setE164(
188 .setProfileGivenName(profileGivenName
)
189 .setProfileFamilyName(profileFamilyName
)
190 .setSystemGivenName(systemGivenName
)
191 .setSystemFamilyName(systemFamilyName
)
192 .setSystemNickname(systemNickname
)
193 .setProfileKey(profileKey
)
194 .setUsername(username
)
195 .setIdentityState(identityState
)
196 .setIdentityKey(identityKey
)
198 .setProfileSharingEnabled(profileSharing
)
199 .setArchived(archived
)
200 .setForcedUnread(forcedUnread
)
201 .setMuteUntil(muteUntil
)
202 .setHideStory(hideStory
)
203 .setUnregisteredTimestamp(unregisteredTimestamp
)
205 .setPniSignatureVerified(pniSignatureVerified
);
206 final var merged
= mergedBuilder
.build();
208 final var matchesRemote
= doProtosMatch(merged
, remote
);
213 final var matchesLocal
= doProtosMatch(merged
, local
);
218 return mergedBuilder
.setId(KeyUtils
.createRawStorageId()).build();
222 protected void insertLocal(SignalContactRecord
record) throws SQLException
{
223 StorageRecordUpdate
<SignalContactRecord
> update
= new StorageRecordUpdate
<>(null, record);
228 protected void updateLocal(StorageRecordUpdate
<SignalContactRecord
> update
) throws SQLException
{
229 final var contactRecord
= update
.newRecord();
230 final var address
= getRecipientAddress(contactRecord
);
231 final var recipientId
= account
.getRecipientStore().resolveRecipientTrusted(connection
, address
);
232 final var recipient
= account
.getRecipientStore().getRecipient(connection
, recipientId
);
234 final var contact
= recipient
.getContact();
235 final var blocked
= contact
!= null && contact
.isBlocked();
236 final var profileShared
= contact
!= null && contact
.isProfileSharingEnabled();
237 final var archived
= contact
!= null && contact
.isArchived();
238 final var hidden
= contact
!= null && contact
.isHidden();
239 final var hideStory
= contact
!= null && contact
.hideStory();
240 final var muteUntil
= contact
== null ?
0 : contact
.muteUntil();
241 final var unregisteredTimestamp
= contact
== null || contact
.unregisteredTimestamp() == null
243 : contact
.unregisteredTimestamp();
244 final var contactGivenName
= contact
== null ?
null : contact
.givenName();
245 final var contactFamilyName
= contact
== null ?
null : contact
.familyName();
246 final var contactNickName
= contact
== null ?
null : contact
.nickName();
247 if (blocked
!= contactRecord
.isBlocked()
248 || profileShared
!= contactRecord
.isProfileSharingEnabled()
249 || archived
!= contactRecord
.isArchived()
250 || hidden
!= contactRecord
.isHidden()
251 || hideStory
!= contactRecord
.shouldHideStory()
252 || muteUntil
!= contactRecord
.getMuteUntil()
253 || unregisteredTimestamp
!= contactRecord
.getUnregisteredTimestamp()
254 || !Objects
.equals(contactRecord
.getSystemGivenName().orElse(null), contactGivenName
)
255 || !Objects
.equals(contactRecord
.getSystemFamilyName().orElse(null), contactFamilyName
)
256 || !Objects
.equals(contactRecord
.getSystemNickname().orElse(null), contactNickName
)) {
257 logger
.debug("Storing new or updated contact {}", recipientId
);
258 final var contactBuilder
= contact
== null ? Contact
.newBuilder() : Contact
.newBuilder(contact
);
259 final var newContact
= contactBuilder
.withIsBlocked(contactRecord
.isBlocked())
260 .withIsProfileSharingEnabled(contactRecord
.isProfileSharingEnabled())
261 .withIsArchived(contactRecord
.isArchived())
262 .withIsHidden(contactRecord
.isHidden())
263 .withMuteUntil(contactRecord
.getMuteUntil())
264 .withHideStory(contactRecord
.shouldHideStory())
265 .withGivenName(contactRecord
.getSystemGivenName().orElse(null))
266 .withFamilyName(contactRecord
.getSystemFamilyName().orElse(null))
267 .withNickName(contactRecord
.getSystemNickname().orElse(null))
268 .withUnregisteredTimestamp(contactRecord
.getUnregisteredTimestamp() == 0
270 : contactRecord
.getUnregisteredTimestamp());
271 account
.getRecipientStore().storeContact(connection
, recipientId
, newContact
.build());
274 final var profile
= recipient
.getProfile();
275 final var profileGivenName
= profile
== null ?
null : profile
.getGivenName();
276 final var profileFamilyName
= profile
== null ?
null : profile
.getFamilyName();
277 if (!Objects
.equals(contactRecord
.getProfileGivenName().orElse(null), profileGivenName
) || !Objects
.equals(
278 contactRecord
.getProfileFamilyName().orElse(null),
279 profileFamilyName
)) {
280 final var profileBuilder
= profile
== null ? Profile
.newBuilder() : Profile
.newBuilder(profile
);
281 final var newProfile
= profileBuilder
.withGivenName(contactRecord
.getProfileGivenName().orElse(null))
282 .withFamilyName(contactRecord
.getProfileFamilyName().orElse(null))
284 account
.getRecipientStore().storeProfile(connection
, recipientId
, newProfile
);
286 if (contactRecord
.getProfileKey().isPresent()) {
288 logger
.trace("Storing profile key {}", recipientId
);
289 final var profileKey
= new ProfileKey(contactRecord
.getProfileKey().get());
290 account
.getRecipientStore().storeProfileKey(connection
, recipientId
, profileKey
);
291 } catch (InvalidInputException e
) {
292 logger
.warn("Received invalid contact profile key from storage");
295 if (contactRecord
.getIdentityKey().isPresent() && contactRecord
.getAci().isPresent()) {
297 logger
.trace("Storing identity key {}", recipientId
);
298 final var identityKey
= new IdentityKey(contactRecord
.getIdentityKey().get());
299 account
.getIdentityKeyStore()
300 .saveIdentity(connection
, contactRecord
.getAci().orElse(null), identityKey
);
302 final var trustLevel
= StorageSyncModels
.remoteToLocal(contactRecord
.getIdentityState());
303 if (trustLevel
!= null) {
304 account
.getIdentityKeyStore()
305 .setIdentityTrustLevel(connection
,
306 contactRecord
.getAci().orElse(null),
310 } catch (InvalidKeyException e
) {
311 logger
.warn("Received invalid contact identity key from storage");
314 account
.getRecipientStore()
315 .storeStorageRecord(connection
, recipientId
, contactRecord
.getId(), contactRecord
.toProto().encode());
318 private static RecipientAddress
getRecipientAddress(final SignalContactRecord contactRecord
) {
319 return new RecipientAddress(contactRecord
.getAci().orElse(null),
320 contactRecord
.getPni().orElse(null),
321 contactRecord
.getNumber().orElse(null),
322 contactRecord
.getUsername().orElse(null));
326 public int compare(SignalContactRecord lhs
, SignalContactRecord rhs
) {
327 if ((lhs
.getAci().isPresent() && Objects
.equals(lhs
.getAci(), rhs
.getAci())) || (
328 lhs
.getNumber().isPresent() && Objects
.equals(lhs
.getNumber(), rhs
.getNumber())
329 ) || (lhs
.getPni().isPresent() && Objects
.equals(lhs
.getPni(), rhs
.getPni()))) {
336 private static boolean isValidE164(String value
) {
337 return E164_PATTERN
.matcher(value
).matches();
340 private static boolean doProtosMatch(SignalContactRecord merged
, SignalContactRecord other
) {
341 return Arrays
.equals(merged
.toProto().encode(), other
.toProto().encode());