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