ZaStoGram_desktop/Telegram/SourceFiles/e2e_cloud/protocol/public_join_catchup.cpp
2026-08-01 22:39:28 +03:00

565 lines
18 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_join_catchup.h"
#include "e2e_cloud/core/envelope_codec.h"
#include "e2e_cloud/mls/client_key_package.h"
#include <algorithm>
#include <map>
#include <utility>
namespace E2ECloud {
namespace {
struct ObservedEnvelope {
TransportEnvelope envelope;
std::uint64_t senderTelegramUserIdBinding = 0;
};
struct AdmissionMaterial {
AccountCredentialPublic credential;
QByteArray keyPackage;
};
struct Candidate {
SignedGroupTransition transition;
AppliedSignedGroupTransition applied;
TransportEnvelope transitionEnvelope;
TransportEnvelope commitEnvelope;
TransportEnvelope distributionEnvelope;
std::optional<AccountCredentialPublic> admittedCredential;
QByteArray targetKeyPackage;
};
[[nodiscard]] bool AdmissionKind(GroupTransitionKind kind) {
return kind == GroupTransitionKind::AddMember
|| kind == GroupTransitionKind::AddClient;
}
[[nodiscard]] bool JoinRelevantKind(ObjectKind kind) {
switch (kind) {
case ObjectKind::InitialGroupState:
case ObjectKind::AccountCredential:
case ObjectKind::ClientKeyPackage:
case ObjectKind::MlsCommit:
case ObjectKind::MlsWelcome:
case ObjectKind::ArchiveEpoch:
case ObjectKind::HistoryGrant:
case ObjectKind::SignedGroupTransition:
case ObjectKind::MlsGroupInfo:
return true;
default:
return false;
}
}
[[nodiscard]] bool JoinInboxKind(ObjectKind kind) {
switch (kind) {
case ObjectKind::ClientKeyPackage:
case ObjectKind::MlsCommit:
case ObjectKind::MlsWelcome:
case ObjectKind::ArchiveEpoch:
case ObjectKind::HistoryGrant:
case ObjectKind::SignedGroupTransition:
return true;
default:
return false;
}
}
[[nodiscard]] bool ValidPayload(const TransportEnvelope &envelope,
const Sha256Provider &sha256) {
return ValidateEnvelope(envelope) == EnvelopeValidationError::None
&& envelope.payloadHash == sha256.digest(envelope.payload);
}
[[nodiscard]] bool SameActor(
const ObservedEnvelope &record,
const SignedGroupTransition &transition,
std::uint64_t telegramUserIdBinding) {
return record.envelope.senderAccountId == transition.actorAccountId
&& record.envelope.senderClientId == transition.actorClientId
&& record.senderTelegramUserIdBinding == telegramUserIdBinding;
}
[[nodiscard]] const ObservedEnvelope *Find(
const std::map<ObjectId, ObservedEnvelope> &records,
ObjectId objectId) {
const auto i = records.find(objectId);
return (i == end(records)) ? nullptr : &i->second;
}
[[nodiscard]] std::optional<AdmissionMaterial> RemoteAdmissionMaterial(
const std::map<ObjectId, ObservedEnvelope> &records,
const SignedGroupTransition &transition,
std::uint64_t telegramPeerIdBinding,
std::uint64_t currentTime,
const Sha256Provider &sha256) {
if (!transition.targetClientAuthorization) {
return std::nullopt;
}
const auto &proof = *transition.targetClientAuthorization;
const auto record = Find(records, proof.authorizationId);
if (!record
|| record->senderTelegramUserIdBinding
!= transition.transition.targetTelegramUserIdBinding
|| record->envelope.senderAccountId
!= transition.transition.targetAccountId
|| record->envelope.senderClientId
!= transition.transition.targetClientId
|| record->envelope.epochOrGeneration
> transition.transition.previousGeneration) {
return std::nullopt;
}
const auto verified = VerifyClientKeyPackageEnvelope(
record->envelope,
transition.transition.conversationId,
telegramPeerIdBinding,
record->envelope.epochOrGeneration,
sha256);
return (verified.result == ClientKeyPackageEnvelopeResult::Verified
&& verified.publication
&& ClientAuthorizationUsableAt(
verified.publication->authorization,
currentTime)
&& verified.publication->authorization == proof)
? std::optional<AdmissionMaterial>({
.credential = verified.publication->accountCredential,
.keyPackage = verified.publication->keyPackage,
})
: std::nullopt;
}
[[nodiscard]] std::optional<AdmissionMaterial> LocalAdmissionMaterial(
const SignedGroupTransition &transition,
const AccountCredentialPublic &localCredential,
std::uint64_t telegramPeerIdBinding,
std::uint64_t currentTime,
const EnvelopeCodec &envelopeCodec,
const Sha256Provider &sha256,
const PersistentKeyPackagePool &keyPackages) {
if (!transition.targetClientAuthorization || !currentTime) {
return std::nullopt;
}
const auto &proof = *transition.targetClientAuthorization;
const auto entry = keyPackages.find(proof.keyPackageHash, currentTime);
const auto envelope = entry
? envelopeCodec.decode(entry->publicationEnvelope)
: std::nullopt;
const auto verified = envelope
? VerifyClientKeyPackageEnvelope(
*envelope,
transition.transition.conversationId,
telegramPeerIdBinding,
envelope->epochOrGeneration,
sha256)
: VerifyClientKeyPackageEnvelopeOutcome();
return (verified.result == ClientKeyPackageEnvelopeResult::Verified
&& verified.publication
&& ClientAuthorizationUsableAt(
verified.publication->authorization,
currentTime)
&& verified.publication->authorization == proof
&& verified.publication->accountCredential == localCredential)
? std::optional<AdmissionMaterial>({
.credential = localCredential,
.keyPackage = verified.publication->keyPackage,
})
: std::nullopt;
}
[[nodiscard]] std::optional<Candidate> VerifyCandidate(
const ObservedEnvelope &transitionRecord,
const std::map<ObjectId, ObservedEnvelope> &records,
ConversationId conversationId,
std::uint64_t telegramPeerIdBinding,
AccountId localAccountId,
ClientId localClientId,
const AccountCredentialPublic &localCredential,
std::uint64_t currentTime,
const EnvelopeCodec &envelopeCodec,
const Sha256Provider &sha256,
const PersistentGroupLedger &groupLedger,
const PersistentKeyPackagePool &keyPackages) {
const auto &envelope = transitionRecord.envelope;
const auto transition = SignedGroupTransitionCodecV1().decode(
envelope.payload);
if (!transition
|| envelope.objectKind != ObjectKind::SignedGroupTransition
|| !ValidPayload(envelope, sha256)
|| envelope.objectId != transition->transition.transitionId
|| envelope.epochOrGeneration != transition->transition.generation
|| envelope.conversationId != conversationId
|| envelope.telegramPeerIdBinding != telegramPeerIdBinding
|| envelope.authenticationData != QByteArray(
reinterpret_cast<const char*>(
transition->actorSignature.data()),
int(transition->actorSignature.size()))
|| transition->transition.previousGeneration
!= groupLedger.checkpoint().generation
|| transition->transition.generation
!= groupLedger.checkpoint().generation + 1
|| transition->previousStateHash
!= groupLedger.checkpoint().stateHash) {
return std::nullopt;
}
const auto actor = groupLedger.state()->memberByClient(
transition->actorClientId);
const auto actorCredential = groupLedger.credential(
transition->actorAccountId);
if (!actor
|| actor->accountId != transition->actorAccountId
|| !actorCredential
|| !SameActor(
transitionRecord,
*transition,
actor->telegramUserIdBinding)) {
return std::nullopt;
}
const auto commit = Find(records, transition->mlsCommitObjectId);
const auto distribution = Find(
records,
transition->archiveDistributionObjectId);
if (!commit
|| !distribution
|| commit->envelope.objectKind != ObjectKind::MlsCommit
|| distribution->envelope.objectKind != ObjectKind::ArchiveEpoch
|| !ValidPayload(commit->envelope, sha256)
|| !ValidPayload(distribution->envelope, sha256)
|| commit->envelope.payloadHash != transition->mlsCommitHash
|| distribution->envelope.payloadHash
!= transition->archiveDistributionHash
|| !SameActor(*commit, *transition, actor->telegramUserIdBinding)
|| !SameActor(
*distribution,
*transition,
actor->telegramUserIdBinding)) {
return std::nullopt;
}
const auto localTarget = transition->transition.targetAccountId
== localAccountId
&& transition->transition.targetClientId == localClientId;
const auto material = AdmissionKind(transition->transition.kind)
? (localTarget
? LocalAdmissionMaterial(
*transition,
localCredential,
telegramPeerIdBinding,
currentTime,
envelopeCodec,
sha256,
keyPackages)
: RemoteAdmissionMaterial(
records,
*transition,
telegramPeerIdBinding,
currentTime,
sha256))
: std::optional<AdmissionMaterial>();
if (AdmissionKind(transition->transition.kind) && !material) {
return std::nullopt;
}
auto applied = VerifyAndApplySignedGroupTransition({
.currentState = groupLedger.state(),
.currentCheckpoint = groupLedger.checkpoint(),
.signedTransition = &*transition,
.actorCredential = actorCredential,
.targetCredential = material ? &material->credential : nullptr,
.mlsCommitObjectId = commit->envelope.objectId,
.mlsCommit = commit->envelope.payload,
.nextArchiveKey = nullptr,
.archiveDistributionObjectId = distribution->envelope.objectId,
.archiveDistribution = distribution->envelope.payload,
.targetKeyPackage = material ? material->keyPackage : QByteArray(),
.allowMissingArchiveKey = true,
}, sha256);
if (applied.result != SignedGroupTransitionResult::Applied
|| !applied.applied) {
return std::nullopt;
}
return Candidate{
.transition = *transition,
.applied = std::move(*applied.applied),
.transitionEnvelope = envelope,
.commitEnvelope = commit->envelope,
.distributionEnvelope = distribution->envelope,
.admittedCredential = (transition->transition.kind
== GroupTransitionKind::AddMember)
? std::optional<AccountCredentialPublic>(material->credential)
: std::nullopt,
.targetKeyPackage = material ? material->keyPackage : QByteArray(),
};
}
} // namespace
bool IsPublicJoinRelevantObject(
const TelegramTransport::UntrustedObject &object,
ConversationId conversationId,
std::uint64_t telegramPeerIdBinding,
const EnvelopeCodec &envelopeCodec) {
const auto envelope = envelopeCodec.decodeUntrusted(object.bytes);
return conversationId
&& telegramPeerIdBinding
&& envelope
&& JoinRelevantKind(envelope->objectKind)
&& envelope->conversationId == conversationId
&& envelope->telegramPeerIdBinding == telegramPeerIdBinding
&& object.observedTelegramPeerIdBinding == telegramPeerIdBinding
&& object.observedMessageId > 0;
}
PublicJoinInboxStageStatus StagePublicJoinObjects(
const std::vector<TelegramTransport::UntrustedObject> &objects,
ConversationId conversationId,
std::uint64_t telegramPeerIdBinding,
const EnvelopeCodec &envelopeCodec,
const Sha256Provider &sha256,
PersistentGroupChangeInbox &inbox) {
if (!conversationId
|| !telegramPeerIdBinding
|| !inbox.loaded()) {
return PublicJoinInboxStageStatus::InvalidState;
}
for (const auto &object : objects) {
const auto envelope = envelopeCodec.decodeUntrusted(object.bytes);
if (!envelope
|| !JoinInboxKind(envelope->objectKind)
|| envelope->conversationId != conversationId
|| envelope->telegramPeerIdBinding
!= telegramPeerIdBinding
|| object.observedTelegramPeerIdBinding
!= telegramPeerIdBinding
|| !object.observedSenderTelegramUserIdBinding
|| object.observedMessageId <= 0
|| ValidateEnvelope(*envelope)
!= EnvelopeValidationError::None
|| envelope->payloadHash
!= sha256.digest(envelope->payload)) {
continue;
}
switch (inbox.stageObserved(
*envelope,
object.observedSenderTelegramUserIdBinding)) {
case GroupChangeInboxStageResult::Staged:
case GroupChangeInboxStageResult::Duplicate:
case GroupChangeInboxStageResult::InvalidEnvelope:
break;
case GroupChangeInboxStageResult::ObjectIdConflict:
return PublicJoinInboxStageStatus::ForkDetected;
case GroupChangeInboxStageResult::CapacityExceeded:
case GroupChangeInboxStageResult::NotLoaded:
case GroupChangeInboxStageResult::PersistenceFailed:
return PublicJoinInboxStageStatus::PersistenceFailure;
}
}
return PublicJoinInboxStageStatus::Staged;
}
auto ReconstructPublicJoinObjects(
ConversationId conversationId,
std::uint64_t telegramPeerIdBinding,
const EnvelopeCodec &envelopeCodec,
const PersistentGroupChangeInbox &inbox)
-> std::optional<std::vector<TelegramTransport::UntrustedObject>> {
if (!conversationId
|| !telegramPeerIdBinding
|| !inbox.loaded()) {
return std::nullopt;
}
auto result = std::vector<TelegramTransport::UntrustedObject>();
result.reserve(inbox.records().size());
auto messageId = std::int64_t(1);
for (const auto &record : inbox.records()) {
if (!JoinInboxKind(record.envelope.objectKind)) {
continue;
}
const auto encoded = envelopeCodec.encode(record.envelope);
if (!encoded
|| !record.observedSenderTelegramUserIdBinding) {
return std::nullopt;
}
result.push_back({
.bytes = encoded->bytes,
.observedTelegramPeerIdBinding = telegramPeerIdBinding,
.observedSenderTelegramUserIdBinding
= record.observedSenderTelegramUserIdBinding,
.observedMessageId = messageId++,
});
}
return result;
}
PublicJoinCatchupOutcome CatchUpPublicJoin(
const std::vector<TelegramTransport::UntrustedObject> &objects,
ConversationId conversationId,
std::uint64_t telegramPeerIdBinding,
AccountId localAccountId,
ClientId localClientId,
const AccountCredentialPublic &localCredential,
std::uint64_t currentTime,
const EnvelopeCodec &envelopeCodec,
const Sha256Provider &sha256,
PersistentGroupLedger &groupLedger,
const PersistentKeyPackagePool &keyPackages) {
if (!conversationId
|| !telegramPeerIdBinding
|| !localAccountId
|| !localClientId
|| !currentTime
|| !groupLedger.loaded()
|| !groupLedger.state()
|| !keyPackages.loaded()
|| groupLedger.state()->conversationId() != conversationId) {
return {};
}
auto records = std::map<ObjectId, ObservedEnvelope>();
for (const auto &object : objects) {
const auto envelope = envelopeCodec.decodeUntrusted(object.bytes);
if (!envelope
|| envelope->conversationId != conversationId
|| envelope->telegramPeerIdBinding != telegramPeerIdBinding
|| object.observedTelegramPeerIdBinding
!= telegramPeerIdBinding
|| !object.observedSenderTelegramUserIdBinding
|| object.observedMessageId <= 0) {
continue;
}
const auto i = records.find(envelope->objectId);
if (i != end(records)) {
if (i->second.envelope != *envelope
|| i->second.senderTelegramUserIdBinding
!= object.observedSenderTelegramUserIdBinding) {
return {
.status = PublicJoinCatchupStatus::ForkDetected,
.bundle = std::nullopt,
.appliedTransitions = 0,
};
}
continue;
}
records.emplace(envelope->objectId, ObservedEnvelope{
.envelope = *envelope,
.senderTelegramUserIdBinding =
object.observedSenderTelegramUserIdBinding,
});
}
auto appliedTransitions = std::uint64_t();
while (true) {
auto candidates = std::vector<Candidate>();
for (const auto &entry : records) {
const auto &record = entry.second;
if (record.envelope.objectKind
!= ObjectKind::SignedGroupTransition
|| record.envelope.epochOrGeneration
!= groupLedger.checkpoint().generation + 1) {
continue;
}
auto candidate = VerifyCandidate(
record,
records,
conversationId,
telegramPeerIdBinding,
localAccountId,
localClientId,
localCredential,
currentTime,
envelopeCodec,
sha256,
groupLedger,
keyPackages);
if (candidate) {
candidates.push_back(std::move(*candidate));
}
}
if (candidates.empty()) {
return {
.status = appliedTransitions
? PublicJoinCatchupStatus::UpdatedWaiting
: PublicJoinCatchupStatus::Waiting,
.bundle = std::nullopt,
.appliedTransitions = appliedTransitions,
};
} else if (candidates.size() != 1) {
return {
.status = PublicJoinCatchupStatus::ForkDetected,
.bundle = std::nullopt,
.appliedTransitions = appliedTransitions,
};
}
auto &candidate = candidates.front();
const auto localTarget = candidate.transition.transition.targetAccountId
== localAccountId
&& candidate.transition.transition.targetClientId == localClientId;
if (localTarget && AdmissionKind(candidate.transition.transition.kind)) {
auto welcomes = std::vector<TransportEnvelope>();
const auto actor = groupLedger.state()->memberByClient(
candidate.transition.actorClientId);
for (const auto &entry : records) {
const auto &record = entry.second;
if (record.envelope.objectKind == ObjectKind::MlsWelcome
&& record.envelope.authenticationData
== candidate.transitionEnvelope.payload
&& actor
&& SameActor(
record,
candidate.transition,
actor->telegramUserIdBinding)
&& ValidPayload(record.envelope, sha256)) {
welcomes.push_back(record.envelope);
}
}
if (welcomes.empty()) {
return {
.status = appliedTransitions
? PublicJoinCatchupStatus::UpdatedWaiting
: PublicJoinCatchupStatus::Waiting,
.bundle = std::nullopt,
.appliedTransitions = appliedTransitions,
};
} else if (welcomes.size() != 1) {
return {
.status = PublicJoinCatchupStatus::ForkDetected,
.bundle = std::nullopt,
.appliedTransitions = appliedTransitions,
};
}
return {
.status = PublicJoinCatchupStatus::Ready,
.bundle = VerifiedPublicJoinBundle{
.transitionEnvelope = candidate.transitionEnvelope,
.commitEnvelope = candidate.commitEnvelope,
.welcomeEnvelope = std::move(welcomes.front()),
.archiveDistributionEnvelope =
candidate.distributionEnvelope,
.targetCredential = localCredential,
.targetKeyPackage = candidate.targetKeyPackage,
},
.appliedTransitions = appliedTransitions,
};
}
const auto committed = groupLedger.commitTransition(
groupLedger.revision(),
candidate.transition,
candidate.applied,
candidate.admittedCredential
? &*candidate.admittedCredential
: nullptr);
if (committed != GroupLedgerCommitResult::Committed) {
return {
.status = PublicJoinCatchupStatus::PersistenceFailure,
.bundle = std::nullopt,
.appliedTransitions = appliedTransitions,
};
}
++appliedTransitions;
}
}
} // namespace E2ECloud