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