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
.storage
.StorageId
;
21 import org
.whispersystems
.signalservice
.internal
.storage
.protos
.ContactRecord
;
22 import org
.whispersystems
.signalservice
.internal
.storage
.protos
.ContactRecord
.IdentityState
;
24 import java
.sql
.Connection
;
25 import java
.sql
.SQLException
;
26 import java
.util
.Arrays
;
27 import java
.util
.Objects
;
28 import java
.util
.Optional
;
29 import java
.util
.regex
.Pattern
;
31 import okio
.ByteString
;
33 import static org
.asamk
.signal
.manager
.util
.Utils
.firstNonEmpty
;
34 import static org
.asamk
.signal
.manager
.util
.Utils
.nullIfEmpty
;
36 public class ContactRecordProcessor
extends DefaultStorageRecordProcessor
<SignalContactRecord
> {
38 private static final Logger logger
= LoggerFactory
.getLogger(ContactRecordProcessor
.class);
40 private static final Pattern E164_PATTERN
= Pattern
.compile("^\\+[1-9]\\d{0,18}$");
42 private final ACI selfAci
;
43 private final PNI selfPni
;
44 private final String selfNumber
;
45 private final SignalAccount account
;
46 private final Connection connection
;
47 private final JobExecutor jobExecutor
;
49 public ContactRecordProcessor(SignalAccount account
, Connection connection
, final JobExecutor jobExecutor
) {
50 this.account
= account
;
51 this.connection
= connection
;
52 this.jobExecutor
= jobExecutor
;
53 this.selfAci
= account
.getAci();
54 this.selfPni
= account
.getPni();
55 this.selfNumber
= account
.getNumber();
60 * - You can't have a contact record without an ACI or PNI.
61 * - You can't have a contact record for yourself. That should be an account record.
64 protected boolean isInvalid(SignalContactRecord remoteRecord
) {
65 final var remote
= remoteRecord
.getProto();
66 final var aci
= ACI
.parseOrNull(remote
.aci
);
67 final var pni
= PNI
.parseOrNull(remote
.pni
);
68 final var e164
= nullIfEmpty(remote
.e164
);
69 boolean hasAci
= aci
!= null && aci
.isValid();
70 boolean hasPni
= pni
!= null && pni
.isValid();
72 if (!hasAci
&& !hasPni
) {
73 logger
.debug("Found a ContactRecord with neither an ACI nor a PNI -- marking as invalid.");
75 } else if (selfAci
!= null && selfAci
.equals(aci
) || (
76 selfPni
!= null && selfPni
.equals(pni
)
77 ) || (selfNumber
!= null && selfNumber
.equals(e164
))) {
78 logger
.debug("Found a ContactRecord for ourselves -- marking as invalid.");
80 } else if (e164
!= null && !isValidE164(e164
)) {
81 logger
.debug("Found a record with an invalid E164 ({}). Marking as invalid.", e164
);
89 protected Optional
<SignalContactRecord
> getMatching(SignalContactRecord remote
) throws SQLException
{
90 final var address
= getRecipientAddress(remote
.getProto());
91 final var recipientId
= account
.getRecipientStore().resolveRecipient(connection
, address
);
92 final var recipient
= account
.getRecipientStore().getRecipient(connection
, recipientId
);
94 final var identifier
= recipient
.getAddress().getIdentifier();
95 final var identity
= account
.getIdentityKeyStore().getIdentityInfo(connection
, identifier
);
96 final var storageId
= account
.getRecipientStore().getStorageId(connection
, recipientId
);
98 return Optional
.of(new SignalContactRecord(storageId
,
99 StorageSyncModels
.localToRemoteRecord(recipient
, identity
)));
103 protected SignalContactRecord
merge(SignalContactRecord remoteRecord
, SignalContactRecord localRecord
) {
104 final var remote
= remoteRecord
.getProto();
105 final var local
= localRecord
.getProto();
107 String profileGivenName
;
108 String profileFamilyName
;
109 if (!remote
.givenName
.isEmpty() || !remote
.familyName
.isEmpty()) {
110 profileGivenName
= remote
.givenName
;
111 profileFamilyName
= remote
.familyName
;
113 profileGivenName
= local
.givenName
;
114 profileFamilyName
= local
.familyName
;
117 IdentityState identityState
;
118 ByteString identityKey
;
119 if (remote
.identityKey
.size() > 0 && (
120 !account
.isPrimaryDevice()
121 || remote
.identityState
!= local
.identityState
122 || local
.identityKey
.size() == 0
125 identityState
= remote
.identityState
;
126 identityKey
= remote
.identityKey
;
128 identityState
= local
.identityState
;
129 identityKey
= local
.identityKey
.size() > 0 ? local
.identityKey
: ByteString
.EMPTY
;
132 if (!local
.aci
.isEmpty()
133 && local
.identityKey
.size() > 0
134 && remote
.identityKey
.size() > 0
135 && !local
.identityKey
.equals(remote
.identityKey
)) {
136 logger
.debug("The local and remote identity keys do not match for {}. Enqueueing a profile fetch.",
138 final var address
= getRecipientAddress(local
);
139 jobExecutor
.enqueueJob(new DownloadProfileJob(address
));
144 if (account
.isPrimaryDevice()) {
145 final var e164sMatchButPnisDont
= !local
.e164
.isEmpty()
146 && local
.e164
.equals(remote
.e164
)
147 && !local
.pni
.isEmpty()
148 && !remote
.pni
.isEmpty()
149 && !local
.pni
.equals(remote
.pni
);
151 final var pnisMatchButE164sDont
= !local
.pni
.isEmpty()
152 && local
.pni
.equals(remote
.pni
)
153 && !local
.e164
.isEmpty()
154 && !remote
.e164
.isEmpty()
155 && !local
.e164
.equals(remote
.e164
);
157 if (e164sMatchButPnisDont
|| pnisMatchButE164sDont
) {
158 if (e164sMatchButPnisDont
) {
159 logger
.debug("Matching E164s, but the PNIs differ! Trusting our local pair.");
160 } else if (pnisMatchButE164sDont
) {
161 logger
.debug("Matching PNIs, but the E164s differ! Trusting our local pair.");
163 jobExecutor
.enqueueJob(new RefreshRecipientsJob());
167 pni
= firstNonEmpty(remote
.pni
, local
.pni
);
168 e164
= firstNonEmpty(remote
.e164
, local
.e164
);
171 pni
= firstNonEmpty(remote
.pni
, local
.pni
);
172 e164
= firstNonEmpty(remote
.e164
, local
.e164
);
175 final var mergedBuilder
= SignalContactRecord
.Companion
.newBuilder(remote
.unknownFields().toByteArray())
176 .aci(local
.aci
.isEmpty() ? remote
.aci
: local
.aci
)
179 .givenName(profileGivenName
)
180 .familyName(profileFamilyName
)
181 .systemGivenName(account
.isPrimaryDevice() ? local
.systemGivenName
: remote
.systemGivenName
)
182 .systemFamilyName(account
.isPrimaryDevice() ? local
.systemFamilyName
: remote
.systemFamilyName
)
183 .systemNickname(remote
.systemNickname
)
184 .profileKey(firstNonEmpty(remote
.profileKey
, local
.profileKey
))
185 .username(firstNonEmpty(remote
.username
, local
.username
))
186 .identityState(identityState
)
187 .identityKey(identityKey
)
188 .blocked(remote
.blocked
)
189 .whitelisted(remote
.whitelisted
)
190 .archived(remote
.archived
)
191 .markedUnread(remote
.markedUnread
)
192 .mutedUntilTimestamp(remote
.mutedUntilTimestamp
)
193 .hideStory(remote
.hideStory
)
194 .unregisteredAtTimestamp(remote
.unregisteredAtTimestamp
)
195 .hidden(remote
.hidden
)
196 .pniSignatureVerified(remote
.pniSignatureVerified
|| local
.pniSignatureVerified
)
197 .nickname(remote
.nickname
)
199 final var merged
= mergedBuilder
.build();
201 final var matchesRemote
= doProtosMatch(merged
, remote
);
206 final var matchesLocal
= doProtosMatch(merged
, local
);
211 return new SignalContactRecord(StorageId
.forContact(KeyUtils
.createRawStorageId()), mergedBuilder
.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 contactProto
= contactRecord
.getProto();
224 final var address
= getRecipientAddress(contactProto
);
225 final var recipientId
= account
.getRecipientStore().resolveRecipientTrusted(connection
, address
);
226 final var recipient
= account
.getRecipientStore().getRecipient(connection
, recipientId
);
228 final var contact
= recipient
.getContact();
229 final var blocked
= contact
!= null && contact
.isBlocked();
230 final var profileShared
= contact
!= null && contact
.isProfileSharingEnabled();
231 final var archived
= contact
!= null && contact
.isArchived();
232 final var hidden
= contact
!= null && contact
.isHidden();
233 final var hideStory
= contact
!= null && contact
.hideStory();
234 final var muteUntil
= contact
== null ?
0 : contact
.muteUntil();
235 final var unregisteredTimestamp
= contact
== null || contact
.unregisteredTimestamp() == null
237 : contact
.unregisteredTimestamp();
238 final var contactGivenName
= contact
== null ?
null : contact
.givenName();
239 final var contactFamilyName
= contact
== null ?
null : contact
.familyName();
240 final var contactNickName
= contact
== null ?
null : contact
.nickName();
241 final var contactNickGivenName
= contact
== null ?
null : contact
.nickNameGivenName();
242 final var contactNickFamilyName
= contact
== null ?
null : contact
.nickNameFamilyName();
243 final var contactNote
= contact
== null ?
null : contact
.note();
244 if (blocked
!= contactProto
.blocked
245 || profileShared
!= contactProto
.whitelisted
246 || archived
!= contactProto
.archived
247 || hidden
!= contactProto
.hidden
248 || hideStory
!= contactProto
.hideStory
249 || muteUntil
!= contactProto
.mutedUntilTimestamp
250 || unregisteredTimestamp
!= contactProto
.unregisteredAtTimestamp
251 || !Objects
.equals(nullIfEmpty(contactProto
.systemGivenName
), contactGivenName
)
252 || !Objects
.equals(nullIfEmpty(contactProto
.systemFamilyName
), contactFamilyName
)
253 || !Objects
.equals(nullIfEmpty(contactProto
.systemNickname
), contactNickName
)
254 || !Objects
.equals(nullIfEmpty(contactProto
.nickname
== null ?
"" : contactProto
.nickname
.given
),
255 contactNickGivenName
)
256 || !Objects
.equals(nullIfEmpty(contactProto
.nickname
== null ?
"" : contactProto
.nickname
.family
),
257 contactNickFamilyName
)
258 || !Objects
.equals(nullIfEmpty(contactProto
.note
), contactNote
)) {
259 logger
.debug("Storing new or updated contact {}", recipientId
);
260 final var contactBuilder
= contact
== null ? Contact
.newBuilder() : Contact
.newBuilder(contact
);
261 final var newContact
= contactBuilder
.withIsBlocked(contactProto
.blocked
)
262 .withIsProfileSharingEnabled(contactProto
.whitelisted
)
263 .withIsArchived(contactProto
.archived
)
264 .withIsHidden(contactProto
.hidden
)
265 .withMuteUntil(contactProto
.mutedUntilTimestamp
)
266 .withHideStory(contactProto
.hideStory
)
267 .withGivenName(nullIfEmpty(contactProto
.systemGivenName
))
268 .withFamilyName(nullIfEmpty(contactProto
.systemFamilyName
))
269 .withNickName(nullIfEmpty(contactProto
.systemNickname
))
270 .withNickNameGivenName(nullIfEmpty(contactProto
.givenName
))
271 .withNickNameFamilyName(nullIfEmpty(contactProto
.familyName
))
272 .withNote(nullIfEmpty(contactProto
.note
))
273 .withUnregisteredTimestamp(contactProto
.unregisteredAtTimestamp
== 0
275 : contactProto
.unregisteredAtTimestamp
);
276 account
.getRecipientStore().storeContact(connection
, recipientId
, newContact
.build());
279 final var profile
= recipient
.getProfile();
280 final var profileGivenName
= profile
== null ?
null : profile
.getGivenName();
281 final var profileFamilyName
= profile
== null ?
null : profile
.getFamilyName();
282 if (!Objects
.equals(nullIfEmpty(contactProto
.givenName
), profileGivenName
) || !Objects
.equals(nullIfEmpty(
283 contactProto
.familyName
), profileFamilyName
)) {
284 final var profileBuilder
= profile
== null ? Profile
.newBuilder() : Profile
.newBuilder(profile
);
285 final var newProfile
= profileBuilder
.withGivenName(nullIfEmpty(contactProto
.givenName
))
286 .withFamilyName(nullIfEmpty(contactProto
.familyName
))
288 account
.getRecipientStore().storeProfile(connection
, recipientId
, newProfile
);
290 if (contactProto
.profileKey
.size() > 0) {
292 logger
.trace("Storing profile key {}", recipientId
);
293 final var profileKey
= new ProfileKey(contactProto
.profileKey
.toByteArray());
294 account
.getRecipientStore().storeProfileKey(connection
, recipientId
, profileKey
);
295 } catch (InvalidInputException e
) {
296 logger
.warn("Received invalid contact profile key from storage");
299 if (contactProto
.identityKey
.size() > 0 && address
.aci().isPresent()) {
301 logger
.trace("Storing identity key {}", recipientId
);
302 final var identityKey
= new IdentityKey(contactProto
.identityKey
.toByteArray());
303 account
.getIdentityKeyStore().saveIdentity(connection
, address
.aci().get(), identityKey
);
305 final var trustLevel
= StorageSyncModels
.remoteToLocal(contactProto
.identityState
);
306 if (trustLevel
!= null) {
307 account
.getIdentityKeyStore()
308 .setIdentityTrustLevel(connection
, address
.aci().get(), identityKey
, trustLevel
);
310 } catch (InvalidKeyException e
) {
311 logger
.warn("Received invalid contact identity key from storage");
314 account
.getRecipientStore()
315 .storeStorageRecord(connection
, recipientId
, contactRecord
.getId(), contactProto
.encode());
318 private static RecipientAddress
getRecipientAddress(final ContactRecord contactRecord
) {
319 return new RecipientAddress(ACI
.parseOrNull(contactRecord
.aci
),
320 PNI
.parseOrNull(contactRecord
.pni
),
321 nullIfEmpty(contactRecord
.e164
),
322 nullIfEmpty(contactRecord
.username
));
326 public int compare(SignalContactRecord lhsRecord
, SignalContactRecord rhsRecord
) {
327 final var lhs
= lhsRecord
.getProto();
328 final var rhs
= rhsRecord
.getProto();
329 if ((!lhs
.aci
.isEmpty() && Objects
.equals(lhs
.aci
, rhs
.aci
)) || (
330 !lhs
.e164
.isEmpty() && Objects
.equals(lhs
.e164
, rhs
.e164
)
331 ) || (!lhs
.pni
.isEmpty() && Objects
.equals(lhs
.pni
, rhs
.pni
))) {
338 private static boolean isValidE164(String value
) {
339 return E164_PATTERN
.matcher(value
).matches();
342 private static boolean doProtosMatch(ContactRecord merged
, ContactRecord other
) {
343 return Arrays
.equals(merged
.encode(), other
.encode());