]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/syncStorage/ContactRecordProcessor.java
6114a0184561208388b6e1b068c93e185efcc361
[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 contactGivenName = contact == null ? null : contact.givenName();
237 final var contactFamilyName = contact == null ? null : contact.familyName();
238 if (blocked != contactRecord.isBlocked()
239 || profileShared != contactRecord.isProfileSharingEnabled()
240 || archived != contactRecord.isArchived()
241 || hidden != contactRecord.isHidden()
242 || !Objects.equals(contactRecord.getSystemGivenName().orElse(null), contactGivenName)
243 || !Objects.equals(contactRecord.getSystemFamilyName().orElse(null), contactFamilyName)) {
244 logger.debug("Storing new or updated contact {}", recipientId);
245 final var contactBuilder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact);
246 final var newContact = contactBuilder.withIsBlocked(contactRecord.isBlocked())
247 .withIsProfileSharingEnabled(contactRecord.isProfileSharingEnabled())
248 .withIsArchived(contactRecord.isArchived())
249 .withIsHidden(contactRecord.isHidden())
250 .withGivenName(contactRecord.getSystemGivenName().orElse(null))
251 .withFamilyName(contactRecord.getSystemFamilyName().orElse(null));
252 account.getRecipientStore().storeContact(connection, recipientId, newContact.build());
253 }
254
255 final var profile = recipient.getProfile();
256 final var profileGivenName = profile == null ? null : profile.getGivenName();
257 final var profileFamilyName = profile == null ? null : profile.getFamilyName();
258 if (!Objects.equals(contactRecord.getProfileGivenName().orElse(null), profileGivenName) || !Objects.equals(
259 contactRecord.getProfileFamilyName().orElse(null),
260 profileFamilyName)) {
261 final var profileBuilder = profile == null ? Profile.newBuilder() : Profile.newBuilder(profile);
262 final var newProfile = profileBuilder.withGivenName(contactRecord.getProfileGivenName().orElse(null))
263 .withFamilyName(contactRecord.getProfileFamilyName().orElse(null))
264 .build();
265 account.getRecipientStore().storeProfile(connection, recipientId, newProfile);
266 }
267 if (contactRecord.getProfileKey().isPresent()) {
268 try {
269 logger.trace("Storing profile key {}", recipientId);
270 final var profileKey = new ProfileKey(contactRecord.getProfileKey().get());
271 account.getRecipientStore().storeProfileKey(connection, recipientId, profileKey);
272 } catch (InvalidInputException e) {
273 logger.warn("Received invalid contact profile key from storage");
274 }
275 }
276 if (contactRecord.getIdentityKey().isPresent() && contactRecord.getAci().isPresent()) {
277 try {
278 logger.trace("Storing identity key {}", recipientId);
279 final var identityKey = new IdentityKey(contactRecord.getIdentityKey().get());
280 account.getIdentityKeyStore()
281 .saveIdentity(connection, contactRecord.getAci().orElse(null), identityKey);
282
283 final var trustLevel = StorageSyncModels.remoteToLocal(contactRecord.getIdentityState());
284 if (trustLevel != null) {
285 account.getIdentityKeyStore()
286 .setIdentityTrustLevel(connection,
287 contactRecord.getAci().orElse(null),
288 identityKey,
289 trustLevel);
290 }
291 } catch (InvalidKeyException e) {
292 logger.warn("Received invalid contact identity key from storage");
293 }
294 }
295 account.getRecipientStore()
296 .storeStorageRecord(connection, recipientId, contactRecord.getId(), contactRecord.toProto().encode());
297 }
298
299 private static RecipientAddress getRecipientAddress(final SignalContactRecord contactRecord) {
300 return new RecipientAddress(contactRecord.getAci().orElse(null),
301 contactRecord.getPni().orElse(null),
302 contactRecord.getNumber().orElse(null),
303 contactRecord.getUsername().orElse(null));
304 }
305
306 @Override
307 public int compare(SignalContactRecord lhs, SignalContactRecord rhs) {
308 if ((lhs.getAci().isPresent() && Objects.equals(lhs.getAci(), rhs.getAci())) || (
309 lhs.getNumber().isPresent() && Objects.equals(lhs.getNumber(), rhs.getNumber())
310 ) || (lhs.getPni().isPresent() && Objects.equals(lhs.getPni(), rhs.getPni()))) {
311 return 0;
312 } else {
313 return 1;
314 }
315 }
316
317 private static boolean isValidE164(String value) {
318 return E164_PATTERN.matcher(value).matches();
319 }
320
321 private static boolean doProtosMatch(SignalContactRecord merged, SignalContactRecord other) {
322 return Arrays.equals(merged.toProto().encode(), other.toProto().encode());
323 }
324 }