]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/syncStorage/StorageSyncValidations.java
e69481edc41333358cbb2804a981d79f970c4396
[signal-cli] / lib / src / main / java / org / asamk / signal / manager / syncStorage / StorageSyncValidations.java
1 package org.asamk.signal.manager.syncStorage;
2
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.storage.SignalStorageManifest;
9 import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
10 import org.whispersystems.signalservice.api.storage.StorageId;
11 import org.whispersystems.signalservice.internal.storage.protos.ManifestRecord;
12
13 import java.nio.ByteBuffer;
14 import java.util.HashSet;
15 import java.util.List;
16 import java.util.Set;
17 import java.util.stream.Collectors;
18
19 public final class StorageSyncValidations {
20
21 private static final Logger logger = LoggerFactory.getLogger(StorageSyncValidations.class);
22
23 private StorageSyncValidations() {
24 }
25
26 public static void validate(
27 WriteOperationResult result,
28 SignalStorageManifest previousManifest,
29 boolean forcePushPending,
30 RecipientAddress self
31 ) {
32 validateManifestAndInserts(result.manifest(), result.inserts(), self);
33
34 if (!result.deletes().isEmpty()) {
35 Set<String> allSetEncoded = result.manifest()
36 .getStorageIds()
37 .stream()
38 .map(StorageId::getRaw)
39 .map(Base64::encodeWithPadding)
40 .collect(Collectors.toSet());
41
42 for (byte[] delete : result.deletes()) {
43 String encoded = Base64.encodeWithPadding(delete);
44 if (allSetEncoded.contains(encoded)) {
45 throw new DeletePresentInFullIdSetError();
46 }
47 }
48 }
49
50 if (previousManifest.getVersion() == 0) {
51 logger.debug(
52 "Previous manifest is empty, not bothering with additional validations around the diffs between the two manifests.");
53 return;
54 }
55
56 if (result.manifest().getVersion() != previousManifest.getVersion() + 1) {
57 throw new IncorrectManifestVersionError();
58 }
59
60 if (forcePushPending) {
61 logger.debug(
62 "Force push pending, not bothering with additional validations around the diffs between the two manifests.");
63 return;
64 }
65
66 Set<ByteBuffer> previousIds = previousManifest.getStorageIds()
67 .stream()
68 .map(id -> ByteBuffer.wrap(id.getRaw()))
69 .collect(Collectors.toSet());
70 Set<ByteBuffer> newIds = result.manifest()
71 .getStorageIds()
72 .stream()
73 .map(id -> ByteBuffer.wrap(id.getRaw()))
74 .collect(Collectors.toSet());
75
76 Set<ByteBuffer> manifestInserts = SetUtil.difference(newIds, previousIds);
77 Set<ByteBuffer> manifestDeletes = SetUtil.difference(previousIds, newIds);
78
79 Set<ByteBuffer> declaredInserts = result.inserts()
80 .stream()
81 .map(r -> ByteBuffer.wrap(r.getId().getRaw()))
82 .collect(Collectors.toSet());
83 Set<ByteBuffer> declaredDeletes = result.deletes().stream().map(ByteBuffer::wrap).collect(Collectors.toSet());
84
85 if (declaredInserts.size() > manifestInserts.size()) {
86 logger.debug("DeclaredInserts: " + declaredInserts.size() + ", ManifestInserts: " + manifestInserts.size());
87 throw new MoreInsertsThanExpectedError();
88 }
89
90 if (declaredInserts.size() < manifestInserts.size()) {
91 logger.debug("DeclaredInserts: " + declaredInserts.size() + ", ManifestInserts: " + manifestInserts.size());
92 throw new LessInsertsThanExpectedError();
93 }
94
95 if (!declaredInserts.containsAll(manifestInserts)) {
96 throw new InsertMismatchError();
97 }
98
99 if (declaredDeletes.size() > manifestDeletes.size()) {
100 logger.debug("DeclaredDeletes: " + declaredDeletes.size() + ", ManifestDeletes: " + manifestDeletes.size());
101 throw new MoreDeletesThanExpectedError();
102 }
103
104 if (declaredDeletes.size() < manifestDeletes.size()) {
105 logger.debug("DeclaredDeletes: " + declaredDeletes.size() + ", ManifestDeletes: " + manifestDeletes.size());
106 throw new LessDeletesThanExpectedError();
107 }
108
109 if (!declaredDeletes.containsAll(manifestDeletes)) {
110 throw new DeleteMismatchError();
111 }
112 }
113
114 public static void validateForcePush(
115 SignalStorageManifest manifest, List<SignalStorageRecord> inserts, RecipientAddress self
116 ) {
117 validateManifestAndInserts(manifest, inserts, self);
118 }
119
120 private static void validateManifestAndInserts(
121 SignalStorageManifest manifest, List<SignalStorageRecord> inserts, RecipientAddress self
122 ) {
123 int accountCount = 0;
124 for (StorageId id : manifest.getStorageIds()) {
125 accountCount += id.getType() == ManifestRecord.Identifier.Type.ACCOUNT.getValue() ? 1 : 0;
126 }
127
128 if (accountCount > 1) {
129 throw new MultipleAccountError();
130 }
131
132 if (accountCount == 0) {
133 throw new MissingAccountError();
134 }
135
136 Set<StorageId> allSet = new HashSet<>(manifest.getStorageIds());
137 Set<StorageId> insertSet = inserts.stream().map(SignalStorageRecord::getId).collect(Collectors.toSet());
138 Set<ByteBuffer> rawIdSet = allSet.stream().map(id -> ByteBuffer.wrap(id.getRaw())).collect(Collectors.toSet());
139
140 if (allSet.size() != manifest.getStorageIds().size()) {
141 throw new DuplicateStorageIdError();
142 }
143
144 if (rawIdSet.size() != allSet.size()) {
145 List<StorageId> ids = manifest.getStorageIdsByType().get(ManifestRecord.Identifier.Type.CONTACT.getValue());
146 if (ids.size() != new HashSet<>(ids).size()) {
147 throw new DuplicateContactIdError();
148 }
149
150 ids = manifest.getStorageIdsByType().get(ManifestRecord.Identifier.Type.GROUPV1.getValue());
151 if (ids.size() != new HashSet<>(ids).size()) {
152 throw new DuplicateGroupV1IdError();
153 }
154
155 ids = manifest.getStorageIdsByType().get(ManifestRecord.Identifier.Type.GROUPV2.getValue());
156 if (ids.size() != new HashSet<>(ids).size()) {
157 throw new DuplicateGroupV2IdError();
158 }
159
160 ids = manifest.getStorageIdsByType().get(ManifestRecord.Identifier.Type.STORY_DISTRIBUTION_LIST.getValue());
161 if (ids.size() != new HashSet<>(ids).size()) {
162 throw new DuplicateDistributionListIdError();
163 }
164
165 throw new DuplicateRawIdAcrossTypesError();
166 }
167
168 if (inserts.size() > insertSet.size()) {
169 throw new DuplicateInsertInWriteError();
170 }
171
172 for (SignalStorageRecord insert : inserts) {
173 if (!allSet.contains(insert.getId())) {
174 throw new InsertNotPresentInFullIdSetError();
175 }
176
177 if (insert.isUnknown()) {
178 throw new UnknownInsertError();
179 }
180
181 if (insert.getContact().isPresent()) {
182 final var contact = insert.getContact().get();
183 final var aci = contact.getAci();
184 final var pni = contact.getPni();
185 final var number = contact.getNumber();
186 final var username = contact.getUsername();
187 final var address = new RecipientAddress(aci, pni, number, username);
188 if (self.matches(address)) {
189 throw new SelfAddedAsContactError();
190 }
191 }
192 if (insert.getAccount().isPresent() && insert.getAccount().get().getProfileKey().isEmpty()) {
193 logger.debug("Uploading a null profile key in our AccountRecord!");
194 }
195 }
196 }
197
198 private static final class DuplicateStorageIdError extends Error {}
199
200 private static final class DuplicateRawIdAcrossTypesError extends Error {}
201
202 private static final class DuplicateContactIdError extends Error {}
203
204 private static final class DuplicateGroupV1IdError extends Error {}
205
206 private static final class DuplicateGroupV2IdError extends Error {}
207
208 private static final class DuplicateDistributionListIdError extends Error {}
209
210 private static final class DuplicateInsertInWriteError extends Error {}
211
212 private static final class InsertNotPresentInFullIdSetError extends Error {}
213
214 private static final class DeletePresentInFullIdSetError extends Error {}
215
216 private static final class UnknownInsertError extends Error {}
217
218 private static final class MultipleAccountError extends Error {}
219
220 private static final class MissingAccountError extends Error {}
221
222 private static final class SelfAddedAsContactError extends Error {}
223
224 private static final class IncorrectManifestVersionError extends Error {}
225
226 private static final class MoreInsertsThanExpectedError extends Error {}
227
228 private static final class LessInsertsThanExpectedError extends Error {}
229
230 private static final class InsertMismatchError extends Error {}
231
232 private static final class MoreDeletesThanExpectedError extends Error {}
233
234 private static final class LessDeletesThanExpectedError extends Error {}
235
236 private static final class DeleteMismatchError extends Error {}
237 }