1 package org
.asamk
.signal
.manager
.syncStorage
;
3 import org
.asamk
.signal
.manager
.storage
.recipients
.RecipientAddress
;
4 import org
.signal
.core
.util
.Base64
;
5 import org
.signal
.core
.util
.SetUtil
;
6 import org
.slf4j
.Logger
;
7 import org
.slf4j
.LoggerFactory
;
8 import org
.whispersystems
.signalservice
.api
.push
.ServiceId
;
9 import org
.whispersystems
.signalservice
.api
.storage
.SignalStorageManifest
;
10 import org
.whispersystems
.signalservice
.api
.storage
.SignalStorageRecord
;
11 import org
.whispersystems
.signalservice
.api
.storage
.StorageId
;
12 import org
.whispersystems
.signalservice
.internal
.storage
.protos
.ManifestRecord
;
14 import java
.nio
.ByteBuffer
;
15 import java
.util
.HashSet
;
16 import java
.util
.List
;
18 import java
.util
.stream
.Collectors
;
20 public final class StorageSyncValidations
{
22 private static final Logger logger
= LoggerFactory
.getLogger(StorageSyncValidations
.class);
24 private StorageSyncValidations() {
27 public static void validate(
28 WriteOperationResult result
,
29 SignalStorageManifest previousManifest
,
30 boolean forcePushPending
,
33 validateManifestAndInserts(result
.manifest(), result
.inserts(), self
);
35 if (!result
.deletes().isEmpty()) {
36 Set
<String
> allSetEncoded
= result
.manifest()
39 .map(StorageId
::getRaw
)
40 .map(Base64
::encodeWithPadding
)
41 .collect(Collectors
.toSet());
43 for (byte[] delete
: result
.deletes()) {
44 String encoded
= Base64
.encodeWithPadding(delete
);
45 if (allSetEncoded
.contains(encoded
)) {
46 throw new DeletePresentInFullIdSetError();
51 if (previousManifest
.getVersion() == 0) {
53 "Previous manifest is empty, not bothering with additional validations around the diffs between the two manifests.");
57 if (result
.manifest().getVersion() != previousManifest
.getVersion() + 1) {
58 throw new IncorrectManifestVersionError();
61 if (forcePushPending
) {
63 "Force push pending, not bothering with additional validations around the diffs between the two manifests.");
67 Set
<ByteBuffer
> previousIds
= previousManifest
.getStorageIds()
69 .map(id
-> ByteBuffer
.wrap(id
.getRaw()))
70 .collect(Collectors
.toSet());
71 Set
<ByteBuffer
> newIds
= result
.manifest()
74 .map(id
-> ByteBuffer
.wrap(id
.getRaw()))
75 .collect(Collectors
.toSet());
77 Set
<ByteBuffer
> manifestInserts
= SetUtil
.difference(newIds
, previousIds
);
78 Set
<ByteBuffer
> manifestDeletes
= SetUtil
.difference(previousIds
, newIds
);
80 Set
<ByteBuffer
> declaredInserts
= result
.inserts()
82 .map(r
-> ByteBuffer
.wrap(r
.getId().getRaw()))
83 .collect(Collectors
.toSet());
84 Set
<ByteBuffer
> declaredDeletes
= result
.deletes().stream().map(ByteBuffer
::wrap
).collect(Collectors
.toSet());
86 if (declaredInserts
.size() > manifestInserts
.size()) {
87 logger
.debug("DeclaredInserts: " + declaredInserts
.size() + ", ManifestInserts: " + manifestInserts
.size());
88 throw new MoreInsertsThanExpectedError();
91 if (declaredInserts
.size() < manifestInserts
.size()) {
92 logger
.debug("DeclaredInserts: " + declaredInserts
.size() + ", ManifestInserts: " + manifestInserts
.size());
93 throw new LessInsertsThanExpectedError();
96 if (!declaredInserts
.containsAll(manifestInserts
)) {
97 throw new InsertMismatchError();
100 if (declaredDeletes
.size() > manifestDeletes
.size()) {
101 logger
.debug("DeclaredDeletes: " + declaredDeletes
.size() + ", ManifestDeletes: " + manifestDeletes
.size());
102 throw new MoreDeletesThanExpectedError();
105 if (declaredDeletes
.size() < manifestDeletes
.size()) {
106 logger
.debug("DeclaredDeletes: " + declaredDeletes
.size() + ", ManifestDeletes: " + manifestDeletes
.size());
107 throw new LessDeletesThanExpectedError();
110 if (!declaredDeletes
.containsAll(manifestDeletes
)) {
111 throw new DeleteMismatchError();
115 public static void validateForcePush(
116 SignalStorageManifest manifest
, List
<SignalStorageRecord
> inserts
, RecipientAddress self
118 validateManifestAndInserts(manifest
, inserts
, self
);
121 private static void validateManifestAndInserts(
122 SignalStorageManifest manifest
, List
<SignalStorageRecord
> inserts
, RecipientAddress self
124 int accountCount
= 0;
125 for (StorageId id
: manifest
.getStorageIds()) {
126 accountCount
+= id
.getType() == ManifestRecord
.Identifier
.Type
.ACCOUNT
.getValue() ?
1 : 0;
129 if (accountCount
> 1) {
130 throw new MultipleAccountError();
133 if (accountCount
== 0) {
134 throw new MissingAccountError();
137 Set
<StorageId
> allSet
= new HashSet
<>(manifest
.getStorageIds());
138 Set
<StorageId
> insertSet
= inserts
.stream().map(SignalStorageRecord
::getId
).collect(Collectors
.toSet());
139 Set
<ByteBuffer
> rawIdSet
= allSet
.stream().map(id
-> ByteBuffer
.wrap(id
.getRaw())).collect(Collectors
.toSet());
141 if (allSet
.size() != manifest
.getStorageIds().size()) {
142 throw new DuplicateStorageIdError();
145 if (rawIdSet
.size() != allSet
.size()) {
146 List
<StorageId
> ids
= manifest
.getStorageIdsByType().get(ManifestRecord
.Identifier
.Type
.CONTACT
.getValue());
147 if (ids
.size() != new HashSet
<>(ids
).size()) {
148 throw new DuplicateContactIdError();
151 ids
= manifest
.getStorageIdsByType().get(ManifestRecord
.Identifier
.Type
.GROUPV1
.getValue());
152 if (ids
.size() != new HashSet
<>(ids
).size()) {
153 throw new DuplicateGroupV1IdError();
156 ids
= manifest
.getStorageIdsByType().get(ManifestRecord
.Identifier
.Type
.GROUPV2
.getValue());
157 if (ids
.size() != new HashSet
<>(ids
).size()) {
158 throw new DuplicateGroupV2IdError();
161 ids
= manifest
.getStorageIdsByType().get(ManifestRecord
.Identifier
.Type
.STORY_DISTRIBUTION_LIST
.getValue());
162 if (ids
.size() != new HashSet
<>(ids
).size()) {
163 throw new DuplicateDistributionListIdError();
166 throw new DuplicateRawIdAcrossTypesError();
169 if (inserts
.size() > insertSet
.size()) {
170 throw new DuplicateInsertInWriteError();
173 for (SignalStorageRecord insert
: inserts
) {
174 if (!allSet
.contains(insert
.getId())) {
175 throw new InsertNotPresentInFullIdSetError();
178 if (insert
.isUnknown()) {
179 throw new UnknownInsertError();
182 if (insert
.getContact().isPresent()) {
183 final var contact
= insert
.getContact().get();
184 final var serviceId
= contact
.getServiceId().map(ServiceId
.class::cast
);
185 final var pni
= contact
.getPni();
186 final var number
= contact
.getNumber();
187 final var username
= contact
.getUsername();
188 final var address
= new RecipientAddress(serviceId
, pni
, number
, username
);
189 if (self
.matches(address
)) {
190 throw new SelfAddedAsContactError();
193 if (insert
.getAccount().isPresent() && insert
.getAccount().get().getProfileKey().isEmpty()) {
194 logger
.debug("Uploading a null profile key in our AccountRecord!");
199 private static final class DuplicateStorageIdError
extends Error
{}
201 private static final class DuplicateRawIdAcrossTypesError
extends Error
{}
203 private static final class DuplicateContactIdError
extends Error
{}
205 private static final class DuplicateGroupV1IdError
extends Error
{}
207 private static final class DuplicateGroupV2IdError
extends Error
{}
209 private static final class DuplicateDistributionListIdError
extends Error
{}
211 private static final class DuplicateInsertInWriteError
extends Error
{}
213 private static final class InsertNotPresentInFullIdSetError
extends Error
{}
215 private static final class DeletePresentInFullIdSetError
extends Error
{}
217 private static final class UnknownInsertError
extends Error
{}
219 private static final class MultipleAccountError
extends Error
{}
221 private static final class MissingAccountError
extends Error
{}
223 private static final class SelfAddedAsContactError
extends Error
{}
225 private static final class IncorrectManifestVersionError
extends Error
{}
227 private static final class MoreInsertsThanExpectedError
extends Error
{}
229 private static final class LessInsertsThanExpectedError
extends Error
{}
231 private static final class InsertMismatchError
extends Error
{}
233 private static final class MoreDeletesThanExpectedError
extends Error
{}
235 private static final class LessDeletesThanExpectedError
extends Error
{}
237 private static final class DeleteMismatchError
extends Error
{}