633 lines
21 KiB
C++
633 lines
21 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_codec.h"
|
|
#include "e2e_cloud/identity/openssl_account_crypto.h"
|
|
#include "e2e_cloud/mls/mls_context_codec.h"
|
|
#include "e2e_cloud/mls/mls_roster_codec.h"
|
|
#include "e2e_cloud/mls/key_package_lifecycle.h"
|
|
#include "e2e_cloud/mls/openmls_application_engine.h"
|
|
#include "e2e_cloud/mls/openmls_bridge.h"
|
|
#include "e2e_cloud/storage/aes_gcm_local_record_protector.h"
|
|
#include "e2e_cloud/storage/persistent_inbound_journal.h"
|
|
#include "e2e_cloud/storage/persistent_key_package_pool.h"
|
|
#include "e2e_cloud/storage/persistent_mls_state.h"
|
|
|
|
#include <algorithm>
|
|
#include <cstdio>
|
|
#include <optional>
|
|
#include <string_view>
|
|
#include <utility>
|
|
|
|
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]] bool Contains(
|
|
const QByteArray &haystack,
|
|
std::string_view needle) {
|
|
const auto first = haystack.constData();
|
|
const auto last = first + haystack.size();
|
|
return std::search(
|
|
first,
|
|
last,
|
|
begin(needle),
|
|
end(needle)) != last;
|
|
}
|
|
|
|
class MemoryBlobStore final : public AtomicBlobStore {
|
|
public:
|
|
[[nodiscard]] BlobReadResult read() const override {
|
|
if (readError) {
|
|
return {
|
|
.status = BlobReadStatus::Error,
|
|
.bytes = {},
|
|
};
|
|
} else if (!bytes) {
|
|
return {
|
|
.status = BlobReadStatus::Missing,
|
|
.bytes = {},
|
|
};
|
|
}
|
|
return {
|
|
.status = BlobReadStatus::Found,
|
|
.bytes = *bytes,
|
|
};
|
|
}
|
|
|
|
bool writeAtomic(const QByteArray &value) override {
|
|
++writeCount;
|
|
if (writeError || (failOnWrite && writeCount == failOnWrite)) {
|
|
return false;
|
|
}
|
|
bytes = value;
|
|
return true;
|
|
}
|
|
|
|
std::optional<QByteArray> bytes;
|
|
bool readError = false;
|
|
bool writeError = false;
|
|
int writeCount = 0;
|
|
int failOnWrite = 0;
|
|
|
|
};
|
|
|
|
[[nodiscard]] int ScenarioContextCodec() {
|
|
const auto codec = MlsContextCodecV1();
|
|
const auto credential = MlsClientCredential{
|
|
.conversationId = FilledId<ConversationId>(1),
|
|
.accountId = FilledId<AccountId>(2),
|
|
.clientId = FilledId<ClientId>(3),
|
|
};
|
|
const auto encodedCredential = codec.encodeCredential(credential);
|
|
if (!encodedCredential
|
|
|| encodedCredential->size() != kMlsClientCredentialEncodedSize
|
|
|| codec.decodeCredential(*encodedCredential) != credential) {
|
|
return Fail("MLS client credential did not round-trip");
|
|
}
|
|
auto corruptedCredential = *encodedCredential;
|
|
corruptedCredential[0] ^= 1;
|
|
if (codec.decodeCredential(corruptedCredential)) {
|
|
return Fail("MLS client credential accepted a wrong domain");
|
|
}
|
|
const auto aad = MlsTransportAad{
|
|
.conversationId = credential.conversationId,
|
|
.objectKind = ObjectKind::MlsApplication,
|
|
.senderAccountId = credential.accountId,
|
|
.senderClientId = credential.clientId,
|
|
.telegramPeerIdBinding = 42,
|
|
.objectId = FilledId<ObjectId>(4),
|
|
.context = QByteArray("reply metadata"),
|
|
};
|
|
const auto encodedAad = codec.encodeAad(aad);
|
|
if (!encodedAad
|
|
|| encodedAad->size()
|
|
!= kMlsTransportAadFixedSize + aad.context.size()
|
|
|| codec.decodeAad(*encodedAad) != aad) {
|
|
return Fail("MLS transport AAD did not round-trip");
|
|
}
|
|
auto trailingAad = *encodedAad;
|
|
trailingAad.append(char(0));
|
|
if (codec.decodeAad(trailingAad)) {
|
|
return Fail("MLS transport AAD accepted trailing bytes");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
[[nodiscard]] int ScenarioTransactionalApplicationSeal() {
|
|
auto localKey = LocalRecordKey();
|
|
localKey.fill(7);
|
|
const auto protector = AesGcmLocalRecordProtector(std::move(localKey));
|
|
auto blob = MemoryBlobStore();
|
|
auto store = PersistentMlsStateStore(blob, protector);
|
|
const auto context = OpenMlsClientContext{
|
|
.conversationId = FilledId<ConversationId>(11),
|
|
.accountId = FilledId<AccountId>(12),
|
|
.clientId = FilledId<ClientId>(13),
|
|
.telegramPeerIdBinding = 123456,
|
|
};
|
|
const auto bridge = OpenMlsBridge();
|
|
const auto contextCodec = MlsContextCodecV1();
|
|
const auto envelopeCodec = EnvelopeCodecV1();
|
|
const auto sha256 = OpenSslSha256Provider();
|
|
if (store.load(context.conversationId) != MlsStateLoadResult::Missing) {
|
|
return Fail("MLS creator store did not load empty");
|
|
}
|
|
const auto initialized = InitializeOpenMlsCreator(
|
|
context,
|
|
bridge,
|
|
contextCodec,
|
|
store);
|
|
if (initialized.status != OpenMlsBootstrapStatus::Initialized
|
|
|| initialized.roster.isEmpty()
|
|
|| store.engineId() != OpenMlsEngineId()
|
|
|| store.revision() != 1) {
|
|
return Fail("OpenMLS creator state was not initialized transactionally");
|
|
}
|
|
const auto rosterCodec = MlsRosterCodecV1();
|
|
const auto creatorRoster = rosterCodec.decode(
|
|
initialized.roster,
|
|
contextCodec);
|
|
auto protectedGroup = ProtectedGroupState::Create({
|
|
.conversationId = context.conversationId,
|
|
.ownerAccountId = context.accountId,
|
|
.ownerClientId = context.clientId,
|
|
.ownerTelegramUserIdBinding = 100,
|
|
.policy = {
|
|
.defaultHistoryAccess = {
|
|
.mode = HistoryAccessMode::FromJoin,
|
|
.boundaryEventId = {},
|
|
},
|
|
},
|
|
});
|
|
if (!creatorRoster
|
|
|| creatorRoster->epoch
|
|
|| creatorRoster->members.size() != 1
|
|
|| creatorRoster->members.front().leafIndex != 0
|
|
|| !protectedGroup
|
|
|| !MlsRosterMatchesGroupState(*creatorRoster, *protectedGroup)) {
|
|
return Fail("OpenMLS creator roster did not match protected clients");
|
|
}
|
|
auto bobKey = LocalRecordKey();
|
|
bobKey.fill(8);
|
|
const auto bobProtector = AesGcmLocalRecordProtector(std::move(bobKey));
|
|
auto bobBlob = MemoryBlobStore();
|
|
auto bobStore = PersistentMlsStateStore(bobBlob, bobProtector);
|
|
const auto bobContext = OpenMlsClientContext{
|
|
.conversationId = context.conversationId,
|
|
.accountId = FilledId<AccountId>(22),
|
|
.clientId = FilledId<ClientId>(23),
|
|
.telegramPeerIdBinding = context.telegramPeerIdBinding,
|
|
};
|
|
const auto bobCredential = contextCodec.encodeCredential({
|
|
.conversationId = bobContext.conversationId,
|
|
.accountId = bobContext.accountId,
|
|
.clientId = bobContext.clientId,
|
|
});
|
|
const auto groupId = QByteArray(
|
|
reinterpret_cast<const char*>(context.conversationId.bytes.data()),
|
|
context.conversationId.bytes.size());
|
|
if (!bobCredential
|
|
|| bobStore.load(context.conversationId)
|
|
!= MlsStateLoadResult::Missing) {
|
|
return Fail("joining MLS client state did not initialize cleanly");
|
|
}
|
|
auto bobPackage = bridge.createKeyPackage(*bobCredential, groupId);
|
|
if (bobPackage.status != OpenMlsBridgeStatus::Ok
|
|
|| bobPackage.state.isEmpty()
|
|
|| bobPackage.keyPackage.isEmpty()
|
|
|| bobStore.initialize(
|
|
OpenMlsEngineId(),
|
|
std::move(bobPackage.state))
|
|
!= MlsStateCommitResult::Committed) {
|
|
return Fail("joining MLS client could not create a KeyPackage");
|
|
}
|
|
const auto addAad = contextCodec.encodeAad({
|
|
.conversationId = context.conversationId,
|
|
.objectKind = ObjectKind::MlsCommit,
|
|
.senderAccountId = context.accountId,
|
|
.senderClientId = context.clientId,
|
|
.telegramPeerIdBinding = context.telegramPeerIdBinding,
|
|
.objectId = FilledId<ObjectId>(24),
|
|
.context = QByteArray("authenticated add transition"),
|
|
});
|
|
if (!addAad) {
|
|
return Fail("MLS add transition AAD was invalid");
|
|
}
|
|
auto added = bridge.addMember(
|
|
store.engineState(),
|
|
bobPackage.keyPackage,
|
|
*addAad);
|
|
if (added.status != OpenMlsBridgeStatus::Ok
|
|
|| added.epoch != 1
|
|
|| added.state.isEmpty()
|
|
|| added.commit.isEmpty()
|
|
|| added.welcome.isEmpty()
|
|
|| store.commit({
|
|
.baseRevision = 1,
|
|
.engineState = std::move(added.state),
|
|
.receipt = std::nullopt,
|
|
.inboundApplication = std::nullopt,
|
|
.removalTombstone = std::nullopt,
|
|
}) != MlsStateCommitResult::Committed) {
|
|
return Fail("MLS creator could not add the joining client");
|
|
}
|
|
const auto addedRoster = rosterCodec.decode(added.roster, contextCodec);
|
|
const auto addTransition = GroupTransition{
|
|
.conversationId = context.conversationId,
|
|
.transitionId = FilledId<ObjectId>(25),
|
|
.previousGeneration = 1,
|
|
.generation = 2,
|
|
.kind = GroupTransitionKind::AddMember,
|
|
.targetAccountId = bobContext.accountId,
|
|
.targetClientId = bobContext.clientId,
|
|
.targetTelegramUserIdBinding = 200,
|
|
.targetRole = GroupRole::Member,
|
|
.targetAdminPermissions = 0,
|
|
.historyAccess = {
|
|
.mode = HistoryAccessMode::FromJoin,
|
|
.boundaryEventId = {},
|
|
},
|
|
};
|
|
if (!addedRoster
|
|
|| addedRoster->epoch != 1
|
|
|| addedRoster->members.size() != 2
|
|
|| protectedGroup->applyVerified(
|
|
addTransition,
|
|
{
|
|
.actor = {
|
|
.accountId = context.accountId,
|
|
.clientId = context.clientId,
|
|
},
|
|
.targetClientAuthorization = VerifiedClientAuthorization{
|
|
.accountId = bobContext.accountId,
|
|
.clientId = bobContext.clientId,
|
|
},
|
|
}) != GroupTransitionResult::Allowed
|
|
|| !MlsRosterMatchesGroupState(*addedRoster, *protectedGroup)) {
|
|
return Fail("OpenMLS admission roster diverged from protected group");
|
|
}
|
|
auto joined = bridge.join(bobStore.engineState(), added.welcome);
|
|
if (joined.status != OpenMlsBridgeStatus::Ok
|
|
|| joined.epoch != 1
|
|
|| joined.state.isEmpty()
|
|
|| joined.roster != added.roster
|
|
|| bobStore.commit({
|
|
.baseRevision = 1,
|
|
.engineState = std::move(joined.state),
|
|
.receipt = std::nullopt,
|
|
.inboundApplication = std::nullopt,
|
|
.removalTombstone = std::nullopt,
|
|
}) != MlsStateCommitResult::Committed) {
|
|
return Fail("joining MLS client could not process its Welcome");
|
|
}
|
|
auto engine = OpenMlsApplicationEngine(
|
|
context,
|
|
bridge,
|
|
contextCodec,
|
|
envelopeCodec,
|
|
sha256,
|
|
store);
|
|
if (!engine.ready()) {
|
|
return Fail("OpenMLS application engine rejected initialized state");
|
|
}
|
|
const auto request = MlsSealRequest{
|
|
.conversationId = context.conversationId,
|
|
.objectId = FilledId<ObjectId>(14),
|
|
.plaintext = QByteArray("secret application body"),
|
|
.authenticatedData = QByteArray("reply metadata"),
|
|
};
|
|
const auto first = engine.protectIdempotently(request);
|
|
if (!first
|
|
|| store.revision() != 3
|
|
|| store.receiptCount() != 1
|
|
|| Contains(first->bytes, "secret application body")) {
|
|
return Fail("MLS application was not sealed and committed atomically");
|
|
}
|
|
const auto envelope = envelopeCodec.decode(*first);
|
|
const auto aad = envelope
|
|
? contextCodec.decodeAad(envelope->authenticationData)
|
|
: std::nullopt;
|
|
if (!envelope
|
|
|| !aad
|
|
|| envelope->conversationId != context.conversationId
|
|
|| envelope->senderAccountId != context.accountId
|
|
|| envelope->senderClientId != context.clientId
|
|
|| envelope->telegramPeerIdBinding != context.telegramPeerIdBinding
|
|
|| envelope->objectId != request.objectId
|
|
|| envelope->payloadHash != sha256.digest(envelope->payload)
|
|
|| aad->conversationId != context.conversationId
|
|
|| aad->senderAccountId != context.accountId
|
|
|| aad->senderClientId != context.clientId
|
|
|| aad->telegramPeerIdBinding != context.telegramPeerIdBinding
|
|
|| aad->objectId != request.objectId
|
|
|| aad->context != request.authenticatedData) {
|
|
return Fail("MLS ciphertext was not bound to its transport context");
|
|
}
|
|
const auto retry = engine.protectIdempotently(request);
|
|
if (retry != first || store.revision() != 3) {
|
|
return Fail("MLS retry did not reuse exact persisted ciphertext");
|
|
}
|
|
auto conflicting = request;
|
|
conflicting.plaintext = QByteArray("different plaintext");
|
|
if (engine.protectIdempotently(conflicting) || store.revision() != 3) {
|
|
return Fail("MLS object identifier accepted conflicting plaintext");
|
|
}
|
|
auto restored = PersistentMlsStateStore(blob, protector);
|
|
if (restored.load(context.conversationId) != MlsStateLoadResult::Loaded) {
|
|
return Fail("MLS application state did not survive restart");
|
|
}
|
|
auto restarted = OpenMlsApplicationEngine(
|
|
context,
|
|
bridge,
|
|
contextCodec,
|
|
envelopeCodec,
|
|
sha256,
|
|
restored);
|
|
if (restarted.protectIdempotently(request) != first
|
|
|| restored.revision() != 3) {
|
|
return Fail("MLS restart regenerated an acknowledged ciphertext");
|
|
}
|
|
const auto stateBeforeFailure = restored.engineState();
|
|
const auto secondRequest = MlsSealRequest{
|
|
.conversationId = context.conversationId,
|
|
.objectId = FilledId<ObjectId>(15),
|
|
.plaintext = QByteArray("second secret body"),
|
|
.authenticatedData = QByteArray(),
|
|
};
|
|
blob.writeError = true;
|
|
if (restarted.protectIdempotently(secondRequest)
|
|
|| restored.engineState() != stateBeforeFailure
|
|
|| restored.revision() != 3) {
|
|
return Fail("failed MLS persistence advanced the live sender ratchet");
|
|
}
|
|
blob.writeError = false;
|
|
if (!restarted.protectIdempotently(secondRequest)
|
|
|| restored.revision() != 4
|
|
|| restored.receiptCount() != 2) {
|
|
return Fail("MLS sender did not recover after a failed atomic write");
|
|
}
|
|
auto journalKey = LocalRecordKey();
|
|
journalKey.fill(9);
|
|
const auto journalProtector = AesGcmLocalRecordProtector(
|
|
std::move(journalKey));
|
|
auto journalBlob = MemoryBlobStore();
|
|
journalBlob.failOnWrite = 2;
|
|
auto journal = PersistentInboundJournal(journalBlob, journalProtector);
|
|
if (journal.load() != InboundJournalLoadResult::Missing) {
|
|
return Fail("MLS inbound journal did not initialize cleanly");
|
|
}
|
|
const auto inboundAuthenticator = OpenMlsEnvelopeAuthenticator(
|
|
contextCodec,
|
|
sha256);
|
|
auto inboundApplier = OpenMlsApplicationInboundApplier(
|
|
bobContext,
|
|
bridge,
|
|
contextCodec,
|
|
sha256,
|
|
bobStore);
|
|
auto inboundProcessor = InboundEnvelopeProcessor(
|
|
context.conversationId,
|
|
context.telegramPeerIdBinding,
|
|
envelopeCodec,
|
|
inboundAuthenticator,
|
|
journal,
|
|
inboundApplier);
|
|
if (inboundProcessor.process(first->bytes)
|
|
!= InboundProcessResult::RecoveryRequired
|
|
|| bobStore.revision() != 3
|
|
|| bobStore.inboundApplicationCount() != 1
|
|
|| journal.lookup(
|
|
context.conversationId,
|
|
request.objectId,
|
|
envelope->payloadHash) != InboundJournalLookup::Pending) {
|
|
return Fail("MLS journal failure lost the atomically decrypted delivery");
|
|
}
|
|
journalBlob.failOnWrite = 0;
|
|
auto bobRestored = PersistentMlsStateStore(bobBlob, bobProtector);
|
|
auto journalRestored = PersistentInboundJournal(
|
|
journalBlob,
|
|
journalProtector);
|
|
if (bobRestored.load(context.conversationId)
|
|
!= MlsStateLoadResult::Loaded
|
|
|| journalRestored.load() != InboundJournalLoadResult::Loaded) {
|
|
return Fail("MLS pending inbound transaction did not survive restart");
|
|
}
|
|
auto inboundApplierRestored = OpenMlsApplicationInboundApplier(
|
|
bobContext,
|
|
bridge,
|
|
contextCodec,
|
|
sha256,
|
|
bobRestored);
|
|
auto inboundProcessorRestored = InboundEnvelopeProcessor(
|
|
context.conversationId,
|
|
context.telegramPeerIdBinding,
|
|
envelopeCodec,
|
|
inboundAuthenticator,
|
|
journalRestored,
|
|
inboundApplierRestored);
|
|
if (inboundProcessorRestored.process(first->bytes)
|
|
!= InboundProcessResult::Accepted
|
|
|| bobRestored.revision() != 3
|
|
|| inboundProcessorRestored.process(first->bytes)
|
|
!= InboundProcessResult::Duplicate
|
|
|| bobRestored.revision() != 3) {
|
|
return Fail("MLS pending journal entry replayed an applied ratchet step");
|
|
}
|
|
const auto delivered = inboundApplierRestored.application(request.objectId);
|
|
if (!delivered
|
|
|| delivered->plaintext != request.plaintext
|
|
|| delivered->context != request.authenticatedData
|
|
|| delivered->senderAccountId != context.accountId
|
|
|| delivered->senderClientId != context.clientId
|
|
|| delivered->senderLeafIndex != 0
|
|
|| delivered->epoch != 1) {
|
|
return Fail("MLS inbound delivery lost authenticated sender metadata");
|
|
}
|
|
auto modifiedHeader = *envelope;
|
|
modifiedHeader.senderAccountId = FilledId<AccountId>(99);
|
|
const auto modifiedEncoded = envelopeCodec.encode(modifiedHeader);
|
|
if (!modifiedEncoded
|
|
|| inboundProcessorRestored.process(modifiedEncoded->bytes)
|
|
!= InboundProcessResult::AuthenticationFailed) {
|
|
return Fail("MLS transport accepted a server-modified sender header");
|
|
}
|
|
if (!inboundApplierRestored.acknowledgeDelivered(request.objectId)
|
|
|| bobRestored.inboundApplicationCount()
|
|
|| bobRestored.revision() != 4) {
|
|
return Fail("MLS decrypted delivery was not durably acknowledged");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
[[nodiscard]] int ScenarioKeyPackageLifecycleConsumesEveryCandidate() {
|
|
auto localKey = LocalRecordKey();
|
|
localKey.fill(17);
|
|
const auto protector = AesGcmLocalRecordProtector(std::move(localKey));
|
|
const auto bridge = OpenMlsBridge();
|
|
const auto contextCodec = MlsContextCodecV1();
|
|
const auto publicationCodec = ClientKeyPackagePublicationCodecV1();
|
|
const auto envelopeCodec = EnvelopeCodecV1();
|
|
const auto sha256 = OpenSslSha256Provider();
|
|
const auto conversationId = FilledId<ConversationId>(71);
|
|
const auto clientId = FilledId<ClientId>(73);
|
|
const auto peerBinding = std::uint64_t(7400);
|
|
const auto generation = std::uint64_t(5);
|
|
const auto createdAt = std::uint64_t(2'000'000);
|
|
auto identity = GenerateAccountPrivateIdentity();
|
|
const auto accountId = identity
|
|
? DeriveAccountId(identity->credential, sha256)
|
|
: std::nullopt;
|
|
if (!identity || !accountId) {
|
|
return Fail("KeyPackage lifecycle identity setup failed");
|
|
}
|
|
const auto context = OpenMlsClientContext{
|
|
.conversationId = conversationId,
|
|
.accountId = *accountId,
|
|
.clientId = clientId,
|
|
.telegramPeerIdBinding = peerBinding,
|
|
};
|
|
auto first = PrepareClientKeyPackage({
|
|
.client = context,
|
|
.currentGeneration = generation,
|
|
.telegramUserIdBinding = 7401,
|
|
.createdAt = createdAt,
|
|
.accountCredential = &identity->credential,
|
|
.accountSigningPrivateKey = &identity->signingPrivateKey,
|
|
}, bridge, contextCodec, publicationCodec, envelopeCodec, sha256);
|
|
auto second = PrepareClientKeyPackage({
|
|
.client = context,
|
|
.currentGeneration = generation,
|
|
.telegramUserIdBinding = 7401,
|
|
.createdAt = createdAt,
|
|
.accountCredential = &identity->credential,
|
|
.accountSigningPrivateKey = &identity->signingPrivateKey,
|
|
}, bridge, contextCodec, publicationCodec, envelopeCodec, sha256);
|
|
auto poolBlob = MemoryBlobStore();
|
|
auto pool = PersistentKeyPackagePool(
|
|
poolBlob,
|
|
protector,
|
|
envelopeCodec,
|
|
sha256);
|
|
if (first.status != PrepareClientKeyPackageStatus::Prepared
|
|
|| !first.entry
|
|
|| second.status != PrepareClientKeyPackageStatus::Prepared
|
|
|| !second.entry
|
|
|| pool.load(conversationId, peerBinding)
|
|
!= KeyPackagePoolLoadResult::Empty
|
|
|| pool.add(std::move(*first.entry))
|
|
!= KeyPackagePoolMutationResult::Committed
|
|
|| pool.add(std::move(*second.entry))
|
|
!= KeyPackagePoolMutationResult::Committed
|
|
|| pool.entries().size() != 2) {
|
|
return Fail("KeyPackage lifecycle did not retain candidate states");
|
|
}
|
|
const auto firstEnvelope = envelopeCodec.decode(
|
|
pool.entries().front().publicationEnvelope);
|
|
const auto firstPublication = firstEnvelope
|
|
? publicationCodec.decode(firstEnvelope->payload)
|
|
: std::nullopt;
|
|
const auto secondEnvelope = envelopeCodec.decode(
|
|
pool.entries().back().publicationEnvelope);
|
|
const auto secondPublication = secondEnvelope
|
|
? publicationCodec.decode(secondEnvelope->payload)
|
|
: std::nullopt;
|
|
if (!firstPublication || !secondPublication) {
|
|
return Fail("KeyPackage lifecycle lost its public candidate");
|
|
}
|
|
auto mlsBlob = MemoryBlobStore();
|
|
auto mlsState = PersistentMlsStateStore(mlsBlob, protector);
|
|
if (mlsState.load(conversationId) != MlsStateLoadResult::Missing
|
|
|| InstallClientKeyPackageForWelcome(
|
|
firstPublication->keyPackage,
|
|
createdAt,
|
|
bridge,
|
|
sha256,
|
|
pool,
|
|
mlsState) != InstallClientKeyPackageStatus::Installed
|
|
|| InstallClientKeyPackageForWelcome(
|
|
secondPublication->keyPackage,
|
|
createdAt,
|
|
bridge,
|
|
sha256,
|
|
pool,
|
|
mlsState) != InstallClientKeyPackageStatus::Installed
|
|
|| InstallClientKeyPackageForWelcome(
|
|
firstPublication->keyPackage,
|
|
createdAt,
|
|
bridge,
|
|
sha256,
|
|
pool,
|
|
mlsState) != InstallClientKeyPackageStatus::Installed
|
|
|| !bridge.isKeyPackageState(mlsState.engineState())) {
|
|
return Fail("retained KeyPackage candidates were not interchangeable");
|
|
}
|
|
const auto ownerCredential = contextCodec.encodeCredential({
|
|
.conversationId = conversationId,
|
|
.accountId = FilledId<AccountId>(76),
|
|
.clientId = FilledId<ClientId>(77),
|
|
});
|
|
const auto groupIdBytes = QByteArray(
|
|
reinterpret_cast<const char*>(conversationId.bytes.data()),
|
|
int(conversationId.bytes.size()));
|
|
const auto owner = ownerCredential
|
|
? bridge.createGroup(*ownerCredential, groupIdBytes)
|
|
: OpenMlsStateOutput();
|
|
const auto added = owner.status == OpenMlsBridgeStatus::Ok
|
|
? bridge.addMember(
|
|
owner.state,
|
|
firstPublication->keyPackage,
|
|
QByteArray("bound lifecycle admission"))
|
|
: OpenMlsCommitOutput();
|
|
const auto joined = added.status == OpenMlsBridgeStatus::Ok
|
|
? bridge.join(mlsState.engineState(), added.welcome)
|
|
: OpenMlsStateOutput();
|
|
if (joined.status != OpenMlsBridgeStatus::Ok
|
|
|| mlsState.commit({
|
|
.baseRevision = mlsState.revision(),
|
|
.engineState = joined.state,
|
|
.receipt = std::nullopt,
|
|
.inboundApplication = std::nullopt,
|
|
.removalTombstone = std::nullopt,
|
|
}) != MlsStateCommitResult::Committed
|
|
|| FinalizeClientKeyPackageWelcome(
|
|
firstPublication->keyPackage,
|
|
bridge,
|
|
sha256,
|
|
pool,
|
|
mlsState) != FinalizeClientKeyPackageStatus::Finalized
|
|
|| !pool.entries().empty()
|
|
|| bridge.inspectGroup(mlsState.engineState()).status
|
|
!= OpenMlsBridgeStatus::Ok) {
|
|
return Fail("Welcome did not consume all obsolete KeyPackage secrets");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
int main(int, char *[]) {
|
|
if (const auto result = ScenarioContextCodec()) {
|
|
return result;
|
|
} else if (const auto result
|
|
= ScenarioKeyPackageLifecycleConsumesEveryCandidate()) {
|
|
return result;
|
|
}
|
|
return ScenarioTransactionalApplicationSeal();
|
|
}
|