1 package org
.asamk
.signal
.manager
.helper
;
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
;
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
;
34 public class StorageHelper
{
36 private static final Logger logger
= LoggerFactory
.getLogger(StorageHelper
.class);
38 private final SignalAccount account
;
39 private final SignalDependencies dependencies
;
40 private final Context context
;
42 public StorageHelper(final Context context
) {
43 this.account
= context
.getAccount();
44 this.dependencies
= context
.getDependencies();
45 this.context
= context
;
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();
56 logger
.debug("Reading data from remote storage");
57 Optional
<SignalStorageManifest
> manifest
;
59 manifest
= dependencies
.getAccountManager()
60 .getStorageManifestIfDifferentVersion(storageKey
, account
.getStorageManifestVersion());
61 } catch (InvalidKeyException e
) {
62 logger
.warn("Manifest couldn't be decrypted, ignoring.");
66 if (manifest
.isEmpty()) {
67 logger
.debug("Manifest is up to date, does not exist or couldn't be decrypted, ignoring.");
71 logger
.trace("Remote storage manifest has {} records", manifest
.get().getStorageIds().size());
72 final var storageIds
= manifest
.get()
75 .filter(id
-> !id
.isUnknown())
76 .collect(Collectors
.toSet());
78 Optional
<SignalStorageManifest
> localManifest
= account
.getStorageManifest();
79 localManifest
.ifPresent(m
-> m
.getStorageIds().forEach(storageIds
::remove
));
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);
94 account
.setStorageManifestVersion(manifest
.get().getVersion());
95 account
.setStorageManifest(manifest
.get());
96 logger
.debug("Done reading data from remote storage");
99 private void readContactRecord(final SignalStorageRecord
record) {
100 if (record == null || record.getContact().isEmpty()) {
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) {
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());
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()
129 contactRecord
.getSystemGivenName().isPresent() && !contactRecord
.getSystemGivenName()
131 .equals(contactGivenName
)
134 contactRecord
.getSystemFamilyName().isPresent() && !contactRecord
.getSystemFamilyName()
136 .equals(contactFamilyName
)
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));
148 account
.getContactStore().storeContact(recipientId
, newContact
.build());
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();
155 contactRecord
.getProfileGivenName().isPresent() && !contactRecord
.getProfileGivenName()
157 .equals(profileGivenName
)
159 contactRecord
.getProfileFamilyName().isPresent() && !contactRecord
.getProfileFamilyName()
161 .equals(profileFamilyName
)
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))
167 account
.getProfileStore().storeProfile(recipientId
, newProfile
);
169 if (contactRecord
.getProfileKey().isPresent()) {
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");
178 if (contactRecord
.getIdentityKey().isPresent() && aci
!= null) {
180 logger
.trace("Storing identity key {}", recipientId
);
181 final var identityKey
= new IdentityKey(contactRecord
.getIdentityKey().get());
182 account
.getIdentityKeyStore().saveIdentity(aci
, identityKey
);
184 final var trustLevel
= TrustLevel
.fromIdentityState(contactRecord
.getIdentityState());
185 if (trustLevel
!= null) {
186 account
.getIdentityKeyStore().setIdentityTrustLevel(aci
, identityKey
, trustLevel
);
188 } catch (InvalidKeyException e
) {
189 logger
.warn("Received invalid contact identity key from storage");
194 private void readGroupV1Record(final SignalStorageRecord
record) {
195 if (record == null || record.getGroupV1().isEmpty()) {
199 final var groupV1Record
= record.getGroupV1().get();
200 final var groupIdV1
= GroupId
.v1(groupV1Record
.getGroupId());
202 var group
= account
.getGroupStore().getGroup(groupIdV1
);
205 context
.getGroupHelper().sendGroupInfoRequest(groupIdV1
, account
.getSelfRecipientId());
206 } catch (Throwable e
) {
207 logger
.warn("Failed to send group request", e
);
209 group
= account
.getGroupStore().getOrCreateGroupV1(groupIdV1
);
211 if (group
!= null && group
.isBlocked() != groupV1Record
.isBlocked()) {
212 group
.setBlocked(groupV1Record
.isBlocked());
213 account
.getGroupStore().updateGroup(group
);
217 private void readGroupV2Record(final SignalStorageRecord
record) {
218 if (record == null || record.getGroupV2().isEmpty()) {
222 final var groupV2Record
= record.getGroupV2().get();
223 if (groupV2Record
.isArchived()) {
227 final GroupMasterKey groupMasterKey
;
229 groupMasterKey
= new GroupMasterKey(groupV2Record
.getMasterKeyBytes());
230 } catch (InvalidInputException e
) {
231 logger
.warn("Received invalid group master key from storage");
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
);
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.");
248 SignalAccountRecord accountRecord
= record.getAccount().orElse(null);
249 if (accountRecord
== null) {
250 logger
.warn("The storage record didn't actually have an account, ignoring.");
254 if (!accountRecord
.getE164().equals(account
.getNumber())) {
255 context
.getJobExecutor().enqueueJob(new CheckWhoAmIJob());
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
;
267 account
.getConfigurationStore().setPhoneNumberUnlisted(accountRecord
.isPhoneNumberUnlisted());
268 account
.setUsername(accountRecord
.getUsername());
270 if (accountRecord
.getProfileKey().isPresent()) {
271 ProfileKey profileKey
;
273 profileKey
= new ProfileKey(accountRecord
.getProfileKey().get());
274 } catch (InvalidInputException e
) {
275 logger
.warn("Received invalid profile key from storage");
278 if (profileKey
!= null) {
279 account
.setProfileKey(profileKey
);
280 final var avatarPath
= accountRecord
.getAvatarUrlPath().orElse(null);
281 context
.getJobExecutor().enqueueJob(new DownloadProfileAvatarJob(avatarPath
));
285 context
.getProfileHelper()
288 accountRecord
.getGivenName().orElse(null),
289 accountRecord
.getFamilyName().orElse(null),
296 private SignalStorageRecord
getSignalStorageRecord(final StorageId accountId
) throws IOException
{
297 List
<SignalStorageRecord
> records
;
299 records
= dependencies
.getAccountManager()
300 .readStorageRecords(account
.getStorageKey(), Collections
.singletonList(accountId
));
301 } catch (InvalidKeyException e
) {
302 logger
.warn("Failed to read storage records, ignoring.");
305 return !records
.isEmpty() ? records
.get(0) : null;
308 private List
<SignalStorageRecord
> getSignalStorageRecords(final Collection
<StorageId
> storageIds
) throws IOException
{
309 List
<SignalStorageRecord
> records
;
311 records
= dependencies
.getAccountManager()
312 .readStorageRecords(account
.getStorageKey(), new ArrayList
<>(storageIds
));
313 } catch (InvalidKeyException e
) {
314 logger
.warn("Failed to read storage records, ignoring.");