]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/syncStorage/ContactRecordProcessor.java
28e7b6635f177e814f6e33ce068120d2e3c0c374
[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.util.OptionalUtil;
21 import org.whispersystems.signalservice.internal.storage.protos.ContactRecord.IdentityState;
22
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;
29
30 public class ContactRecordProcessor extends DefaultStorageRecordProcessor<SignalContactRecord> {
31
32 private static final Logger logger = LoggerFactory.getLogger(ContactRecordProcessor.class);
33
34 private static final Pattern E164_PATTERN = Pattern.compile("^\\+[1-9]\\d{0,18}$");
35
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;
42
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();
50 }
51
52 /**
53 * Error cases:
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.
56 */
57 @Override
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();
61
62 if (!hasAci && !hasPni) {
63 logger.debug("Found a ContactRecord with neither an ACI nor a PNI -- marking as invalid.");
64 return true;
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.");
69 return true;
70 } else if (remote.getNumber().isPresent() && !isValidE164(remote.getNumber().get())) {
71 logger.debug("Found a record with an invalid E164. Marking as invalid.");
72 return true;
73 } else {
74 return false;
75 }
76 }
77
78 @Override
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);
83
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);
87
88 return Optional.of(StorageSyncModels.localToRemoteRecord(recipient, identity, storageId.getRaw())
89 .getContact()
90 .get());
91 }
92
93 @Override
94 protected SignalContactRecord merge(
95 SignalContactRecord remote, SignalContactRecord local
96 ) {
97 String profileGivenName;
98 String profileFamilyName;
99 if (remote.getProfileGivenName().isPresent() || remote.getProfileFamilyName().isPresent()) {
100 profileGivenName = remote.getProfileGivenName().orElse("");
101 profileFamilyName = remote.getProfileFamilyName().orElse("");
102 } else {
103 profileGivenName = local.getProfileGivenName().orElse("");
104 profileFamilyName = local.getProfileFamilyName().orElse("");
105 }
106
107 String nicknameGivenName;
108 String nicknameFamilyName;
109 if (remote.getNicknameGivenName().isPresent()) {
110 nicknameGivenName = remote.getNicknameGivenName().orElse("");
111 nicknameFamilyName = remote.getNicknameFamilyName().orElse("");
112 } else {
113 nicknameGivenName = local.getNicknameGivenName().orElse("");
114 nicknameFamilyName = local.getNicknameFamilyName().orElse("");
115 }
116
117 if (nicknameGivenName.isBlank() && !nicknameFamilyName.isBlank()) {
118 logger.debug("Processed invalid nickname. Missing given name.");
119
120 nicknameGivenName = "";
121 nicknameFamilyName = "";
122 }
123
124 IdentityState identityState;
125 byte[] identityKey;
126 if (remote.getIdentityKey().isPresent() && (
127 remote.getIdentityState() != local.getIdentityState()
128 || local.getIdentityKey().isEmpty()
129 || !account.isPrimaryDevice()
130
131 )) {
132 identityState = remote.getIdentityState();
133 identityKey = remote.getIdentityKey().get();
134 } else {
135 identityState = local.getIdentityState();
136 identityKey = local.getIdentityKey().orElse(null);
137 }
138
139 if (local.getAci().isPresent()
140 && local.getIdentityKey().isPresent()
141 && remote.getIdentityKey().isPresent()
142 && !Arrays.equals(local.getIdentityKey().get(), remote.getIdentityKey().get())) {
143 logger.debug("The local and remote identity keys do not match for {}. Enqueueing a profile fetch.",
144 local.getAci().orElse(null));
145 final var address = getRecipientAddress(local);
146 jobExecutor.enqueueJob(new DownloadProfileJob(address));
147 }
148
149 final var e164sMatchButPnisDont = local.getNumber().isPresent()
150 && local.getNumber()
151 .get()
152 .equals(remote.getNumber().orElse(null))
153 && local.getPni().isPresent()
154 && remote.getPni().isPresent()
155 && !local.getPni().get().equals(remote.getPni().get());
156
157 final var pnisMatchButE164sDont = local.getPni().isPresent()
158 && local.getPni()
159 .get()
160 .equals(remote.getPni().orElse(null))
161 && local.getNumber().isPresent()
162 && remote.getNumber().isPresent()
163 && !local.getNumber().get().equals(remote.getNumber().get());
164
165 PNI pni;
166 String e164;
167 if (!account.isPrimaryDevice() && (e164sMatchButPnisDont || pnisMatchButE164sDont)) {
168 if (e164sMatchButPnisDont) {
169 logger.debug("Matching E164s, but the PNIs differ! Trusting our local pair.");
170 } else if (pnisMatchButE164sDont) {
171 logger.debug("Matching PNIs, but the E164s differ! Trusting our local pair.");
172 }
173 jobExecutor.enqueueJob(new RefreshRecipientsJob());
174 pni = local.getPni().get();
175 e164 = local.getNumber().get();
176 } else {
177 pni = OptionalUtil.or(remote.getPni(), local.getPni()).orElse(null);
178 e164 = OptionalUtil.or(remote.getNumber(), local.getNumber()).orElse(null);
179 }
180
181 final var unknownFields = remote.serializeUnknownFields();
182 final var aci = local.getAci().isEmpty() ? remote.getAci().orElse(null) : local.getAci().get();
183 final var profileKey = OptionalUtil.or(remote.getProfileKey(), local.getProfileKey()).orElse(null);
184 final var username = OptionalUtil.or(remote.getUsername(), local.getUsername()).orElse("");
185 final var blocked = remote.isBlocked();
186 final var profileSharing = remote.isProfileSharingEnabled();
187 final var archived = remote.isArchived();
188 final var forcedUnread = remote.isForcedUnread();
189 final var muteUntil = remote.getMuteUntil();
190 final var hideStory = remote.shouldHideStory();
191 final var unregisteredTimestamp = remote.getUnregisteredTimestamp();
192 final var hidden = remote.isHidden();
193 final var systemGivenName = account.isPrimaryDevice()
194 ? local.getSystemGivenName().orElse("")
195 : remote.getSystemGivenName().orElse("");
196 final var systemFamilyName = account.isPrimaryDevice()
197 ? local.getSystemFamilyName().orElse("")
198 : remote.getSystemFamilyName().orElse("");
199 final var systemNickname = remote.getSystemNickname().orElse("");
200 final var pniSignatureVerified = remote.isPniSignatureVerified() || local.isPniSignatureVerified();
201 final var note = remote.getNote().or(local::getNote).orElse("");
202
203 final var mergedBuilder = new SignalContactRecord.Builder(remote.getId().getRaw(), aci, unknownFields).setE164(
204 e164)
205 .setPni(pni)
206 .setProfileGivenName(profileGivenName)
207 .setProfileFamilyName(profileFamilyName)
208 .setSystemGivenName(systemGivenName)
209 .setSystemFamilyName(systemFamilyName)
210 .setSystemNickname(systemNickname)
211 .setProfileKey(profileKey)
212 .setUsername(username)
213 .setIdentityState(identityState)
214 .setIdentityKey(identityKey)
215 .setBlocked(blocked)
216 .setProfileSharingEnabled(profileSharing)
217 .setArchived(archived)
218 .setForcedUnread(forcedUnread)
219 .setMuteUntil(muteUntil)
220 .setHideStory(hideStory)
221 .setUnregisteredTimestamp(unregisteredTimestamp)
222 .setHidden(hidden)
223 .setPniSignatureVerified(pniSignatureVerified)
224 .setNicknameGivenName(nicknameGivenName)
225 .setNicknameFamilyName(nicknameFamilyName)
226 .setNote(note);
227 final var merged = mergedBuilder.build();
228
229 final var matchesRemote = doProtosMatch(merged, remote);
230 if (matchesRemote) {
231 return remote;
232 }
233
234 final var matchesLocal = doProtosMatch(merged, local);
235 if (matchesLocal) {
236 return local;
237 }
238
239 return mergedBuilder.setId(KeyUtils.createRawStorageId()).build();
240 }
241
242 @Override
243 protected void insertLocal(SignalContactRecord record) throws SQLException {
244 StorageRecordUpdate<SignalContactRecord> update = new StorageRecordUpdate<>(null, record);
245 updateLocal(update);
246 }
247
248 @Override
249 protected void updateLocal(StorageRecordUpdate<SignalContactRecord> update) throws SQLException {
250 final var contactRecord = update.newRecord();
251 final var address = getRecipientAddress(contactRecord);
252 final var recipientId = account.getRecipientStore().resolveRecipientTrusted(connection, address);
253 final var recipient = account.getRecipientStore().getRecipient(connection, recipientId);
254
255 final var contact = recipient.getContact();
256 final var blocked = contact != null && contact.isBlocked();
257 final var profileShared = contact != null && contact.isProfileSharingEnabled();
258 final var archived = contact != null && contact.isArchived();
259 final var hidden = contact != null && contact.isHidden();
260 final var hideStory = contact != null && contact.hideStory();
261 final var muteUntil = contact == null ? 0 : contact.muteUntil();
262 final var unregisteredTimestamp = contact == null || contact.unregisteredTimestamp() == null
263 ? 0
264 : contact.unregisteredTimestamp();
265 final var contactGivenName = contact == null ? null : contact.givenName();
266 final var contactFamilyName = contact == null ? null : contact.familyName();
267 final var contactNickName = contact == null ? null : contact.nickName();
268 if (blocked != contactRecord.isBlocked()
269 || profileShared != contactRecord.isProfileSharingEnabled()
270 || archived != contactRecord.isArchived()
271 || hidden != contactRecord.isHidden()
272 || hideStory != contactRecord.shouldHideStory()
273 || muteUntil != contactRecord.getMuteUntil()
274 || unregisteredTimestamp != contactRecord.getUnregisteredTimestamp()
275 || !Objects.equals(contactRecord.getSystemGivenName().orElse(null), contactGivenName)
276 || !Objects.equals(contactRecord.getSystemFamilyName().orElse(null), contactFamilyName)
277 || !Objects.equals(contactRecord.getSystemNickname().orElse(null), contactNickName)) {
278 logger.debug("Storing new or updated contact {}", recipientId);
279 final var contactBuilder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact);
280 final var newContact = contactBuilder.withIsBlocked(contactRecord.isBlocked())
281 .withIsProfileSharingEnabled(contactRecord.isProfileSharingEnabled())
282 .withIsArchived(contactRecord.isArchived())
283 .withIsHidden(contactRecord.isHidden())
284 .withMuteUntil(contactRecord.getMuteUntil())
285 .withHideStory(contactRecord.shouldHideStory())
286 .withGivenName(contactRecord.getSystemGivenName().orElse(null))
287 .withFamilyName(contactRecord.getSystemFamilyName().orElse(null))
288 .withNickName(contactRecord.getSystemNickname().orElse(null))
289 .withUnregisteredTimestamp(contactRecord.getUnregisteredTimestamp() == 0
290 ? null
291 : contactRecord.getUnregisteredTimestamp());
292 account.getRecipientStore().storeContact(connection, recipientId, newContact.build());
293 }
294
295 final var profile = recipient.getProfile();
296 final var profileGivenName = profile == null ? null : profile.getGivenName();
297 final var profileFamilyName = profile == null ? null : profile.getFamilyName();
298 if (!Objects.equals(contactRecord.getProfileGivenName().orElse(null), profileGivenName) || !Objects.equals(
299 contactRecord.getProfileFamilyName().orElse(null),
300 profileFamilyName)) {
301 final var profileBuilder = profile == null ? Profile.newBuilder() : Profile.newBuilder(profile);
302 final var newProfile = profileBuilder.withGivenName(contactRecord.getProfileGivenName().orElse(null))
303 .withFamilyName(contactRecord.getProfileFamilyName().orElse(null))
304 .build();
305 account.getRecipientStore().storeProfile(connection, recipientId, newProfile);
306 }
307 if (contactRecord.getProfileKey().isPresent()) {
308 try {
309 logger.trace("Storing profile key {}", recipientId);
310 final var profileKey = new ProfileKey(contactRecord.getProfileKey().get());
311 account.getRecipientStore().storeProfileKey(connection, recipientId, profileKey);
312 } catch (InvalidInputException e) {
313 logger.warn("Received invalid contact profile key from storage");
314 }
315 }
316 if (contactRecord.getIdentityKey().isPresent() && contactRecord.getAci().isPresent()) {
317 try {
318 logger.trace("Storing identity key {}", recipientId);
319 final var identityKey = new IdentityKey(contactRecord.getIdentityKey().get());
320 account.getIdentityKeyStore()
321 .saveIdentity(connection, contactRecord.getAci().orElse(null), identityKey);
322
323 final var trustLevel = StorageSyncModels.remoteToLocal(contactRecord.getIdentityState());
324 if (trustLevel != null) {
325 account.getIdentityKeyStore()
326 .setIdentityTrustLevel(connection,
327 contactRecord.getAci().orElse(null),
328 identityKey,
329 trustLevel);
330 }
331 } catch (InvalidKeyException e) {
332 logger.warn("Received invalid contact identity key from storage");
333 }
334 }
335 account.getRecipientStore()
336 .storeStorageRecord(connection, recipientId, contactRecord.getId(), contactRecord.toProto().encode());
337 }
338
339 private static RecipientAddress getRecipientAddress(final SignalContactRecord contactRecord) {
340 return new RecipientAddress(contactRecord.getAci().orElse(null),
341 contactRecord.getPni().orElse(null),
342 contactRecord.getNumber().orElse(null),
343 contactRecord.getUsername().orElse(null));
344 }
345
346 @Override
347 public int compare(SignalContactRecord lhs, SignalContactRecord rhs) {
348 if ((lhs.getAci().isPresent() && Objects.equals(lhs.getAci(), rhs.getAci())) || (
349 lhs.getNumber().isPresent() && Objects.equals(lhs.getNumber(), rhs.getNumber())
350 ) || (lhs.getPni().isPresent() && Objects.equals(lhs.getPni(), rhs.getPni()))) {
351 return 0;
352 } else {
353 return 1;
354 }
355 }
356
357 private static boolean isValidE164(String value) {
358 return E164_PATTERN.matcher(value).matches();
359 }
360
361 private static boolean doProtosMatch(SignalContactRecord merged, SignalContactRecord other) {
362 return Arrays.equals(merged.toProto().encode(), other.toProto().encode());
363 }
364 }