]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/syncStorage/ContactRecordProcessor.java
2e557e1a015e240f6b252d502f2eb14341fdc1ea
[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.getIdentityState() != local.getIdentityState() && remote.getIdentityKey().isPresent())
109 || (remote.getIdentityKey().isPresent() && local.getIdentityKey().isEmpty())) {
110 identityState = remote.getIdentityState();
111 identityKey = remote.getIdentityKey().get();
112 } else {
113 identityState = local.getIdentityState();
114 identityKey = local.getIdentityKey().orElse(null);
115 }
116
117 if (local.getAci().isPresent() && identityKey != null && remote.getIdentityKey().isPresent() && !Arrays.equals(
118 identityKey,
119 remote.getIdentityKey().get())) {
120 logger.debug("The local and remote identity keys do not match for {}. Enqueueing a profile fetch.",
121 local.getAci().orElse(null));
122 final var address = getRecipientAddress(local);
123 jobExecutor.enqueueJob(new DownloadProfileJob(address));
124 }
125
126 final var e164sMatchButPnisDont = local.getNumber().isPresent()
127 && local.getNumber()
128 .get()
129 .equals(remote.getNumber().orElse(null))
130 && local.getPni().isPresent()
131 && remote.getPni().isPresent()
132 && !local.getPni().get().equals(remote.getPni().get());
133
134 final var pnisMatchButE164sDont = local.getPni().isPresent()
135 && local.getPni()
136 .get()
137 .equals(remote.getPni().orElse(null))
138 && local.getNumber().isPresent()
139 && remote.getNumber().isPresent()
140 && !local.getNumber().get().equals(remote.getNumber().get());
141
142 PNI pni;
143 String e164;
144 if (e164sMatchButPnisDont) {
145 logger.debug("Matching E164s, but the PNIs differ! Trusting our local pair.");
146 // TODO [pnp] Schedule CDS fetch?
147 pni = local.getPni().get();
148 e164 = local.getNumber().get();
149 } else if (pnisMatchButE164sDont) {
150 logger.debug("Matching PNIs, but the E164s differ! Trusting our local pair.");
151 // TODO [pnp] Schedule CDS fetch?
152 pni = local.getPni().get();
153 e164 = local.getNumber().get();
154 } else {
155 pni = OptionalUtil.or(remote.getPni(), local.getPni()).orElse(null);
156 e164 = OptionalUtil.or(remote.getNumber(), local.getNumber()).orElse(null);
157 }
158
159 final var unknownFields = remote.serializeUnknownFields();
160 final var aci = local.getAci().isEmpty() ? remote.getAci().orElse(null) : local.getAci().get();
161 final var profileKey = OptionalUtil.or(remote.getProfileKey(), local.getProfileKey()).orElse(null);
162 final var username = OptionalUtil.or(remote.getUsername(), local.getUsername()).orElse("");
163 final var blocked = remote.isBlocked();
164 final var profileSharing = remote.isProfileSharingEnabled();
165 final var archived = remote.isArchived();
166 final var forcedUnread = remote.isForcedUnread();
167 final var muteUntil = remote.getMuteUntil();
168 final var hideStory = remote.shouldHideStory();
169 final var unregisteredTimestamp = remote.getUnregisteredTimestamp();
170 final var hidden = remote.isHidden();
171 final var systemGivenName = account.isPrimaryDevice()
172 ? local.getSystemGivenName().orElse("")
173 : remote.getSystemGivenName().orElse("");
174 final var systemFamilyName = account.isPrimaryDevice()
175 ? local.getSystemFamilyName().orElse("")
176 : remote.getSystemFamilyName().orElse("");
177 final var systemNickname = remote.getSystemNickname().orElse("");
178
179 final var mergedBuilder = new SignalContactRecord.Builder(remote.getId().getRaw(), aci, unknownFields).setE164(
180 e164)
181 .setPni(pni)
182 .setProfileGivenName(profileGivenName)
183 .setProfileFamilyName(profileFamilyName)
184 .setSystemGivenName(systemGivenName)
185 .setSystemFamilyName(systemFamilyName)
186 .setSystemNickname(systemNickname)
187 .setProfileKey(profileKey)
188 .setUsername(username)
189 .setIdentityState(identityState)
190 .setIdentityKey(identityKey)
191 .setBlocked(blocked)
192 .setProfileSharingEnabled(profileSharing)
193 .setArchived(archived)
194 .setForcedUnread(forcedUnread)
195 .setMuteUntil(muteUntil)
196 .setHideStory(hideStory)
197 .setUnregisteredTimestamp(unregisteredTimestamp)
198 .setHidden(hidden);
199 final var merged = mergedBuilder.build();
200
201 final var matchesRemote = doProtosMatch(merged, remote);
202 if (matchesRemote) {
203 return remote;
204 }
205
206 final var matchesLocal = doProtosMatch(merged, local);
207 if (matchesLocal) {
208 return local;
209 }
210
211 return mergedBuilder.setId(KeyUtils.createRawStorageId()).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 address = getRecipientAddress(contactRecord);
224 final var recipientId = account.getRecipientStore().resolveRecipientTrusted(connection, address);
225 final var recipient = account.getRecipientStore().getRecipient(connection, recipientId);
226
227 final var contact = recipient.getContact();
228 final var blocked = contact != null && contact.isBlocked();
229 final var profileShared = contact != null && contact.isProfileSharingEnabled();
230 final var archived = contact != null && contact.isArchived();
231 final var hidden = contact != null && contact.isHidden();
232 final var contactGivenName = contact == null ? null : contact.givenName();
233 final var contactFamilyName = contact == null ? null : contact.familyName();
234 if (blocked != contactRecord.isBlocked()
235 || profileShared != contactRecord.isProfileSharingEnabled()
236 || archived != contactRecord.isArchived()
237 || hidden != contactRecord.isHidden()
238 || (
239 contactRecord.getSystemGivenName().isPresent() && !contactRecord.getSystemGivenName()
240 .get()
241 .equals(contactGivenName)
242 )
243 || (
244 contactRecord.getSystemFamilyName().isPresent() && !contactRecord.getSystemFamilyName()
245 .get()
246 .equals(contactFamilyName)
247 )) {
248 logger.debug("Storing new or updated contact {}", recipientId);
249 final var contactBuilder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact);
250 final var newContact = contactBuilder.withIsBlocked(contactRecord.isBlocked())
251 .withIsProfileSharingEnabled(contactRecord.isProfileSharingEnabled())
252 .withIsArchived(contactRecord.isArchived())
253 .withIsHidden(contactRecord.isHidden());
254 if (contactRecord.getSystemGivenName().isPresent() || contactRecord.getSystemFamilyName().isPresent()) {
255 newContact.withGivenName(contactRecord.getSystemGivenName().orElse(null))
256 .withFamilyName(contactRecord.getSystemFamilyName().orElse(null));
257 }
258 account.getRecipientStore().storeContact(connection, recipientId, newContact.build());
259 }
260
261 final var profile = recipient.getProfile();
262 final var profileGivenName = profile == null ? null : profile.getGivenName();
263 final var profileFamilyName = profile == null ? null : profile.getFamilyName();
264 if ((
265 contactRecord.getProfileGivenName().isPresent() && !contactRecord.getProfileGivenName()
266 .get()
267 .equals(profileGivenName)
268 ) || (
269 contactRecord.getProfileFamilyName().isPresent() && !contactRecord.getProfileFamilyName()
270 .get()
271 .equals(profileFamilyName)
272 )) {
273 final var profileBuilder = profile == null ? Profile.newBuilder() : Profile.newBuilder(profile);
274 final var newProfile = profileBuilder.withGivenName(contactRecord.getProfileGivenName().orElse(null))
275 .withFamilyName(contactRecord.getProfileFamilyName().orElse(null))
276 .build();
277 account.getRecipientStore().storeProfile(connection, recipientId, newProfile);
278 }
279 if (contactRecord.getProfileKey().isPresent()) {
280 try {
281 logger.trace("Storing profile key {}", recipientId);
282 final var profileKey = new ProfileKey(contactRecord.getProfileKey().get());
283 account.getRecipientStore().storeProfileKey(connection, recipientId, profileKey);
284 } catch (InvalidInputException e) {
285 logger.warn("Received invalid contact profile key from storage");
286 }
287 }
288 if (contactRecord.getIdentityKey().isPresent() && contactRecord.getAci().orElse(null) != null) {
289 try {
290 logger.trace("Storing identity key {}", recipientId);
291 final var identityKey = new IdentityKey(contactRecord.getIdentityKey().get());
292 account.getIdentityKeyStore()
293 .saveIdentity(connection, contactRecord.getAci().orElse(null), identityKey);
294
295 final var trustLevel = StorageSyncModels.remoteToLocal(contactRecord.getIdentityState());
296 if (trustLevel != null) {
297 account.getIdentityKeyStore()
298 .setIdentityTrustLevel(connection,
299 contactRecord.getAci().orElse(null),
300 identityKey,
301 trustLevel);
302 }
303 } catch (InvalidKeyException e) {
304 logger.warn("Received invalid contact identity key from storage");
305 }
306 }
307 account.getRecipientStore()
308 .storeStorageRecord(connection, recipientId, contactRecord.getId(), contactRecord.toProto().encode());
309 }
310
311 private static RecipientAddress getRecipientAddress(final SignalContactRecord contactRecord) {
312 return new RecipientAddress(contactRecord.getAci().orElse(null),
313 contactRecord.getPni().orElse(null),
314 contactRecord.getNumber().orElse(null),
315 contactRecord.getUsername().orElse(null));
316 }
317
318 @Override
319 public int compare(SignalContactRecord lhs, SignalContactRecord rhs) {
320 if ((lhs.getAci().isPresent() && Objects.equals(lhs.getAci(), rhs.getAci())) || (
321 lhs.getNumber().isPresent() && Objects.equals(lhs.getNumber(), rhs.getNumber())
322 ) || (lhs.getPni().isPresent() && Objects.equals(lhs.getPni(), rhs.getPni()))) {
323 return 0;
324 } else {
325 return 1;
326 }
327 }
328
329 private static boolean isValidE164(String value) {
330 return E164_PATTERN.matcher(value).matches();
331 }
332
333 private static boolean doProtosMatch(SignalContactRecord merged, SignalContactRecord other) {
334 return Arrays.equals(merged.toProto().encode(), other.toProto().encode());
335 }
336 }