774 lines
24 KiB
C++
774 lines
24 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_bootstrap_transaction.h"
|
|
|
|
#include "e2e_cloud/mls/openmls_application_engine.h"
|
|
|
|
#include <openssl/crypto.h>
|
|
|
|
#include <QtCore/QScopeGuard>
|
|
|
|
#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', 'B', 'T',
|
|
};
|
|
inline constexpr auto kMaximumMlsStateSize = 64 * 1024 * 1024;
|
|
inline constexpr auto kMaximumPublicObjectSize = 16 * 1024 * 1024;
|
|
inline constexpr auto kMaximumEnvelopeSize = 18 * 1024 * 1024;
|
|
inline constexpr auto kMaximumSnapshotSize = 128 * 1024 * 1024;
|
|
|
|
struct Reader {
|
|
const QByteArray &bytes;
|
|
int offset = 0;
|
|
};
|
|
|
|
void AppendUint8(QByteArray &result, std::uint8_t value) {
|
|
result.append(char(value));
|
|
}
|
|
|
|
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 ReadUint8(Reader &reader, std::uint8_t &value) {
|
|
if (reader.offset == reader.bytes.size()) {
|
|
return false;
|
|
}
|
|
value = std::uint8_t(reader.bytes[reader.offset++]);
|
|
return true;
|
|
}
|
|
|
|
[[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;
|
|
}
|
|
value = 0;
|
|
for (auto i = 0; i != 8; ++i) {
|
|
value = (value << 8)
|
|
| std::uint8_t(reader.bytes[reader.offset + 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;
|
|
}
|
|
|
|
[[nodiscard]] QByteArray Purpose(ConversationId conversationId) {
|
|
auto result = QByteArray("TDE2E/group-bootstrap-transaction/v1");
|
|
result.append(char(0));
|
|
AppendArray(result, conversationId.bytes);
|
|
return result;
|
|
}
|
|
|
|
void Cleanse(QByteArray &bytes) {
|
|
if (!bytes.isEmpty()) {
|
|
OPENSSL_cleanse(bytes.data(), bytes.size());
|
|
}
|
|
bytes.clear();
|
|
}
|
|
|
|
template <typename Value>
|
|
void Cleanse(Value &value) {
|
|
OPENSSL_cleanse(value.data(), value.size());
|
|
}
|
|
|
|
void Cleanse(GroupBootstrapTransaction &transaction) {
|
|
Cleanse(transaction.mlsEngineState);
|
|
transaction.archiveEpoch.key = ArchiveKey32();
|
|
}
|
|
|
|
[[nodiscard]] bool SameArchiveEpoch(
|
|
const ArchiveEpochSecret &a,
|
|
const ArchiveEpochSecret &b) {
|
|
return a.generation == b.generation
|
|
&& a.activationGroupGeneration == b.activationGroupGeneration
|
|
&& a.activationEventId == b.activationEventId
|
|
&& a.key.valid()
|
|
&& b.key.valid()
|
|
&& a.key.bytes() == b.key.bytes();
|
|
}
|
|
|
|
[[nodiscard]] bool BasicTransactionStructure(
|
|
const GroupBootstrapTransaction &transaction) {
|
|
if (!transaction.conversationId
|
|
|| transaction.genesis.conversationId != transaction.conversationId
|
|
|| transaction.genesis.genesisObjectId == ObjectId()
|
|
|| transaction.initialMlsPublicObject.isEmpty()
|
|
|| transaction.initialMlsPublicObject.size()
|
|
> kMaximumPublicObjectSize
|
|
|| transaction.archiveEpoch.generation != 1
|
|
|| transaction.archiveEpoch.activationGroupGeneration != 1
|
|
|| !transaction.archiveEpoch.activationEventId
|
|
|| !transaction.archiveEpoch.key.valid()
|
|
|| transaction.mlsEngineState.isEmpty()
|
|
|| transaction.mlsEngineState.size() > kMaximumMlsStateSize
|
|
|| transaction.outboxEnvelopes.size() != 4) {
|
|
return false;
|
|
}
|
|
auto objectIds = std::set<ObjectId>();
|
|
for (const auto &envelope : transaction.outboxEnvelopes) {
|
|
if (envelope.conversationId != transaction.conversationId
|
|
|| !envelope.objectId
|
|
|| envelope.objectId
|
|
== transaction.archiveEpoch.activationEventId
|
|
|| envelope.bytes.isEmpty()
|
|
|| envelope.bytes.size() > kMaximumEnvelopeSize
|
|
|| !objectIds.emplace(envelope.objectId).second) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
[[nodiscard]] QByteArray EncodeSnapshot(
|
|
ConversationId conversationId,
|
|
std::uint64_t revision,
|
|
const GroupBootstrapTransaction *transaction) {
|
|
auto result = QByteArray();
|
|
AppendArray(result, kMagic);
|
|
AppendUint16(result, 1);
|
|
AppendArray(result, conversationId.bytes);
|
|
AppendUint64(result, revision);
|
|
AppendUint8(result, transaction ? 1 : 0);
|
|
if (!transaction) {
|
|
return result;
|
|
}
|
|
const auto genesis = SignedGroupGenesisCodecV1().encode(
|
|
transaction->genesis);
|
|
const auto credential = AccountCredentialCodecV1().encode(
|
|
transaction->ownerCredential);
|
|
if (!genesis || !credential) {
|
|
return {};
|
|
}
|
|
AppendUint64(result, transaction->outboxBaseRevision);
|
|
AppendBytes(result, *genesis);
|
|
AppendBytes(result, *credential);
|
|
AppendBytes(result, transaction->initialMlsPublicObject);
|
|
AppendUint64(result, transaction->archiveEpoch.generation);
|
|
AppendUint64(
|
|
result,
|
|
transaction->archiveEpoch.activationGroupGeneration);
|
|
AppendArray(result, transaction->archiveEpoch.activationEventId.bytes);
|
|
AppendArray(result, transaction->archiveEpoch.key.bytes());
|
|
AppendBytes(result, transaction->mlsEngineState);
|
|
AppendUint16(result, std::uint16_t(transaction->outboxEnvelopes.size()));
|
|
for (const auto &envelope : transaction->outboxEnvelopes) {
|
|
AppendArray(result, envelope.conversationId.bytes);
|
|
AppendArray(result, envelope.objectId.bytes);
|
|
AppendBytes(result, envelope.bytes);
|
|
}
|
|
return (result.size() <= kMaximumSnapshotSize) ? result : QByteArray();
|
|
}
|
|
|
|
[[nodiscard]] bool SameTransaction(
|
|
const GroupBootstrapTransaction &a,
|
|
const GroupBootstrapTransaction &b) {
|
|
auto encodedA = EncodeSnapshot(a.conversationId, 1, &a);
|
|
auto encodedB = EncodeSnapshot(b.conversationId, 1, &b);
|
|
const auto same = !encodedA.isEmpty()
|
|
&& encodedA.size() == encodedB.size()
|
|
&& CRYPTO_memcmp(
|
|
encodedA.constData(),
|
|
encodedB.constData(),
|
|
encodedA.size()) == 0;
|
|
Cleanse(encodedA);
|
|
Cleanse(encodedB);
|
|
return same;
|
|
}
|
|
|
|
struct DecodedSnapshot {
|
|
std::uint64_t revision = 0;
|
|
std::optional<GroupBootstrapTransaction> transaction;
|
|
};
|
|
|
|
[[nodiscard]] std::optional<DecodedSnapshot> DecodeSnapshot(
|
|
const QByteArray &bytes,
|
|
ConversationId expectedConversationId) {
|
|
if (bytes.isEmpty() || bytes.size() > kMaximumSnapshotSize) {
|
|
return std::nullopt;
|
|
}
|
|
auto reader = Reader{ bytes };
|
|
auto magic = std::array<std::uint8_t, 8>();
|
|
auto version = std::uint16_t();
|
|
auto conversationId = ConversationId();
|
|
auto revision = std::uint64_t();
|
|
auto present = std::uint8_t();
|
|
if (!ReadArray(reader, magic)
|
|
|| !ReadUint16(reader, version)
|
|
|| !ReadArray(reader, conversationId.bytes)
|
|
|| !ReadUint64(reader, revision)
|
|
|| !ReadUint8(reader, present)
|
|
|| magic != kMagic
|
|
|| version != 1
|
|
|| conversationId != expectedConversationId
|
|
|| !revision
|
|
|| present > 1) {
|
|
return std::nullopt;
|
|
} else if (!present) {
|
|
return (reader.offset == bytes.size())
|
|
? std::optional<DecodedSnapshot>({
|
|
.revision = revision,
|
|
.transaction = std::nullopt,
|
|
})
|
|
: std::nullopt;
|
|
}
|
|
auto transaction = GroupBootstrapTransaction();
|
|
transaction.conversationId = conversationId;
|
|
auto genesisBytes = QByteArray();
|
|
auto credentialBytes = QByteArray();
|
|
auto archiveKey = std::array<std::uint8_t, kArchiveKeySize>();
|
|
auto envelopeCount = std::uint16_t();
|
|
if (!ReadUint64(reader, transaction.outboxBaseRevision)
|
|
|| !ReadBytes(
|
|
reader,
|
|
kSignedGroupGenesisEncodedSize,
|
|
genesisBytes)
|
|
|| !ReadBytes(
|
|
reader,
|
|
kAccountCredentialEncodedSize,
|
|
credentialBytes)
|
|
|| !ReadBytes(
|
|
reader,
|
|
kMaximumPublicObjectSize,
|
|
transaction.initialMlsPublicObject)
|
|
|| !ReadUint64(reader, transaction.archiveEpoch.generation)
|
|
|| !ReadUint64(
|
|
reader,
|
|
transaction.archiveEpoch.activationGroupGeneration)
|
|
|| !ReadArray(
|
|
reader,
|
|
transaction.archiveEpoch.activationEventId.bytes)
|
|
|| !ReadArray(reader, archiveKey)
|
|
|| !ReadBytes(
|
|
reader,
|
|
kMaximumMlsStateSize,
|
|
transaction.mlsEngineState)
|
|
|| !ReadUint16(reader, envelopeCount)
|
|
|| envelopeCount != 4) {
|
|
Cleanse(transaction.mlsEngineState);
|
|
Cleanse(archiveKey);
|
|
return std::nullopt;
|
|
}
|
|
transaction.genesis = SignedGroupGenesisCodecV1().decode(
|
|
genesisBytes).value_or(SignedGroupGenesis());
|
|
transaction.ownerCredential = AccountCredentialCodecV1().decode(
|
|
credentialBytes).value_or(AccountCredentialPublic());
|
|
transaction.archiveEpoch.key = ArchiveKey32(std::move(archiveKey));
|
|
for (auto i = std::uint16_t(0); i != envelopeCount; ++i) {
|
|
auto envelope = EncodedEnvelope();
|
|
if (!ReadArray(reader, envelope.conversationId.bytes)
|
|
|| !ReadArray(reader, envelope.objectId.bytes)
|
|
|| !ReadBytes(reader, kMaximumEnvelopeSize, envelope.bytes)) {
|
|
Cleanse(transaction.mlsEngineState);
|
|
return std::nullopt;
|
|
}
|
|
transaction.outboxEnvelopes.push_back(std::move(envelope));
|
|
}
|
|
if (reader.offset != bytes.size()
|
|
|| !BasicTransactionStructure(transaction)) {
|
|
Cleanse(transaction.mlsEngineState);
|
|
return std::nullopt;
|
|
}
|
|
return DecodedSnapshot{
|
|
.revision = revision,
|
|
.transaction = std::move(transaction),
|
|
};
|
|
}
|
|
|
|
[[nodiscard]] bool ValidBootstrapBundle(
|
|
const GroupBootstrapTransaction &transaction,
|
|
const EnvelopeCodecV1 &envelopeCodec,
|
|
const Sha256Provider &sha256,
|
|
VerifySignedGroupGenesisOutcome &verified) {
|
|
if (!BasicTransactionStructure(transaction)) {
|
|
return false;
|
|
}
|
|
verified = VerifySignedGroupGenesis({
|
|
.genesis = &transaction.genesis,
|
|
.ownerCredential = &transaction.ownerCredential,
|
|
.genesisObjectId = transaction.genesis.genesisObjectId,
|
|
.initialMlsPublicObjectId =
|
|
transaction.genesis.initialMlsPublicObjectId,
|
|
.initialMlsPublicObject = transaction.initialMlsPublicObject,
|
|
.initialArchiveKey = &transaction.archiveEpoch.key,
|
|
}, sha256);
|
|
if (!verified.verified
|
|
|| transaction.archiveEpoch.activationEventId
|
|
!= transaction.genesis.archiveActivationEventId) {
|
|
return false;
|
|
}
|
|
auto kinds = std::set<ObjectKind>();
|
|
for (const auto &encoded : transaction.outboxEnvelopes) {
|
|
const auto envelope = envelopeCodec.decode(encoded);
|
|
if (!envelope
|
|
|| envelope->senderAccountId
|
|
!= transaction.genesis.ownerAccountId
|
|
|| envelope->senderClientId
|
|
!= transaction.genesis.ownerClientId
|
|
|| envelope->telegramPeerIdBinding
|
|
!= transaction.genesis.telegramPeerIdBinding
|
|
|| envelope->payloadHash != sha256.digest(envelope->payload)
|
|
|| !kinds.emplace(envelope->objectKind).second) {
|
|
return false;
|
|
}
|
|
switch (envelope->objectKind) {
|
|
case ObjectKind::AccountCredential:
|
|
if (envelope->payload != *AccountCredentialCodecV1().encode(
|
|
transaction.ownerCredential)) {
|
|
return false;
|
|
}
|
|
break;
|
|
case ObjectKind::MlsGroupInfo:
|
|
if (envelope->objectId
|
|
!= transaction.genesis.initialMlsPublicObjectId
|
|
|| envelope->payload != transaction.initialMlsPublicObject) {
|
|
return false;
|
|
}
|
|
break;
|
|
case ObjectKind::InitialGroupState:
|
|
if (envelope->objectId != transaction.genesis.genesisObjectId
|
|
|| envelope->payload
|
|
!= *SignedGroupGenesisCodecV1().encode(
|
|
transaction.genesis)) {
|
|
return false;
|
|
}
|
|
break;
|
|
case ObjectKind::HistoryGrant: {
|
|
const auto grant = EncryptedHistoryGrantCodecV1().decode(
|
|
envelope->payload);
|
|
if (!grant
|
|
|| grant->conversationId != transaction.conversationId
|
|
|| grant->grantId != envelope->objectId
|
|
|| grant->issuerAccountId
|
|
!= transaction.genesis.ownerAccountId
|
|
|| grant->issuerClientId
|
|
!= transaction.genesis.ownerClientId
|
|
|| grant->recipientAccountId
|
|
!= transaction.genesis.ownerAccountId
|
|
|| grant->telegramPeerIdBinding
|
|
!= transaction.genesis.telegramPeerIdBinding
|
|
|| grant->groupGeneration != 1
|
|
|| grant->historyAccess != HistoryAccess{
|
|
.mode = HistoryAccessMode::Full,
|
|
.boundaryEventId = {},
|
|
}
|
|
|| envelope->authenticationData != QByteArray(
|
|
reinterpret_cast<const char*>(grant->signature.data()),
|
|
int(grant->signature.size()))
|
|
|| !VerifyEncryptedHistoryGrantSignature(
|
|
*grant,
|
|
transaction.ownerCredential,
|
|
sha256)) {
|
|
return false;
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
return kinds == std::set<ObjectKind>{
|
|
ObjectKind::AccountCredential,
|
|
ObjectKind::MlsGroupInfo,
|
|
ObjectKind::InitialGroupState,
|
|
ObjectKind::HistoryGrant,
|
|
};
|
|
}
|
|
|
|
} // namespace
|
|
|
|
PersistentGroupBootstrapJournal::PersistentGroupBootstrapJournal(
|
|
AtomicBlobStore &blobStore,
|
|
const LocalRecordProtector &protector)
|
|
: _blobStore(blobStore)
|
|
, _protector(protector) {
|
|
}
|
|
|
|
PersistentGroupBootstrapJournal::~PersistentGroupBootstrapJournal() {
|
|
reset();
|
|
}
|
|
|
|
GroupBootstrapJournalLoadResult PersistentGroupBootstrapJournal::load(
|
|
ConversationId conversationId) {
|
|
reset();
|
|
if (!conversationId) {
|
|
return GroupBootstrapJournalLoadResult::InvalidSnapshot;
|
|
}
|
|
_conversationId = conversationId;
|
|
const auto stored = _blobStore.read();
|
|
if (stored.status == BlobReadStatus::Missing) {
|
|
_loaded = true;
|
|
return GroupBootstrapJournalLoadResult::Empty;
|
|
} else if (stored.status != BlobReadStatus::Found) {
|
|
reset();
|
|
return GroupBootstrapJournalLoadResult::StorageError;
|
|
}
|
|
auto plaintext = _protector.open(Purpose(conversationId), stored.bytes);
|
|
if (!plaintext) {
|
|
reset();
|
|
return GroupBootstrapJournalLoadResult::AuthenticationFailed;
|
|
}
|
|
auto decoded = DecodeSnapshot(*plaintext, conversationId);
|
|
Cleanse(*plaintext);
|
|
if (!decoded) {
|
|
reset();
|
|
return GroupBootstrapJournalLoadResult::InvalidSnapshot;
|
|
}
|
|
_revision = decoded->revision;
|
|
_pending = std::move(decoded->transaction);
|
|
_loaded = true;
|
|
return _pending
|
|
? GroupBootstrapJournalLoadResult::Pending
|
|
: GroupBootstrapJournalLoadResult::Empty;
|
|
}
|
|
|
|
GroupBootstrapJournalCommitResult PersistentGroupBootstrapJournal::prepare(
|
|
GroupBootstrapTransaction transaction) {
|
|
const auto transactionGuard = qScopeGuard([&] {
|
|
Cleanse(transaction);
|
|
});
|
|
if (!_loaded) {
|
|
return GroupBootstrapJournalCommitResult::NotLoaded;
|
|
} else if (!BasicTransactionStructure(transaction)
|
|
|| transaction.conversationId != _conversationId
|
|
|| _revision == std::numeric_limits<std::uint64_t>::max()) {
|
|
return GroupBootstrapJournalCommitResult::InvalidTransaction;
|
|
} else if (_pending) {
|
|
return SameTransaction(*_pending, transaction)
|
|
? GroupBootstrapJournalCommitResult::AlreadyPrepared
|
|
: GroupBootstrapJournalCommitResult::TransactionConflict;
|
|
}
|
|
const auto revision = _revision + 1;
|
|
if (!persist(&transaction, revision)) {
|
|
return GroupBootstrapJournalCommitResult::PersistenceFailed;
|
|
}
|
|
_pending = std::move(transaction);
|
|
_revision = revision;
|
|
return GroupBootstrapJournalCommitResult::Prepared;
|
|
}
|
|
|
|
GroupBootstrapJournalCommitResult PersistentGroupBootstrapJournal::clear(
|
|
ObjectId genesisObjectId) {
|
|
if (!_loaded) {
|
|
return GroupBootstrapJournalCommitResult::NotLoaded;
|
|
} else if (!_pending
|
|
|| _pending->genesis.genesisObjectId != genesisObjectId
|
|
|| _revision == std::numeric_limits<std::uint64_t>::max()) {
|
|
return GroupBootstrapJournalCommitResult::TransactionConflict;
|
|
}
|
|
const auto revision = _revision + 1;
|
|
if (!persist(nullptr, revision)) {
|
|
return GroupBootstrapJournalCommitResult::PersistenceFailed;
|
|
}
|
|
Cleanse(*_pending);
|
|
_pending.reset();
|
|
_revision = revision;
|
|
return GroupBootstrapJournalCommitResult::Cleared;
|
|
}
|
|
|
|
bool PersistentGroupBootstrapJournal::loaded() const {
|
|
return _loaded;
|
|
}
|
|
|
|
const GroupBootstrapTransaction *PersistentGroupBootstrapJournal::pending()
|
|
const {
|
|
return _pending ? &*_pending : nullptr;
|
|
}
|
|
|
|
bool PersistentGroupBootstrapJournal::persist(
|
|
const GroupBootstrapTransaction *transaction,
|
|
std::uint64_t revision) const {
|
|
if (!_conversationId || !revision) {
|
|
return false;
|
|
}
|
|
auto plaintext = EncodeSnapshot(_conversationId, revision, transaction);
|
|
if (plaintext.isEmpty()) {
|
|
return false;
|
|
}
|
|
const auto protectedBytes = _protector.seal(
|
|
Purpose(_conversationId),
|
|
plaintext);
|
|
Cleanse(plaintext);
|
|
return protectedBytes && _blobStore.writeAtomic(*protectedBytes);
|
|
}
|
|
|
|
void PersistentGroupBootstrapJournal::reset() {
|
|
if (_pending) {
|
|
Cleanse(*_pending);
|
|
}
|
|
_pending.reset();
|
|
_conversationId = {};
|
|
_revision = 0;
|
|
_loaded = false;
|
|
}
|
|
|
|
GroupBootstrapTransactionCoordinator::GroupBootstrapTransactionCoordinator(
|
|
PersistentGroupBootstrapJournal &journal,
|
|
PersistentMlsStateStore &mlsState,
|
|
PersistentArchiveState &archiveState,
|
|
PersistentGroupLedger &groupLedger,
|
|
PersistentOutboxStore &outbox,
|
|
const EnvelopeCodecV1 &envelopeCodec,
|
|
const Sha256Provider &sha256)
|
|
: _journal(journal)
|
|
, _mlsState(mlsState)
|
|
, _archiveState(archiveState)
|
|
, _groupLedger(groupLedger)
|
|
, _outbox(outbox)
|
|
, _envelopeCodec(envelopeCodec)
|
|
, _sha256(sha256) {
|
|
}
|
|
|
|
GroupBootstrapApplyStatus GroupBootstrapTransactionCoordinator::apply(
|
|
GroupBootstrapTransaction transaction) {
|
|
const auto transactionGuard = qScopeGuard([&] {
|
|
Cleanse(transaction);
|
|
});
|
|
if (!_journal.loaded()
|
|
|| !_mlsState.loaded()
|
|
|| !_outbox.loaded()) {
|
|
return GroupBootstrapApplyStatus::JournalUnavailable;
|
|
} else if (transaction.outboxBaseRevision != _outbox.revision()) {
|
|
return GroupBootstrapApplyStatus::RevisionConflict;
|
|
}
|
|
auto verified = VerifySignedGroupGenesisOutcome();
|
|
if (!ValidBootstrapBundle(
|
|
transaction,
|
|
_envelopeCodec,
|
|
_sha256,
|
|
verified)) {
|
|
return GroupBootstrapApplyStatus::InvalidTransaction;
|
|
}
|
|
switch (_journal.prepare(std::move(transaction))) {
|
|
case GroupBootstrapJournalCommitResult::Prepared:
|
|
case GroupBootstrapJournalCommitResult::AlreadyPrepared:
|
|
return replay(false);
|
|
case GroupBootstrapJournalCommitResult::NotLoaded:
|
|
return GroupBootstrapApplyStatus::JournalUnavailable;
|
|
case GroupBootstrapJournalCommitResult::InvalidTransaction:
|
|
return GroupBootstrapApplyStatus::InvalidTransaction;
|
|
case GroupBootstrapJournalCommitResult::TransactionConflict:
|
|
return GroupBootstrapApplyStatus::RevisionConflict;
|
|
case GroupBootstrapJournalCommitResult::PersistenceFailed:
|
|
case GroupBootstrapJournalCommitResult::Cleared:
|
|
return GroupBootstrapApplyStatus::JournalClearFailure;
|
|
}
|
|
return GroupBootstrapApplyStatus::InvalidTransaction;
|
|
}
|
|
|
|
GroupBootstrapApplyStatus GroupBootstrapTransactionCoordinator::recover() {
|
|
return replay(true);
|
|
}
|
|
|
|
GroupBootstrapApplyStatus GroupBootstrapTransactionCoordinator::replay(
|
|
bool recovery) {
|
|
const auto transaction = _journal.pending();
|
|
if (!transaction) {
|
|
return GroupBootstrapApplyStatus::NoPendingTransaction;
|
|
}
|
|
auto verified = VerifySignedGroupGenesisOutcome();
|
|
if (!ValidBootstrapBundle(
|
|
*transaction,
|
|
_envelopeCodec,
|
|
_sha256,
|
|
verified)
|
|
|| !verified.verified) {
|
|
return GroupBootstrapApplyStatus::InvalidTransaction;
|
|
}
|
|
if (_mlsState.revision() == 0) {
|
|
if (_mlsState.initialize(
|
|
OpenMlsEngineId(),
|
|
transaction->mlsEngineState)
|
|
!= MlsStateCommitResult::Committed) {
|
|
return GroupBootstrapApplyStatus::MlsPersistenceFailure;
|
|
}
|
|
} else if (_mlsState.revision() != 1
|
|
|| _mlsState.engineId() != OpenMlsEngineId()
|
|
|| _mlsState.engineState() != transaction->mlsEngineState) {
|
|
return GroupBootstrapApplyStatus::RevisionConflict;
|
|
}
|
|
if (_archiveState.revision() == 0) {
|
|
if (_archiveState.initialize({
|
|
.generation = transaction->archiveEpoch.generation,
|
|
.activationGroupGeneration =
|
|
transaction->archiveEpoch.activationGroupGeneration,
|
|
.activationEventId =
|
|
transaction->archiveEpoch.activationEventId,
|
|
.key = transaction->archiveEpoch.key.clone(),
|
|
}) != ArchiveStateCommitResult::Committed) {
|
|
return GroupBootstrapApplyStatus::ArchivePersistenceFailure;
|
|
}
|
|
} else if (_archiveState.revision() != 1
|
|
|| !_archiveState.currentEpoch()
|
|
|| !SameArchiveEpoch(
|
|
*_archiveState.currentEpoch(),
|
|
transaction->archiveEpoch)) {
|
|
return GroupBootstrapApplyStatus::RevisionConflict;
|
|
}
|
|
if (_groupLedger.revision() == 0) {
|
|
if (_groupLedger.initialize(
|
|
transaction->genesis,
|
|
verified.verified->state,
|
|
verified.verified->checkpoint,
|
|
transaction->ownerCredential)
|
|
!= GroupLedgerCommitResult::Committed) {
|
|
return GroupBootstrapApplyStatus::GroupPersistenceFailure;
|
|
}
|
|
} else if (_groupLedger.revision() != 1
|
|
|| _groupLedger.checkpoint() != verified.verified->checkpoint
|
|
|| _groupLedger.events() != std::vector<GroupLedgerEvent>{ {
|
|
.kind = GroupLedgerEventKind::Genesis,
|
|
.generation = 1,
|
|
.objectId = transaction->genesis.genesisObjectId,
|
|
.bytes = *SignedGroupGenesisCodecV1().encode(
|
|
transaction->genesis),
|
|
} }) {
|
|
return GroupBootstrapApplyStatus::RevisionConflict;
|
|
}
|
|
for (auto i = std::size_t(0);
|
|
i != transaction->outboxEnvelopes.size();
|
|
++i) {
|
|
const auto &expected = transaction->outboxEnvelopes[i];
|
|
auto existing = _outbox.item(expected.objectId);
|
|
const auto existingGuard = qScopeGuard([&] {
|
|
if (existing) {
|
|
CleanseOutboxItem(*existing);
|
|
}
|
|
});
|
|
if (existing) {
|
|
if (existing->stage != OutboxItemStage::Sealed
|
|
|| existing->sealed != expected) {
|
|
return GroupBootstrapApplyStatus::RevisionConflict;
|
|
}
|
|
continue;
|
|
} else if (_outbox.revision()
|
|
!= transaction->outboxBaseRevision + i
|
|
|| !_outbox.appendSealed(expected)) {
|
|
return GroupBootstrapApplyStatus::OutboxPersistenceFailure;
|
|
}
|
|
}
|
|
if (_journal.clear(transaction->genesis.genesisObjectId)
|
|
!= GroupBootstrapJournalCommitResult::Cleared) {
|
|
return GroupBootstrapApplyStatus::JournalClearFailure;
|
|
}
|
|
return recovery
|
|
? GroupBootstrapApplyStatus::Recovered
|
|
: GroupBootstrapApplyStatus::Applied;
|
|
}
|
|
|
|
GroupBootstrapTransaction MakeGroupBootstrapTransaction(
|
|
PreparedProtectedGroupBootstrap &&prepared,
|
|
const AccountCredentialPublic &ownerCredential,
|
|
std::uint64_t outboxBaseRevision) {
|
|
return {
|
|
.conversationId = prepared.conversationId,
|
|
.outboxBaseRevision = outboxBaseRevision,
|
|
.genesis = prepared.genesis,
|
|
.ownerCredential = ownerCredential,
|
|
.initialMlsPublicObject =
|
|
std::move(prepared.initialMlsPublicObject),
|
|
.archiveEpoch = {
|
|
.generation = prepared.archiveEpoch.generation,
|
|
.activationGroupGeneration =
|
|
prepared.archiveEpoch.activationGroupGeneration,
|
|
.activationEventId = prepared.archiveEpoch.activationEventId,
|
|
.key = std::move(prepared.archiveEpoch.key),
|
|
},
|
|
.mlsEngineState = std::move(prepared.mlsEngineState),
|
|
.outboxEnvelopes = std::move(prepared.outboxEnvelopes),
|
|
};
|
|
}
|
|
|
|
} // namespace E2ECloud
|