]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/syncStorage/ContactRecordProcessor.java
2863874b0772b28508c096c854bb745c802d24f3
[signal-cli] / lib / src / main / java / org / asamk / signal / manager / syncStorage / ContactRecordProcessor.java
1 package org.asamk.signal.manager.syncStorage;
2
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;
23
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;
30
31 import okio.ByteString;
32
33 import static org.asamk.signal.manager.util.Utils.firstNonEmpty;
34 import static org.asamk.signal.manager.util.Utils.nullIfEmpty;
35
36 public class ContactRecordProcessor extends DefaultStorageRecordProcessor<SignalContactRecord> {
37
38 private static final Logger logger = LoggerFactory.getLogger(ContactRecordProcessor.class);
39
40 private static final Pattern E164_PATTERN = Pattern.compile("^\\+[1-9]\\d{0,18}$");
41
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;
48
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();
56 }
57
58 /**
59 * Error cases:
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.
62 */
63 @Override
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();
71
72 if (!hasAci && !hasPni) {
73 logger.debug("Found a ContactRecord with neither an ACI nor a PNI -- marking as invalid.");
74 return true;
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.");
79 return true;
80 } else if (e164 != null && !isValidE164(e164)) {
81 logger.debug("Found a record with an invalid E164 ({}). Marking as invalid.", e164);
82 return true;
83 } else {
84 return false;
85 }
86 }
87
88 @Override
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);
93
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);
97
98 return Optional.of(new SignalContactRecord(storageId,
99 StorageSyncModels.localToRemoteRecord(recipient, identity)));
100 }
101
102 @Override
103 protected SignalContactRecord merge(SignalContactRecord remoteRecord, SignalContactRecord localRecord) {
104 final var remote = remoteRecord.getProto();
105 final var local = localRecord.getProto();
106
107 String profileGivenName;
108 String profileFamilyName;
109 if (!remote.givenName.isEmpty() || !remote.familyName.isEmpty()) {
110 profileGivenName = remote.givenName;
111 profileFamilyName = remote.familyName;
112 } else {
113 profileGivenName = local.givenName;
114 profileFamilyName = local.familyName;
115 }
116
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
123
124 )) {
125 identityState = remote.identityState;
126 identityKey = remote.identityKey;
127 } else {
128 identityState = local.identityState;
129 identityKey = local.identityKey.size() > 0 ? local.identityKey : ByteString.EMPTY;
130 }
131
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.",
137 local.aci);
138 final var address = getRecipientAddress(local);
139 jobExecutor.enqueueJob(new DownloadProfileJob(address));
140 }
141
142 String pni;
143 String e164;
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);
150
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);
156
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.");
162 }
163 jobExecutor.enqueueJob(new RefreshRecipientsJob());
164 pni = local.pni;
165 e164 = local.e164;
166 } else {
167 pni = firstNonEmpty(remote.pni, local.pni);
168 e164 = firstNonEmpty(remote.e164, local.e164);
169 }
170 } else {
171 pni = firstNonEmpty(remote.pni, local.pni);
172 e164 = firstNonEmpty(remote.e164, local.e164);
173 }
174
175 final var mergedBuilder = SignalContactRecord.Companion.newBuilder(remote.unknownFields().toByteArray())
176 .aci(local.aci.isEmpty() ? remote.aci : local.aci)
177 .e164(e164)
178 .pni(pni)
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)
198 .note(remote.note);
199 final var merged = mergedBuilder.build();
200
201 final var matchesRemote = doProtosMatch(merged, remote);
202 if (matchesRemote) {
203 return remoteRecord;
204 }
205
206 final var matchesLocal = doProtosMatch(merged, local);
207 if (matchesLocal) {
208 return localRecord;
209 }
210
211 return new SignalContactRecord(StorageId.forContact(KeyUtils.createRawStorageId()), mergedBuilder.build());
212 }
213
214 @Override
215 protected void insertLocal(SignalContactRecord record) throws SQLException {
216 StorageRecordUpdate<SignalContactRecord> update = new StorageRecordUpdate<>(null, record);
217 updateLocal(update);
218 }
219
220 @Override
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);
227
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
236 ? 0
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.nickname == null
271 ? null
272 : contactProto.nickname.given))
273 .withNickNameFamilyName(nullIfEmpty(contactProto.nickname == null
274 ? null
275 : contactProto.nickname.family))
276 .withNote(nullIfEmpty(contactProto.note))
277 .withUnregisteredTimestamp(contactProto.unregisteredAtTimestamp == 0
278 ? null
279 : contactProto.unregisteredAtTimestamp);
280 account.getRecipientStore().storeContact(connection, recipientId, newContact.build());
281 }
282
283 final var profile = recipient.getProfile();
284 final var profileGivenName = profile == null ? null : profile.getGivenName();
285 final var profileFamilyName = profile == null ? null : profile.getFamilyName();
286 if (!Objects.equals(nullIfEmpty(contactProto.givenName), profileGivenName) || !Objects.equals(nullIfEmpty(
287 contactProto.familyName), profileFamilyName)) {
288 final var profileBuilder = profile == null ? Profile.newBuilder() : Profile.newBuilder(profile);
289 final var newProfile = profileBuilder.withGivenName(nullIfEmpty(contactProto.givenName))
290 .withFamilyName(nullIfEmpty(contactProto.familyName))
291 .build();
292 account.getRecipientStore().storeProfile(connection, recipientId, newProfile);
293 }
294 if (contactProto.profileKey.size() > 0) {
295 try {
296 logger.trace("Storing profile key {}", recipientId);
297 final var profileKey = new ProfileKey(contactProto.profileKey.toByteArray());
298 account.getRecipientStore().storeProfileKey(connection, recipientId, profileKey);
299 } catch (InvalidInputException e) {
300 logger.warn("Received invalid contact profile key from storage");
301 }
302 }
303 if (contactProto.identityKey.size() > 0 && address.aci().isPresent()) {
304 try {
305 logger.trace("Storing identity key {}", recipientId);
306 final var identityKey = new IdentityKey(contactProto.identityKey.toByteArray());
307 account.getIdentityKeyStore().saveIdentity(connection, address.aci().get(), identityKey);
308
309 final var trustLevel = StorageSyncModels.remoteToLocal(contactProto.identityState);
310 if (trustLevel != null) {
311 account.getIdentityKeyStore()
312 .setIdentityTrustLevel(connection, address.aci().get(), identityKey, trustLevel);
313 }
314 } catch (InvalidKeyException e) {
315 logger.warn("Received invalid contact identity key from storage");
316 }
317 }
318 account.getRecipientStore()
319 .storeStorageRecord(connection, recipientId, contactRecord.getId(), contactProto.encode());
320 }
321
322 private static RecipientAddress getRecipientAddress(final ContactRecord contactRecord) {
323 return new RecipientAddress(ACI.parseOrNull(contactRecord.aci),
324 PNI.parseOrNull(contactRecord.pni),
325 nullIfEmpty(contactRecord.e164),
326 nullIfEmpty(contactRecord.username));
327 }
328
329 @Override
330 public int compare(SignalContactRecord lhsRecord, SignalContactRecord rhsRecord) {
331 final var lhs = lhsRecord.getProto();
332 final var rhs = rhsRecord.getProto();
333 if ((!lhs.aci.isEmpty() && Objects.equals(lhs.aci, rhs.aci)) || (
334 !lhs.e164.isEmpty() && Objects.equals(lhs.e164, rhs.e164)
335 ) || (!lhs.pni.isEmpty() && Objects.equals(lhs.pni, rhs.pni))) {
336 return 0;
337 } else {
338 return 1;
339 }
340 }
341
342 private static boolean isValidE164(String value) {
343 return E164_PATTERN.matcher(value).matches();
344 }
345
346 private static boolean doProtosMatch(ContactRecord merged, ContactRecord other) {
347 return Arrays.equals(merged.encode(), other.encode());
348 }
349 }