ZaStoGram_desktop/Telegram/SourceFiles/e2e_cloud/protocol/public_group_bootstrap.cpp
2026-08-01 22:41:13 +03:00

261 lines
8.1 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/protocol/public_group_bootstrap.h"
#include <algorithm>
#include <map>
#include <utility>
namespace E2ECloud {
namespace {
inline constexpr auto kMaximumObjects = std::size_t(65'536);
inline constexpr auto kMaximumBytes = std::uint64_t(512 * 1024 * 1024);
struct Candidate {
TelegramTransport::UntrustedObject observed;
TransportEnvelope envelope;
};
[[nodiscard]] bool SameOwnerEnvelope(
const Candidate &candidate,
const SignedGroupGenesis &genesis,
ObjectKind kind,
std::uint64_t generation) {
return candidate.envelope.conversationId == genesis.conversationId
&& candidate.envelope.objectKind == kind
&& candidate.envelope.senderAccountId == genesis.ownerAccountId
&& candidate.envelope.senderClientId == genesis.ownerClientId
&& candidate.envelope.telegramPeerIdBinding
== genesis.telegramPeerIdBinding
&& candidate.envelope.epochOrGeneration == generation
&& candidate.observed.observedTelegramPeerIdBinding
== genesis.telegramPeerIdBinding
&& candidate.observed.observedSenderTelegramUserIdBinding
== genesis.ownerTelegramUserIdBinding
&& candidate.observed.observedMessageId > 0
&& candidate.envelope.authenticationData == QByteArray(
reinterpret_cast<const char*>(genesis.ownerSignature.data()),
int(genesis.ownerSignature.size()));
}
[[nodiscard]] bool BootstrapKind(const TransportEnvelope &envelope) {
switch (envelope.objectKind) {
case ObjectKind::InitialGroupState:
case ObjectKind::AccountCredential:
return envelope.epochOrGeneration == 1;
case ObjectKind::MlsGroupInfo:
return envelope.epochOrGeneration == 0;
default:
return false;
}
}
} // namespace
bool IsPublicGroupBootstrapCandidate(
const TelegramTransport::UntrustedObject &object,
std::uint64_t expectedTelegramPeerIdBinding,
std::optional<ConversationId> expectedConversationId,
const EnvelopeCodec &envelopeCodec) {
const auto envelope = envelopeCodec.decodeUntrusted(object.bytes);
return expectedTelegramPeerIdBinding
&& (!expectedConversationId || *expectedConversationId)
&& envelope
&& BootstrapKind(*envelope)
&& envelope->telegramPeerIdBinding
== expectedTelegramPeerIdBinding
&& object.observedTelegramPeerIdBinding
== expectedTelegramPeerIdBinding
&& object.observedMessageId > 0
&& (!expectedConversationId
|| envelope->conversationId == *expectedConversationId);
}
bool PublicGroupBootstrapCanReachCheckpoint(
const VerifiedPublicGroupBootstrap &verified,
Checkpoint targetCheckpoint) {
return targetCheckpoint.conversationId
== verified.genesis.conversationId
&& targetCheckpoint.generation
&& targetCheckpoint.stateHash
&& targetCheckpoint.generation >= verified.checkpoint.generation
&& (targetCheckpoint.generation != verified.checkpoint.generation
|| targetCheckpoint == verified.checkpoint);
}
PublicGroupBootstrapOutcome VerifyPublicGroupBootstrap(
const std::vector<TelegramTransport::UntrustedObject> &objects,
std::uint64_t expectedTelegramPeerIdBinding,
std::optional<ConversationId> expectedConversationId,
std::optional<AccountId> expectedOwnerAccountId,
const EnvelopeCodec &envelopeCodec,
const Sha256Provider &sha256) {
if (!expectedTelegramPeerIdBinding
|| (expectedConversationId && !*expectedConversationId)
|| (expectedOwnerAccountId && !*expectedOwnerAccountId)) {
return {
.status = PublicGroupBootstrapStatus::CapacityExceeded,
.verified = std::nullopt,
};
}
auto totalBytes = std::uint64_t();
auto unique = std::map<ObjectId, TelegramTransport::UntrustedObject>();
auto candidates = std::vector<Candidate>();
for (const auto &object : objects) {
const auto envelope = envelopeCodec.decodeUntrusted(object.bytes);
if (!envelope
|| !BootstrapKind(*envelope)
|| envelope->telegramPeerIdBinding
!= expectedTelegramPeerIdBinding
|| object.observedTelegramPeerIdBinding
!= expectedTelegramPeerIdBinding
|| object.observedMessageId <= 0
|| (expectedConversationId
&& envelope->conversationId
!= *expectedConversationId)) {
continue;
}
if (object.bytes.size() < 0
|| unique.size() == kMaximumObjects
|| totalBytes > kMaximumBytes
- std::uint64_t(object.bytes.size())) {
return {
.status = PublicGroupBootstrapStatus::CapacityExceeded,
.verified = std::nullopt,
};
}
totalBytes += std::uint64_t(object.bytes.size());
if (totalBytes > kMaximumBytes) {
return {
.status = PublicGroupBootstrapStatus::CapacityExceeded,
.verified = std::nullopt,
};
}
const auto i = unique.find(envelope->objectId);
if (i != end(unique)) {
if (i->second.bytes != object.bytes
|| i->second.observedTelegramPeerIdBinding
!= object.observedTelegramPeerIdBinding
|| i->second.observedSenderTelegramUserIdBinding
!= object.observedSenderTelegramUserIdBinding) {
return {
.status = PublicGroupBootstrapStatus::ObjectConflict,
.verified = std::nullopt,
};
}
continue;
}
unique.emplace(envelope->objectId, object);
candidates.push_back({
.observed = object,
.envelope = *envelope,
});
}
auto results = std::vector<VerifiedPublicGroupBootstrap>();
for (const auto &genesisCandidate : candidates) {
if (genesisCandidate.envelope.objectKind
!= ObjectKind::InitialGroupState
|| genesisCandidate.envelope.epochOrGeneration != 1
|| genesisCandidate.envelope.payloadHash
!= sha256.digest(genesisCandidate.envelope.payload)) {
continue;
}
const auto genesis = SignedGroupGenesisCodecV1().decode(
genesisCandidate.envelope.payload);
if (!genesis
|| genesisCandidate.envelope.objectId
!= genesis->genesisObjectId
|| (expectedOwnerAccountId
&& genesis->ownerAccountId != *expectedOwnerAccountId)
|| !SameOwnerEnvelope(
genesisCandidate,
*genesis,
ObjectKind::InitialGroupState,
1)) {
continue;
}
const auto mls = std::find_if(
begin(candidates),
end(candidates),
[&](const Candidate &candidate) {
return candidate.envelope.objectId
== genesis->initialMlsPublicObjectId;
});
if (mls == end(candidates)
|| !SameOwnerEnvelope(
*mls,
*genesis,
ObjectKind::MlsGroupInfo,
0)
|| mls->envelope.payloadHash
!= sha256.digest(mls->envelope.payload)) {
continue;
}
for (const auto &credentialCandidate : candidates) {
if (!SameOwnerEnvelope(
credentialCandidate,
*genesis,
ObjectKind::AccountCredential,
1)
|| credentialCandidate.envelope.payloadHash
!= sha256.digest(credentialCandidate.envelope.payload)) {
continue;
}
const auto credential = AccountCredentialCodecV1().decode(
credentialCandidate.envelope.payload);
if (!credential) {
continue;
}
auto verified = VerifySignedGroupGenesisPublic({
.genesis = &*genesis,
.ownerCredential = &*credential,
.genesisObjectId = genesisCandidate.envelope.objectId,
.initialMlsPublicObjectId = mls->envelope.objectId,
.initialMlsPublicObject = mls->envelope.payload,
.initialArchiveKey = nullptr,
}, sha256);
if (!verified.verified) {
continue;
}
results.push_back({
.genesis = *genesis,
.ownerCredential = *credential,
.initialMlsPublicObject = mls->envelope.payload,
.state = std::move(verified.verified->state),
.checkpoint = verified.verified->checkpoint,
.genesisTelegramMessageId =
genesisCandidate.observed.observedMessageId,
});
break;
}
}
if (results.empty()) {
return {
.status = PublicGroupBootstrapStatus::Missing,
.verified = std::nullopt,
};
}
const auto firstGenesis = SignedGroupGenesisCodecV1().encode(
results.front().genesis);
for (auto i = std::size_t(1); i != results.size(); ++i) {
if (SignedGroupGenesisCodecV1().encode(results[i].genesis)
!= firstGenesis) {
return {
.status = PublicGroupBootstrapStatus::Ambiguous,
.verified = std::nullopt,
};
}
}
return {
.status = PublicGroupBootstrapStatus::Verified,
.verified = std::move(results.front()),
};
}
} // namespace E2ECloud