Signal-Android/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncValidations.java

209 wiersze
7.3 KiB
Java

package org.thoughtcrime.securesms.storage;
import androidx.annotation.NonNull;
import com.annimon.stream.Collectors;
import com.annimon.stream.Stream;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.Base64;
import org.signal.core.util.SetUtil;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.whispersystems.signalservice.api.storage.SignalContactRecord;
import org.whispersystems.signalservice.api.storage.SignalStorageManifest;
import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
import org.whispersystems.signalservice.api.storage.StorageId;
import org.whispersystems.signalservice.internal.storage.protos.ManifestRecord;
import java.nio.ByteBuffer;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public final class StorageSyncValidations {
private static final String TAG = Log.tag(StorageSyncValidations.class);
private StorageSyncValidations() {}
public static void validate(@NonNull StorageSyncHelper.WriteOperationResult result,
@NonNull SignalStorageManifest previousManifest,
boolean forcePushPending,
@NonNull Recipient self)
{
validateManifestAndInserts(result.getManifest(), result.getInserts(), self);
if (result.getDeletes().size() > 0) {
Set<String> allSetEncoded = Stream.of(result.getManifest().getStorageIds()).map(StorageId::getRaw).map(Base64::encodeBytes).collect(Collectors.toSet());
for (byte[] delete : result.getDeletes()) {
String encoded = Base64.encodeBytes(delete);
if (allSetEncoded.contains(encoded)) {
throw new DeletePresentInFullIdSetError();
}
}
}
if (previousManifest.getVersion() == 0) {
Log.i(TAG, "Previous manifest is empty, not bothering with additional validations around the diffs between the two manifests.");
return;
}
if (result.getManifest().getVersion() != previousManifest.getVersion() + 1) {
throw new IncorrectManifestVersionError();
}
if (forcePushPending) {
Log.i(TAG, "Force push pending, not bothering with additional validations around the diffs between the two manifests.");
return;
}
Set<ByteBuffer> previousIds = Stream.of(previousManifest.getStorageIds()).map(id -> ByteBuffer.wrap(id.getRaw())).collect(Collectors.toSet());
Set<ByteBuffer> newIds = Stream.of(result.getManifest().getStorageIds()).map(id -> ByteBuffer.wrap(id.getRaw())).collect(Collectors.toSet());
Set<ByteBuffer> manifestInserts = SetUtil.difference(newIds, previousIds);
Set<ByteBuffer> manifestDeletes = SetUtil.difference(previousIds, newIds);
Set<ByteBuffer> declaredInserts = Stream.of(result.getInserts()).map(r -> ByteBuffer.wrap(r.getId().getRaw())).collect(Collectors.toSet());
Set<ByteBuffer> declaredDeletes = Stream.of(result.getDeletes()).map(ByteBuffer::wrap).collect(Collectors.toSet());
if (declaredInserts.size() > manifestInserts.size()) {
Log.w(TAG, "DeclaredInserts: " + declaredInserts.size() + ", ManifestInserts: " + manifestInserts.size());
throw new MoreInsertsThanExpectedError();
}
if (declaredInserts.size() < manifestInserts.size()) {
Log.w(TAG, "DeclaredInserts: " + declaredInserts.size() + ", ManifestInserts: " + manifestInserts.size());
throw new LessInsertsThanExpectedError();
}
if (!declaredInserts.containsAll(manifestInserts)) {
throw new InsertMismatchError();
}
if (declaredDeletes.size() > manifestDeletes.size()) {
Log.w(TAG, "DeclaredDeletes: " + declaredDeletes.size() + ", ManifestDeletes: " + manifestDeletes.size());
throw new MoreDeletesThanExpectedError();
}
if (declaredDeletes.size() < manifestDeletes.size()) {
Log.w(TAG, "DeclaredDeletes: " + declaredDeletes.size() + ", ManifestDeletes: " + manifestDeletes.size());
throw new LessDeletesThanExpectedError();
}
if (!declaredDeletes.containsAll(manifestDeletes)) {
throw new DeleteMismatchError();
}
}
public static void validateForcePush(@NonNull SignalStorageManifest manifest, @NonNull List<SignalStorageRecord> inserts, @NonNull Recipient self) {
validateManifestAndInserts(manifest, inserts, self);
}
private static void validateManifestAndInserts(@NonNull SignalStorageManifest manifest, @NonNull List<SignalStorageRecord> inserts, @NonNull Recipient self) {
Set<StorageId> allSet = new HashSet<>(manifest.getStorageIds());
Set<StorageId> insertSet = new HashSet<>(Stream.of(inserts).map(SignalStorageRecord::getId).toList());
Set<ByteBuffer> rawIdSet = Stream.of(allSet).map(id -> ByteBuffer.wrap(id.getRaw())).collect(Collectors.toSet());
if (allSet.size() != manifest.getStorageIds().size()) {
throw new DuplicateStorageIdError();
}
if (rawIdSet.size() != allSet.size()) {
throw new DuplicateRawIdError();
}
if (inserts.size() > insertSet.size()) {
throw new DuplicateInsertInWriteError();
}
int accountCount = 0;
for (StorageId id : manifest.getStorageIds()) {
accountCount += id.getType() == ManifestRecord.Identifier.Type.ACCOUNT_VALUE ? 1 : 0;
}
if (accountCount > 1) {
throw new MultipleAccountError();
}
if (accountCount == 0) {
throw new MissingAccountError();
}
for (SignalStorageRecord insert : inserts) {
if (!allSet.contains(insert.getId())) {
throw new InsertNotPresentInFullIdSetError();
}
if (insert.isUnknown()) {
throw new UnknownInsertError();
}
if (insert.getContact().isPresent()) {
SignalContactRecord contact = insert.getContact().get();
if (self.requireServiceId().equals(contact.getServiceId()) ||
self.requirePni().equals(contact.getPni().orElse(null)) ||
self.requireE164().equals(contact.getNumber().orElse("")))
{
throw new SelfAddedAsContactError();
}
}
if (insert.getAccount().isPresent() && !insert.getAccount().get().getProfileKey().isPresent()) {
Log.w(TAG, "Uploading a null profile key in our AccountRecord!");
}
}
}
private static final class DuplicateStorageIdError extends Error {
}
private static final class DuplicateRawIdError extends Error {
}
private static final class DuplicateInsertInWriteError extends Error {
}
private static final class InsertNotPresentInFullIdSetError extends Error {
}
private static final class DeletePresentInFullIdSetError extends Error {
}
private static final class UnknownInsertError extends Error {
}
private static final class MultipleAccountError extends Error {
}
private static final class MissingAccountError extends Error {
}
private static final class SelfAddedAsContactError extends Error {
}
private static final class IncorrectManifestVersionError extends Error {
}
private static final class MoreInsertsThanExpectedError extends Error {
}
private static final class LessInsertsThanExpectedError extends Error {
}
private static final class InsertMismatchError extends Error {
}
private static final class MoreDeletesThanExpectedError extends Error {
}
private static final class LessDeletesThanExpectedError extends Error {
}
private static final class DeleteMismatchError extends Error {
}
}