]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/helper/StorageHelper.java
fa1eef112af60bf5a9bd2e49cd4f7c0788d46203
[signal-cli] / lib / src / main / java / org / asamk / signal / manager / helper / StorageHelper.java
1 package org.asamk.signal.manager.helper;
2
3 import org.asamk.signal.manager.api.Contact;
4 import org.asamk.signal.manager.api.GroupId;
5 import org.asamk.signal.manager.api.PhoneNumberSharingMode;
6 import org.asamk.signal.manager.api.Profile;
7 import org.asamk.signal.manager.api.TrustLevel;
8 import org.asamk.signal.manager.internal.SignalDependencies;
9 import org.asamk.signal.manager.jobs.CheckWhoAmIJob;
10 import org.asamk.signal.manager.jobs.DownloadProfileAvatarJob;
11 import org.asamk.signal.manager.storage.SignalAccount;
12 import org.asamk.signal.manager.storage.recipients.RecipientAddress;
13 import org.signal.libsignal.protocol.IdentityKey;
14 import org.signal.libsignal.protocol.InvalidKeyException;
15 import org.signal.libsignal.zkgroup.InvalidInputException;
16 import org.signal.libsignal.zkgroup.groups.GroupMasterKey;
17 import org.signal.libsignal.zkgroup.profiles.ProfileKey;
18 import org.slf4j.Logger;
19 import org.slf4j.LoggerFactory;
20 import org.whispersystems.signalservice.api.storage.SignalAccountRecord;
21 import org.whispersystems.signalservice.api.storage.SignalStorageManifest;
22 import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
23 import org.whispersystems.signalservice.api.storage.StorageId;
24 import org.whispersystems.signalservice.internal.storage.protos.ManifestRecord;
25
26 import java.io.IOException;
27 import java.util.ArrayList;
28 import java.util.Collection;
29 import java.util.Collections;
30 import java.util.List;
31 import java.util.Optional;
32 import java.util.stream.Collectors;
33
34 public class StorageHelper {
35
36 private static final Logger logger = LoggerFactory.getLogger(StorageHelper.class);
37
38 private final SignalAccount account;
39 private final SignalDependencies dependencies;
40 private final Context context;
41
42 public StorageHelper(final Context context) {
43 this.account = context.getAccount();
44 this.dependencies = context.getDependencies();
45 this.context = context;
46 }
47
48 public void readDataFromStorage() throws IOException {
49 final var storageKey = account.getOrCreateStorageKey();
50 if (storageKey == null) {
51 logger.debug("Storage key unknown, requesting from primary device.");
52 context.getSyncHelper().requestSyncKeys();
53 return;
54 }
55
56 logger.debug("Reading data from remote storage");
57 Optional<SignalStorageManifest> manifest;
58 try {
59 manifest = dependencies.getAccountManager()
60 .getStorageManifestIfDifferentVersion(storageKey, account.getStorageManifestVersion());
61 } catch (InvalidKeyException e) {
62 logger.warn("Manifest couldn't be decrypted, ignoring.");
63 return;
64 }
65
66 if (manifest.isEmpty()) {
67 logger.debug("Manifest is up to date, does not exist or couldn't be decrypted, ignoring.");
68 return;
69 }
70
71 logger.trace("Remote storage manifest has {} records", manifest.get().getStorageIds().size());
72 final var storageIds = manifest.get()
73 .getStorageIds()
74 .stream()
75 .filter(id -> !id.isUnknown())
76 .collect(Collectors.toSet());
77
78 Optional<SignalStorageManifest> localManifest = account.getStorageManifest();
79 localManifest.ifPresent(m -> m.getStorageIds().forEach(storageIds::remove));
80
81 logger.trace("Reading {} new records", manifest.get().getStorageIds().size());
82 for (final var record : getSignalStorageRecords(storageIds)) {
83 logger.debug("Reading record of type {}", record.getType());
84 if (record.getType() == ManifestRecord.Identifier.Type.ACCOUNT.getValue()) {
85 readAccountRecord(record);
86 } else if (record.getType() == ManifestRecord.Identifier.Type.GROUPV2.getValue()) {
87 readGroupV2Record(record);
88 } else if (record.getType() == ManifestRecord.Identifier.Type.GROUPV1.getValue()) {
89 readGroupV1Record(record);
90 } else if (record.getType() == ManifestRecord.Identifier.Type.CONTACT.getValue()) {
91 readContactRecord(record);
92 }
93 }
94 account.setStorageManifestVersion(manifest.get().getVersion());
95 account.setStorageManifest(manifest.get());
96 logger.debug("Done reading data from remote storage");
97 }
98
99 private void readContactRecord(final SignalStorageRecord record) {
100 if (record == null || record.getContact().isEmpty()) {
101 return;
102 }
103
104 final var contactRecord = record.getContact().get();
105 final var aci = contactRecord.getAci().orElse(null);
106 final var pni = contactRecord.getPni().orElse(null);
107 if (contactRecord.getNumber().isEmpty() && aci == null && pni == null) {
108 return;
109 }
110 final var address = new RecipientAddress(aci, pni, contactRecord.getNumber().orElse(null));
111 var recipientId = account.getRecipientResolver().resolveRecipient(address);
112 if (aci != null && contactRecord.getUsername().isPresent()) {
113 recipientId = account.getRecipientTrustedResolver()
114 .resolveRecipientTrusted(aci, contactRecord.getUsername().get());
115 }
116
117 final var contact = account.getContactStore().getContact(recipientId);
118 final var blocked = contact != null && contact.isBlocked();
119 final var profileShared = contact != null && contact.isProfileSharingEnabled();
120 final var archived = contact != null && contact.isArchived();
121 final var hidden = contact != null && contact.isHidden();
122 final var contactGivenName = contact == null ? null : contact.givenName();
123 final var contactFamilyName = contact == null ? null : contact.familyName();
124 if (blocked != contactRecord.isBlocked()
125 || profileShared != contactRecord.isProfileSharingEnabled()
126 || archived != contactRecord.isArchived()
127 || hidden != contactRecord.isHidden()
128 || (
129 contactRecord.getSystemGivenName().isPresent() && !contactRecord.getSystemGivenName()
130 .get()
131 .equals(contactGivenName)
132 )
133 || (
134 contactRecord.getSystemFamilyName().isPresent() && !contactRecord.getSystemFamilyName()
135 .get()
136 .equals(contactFamilyName)
137 )) {
138 logger.debug("Storing new or updated contact {}", recipientId);
139 final var contactBuilder = contact == null ? Contact.newBuilder() : Contact.newBuilder(contact);
140 final var newContact = contactBuilder.withIsBlocked(contactRecord.isBlocked())
141 .withIsProfileSharingEnabled(contactRecord.isProfileSharingEnabled())
142 .withIsArchived(contactRecord.isArchived())
143 .withIsHidden(contactRecord.isHidden());
144 if (contactRecord.getSystemGivenName().isPresent() || contactRecord.getSystemFamilyName().isPresent()) {
145 newContact.withGivenName(contactRecord.getSystemGivenName().orElse(null))
146 .withFamilyName(contactRecord.getSystemFamilyName().orElse(null));
147 }
148 account.getContactStore().storeContact(recipientId, newContact.build());
149 }
150
151 final var profile = account.getProfileStore().getProfile(recipientId);
152 final var profileGivenName = profile == null ? null : profile.getGivenName();
153 final var profileFamilyName = profile == null ? null : profile.getFamilyName();
154 if ((
155 contactRecord.getProfileGivenName().isPresent() && !contactRecord.getProfileGivenName()
156 .get()
157 .equals(profileGivenName)
158 ) || (
159 contactRecord.getProfileFamilyName().isPresent() && !contactRecord.getProfileFamilyName()
160 .get()
161 .equals(profileFamilyName)
162 )) {
163 final var profileBuilder = profile == null ? Profile.newBuilder() : Profile.newBuilder(profile);
164 final var newProfile = profileBuilder.withGivenName(contactRecord.getProfileGivenName().orElse(null))
165 .withFamilyName(contactRecord.getProfileFamilyName().orElse(null))
166 .build();
167 account.getProfileStore().storeProfile(recipientId, newProfile);
168 }
169 if (contactRecord.getProfileKey().isPresent()) {
170 try {
171 logger.trace("Storing profile key {}", recipientId);
172 final var profileKey = new ProfileKey(contactRecord.getProfileKey().get());
173 account.getProfileStore().storeProfileKey(recipientId, profileKey);
174 } catch (InvalidInputException e) {
175 logger.warn("Received invalid contact profile key from storage");
176 }
177 }
178 if (contactRecord.getIdentityKey().isPresent() && aci != null) {
179 try {
180 logger.trace("Storing identity key {}", recipientId);
181 final var identityKey = new IdentityKey(contactRecord.getIdentityKey().get());
182 account.getIdentityKeyStore().saveIdentity(aci, identityKey);
183
184 final var trustLevel = TrustLevel.fromIdentityState(contactRecord.getIdentityState());
185 if (trustLevel != null) {
186 account.getIdentityKeyStore().setIdentityTrustLevel(aci, identityKey, trustLevel);
187 }
188 } catch (InvalidKeyException e) {
189 logger.warn("Received invalid contact identity key from storage");
190 }
191 }
192 }
193
194 private void readGroupV1Record(final SignalStorageRecord record) {
195 if (record == null || record.getGroupV1().isEmpty()) {
196 return;
197 }
198
199 final var groupV1Record = record.getGroupV1().get();
200 final var groupIdV1 = GroupId.v1(groupV1Record.getGroupId());
201
202 var group = account.getGroupStore().getGroup(groupIdV1);
203 if (group == null) {
204 try {
205 context.getGroupHelper().sendGroupInfoRequest(groupIdV1, account.getSelfRecipientId());
206 } catch (Throwable e) {
207 logger.warn("Failed to send group request", e);
208 }
209 group = account.getGroupStore().getOrCreateGroupV1(groupIdV1);
210 }
211 if (group != null && group.isBlocked() != groupV1Record.isBlocked()) {
212 group.setBlocked(groupV1Record.isBlocked());
213 account.getGroupStore().updateGroup(group);
214 }
215 }
216
217 private void readGroupV2Record(final SignalStorageRecord record) {
218 if (record == null || record.getGroupV2().isEmpty()) {
219 return;
220 }
221
222 final var groupV2Record = record.getGroupV2().get();
223 if (groupV2Record.isArchived()) {
224 return;
225 }
226
227 final GroupMasterKey groupMasterKey;
228 try {
229 groupMasterKey = new GroupMasterKey(groupV2Record.getMasterKeyBytes());
230 } catch (InvalidInputException e) {
231 logger.warn("Received invalid group master key from storage");
232 return;
233 }
234
235 final var group = context.getGroupHelper().getOrMigrateGroup(groupMasterKey, 0, null);
236 if (group.isBlocked() != groupV2Record.isBlocked()) {
237 group.setBlocked(groupV2Record.isBlocked());
238 account.getGroupStore().updateGroup(group);
239 }
240 }
241
242 private void readAccountRecord(final SignalStorageRecord record) throws IOException {
243 if (record == null) {
244 logger.warn("Could not find account record, even though we had an ID, ignoring.");
245 return;
246 }
247
248 SignalAccountRecord accountRecord = record.getAccount().orElse(null);
249 if (accountRecord == null) {
250 logger.warn("The storage record didn't actually have an account, ignoring.");
251 return;
252 }
253
254 if (!accountRecord.getE164().equals(account.getNumber())) {
255 context.getJobExecutor().enqueueJob(new CheckWhoAmIJob());
256 }
257
258 account.getConfigurationStore().setReadReceipts(accountRecord.isReadReceiptsEnabled());
259 account.getConfigurationStore().setTypingIndicators(accountRecord.isTypingIndicatorsEnabled());
260 account.getConfigurationStore()
261 .setUnidentifiedDeliveryIndicators(accountRecord.isSealedSenderIndicatorsEnabled());
262 account.getConfigurationStore().setLinkPreviews(accountRecord.isLinkPreviewsEnabled());
263 account.getConfigurationStore().setPhoneNumberSharingMode(switch (accountRecord.getPhoneNumberSharingMode()) {
264 case EVERYBODY -> PhoneNumberSharingMode.EVERYBODY;
265 case NOBODY, UNKNOWN -> PhoneNumberSharingMode.NOBODY;
266 });
267 account.getConfigurationStore().setPhoneNumberUnlisted(accountRecord.isPhoneNumberUnlisted());
268 account.setUsername(accountRecord.getUsername());
269
270 if (accountRecord.getProfileKey().isPresent()) {
271 ProfileKey profileKey;
272 try {
273 profileKey = new ProfileKey(accountRecord.getProfileKey().get());
274 } catch (InvalidInputException e) {
275 logger.warn("Received invalid profile key from storage");
276 profileKey = null;
277 }
278 if (profileKey != null) {
279 account.setProfileKey(profileKey);
280 final var avatarPath = accountRecord.getAvatarUrlPath().orElse(null);
281 context.getJobExecutor().enqueueJob(new DownloadProfileAvatarJob(avatarPath));
282 }
283 }
284
285 context.getProfileHelper()
286 .setProfile(false,
287 false,
288 accountRecord.getGivenName().orElse(null),
289 accountRecord.getFamilyName().orElse(null),
290 null,
291 null,
292 null,
293 null);
294 }
295
296 private SignalStorageRecord getSignalStorageRecord(final StorageId accountId) throws IOException {
297 List<SignalStorageRecord> records;
298 try {
299 records = dependencies.getAccountManager()
300 .readStorageRecords(account.getStorageKey(), Collections.singletonList(accountId));
301 } catch (InvalidKeyException e) {
302 logger.warn("Failed to read storage records, ignoring.");
303 return null;
304 }
305 return !records.isEmpty() ? records.get(0) : null;
306 }
307
308 private List<SignalStorageRecord> getSignalStorageRecords(final Collection<StorageId> storageIds) throws IOException {
309 List<SignalStorageRecord> records;
310 try {
311 records = dependencies.getAccountManager()
312 .readStorageRecords(account.getStorageKey(), new ArrayList<>(storageIds));
313 } catch (InvalidKeyException e) {
314 logger.warn("Failed to read storage records, ignoring.");
315 return List.of();
316 }
317 return records;
318 }
319 }