1235 lines
40 KiB
C++
1235 lines
40 KiB
C++
/*
|
|
This file is part of Telegram Desktop,
|
|
the official desktop application for the Telegram messaging service.
|
|
|
|
For license and copyright information please follow this link:
|
|
https://github.com/telegramdesktop/tdesktop/blob/master/LEGAL
|
|
*/
|
|
#include "e2e_cloud/core/envelope.h"
|
|
#include "e2e_cloud/core/envelope_codec.h"
|
|
#include "e2e_cloud/core/freshness_gate.h"
|
|
#include "e2e_cloud/core/outbox.h"
|
|
#include "e2e_cloud/group/group_state.h"
|
|
#include "e2e_cloud/transport/cloud_vault_transport.h"
|
|
#include "e2e_cloud/transport/outbox_upload_controller.h"
|
|
#include "e2e_cloud/transport/telegram_carrier_transport.h"
|
|
|
|
#include <algorithm>
|
|
#include <cstdio>
|
|
#include <optional>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
namespace {
|
|
|
|
using namespace E2ECloud;
|
|
|
|
template <typename Id>
|
|
[[nodiscard]] Id FilledId(std::uint8_t value) {
|
|
auto result = Id();
|
|
result.bytes.fill(value);
|
|
return result;
|
|
}
|
|
|
|
[[nodiscard]] int Fail(const char *message) {
|
|
std::fprintf(stderr, "%s\n", message);
|
|
return 1;
|
|
}
|
|
|
|
[[nodiscard]] Checkpoint MakeCheckpoint(
|
|
ConversationId conversationId,
|
|
std::uint64_t generation,
|
|
std::uint8_t hashValue) {
|
|
return {
|
|
.conversationId = conversationId,
|
|
.generation = generation,
|
|
.stateHash = FilledId<Digest>(hashValue),
|
|
};
|
|
}
|
|
|
|
[[nodiscard]] FreshnessResponse MakeResponse(
|
|
const Checkpoint &checkpoint,
|
|
ChallengeNonce nonce,
|
|
Checkpoint challengedCheckpoint = {}) {
|
|
return {
|
|
.conversationId = checkpoint.conversationId,
|
|
.nonce = nonce,
|
|
.challengedCheckpoint = challengedCheckpoint.conversationId
|
|
? challengedCheckpoint
|
|
: checkpoint,
|
|
.checkpoint = checkpoint,
|
|
.witnessAccountId = FilledId<AccountId>(4),
|
|
.witnessClientId = FilledId<ClientId>(5),
|
|
.authenticatedProof = QByteArray("proof"),
|
|
};
|
|
}
|
|
|
|
class TestVerifier final : public FreshnessResponseVerifier {
|
|
public:
|
|
[[nodiscard]] bool verify(
|
|
const FreshnessResponse &response) const override {
|
|
++calls;
|
|
return accept && !response.authenticatedProof.isEmpty();
|
|
}
|
|
|
|
bool accept = true;
|
|
mutable int calls = 0;
|
|
|
|
};
|
|
|
|
class TestOutboxStore final : public ProtectedOutboxStore {
|
|
public:
|
|
bool append(PendingMessage message) override {
|
|
if (failAppend) {
|
|
return false;
|
|
}
|
|
for (const auto &item : items) {
|
|
if (item.draft.objectId == message.objectId) {
|
|
return false;
|
|
}
|
|
}
|
|
items.push_back({
|
|
.draft = std::move(message),
|
|
.stage = OutboxItemStage::Draft,
|
|
.sealed = std::nullopt,
|
|
});
|
|
return true;
|
|
}
|
|
|
|
bool appendSealed(EncodedEnvelope envelope) override {
|
|
for (const auto &item : items) {
|
|
if (item.draft.objectId == envelope.objectId) {
|
|
return item.stage == OutboxItemStage::Sealed
|
|
&& item.sealed == envelope;
|
|
}
|
|
}
|
|
items.push_back({
|
|
.draft = {
|
|
.conversationId = envelope.conversationId,
|
|
.objectId = envelope.objectId,
|
|
.plaintext = {},
|
|
.authenticatedData = {},
|
|
},
|
|
.stage = OutboxItemStage::Sealed,
|
|
.sealed = std::move(envelope),
|
|
});
|
|
return true;
|
|
}
|
|
|
|
bool appendSealedThenDraft(
|
|
EncodedEnvelope envelope,
|
|
PendingMessage message) override {
|
|
if (!appendSealed(std::move(envelope))) {
|
|
return false;
|
|
}
|
|
return append(std::move(message));
|
|
}
|
|
|
|
[[nodiscard]] std::optional<OutboxItem> front(
|
|
ConversationId conversationId) const override {
|
|
for (const auto &item : items) {
|
|
if (item.draft.conversationId == conversationId) {
|
|
return item;
|
|
}
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
|
|
bool replaceWithSealed(
|
|
ObjectId objectId,
|
|
EncodedEnvelope envelope) override {
|
|
if (failReplace) {
|
|
return false;
|
|
}
|
|
for (auto &item : items) {
|
|
if (item.draft.objectId == objectId) {
|
|
item.stage = OutboxItemStage::Sealed;
|
|
item.sealed = std::move(envelope);
|
|
item.draft.plaintext.clear();
|
|
item.draft.authenticatedData.clear();
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool remove(ObjectId objectId) override {
|
|
if (failRemove) {
|
|
return false;
|
|
}
|
|
for (auto i = items.begin(); i != items.end(); ++i) {
|
|
if (i->draft.objectId == objectId) {
|
|
items.erase(i);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
[[nodiscard]] bool contains(ObjectId objectId) const override {
|
|
return std::any_of(items.begin(), items.end(), [&](const auto &item) {
|
|
return item.draft.objectId == objectId;
|
|
});
|
|
}
|
|
|
|
std::vector<OutboxItem> items;
|
|
bool failAppend = false;
|
|
bool failReplace = false;
|
|
bool failRemove = false;
|
|
|
|
};
|
|
|
|
class TestProtector final : public OutboundMessageProtector {
|
|
public:
|
|
[[nodiscard]] std::optional<EncodedEnvelope> protectIdempotently(
|
|
const MlsSealRequest &request) override {
|
|
++calls;
|
|
if (fail) {
|
|
return std::nullopt;
|
|
}
|
|
return EncodedEnvelope{
|
|
.conversationId = wrongConversation
|
|
? FilledId<ConversationId>(99)
|
|
: request.conversationId,
|
|
.objectId = request.objectId,
|
|
.bytes = QByteArray("sealed:") + request.plaintext,
|
|
};
|
|
}
|
|
|
|
int calls = 0;
|
|
bool fail = false;
|
|
bool wrongConversation = false;
|
|
|
|
};
|
|
|
|
class TestTransport final : public TelegramTransport {
|
|
public:
|
|
void uploadExact(
|
|
EncodedEnvelope envelope,
|
|
UploadCallback callback) override {
|
|
uploads.push_back(std::move(envelope));
|
|
callbacks.push_back(std::move(callback));
|
|
}
|
|
|
|
void downloadPage(
|
|
DownloadRequest,
|
|
DownloadCallback callback) override {
|
|
callback({
|
|
.result = UploadResult::Accepted,
|
|
.untrustedObjects = {},
|
|
.nextCursor = {},
|
|
.complete = true,
|
|
});
|
|
}
|
|
|
|
void finish(std::size_t index, UploadResult result) {
|
|
callbacks.at(index)(result);
|
|
}
|
|
|
|
std::vector<EncodedEnvelope> uploads;
|
|
std::vector<UploadCallback> callbacks;
|
|
|
|
};
|
|
|
|
class TestCarrierBackend final : public TelegramCarrierBackend {
|
|
public:
|
|
void uploadDocument(
|
|
QByteArray bytes,
|
|
QString filename,
|
|
QString mimeType,
|
|
UploadCallback callback) override {
|
|
uploadedBytes.push_back(std::move(bytes));
|
|
filenames.push_back(std::move(filename));
|
|
mimeTypes.push_back(std::move(mimeType));
|
|
uploadCallbacks.push_back(std::move(callback));
|
|
}
|
|
|
|
void sendUploadedDocument(
|
|
std::uint64_t telegramPeerId,
|
|
UploadedCarrierFile file,
|
|
QString filename,
|
|
QString mimeType,
|
|
SendCallback callback) override {
|
|
peerIds.push_back(telegramPeerId);
|
|
tokens.push_back(std::move(file.backendToken));
|
|
filenames.push_back(std::move(filename));
|
|
mimeTypes.push_back(std::move(mimeType));
|
|
sendCallbacks.push_back(std::move(callback));
|
|
}
|
|
|
|
void downloadDocuments(
|
|
std::uint64_t telegramPeerId,
|
|
QByteArray cursor,
|
|
int limit,
|
|
DownloadCallback callback) override {
|
|
peerIds.push_back(telegramPeerId);
|
|
cursors.push_back(std::move(cursor));
|
|
limits.push_back(limit);
|
|
downloadCallbacks.push_back(std::move(callback));
|
|
}
|
|
|
|
void findDocument(
|
|
std::uint64_t telegramPeerId,
|
|
DiscoveryCallback callback) override {
|
|
peerIds.push_back(telegramPeerId);
|
|
discoveryCallbacks.push_back(std::move(callback));
|
|
}
|
|
|
|
std::vector<QByteArray> uploadedBytes;
|
|
std::vector<QString> filenames;
|
|
std::vector<QString> mimeTypes;
|
|
std::vector<std::uint64_t> peerIds;
|
|
std::vector<QByteArray> tokens;
|
|
std::vector<QByteArray> cursors;
|
|
std::vector<int> limits;
|
|
std::vector<UploadCallback> uploadCallbacks;
|
|
std::vector<SendCallback> sendCallbacks;
|
|
std::vector<DownloadCallback> downloadCallbacks;
|
|
std::vector<DiscoveryCallback> discoveryCallbacks;
|
|
|
|
};
|
|
|
|
[[nodiscard]] PendingMessage MakeMessage(ConversationId conversationId) {
|
|
return {
|
|
.conversationId = conversationId,
|
|
.objectId = FilledId<ObjectId>(7),
|
|
.plaintext = QByteArray("message"),
|
|
.authenticatedData = QByteArray("metadata"),
|
|
};
|
|
}
|
|
|
|
[[nodiscard]] TransportEnvelope MakeEnvelope() {
|
|
return {
|
|
.conversationId = FilledId<ConversationId>(1),
|
|
.objectKind = ObjectKind::MlsApplication,
|
|
.senderAccountId = FilledId<AccountId>(2),
|
|
.senderClientId = FilledId<ClientId>(3),
|
|
.telegramPeerIdBinding = 42,
|
|
.epochOrGeneration = 8,
|
|
.objectId = FilledId<ObjectId>(4),
|
|
.payloadHash = FilledId<Digest>(5),
|
|
.payload = QByteArray("payload"),
|
|
.authenticationData = QByteArray("authenticated"),
|
|
};
|
|
}
|
|
|
|
[[nodiscard]] int ScenarioEnvelopeValidation() {
|
|
auto envelope = MakeEnvelope();
|
|
if (ValidateEnvelope(envelope) != EnvelopeValidationError::None) {
|
|
return Fail("valid transport envelope was rejected");
|
|
}
|
|
envelope.applicationProtocolVersion = kApplicationProtocolVersion + 1;
|
|
if (ValidateEnvelope(envelope)
|
|
!= EnvelopeValidationError::UnsupportedVersion) {
|
|
return Fail("unsupported transport envelope version was accepted");
|
|
}
|
|
envelope.applicationProtocolVersion = kApplicationProtocolVersion;
|
|
envelope.objectKind = static_cast<ObjectKind>(1000);
|
|
if (ValidateEnvelope(envelope)
|
|
!= EnvelopeValidationError::UnknownObjectKind) {
|
|
return Fail("unknown transport envelope kind was accepted");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
[[nodiscard]] int ScenarioHistoryAccessBoundary() {
|
|
if (!IsValidHistoryAccess({
|
|
.mode = HistoryAccessMode::None,
|
|
.boundaryEventId = {},
|
|
})
|
|
|| !IsValidHistoryAccess({
|
|
.mode = HistoryAccessMode::FromJoin,
|
|
.boundaryEventId = {},
|
|
})
|
|
|| !IsValidHistoryAccess({
|
|
.mode = HistoryAccessMode::Full,
|
|
.boundaryEventId = {},
|
|
})
|
|
|| IsValidHistoryAccess({
|
|
.mode = HistoryAccessMode::Since,
|
|
.boundaryEventId = {},
|
|
})
|
|
|| !IsValidHistoryAccess({
|
|
.mode = HistoryAccessMode::Since,
|
|
.boundaryEventId = FilledId<ObjectId>(3),
|
|
})) {
|
|
return Fail("history boundary accepted an ambiguous policy");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
[[nodiscard]] int ScenarioEnvelopeCodec() {
|
|
const auto envelope = MakeEnvelope();
|
|
const auto codec = EnvelopeCodecV1();
|
|
const auto first = codec.encode(envelope);
|
|
const auto second = codec.encode(envelope);
|
|
if (!first
|
|
|| first != second
|
|
|| codec.decode(*first) != envelope
|
|
|| first->bytes.size() != 202
|
|
|| std::uint8_t(first->bytes[11]) != 1
|
|
|| std::uint8_t(first->bytes[45]) != 8
|
|
|| std::uint8_t(first->bytes[101]) != 42
|
|
|| std::uint8_t(first->bytes[109]) != 8
|
|
|| std::uint8_t(first->bytes[177]) != 7
|
|
|| std::uint8_t(first->bytes[188]) != 13) {
|
|
return Fail("version one envelope codec was not deterministic");
|
|
}
|
|
auto trailing = *first;
|
|
trailing.bytes.append('x');
|
|
if (codec.decode(trailing)) {
|
|
return Fail("envelope codec accepted trailing bytes");
|
|
}
|
|
auto mismatched = *first;
|
|
mismatched.objectId = FilledId<ObjectId>(99);
|
|
if (codec.decode(mismatched)) {
|
|
return Fail("envelope codec accepted a mismatched transport identity");
|
|
}
|
|
auto truncated = *first;
|
|
truncated.bytes.chop(1);
|
|
if (codec.decode(truncated)) {
|
|
return Fail("envelope codec accepted truncated authentication data");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
[[nodiscard]] int ScenarioFreshnessConfirmation() {
|
|
const auto conversationId = FilledId<ConversationId>(1);
|
|
const auto checkpoint = MakeCheckpoint(conversationId, 8, 2);
|
|
const auto nonce = FilledId<ChallengeNonce>(3);
|
|
auto gate = FreshnessGate(checkpoint);
|
|
auto verifier = TestVerifier();
|
|
if (gate.sendingAllowed()
|
|
|| gate.administrationAllowed()
|
|
|| !gate.beginChallenge(nonce)) {
|
|
return Fail("freshness gate opened before witness confirmation");
|
|
}
|
|
auto wrong = MakeResponse(checkpoint, FilledId<ChallengeNonce>(9));
|
|
if (gate.acceptResponse(wrong, verifier)
|
|
!= FreshnessResponseResult::WrongChallenge
|
|
|| verifier.calls
|
|
|| gate.state() != FreshnessState::WaitingForWitness) {
|
|
return Fail("wrong freshness challenge changed gate state");
|
|
}
|
|
const auto response = MakeResponse(checkpoint, nonce);
|
|
if (gate.acceptResponse(response, verifier)
|
|
!= FreshnessResponseResult::Accepted
|
|
|| verifier.calls != 1
|
|
|| !gate.sendingAllowed()
|
|
|| !gate.administrationAllowed()) {
|
|
return Fail("valid freshness witness did not open the gate");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
[[nodiscard]] int ScenarioFreshnessResynchronization() {
|
|
const auto conversationId = FilledId<ConversationId>(1);
|
|
const auto known = MakeCheckpoint(conversationId, 8, 2);
|
|
const auto current = MakeCheckpoint(conversationId, 10, 4);
|
|
const auto nonce = FilledId<ChallengeNonce>(3);
|
|
auto gate = FreshnessGate(known);
|
|
auto verifier = TestVerifier();
|
|
if (!gate.beginChallenge(nonce)
|
|
|| gate.acceptResponse(MakeResponse(current, nonce, known), verifier)
|
|
!= FreshnessResponseResult::ResynchronizationRequired
|
|
|| gate.sendingAllowed()
|
|
|| gate.administrationAllowed()
|
|
|| gate.resynchronizationTarget() != current
|
|
|| gate.completeResynchronization(known, verifier)) {
|
|
return Fail("newer witness state bypassed required resynchronization");
|
|
}
|
|
verifier.accept = false;
|
|
if (gate.completeResynchronization(current, verifier)
|
|
|| gate.sendingAllowed()
|
|
|| gate.state() != FreshnessState::ResynchronizationRequired) {
|
|
return Fail("untrusted witness survived freshness resynchronization");
|
|
}
|
|
verifier.accept = true;
|
|
if (!gate.completeResynchronization(current, verifier)
|
|
|| !gate.sendingAllowed()
|
|
|| gate.knownCheckpoint() != current
|
|
|| verifier.calls != 3) {
|
|
return Fail("newer witness state bypassed required resynchronization");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
[[nodiscard]] int ScenarioFreshnessFork() {
|
|
const auto conversationId = FilledId<ConversationId>(1);
|
|
const auto known = MakeCheckpoint(conversationId, 8, 2);
|
|
const auto fork = MakeCheckpoint(conversationId, 8, 9);
|
|
const auto nonce = FilledId<ChallengeNonce>(3);
|
|
auto gate = FreshnessGate(known);
|
|
auto verifier = TestVerifier();
|
|
if (!gate.beginChallenge(nonce)
|
|
|| gate.acceptResponse(MakeResponse(fork, nonce, known), verifier)
|
|
!= FreshnessResponseResult::ForkDetected
|
|
|| gate.state() != FreshnessState::Forked
|
|
|| gate.sendingAllowed()
|
|
|| gate.beginChallenge(FilledId<ChallengeNonce>(8))) {
|
|
return Fail("conflicting checkpoint did not fail closed");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
[[nodiscard]] int ScenarioOutboxWaitsForFreshness() {
|
|
const auto conversationId = FilledId<ConversationId>(1);
|
|
auto gate = FreshnessGate(MakeCheckpoint(conversationId, 8, 2));
|
|
auto store = TestOutboxStore();
|
|
auto protector = TestProtector();
|
|
auto coordinator = OutboxCoordinator(gate, store, protector);
|
|
const auto queued = coordinator.enqueue(MakeMessage(conversationId));
|
|
const auto dispatch = coordinator.dispatchNext();
|
|
if (queued != EnqueueResult::Queued
|
|
|| dispatch.result != OutboxDispatchResult::AwaitingFreshness
|
|
|| dispatch.envelope
|
|
|| protector.calls
|
|
|| store.items.size() != 1) {
|
|
return Fail("pending outbox content escaped before freshness");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
[[nodiscard]] int ScenarioOutboxSendsAfterFreshness() {
|
|
const auto conversationId = FilledId<ConversationId>(1);
|
|
const auto checkpoint = MakeCheckpoint(conversationId, 8, 2);
|
|
const auto nonce = FilledId<ChallengeNonce>(3);
|
|
auto gate = FreshnessGate(checkpoint);
|
|
auto verifier = TestVerifier();
|
|
auto store = TestOutboxStore();
|
|
auto protector = TestProtector();
|
|
auto coordinator = OutboxCoordinator(gate, store, protector);
|
|
if (coordinator.enqueue(MakeMessage(conversationId))
|
|
!= EnqueueResult::Queued
|
|
|| !gate.beginChallenge(nonce)
|
|
|| gate.acceptResponse(MakeResponse(checkpoint, nonce), verifier)
|
|
!= FreshnessResponseResult::Accepted
|
|
|| protector.calls) {
|
|
return Fail("confirmed outbox content was not sent exactly once");
|
|
}
|
|
const auto dispatch = coordinator.dispatchNext();
|
|
if (dispatch.result != OutboxDispatchResult::Ready
|
|
|| !dispatch.envelope
|
|
|| protector.calls != 1
|
|
|| coordinator.dispatchNext().result
|
|
!= OutboxDispatchResult::UploadInProgress
|
|
|| !coordinator.acknowledgeUploaded(dispatch.envelope->objectId)
|
|
|| !store.items.empty()
|
|
|| coordinator.dispatchNext().result != OutboxDispatchResult::Empty) {
|
|
return Fail("confirmed outbox content was not acknowledged exactly once");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
[[nodiscard]] int ScenarioOutboxRetriesExactCiphertext() {
|
|
const auto conversationId = FilledId<ConversationId>(1);
|
|
const auto checkpoint = MakeCheckpoint(conversationId, 8, 2);
|
|
const auto nonce = FilledId<ChallengeNonce>(3);
|
|
auto gate = FreshnessGate(checkpoint);
|
|
auto verifier = TestVerifier();
|
|
auto store = TestOutboxStore();
|
|
auto protector = TestProtector();
|
|
auto coordinator = OutboxCoordinator(gate, store, protector);
|
|
if (coordinator.enqueue(MakeMessage(conversationId))
|
|
!= EnqueueResult::Queued
|
|
|| !gate.beginChallenge(nonce)
|
|
|| gate.acceptResponse(MakeResponse(checkpoint, nonce), verifier)
|
|
!= FreshnessResponseResult::Accepted
|
|
|| protector.calls) {
|
|
return Fail("outbox retry setup failed");
|
|
}
|
|
const auto first = coordinator.dispatchNext();
|
|
if (first.result != OutboxDispatchResult::Ready
|
|
|| !first.envelope
|
|
|| !coordinator.markUploadFailed(first.envelope->objectId)) {
|
|
return Fail("outbox upload failure was not retained for retry");
|
|
}
|
|
const auto second = coordinator.dispatchNext();
|
|
if (second.result != OutboxDispatchResult::Ready
|
|
|| !second.envelope
|
|
|| first.envelope != second.envelope
|
|
|| protector.calls != 1
|
|
|| store.items.size() != 1
|
|
|| store.items.front().stage != OutboxItemStage::Sealed
|
|
|| !store.items.front().draft.plaintext.isEmpty()
|
|
|| !coordinator.acknowledgeUploaded(second.envelope->objectId)
|
|
|| !store.items.empty()) {
|
|
return Fail("outbox retry regenerated or lost sealed ciphertext");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
[[nodiscard]] int ScenarioOutboxRejectsProtectorMismatch() {
|
|
const auto conversationId = FilledId<ConversationId>(1);
|
|
const auto checkpoint = MakeCheckpoint(conversationId, 8, 2);
|
|
const auto nonce = FilledId<ChallengeNonce>(3);
|
|
auto gate = FreshnessGate(checkpoint);
|
|
auto verifier = TestVerifier();
|
|
auto store = TestOutboxStore();
|
|
auto protector = TestProtector();
|
|
protector.wrongConversation = true;
|
|
auto coordinator = OutboxCoordinator(gate, store, protector);
|
|
if (coordinator.enqueue(MakeMessage(conversationId))
|
|
!= EnqueueResult::Queued
|
|
|| !gate.beginChallenge(nonce)
|
|
|| gate.acceptResponse(MakeResponse(checkpoint, nonce), verifier)
|
|
!= FreshnessResponseResult::Accepted
|
|
|| coordinator.dispatchNext().result
|
|
!= OutboxDispatchResult::InvalidItem
|
|
|| store.items.size() != 1) {
|
|
return Fail("mismatched protected envelope reached transport");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
[[nodiscard]] int ScenarioUploadControllerAcknowledgesRpcSuccess() {
|
|
const auto conversationId = FilledId<ConversationId>(1);
|
|
const auto checkpoint = MakeCheckpoint(conversationId, 8, 2);
|
|
const auto nonce = FilledId<ChallengeNonce>(3);
|
|
auto gate = FreshnessGate(checkpoint);
|
|
auto verifier = TestVerifier();
|
|
auto store = TestOutboxStore();
|
|
auto protector = TestProtector();
|
|
auto transport = TestTransport();
|
|
auto completions = std::vector<UploadCompletion>();
|
|
auto coordinator = OutboxCoordinator(gate, store, protector);
|
|
auto controller = OutboxUploadController(
|
|
coordinator,
|
|
transport,
|
|
[&](UploadCompletion completion) {
|
|
completions.push_back(completion);
|
|
});
|
|
if (coordinator.enqueue(MakeMessage(conversationId))
|
|
!= EnqueueResult::Queued
|
|
|| !gate.beginChallenge(nonce)
|
|
|| gate.acceptResponse(MakeResponse(checkpoint, nonce), verifier)
|
|
!= FreshnessResponseResult::Accepted
|
|
|| controller.pump() != UploadPumpResult::Started
|
|
|| !controller.uploadInProgress()
|
|
|| controller.pump() != UploadPumpResult::UploadInProgress
|
|
|| transport.uploads.size() != 1
|
|
|| store.items.size() != 1) {
|
|
return Fail("upload controller did not retain an in-flight envelope");
|
|
}
|
|
transport.finish(0, TelegramTransport::UploadResult::Accepted);
|
|
if (controller.uploadInProgress()
|
|
|| !store.items.empty()
|
|
|| completions.size() != 1
|
|
|| completions.front().objectId != transport.uploads.front().objectId
|
|
|| completions.front().transportResult
|
|
!= TelegramTransport::UploadResult::Accepted
|
|
|| !completions.front().outboxUpdated
|
|
|| controller.pump() != UploadPumpResult::Empty) {
|
|
return Fail("successful Telegram RPC did not acknowledge the outbox");
|
|
}
|
|
transport.finish(0, TelegramTransport::UploadResult::Accepted);
|
|
if (completions.size() != 1) {
|
|
return Fail("duplicate upload callback changed the outbox twice");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
[[nodiscard]] int ScenarioUploadControllerRetriesExactEnvelope() {
|
|
const auto conversationId = FilledId<ConversationId>(1);
|
|
const auto checkpoint = MakeCheckpoint(conversationId, 8, 2);
|
|
const auto nonce = FilledId<ChallengeNonce>(3);
|
|
auto gate = FreshnessGate(checkpoint);
|
|
auto verifier = TestVerifier();
|
|
auto store = TestOutboxStore();
|
|
auto protector = TestProtector();
|
|
auto transport = TestTransport();
|
|
auto coordinator = OutboxCoordinator(gate, store, protector);
|
|
auto controller = OutboxUploadController(
|
|
coordinator,
|
|
transport,
|
|
nullptr);
|
|
if (coordinator.enqueue(MakeMessage(conversationId))
|
|
!= EnqueueResult::Queued
|
|
|| !gate.beginChallenge(nonce)
|
|
|| gate.acceptResponse(MakeResponse(checkpoint, nonce), verifier)
|
|
!= FreshnessResponseResult::Accepted
|
|
|| controller.pump() != UploadPumpResult::Started) {
|
|
return Fail("upload retry setup failed");
|
|
}
|
|
transport.finish(0, TelegramTransport::UploadResult::RetryableError);
|
|
if (controller.uploadInProgress()
|
|
|| store.items.size() != 1
|
|
|| controller.pump() != UploadPumpResult::Started
|
|
|| transport.uploads.size() != 2
|
|
|| transport.uploads[0] != transport.uploads[1]
|
|
|| protector.calls != 1) {
|
|
return Fail("upload controller did not retry exact ciphertext");
|
|
}
|
|
transport.finish(0, TelegramTransport::UploadResult::Accepted);
|
|
if (!controller.uploadInProgress()
|
|
|| store.items.size() != 1) {
|
|
return Fail("stale upload callback acknowledged the active retry");
|
|
}
|
|
transport.finish(1, TelegramTransport::UploadResult::Accepted);
|
|
if (!store.items.empty()) {
|
|
return Fail("retried upload did not leave the outbox");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
[[nodiscard]] int ScenarioCarrierRetryIgnoresStaleBackendUpload() {
|
|
const auto conversationId = FilledId<ConversationId>(1);
|
|
auto envelope = MakeEnvelope();
|
|
envelope.conversationId = conversationId;
|
|
const auto encoded = EnvelopeCodecV1().encode(envelope);
|
|
if (!encoded) {
|
|
return Fail("carrier stale callback fixture could not be encoded");
|
|
}
|
|
auto backend = TestCarrierBackend();
|
|
auto results = std::vector<TelegramTransport::UploadResult>();
|
|
auto transport = TelegramCarrierTransport(
|
|
conversationId,
|
|
42,
|
|
backend);
|
|
const auto upload = [&] {
|
|
transport.uploadExact(
|
|
*encoded,
|
|
[&](TelegramTransport::UploadResult result) {
|
|
results.push_back(result);
|
|
});
|
|
};
|
|
upload();
|
|
backend.uploadCallbacks[0](
|
|
TelegramTransport::UploadResult::RetryableError,
|
|
{});
|
|
upload();
|
|
backend.uploadCallbacks[0](
|
|
TelegramTransport::UploadResult::Accepted,
|
|
UploadedCarrierFile{ QByteArray("stale token") });
|
|
if (results != std::vector{
|
|
TelegramTransport::UploadResult::RetryableError }
|
|
|| !backend.sendCallbacks.empty()) {
|
|
return Fail("stale backend upload entered the replacement send stage");
|
|
}
|
|
backend.uploadCallbacks[1](
|
|
TelegramTransport::UploadResult::Accepted,
|
|
UploadedCarrierFile{ QByteArray("current token") });
|
|
if (backend.sendCallbacks.size() != 1) {
|
|
return Fail("current backend upload did not enter its send stage");
|
|
}
|
|
backend.sendCallbacks[0](TelegramTransport::UploadResult::Accepted);
|
|
if (results != std::vector{
|
|
TelegramTransport::UploadResult::RetryableError,
|
|
TelegramTransport::UploadResult::Accepted }) {
|
|
return Fail("current carrier retry did not complete");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
[[nodiscard]] int ScenarioVaultRetryIgnoresStaleBackendUpload() {
|
|
auto backend = TestCarrierBackend();
|
|
auto results = std::vector<TelegramTransport::UploadResult>();
|
|
auto transport = TelegramCloudVaultTransport(777, backend);
|
|
const auto upload = [&] {
|
|
transport.uploadExact(
|
|
QByteArray("password-protected vault"),
|
|
[&](TelegramTransport::UploadResult result) {
|
|
results.push_back(result);
|
|
});
|
|
};
|
|
upload();
|
|
backend.uploadCallbacks[0](
|
|
TelegramTransport::UploadResult::RetryableError,
|
|
{});
|
|
upload();
|
|
backend.uploadCallbacks[0](
|
|
TelegramTransport::UploadResult::Accepted,
|
|
UploadedCarrierFile{ QByteArray("stale vault token") });
|
|
if (results != std::vector{
|
|
TelegramTransport::UploadResult::RetryableError }
|
|
|| !backend.sendCallbacks.empty()) {
|
|
return Fail("stale vault upload entered the replacement send stage");
|
|
}
|
|
backend.uploadCallbacks[1](
|
|
TelegramTransport::UploadResult::Accepted,
|
|
UploadedCarrierFile{ QByteArray("current vault token") });
|
|
if (backend.sendCallbacks.size() != 1) {
|
|
return Fail("current vault upload did not enter its send stage");
|
|
}
|
|
backend.sendCallbacks[0](TelegramTransport::UploadResult::Accepted);
|
|
if (results != std::vector{
|
|
TelegramTransport::UploadResult::RetryableError,
|
|
TelegramTransport::UploadResult::Accepted }) {
|
|
return Fail("current vault retry did not complete");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
[[nodiscard]] int ScenarioUploadRetriesAfterAcknowledgeFailure() {
|
|
const auto conversationId = FilledId<ConversationId>(1);
|
|
const auto checkpoint = MakeCheckpoint(conversationId, 8, 2);
|
|
const auto nonce = FilledId<ChallengeNonce>(3);
|
|
auto gate = FreshnessGate(checkpoint);
|
|
auto verifier = TestVerifier();
|
|
auto store = TestOutboxStore();
|
|
auto protector = TestProtector();
|
|
auto transport = TestTransport();
|
|
auto completions = std::vector<UploadCompletion>();
|
|
auto coordinator = OutboxCoordinator(gate, store, protector);
|
|
auto controller = OutboxUploadController(
|
|
coordinator,
|
|
transport,
|
|
[&](UploadCompletion completion) {
|
|
completions.push_back(completion);
|
|
});
|
|
if (coordinator.enqueue(MakeMessage(conversationId))
|
|
!= EnqueueResult::Queued
|
|
|| !gate.beginChallenge(nonce)
|
|
|| gate.acceptResponse(MakeResponse(checkpoint, nonce), verifier)
|
|
!= FreshnessResponseResult::Accepted
|
|
|| controller.pump() != UploadPumpResult::Started) {
|
|
return Fail("acknowledgement failure retry setup failed");
|
|
}
|
|
store.failRemove = true;
|
|
transport.finish(0, TelegramTransport::UploadResult::Accepted);
|
|
store.failRemove = false;
|
|
if (completions.size() != 1
|
|
|| completions.front().outboxUpdated
|
|
|| controller.uploadInProgress()
|
|
|| controller.pump() != UploadPumpResult::Started
|
|
|| transport.uploads.size() != 2
|
|
|| transport.uploads[0] != transport.uploads[1]
|
|
|| protector.calls != 1) {
|
|
return Fail("failed local acknowledgement left upload stuck");
|
|
}
|
|
transport.finish(1, TelegramTransport::UploadResult::Accepted);
|
|
if (completions.size() != 2
|
|
|| !completions.back().outboxUpdated
|
|
|| !store.items.empty()
|
|
|| controller.pump() != UploadPumpResult::Empty) {
|
|
return Fail("acknowledgement retry did not clear the outbox");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
[[nodiscard]] int ScenarioUploadWaitsForDurablePreAcknowledgement() {
|
|
const auto conversationId = FilledId<ConversationId>(1);
|
|
const auto checkpoint = MakeCheckpoint(conversationId, 8, 2);
|
|
const auto nonce = FilledId<ChallengeNonce>(3);
|
|
auto gate = FreshnessGate(checkpoint);
|
|
auto verifier = TestVerifier();
|
|
auto store = TestOutboxStore();
|
|
auto protector = TestProtector();
|
|
auto transport = TestTransport();
|
|
auto completions = std::vector<UploadCompletion>();
|
|
auto prepared = false;
|
|
auto prepareCalls = 0;
|
|
auto coordinator = OutboxCoordinator(gate, store, protector);
|
|
auto controller = OutboxUploadController(
|
|
coordinator,
|
|
transport,
|
|
[&](ObjectId) {
|
|
++prepareCalls;
|
|
return prepared;
|
|
},
|
|
[&](UploadCompletion completion) {
|
|
completions.push_back(completion);
|
|
});
|
|
if (coordinator.enqueue(MakeMessage(conversationId))
|
|
!= EnqueueResult::Queued
|
|
|| !gate.beginChallenge(nonce)
|
|
|| gate.acceptResponse(MakeResponse(checkpoint, nonce), verifier)
|
|
!= FreshnessResponseResult::Accepted
|
|
|| controller.pump() != UploadPumpResult::Started) {
|
|
return Fail("pre-acknowledgement retry setup failed");
|
|
}
|
|
transport.finish(0, TelegramTransport::UploadResult::Accepted);
|
|
if (prepareCalls != 1
|
|
|| completions.size() != 1
|
|
|| completions.front().outboxUpdated
|
|
|| controller.uploadInProgress()
|
|
|| store.items.size() != 1
|
|
|| controller.pump() != UploadPumpResult::Started
|
|
|| transport.uploads.size() != 2
|
|
|| transport.uploads[0] != transport.uploads[1]
|
|
|| protector.calls != 1) {
|
|
return Fail("failed pre-acknowledgement discarded exact ciphertext");
|
|
}
|
|
prepared = true;
|
|
transport.finish(1, TelegramTransport::UploadResult::Accepted);
|
|
if (prepareCalls != 2
|
|
|| completions.size() != 2
|
|
|| !completions.back().outboxUpdated
|
|
|| !store.items.empty()
|
|
|| controller.pump() != UploadPumpResult::Empty) {
|
|
return Fail("durable pre-acknowledgement did not clear the outbox");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
[[nodiscard]] int ScenarioUploadCallbackCannotOutliveController() {
|
|
const auto conversationId = FilledId<ConversationId>(1);
|
|
const auto checkpoint = MakeCheckpoint(conversationId, 8, 2);
|
|
const auto nonce = FilledId<ChallengeNonce>(3);
|
|
auto gate = FreshnessGate(checkpoint);
|
|
auto verifier = TestVerifier();
|
|
auto store = TestOutboxStore();
|
|
auto protector = TestProtector();
|
|
auto transport = TestTransport();
|
|
auto coordinator = OutboxCoordinator(gate, store, protector);
|
|
if (coordinator.enqueue(MakeMessage(conversationId))
|
|
!= EnqueueResult::Queued
|
|
|| !gate.beginChallenge(nonce)
|
|
|| gate.acceptResponse(MakeResponse(checkpoint, nonce), verifier)
|
|
!= FreshnessResponseResult::Accepted) {
|
|
return Fail("upload callback lifetime setup failed");
|
|
}
|
|
{
|
|
auto controller = OutboxUploadController(
|
|
coordinator,
|
|
transport,
|
|
nullptr);
|
|
if (controller.pump() != UploadPumpResult::Started) {
|
|
return Fail("upload callback lifetime setup did not start");
|
|
}
|
|
}
|
|
transport.finish(0, TelegramTransport::UploadResult::Accepted);
|
|
if (store.items.size() != 1) {
|
|
return Fail("destroyed upload controller handled a late callback");
|
|
}
|
|
auto replacement = OutboxUploadController(
|
|
coordinator,
|
|
transport,
|
|
nullptr);
|
|
if (replacement.pump() != UploadPumpResult::Started
|
|
|| transport.uploads.size() != 2
|
|
|| transport.uploads[0] != transport.uploads[1]) {
|
|
return Fail("destroyed upload controller left the outbox stuck");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
[[nodiscard]] int ScenarioCarrierAcknowledgesOnlyAfterSendMedia() {
|
|
const auto conversationId = FilledId<ConversationId>(1);
|
|
auto envelope = MakeEnvelope();
|
|
envelope.conversationId = conversationId;
|
|
const auto encoded = EnvelopeCodecV1().encode(envelope);
|
|
if (!encoded) {
|
|
return Fail("carrier test envelope could not be encoded");
|
|
}
|
|
auto backend = TestCarrierBackend();
|
|
auto results = std::vector<TelegramTransport::UploadResult>();
|
|
auto transport = TelegramCarrierTransport(
|
|
conversationId,
|
|
42,
|
|
backend);
|
|
transport.uploadExact(*encoded, [&](TelegramTransport::UploadResult result) {
|
|
results.push_back(result);
|
|
});
|
|
if (backend.uploadedBytes != std::vector<QByteArray>{
|
|
encoded->bytes }
|
|
|| backend.uploadCallbacks.size() != 1
|
|
|| !backend.sendCallbacks.empty()
|
|
|| !results.empty()
|
|
|| backend.filenames != std::vector<QString>{
|
|
ProtectedContentCarrierFilename() }) {
|
|
return Fail("carrier bypassed the Telegram upload stage");
|
|
}
|
|
backend.uploadCallbacks.front()(
|
|
TelegramTransport::UploadResult::Accepted,
|
|
UploadedCarrierFile{ QByteArray("uploaded token") });
|
|
if (backend.sendCallbacks.size() != 1
|
|
|| backend.peerIds != std::vector<std::uint64_t>{ 42 }
|
|
|| backend.tokens != std::vector<QByteArray>{
|
|
QByteArray("uploaded token") }
|
|
|| !results.empty()) {
|
|
return Fail("carrier acknowledged before messages.sendMedia");
|
|
}
|
|
backend.sendCallbacks.front()(
|
|
TelegramTransport::UploadResult::Accepted);
|
|
if (results != std::vector<TelegramTransport::UploadResult>{
|
|
TelegramTransport::UploadResult::Accepted }
|
|
|| backend.filenames.size() != 2
|
|
|| backend.filenames[0] != backend.filenames[1]
|
|
|| backend.mimeTypes.size() != 2
|
|
|| backend.mimeTypes[0] != backend.mimeTypes[1]) {
|
|
return Fail("carrier did not preserve generic document metadata");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
[[nodiscard]] int ScenarioCarrierPropagatesUploadFailure() {
|
|
const auto conversationId = FilledId<ConversationId>(1);
|
|
auto envelope = MakeEnvelope();
|
|
envelope.conversationId = conversationId;
|
|
envelope.objectKind = ObjectKind::AccountCredential;
|
|
const auto encoded = EnvelopeCodecV1().encode(envelope);
|
|
if (!encoded) {
|
|
return Fail("carrier control envelope could not be encoded");
|
|
}
|
|
auto backend = TestCarrierBackend();
|
|
auto result = std::optional<TelegramTransport::UploadResult>();
|
|
auto transport = TelegramCarrierTransport(
|
|
conversationId,
|
|
42,
|
|
backend);
|
|
transport.uploadExact(*encoded, [&](TelegramTransport::UploadResult value) {
|
|
result = value;
|
|
});
|
|
if (backend.filenames != std::vector<QString>{
|
|
ProtectedControlCarrierFilename() }) {
|
|
return Fail("carrier did not route control metadata separately");
|
|
}
|
|
backend.uploadCallbacks.front()(
|
|
TelegramTransport::UploadResult::RetryableError,
|
|
UploadedCarrierFile());
|
|
if (result != TelegramTransport::UploadResult::RetryableError
|
|
|| !backend.sendCallbacks.empty()) {
|
|
return Fail("carrier converted an upload failure into a send");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
[[nodiscard]] int ScenarioCarrierDownloadsOnlyUntrustedBytes() {
|
|
const auto conversationId = FilledId<ConversationId>(1);
|
|
auto backend = TestCarrierBackend();
|
|
auto result = std::optional<TelegramTransport::DownloadResult>();
|
|
auto transport = TelegramCarrierTransport(
|
|
conversationId,
|
|
42,
|
|
backend);
|
|
transport.downloadPage(
|
|
{
|
|
.conversationId = conversationId,
|
|
.cursor = QByteArray("page one"),
|
|
.limit = 37,
|
|
},
|
|
[&](TelegramTransport::DownloadResult value) {
|
|
result = std::move(value);
|
|
});
|
|
if (backend.downloadCallbacks.size() != 1
|
|
|| backend.cursors != std::vector<QByteArray>{
|
|
QByteArray("page one") }
|
|
|| backend.limits != std::vector<int>{ 37 }
|
|
|| result) {
|
|
return Fail("carrier download did not remain asynchronous");
|
|
}
|
|
backend.downloadCallbacks.front()(
|
|
TelegramTransport::UploadResult::Accepted,
|
|
{
|
|
.untrustedObjects = {
|
|
{
|
|
.bytes = QByteArray("untrusted one"),
|
|
.observedTelegramPeerIdBinding = 42,
|
|
.observedSenderTelegramUserIdBinding = 1001,
|
|
.observedMessageId = 7,
|
|
},
|
|
{
|
|
.bytes = QByteArray("untrusted two"),
|
|
.observedTelegramPeerIdBinding = 42,
|
|
.observedSenderTelegramUserIdBinding = 1002,
|
|
.observedMessageId = 8,
|
|
},
|
|
},
|
|
.nextCursor = QByteArray("page two"),
|
|
.complete = false,
|
|
});
|
|
if (!result
|
|
|| result->result != TelegramTransport::UploadResult::Accepted
|
|
|| result->untrustedObjects
|
|
!= std::vector<TelegramTransport::UntrustedObject>{
|
|
{
|
|
.bytes = QByteArray("untrusted one"),
|
|
.observedTelegramPeerIdBinding = 42,
|
|
.observedSenderTelegramUserIdBinding = 1001,
|
|
.observedMessageId = 7,
|
|
},
|
|
{
|
|
.bytes = QByteArray("untrusted two"),
|
|
.observedTelegramPeerIdBinding = 42,
|
|
.observedSenderTelegramUserIdBinding = 1002,
|
|
.observedMessageId = 8,
|
|
} }
|
|
|| result->nextCursor != QByteArray("page two")
|
|
|| result->complete) {
|
|
return Fail("carrier transport interpreted untrusted protocol bytes");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
[[nodiscard]] int ScenarioCloudVaultUsesSavedMessagesCarrier() {
|
|
auto backend = TestCarrierBackend();
|
|
auto transport = TelegramCloudVaultTransport(777, backend);
|
|
auto uploadResult = std::optional<TelegramTransport::UploadResult>();
|
|
transport.uploadExact(
|
|
QByteArray("password-protected vault"),
|
|
[&](TelegramTransport::UploadResult result) {
|
|
uploadResult = result;
|
|
});
|
|
if (backend.uploadedBytes != std::vector<QByteArray>{
|
|
QByteArray("password-protected vault") }
|
|
|| backend.filenames != std::vector<QString>{
|
|
CloudVaultCarrierFilename() }
|
|
|| backend.mimeTypes != std::vector<QString>{
|
|
CloudVaultCarrierMimeType() }
|
|
|| uploadResult) {
|
|
return Fail("cloud vault bypassed its opaque upload stage");
|
|
}
|
|
backend.uploadCallbacks.front()(
|
|
TelegramTransport::UploadResult::Accepted,
|
|
UploadedCarrierFile{ QByteArray("vault token") });
|
|
if (backend.peerIds != std::vector<std::uint64_t>{ 777 }
|
|
|| backend.sendCallbacks.size() != 1
|
|
|| uploadResult) {
|
|
return Fail("cloud vault was acknowledged before Saved Messages send");
|
|
}
|
|
backend.sendCallbacks.front()(
|
|
TelegramTransport::UploadResult::Accepted);
|
|
if (uploadResult != TelegramTransport::UploadResult::Accepted
|
|
|| backend.filenames.size() != 2
|
|
|| backend.filenames[0] != backend.filenames[1]) {
|
|
return Fail("cloud vault carrier did not complete exact upload");
|
|
}
|
|
auto discovery = std::optional<bool>();
|
|
transport.discover([&](
|
|
TelegramTransport::UploadResult result,
|
|
bool present) {
|
|
if (result == TelegramTransport::UploadResult::Accepted) {
|
|
discovery = present;
|
|
}
|
|
});
|
|
if (backend.discoveryCallbacks.size() != 1
|
|
|| backend.peerIds.back() != 777
|
|
|| discovery) {
|
|
return Fail("cloud vault discovery bypassed its metadata backend");
|
|
}
|
|
backend.discoveryCallbacks.front()(
|
|
TelegramTransport::UploadResult::Accepted,
|
|
true);
|
|
if (discovery != true) {
|
|
return Fail("cloud vault discovery did not return backend presence");
|
|
}
|
|
auto page = std::optional<CarrierDownloadPage>();
|
|
transport.downloadPage(
|
|
QByteArray("cursor"),
|
|
50,
|
|
[&](
|
|
TelegramTransport::UploadResult result,
|
|
CarrierDownloadPage value) {
|
|
if (result == TelegramTransport::UploadResult::Accepted) {
|
|
page = std::move(value);
|
|
}
|
|
});
|
|
backend.downloadCallbacks.front()(
|
|
TelegramTransport::UploadResult::Accepted,
|
|
{
|
|
.untrustedObjects = {
|
|
{ .bytes = QByteArray("untrusted vault") },
|
|
},
|
|
.nextCursor = QByteArray("next"),
|
|
.complete = true,
|
|
});
|
|
if (!page
|
|
|| page->untrustedObjects
|
|
!= std::vector<TelegramTransport::UntrustedObject>{
|
|
{ .bytes = QByteArray("untrusted vault") } }
|
|
|| page->nextCursor != QByteArray("next")
|
|
|| !page->complete) {
|
|
return Fail("cloud vault carrier interpreted remote vault bytes");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
[[nodiscard]] int ScenarioCarrierMetadataRecognitionIsExact() {
|
|
const auto mime = ProtectedCarrierMimeType();
|
|
const auto fileId = FilledId<FileId>(9);
|
|
const auto fileChunk = ProtectedFileChunkCarrierFilename(fileId);
|
|
const auto legacyFileChunk = QString(fileChunk).replace(
|
|
QString::fromLatin1("protected-file-"),
|
|
QString::fromLatin1("protected_file_"));
|
|
const auto legacyShortFileChunk = QString::fromLatin1(
|
|
"protected_file_"
|
|
"9db6fe65cacc30b368b72533b6d6901c914101ffae6e4dbd"
|
|
".tde2e");
|
|
if (!IsProtectedGroupCarrierMetadata(
|
|
ProtectedLegacyCarrierFilename(),
|
|
mime)
|
|
|| !IsProtectedGroupCarrierMetadata(
|
|
ProtectedControlCarrierFilename(),
|
|
mime)
|
|
|| !IsProtectedGroupCarrierMetadata(
|
|
ProtectedContentCarrierFilename(),
|
|
mime)
|
|
|| !IsProtectedGroupCarrierMetadata(fileChunk, mime)
|
|
|| ProtectedFileChunkCarrierFileId(fileChunk) != fileId
|
|
|| !IsProtectedGroupCarrierMetadata(legacyFileChunk, mime)
|
|
|| ProtectedFileChunkCarrierFileId(legacyFileChunk) != fileId
|
|
|| !IsProtectedGroupCarrierMetadata(legacyShortFileChunk, mime)
|
|
|| ProtectedFileChunkCarrierFileId(
|
|
legacyShortFileChunk).has_value()
|
|
|| !IsProtectedGroupCarrierFilename(
|
|
ProtectedLegacyCarrierFilename())
|
|
|| !IsProtectedGroupCarrierFilename(fileChunk)
|
|
|| !IsProtectedGroupCarrierFilename(legacyFileChunk)
|
|
|| !IsProtectedGroupCarrierFilename(legacyShortFileChunk)
|
|
|| ProtectedFileChunkCarrierFilename(FileId()).size()
|
|
|| ProtectedFileChunkCarrierFileId(
|
|
fileChunk.toUpper()).has_value()
|
|
|| ProtectedFileChunkCarrierFileId(
|
|
fileChunk + QString::fromLatin1("x")).has_value()
|
|
|| !IsProtectedVaultCarrierMetadata(
|
|
CloudVaultCarrierFilename(),
|
|
mime)
|
|
|| !IsProtectedCarrierMetadata(
|
|
CloudVaultCarrierFilename(),
|
|
mime)
|
|
|| IsProtectedCarrierMetadata(
|
|
QString::fromLatin1("notes.tde2e"),
|
|
mime)
|
|
|| IsProtectedCarrierMetadata(
|
|
ProtectedControlCarrierFilename(),
|
|
QString::fromLatin1("application/pdf"))
|
|
|| IsProtectedGroupCarrierMetadata(
|
|
CloudVaultCarrierFilename(),
|
|
mime)
|
|
|| IsProtectedGroupCarrierFilename(
|
|
CloudVaultCarrierFilename())
|
|
|| IsProtectedGroupCarrierFilename(
|
|
QString::fromLatin1("notes.tde2e"))
|
|
|| IsProtectedGroupCarrierFilename(
|
|
legacyShortFileChunk.toUpper())
|
|
|| IsProtectedGroupCarrierFilename(
|
|
legacyShortFileChunk + QString::fromLatin1("x"))) {
|
|
return Fail("protected carrier metadata recognition was not exact");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
int main(int, char *[]) {
|
|
for (const auto scenario : {
|
|
ScenarioEnvelopeValidation,
|
|
ScenarioHistoryAccessBoundary,
|
|
ScenarioEnvelopeCodec,
|
|
ScenarioFreshnessConfirmation,
|
|
ScenarioFreshnessResynchronization,
|
|
ScenarioFreshnessFork,
|
|
ScenarioOutboxWaitsForFreshness,
|
|
ScenarioOutboxSendsAfterFreshness,
|
|
ScenarioOutboxRetriesExactCiphertext,
|
|
ScenarioOutboxRejectsProtectorMismatch,
|
|
ScenarioUploadControllerAcknowledgesRpcSuccess,
|
|
ScenarioUploadControllerRetriesExactEnvelope,
|
|
ScenarioUploadRetriesAfterAcknowledgeFailure,
|
|
ScenarioUploadWaitsForDurablePreAcknowledgement,
|
|
ScenarioUploadCallbackCannotOutliveController,
|
|
ScenarioCarrierAcknowledgesOnlyAfterSendMedia,
|
|
ScenarioCarrierPropagatesUploadFailure,
|
|
ScenarioCarrierRetryIgnoresStaleBackendUpload,
|
|
ScenarioVaultRetryIgnoresStaleBackendUpload,
|
|
ScenarioCarrierDownloadsOnlyUntrustedBytes,
|
|
ScenarioCloudVaultUsesSavedMessagesCarrier,
|
|
ScenarioCarrierMetadataRecognitionIsExact,
|
|
}) {
|
|
if (const auto result = scenario()) {
|
|
return result;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|