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

862 lines
26 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/group_change_inbox.h"
#include "e2e_cloud/mls/client_key_package.h"
#include <openssl/crypto.h>
#include <algorithm>
#include <array>
#include <limits>
#include <set>
#include <utility>
namespace E2ECloud {
namespace {
inline constexpr auto kMagic = std::array<std::uint8_t, 8>{
'T', 'D', 'E', '2', 'E', 'G', 'I', 'B',
};
inline constexpr auto kPurpose = "TDE2E/local-group-change-inbox/v1";
inline constexpr auto kMaximumEnvelopes = std::size_t(4096);
inline constexpr auto kMaximumEnvelopeSize = 18 * 1024 * 1024;
inline constexpr auto kMaximumSnapshotSize = 64 * 1024 * 1024;
struct Reader {
const QByteArray &bytes;
int offset = 0;
};
void AppendUint16(QByteArray &result, std::uint16_t value) {
result.append(char(value >> 8));
result.append(char(value));
}
void AppendUint32(QByteArray &result, std::uint32_t value) {
result.append(char(value >> 24));
result.append(char(value >> 16));
result.append(char(value >> 8));
result.append(char(value));
}
void AppendUint64(QByteArray &result, std::uint64_t value) {
for (auto shift = 56; shift >= 0; shift -= 8) {
result.append(char(value >> shift));
}
}
template <typename Array>
void AppendArray(QByteArray &result, const Array &value) {
result.append(
reinterpret_cast<const char*>(value.data()),
int(value.size()));
}
void AppendBytes(QByteArray &result, const QByteArray &value) {
AppendUint32(result, std::uint32_t(value.size()));
result.append(value);
}
[[nodiscard]] bool ReadUint16(Reader &reader, std::uint16_t &value) {
if (reader.bytes.size() - reader.offset < 2) {
return false;
}
const auto data = reinterpret_cast<const std::uint8_t*>(
reader.bytes.constData() + reader.offset);
value = (std::uint16_t(data[0]) << 8) | std::uint16_t(data[1]);
reader.offset += 2;
return true;
}
[[nodiscard]] bool ReadUint32(Reader &reader, std::uint32_t &value) {
if (reader.bytes.size() - reader.offset < 4) {
return false;
}
const auto data = reinterpret_cast<const std::uint8_t*>(
reader.bytes.constData() + reader.offset);
value = (std::uint32_t(data[0]) << 24)
| (std::uint32_t(data[1]) << 16)
| (std::uint32_t(data[2]) << 8)
| std::uint32_t(data[3]);
reader.offset += 4;
return true;
}
[[nodiscard]] bool ReadUint64(Reader &reader, std::uint64_t &value) {
if (reader.bytes.size() - reader.offset < 8) {
return false;
}
const auto data = reinterpret_cast<const std::uint8_t*>(
reader.bytes.constData() + reader.offset);
value = 0;
for (auto i = 0; i != 8; ++i) {
value = (value << 8) | std::uint64_t(data[i]);
}
reader.offset += 8;
return true;
}
template <typename Array>
[[nodiscard]] bool ReadArray(Reader &reader, Array &value) {
const auto size = int(value.size());
if (reader.bytes.size() - reader.offset < size) {
return false;
}
std::copy_n(
reinterpret_cast<const std::uint8_t*>(
reader.bytes.constData() + reader.offset),
size,
value.data());
reader.offset += size;
return true;
}
[[nodiscard]] bool ReadBytes(
Reader &reader,
int maximumSize,
QByteArray &value) {
auto size = std::uint32_t();
if (!ReadUint32(reader, size)
|| size > std::uint32_t(maximumSize)
|| reader.bytes.size() - reader.offset < int(size)) {
return false;
}
value = QByteArray(reader.bytes.constData() + reader.offset, int(size));
reader.offset += int(size);
return true;
}
void Cleanse(QByteArray &bytes) {
if (!bytes.isEmpty()) {
OPENSSL_cleanse(bytes.data(), bytes.size());
}
bytes.clear();
}
[[nodiscard]] QByteArray Purpose(ConversationId conversationId) {
auto result = QByteArray(kPurpose);
result.append('/');
AppendArray(result, conversationId.bytes);
return result;
}
[[nodiscard]] bool SupportedKind(ObjectKind kind) {
return kind == ObjectKind::SignedGroupTransition
|| kind == ObjectKind::MlsCommit
|| kind == ObjectKind::ArchiveEpoch
|| kind == ObjectKind::ClientKeyPackage
|| kind == ObjectKind::MlsWelcome
|| kind == ObjectKind::HistoryGrant;
}
[[nodiscard]] const StagedGroupChangeEnvelope *FindRecord(
const std::vector<StagedGroupChangeEnvelope> &records,
ObjectId objectId) {
const auto i = std::find_if(
std::begin(records),
std::end(records),
[&](const StagedGroupChangeEnvelope &record) {
return record.envelope.objectId == objectId;
});
return (i == std::end(records)) ? nullptr : &*i;
}
[[nodiscard]] bool AadMatchesEnvelope(
const MlsTransportAad &aad,
const TransportEnvelope &envelope) {
return aad.conversationId == envelope.conversationId
&& aad.objectKind == envelope.objectKind
&& aad.senderAccountId == envelope.senderAccountId
&& aad.senderClientId == envelope.senderClientId
&& aad.telegramPeerIdBinding == envelope.telegramPeerIdBinding
&& aad.objectId == envelope.objectId;
}
} // namespace
PersistentGroupChangeInbox::PersistentGroupChangeInbox(
AtomicBlobStore &blobStore,
const LocalRecordProtector &protector,
const EnvelopeCodec &envelopeCodec,
const Sha256Provider &sha256)
: _blobStore(blobStore)
, _protector(protector)
, _envelopeCodec(envelopeCodec)
, _sha256(sha256) {
}
GroupChangeInboxLoadResult PersistentGroupChangeInbox::load(
ConversationId conversationId) {
_conversationId = {};
_records.clear();
_revision = 0;
_loaded = false;
if (!conversationId) {
return GroupChangeInboxLoadResult::InvalidSnapshot;
}
const auto stored = _blobStore.read();
if (stored.status == BlobReadStatus::Error) {
return GroupChangeInboxLoadResult::StorageError;
} else if (stored.status == BlobReadStatus::Missing) {
_conversationId = conversationId;
_loaded = true;
return GroupChangeInboxLoadResult::Missing;
}
auto opened = _protector.open(Purpose(conversationId), stored.bytes);
if (!opened) {
return GroupChangeInboxLoadResult::AuthenticationFailed;
}
auto reader = Reader{ *opened };
auto magic = std::array<std::uint8_t, 8>();
auto version = std::uint16_t();
auto storedConversationId = ConversationId();
auto revision = std::uint64_t();
auto count = std::uint32_t();
if (!ReadArray(reader, magic)
|| !ReadUint16(reader, version)
|| !ReadArray(reader, storedConversationId.bytes)
|| !ReadUint64(reader, revision)
|| !ReadUint32(reader, count)
|| magic != kMagic
|| (version != 1 && version != 2)
|| storedConversationId != conversationId
|| !revision
|| count > kMaximumEnvelopes) {
Cleanse(*opened);
return GroupChangeInboxLoadResult::InvalidSnapshot;
}
_conversationId = conversationId;
auto records = std::vector<StagedGroupChangeEnvelope>();
records.reserve(count);
auto objectIds = std::set<ObjectId>();
for (auto index = std::uint32_t(); index != count; ++index) {
auto objectId = ObjectId();
auto payloadHash = Digest();
auto observedSenderTelegramUserIdBinding = std::uint64_t();
auto encodedBytes = QByteArray();
if (!ReadArray(reader, objectId.bytes)
|| !ReadArray(reader, payloadHash.bytes)
|| (version == 2
&& !ReadUint64(
reader,
observedSenderTelegramUserIdBinding))
|| !ReadBytes(reader, kMaximumEnvelopeSize, encodedBytes)) {
Cleanse(*opened);
return GroupChangeInboxLoadResult::InvalidSnapshot;
}
const auto envelope = _envelopeCodec.decode({
.conversationId = conversationId,
.objectId = objectId,
.bytes = std::move(encodedBytes),
});
if (!envelope
|| envelope->payloadHash != payloadHash
|| !validEnvelope(*envelope)
|| !objectIds.emplace(objectId).second) {
Cleanse(*opened);
return GroupChangeInboxLoadResult::InvalidSnapshot;
}
records.push_back({
.envelope = *envelope,
.observedSenderTelegramUserIdBinding
= observedSenderTelegramUserIdBinding,
});
}
const auto complete = reader.offset == reader.bytes.size();
Cleanse(*opened);
if (!complete) {
return GroupChangeInboxLoadResult::InvalidSnapshot;
}
_records = std::move(records);
_revision = revision;
_loaded = true;
return GroupChangeInboxLoadResult::Loaded;
}
GroupChangeInboxStageResult PersistentGroupChangeInbox::stage(
const TransportEnvelope &envelope) {
return stageRecord({ .envelope = envelope });
}
GroupChangeInboxStageResult PersistentGroupChangeInbox::stageObserved(
const TransportEnvelope &envelope,
std::uint64_t observedSenderTelegramUserIdBinding) {
if (!observedSenderTelegramUserIdBinding) {
return GroupChangeInboxStageResult::InvalidEnvelope;
}
return stageRecord({
.envelope = envelope,
.observedSenderTelegramUserIdBinding
= observedSenderTelegramUserIdBinding,
});
}
GroupChangeInboxStageResult PersistentGroupChangeInbox::stageRecord(
StagedGroupChangeEnvelope record) {
if (!_loaded) {
return GroupChangeInboxStageResult::NotLoaded;
} else if (!validEnvelope(record.envelope)) {
return GroupChangeInboxStageResult::InvalidEnvelope;
}
const auto current = FindRecord(_records, record.envelope.objectId);
if (current) {
return (*current == record)
? GroupChangeInboxStageResult::Duplicate
: GroupChangeInboxStageResult::ObjectIdConflict;
}
const auto existing = lookup(
record.envelope.objectId,
record.envelope.payloadHash);
if (existing == GroupChangeInboxLookup::Present) {
return GroupChangeInboxStageResult::Duplicate;
} else if (existing == GroupChangeInboxLookup::ObjectIdConflict) {
return GroupChangeInboxStageResult::ObjectIdConflict;
} else if (existing == GroupChangeInboxLookup::Unavailable) {
return GroupChangeInboxStageResult::NotLoaded;
} else if (_records.size() == kMaximumEnvelopes
|| _revision == std::numeric_limits<std::uint64_t>::max()) {
return GroupChangeInboxStageResult::CapacityExceeded;
}
auto records = _records;
records.push_back(std::move(record));
const auto revision = _revision + 1;
if (!persist(records, revision)) {
return GroupChangeInboxStageResult::PersistenceFailed;
}
_records = std::move(records);
_revision = revision;
return GroupChangeInboxStageResult::Staged;
}
GroupChangeInboxLookup PersistentGroupChangeInbox::lookup(
ObjectId objectId,
Digest payloadHash) const {
if (!_loaded || !objectId || !payloadHash) {
return GroupChangeInboxLookup::Unavailable;
}
const auto record = FindRecord(_records, objectId);
if (!record) {
return GroupChangeInboxLookup::Missing;
}
return (record->envelope.payloadHash == payloadHash)
? GroupChangeInboxLookup::Present
: GroupChangeInboxLookup::ObjectIdConflict;
}
GroupChangeInboxReadyResult PersistentGroupChangeInbox::ready(
std::uint64_t previousGeneration) const {
if (!_loaded || !previousGeneration) {
return {
.status = GroupChangeInboxReadyStatus::Unavailable,
.bundle = std::nullopt,
};
}
auto candidates = std::vector<
std::pair<const TransportEnvelope*, SignedGroupTransition>>();
for (const auto &record : _records) {
const auto &envelope = record.envelope;
if (envelope.objectKind != ObjectKind::SignedGroupTransition) {
continue;
}
const auto transition = SignedGroupTransitionCodecV1().decode(
envelope.payload);
if (transition
&& transition->transition.previousGeneration
== previousGeneration
&& transition->transition.generation
== previousGeneration + 1) {
candidates.emplace_back(&envelope, *transition);
}
}
if (candidates.empty()) {
return {
.status = GroupChangeInboxReadyStatus::Incomplete,
.bundle = std::nullopt,
};
} else if (candidates.size() != 1) {
return {
.status = GroupChangeInboxReadyStatus::ForkDetected,
.bundle = std::nullopt,
};
}
const auto &[transitionEnvelope, signedTransition] = candidates.front();
const auto commitRecord = FindRecord(
_records,
signedTransition.mlsCommitObjectId);
const auto distributionRecord = FindRecord(
_records,
signedTransition.archiveDistributionObjectId);
if (!commitRecord || !distributionRecord) {
return {
.status = GroupChangeInboxReadyStatus::Incomplete,
.bundle = std::nullopt,
};
}
const auto &commitEnvelope = commitRecord->envelope;
const auto &distributionEnvelope = distributionRecord->envelope;
if (transitionEnvelope->objectId
!= signedTransition.transition.transitionId
|| transitionEnvelope->payloadHash
!= _sha256.digest(transitionEnvelope->payload)
|| commitEnvelope.objectKind != ObjectKind::MlsCommit
|| commitEnvelope.payloadHash != signedTransition.mlsCommitHash
|| distributionEnvelope.objectKind != ObjectKind::ArchiveEpoch
|| distributionEnvelope.payloadHash
!= signedTransition.archiveDistributionHash) {
return {
.status = GroupChangeInboxReadyStatus::ForkDetected,
.bundle = std::nullopt,
};
}
return {
.status = GroupChangeInboxReadyStatus::Ready,
.bundle = GroupChangeInboxBundle{
.signedTransition = signedTransition,
.transitionEnvelope = *transitionEnvelope,
.commitEnvelope = commitEnvelope,
.archiveDistributionEnvelope = distributionEnvelope,
},
};
}
std::vector<ForkRecoveryCandidate> PersistentGroupChangeInbox::candidates(
std::uint64_t previousGeneration) const {
if (!_loaded || !previousGeneration) {
return {};
}
auto result = std::vector<ForkRecoveryCandidate>();
for (const auto &record : _records) {
const auto &envelope = record.envelope;
if (envelope.objectKind != ObjectKind::SignedGroupTransition) {
continue;
}
const auto transition = SignedGroupTransitionCodecV1().decode(
envelope.payload);
if (transition
&& transition->transition.transitionId == envelope.objectId
&& transition->transition.previousGeneration
== previousGeneration
&& transition->transition.generation
== previousGeneration + 1
&& envelope.payloadHash == _sha256.digest(envelope.payload)) {
result.push_back({
.transitionId = envelope.objectId,
.transitionPayloadHash = envelope.payloadHash,
});
}
}
std::sort(
begin(result),
end(result),
[](const auto &a, const auto &b) {
return (a.transitionId != b.transitionId)
? a.transitionId < b.transitionId
: a.transitionPayloadHash < b.transitionPayloadHash;
});
return result;
}
bool PersistentGroupChangeInbox::discardBundle(ObjectId transitionId) {
if (!_loaded || !transitionId) {
return false;
}
const auto transitionRecord = FindRecord(_records, transitionId);
if (!transitionRecord) {
return true;
}
const auto transition = SignedGroupTransitionCodecV1().decode(
transitionRecord->envelope.payload);
if (!transition
|| transition->transition.transitionId != transitionId
|| _revision == std::numeric_limits<std::uint64_t>::max()) {
return false;
}
auto objectIds = std::set<ObjectId>{
transitionId,
transition->mlsCommitObjectId,
transition->archiveDistributionObjectId,
};
if (transition->targetClientAuthorization) {
objectIds.emplace(
transition->targetClientAuthorization->authorizationId);
}
const auto transitionPayload = transitionRecord->envelope.payload;
auto records = _records;
records.erase(
std::remove_if(
std::begin(records),
std::end(records),
[&](const StagedGroupChangeEnvelope &record) {
return objectIds.contains(record.envelope.objectId)
|| (record.envelope.objectKind
== ObjectKind::MlsWelcome
&& record.envelope.authenticationData
== transitionPayload);
}),
std::end(records));
const auto revision = _revision + 1;
if (!persist(records, revision)) {
return false;
}
_records = std::move(records);
_revision = revision;
return true;
}
bool PersistentGroupChangeInbox::discardAppliedTransitions(
std::uint64_t generation) {
if (!_loaded || !generation) {
return false;
}
auto objectIds = std::set<ObjectId>();
auto transitionPayloads = std::vector<QByteArray>();
for (const auto &record : _records) {
if (record.envelope.objectKind
!= ObjectKind::SignedGroupTransition) {
continue;
}
const auto transition = SignedGroupTransitionCodecV1().decode(
record.envelope.payload);
if (!transition
|| transition->transition.generation > generation) {
continue;
}
objectIds.emplace(record.envelope.objectId);
objectIds.emplace(transition->mlsCommitObjectId);
objectIds.emplace(transition->archiveDistributionObjectId);
if (transition->targetClientAuthorization) {
objectIds.emplace(
transition->targetClientAuthorization->authorizationId);
}
transitionPayloads.push_back(record.envelope.payload);
}
if (objectIds.empty()) {
return true;
}
auto records = _records;
records.erase(
std::remove_if(
std::begin(records),
std::end(records),
[&](const StagedGroupChangeEnvelope &record) {
return objectIds.contains(record.envelope.objectId)
|| (record.envelope.objectKind
== ObjectKind::MlsWelcome
&& std::find(
std::begin(transitionPayloads),
std::end(transitionPayloads),
record.envelope.authenticationData)
!= std::end(transitionPayloads));
}),
std::end(records));
if (_revision == std::numeric_limits<std::uint64_t>::max()) {
return false;
}
const auto revision = _revision + 1;
if (!persist(records, revision)) {
return false;
}
_records = std::move(records);
_revision = revision;
return true;
}
bool PersistentGroupChangeInbox::discardExpiredKeyPackages(
std::uint64_t currentTime) {
if (!_loaded || !currentTime) {
return false;
}
auto referenced = std::set<ObjectId>();
for (const auto &record : _records) {
if (record.envelope.objectKind
!= ObjectKind::SignedGroupTransition) {
continue;
}
const auto transition = SignedGroupTransitionCodecV1().decode(
record.envelope.payload);
if (transition && transition->targetClientAuthorization) {
referenced.emplace(
transition->targetClientAuthorization->authorizationId);
}
}
auto records = _records;
records.erase(
std::remove_if(
std::begin(records),
std::end(records),
[&](const StagedGroupChangeEnvelope &record) {
if (record.envelope.objectKind
!= ObjectKind::ClientKeyPackage
|| referenced.contains(record.envelope.objectId)) {
return false;
}
const auto publication
= ClientKeyPackagePublicationCodecV1().decode(
record.envelope.payload);
return !publication
|| publication->authorization.expiresAt <= currentTime;
}),
std::end(records));
if (records.size() == _records.size()) {
return true;
} else if (_revision == std::numeric_limits<std::uint64_t>::max()) {
return false;
}
const auto revision = _revision + 1;
if (!persist(records, revision)) {
return false;
}
_records = std::move(records);
_revision = revision;
return true;
}
bool PersistentGroupChangeInbox::discardJoinOnlyObjects() {
if (!_loaded) {
return false;
}
auto records = _records;
records.erase(
std::remove_if(
std::begin(records),
std::end(records),
[](const StagedGroupChangeEnvelope &record) {
return record.envelope.objectKind == ObjectKind::MlsWelcome
|| record.envelope.objectKind
== ObjectKind::HistoryGrant;
}),
std::end(records));
if (records.size() == _records.size()) {
return true;
} else if (_revision == std::numeric_limits<std::uint64_t>::max()) {
return false;
}
const auto revision = _revision + 1;
if (!persist(records, revision)) {
return false;
}
_records = std::move(records);
_revision = revision;
return true;
}
bool PersistentGroupChangeInbox::loaded() const {
return _loaded;
}
std::size_t PersistentGroupChangeInbox::size() const {
return _records.size();
}
std::uint64_t PersistentGroupChangeInbox::revision() const {
return _revision;
}
auto PersistentGroupChangeInbox::records() const
-> const std::vector<StagedGroupChangeEnvelope> & {
return _records;
}
bool PersistentGroupChangeInbox::persist(
const std::vector<StagedGroupChangeEnvelope> &records,
std::uint64_t revision) const {
if (!_conversationId
|| !revision
|| records.size() > kMaximumEnvelopes) {
return false;
}
auto plaintext = QByteArray();
AppendArray(plaintext, kMagic);
AppendUint16(plaintext, 2);
AppendArray(plaintext, _conversationId.bytes);
AppendUint64(plaintext, revision);
AppendUint32(plaintext, std::uint32_t(records.size()));
auto objectIds = std::set<ObjectId>();
for (const auto &record : records) {
const auto &envelope = record.envelope;
const auto encoded = _envelopeCodec.encode(envelope);
if (!validEnvelope(envelope)
|| !objectIds.emplace(envelope.objectId).second
|| !encoded
|| encoded->bytes.size() > kMaximumEnvelopeSize) {
Cleanse(plaintext);
return false;
}
AppendArray(plaintext, envelope.objectId.bytes);
AppendArray(plaintext, envelope.payloadHash.bytes);
AppendUint64(
plaintext,
record.observedSenderTelegramUserIdBinding);
AppendBytes(plaintext, encoded->bytes);
if (plaintext.size() > kMaximumSnapshotSize) {
Cleanse(plaintext);
return false;
}
}
const auto protectedBytes = _protector.seal(
Purpose(_conversationId),
plaintext);
Cleanse(plaintext);
return protectedBytes && _blobStore.writeAtomic(*protectedBytes);
}
bool PersistentGroupChangeInbox::validEnvelope(
const TransportEnvelope &envelope) const {
return envelope.conversationId == _conversationId
&& SupportedKind(envelope.objectKind)
&& ValidateEnvelope(envelope) == EnvelopeValidationError::None
&& envelope.payloadHash == _sha256.digest(envelope.payload)
&& envelope.payload.size() <= kMaximumEnvelopeSize;
}
GroupChangeInboxApplier::GroupChangeInboxApplier(
PersistentGroupChangeInbox &inbox,
const PersistentForkRecoveryLedger *forkLedger)
: _inbox(inbox)
, _forkLedger(forkLedger) {
}
InboundApplyResult GroupChangeInboxApplier::apply(
const TransportEnvelope &envelope) {
if (_forkLedger
&& envelope.objectKind == ObjectKind::SignedGroupTransition) {
const auto transition = SignedGroupTransitionCodecV1().decode(
envelope.payload);
if (!transition) {
return InboundApplyResult::Rejected;
}
switch (_forkLedger->observe(
transition->transition.previousGeneration,
{
.transitionId = envelope.objectId,
.transitionPayloadHash = envelope.payloadHash,
})) {
case ForkRecoveryCandidateObservation::Known:
case ForkRecoveryCandidateObservation::Unresolved:
break;
case ForkRecoveryCandidateObservation::HiddenCandidate:
case ForkRecoveryCandidateObservation::ObjectIdConflict:
return InboundApplyResult::ForkDetected;
case ForkRecoveryCandidateObservation::Unavailable:
return InboundApplyResult::Deferred;
}
}
switch (_inbox.stage(envelope)) {
case GroupChangeInboxStageResult::Staged:
case GroupChangeInboxStageResult::Duplicate:
return InboundApplyResult::Applied;
case GroupChangeInboxStageResult::ObjectIdConflict:
return InboundApplyResult::ForkDetected;
case GroupChangeInboxStageResult::InvalidEnvelope:
return InboundApplyResult::Rejected;
case GroupChangeInboxStageResult::CapacityExceeded:
case GroupChangeInboxStageResult::NotLoaded:
case GroupChangeInboxStageResult::PersistenceFailed:
return InboundApplyResult::Deferred;
}
return InboundApplyResult::Rejected;
}
InboundRecoveryResult GroupChangeInboxApplier::recover(
const TransportEnvelope &envelope) const {
switch (_inbox.lookup(envelope.objectId, envelope.payloadHash)) {
case GroupChangeInboxLookup::Present:
return InboundRecoveryResult::Applied;
case GroupChangeInboxLookup::Missing:
return InboundRecoveryResult::NotApplied;
case GroupChangeInboxLookup::ObjectIdConflict:
case GroupChangeInboxLookup::Unavailable:
return InboundRecoveryResult::Unknown;
}
return InboundRecoveryResult::Unknown;
}
OpenMlsGroupChangeEnvelopeAuthenticator::
OpenMlsGroupChangeEnvelopeAuthenticator(
const MlsContextCodecV1 &contextCodec,
const GroupControlCodecV1 &controlCodec,
const PersistentGroupLedger &groupLedger,
const Sha256Provider &sha256)
: _contextCodec(contextCodec)
, _controlCodec(controlCodec)
, _groupLedger(groupLedger)
, _sha256(sha256) {
}
bool OpenMlsGroupChangeEnvelopeAuthenticator::authenticate(
const TransportEnvelope &envelope) const {
if (!_groupLedger.loaded()
|| !_groupLedger.state()
|| envelope.conversationId
!= _groupLedger.state()->conversationId()
|| envelope.payloadHash != _sha256.digest(envelope.payload)) {
return false;
}
if (envelope.objectKind == ObjectKind::SignedGroupTransition) {
const auto transition = SignedGroupTransitionCodecV1().decode(
envelope.payload);
if (!transition
|| transition->transition.transitionId != envelope.objectId
|| transition->transition.conversationId
!= envelope.conversationId
|| transition->transition.generation
!= envelope.epochOrGeneration
|| transition->actorAccountId != envelope.senderAccountId
|| transition->actorClientId != envelope.senderClientId
|| envelope.authenticationData.size()
!= int(transition->actorSignature.size())
|| !std::equal(
std::begin(transition->actorSignature),
std::end(transition->actorSignature),
reinterpret_cast<const std::uint8_t*>(
envelope.authenticationData.constData()))
|| !_groupLedger.wasClientActiveAt(
transition->actorAccountId,
transition->actorClientId,
transition->transition.previousGeneration)) {
return false;
}
const auto credential = _groupLedger.credential(
transition->actorAccountId);
const auto signedBody = QByteArray(
envelope.payload.constData(),
envelope.payload.size() - int(transition->actorSignature.size()));
return credential && VerifyAccountSignature(
*credential,
AccountSignatureDomain::GroupTransition,
signedBody,
transition->actorSignature);
}
if (envelope.objectKind != ObjectKind::MlsCommit
&& envelope.objectKind != ObjectKind::ArchiveEpoch) {
return false;
}
const auto aad = _contextCodec.decodeAad(envelope.authenticationData);
if (!aad || !AadMatchesEnvelope(*aad, envelope)) {
return false;
}
const auto preludeBytes = (envelope.objectKind == ObjectKind::MlsCommit)
? aad->context
: (aad->context.size() == kGroupChangePreludeEncodedSize + 32)
? QByteArray(
aad->context.constData(),
kGroupChangePreludeEncodedSize)
: QByteArray();
const auto prelude = _controlCodec.decodePrelude(preludeBytes);
return prelude
&& prelude->transition.conversationId == envelope.conversationId
&& prelude->actorAccountId == envelope.senderAccountId
&& prelude->actorClientId == envelope.senderClientId
&& _groupLedger.wasClientActiveAt(
prelude->actorAccountId,
prelude->actorClientId,
prelude->transition.previousGeneration);
}
} // namespace E2ECloud