ZaStoGram_desktop/Telegram/SourceFiles/e2e_cloud/desktop/desktop_service.cpp

7916 lines
251 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/desktop/desktop_service.h"
#include "base/call_delayed.h"
#include "base/unixtime.h"
#include "crl/crl.h"
#include "core/application.h"
#include "data/data_session.h"
#include "data/data_peer.h"
#include "data/data_document.h"
#include "data/data_document_media.h"
#include "e2e_cloud/archive/archive_epoch_crypto.h"
#include "e2e_cloud/archive/archived_content_reader.h"
#include "e2e_cloud/archive/archived_content_outbox.h"
#include "e2e_cloud/archive/history_grant_service.h"
#include "e2e_cloud/archive/persistent_archive_state.h"
#include "e2e_cloud/content/protected_message_body.h"
#include "e2e_cloud/files/file_chunk_file_store.h"
#include "e2e_cloud/files/file_chunk_envelope.h"
#include "e2e_cloud/files/persistent_file_transfer.h"
#include "e2e_cloud/files/private_file_manifest.h"
#include "e2e_cloud/group/persistent_group_ledger.h"
#include "e2e_cloud/identity/safety_gossip.h"
#include "e2e_cloud/mls/key_package_lifecycle.h"
#include "e2e_cloud/mls/mls_outbox_reconciler.h"
#include "e2e_cloud/mls/observed_key_package.h"
#include "e2e_cloud/mls/openmls_application_engine.h"
#include "e2e_cloud/mls/openmls_group_change_engine.h"
#include "e2e_cloud/mls/openmls_inbound_group_change.h"
#include "e2e_cloud/protocol/group_bootstrap.h"
#include "e2e_cloud/protocol/group_bootstrap_transaction.h"
#include "e2e_cloud/protocol/group_change_transaction.h"
#include "e2e_cloud/protocol/freshness_protocol.h"
#include "e2e_cloud/protocol/observed_group_change_sync.h"
#include "e2e_cloud/protocol/observed_content_processor.h"
#include "e2e_cloud/protocol/public_join_catchup.h"
#include "e2e_cloud/storage/aes_gcm_local_record_protector.h"
#include "e2e_cloud/storage/file_atomic_blob_store.h"
#include "e2e_cloud/storage/local_record_key_derivation.h"
#include "e2e_cloud/storage/persistent_conversation_metadata.h"
#include "e2e_cloud/storage/persistent_content_sync_state.h"
#include "e2e_cloud/storage/persistent_control_observation_state.h"
#include "e2e_cloud/storage/persistent_freshness_trust.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 "e2e_cloud/storage/persistent_outbox.h"
#include "e2e_cloud/transport/telegram_carrier_transport.h"
#include "e2e_cloud/transport/file_chunk_download_controller.h"
#include "e2e_cloud/transport/observed_content_sync_controller.h"
#include "e2e_cloud/transport/outbox_upload_controller.h"
#include "main/main_session.h"
#include "history/history.h"
#include "history/history_item.h"
#include "lang/lang_keys.h"
#include "storage/storage_account.h"
#include "storage/localimageloader.h"
#include "ui/chat/attach/attach_prepare.h"
#include "ui/image/image_location_factory.h"
#include <openssl/crypto.h>
#include <openssl/evp.h>
#include <openssl/rand.h>
#include <QtCore/QBuffer>
#include <QtCore/QDir>
#include <QtCore/QFile>
#include <QtCore/QFileInfo>
#include <QtCore/QMimeDatabase>
#include <QtCore/QSaveFile>
#include <QtCore/QScopeGuard>
#include <QtGui/QImage>
#include <QtGui/QImageReader>
#include <algorithm>
#include <array>
#include <atomic>
#include <cctype>
#include <deque>
#include <limits>
#include <set>
#include <utility>
namespace E2ECloud {
namespace {
inline constexpr auto kControlSyncOverlap = std::size_t(32);
inline constexpr auto kProtectedPeersPref = "e2e_cloud_protected_peers_v1";
inline constexpr auto kMaximumProtectedPeerMarkers = std::size_t(65'536);
inline constexpr auto kProtectedPeerMarkerMagic = std::array<std::uint8_t, 8>{
'T', 'D', 'E', '2', 'E', 'P', 'P', '1'
};
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));
}
}
[[nodiscard]] std::uint32_t ReadUint32(const std::uint8_t *bytes) {
return (std::uint32_t(bytes[0]) << 24)
| (std::uint32_t(bytes[1]) << 16)
| (std::uint32_t(bytes[2]) << 8)
| std::uint32_t(bytes[3]);
}
[[nodiscard]] std::uint64_t ReadUint64(const std::uint8_t *bytes) {
auto result = std::uint64_t();
for (auto i = 0; i != 8; ++i) {
result = (result << 8) | bytes[i];
}
return result;
}
[[nodiscard]] QByteArray EncodeProtectedPeerMarkers(
std::uint64_t telegramUserIdBinding,
const std::set<std::uint64_t> &peers) {
auto result = QByteArray();
result.reserve(int(
kProtectedPeerMarkerMagic.size()
+ 8
+ 4
+ (peers.size() * 8)));
result.append(
reinterpret_cast<const char*>(kProtectedPeerMarkerMagic.data()),
kProtectedPeerMarkerMagic.size());
AppendUint64(result, telegramUserIdBinding);
AppendUint32(result, std::uint32_t(peers.size()));
for (const auto peer : peers) {
AppendUint64(result, peer);
}
return result;
}
[[nodiscard]] auto DecodeProtectedPeerMarkers(
const QByteArray &encoded,
std::uint64_t telegramUserIdBinding)
-> std::optional<std::set<std::uint64_t>> {
if (encoded.isEmpty()) {
return std::set<std::uint64_t>();
}
constexpr auto kHeaderSize = std::size_t(8 + 8 + 4);
if (encoded.size() < 0 || std::size_t(encoded.size()) < kHeaderSize) {
return std::nullopt;
}
const auto bytes = reinterpret_cast<const std::uint8_t*>(
encoded.constData());
if (!std::equal(
begin(kProtectedPeerMarkerMagic),
end(kProtectedPeerMarkerMagic),
bytes)
|| ReadUint64(bytes + 8) != telegramUserIdBinding) {
return std::nullopt;
}
const auto count = std::size_t(ReadUint32(bytes + 16));
if (count > kMaximumProtectedPeerMarkers
|| std::size_t(encoded.size()) != kHeaderSize + (count * 8)) {
return std::nullopt;
}
auto result = std::set<std::uint64_t>();
auto previous = std::uint64_t();
for (auto i = std::size_t(); i != count; ++i) {
const auto peer = ReadUint64(bytes + kHeaderSize + (i * 8));
if (!peer || (i && peer <= previous)) {
return std::nullopt;
}
result.emplace(peer);
previous = peer;
}
return result;
}
[[nodiscard]] Argon2idConfig DesktopArgon2idConfig() {
return {
.parameterVersion = 1,
.memoryKibibytes = 64 * 1024,
.iterations = 3,
.parallelism = 1,
};
}
void SelectCloudVaultOffMain(
std::vector<QByteArray> candidates,
QByteArray password,
std::uint64_t telegramUserIdBinding,
std::optional<CloudVaultAnchor> localAnchor,
CloudVaultSyncController::SelectionCompletion completion) {
crl::async([
candidates = std::move(candidates),
password = std::move(password),
telegramUserIdBinding,
localAnchor,
completion = std::move(completion)
]() mutable {
auto passwordKdf = Argon2idPasswordKdf();
auto sha256 = OpenSslSha256Provider();
auto codec = CloudVaultCodecV1(passwordKdf, sha256);
auto selector = CloudVaultSelector(codec, sha256);
auto result = selector.select(
std::move(candidates),
std::move(password),
telegramUserIdBinding,
localAnchor);
crl::on_main([
completion = std::move(completion),
result = std::move(result)
]() mutable {
completion(std::move(result));
});
});
}
void CreateCloudVaultOffMain(
std::uint64_t telegramUserIdBinding,
QByteArray password,
Argon2idConfig config,
std::function<void(std::optional<CreatedCloudVault>)> completion) {
crl::async([
telegramUserIdBinding,
password = std::move(password),
config,
completion = std::move(completion)
]() mutable {
auto passwordKdf = Argon2idPasswordKdf();
auto sha256 = OpenSslSha256Provider();
auto codec = CloudVaultCodecV1(passwordKdf, sha256);
auto identity = GenerateAccountPrivateIdentity();
auto result = identity
? codec.create(
telegramUserIdBinding,
std::move(*identity),
std::move(password),
config)
: std::nullopt;
crl::on_main([
completion = std::move(completion),
result = std::move(result)
]() mutable {
completion(std::move(result));
});
});
}
template <typename Id>
[[nodiscard]] std::optional<Id> RandomId() {
auto result = Id();
return (RAND_bytes(result.bytes.data(), result.bytes.size()) == 1
&& bool(result))
? std::optional<Id>(result)
: std::nullopt;
}
[[nodiscard]] QString ConversationDirectory(
std::uint64_t telegramUserIdBinding,
ConversationId conversationId) {
const auto encoded = QByteArray(
reinterpret_cast<const char*>(conversationId.bytes.data()),
int(conversationId.bytes.size())).toHex();
return cWorkingDir()
+ u"tdata/e2e_cloud/"_q
+ QString::number(telegramUserIdBinding)
+ u"/"_q
+ QString::fromLatin1(encoded)
+ u"/"_q;
}
[[nodiscard]] QString StagedProtectedImageDirectory(
std::uint64_t telegramUserIdBinding,
ConversationId conversationId) {
return ConversationDirectory(telegramUserIdBinding, conversationId)
+ u"staged-images/"_q;
}
[[nodiscard]] QString StagedProtectedFileDirectory(
std::uint64_t telegramUserIdBinding,
ConversationId conversationId) {
return ConversationDirectory(telegramUserIdBinding, conversationId)
+ u"staged-files/"_q;
}
void CleanupProtectedMediaCache(not_null<Main::Session*> session) {
QDir(session->local().tempDirectory() + u"protected_media/"_q)
.removeRecursively();
}
[[nodiscard]] bool IsStagedProtectedImage(
std::uint64_t telegramUserIdBinding,
ConversationId conversationId,
const QString &path) {
const auto info = QFileInfo(path);
const auto directory = QDir::cleanPath(
StagedProtectedImageDirectory(
telegramUserIdBinding,
conversationId));
return QDir::cleanPath(info.absolutePath()) == directory
&& info.fileName().startsWith(u"image-"_q)
&& info.fileName().endsWith(u".png"_q);
}
[[nodiscard]] bool IsStagedProtectedFile(
std::uint64_t telegramUserIdBinding,
ConversationId conversationId,
const QString &path) {
const auto info = QFileInfo(path);
const auto directory = QDir::cleanPath(
StagedProtectedFileDirectory(
telegramUserIdBinding,
conversationId));
return QDir::cleanPath(info.absolutePath()) == directory
&& info.fileName().startsWith(u"file-"_q)
&& info.fileName().endsWith(u".source"_q);
}
[[nodiscard]] bool IsStagedProtectedSource(
std::uint64_t telegramUserIdBinding,
ConversationId conversationId,
const QString &path) {
return IsStagedProtectedImage(
telegramUserIdBinding,
conversationId,
path) || IsStagedProtectedFile(
telegramUserIdBinding,
conversationId,
path);
}
void RemoveStagedProtectedSource(
std::uint64_t telegramUserIdBinding,
ConversationId conversationId,
const QString &path) {
if (IsStagedProtectedSource(
telegramUserIdBinding,
conversationId,
path)) {
QFile::remove(path);
}
}
[[nodiscard]] std::optional<QString> PrepareStagedProtectedFile(
std::uint64_t telegramUserIdBinding,
ConversationId conversationId) {
const auto id = RandomId<ObjectId>();
if (!id) {
return std::nullopt;
}
const auto directory = StagedProtectedFileDirectory(
telegramUserIdBinding,
conversationId);
if (!QDir().mkpath(directory)) {
return std::nullopt;
}
const auto encoded = QByteArray(
reinterpret_cast<const char*>(id->bytes.data()),
int(id->bytes.size())).toHex();
return directory
+ u"file-"_q
+ QString::fromLatin1(encoded)
+ u".source"_q;
}
void CleanupStagedProtectedSources(
const QString &conversationDirectory,
const QString &keepPath) {
const auto keep = keepPath.isEmpty()
? QString()
: QDir::cleanPath(QFileInfo(keepPath).absoluteFilePath());
for (const auto &name : { u"staged-files"_q, u"staged-images"_q }) {
auto directory = QDir(conversationDirectory + name + u"/"_q);
if (!directory.exists()) {
continue;
}
const auto entries = directory.entryInfoList(
QDir::Files
| QDir::Hidden
| QDir::System
| QDir::NoDotAndDotDot);
for (const auto &entry : entries) {
if (keep.isEmpty()
|| QDir::cleanPath(entry.absoluteFilePath()) != keep) {
QFile::remove(entry.absoluteFilePath());
}
}
if (directory.entryList(
QDir::AllEntries | QDir::NoDotAndDotDot).isEmpty()) {
QDir(conversationDirectory).rmdir(name);
}
}
}
[[nodiscard]] std::optional<QString> StageProtectedImage(
std::uint64_t telegramUserIdBinding,
ConversationId conversationId,
const QImage &image) {
const auto id = RandomId<ObjectId>();
if (!id || image.isNull() || !image.size().isValid()) {
return std::nullopt;
}
const auto directory = StagedProtectedImageDirectory(
telegramUserIdBinding,
conversationId);
if (!QDir().mkpath(directory)) {
return std::nullopt;
}
const auto encoded = QByteArray(
reinterpret_cast<const char*>(id->bytes.data()),
int(id->bytes.size())).toHex();
const auto path = directory
+ u"image-"_q
+ QString::fromLatin1(encoded)
+ u".png"_q;
auto output = QSaveFile(path);
if (!output.open(QIODevice::WriteOnly)) {
return std::nullopt;
}
output.setPermissions(QFileDevice::ReadOwner | QFileDevice::WriteOwner);
if (!image.save(&output, "PNG") || !output.commit()) {
output.cancelWriting();
return std::nullopt;
}
return path;
}
bool DiscardUncommittedConversationDirectory(const QString &directory) {
auto target = QDir(directory);
return !target.exists() || target.removeRecursively();
}
[[nodiscard]] QString ConversationSetupMarker(const QString &directory) {
return directory + u"setup.pending"_q;
}
[[nodiscard]] bool BeginConversationSetup(const QString &directory) {
if (!QDir().mkpath(directory)) {
return false;
}
auto file = QSaveFile(ConversationSetupMarker(directory));
if (!file.open(QIODevice::WriteOnly)) {
return false;
}
file.setPermissions(QFileDevice::ReadOwner | QFileDevice::WriteOwner);
const auto marker = QByteArray("TDE2ESETUP1");
if (file.write(marker) != marker.size()) {
file.cancelWriting();
return false;
}
return file.commit();
}
[[nodiscard]] bool FinishConversationSetup(const QString &directory) {
const auto marker = ConversationSetupMarker(directory);
return !QFileInfo::exists(marker) || QFile::remove(marker);
}
[[nodiscard]] bool ConversationSetupPending(const QString &directory) {
return QFileInfo::exists(ConversationSetupMarker(directory));
}
[[nodiscard]] bool IsProtectedGroupPeerBinding(
not_null<Main::Session*> session,
std::uint64_t telegramPeerIdBinding) {
if (!telegramPeerIdBinding) {
return false;
}
const auto peerId = PeerId(telegramPeerIdBinding);
if (!peerIsChat(peerId) && !peerIsChannel(peerId)) {
return false;
}
const auto peer = session->data().peerLoaded(peerId);
return !peer || peer->isChat() || peer->isMegagroup();
}
[[nodiscard]] bool BootstrapMatchesConversation(
const VerifiedPublicGroupBootstrap &verified,
const CloudVaultConversation &conversation) {
return verified.genesis.conversationId == conversation.conversationId
&& verified.genesis.telegramPeerIdBinding
== conversation.telegramPeerIdBinding
&& verified.genesis.ownerAccountId == conversation.ownerAccountId
&& verified.checkpoint.conversationId
== conversation.conversationId
&& PublicGroupBootstrapCanReachCheckpoint(
verified,
conversation.checkpoint);
}
[[nodiscard]] bool ValidConversationCheckpoint(
const Checkpoint &checkpoint,
ConversationId conversationId) {
return checkpoint.conversationId == conversationId
&& checkpoint.generation
&& checkpoint.stateHash;
}
[[nodiscard]] QString AccountDirectory(
std::uint64_t telegramUserIdBinding) {
return cWorkingDir()
+ u"tdata/e2e_cloud/"_q
+ QString::number(telegramUserIdBinding)
+ u"/"_q;
}
[[nodiscard]] QString CloudVaultAnchorPath(
std::uint64_t telegramUserIdBinding) {
return AccountDirectory(telegramUserIdBinding) + u"vault.anchor"_q;
}
[[nodiscard]] std::optional<ConversationId> DecodeConversationDirectory(
const QString &name) {
const auto encoded = name.toLatin1();
if (encoded.size() != 64
|| !std::all_of(
encoded.begin(),
encoded.end(),
[](char value) {
return std::isxdigit(static_cast<unsigned char>(value));
})) {
return std::nullopt;
}
const auto bytes = QByteArray::fromHex(encoded);
if (bytes.size() != 32) {
return std::nullopt;
}
auto result = ConversationId();
std::copy_n(
reinterpret_cast<const std::uint8_t*>(bytes.constData()),
result.bytes.size(),
result.bytes.begin());
return result ? std::optional<ConversationId>(result) : std::nullopt;
}
void Cleanse(QByteArray &bytes) {
if (!bytes.isEmpty()) {
OPENSSL_cleanse(bytes.data(), bytes.size());
}
bytes.clear();
}
inline constexpr auto kProtectedFilePreviewSide = 320;
inline constexpr auto kProtectedFilePreviewQuality = 82;
struct ProtectedHistoryFile {
QString filename;
QString mimeType;
std::uint64_t size = 0;
std::optional<PrivateFilePreview> preview;
};
template <typename Id>
[[nodiscard]] QByteArray ProtectedHistoryIdBytes(Id id) {
return QByteArray(
reinterpret_cast<const char*>(id.bytes.data()),
int(id.bytes.size()));
}
[[nodiscard]] DocumentId ProtectedHistoryDocumentId(
ConversationId conversationId,
ObjectId eventObjectId,
const Sha256Provider &sha256) {
auto input = QByteArray("TDE2E/protected-history-document/v1");
input.append(ProtectedHistoryIdBytes(conversationId));
input.append(ProtectedHistoryIdBytes(eventObjectId));
const auto digest = sha256.digest(input);
auto result = std::uint64_t();
for (auto index = 0; index != 8; ++index) {
result = (result << 8) | digest.bytes[index];
}
return result ? result : 1;
}
[[nodiscard]] not_null<DocumentData*> CreateProtectedHistoryDocument(
not_null<Main::Session*> session,
ConversationId conversationId,
ObjectId eventObjectId,
TimeId date,
const ProtectedHistoryFile &file,
const Sha256Provider &sha256) {
auto attributes = QVector<MTPDocumentAttribute>{
MTP_documentAttributeFilename(MTP_string(file.filename)),
};
auto thumbnail = ImageWithLocation();
if (file.preview) {
auto buffer = QBuffer();
buffer.setData(file.preview->jpegBytes);
if (buffer.open(QIODevice::ReadOnly)) {
auto reader = QImageReader(&buffer, "JPG");
const auto size = reader.size();
if (size.isValid()
&& size.width() <= kProtectedFilePreviewSide
&& size.height() <= kProtectedFilePreviewSide) {
auto image = reader.read();
if (!image.isNull()) {
thumbnail = Images::FromImageInMemory(
image,
"JPG",
file.preview->jpegBytes);
}
}
}
if (file.mimeType.startsWith(u"video/"_q)) {
attributes.push_back(MTP_documentAttributeVideo(
MTP_flags(MTPDdocumentAttributeVideo::Flags(0)),
MTP_double(file.preview->durationMilliseconds / 1000.),
MTP_int(file.preview->width),
MTP_int(file.preview->height),
MTPint(),
MTPdouble(),
MTPstring()));
} else {
attributes.push_back(MTP_documentAttributeImageSize(
MTP_int(file.preview->width),
MTP_int(file.preview->height)));
}
}
const auto result = session->data().document(
ProtectedHistoryDocumentId(
conversationId,
eventObjectId,
sha256),
0,
QByteArray(),
date,
attributes,
file.mimeType,
InlineImageLocation(),
thumbnail,
ImageWithLocation(),
false,
0,
file.size);
if (result->loading()) {
result->cancel();
}
result->resetCancelled();
result->status = FileReady;
return result;
}
[[nodiscard]] ObjectId FreshnessResponseObjectId(
ObjectId challengeObjectId,
ClientId witnessClientId,
const Sha256Provider &sha256) {
auto input = QByteArray("TDE2E/freshness-response-object/v1");
input.append(
reinterpret_cast<const char*>(challengeObjectId.bytes.data()),
challengeObjectId.bytes.size());
input.append(
reinterpret_cast<const char*>(witnessClientId.bytes.data()),
witnessClientId.bytes.size());
const auto digest = sha256.digest(input);
auto result = ObjectId();
std::copy(digest.bytes.begin(), digest.bytes.end(), result.bytes.begin());
return result;
}
inline constexpr auto kDesktopFileChunkSize = std::uint32_t(1024 * 1024);
inline constexpr auto kFileDownloadPageBytes = 16 * 1024 * 1024;
inline constexpr auto kFileDownloadExtraObjects = std::uint64_t(1024);
inline constexpr auto kFileDownloadExtraPages = std::uint64_t(1024);
inline constexpr auto kFileDownloadExtraBytes
= std::uint64_t(16) * 1024 * 1024;
inline constexpr auto kFileCleanupChunksPerTurn = std::uint32_t(64);
inline constexpr auto kFileRetryInitialDelay = crl::time(1000);
inline constexpr auto kFileRetryMaximumDelay = crl::time(30'000);
inline constexpr auto kFileDownloadMaximumRetries = 20;
inline constexpr auto kMaximumQueuedFileDownloads = std::size_t(64);
[[nodiscard]] crl::time FileRetryDelay(int attempt) {
const auto shift = std::min(attempt, 5);
return std::min(
kFileRetryMaximumDelay,
kFileRetryInitialDelay * (crl::time(1) << shift));
}
struct HashedFile {
std::uint64_t size = 0;
Digest hash;
std::optional<PrivateFilePreview> preview;
};
[[nodiscard]] std::optional<PrivateFilePreview> PrepareFilePreview(
const QString &path,
const QString &mimeType = QString()) {
const auto mime = mimeType.isEmpty()
? QMimeDatabase().mimeTypeForFile(
path,
QMimeDatabase::MatchExtension).name()
: mimeType;
auto information = FileLoadTask::ReadMediaInformation(
path,
QByteArray(),
mime);
if (!information) {
return std::nullopt;
}
auto image = QImage();
auto duration = crl::time();
if (const auto data = std::get_if<Ui::PreparedFileInformation::Image>(
&information->media)) {
image = data->data;
} else if (const auto data = std::get_if<
Ui::PreparedFileInformation::Video>(&information->media)) {
image = data->thumbnail;
duration = std::max(data->duration, crl::time());
} else if (const auto data = std::get_if<
Ui::PreparedFileInformation::Song>(&information->media)) {
image = data->cover;
duration = std::max(data->duration, crl::time());
}
if (image.isNull() || image.width() <= 0 || image.height() <= 0) {
return std::nullopt;
}
auto dimensions = image.size();
if (dimensions.width() > kMaximumPrivateFilePreviewDimension
|| dimensions.height() > kMaximumPrivateFilePreviewDimension) {
dimensions.scale(
kMaximumPrivateFilePreviewDimension,
kMaximumPrivateFilePreviewDimension,
Qt::KeepAspectRatio);
}
if (!dimensions.isValid() || dimensions.isEmpty()) {
return std::nullopt;
}
if (image.width() > kProtectedFilePreviewSide
|| image.height() > kProtectedFilePreviewSide) {
image = image.scaled(
kProtectedFilePreviewSide,
kProtectedFilePreviewSide,
Qt::KeepAspectRatio,
Qt::SmoothTransformation);
}
auto bytes = QByteArray();
auto buffer = QBuffer(&bytes);
if (!buffer.open(QIODevice::WriteOnly)
|| !image.save(&buffer, "JPG", kProtectedFilePreviewQuality)
|| bytes.isEmpty()
|| bytes.size() > kMaximumPrivateFilePreviewSize) {
return std::nullopt;
}
return PrivateFilePreview{
.width = std::uint32_t(dimensions.width()),
.height = std::uint32_t(dimensions.height()),
.durationMilliseconds = std::uint32_t(std::min<std::uint64_t>(
std::uint64_t(duration),
std::numeric_limits<std::uint32_t>::max())),
.jpegBytes = std::move(bytes),
};
}
[[nodiscard]] std::optional<HashedFile> HashFile(
const QString &path,
std::shared_ptr<std::atomic_bool> cancellation = nullptr,
bool preparePreview = false,
const QString &stagedPath = QString(),
const QString &previewMimeType = QString()) {
auto file = QFile(path);
if (!file.open(QIODevice::ReadOnly)
|| file.size() < 0
|| std::uint64_t(file.size()) > kMaximumProtectedFileSize) {
return std::nullopt;
}
const auto originalSize = file.size();
auto staged = std::unique_ptr<QSaveFile>();
if (!stagedPath.isEmpty()) {
staged = std::make_unique<QSaveFile>(stagedPath);
if (!staged->open(QIODevice::WriteOnly)) {
return std::nullopt;
}
staged->setPermissions(
QFileDevice::ReadOwner | QFileDevice::WriteOwner);
}
const auto context = EVP_MD_CTX_new();
if (!context || EVP_DigestInit_ex(context, EVP_sha256(), nullptr) != 1) {
EVP_MD_CTX_free(context);
if (staged) {
staged->cancelWriting();
}
return std::nullopt;
}
auto processed = qint64();
auto ok = true;
while (processed != originalSize) {
if (cancellation
&& cancellation->load(std::memory_order_relaxed)) {
ok = false;
break;
}
auto bytes = file.read(std::min<qint64>(
kDesktopFileChunkSize,
originalSize - processed));
const auto size = bytes.size();
const auto copied = !staged
|| staged->write(bytes) == bytes.size();
const auto updated = copied
&& !bytes.isEmpty()
&& EVP_DigestUpdate(
context,
bytes.constData(),
bytes.size()) == 1;
Cleanse(bytes);
if (!updated) {
ok = false;
break;
}
processed += size;
}
auto result = HashedFile{
.size = std::uint64_t(originalSize),
.hash = {},
.preview = {},
};
auto hashSize = 0U;
ok = ok
&& (!cancellation
|| !cancellation->load(std::memory_order_relaxed))
&& file.size() == originalSize
&& EVP_DigestFinal_ex(
context,
result.hash.bytes.data(),
&hashSize) == 1
&& hashSize == result.hash.bytes.size();
EVP_MD_CTX_free(context);
auto stagedCommitted = false;
if (ok && staged) {
stagedCommitted = staged->commit();
ok = stagedCommitted;
} else if (staged) {
staged->cancelWriting();
}
if (ok && preparePreview) {
result.preview = PrepareFilePreview(
stagedCommitted ? stagedPath : path,
previewMimeType);
ok = !cancellation
|| !cancellation->load(std::memory_order_relaxed);
}
if (!ok && stagedCommitted) {
QFile::remove(stagedPath);
}
return (ok && result.hash)
? std::optional<HashedFile>(result)
: std::nullopt;
}
} // namespace
struct DesktopService::PendingGroupCreation {
enum class Phase {
Creating,
AwaitingAdmission,
Active,
Removed,
};
struct PendingFileDownload {
struct PendingWrite {
PendingWrite(
std::unique_ptr<QSaveFile> output,
EVP_MD_CTX *digest,
PrivateFileManifest manifest)
: output(std::move(output))
, digest(digest)
, manifest(std::move(manifest)) {
}
~PendingWrite() {
EVP_MD_CTX_free(digest);
}
std::unique_ptr<QSaveFile> output;
EVP_MD_CTX *digest = nullptr;
PrivateFileManifest manifest;
std::uint64_t written = 0;
std::uint32_t nextChunkIndex = 0;
std::uint32_t cleanupChunkIndex = 0;
bool committed = false;
};
ObjectId eventObjectId;
FileId fileId;
QString path;
std::uint64_t plaintextSize = 0;
std::uint32_t chunkSize = 0;
std::uint32_t chunkCount = 0;
std::set<std::uint32_t> missingChunkIndices;
std::function<void(ProtectedFileSaveResult)> callback;
ProtectedFileSaveProgress progress;
std::unique_ptr<PendingWrite> write;
std::uint64_t retryToken = 0;
int retryAttempt = 0;
int retryCount = 0;
FileDownloadCarrierSearch carrierSearch
= FileDownloadCarrierSearch::Current;
};
struct PendingFileDownloadRequest {
ObjectId eventObjectId;
QString path;
std::function<void(ProtectedFileSaveResult)> callback;
ProtectedFileSaveProgress progress;
};
PendingGroupCreation(
QString directory,
LocalRecordKey &&localRecordKey,
not_null<Main::Session*> session,
std::uint64_t telegramPeerIdBinding,
ConversationId conversationId,
const Sha256Provider &sha256)
: conversationId(conversationId)
, telegramPeerIdBinding(telegramPeerIdBinding)
, directory(std::move(directory))
, protector(std::move(localRecordKey))
, metadataBlob(this->directory + u"conversation.meta"_q)
, journalBlob(this->directory + u"bootstrap.wal"_q)
, mlsBlob(this->directory + u"mls.state"_q)
, archiveBlob(this->directory + u"archive.state"_q)
, groupBlob(this->directory + u"group.state"_q)
, outboxBlob(this->directory + u"outbox.state"_q)
, changeJournalBlob(this->directory + u"group-change.wal"_q)
, changeInboxBlob(this->directory + u"group-change-inbox.state"_q)
, keyPackagePoolBlob(this->directory + u"key-packages.state"_q)
, freshnessTrustBlob(this->directory + u"freshness-trust.state"_q)
, inboundJournalBlob(this->directory + u"content-inbound.state"_q)
, controlInboundJournalBlob(
this->directory + u"control-inbound.state"_q)
, contentIndexBlob(this->directory + u"content-index.state"_q)
, controlSyncBlob(this->directory + u"control-sync.state"_q)
, contentSyncBlob(this->directory + u"content-sync.state"_q)
, fileTransferBlob(this->directory + u"file-transfer.state"_q)
, metadata(metadataBlob, protector)
, journal(journalBlob, protector)
, mlsState(mlsBlob, protector)
, archiveState(archiveBlob, protector)
, groupLedger(groupBlob, protector, sha256)
, outbox(outboxBlob, protector)
, changeJournal(changeJournalBlob, protector)
, changeInbox(
changeInboxBlob,
protector,
envelopeCodec,
sha256)
, keyPackages(
keyPackagePoolBlob,
protector,
envelopeCodec,
sha256)
, freshnessTrust(freshnessTrustBlob, protector)
, inboundJournal(inboundJournalBlob, protector)
, controlInboundJournal(
controlInboundJournalBlob,
protector,
InboundJournalDomain::Control)
, contentStore(
conversationId,
this->directory + u"content-records"_q,
contentIndexBlob,
protector,
sha256)
, controlSyncState(
controlSyncBlob,
protector)
, contentSyncState(contentSyncBlob, protector)
, fileTransfer(fileTransferBlob, protector)
, chunkStore(this->directory + u"file-chunks"_q, protector)
, backend(
session,
session->data().history(PeerId(telegramPeerIdBinding)),
telegramPeerIdBinding)
, transport(conversationId, telegramPeerIdBinding, backend)
, controlBackend(
session,
session->data().history(PeerId(telegramPeerIdBinding)),
telegramPeerIdBinding,
ProtectedControlCarrierFilename(),
ProtectedCarrierMimeType(),
ProtectedCarrierMaximumObjectSize())
, controlTransport(
conversationId,
telegramPeerIdBinding,
controlBackend)
, contentBackend(
session,
session->data().history(PeerId(telegramPeerIdBinding)),
telegramPeerIdBinding,
ProtectedContentCarrierFilename(),
ProtectedCarrierMimeType(),
ProtectedCarrierMaximumObjectSize())
, contentTransport(
conversationId,
telegramPeerIdBinding,
contentBackend) {
}
~PendingGroupCreation() {
if (fileHashCancellation) {
fileHashCancellation->store(true, std::memory_order_relaxed);
}
if (fileFinalHashCancellation) {
fileFinalHashCancellation->store(true, std::memory_order_relaxed);
}
}
ConversationId conversationId;
std::uint64_t telegramPeerIdBinding = 0;
QString directory;
AesGcmLocalRecordProtector protector;
FileAtomicBlobStore metadataBlob;
FileAtomicBlobStore journalBlob;
FileAtomicBlobStore mlsBlob;
FileAtomicBlobStore archiveBlob;
FileAtomicBlobStore groupBlob;
FileAtomicBlobStore outboxBlob;
FileAtomicBlobStore changeJournalBlob;
FileAtomicBlobStore changeInboxBlob;
FileAtomicBlobStore keyPackagePoolBlob;
FileAtomicBlobStore freshnessTrustBlob;
FileAtomicBlobStore inboundJournalBlob;
FileAtomicBlobStore controlInboundJournalBlob;
FileAtomicBlobStore contentIndexBlob;
FileAtomicBlobStore controlSyncBlob;
FileAtomicBlobStore contentSyncBlob;
FileAtomicBlobStore fileTransferBlob;
EnvelopeCodecV1 envelopeCodec;
PersistentConversationMetadata metadata;
PersistentGroupBootstrapJournal journal;
PersistentMlsStateStore mlsState;
PersistentArchiveState archiveState;
PersistentGroupLedger groupLedger;
PersistentOutboxStore outbox;
PersistentGroupChangeJournal changeJournal;
PersistentGroupChangeInbox changeInbox;
PersistentKeyPackagePool keyPackages;
PersistentFreshnessTrust freshnessTrust;
PersistentInboundJournal inboundJournal;
PersistentInboundJournal controlInboundJournal;
PersistentContentStore contentStore;
PersistentControlObservationState controlSyncState;
PersistentContentSyncState contentSyncState;
PersistentFileTransfer fileTransfer;
FileChunkFileStore chunkStore;
std::unique_ptr<FreshnessGate> freshnessGate;
std::unique_ptr<OpenMlsApplicationEngine> applicationEngine;
std::unique_ptr<OutboxCoordinator> outboxCoordinator;
std::unique_ptr<OutboxUploadController> uploadController;
TelegramSessionCarrierBackend backend;
TelegramCarrierTransport transport;
TelegramSessionCarrierBackend controlBackend;
TelegramCarrierTransport controlTransport;
TelegramSessionCarrierBackend contentBackend;
TelegramCarrierTransport contentTransport;
CloudVaultConversation conversation;
Checkpoint joinTargetCheckpoint;
std::optional<PreparedCloudVaultUpdate> vaultUpdate;
std::unique_ptr<PublicBootstrapSyncController> observation;
std::unique_ptr<ObservedContentSyncController> contentObservation;
std::unique_ptr<TelegramSessionCarrierBackend> fileDownloadBackend;
std::unique_ptr<TelegramCarrierTransport> fileDownloadTransport;
std::unique_ptr<FileChunkDownloadController> fileDownloadController;
std::optional<PendingFileDownload> pendingFileDownload;
std::deque<PendingFileDownloadRequest> fileDownloadQueue;
QString filePreparationPath;
QString filePreparationFilename;
QString filePreparationMimeType;
std::optional<HashedFile> filePreparationSource;
std::set<AccountId> safetyWitnesses;
struct MaterializedHistoryEntry {
ObjectId eventObjectId;
ObjectId latestMutationEventObjectId;
FullMsgId fullId;
};
std::vector<MaterializedHistoryEntry> materializedHistory;
std::map<ObjectId, QString> localProtectedFilePaths;
std::uint64_t materializedHistoryPeerIdBinding = 0;
std::size_t materializedHistoryLimit = 0;
std::uint64_t safetyWitnessGeneration = 0;
std::uint64_t groupObservationRetryToken = 0;
std::uint64_t fileRetryToken = 0;
bool ownSafetyGossipObserved = false;
bool observationDirty = false;
bool contentObservationDirty = false;
bool vaultPreflightRequired = true;
bool uploadInProgress = false;
bool fileHashInProgress = false;
bool fileHashCancelRequested = false;
bool fileFinalHashInProgress = false;
bool queuedContentReconciled = false;
int groupObservationRetryAttempt = 0;
int fileRetryAttempt = 0;
std::shared_ptr<std::atomic_bool> fileHashCancellation;
std::shared_ptr<std::atomic_bool> fileFinalHashCancellation;
Phase phase = Phase::Creating;
};
struct DesktopService::PendingGroupJoin {
PendingGroupJoin(
not_null<Main::Session*> session,
CloudVaultConversation conversation)
: conversation(conversation)
, backend(
session,
session->data().history(PeerId(
conversation.telegramPeerIdBinding)),
conversation.telegramPeerIdBinding)
, transport(
conversation.conversationId,
conversation.telegramPeerIdBinding,
backend)
, controlBackend(
session,
session->data().history(PeerId(
conversation.telegramPeerIdBinding)),
conversation.telegramPeerIdBinding,
ProtectedControlCarrierFilename(),
ProtectedCarrierMimeType(),
ProtectedCarrierMaximumObjectSize())
, controlTransport(
conversation.conversationId,
conversation.telegramPeerIdBinding,
controlBackend) {
}
CloudVaultConversation conversation;
EnvelopeCodecV1 envelopeCodec;
TelegramSessionCarrierBackend backend;
TelegramCarrierTransport transport;
TelegramSessionCarrierBackend controlBackend;
TelegramCarrierTransport controlTransport;
std::unique_ptr<PublicBootstrapSyncController> sync;
};
struct DesktopService::PendingGroupDiscovery {
PendingGroupDiscovery(
not_null<Main::Session*> session,
std::uint64_t telegramPeerIdBinding)
: telegramPeerIdBinding(telegramPeerIdBinding)
, backend(
session,
session->data().history(PeerId(telegramPeerIdBinding)),
telegramPeerIdBinding,
ProtectedControlCarrierFilename(),
ProtectedCarrierMimeType(),
ProtectedCarrierMaximumObjectSize()) {
}
std::uint64_t telegramPeerIdBinding = 0;
EnvelopeCodecV1 envelopeCodec;
TelegramSessionCarrierBackend backend;
std::unique_ptr<PublicBootstrapDiscoveryController> sync;
};
enum class DesktopService::LocalGroupRecoveryResult {
Missing,
Complete,
Restored,
Invalid,
};
enum class DesktopService::FileTransferCancellationResult {
Finished,
RetryableCleanupFailure,
Failure,
};
enum class DesktopService::QueuedContentRecoveryResult {
Ready,
Recovered,
PersistenceFailed,
Invalid,
};
DesktopService::DesktopService(not_null<Main::Session*> session)
: _session(session)
, _telegramUserIdBinding(session->userId().bare)
, _telegramSelfPeerId(session->userPeerId().value)
, _vaultCodec(_passwordKdf, _sha256)
, _vaultSelector(_vaultCodec, _sha256)
, _vaultAnchorBlob(CloudVaultAnchorPath(_telegramUserIdBinding))
, _vaultAnchor(_vaultAnchorBlob, _telegramUserIdBinding)
, _backend(std::make_unique<TelegramSessionCarrierBackend>(
_session,
_session->data().history(_session->userPeerId()),
_telegramSelfPeerId,
CloudVaultCarrierFilename(),
CloudVaultCarrierMimeType(),
CloudVaultMaximumCarrierSize()))
, _remote(std::make_unique<TelegramCloudVaultTransport>(
_telegramSelfPeerId,
*_backend)) {
CleanupProtectedMediaCache(_session);
auto protectedPeers = DecodeProtectedPeerMarkers(
_session->local().readPref<QByteArray>(kProtectedPeersPref),
_telegramUserIdBinding);
if (protectedPeers) {
_presentationProtectedPeers = std::move(*protectedPeers);
} else {
_presentationProtectedPeersValid = false;
}
const auto anchorLoad = _vaultAnchor.load();
if (anchorLoad == CloudVaultAnchorLoadResult::StorageError
|| anchorLoad == CloudVaultAnchorLoadResult::InvalidSnapshot) {
_vaultState = DesktopVaultState::SecurityBlocked;
}
_session->data().newItemAdded(
) | rpl::on_next([weak = base::weak_ptr(this)](
not_null<HistoryItem*> item) {
if (weak) {
weak->handleNewTelegramItem(item);
}
}, _lifetime);
Core::App().passcodeLockValue(
) | rpl::on_next([weak = base::weak_ptr(this)](bool locked) {
if (locked && weak) {
weak->lock();
}
}, _lifetime);
_vaultState.value(
) | rpl::on_next([weak = base::weak_ptr(this)](DesktopVaultState state) {
if (state == DesktopVaultState::SecurityBlocked && weak) {
weak->scheduleSecurityLock();
}
}, _lifetime);
}
DesktopService::~DesktopService() {
CleanupProtectedMediaCache(_session);
if (_pendingGroupCreation) {
clearMaterializedProtectedHistory(*_pendingGroupCreation);
}
for (const auto &entry : _groups) {
clearMaterializedProtectedHistory(*entry.second);
if (!entry.second->fileTransfer.pending()) {
RemoveStagedProtectedSource(
_telegramUserIdBinding,
entry.first,
entry.second->filePreparationPath);
}
}
Cleanse(_pendingUnlockPassword);
Cleanse(_unlockedPassword);
}
DesktopVaultState DesktopService::vaultState() const {
return _vaultState.current();
}
rpl::producer<DesktopVaultState> DesktopService::vaultStateValue() const {
return _vaultState.value();
}
const UnlockedCloudVault *DesktopService::vault() const {
return vaultReady() ? &*_vault : nullptr;
}
bool DesktopService::vaultReady() const {
return _vaultState.current() == DesktopVaultState::Ready && bool(_vault);
}
bool DesktopService::hasProtectedRuntimeState() const {
return _vault
|| _sync
|| _pendingCreation
|| _pendingGroupCreation
|| _pendingGroupJoin
|| _pendingGroupDiscovery
|| !_groups.empty()
|| !_pendingUnlockPassword.isEmpty()
|| !_unlockedPassword.isEmpty();
}
void DesktopService::scheduleSecurityLock() {
if (_securityLockScheduled || !hasProtectedRuntimeState()) {
return;
}
_securityLockScheduled = true;
crl::on_main([weak = base::weak_ptr(this)] {
if (!weak) {
return;
}
weak->_securityLockScheduled = false;
if (weak->_vaultState.current() == DesktopVaultState::SecurityBlocked
&& weak->hasProtectedRuntimeState()) {
weak->lock();
}
});
}
void DesktopService::ensureVaultDiscovery() {
const auto state = _vaultState.current();
if (_sync
|| !_vaultAnchor.loaded()
|| (state != DesktopVaultState::Uninitialized
&& state != DesktopVaultState::Missing
&& state != DesktopVaultState::DiscoveryRetryableError
&& state != DesktopVaultState::DiscoveryPermanentError)) {
return;
}
_vaultState = DesktopVaultState::Discovering;
_sync = std::make_unique<CloudVaultSyncController>(
_telegramUserIdBinding,
*_remote,
_vaultSelector,
[weak = base::weak_ptr(this)](CloudVaultSyncCompletion result) {
if (weak) {
weak->applyVaultDiscoveryResult(std::move(result));
}
},
SelectCloudVaultOffMain);
if (!_sync->startDiscovery()) {
_sync.reset();
_vaultState = DesktopVaultState::DiscoveryRetryableError;
}
}
bool DesktopService::unlock(QByteArray password) {
const auto state = _vaultState.current();
if (_sync
|| _pendingCreation
|| !_vaultAnchor.loaded()
|| (state != DesktopVaultState::Locked
&& state != DesktopVaultState::WrongPasswordOrDamaged
&& state != DesktopVaultState::RetryableTransportError
&& state != DesktopVaultState::PermanentTransportError)
|| password.isEmpty()) {
return false;
}
Cleanse(_pendingUnlockPassword);
_pendingUnlockPassword = password;
_vault.reset();
_vaultState = DesktopVaultState::Loading;
_sync = std::make_unique<CloudVaultSyncController>(
_telegramUserIdBinding,
*_remote,
_vaultSelector,
[weak = base::weak_ptr(this)](CloudVaultSyncCompletion result) {
if (weak) {
weak->applySyncResult(std::move(result));
}
},
SelectCloudVaultOffMain);
if (!_sync->start(std::move(password), _vaultAnchor.anchor())) {
_sync.reset();
Cleanse(_pendingUnlockPassword);
_vaultState = DesktopVaultState::Locked;
return false;
}
return true;
}
bool DesktopService::createVault(QByteArray password) {
if (_sync
|| _pendingCreation
|| _vault
|| _vaultAnchor.anchor()
|| _vaultState.current() != DesktopVaultState::Missing
|| password.isEmpty()) {
return false;
}
Cleanse(_pendingUnlockPassword);
_pendingUnlockPassword = password;
Cleanse(password);
_vaultState = DesktopVaultState::Creating;
_sync = std::make_unique<CloudVaultSyncController>(
_telegramUserIdBinding,
*_remote,
_vaultSelector,
[weak = base::weak_ptr(this)](CloudVaultSyncCompletion result) {
if (weak) {
weak->applyVaultCreationDiscoveryResult(
std::move(result));
}
},
SelectCloudVaultOffMain);
const auto started = _sync->startDiscovery();
if (!started) {
_sync.reset();
Cleanse(_pendingUnlockPassword);
_vaultState = DesktopVaultState::DiscoveryRetryableError;
}
return started;
}
bool DesktopService::retryCreateVault() {
if (!_pendingCreation
|| _vaultState.current()
!= DesktopVaultState::RetryableTransportError) {
return false;
}
uploadPendingCreation();
return true;
}
DesktopGroupCreationState DesktopService::groupCreationState() const {
return _groupCreationState.current();
}
rpl::producer<DesktopGroupCreationState>
DesktopService::groupCreationStateValue() const {
return _groupCreationState.value();
}
bool DesktopService::createProtectedGroup(
not_null<PeerData*> peer,
HistoryAccess defaultHistoryAccess) {
const auto operationState = _groupCreationState.current();
if (!_vault
|| _vaultState.current() != DesktopVaultState::Ready
|| (operationState != DesktopGroupCreationState::Idle
&& operationState != DesktopGroupCreationState::Ready)
|| _pendingGroupCreation
|| _pendingGroupJoin
|| _pendingGroupDiscovery
|| (!peer->isChat() && !peer->isMegagroup())
|| std::any_of(
begin(_vault->conversations),
end(_vault->conversations),
[&](const CloudVaultConversation &conversation) {
return conversation.telegramPeerIdBinding == peer->id.value;
})
|| !IsValidHistoryAccess(defaultHistoryAccess)) {
return false;
}
_groupCreationState = DesktopGroupCreationState::Preparing;
const auto conversationId = RandomId<ConversationId>();
const auto clientId = RandomId<ClientId>();
const auto genesisObjectId = RandomId<ObjectId>();
const auto credentialObjectId = RandomId<ObjectId>();
const auto mlsPublicObjectId = RandomId<ObjectId>();
const auto archiveActivationEventId = RandomId<ObjectId>();
const auto ownerHistoryGrantObjectId = RandomId<ObjectId>();
const auto archiveKey = ArchiveEpochCrypto().generateKey();
if (!conversationId
|| !clientId
|| !genesisObjectId
|| !credentialObjectId
|| !mlsPublicObjectId
|| !archiveActivationEventId
|| !ownerHistoryGrantObjectId
|| !archiveKey) {
_groupCreationState = DesktopGroupCreationState::LocalFailure;
return false;
}
auto localRecordKey = DeriveConversationLocalRecordKey(
_vault->masterKey,
_telegramUserIdBinding,
*conversationId);
if (!localRecordKey) {
_groupCreationState = DesktopGroupCreationState::LocalFailure;
return false;
}
auto operation = std::make_unique<PendingGroupCreation>(
ConversationDirectory(_telegramUserIdBinding, *conversationId),
std::move(*localRecordKey),
_session,
peer->id.value,
*conversationId,
_sha256);
if (operation->metadata.load(*conversationId)
!= ConversationMetadataLoadResult::Missing
|| operation->journal.load(*conversationId)
!= GroupBootstrapJournalLoadResult::Empty
|| operation->mlsState.load(*conversationId)
!= MlsStateLoadResult::Missing
|| operation->archiveState.load(*conversationId)
!= ArchiveStateLoadResult::Missing
|| operation->groupLedger.load(*conversationId)
!= GroupLedgerLoadResult::Missing
|| operation->outbox.load()
!= PersistentOutboxLoadResult::Empty
|| operation->changeJournal.load(*conversationId)
!= GroupChangeJournalLoadResult::Empty
|| operation->changeInbox.load(*conversationId)
!= GroupChangeInboxLoadResult::Missing
|| operation->keyPackages.load(*conversationId, peer->id.value)
!= KeyPackagePoolLoadResult::Empty
|| operation->freshnessTrust.load(*conversationId)
!= FreshnessTrustLoadResult::Missing
|| operation->inboundJournal.load()
!= InboundJournalLoadResult::Missing
|| operation->controlInboundJournal.load()
!= InboundJournalLoadResult::Missing
|| operation->contentStore.load()
!= ContentStoreLoadResult::Missing
|| operation->controlSyncState.load(*conversationId)
!= ControlObservationStateLoadResult::Missing
|| operation->contentSyncState.load(*conversationId)
!= ContentSyncStateLoadResult::Missing
|| operation->fileTransfer.load(*conversationId)
!= FileTransferLoadResult::Empty) {
_groupCreationState = DesktopGroupCreationState::LocalFailure;
return false;
}
CleanupStagedProtectedSources(operation->directory, QString());
auto bootstrap = PrepareProtectedGroupBootstrap({
.conversationId = *conversationId,
.telegramPeerIdBinding = peer->id.value,
.ownerTelegramUserIdBinding = _telegramUserIdBinding,
.ownerClientId = *clientId,
.policy = { .defaultHistoryAccess = defaultHistoryAccess },
.genesisObjectId = *genesisObjectId,
.ownerCredentialObjectId = *credentialObjectId,
.initialMlsPublicObjectId = *mlsPublicObjectId,
.archiveActivationEventId = *archiveActivationEventId,
.ownerHistoryGrantObjectId = *ownerHistoryGrantObjectId,
.ownerIdentity = &_vault->identity,
.initialArchiveKey = &*archiveKey,
},
OpenMlsBridge(),
MlsContextCodecV1(),
MlsRosterCodecV1(),
EnvelopeCodecV1(),
_sha256);
if (!bootstrap.prepared) {
_groupCreationState = DesktopGroupCreationState::LocalFailure;
return false;
}
const auto conversation = CloudVaultConversation{
.conversationId = *conversationId,
.telegramPeerIdBinding = peer->id.value,
.checkpoint = bootstrap.prepared->checkpoint,
.ownerAccountId = bootstrap.prepared->ownerAccountId,
};
const auto envelopeCodec = EnvelopeCodecV1();
if (!BeginConversationSetup(operation->directory)) {
DiscardUncommittedConversationDirectory(operation->directory);
_groupCreationState = DesktopGroupCreationState::LocalFailure;
return false;
}
auto coordinator = GroupBootstrapTransactionCoordinator(
operation->journal,
operation->mlsState,
operation->archiveState,
operation->groupLedger,
operation->outbox,
envelopeCodec,
_sha256);
auto transaction = MakeGroupBootstrapTransaction(
std::move(*bootstrap.prepared),
_vault->identity.credential,
operation->outbox.revision());
if (coordinator.apply(std::move(transaction))
!= GroupBootstrapApplyStatus::Applied) {
DiscardUncommittedConversationDirectory(operation->directory);
_groupCreationState = DesktopGroupCreationState::LocalFailure;
return false;
}
if (operation->freshnessTrust.initialize(true)
!= FreshnessTrustCommitResult::Committed
|| operation->metadata.initialize({
.conversationId = *conversationId,
.telegramPeerIdBinding = peer->id.value,
.accountId = conversation.ownerAccountId,
.clientId = *clientId,
}) != ConversationMetadataCommitResult::Committed) {
DiscardUncommittedConversationDirectory(operation->directory);
_groupCreationState = DesktopGroupCreationState::LocalFailure;
return false;
}
(void)FinishConversationSetup(operation->directory);
rememberProtectedPeerForPresentation(peer->id.value);
operation->conversation = conversation;
operation->freshnessGate = std::make_unique<FreshnessGate>(
conversation.checkpoint,
true);
_pendingGroupCreation = std::move(operation);
publishNextBootstrapObject();
return true;
}
bool DesktopService::retryProtectedGroupCreation() {
const auto state = _groupCreationState.current();
if (!vaultReady()
|| (state != DesktopGroupCreationState::RetryableTransportError
&& state != DesktopGroupCreationState::PermanentTransportError)
|| _pendingGroupJoin) {
return false;
}
if (!_pendingGroupCreation) {
auto admissions = std::vector<ConversationId>();
for (const auto &[conversationId, group] : _groups) {
if (group->phase
== PendingGroupCreation::Phase::AwaitingAdmission) {
admissions.push_back(conversationId);
}
}
if (!admissions.empty()) {
_groupCreationState
= DesktopGroupCreationState::AwaitingAdmission;
}
auto restartedAdmission = false;
auto admissionRestartFailed = false;
for (const auto conversationId : admissions) {
if (_pendingGroupCreation
|| _pendingGroupJoin
|| _pendingGroupDiscovery) {
return true;
}
auto i = _groups.find(conversationId);
if (i == end(_groups)
|| i->second->phase
!= PendingGroupCreation::Phase::AwaitingAdmission) {
continue;
}
if (!i->second->observation) {
beginGroupObservation(conversationId);
}
if (_pendingGroupCreation
|| _pendingGroupJoin
|| _pendingGroupDiscovery) {
return true;
} else if (!vaultReady()
|| _groupCreationState.current()
!= DesktopGroupCreationState::AwaitingAdmission) {
return false;
}
i = _groups.find(conversationId);
const auto stillAwaiting = i != end(_groups)
&& i->second->phase
== PendingGroupCreation::Phase::AwaitingAdmission;
const auto restarted = stillAwaiting
&& bool(i->second->observation);
restartedAdmission = restartedAdmission || restarted;
admissionRestartFailed = admissionRestartFailed
|| (stillAwaiting && !restarted);
}
if (!admissions.empty()) {
const auto stillAwaiting = std::any_of(
begin(_groups),
end(_groups),
[](const auto &entry) {
return entry.second->phase
== PendingGroupCreation::Phase::AwaitingAdmission;
});
if (stillAwaiting && admissionRestartFailed) {
_groupCreationState
= DesktopGroupCreationState::RetryableTransportError;
return false;
}
return restartedAdmission || !stillAwaiting;
}
_groupCreationState = DesktopGroupCreationState::Ready;
resumePendingGroupCreation();
return true;
} else if (_pendingGroupCreation->uploadInProgress) {
return false;
}
if (_pendingGroupCreation->vaultUpdate) {
uploadPendingConversationIndex();
} else {
publishNextBootstrapObject();
}
return true;
}
std::vector<DesktopProtectedGroupSummary>
DesktopService::protectedGroups() const {
auto result = std::vector<DesktopProtectedGroupSummary>();
if (!vaultReady()) {
return result;
}
result.reserve(_groups.size());
for (const auto &[conversationId, group] : _groups) {
const auto state = group->groupLedger.state();
const auto removed = group->phase
== PendingGroupCreation::Phase::Removed;
result.push_back({
.conversationId = conversationId,
.telegramPeerIdBinding = group->telegramPeerIdBinding,
.title = _session->data().peer(
PeerId(group->telegramPeerIdBinding))->name(),
.generation = state ? state->generation() : 0,
.contentCount = group->contentStore.size(),
.active = group->phase == PendingGroupCreation::Phase::Active,
.removed = removed,
.sendingAllowed = !removed
&& group->freshnessGate
&& group->freshnessGate->sendingAllowed(),
.fileTransferPending = group->fileHashInProgress
|| !group->filePreparationPath.isEmpty()
|| bool(group->fileTransfer.pending()),
});
}
std::sort(
begin(result),
end(result),
[](const auto &a, const auto &b) {
return a.title.compare(b.title, Qt::CaseInsensitive) < 0;
});
return result;
}
std::optional<ConversationId> DesktopService::protectedConversationForPeer(
std::uint64_t telegramPeerIdBinding) const {
if (!vaultReady() || !telegramPeerIdBinding) {
return std::nullopt;
}
auto result = std::optional<ConversationId>();
const auto consider = [&](
ConversationId conversationId,
std::uint64_t peerId) {
if (peerId != telegramPeerIdBinding) {
return true;
} else if (result && *result != conversationId) {
return false;
}
result = conversationId;
return true;
};
for (const auto &[conversationId, group] : _groups) {
if (!consider(conversationId, group->telegramPeerIdBinding)) {
return std::nullopt;
}
}
if (_vault) {
for (const auto &conversation : _vault->conversations) {
if (!consider(
conversation.conversationId,
conversation.telegramPeerIdBinding)) {
return std::nullopt;
}
}
}
if (_pendingGroupCreation
&& !consider(
_pendingGroupCreation->conversation.conversationId,
_pendingGroupCreation->telegramPeerIdBinding)) {
return std::nullopt;
}
return result;
}
bool DesktopService::isProtectedPeerForPresentation(
std::uint64_t telegramPeerIdBinding,
std::uint64_t linkedTelegramPeerIdBinding) {
if (!telegramPeerIdBinding) {
return false;
}
const auto known = [&](std::uint64_t peerId) {
return peerId
&& (!_presentationProtectedPeersValid
|| _presentationProtectedPeers.contains(peerId)
|| protectedConversationForPeer(peerId).has_value());
};
if (known(telegramPeerIdBinding)) {
return true;
} else if (!known(linkedTelegramPeerIdBinding)) {
return false;
}
rememberProtectedPeerForPresentation(telegramPeerIdBinding);
return true;
}
void DesktopService::rememberProtectedPeerForPresentation(
std::uint64_t telegramPeerIdBinding) {
if (!telegramPeerIdBinding
|| !_presentationProtectedPeersValid
|| _presentationProtectedPeers.contains(telegramPeerIdBinding)) {
return;
}
if (_presentationProtectedPeers.size()
>= kMaximumProtectedPeerMarkers) {
_presentationProtectedPeersValid = false;
_session->local().writePrefNow<QByteArray>(
kProtectedPeersPref,
QByteArray("invalid"));
return;
}
_presentationProtectedPeers.emplace(telegramPeerIdBinding);
_session->local().writePrefNow<QByteArray>(
kProtectedPeersPref,
EncodeProtectedPeerMarkers(
_telegramUserIdBinding,
_presentationProtectedPeers));
_session->data().notifyHistoryChangeDelayed(
_session->data().history(PeerId(telegramPeerIdBinding)));
}
std::vector<ProtectedContentRecord> DesktopService::protectedContent(
ConversationId conversationId,
std::size_t offset,
std::size_t limit,
std::optional<ObjectKind> kind) const {
if (!vaultReady()) {
return {};
}
const auto i = _groups.find(conversationId);
return (i != end(_groups))
? i->second->contentStore.records(offset, limit, kind)
: std::vector<ProtectedContentRecord>();
}
std::size_t DesktopService::protectedContentCount(
ConversationId conversationId,
std::optional<ObjectKind> kind) const {
if (!vaultReady()) {
return 0;
}
const auto i = _groups.find(conversationId);
return (i != end(_groups)) ? i->second->contentStore.size(kind) : 0;
}
void DesktopService::materializeProtectedHistory(
ConversationId conversationId,
std::uint64_t telegramPeerIdBinding,
std::size_t limit) {
const auto i = _groups.find(conversationId);
if (!vaultReady()
|| i == end(_groups)
|| !telegramPeerIdBinding
|| !limit) {
return;
}
auto &group = *i->second;
if (group.materializedHistoryPeerIdBinding != telegramPeerIdBinding) {
clearMaterializedProtectedHistory(group);
group.materializedHistoryPeerIdBinding = telegramPeerIdBinding;
}
group.materializedHistoryLimit = std::max(
group.materializedHistoryLimit,
limit);
refreshMaterializedProtectedHistory(group);
}
void DesktopService::clearMaterializedProtectedHistory(
PendingGroupCreation &group) {
for (const auto &entry : group.materializedHistory) {
const auto item = _session->data().message(entry.fullId);
if (item && item->isE2ECloudDecrypted()) {
item->history()->destroyMessage(item);
}
}
group.materializedHistory.clear();
}
void DesktopService::refreshMaterializedProtectedHistory(
PendingGroupCreation &group) {
if (!group.materializedHistoryLimit) {
return;
}
const auto total = group.contentStore.size();
struct RenderedRecord {
ObjectId eventObjectId;
AccountId senderAccountId;
std::uint64_t unixTime = 0;
std::uint64_t editUnixTime = 0;
QString text;
std::optional<ProtectedHistoryFile> file;
ObjectId latestMutationEventObjectId;
bool deleted = false;
};
struct RenderedMutation {
ObjectId eventObjectId;
AccountId senderAccountId;
ProtectedMessageAction action = ProtectedMessageAction::Edit;
std::uint64_t unixTime = 0;
QString text;
};
auto rendered = std::vector<RenderedRecord>();
rendered.reserve(group.materializedHistoryLimit);
auto mutationsByTarget = std::map<
ObjectId,
std::vector<RenderedMutation>>();
const auto applyMutations = [&](RenderedRecord &entry) {
const auto i = mutationsByTarget.find(entry.eventObjectId);
if (i == end(mutationsByTarget)) {
return;
}
const auto deletion = std::find_if(
begin(i->second),
end(i->second),
[&](const RenderedMutation &mutation) {
return mutation.senderAccountId == entry.senderAccountId
&& mutation.action
== ProtectedMessageAction::Delete;
});
if (deletion != end(i->second)) {
entry.deleted = true;
entry.latestMutationEventObjectId = deletion->eventObjectId;
} else if (!entry.file) {
const auto edit = std::find_if(
begin(i->second),
end(i->second),
[&](const RenderedMutation &mutation) {
return mutation.senderAccountId == entry.senderAccountId
&& mutation.action
== ProtectedMessageAction::Edit;
});
if (edit != end(i->second)) {
entry.text = u"🔒 "_q + edit->text;
entry.editUnixTime = edit->unixTime;
entry.latestMutationEventObjectId = edit->eventObjectId;
}
}
mutationsByTarget.erase(i);
};
const auto processRecords = [&](
std::vector<ProtectedContentRecord> &records) {
for (auto i = records.rbegin(); i != records.rend(); ++i) {
const auto &record = *i;
if (record.objectKind == ObjectKind::EncryptedMessageBody) {
auto body = ProtectedMessageBodyCodecV1().decodePlaintext(
record.plaintext);
if (!body) {
continue;
}
const auto text = QString::fromUtf8(body->textUtf8);
Cleanse(body->textUtf8);
if (body->action != ProtectedMessageAction::Create) {
mutationsByTarget[body->targetEventObjectId].push_back({
.eventObjectId = record.eventObjectId,
.senderAccountId = record.senderAccountId,
.action = body->action,
.unixTime = record.unixTime,
.text = text,
});
continue;
}
auto entry = RenderedRecord{
.eventObjectId = record.eventObjectId,
.senderAccountId = record.senderAccountId,
.unixTime = record.unixTime,
.text = u"🔒 "_q + text,
};
applyMutations(entry);
if (!entry.deleted) {
rendered.push_back(std::move(entry));
}
} else if (record.objectKind
== ObjectKind::EncryptedFileManifest) {
const auto manifest = PrivateFileManifestCodecV1()
.decodePlaintext(record.plaintext);
if (!manifest) {
continue;
}
auto entry = RenderedRecord{
.eventObjectId = record.eventObjectId,
.senderAccountId = record.senderAccountId,
.unixTime = record.unixTime,
.text = u"🔒"_q,
.file = ProtectedHistoryFile{
.filename = QString::fromUtf8(
manifest->filenameUtf8),
.mimeType = QString::fromUtf8(
manifest->mimeTypeUtf8),
.size = manifest->context.plaintextSize,
.preview = manifest->preview,
},
};
applyMutations(entry);
if (!entry.deleted) {
rendered.push_back(std::move(entry));
}
}
if (rendered.size() == group.materializedHistoryLimit) {
break;
}
}
for (auto &record : records) {
Cleanse(record.plaintext);
}
};
const auto firstCount = std::min(total, group.materializedHistoryLimit);
auto offset = total - firstCount;
auto records = group.contentStore.records(
total - firstCount,
firstCount);
processRecords(records);
while (rendered.size() < group.materializedHistoryLimit && offset) {
const auto count = std::min(offset, group.materializedHistoryLimit);
offset -= count;
records = group.contentStore.records(offset, count);
processRecords(records);
}
std::reverse(begin(rendered), end(rendered));
auto appendFrom = std::size_t();
if (group.materializedHistory.size() <= rendered.size()) {
appendFrom = group.materializedHistory.size();
for (auto index = std::size_t(); index != appendFrom; ++index) {
const auto &entry = group.materializedHistory[index];
const auto item = _session->data().message(entry.fullId);
if (entry.eventObjectId != rendered[index].eventObjectId
|| entry.latestMutationEventObjectId
!= rendered[index].latestMutationEventObjectId
|| !item
|| !item->isE2ECloudDecrypted()) {
appendFrom = 0;
break;
}
}
} else {
appendFrom = 0;
}
if (!appendFrom && !group.materializedHistory.empty()) {
clearMaterializedProtectedHistory(group);
}
const auto security = protectedSecurity(group.conversationId);
const auto history = _session->data().history(PeerId(
group.materializedHistoryPeerIdBinding
? group.materializedHistoryPeerIdBinding
: group.telegramPeerIdBinding));
for (auto index = appendFrom; index != rendered.size(); ++index) {
const auto &record = rendered[index];
auto member = static_cast<const DesktopProtectedMember*>(nullptr);
if (security) {
const auto found = std::find_if(
begin(security->members),
end(security->members),
[&](const DesktopProtectedMember &value) {
return value.accountId == record.senderAccountId;
});
member = (found != end(security->members)) ? &*found : nullptr;
}
const auto knownMember = member && member->telegramUserIdBinding;
auto flags = MessageFlags();
auto from = PeerId();
if (knownMember) {
flags |= MessageFlag::HasFromId;
from = peerFromUser(UserId(member->telegramUserIdBinding));
if (member->local) {
flags |= MessageFlag::Outgoing;
}
}
const auto maximumTime = std::uint64_t(
std::numeric_limits<TimeId>::max());
auto fields = HistoryItemCommonFields{
.id = _session->data().nextLocalMessageId(),
.flags = flags,
.from = from,
.date = TimeId(std::min(record.unixTime, maximumTime)),
.e2eCloudEditDate = TimeId(std::min(
record.editUnixTime,
maximumTime)),
.e2eCloudDecrypted = true,
.e2eCloudConversationId = ProtectedHistoryIdBytes(
group.conversationId),
.e2eCloudEventObjectId = ProtectedHistoryIdBytes(
record.eventObjectId),
};
const auto item = [&] {
if (!record.file) {
return history->addExistingLocalMessage(
std::move(fields),
TextWithEntities{ .text = record.text },
MTP_messageMediaEmpty());
}
const auto document = CreateProtectedHistoryDocument(
_session,
group.conversationId,
record.eventObjectId,
fields.date,
*record.file,
_sha256);
const auto local = group.localProtectedFilePaths.find(
record.eventObjectId);
if (local != end(group.localProtectedFilePaths)
&& QFileInfo(local->second).isFile()) {
document->setLocation(Core::FileLocation(local->second));
}
return history->addExistingLocalMessage(
std::move(fields),
document,
TextWithEntities{ .text = record.text });
}();
group.materializedHistory.push_back({
.eventObjectId = record.eventObjectId,
.latestMutationEventObjectId
= record.latestMutationEventObjectId,
.fullId = item->fullId(),
});
}
_session->data().notifyHistoryChangeDelayed(history);
}
std::optional<DesktopProtectedSecurity> DesktopService::protectedSecurity(
ConversationId conversationId) const {
const auto i = _groups.find(conversationId);
if (!vaultReady() || i == end(_groups)) {
return std::nullopt;
}
const auto &group = *i->second;
const auto state = group.groupLedger.state();
const auto localAccountId = DeriveAccountId(
_vault->identity.credential,
_sha256);
if (!state || !localAccountId) {
return std::nullopt;
}
const auto owner = std::find_if(
begin(state->members()),
end(state->members()),
[](const GroupMember &member) {
return member.role == GroupRole::Owner;
});
auto accountIds = std::vector<AccountId>();
accountIds.reserve(state->members().size());
for (const auto &member : state->members()) {
accountIds.push_back(member.accountId);
}
const auto groupDigest = (owner != end(state->members()))
? DeriveGroupSafetyDigest(
conversationId,
owner->accountId,
accountIds,
_sha256)
: std::nullopt;
if (!groupDigest) {
return std::nullopt;
}
const auto localMember = state->member(*localAccountId);
const auto administrationAllowed = localMember
&& group.freshnessGate
&& group.freshnessGate->administrationAllowed();
const auto hasPermission = [&](AdminPermission permission) {
return administrationAllowed
&& (localMember->role == GroupRole::Owner
|| (localMember->role == GroupRole::Administrator
&& (localMember->adminPermissions
& PermissionMask(permission))));
};
auto witnesses = (group.safetyWitnessGeneration == state->generation())
? group.safetyWitnesses
: std::set<AccountId>();
if (state->member(*localAccountId)) {
witnesses.emplace(*localAccountId);
}
auto result = DesktopProtectedSecurity{
.generation = state->generation(),
.groupSafetyCode = FormatSafetyCode(*groupDigest).value_or(QString()),
.memberCount = state->members().size(),
.witnessCount = 0,
.defaultHistoryAccess = state->policy().defaultHistoryAccess,
.canSetRoles = administrationAllowed
&& localMember->role == GroupRole::Owner,
.canRemoveMembers = hasPermission(AdminPermission::RemoveMembers),
.canGrantHistory = hasPermission(AdminPermission::GrantHistory),
.canGrantFullHistory = hasPermission(
AdminPermission::GrantFullHistory),
.canChangeDefaultHistory = hasPermission(
AdminPermission::ChangeDefaultHistory),
.members = {},
};
result.members.reserve(state->members().size());
for (const auto &member : state->members()) {
auto accountDigest = Digest();
accountDigest.bytes = member.accountId.bytes;
const auto pairwise = (member.accountId != *localAccountId)
? DerivePairwiseSafetyDigest(
*localAccountId,
member.accountId,
_sha256)
: std::nullopt;
result.members.push_back({
.accountId = member.accountId,
.telegramUserIdBinding = member.telegramUserIdBinding,
.role = member.role,
.historyAccess = member.historyAccess,
.clientCount = member.clients.size(),
.accountSafetyCode = FormatSafetyCode(accountDigest)
.value_or(QString()),
.pairwiseSafetyCode = pairwise
? FormatSafetyCode(*pairwise).value_or(QString())
: QString(),
.local = member.accountId == *localAccountId,
});
if (witnesses.contains(member.accountId)) {
++result.witnessCount;
}
}
std::sort(
begin(result.members),
end(result.members),
[](const auto &a, const auto &b) {
return (a.role != b.role)
? a.role == GroupRole::Owner
: a.telegramUserIdBinding < b.telegramUserIdBinding;
});
return result;
}
bool DesktopService::sendProtectedText(
ConversationId conversationId,
QString text) {
return !text.isEmpty() && queueProtectedMessageBody(
conversationId,
{
.unixTime = std::uint64_t(base::unixtime::now()),
.textUtf8 = text.toUtf8(),
});
}
bool DesktopService::editProtectedText(
ConversationId conversationId,
ObjectId targetEventObjectId,
QString text) {
if (text.isEmpty()) {
return false;
}
return queueProtectedMessageBody(
conversationId,
{
.action = ProtectedMessageAction::Edit,
.targetEventObjectId = targetEventObjectId,
.textUtf8 = text.toUtf8(),
});
}
bool DesktopService::deleteProtectedMessage(
ConversationId conversationId,
ObjectId targetEventObjectId) {
const auto i = _groups.find(conversationId);
const auto pending = (i != end(_groups))
? i->second->fileTransfer.pending()
: nullptr;
const auto cancelMatchingFile = pending
&& pending->eventObjectId == targetEventObjectId;
if (!queueProtectedMessageBody(
conversationId,
{
.action = ProtectedMessageAction::Delete,
.targetEventObjectId = targetEventObjectId,
},
false)) {
return false;
}
// Persist the deletion before cancelling its matching transfer. The
// cancellation pump removes only the manifest pair, so this tombstone
// remains queued and is published immediately after local cleanup.
auto cancellationFailed = false;
if (cancelMatchingFile && !pending->cancelRequested) {
const auto requested = i->second->fileTransfer.requestCancel();
if (requested != FileTransferCommitResult::Committed
&& requested != FileTransferCommitResult::AlreadyCommitted) {
cancellationFailed = true;
} else if (i->second->fileFinalHashCancellation) {
i->second->fileFinalHashCancellation->store(
true,
std::memory_order_relaxed);
}
}
pumpActiveOutbox(conversationId);
if (cancellationFailed) {
setContentState(conversationId, DesktopContentState::LocalFailure);
}
return true;
}
bool DesktopService::queueProtectedMessageBody(
ConversationId conversationId,
ProtectedMessageBody body,
bool pump) {
const auto i = _groups.find(conversationId);
if (!vaultReady()
|| i == end(_groups)
|| i->second->observation
|| i->second->observationDirty
|| i->second->fileHashInProgress
|| !i->second->filePreparationPath.isEmpty()
|| i->second->phase != PendingGroupCreation::Phase::Active) {
return false;
}
auto &group = *i->second;
const auto metadata = group.metadata.metadata();
const auto state = group.groupLedger.state();
const auto epoch = group.archiveState.currentEpoch();
if (!metadata || !state || !epoch) {
setContentState(conversationId, DesktopContentState::LocalFailure);
return false;
}
if (body.action == ProtectedMessageAction::Create) {
if (!body.unixTime || body.targetEventObjectId) {
return false;
}
} else {
auto target = group.contentStore.record(body.targetEventObjectId);
const auto targetGuard = qScopeGuard([&] {
if (target) {
Cleanse(target->plaintext);
}
});
if (!target
|| target->senderAccountId != metadata->accountId
|| (body.action == ProtectedMessageAction::Edit
&& target->objectKind != ObjectKind::EncryptedMessageBody)
|| (target->objectKind != ObjectKind::EncryptedMessageBody
&& target->objectKind != ObjectKind::EncryptedFileManifest)) {
return false;
}
if (target->objectKind == ObjectKind::EncryptedMessageBody) {
auto targetBody = ProtectedMessageBodyCodecV1().decodePlaintext(
target->plaintext);
const auto targetIsCreate = targetBody
&& targetBody->action == ProtectedMessageAction::Create;
if (targetBody) {
Cleanse(targetBody->textUtf8);
}
if (!targetIsCreate) {
return false;
}
}
auto latestTime = target->unixTime;
auto records = group.contentStore.records(
0,
group.contentStore.size(),
ObjectKind::EncryptedMessageBody);
for (auto &record : records) {
auto mutation = ProtectedMessageBodyCodecV1().decodePlaintext(
record.plaintext);
if (mutation
&& mutation->action != ProtectedMessageAction::Create
&& mutation->targetEventObjectId
== body.targetEventObjectId) {
latestTime = std::max(latestTime, mutation->unixTime);
}
if (mutation) {
Cleanse(mutation->textUtf8);
}
Cleanse(record.plaintext);
}
const auto now = std::uint64_t(base::unixtime::now());
if (latestTime
== std::uint64_t(std::numeric_limits<std::int64_t>::max())) {
return false;
}
body.unixTime = std::max(now, latestTime + 1);
}
const auto eventObjectId = RandomId<ObjectId>();
const auto contentObjectId = RandomId<ObjectId>();
auto plaintext = ProtectedMessageBodyCodecV1().encodePlaintext(body);
const auto plaintextGuard = qScopeGuard([&] {
if (plaintext) {
Cleanse(*plaintext);
}
});
if (!eventObjectId
|| !contentObjectId
|| !plaintext
|| !initializeActivePipeline(group)) {
setContentState(
conversationId,
(_vaultState.current() == DesktopVaultState::SecurityBlocked)
? DesktopContentState::SecurityBlocked
: DesktopContentState::LocalFailure);
return false;
}
if (group.freshnessGate->state() == FreshnessState::Required
&& !group.outbox.size()
&& !queueFreshnessChallenge(group)) {
setContentState(conversationId, DesktopContentState::LocalFailure);
return false;
}
const auto queued = QueueArchivedContent({
.conversationId = conversationId,
.eventObjectId = *eventObjectId,
.contentObjectId = *contentObjectId,
.objectKind = ObjectKind::EncryptedMessageBody,
.senderAccountId = metadata->accountId,
.senderClientId = metadata->clientId,
.telegramPeerIdBinding = group.telegramPeerIdBinding,
.groupGeneration = state->generation(),
.archiveEpochGeneration = epoch->generation,
.archiveEpochKey = &epoch->key,
.senderSigningPrivateKey = &_vault->identity.signingPrivateKey,
.plaintext = *plaintext,
.mlsContext = QByteArray("TDE2E/archived-content/v1"),
},
ArchiveEpochCrypto(),
EncryptedArchivedContentCodecV1(),
ArchivedContentDescriptorCodecV1(),
group.envelopeCodec,
_sha256,
*group.outboxCoordinator);
if (queued != ArchivedContentQueueResult::Queued) {
setContentState(conversationId, DesktopContentState::LocalFailure);
return false;
}
group.queuedContentReconciled = false;
const auto stored = group.contentStore.append({
.conversationId = conversationId,
.eventObjectId = *eventObjectId,
.contentObjectId = *contentObjectId,
.objectKind = ObjectKind::EncryptedMessageBody,
.groupGeneration = state->generation(),
.senderAccountId = metadata->accountId,
.senderClientId = metadata->clientId,
.unixTime = body.unixTime,
.observedTelegramMessageId = 0,
.plaintext = *plaintext,
});
if (stored == ContentStoreAppendResult::Conflict) {
_vaultState = DesktopVaultState::SecurityBlocked;
setContentState(conversationId, DesktopContentState::SecurityBlocked);
return false;
} else if (stored == ContentStoreAppendResult::InvalidRecord
|| stored == ContentStoreAppendResult::PersistenceFailed) {
setContentState(conversationId, DesktopContentState::LocalFailure);
return false;
}
if (stored == ContentStoreAppendResult::Stored) {
notifyContentRevision();
}
group.queuedContentReconciled = true;
if (pump) {
pumpActiveOutbox(conversationId);
}
return true;
}
bool DesktopService::sendProtectedFile(
ConversationId conversationId,
QString path) {
const auto i = _groups.find(conversationId);
if (!vaultReady()
|| i == end(_groups)
|| path.isEmpty()
|| i->second->observation
|| i->second->observationDirty
|| i->second->fileHashInProgress
|| !i->second->filePreparationPath.isEmpty()
|| i->second->fileTransfer.pending()
|| i->second->phase != PendingGroupCreation::Phase::Active) {
return false;
}
auto &group = *i->second;
// An earlier protected object may still own the outbox or upload slot.
// Staging is independent; pumpActiveOutbox creates this file's durable
// manifest only after both become empty, preserving their exact order.
const auto sourceInfo = QFileInfo(path);
const auto absolutePath = sourceInfo.absoluteFilePath();
if (absolutePath.isEmpty() || !sourceInfo.isFile()) {
return false;
}
const auto ownedSource = IsStagedProtectedSource(
_telegramUserIdBinding,
conversationId,
absolutePath);
const auto stagedPath = ownedSource
? std::optional<QString>(absolutePath)
: PrepareStagedProtectedFile(
_telegramUserIdBinding,
conversationId);
if (!stagedPath) {
return false;
}
const auto mimeType = QMimeDatabase().mimeTypeForFile(
absolutePath,
QMimeDatabase::MatchExtension).name();
group.filePreparationPath = *stagedPath;
group.filePreparationFilename = IsStagedProtectedImage(
_telegramUserIdBinding,
conversationId,
absolutePath)
? u"image.png"_q
: sourceInfo.fileName();
group.filePreparationMimeType = mimeType;
group.filePreparationSource.reset();
group.fileHashCancelRequested = false;
group.fileHashInProgress = true;
const auto cancellation = std::make_shared<std::atomic_bool>(false);
group.fileHashCancellation = cancellation;
setContentState(conversationId, DesktopContentState::Synchronizing);
notifyFileTransferRevision();
const auto operationEpoch = _operationEpoch;
crl::async([weak = base::weak_ptr(this),
conversationId,
absolutePath,
stagedPath = *stagedPath,
mimeType,
ownedSource,
cancellation,
operationEpoch] {
const auto source = HashFile(
absolutePath,
cancellation,
true,
ownedSource ? QString() : stagedPath,
mimeType);
crl::on_main([weak,
conversationId,
stagedPath,
cancellation,
operationEpoch,
source] {
if (!weak || weak->_operationEpoch != operationEpoch) {
return;
}
const auto i = weak->_groups.find(conversationId);
if (i == end(weak->_groups)
|| !i->second->fileHashInProgress
|| i->second->fileHashCancellation != cancellation
|| i->second->filePreparationPath != stagedPath) {
return;
}
auto &group = *i->second;
group.fileHashInProgress = false;
group.fileHashCancellation.reset();
if (group.fileHashCancelRequested) {
group.fileHashCancelRequested = false;
RemoveStagedProtectedSource(
weak->_telegramUserIdBinding,
conversationId,
group.filePreparationPath);
group.filePreparationPath.clear();
group.filePreparationFilename.clear();
group.filePreparationMimeType.clear();
group.filePreparationSource.reset();
weak->notifyFileTransferRevision();
weak->pumpActiveOutbox(conversationId);
return;
} else if (!source) {
RemoveStagedProtectedSource(
weak->_telegramUserIdBinding,
conversationId,
group.filePreparationPath);
group.filePreparationPath.clear();
group.filePreparationFilename.clear();
group.filePreparationMimeType.clear();
group.filePreparationSource.reset();
weak->notifyFileTransferRevision();
weak->setContentState(
conversationId,
DesktopContentState::LocalFailure);
return;
}
group.filePreparationSource = source;
weak->pumpActiveOutbox(conversationId);
});
});
return true;
}
bool DesktopService::sendProtectedImage(
ConversationId conversationId,
const QImage &image) {
const auto path = StageProtectedImage(
_telegramUserIdBinding,
conversationId,
image);
if (!path) {
return false;
} else if (!sendProtectedFile(conversationId, *path)) {
RemoveStagedProtectedSource(
_telegramUserIdBinding,
conversationId,
*path);
return false;
}
return true;
}
bool DesktopService::cancelProtectedFileTransfer(
ConversationId conversationId) {
const auto i = _groups.find(conversationId);
if (!vaultReady()
|| i == end(_groups)
|| i->second->phase != PendingGroupCreation::Phase::Active) {
return false;
}
auto &group = *i->second;
if (group.fileHashInProgress) {
if (!group.fileHashCancellation) {
setContentState(conversationId, DesktopContentState::LocalFailure);
return false;
}
group.fileHashCancelRequested = true;
group.fileHashCancellation->store(true, std::memory_order_relaxed);
setContentState(conversationId, DesktopContentState::Synchronizing);
return true;
} else if (!group.filePreparationPath.isEmpty()) {
RemoveStagedProtectedSource(
_telegramUserIdBinding,
conversationId,
group.filePreparationPath);
group.filePreparationPath.clear();
group.filePreparationFilename.clear();
group.filePreparationMimeType.clear();
group.filePreparationSource.reset();
notifyFileTransferRevision();
pumpActiveOutbox(conversationId);
return true;
} else if (!group.fileTransfer.pending()) {
return false;
}
const auto requested = group.fileTransfer.requestCancel();
if (requested != FileTransferCommitResult::Committed
&& requested != FileTransferCommitResult::AlreadyCommitted) {
setContentState(conversationId, DesktopContentState::LocalFailure);
return false;
}
if (group.fileFinalHashCancellation) {
group.fileFinalHashCancellation->store(
true,
std::memory_order_relaxed);
}
pumpActiveOutbox(conversationId);
return true;
}
bool DesktopService::saveProtectedFile(
ConversationId conversationId,
ObjectId eventObjectId,
QString path,
std::function<void(ProtectedFileSaveResult)> callback,
ProtectedFileSaveProgress progress) {
if (!vaultReady()) {
if (callback) {
callback(ProtectedFileSaveResult::SecurityBlocked);
}
return false;
}
const auto i = _groups.find(conversationId);
auto record = (i != end(_groups))
? i->second->contentStore.record(eventObjectId)
: std::nullopt;
const auto recordGuard = qScopeGuard([&] {
if (record) {
Cleanse(record->plaintext);
}
});
const auto manifest = (record
&& record->objectKind == ObjectKind::EncryptedFileManifest)
? PrivateFileManifestCodecV1().decodePlaintext(record->plaintext)
: std::nullopt;
if (!manifest
|| path.isEmpty()
|| record->observedTelegramMessageId <= 0
|| record->observedTelegramMessageId
> std::numeric_limits<int>::max()) {
if (callback) {
callback(ProtectedFileSaveResult::InvalidRequest);
}
return false;
}
auto &group = *i->second;
const auto duplicate = (group.pendingFileDownload
&& group.pendingFileDownload->eventObjectId == eventObjectId)
|| std::any_of(
begin(group.fileDownloadQueue),
end(group.fileDownloadQueue),
[&](const auto &request) {
return request.eventObjectId == eventObjectId;
});
if (duplicate
|| group.fileDownloadQueue.size()
>= kMaximumQueuedFileDownloads) {
if (callback) {
callback(ProtectedFileSaveResult::Busy);
}
return false;
}
group.fileDownloadQueue.push_back({
.eventObjectId = eventObjectId,
.path = std::move(path),
.callback = std::move(callback),
.progress = std::move(progress),
});
(void)startNextProtectedFileDownload(conversationId);
return true;
}
bool DesktopService::startNextProtectedFileDownload(
ConversationId conversationId) {
while (vaultReady()) {
const auto i = _groups.find(conversationId);
if (i == end(_groups)
|| i->second->pendingFileDownload
|| i->second->observation
|| i->second->observationDirty
|| i->second->fileDownloadQueue.empty()) {
return false;
}
auto &group = *i->second;
auto request = std::move(group.fileDownloadQueue.front());
group.fileDownloadQueue.pop_front();
auto record = group.contentStore.record(request.eventObjectId);
const auto recordGuard = qScopeGuard([&] {
if (record) {
Cleanse(record->plaintext);
}
});
const auto manifest = (record
&& record->objectKind == ObjectKind::EncryptedFileManifest)
? PrivateFileManifestCodecV1().decodePlaintext(record->plaintext)
: std::nullopt;
if (!manifest
|| request.path.isEmpty()
|| record->observedTelegramMessageId <= 0
|| record->observedTelegramMessageId
> std::numeric_limits<int>::max()) {
auto callback = std::move(request.callback);
const auto weak = base::weak_ptr(this);
if (callback) {
callback(ProtectedFileSaveResult::InvalidRequest);
}
if (!weak) {
return false;
}
continue;
}
auto missing = std::set<std::uint32_t>();
for (auto index = std::uint32_t();
index != manifest->context.chunkCount;
++index) {
if (!group.chunkStore.hasChunk(
conversationId,
manifest->context.fileId,
index)) {
missing.emplace(index);
}
}
group.pendingFileDownload = PendingGroupCreation::PendingFileDownload{
.eventObjectId = request.eventObjectId,
.fileId = manifest->context.fileId,
.path = std::move(request.path),
.plaintextSize = manifest->context.plaintextSize,
.chunkSize = manifest->context.chunkSize,
.chunkCount = manifest->context.chunkCount,
.missingChunkIndices = std::move(missing),
.callback = std::move(request.callback),
.progress = std::move(request.progress),
.carrierSearch = FileDownloadCarrierSearch::Current,
};
if (group.pendingFileDownload->missingChunkIndices.empty()) {
const auto started = writePendingProtectedFile(conversationId);
if (!started) {
finishFileChunkDownload(
conversationId,
ProtectedFileSaveResult::LocalFailure);
}
return started;
}
if (!beginFileChunkDownload(
conversationId,
FileDownloadCarrierSearch::Current)) {
finishFileChunkDownload(
conversationId,
ProtectedFileSaveResult::LocalFailure);
return false;
}
return true;
}
return false;
}
bool DesktopService::beginFileChunkDownload(
ConversationId conversationId,
FileDownloadCarrierSearch carrierSearch) {
const auto i = _groups.find(conversationId);
if (!vaultReady()
|| i == end(_groups)
|| !i->second->pendingFileDownload) {
return false;
}
auto &group = *i->second;
auto record = group.contentStore.record(
group.pendingFileDownload->eventObjectId);
const auto recordGuard = qScopeGuard([&] {
if (record) {
Cleanse(record->plaintext);
}
});
const auto manifest = (record
&& record->objectKind == ObjectKind::EncryptedFileManifest)
? PrivateFileManifestCodecV1().decodePlaintext(record->plaintext)
: std::nullopt;
if (!manifest
|| manifest->context.fileId != group.pendingFileDownload->fileId
|| record->observedTelegramMessageId <= 0
|| record->observedTelegramMessageId
> std::numeric_limits<int>::max()) {
return false;
}
const auto chunkCount = std::uint64_t(manifest->context.chunkCount);
if (chunkCount > std::numeric_limits<std::uint64_t>::max()
- kFileDownloadExtraObjects
|| chunkCount > (std::numeric_limits<std::uint64_t>::max()
- kFileDownloadExtraBytes
- manifest->context.plaintextSize) / 2048) {
return false;
}
const auto maximumObjects = chunkCount + kFileDownloadExtraObjects;
const auto maximumPages = (maximumObjects + 99) / 100
+ kFileDownloadExtraPages;
const auto maximumBytes = manifest->context.plaintextSize
+ (chunkCount * 2048)
+ kFileDownloadExtraBytes;
const auto encodedFileId = QByteArray(
reinterpret_cast<const char*>(manifest->context.fileId.bytes.data()),
manifest->context.fileId.bytes.size()).toHex();
const auto filename = [&] {
switch (carrierSearch) {
case FileDownloadCarrierSearch::Current:
return ProtectedFileChunkCarrierFilename(
manifest->context.fileId);
case FileDownloadCarrierSearch::LegacyFileId:
return u"protected_file_"_q
+ QString::fromLatin1(encodedFileId)
+ u".tde2e"_q;
case FileDownloadCarrierSearch::LegacyTruncatedFileId:
return u"protected_file_"_q
+ QString::fromLatin1(encodedFileId.left(48))
+ u".tde2e"_q;
case FileDownloadCarrierSearch::Content:
return ProtectedContentCarrierFilename();
case FileDownloadCarrierSearch::Legacy:
return ProtectedLegacyCarrierFilename();
case FileDownloadCarrierSearch::Family:
return QString();
}
Unexpected("File download carrier search type.");
}();
const auto broadCarrierSearch
= (carrierSearch == FileDownloadCarrierSearch::Family);
group.pendingFileDownload->carrierSearch = carrierSearch;
group.fileDownloadController.reset();
group.fileDownloadTransport.reset();
group.fileDownloadBackend = std::make_unique<
TelegramSessionCarrierBackend>(
_session,
_session->data().history(PeerId(group.telegramPeerIdBinding)),
group.telegramPeerIdBinding,
filename,
ProtectedCarrierMimeType(),
ProtectedFileChunkMaximumObjectSize(),
kFileDownloadPageBytes,
int(record->observedTelegramMessageId),
broadCarrierSearch);
group.fileDownloadTransport = std::make_unique<TelegramCarrierTransport>(
conversationId,
group.telegramPeerIdBinding,
*group.fileDownloadBackend);
const auto eventObjectId = group.pendingFileDownload->eventObjectId;
const auto operationEpoch = _operationEpoch;
group.fileDownloadController = std::make_unique<
FileChunkDownloadController>(
conversationId,
group.telegramPeerIdBinding,
record->observedTelegramMessageId,
maximumPages,
maximumObjects,
maximumBytes,
*group.fileDownloadTransport,
[weak = base::weak_ptr(this), conversationId](
std::vector<TelegramTransport::UntrustedObject> objects) {
return weak
? weak->processFileChunkDownloadPage(
conversationId,
std::move(objects))
: FileChunkDownloadPageStatus::PersistenceFailed;
},
[weak = base::weak_ptr(this),
conversationId,
eventObjectId,
operationEpoch](
FileChunkDownloadCompletion completion) {
crl::on_main([weak,
conversationId,
eventObjectId,
operationEpoch,
completion] {
if (weak) {
weak->applyFileChunkDownload(
conversationId,
eventObjectId,
operationEpoch,
completion);
}
});
});
return group.fileDownloadController->start();
}
void DesktopService::scheduleFileDownloadRetry(
ConversationId conversationId,
ObjectId eventObjectId,
FileDownloadCarrierSearch carrierSearch) {
const auto i = _groups.find(conversationId);
if (!vaultReady()
|| i == end(_groups)
|| !i->second->pendingFileDownload
|| i->second->pendingFileDownload->eventObjectId != eventObjectId) {
return;
}
auto &pending = *i->second->pendingFileDownload;
const auto delay = FileRetryDelay(pending.retryAttempt);
pending.retryAttempt = std::min(pending.retryAttempt + 1, 6);
++pending.retryCount;
if (++pending.retryToken == 0) {
++pending.retryToken;
}
const auto token = pending.retryToken;
base::call_delayed(delay, [weak = base::weak_ptr(this),
conversationId,
eventObjectId,
carrierSearch,
token] {
if (!weak || !weak->vaultReady()) {
return;
}
const auto i = weak->_groups.find(conversationId);
if (i == end(weak->_groups)
|| !i->second->pendingFileDownload
|| i->second->pendingFileDownload->eventObjectId
!= eventObjectId
|| i->second->pendingFileDownload->retryToken != token) {
return;
}
if (!weak->beginFileChunkDownload(
conversationId,
carrierSearch)) {
weak->finishFileChunkDownload(
conversationId,
ProtectedFileSaveResult::LocalFailure);
}
});
}
FileChunkDownloadPageStatus DesktopService::processFileChunkDownloadPage(
ConversationId conversationId,
std::vector<TelegramTransport::UntrustedObject> objects) {
if (!vaultReady()) {
return FileChunkDownloadPageStatus::SecurityBlocked;
}
const auto i = _groups.find(conversationId);
if (i == end(_groups) || !i->second->pendingFileDownload) {
return FileChunkDownloadPageStatus::PersistenceFailed;
}
auto &group = *i->second;
const auto metadata = group.metadata.metadata();
if (!metadata) {
return FileChunkDownloadPageStatus::PersistenceFailed;
}
const auto processed = ProcessObservedFileChunkPage(
objects,
group.pendingFileDownload->fileId,
{
.conversationId = conversationId,
.accountId = metadata->accountId,
.clientId = metadata->clientId,
.telegramPeerIdBinding = group.telegramPeerIdBinding,
},
group.envelopeCodec,
_sha256,
group.groupLedger,
group.contentStore,
group.chunkStore);
if (processed.status == ObservedContentProcessStatus::SecurityBlocked) {
return FileChunkDownloadPageStatus::SecurityBlocked;
} else if (processed.status != ObservedContentProcessStatus::Processed) {
return FileChunkDownloadPageStatus::PersistenceFailed;
}
const auto missingBefore = group.pendingFileDownload
->missingChunkIndices.size();
for (const auto index : processed.availableChunkIndices) {
group.pendingFileDownload->missingChunkIndices.erase(index);
}
if (group.pendingFileDownload->missingChunkIndices.size()
< missingBefore) {
group.pendingFileDownload->retryAttempt = 0;
group.pendingFileDownload->retryCount = 0;
if (const auto &progress = group.pendingFileDownload->progress) {
const auto &pending = *group.pendingFileDownload;
auto received = std::uint64_t();
for (auto index = std::uint32_t();
index != pending.chunkCount;
++index) {
if (pending.missingChunkIndices.contains(index)) {
continue;
}
const auto offset = std::uint64_t(index)
* pending.chunkSize;
if (offset < pending.plaintextSize) {
received += std::min(
std::uint64_t(pending.chunkSize),
pending.plaintextSize - offset);
}
}
progress(received, pending.plaintextSize);
}
}
return group.pendingFileDownload->missingChunkIndices.empty()
? FileChunkDownloadPageStatus::Complete
: FileChunkDownloadPageStatus::Incomplete;
}
void DesktopService::applyFileChunkDownload(
ConversationId conversationId,
ObjectId eventObjectId,
std::uint64_t operationEpoch,
FileChunkDownloadCompletion completion) {
if (!vaultReady() || _operationEpoch != operationEpoch) {
return;
}
const auto i = _groups.find(conversationId);
if (i == end(_groups)
|| !i->second->pendingFileDownload
|| i->second->pendingFileDownload->eventObjectId
!= eventObjectId) {
return;
}
auto &group = *i->second;
const auto previousCarrierSearch
= group.pendingFileDownload->carrierSearch;
group.fileDownloadController.reset();
group.fileDownloadTransport.reset();
group.fileDownloadBackend.reset();
if (completion.status == FileChunkDownloadStatus::SecurityBlocked
|| completion.status == FileChunkDownloadStatus::InvalidPagination
|| completion.status == FileChunkDownloadStatus::LimitExceeded) {
_vaultState = DesktopVaultState::SecurityBlocked;
setContentState(
conversationId,
DesktopContentState::SecurityBlocked);
finishFileChunkDownload(
conversationId,
ProtectedFileSaveResult::SecurityBlocked);
return;
} else if (completion.status == FileChunkDownloadStatus::Complete) {
if (++group.pendingFileDownload->retryToken == 0) {
++group.pendingFileDownload->retryToken;
}
if (!writePendingProtectedFile(conversationId)) {
finishFileChunkDownload(
conversationId,
ProtectedFileSaveResult::LocalFailure);
}
return;
} else if (completion.status == FileChunkDownloadStatus::Missing
|| completion.status
== FileChunkDownloadStatus::RetryableTransportError) {
const auto nextCarrierSearch = [=]()
-> std::optional<FileDownloadCarrierSearch> {
switch (previousCarrierSearch) {
case FileDownloadCarrierSearch::Current:
return FileDownloadCarrierSearch::LegacyFileId;
case FileDownloadCarrierSearch::LegacyFileId:
return FileDownloadCarrierSearch::LegacyTruncatedFileId;
case FileDownloadCarrierSearch::LegacyTruncatedFileId:
return FileDownloadCarrierSearch::Content;
case FileDownloadCarrierSearch::Content:
return FileDownloadCarrierSearch::Legacy;
case FileDownloadCarrierSearch::Legacy:
return FileDownloadCarrierSearch::Family;
case FileDownloadCarrierSearch::Family:
return std::nullopt;
}
Unexpected("File download carrier search type.");
}();
if (nextCarrierSearch
&& beginFileChunkDownload(
conversationId,
*nextCarrierSearch)) {
return;
}
if (!nextCarrierSearch
&& completion.status
== FileChunkDownloadStatus::RetryableTransportError
&& group.pendingFileDownload->retryCount
< kFileDownloadMaximumRetries) {
scheduleFileDownloadRetry(
conversationId,
eventObjectId,
previousCarrierSearch);
return;
}
}
const auto result = (completion.status
== FileChunkDownloadStatus::RetryableTransportError)
? ProtectedFileSaveResult::RetryableTransportError
: (completion.status
== FileChunkDownloadStatus::PermanentTransportError)
? ProtectedFileSaveResult::PermanentTransportError
: (completion.status == FileChunkDownloadStatus::Missing)
? ProtectedFileSaveResult::MissingChunks
: ProtectedFileSaveResult::LocalFailure;
finishFileChunkDownload(conversationId, result);
}
bool DesktopService::writePendingProtectedFile(
ConversationId conversationId) {
const auto i = _groups.find(conversationId);
if (!vaultReady()
|| i == end(_groups)
|| !i->second->pendingFileDownload
|| i->second->pendingFileDownload->write) {
return false;
}
auto &group = *i->second;
auto record = group.contentStore.record(
group.pendingFileDownload->eventObjectId);
const auto recordGuard = qScopeGuard([&] {
if (record) {
Cleanse(record->plaintext);
}
});
auto manifest = (record
&& record->objectKind == ObjectKind::EncryptedFileManifest)
? PrivateFileManifestCodecV1().decodePlaintext(record->plaintext)
: std::nullopt;
if (!manifest
|| manifest->context.fileId != group.pendingFileDownload->fileId) {
return false;
}
auto output = std::make_unique<QSaveFile>(
group.pendingFileDownload->path);
if (!output->open(QIODevice::WriteOnly)) {
return false;
}
const auto digest = EVP_MD_CTX_new();
if (!digest || EVP_DigestInit_ex(digest, EVP_sha256(), nullptr) != 1) {
EVP_MD_CTX_free(digest);
output->cancelWriting();
return false;
}
const auto eventObjectId = group.pendingFileDownload->eventObjectId;
group.pendingFileDownload->write = std::make_unique<
PendingGroupCreation::PendingFileDownload::PendingWrite>(
std::move(output),
digest,
std::move(*manifest));
const auto operationEpoch = _operationEpoch;
crl::on_main([weak = base::weak_ptr(this),
conversationId,
eventObjectId,
operationEpoch] {
if (weak) {
weak->continuePendingProtectedFileWrite(
conversationId,
eventObjectId,
operationEpoch);
}
});
return true;
}
void DesktopService::continuePendingProtectedFileWrite(
ConversationId conversationId,
ObjectId eventObjectId,
std::uint64_t operationEpoch) {
if (!vaultReady() || _operationEpoch != operationEpoch) {
return;
}
const auto i = _groups.find(conversationId);
if (i == end(_groups)
|| !i->second->pendingFileDownload
|| i->second->pendingFileDownload->eventObjectId != eventObjectId
|| !i->second->pendingFileDownload->write) {
return;
}
auto &group = *i->second;
auto &write = *group.pendingFileDownload->write;
const auto scheduleNext = [=] {
crl::on_main([weak = base::weak_ptr(this),
conversationId,
eventObjectId,
operationEpoch] {
if (weak) {
weak->continuePendingProtectedFileWrite(
conversationId,
eventObjectId,
operationEpoch);
}
});
};
if (write.committed) {
const auto remaining = write.manifest.context.chunkCount
- write.cleanupChunkIndex;
const auto endIndex = write.cleanupChunkIndex
+ std::min(remaining, kFileCleanupChunksPerTurn);
while (write.cleanupChunkIndex != endIndex) {
(void)group.chunkStore.removeChunk(
conversationId,
write.manifest.context.fileId,
write.cleanupChunkIndex++);
}
if (write.cleanupChunkIndex
!= write.manifest.context.chunkCount) {
scheduleNext();
} else {
finishFileChunkDownload(
conversationId,
ProtectedFileSaveResult::Saved);
}
return;
}
if (write.nextChunkIndex != write.manifest.context.chunkCount) {
const auto index = write.nextChunkIndex;
const auto stored = group.chunkStore.read(
conversationId,
write.manifest.context.fileId,
index);
auto plaintext = (stored.status == FileChunkReadStatus::Found)
? AesGcmFileChunkCipher().decrypt(
write.manifest.key,
write.manifest.context,
index,
stored.chunk.exactCiphertext)
: std::nullopt;
const auto written = plaintext
? write.output->write(*plaintext)
: -1;
const auto updated = plaintext
&& written == plaintext->size()
&& EVP_DigestUpdate(
write.digest,
plaintext->constData(),
plaintext->size()) == 1;
if (!updated) {
if (plaintext) {
Cleanse(*plaintext);
}
(void)group.chunkStore.removeChunk(
conversationId,
write.manifest.context.fileId,
index);
finishFileChunkDownload(
conversationId,
ProtectedFileSaveResult::LocalFailure);
return;
}
write.written += std::uint64_t(plaintext->size());
++write.nextChunkIndex;
Cleanse(*plaintext);
scheduleNext();
return;
}
auto hash = Digest();
auto hashSize = 0U;
const auto verified = write.written
== write.manifest.context.plaintextSize
&& EVP_DigestFinal_ex(
write.digest,
hash.bytes.data(),
&hashSize) == 1
&& hashSize == hash.bytes.size()
&& hash == write.manifest.plaintextHash;
EVP_MD_CTX_free(write.digest);
write.digest = nullptr;
if (!verified || !write.output->commit()) {
finishFileChunkDownload(
conversationId,
ProtectedFileSaveResult::LocalFailure);
return;
}
write.output.reset();
write.committed = true;
scheduleNext();
}
void DesktopService::finishFileChunkDownload(
ConversationId conversationId,
ProtectedFileSaveResult result) {
const auto i = _groups.find(conversationId);
if (i == end(_groups) || !i->second->pendingFileDownload) {
return;
}
auto &group = *i->second;
group.fileDownloadController.reset();
group.fileDownloadTransport.reset();
group.fileDownloadBackend.reset();
if (result == ProtectedFileSaveResult::Saved) {
group.localProtectedFilePaths.insert_or_assign(
group.pendingFileDownload->eventObjectId,
group.pendingFileDownload->path);
}
auto callback = std::move(group.pendingFileDownload->callback);
group.pendingFileDownload.reset();
if (group.observationDirty) {
beginGroupObservation(conversationId);
}
const auto weak = base::weak_ptr(this);
if (callback) {
callback(result);
}
if (weak) {
(void)weak->startNextProtectedFileDownload(conversationId);
}
}
bool DesktopService::setProtectedDefaultHistory(
ConversationId conversationId,
HistoryAccess historyAccess) {
if (!IsValidHistoryAccess(historyAccess)) {
return false;
}
return applyAdministrativeTransition(conversationId, {
.kind = GroupTransitionKind::SetDefaultHistory,
.targetRole = GroupRole::Member,
.historyAccess = historyAccess,
});
}
bool DesktopService::setProtectedMemberHistory(
ConversationId conversationId,
AccountId accountId,
HistoryAccess historyAccess) {
if (!accountId || !IsValidHistoryAccess(historyAccess)) {
return false;
}
return applyAdministrativeTransition(conversationId, {
.kind = GroupTransitionKind::SetMemberHistory,
.targetAccountId = accountId,
.targetRole = GroupRole::Member,
.historyAccess = historyAccess,
});
}
bool DesktopService::setProtectedMemberRole(
ConversationId conversationId,
AccountId accountId,
GroupRole role) {
if (!accountId
|| (role != GroupRole::Member
&& role != GroupRole::Administrator)) {
return false;
}
return applyAdministrativeTransition(conversationId, {
.kind = GroupTransitionKind::SetRole,
.targetAccountId = accountId,
.targetRole = role,
.targetAdminPermissions = (role == GroupRole::Administrator)
? kDefaultAdminPermissions
: 0,
.historyAccess = {},
});
}
bool DesktopService::removeProtectedMember(
ConversationId conversationId,
AccountId accountId) {
if (!accountId) {
return false;
}
return applyAdministrativeTransition(conversationId, {
.kind = GroupTransitionKind::RemoveMember,
.targetAccountId = accountId,
.targetRole = GroupRole::Member,
.historyAccess = {},
});
}
DesktopContentState DesktopService::contentState(
ConversationId conversationId) const {
const auto i = _contentStates.find(conversationId);
return (i != end(_contentStates))
? i->second
: DesktopContentState::Idle;
}
rpl::producer<DesktopContentState> DesktopService::contentStateValue() const {
return _contentState.value();
}
void DesktopService::setContentState(
ConversationId conversationId,
DesktopContentState state) {
_contentStates[conversationId] = state;
_contentState.force_assign(state);
}
rpl::producer<std::uint64_t> DesktopService::contentRevisionValue() const {
return _contentRevision.value();
}
void DesktopService::notifyContentRevision() {
for (const auto &entry : _groups) {
refreshMaterializedProtectedHistory(*entry.second);
}
const auto revision = _contentRevision.current();
if (revision != std::numeric_limits<std::uint64_t>::max()) {
_contentRevision = revision + 1;
}
}
auto DesktopService::fileTransferRevisionValue() const
-> rpl::producer<std::uint64_t> {
return _fileTransferRevision.value();
}
void DesktopService::notifyFileTransferRevision() {
const auto revision = _fileTransferRevision.current();
if (revision != std::numeric_limits<std::uint64_t>::max()) {
_fileTransferRevision = revision + 1;
}
}
rpl::producer<std::uint64_t> DesktopService::securityRevisionValue() const {
return _securityRevision.value();
}
void DesktopService::notifySecurityRevision() {
const auto revision = _securityRevision.current();
if (revision != std::numeric_limits<std::uint64_t>::max()) {
_securityRevision = revision + 1;
}
}
void DesktopService::synchronizeProtectedContent(
ConversationId conversationId) {
if (!vaultReady()) {
return;
}
beginGroupObservation(conversationId);
pumpActiveOutbox(conversationId);
beginContentObservation(conversationId);
}
void DesktopService::lock() {
const auto securityBlocked = _vaultState.current()
== DesktopVaultState::SecurityBlocked;
const auto knownVault = _vault || _vaultAnchor.anchor().has_value();
if (++_operationEpoch == 0) {
++_operationEpoch;
}
_sync.reset();
_pendingCreation.reset();
if (_pendingGroupCreation) {
clearMaterializedProtectedHistory(*_pendingGroupCreation);
}
for (const auto &entry : _groups) {
clearMaterializedProtectedHistory(*entry.second);
if (!entry.second->fileTransfer.pending()) {
RemoveStagedProtectedSource(
_telegramUserIdBinding,
entry.first,
entry.second->filePreparationPath);
}
}
_pendingGroupCreation.reset();
_pendingGroupJoin.reset();
_pendingGroupDiscovery.reset();
_groupDiscoveryQueue.clear();
_groups.clear();
CleanupProtectedMediaCache(_session);
_vault.reset();
Cleanse(_pendingUnlockPassword);
Cleanse(_unlockedPassword);
if (securityBlocked) {
_vaultState = DesktopVaultState::SecurityBlocked;
} else if (knownVault) {
_vaultState = DesktopVaultState::Locked;
} else {
_vaultState = DesktopVaultState::Uninitialized;
}
_groupCreationState = DesktopGroupCreationState::Idle;
_contentState = DesktopContentState::Idle;
_contentStates.clear();
notifyFileTransferRevision();
notifyContentRevision();
notifySecurityRevision();
}
void DesktopService::applyVaultDiscoveryResult(
CloudVaultSyncCompletion result) {
_sync.reset();
switch (result.status) {
case CloudVaultSyncStatus::Present:
_vaultState = DesktopVaultState::Locked;
break;
case CloudVaultSyncStatus::Missing:
_vaultState = _vaultAnchor.anchor()
? DesktopVaultState::SecurityBlocked
: DesktopVaultState::Missing;
break;
case CloudVaultSyncStatus::RetryableTransportError:
_vaultState = DesktopVaultState::DiscoveryRetryableError;
break;
case CloudVaultSyncStatus::PermanentTransportError:
_vaultState = DesktopVaultState::DiscoveryPermanentError;
break;
case CloudVaultSyncStatus::CapacityExceeded:
case CloudVaultSyncStatus::InvalidPagination:
case CloudVaultSyncStatus::Selected:
case CloudVaultSyncStatus::Unreadable:
case CloudVaultSyncStatus::IdentityConflict:
case CloudVaultSyncStatus::ForkDetected:
case CloudVaultSyncStatus::RollbackDetected:
case CloudVaultSyncStatus::ChainGap:
_vaultState = DesktopVaultState::SecurityBlocked;
break;
case CloudVaultSyncStatus::Cancelled:
_vaultState = DesktopVaultState::Uninitialized;
break;
}
}
void DesktopService::applyVaultCreationDiscoveryResult(
CloudVaultSyncCompletion result) {
_sync.reset();
if (result.status == CloudVaultSyncStatus::Missing) {
const auto operationEpoch = _operationEpoch;
CreateCloudVaultOffMain(
_telegramUserIdBinding,
_pendingUnlockPassword,
DesktopArgon2idConfig(),
[weak = base::weak_ptr(this), operationEpoch](
std::optional<CreatedCloudVault> created) mutable {
if (!weak
|| weak->_operationEpoch != operationEpoch
|| weak->_vaultState.current()
!= DesktopVaultState::Creating
|| weak->_sync
|| weak->_pendingCreation) {
return;
}
weak->_pendingCreation = std::move(created);
if (weak->_pendingCreation) {
weak->uploadPendingCreation();
} else {
Cleanse(weak->_pendingUnlockPassword);
weak->_vaultState
= DesktopVaultState::WrongPasswordOrDamaged;
}
});
return;
} else if (result.status == CloudVaultSyncStatus::Present) {
_vaultState = DesktopVaultState::Locked;
} else if (result.status
== CloudVaultSyncStatus::RetryableTransportError) {
_vaultState = DesktopVaultState::DiscoveryRetryableError;
} else if (result.status
== CloudVaultSyncStatus::PermanentTransportError) {
_vaultState = DesktopVaultState::DiscoveryPermanentError;
} else if (result.status == CloudVaultSyncStatus::Cancelled) {
_vaultState = DesktopVaultState::Missing;
} else {
_vaultState = DesktopVaultState::SecurityBlocked;
}
Cleanse(_pendingUnlockPassword);
}
void DesktopService::applySyncResult(CloudVaultSyncCompletion result) {
_sync.reset();
switch (result.status) {
case CloudVaultSyncStatus::Selected:
if (result.vault) {
auto selected = std::move(*result.vault);
if (!commitVaultAnchor(selected)) {
_vaultState = DesktopVaultState::SecurityBlocked;
} else {
_vault = std::move(selected);
Cleanse(_unlockedPassword);
_unlockedPassword = std::move(_pendingUnlockPassword);
_vaultState = DesktopVaultState::Ready;
resumePendingGroupCreation();
}
} else {
_vaultState = DesktopVaultState::WrongPasswordOrDamaged;
}
break;
case CloudVaultSyncStatus::Present:
_vaultState = DesktopVaultState::SecurityBlocked;
break;
case CloudVaultSyncStatus::Missing:
_vaultState = DesktopVaultState::Missing;
break;
case CloudVaultSyncStatus::Unreadable:
_vaultState = DesktopVaultState::WrongPasswordOrDamaged;
break;
case CloudVaultSyncStatus::RetryableTransportError:
_vaultState = DesktopVaultState::RetryableTransportError;
break;
case CloudVaultSyncStatus::PermanentTransportError:
_vaultState = DesktopVaultState::PermanentTransportError;
break;
case CloudVaultSyncStatus::CapacityExceeded:
case CloudVaultSyncStatus::IdentityConflict:
case CloudVaultSyncStatus::ForkDetected:
case CloudVaultSyncStatus::RollbackDetected:
case CloudVaultSyncStatus::ChainGap:
case CloudVaultSyncStatus::InvalidPagination:
_vaultState = DesktopVaultState::SecurityBlocked;
break;
case CloudVaultSyncStatus::Cancelled:
_vaultState = DesktopVaultState::Locked;
break;
}
if (_vaultState.current() != DesktopVaultState::Ready) {
Cleanse(_pendingUnlockPassword);
}
}
void DesktopService::uploadPendingCreation() {
if (!_pendingCreation) {
return;
}
_vaultState = DesktopVaultState::Creating;
const auto operationEpoch = _operationEpoch;
_remote->uploadExact(
_pendingCreation->encoded,
[weak = base::weak_ptr(this), operationEpoch](
TelegramTransport::UploadResult result) {
if (!weak
|| weak->_operationEpoch != operationEpoch
|| !weak->_pendingCreation) {
return;
}
if (result == TelegramTransport::UploadResult::Accepted) {
if (!weak->commitVaultAnchor(
weak->_pendingCreation->unlocked)) {
weak->_pendingCreation.reset();
Cleanse(weak->_pendingUnlockPassword);
weak->_vaultState = DesktopVaultState::SecurityBlocked;
return;
}
weak->_vault = std::move(weak->_pendingCreation->unlocked);
weak->_pendingCreation.reset();
Cleanse(weak->_unlockedPassword);
weak->_unlockedPassword = std::move(
weak->_pendingUnlockPassword);
weak->_vaultState = DesktopVaultState::Ready;
weak->resumePendingGroupCreation();
} else if (result
== TelegramTransport::UploadResult::RetryableError) {
weak->_vaultState
= DesktopVaultState::RetryableTransportError;
} else {
weak->_pendingCreation.reset();
Cleanse(weak->_pendingUnlockPassword);
weak->_vaultState
= DesktopVaultState::PermanentTransportError;
}
});
}
void DesktopService::beginGroupVaultPreflight() {
if (!vaultReady()) {
return;
}
if (!_pendingGroupCreation
|| !_pendingGroupCreation->vaultPreflightRequired
|| _sync
|| _unlockedPassword.isEmpty()) {
_groupCreationState = DesktopGroupCreationState::LocalFailure;
return;
}
if (!_vaultAnchor.anchor()) {
_groupCreationState = DesktopGroupCreationState::LocalFailure;
return;
}
_groupCreationState = DesktopGroupCreationState::UpdatingVault;
_sync = std::make_unique<CloudVaultSyncController>(
_telegramUserIdBinding,
*_remote,
_vaultSelector,
[weak = base::weak_ptr(this)](CloudVaultSyncCompletion result) {
if (weak) {
weak->applyGroupVaultSyncResult(std::move(result));
}
},
SelectCloudVaultOffMain);
if (!_sync->start(_unlockedPassword, _vaultAnchor.anchor())) {
_sync.reset();
_groupCreationState = DesktopGroupCreationState::LocalFailure;
}
}
void DesktopService::applyGroupVaultSyncResult(
CloudVaultSyncCompletion result) {
if (!vaultReady()) {
return;
}
_sync.reset();
if (!_pendingGroupCreation) {
return;
}
if (result.status == CloudVaultSyncStatus::RetryableTransportError) {
_groupCreationState = DesktopGroupCreationState::RetryableTransportError;
return;
} else if (result.status == CloudVaultSyncStatus::PermanentTransportError) {
_groupCreationState = DesktopGroupCreationState::PermanentTransportError;
return;
} else if (result.status != CloudVaultSyncStatus::Selected
|| !result.vault) {
_vaultState = DesktopVaultState::SecurityBlocked;
_groupCreationState = DesktopGroupCreationState::LocalFailure;
return;
}
auto selected = std::move(*result.vault);
if (!commitVaultAnchor(selected)) {
_vaultState = DesktopVaultState::SecurityBlocked;
_groupCreationState = DesktopGroupCreationState::LocalFailure;
return;
}
_vault = std::move(selected);
const auto &conversation = _pendingGroupCreation->conversation;
auto conversations = _vault->conversations;
const auto existing = std::find_if(
begin(conversations),
end(conversations),
[&](const CloudVaultConversation &value) {
return value.conversationId == conversation.conversationId
|| value.telegramPeerIdBinding
== conversation.telegramPeerIdBinding;
});
if (existing != end(conversations)) {
if (existing->conversationId != conversation.conversationId
|| existing->telegramPeerIdBinding
!= conversation.telegramPeerIdBinding
|| existing->ownerAccountId != conversation.ownerAccountId
|| existing->checkpoint.conversationId
!= conversation.conversationId
|| (existing->checkpoint.generation
== conversation.checkpoint.generation
&& existing->checkpoint != conversation.checkpoint)
|| existing->checkpoint.generation
> conversation.checkpoint.generation) {
_vaultState = DesktopVaultState::SecurityBlocked;
_groupCreationState = DesktopGroupCreationState::LocalFailure;
return;
} else if (*existing == conversation) {
_pendingGroupCreation->vaultPreflightRequired = false;
publishNextBootstrapObject();
return;
}
*existing = conversation;
} else {
conversations.push_back(conversation);
}
_pendingGroupCreation->vaultUpdate = _vaultCodec.prepareUpdate(
*_vault,
std::move(conversations));
if (!_pendingGroupCreation->vaultUpdate) {
_groupCreationState = DesktopGroupCreationState::LocalFailure;
return;
}
_pendingGroupCreation->vaultPreflightRequired = false;
uploadPendingConversationIndex();
}
void DesktopService::uploadPendingConversationIndex() {
if (!vaultReady()
|| !_pendingGroupCreation
|| !_pendingGroupCreation->vaultUpdate
|| _pendingGroupCreation->uploadInProgress) {
return;
}
_pendingGroupCreation->uploadInProgress = true;
_groupCreationState = DesktopGroupCreationState::UpdatingVault;
const auto operationEpoch = _operationEpoch;
_remote->uploadExact(
_pendingGroupCreation->vaultUpdate->encoded,
[weak = base::weak_ptr(this), operationEpoch](
TelegramTransport::UploadResult result) {
if (!weak
|| !weak->vaultReady()
|| weak->_operationEpoch != operationEpoch
|| !weak->_pendingGroupCreation
|| !weak->_pendingGroupCreation->vaultUpdate
|| !weak->_vault) {
return;
}
weak->_pendingGroupCreation->uploadInProgress = false;
if (result == TelegramTransport::UploadResult::Accepted) {
if (!weak->commitVaultAnchor(
*weak->_pendingGroupCreation->vaultUpdate,
weak->_vault->identity)) {
weak->_vaultState = DesktopVaultState::SecurityBlocked;
weak->_groupCreationState
= DesktopGroupCreationState::LocalFailure;
return;
}
auto update = std::move(
*weak->_pendingGroupCreation->vaultUpdate);
weak->_pendingGroupCreation->vaultUpdate.reset();
if (!weak->_vaultCodec.applyPublished(
*weak->_vault,
std::move(update))) {
weak->_vaultState = DesktopVaultState::SecurityBlocked;
weak->_groupCreationState
= DesktopGroupCreationState::LocalFailure;
return;
}
weak->publishNextBootstrapObject();
} else if (result
== TelegramTransport::UploadResult::RetryableError) {
weak->_groupCreationState
= DesktopGroupCreationState::RetryableTransportError;
} else {
weak->_groupCreationState
= DesktopGroupCreationState::PermanentTransportError;
}
});
}
void DesktopService::publishNextBootstrapObject() {
if (!vaultReady()
|| !_pendingGroupCreation
|| _pendingGroupCreation->uploadInProgress
|| _pendingGroupCreation->vaultUpdate) {
return;
}
auto &group = *_pendingGroupCreation;
if (group.phase == PendingGroupCreation::Phase::Active) {
const auto recovered = recoverQueuedContentHistory(group);
if (recovered == QueuedContentRecoveryResult::Invalid) {
_vaultState = DesktopVaultState::SecurityBlocked;
_groupCreationState = DesktopGroupCreationState::LocalFailure;
return;
} else if (recovered
== QueuedContentRecoveryResult::PersistenceFailed) {
_groupCreationState = DesktopGroupCreationState::LocalFailure;
return;
}
}
if (const auto pending = group.fileTransfer.pending();
pending
&& group.phase == PendingGroupCreation::Phase::Removed
&& !pending->cancelRequested) {
const auto requested = group.fileTransfer.requestCancel();
if (requested != FileTransferCommitResult::Committed
&& requested != FileTransferCommitResult::AlreadyCommitted) {
_groupCreationState = DesktopGroupCreationState::LocalFailure;
return;
}
}
if (const auto pending = group.fileTransfer.pending();
pending
&& pending->cancelRequested) {
const auto result = finishFileTransferCancellation(group);
if (result
== FileTransferCancellationResult::RetryableCleanupFailure) {
_groupCreationState
= DesktopGroupCreationState::PublishingBootstrap;
scheduleFileTransferRetry(group.conversationId);
return;
} else if (result == FileTransferCancellationResult::Failure) {
_groupCreationState = DesktopGroupCreationState::LocalFailure;
return;
}
}
if (group.phase == PendingGroupCreation::Phase::Removed
&& !group.outbox.clear()) {
_groupCreationState = DesktopGroupCreationState::LocalFailure;
return;
}
if (_pendingGroupCreation->phase == PendingGroupCreation::Phase::Active
&& _pendingGroupCreation->freshnessGate
&& _pendingGroupCreation->freshnessGate->state()
== FreshnessState::Required
&& !_pendingGroupCreation->outbox.size()
&& !queueFreshnessChallenge(*_pendingGroupCreation)) {
_groupCreationState = DesktopGroupCreationState::LocalFailure;
return;
}
auto item = _pendingGroupCreation->outbox.front(
_pendingGroupCreation->conversationId);
const auto itemGuard = qScopeGuard([&] {
if (item) {
CleanseOutboxItem(*item);
}
});
if (!item) {
if (_pendingGroupCreation->vaultPreflightRequired) {
beginGroupVaultPreflight();
return;
}
const auto conversationId = _pendingGroupCreation->conversationId;
const auto phase = _pendingGroupCreation->phase;
if (phase == PendingGroupCreation::Phase::Creating) {
_pendingGroupCreation->phase = PendingGroupCreation::Phase::Active;
}
_groups[conversationId] = std::move(_pendingGroupCreation);
if (_groups[conversationId]->phase
== PendingGroupCreation::Phase::Active
&& (!resumeObservedJoinHistory(conversationId)
|| !initializeActivePipeline(*_groups[conversationId]))) {
_groupCreationState = DesktopGroupCreationState::LocalFailure;
setContentState(
conversationId,
DesktopContentState::LocalFailure);
return;
}
if (phase == PendingGroupCreation::Phase::AwaitingAdmission) {
_groupCreationState = DesktopGroupCreationState::AwaitingAdmission;
} else if (_groups[conversationId]->freshnessGate
&& !_groups[conversationId]
->freshnessGate->sendingAllowed()
&& phase != PendingGroupCreation::Phase::Removed) {
_groupCreationState = DesktopGroupCreationState::AwaitingFreshness;
} else {
_groupCreationState = DesktopGroupCreationState::Ready;
}
beginGroupObservation(conversationId);
beginContentObservation(conversationId);
resumeDeferredGroupObservations();
resumePendingGroupCreation();
startNextGroupDiscovery();
return;
} else if (item->stage != OutboxItemStage::Sealed
|| !item->sealed) {
_groupCreationState = DesktopGroupCreationState::LocalFailure;
return;
}
const auto persistedEnvelope = _pendingGroupCreation->envelopeCodec.decode(
*item->sealed);
if (_pendingGroupCreation->phase == PendingGroupCreation::Phase::Active
&& persistedEnvelope
&& persistedEnvelope->objectKind
== ObjectKind::FreshnessChallenge
&& !resumeQueuedFreshnessChallenge(
*_pendingGroupCreation,
*item->sealed)) {
_groupCreationState = DesktopGroupCreationState::LocalFailure;
return;
}
const auto objectId = item->sealed->objectId;
_pendingGroupCreation->uploadInProgress = true;
_groupCreationState = DesktopGroupCreationState::PublishingBootstrap;
_pendingGroupCreation->transport.uploadExact(
*item->sealed,
[weak = base::weak_ptr(this), objectId](
TelegramTransport::UploadResult result) {
if (!weak
|| !weak->vaultReady()
|| !weak->_pendingGroupCreation) {
return;
}
weak->_pendingGroupCreation->uploadInProgress = false;
if (result == TelegramTransport::UploadResult::Accepted) {
if (!weak->prepareFileManifestAcknowledgement(
*weak->_pendingGroupCreation,
objectId)
|| !weak->_pendingGroupCreation->outbox.remove(objectId)) {
weak->_groupCreationState
= DesktopGroupCreationState::LocalFailure;
return;
}
const auto reconciled = ReconcileMlsOutboxReceipts(
weak->_pendingGroupCreation->mlsState,
weak->_pendingGroupCreation->outbox,
weak->_pendingGroupCreation->envelopeCodec,
weak->_pendingGroupCreation->inboundJournal);
if (reconciled == MlsReceiptReconcileResult::ObjectIdConflict) {
weak->_vaultState = DesktopVaultState::SecurityBlocked;
weak->_groupCreationState
= DesktopGroupCreationState::LocalFailure;
return;
} else if (reconciled
!= MlsReceiptReconcileResult::Reconciled
&& reconciled
!= MlsReceiptReconcileResult::NothingToDo) {
weak->_groupCreationState
= DesktopGroupCreationState::LocalFailure;
return;
}
weak->publishNextBootstrapObject();
} else if (result
== TelegramTransport::UploadResult::RetryableError) {
weak->_groupCreationState
= DesktopGroupCreationState::RetryableTransportError;
} else {
weak->_groupCreationState
= DesktopGroupCreationState::PermanentTransportError;
}
});
}
void DesktopService::resumePendingGroupCreation() {
if (!vaultReady() || _pendingGroupCreation) {
return;
}
for (const auto &conversation : _vault->conversations) {
rememberProtectedPeerForPresentation(
conversation.telegramPeerIdBinding);
}
for (const auto &conversation : _vault->conversations) {
if (_groups.contains(conversation.conversationId)) {
continue;
}
const auto directory = ConversationDirectory(
_telegramUserIdBinding,
conversation.conversationId);
if (QFileInfo::exists(directory + u"conversation.meta"_q)
&& ConversationSetupPending(directory)) {
(void)FinishConversationSetup(directory);
}
const auto result = restoreLocalGroup(
conversation.conversationId,
&conversation);
if (result == LocalGroupRecoveryResult::Restored) {
publishNextBootstrapObject();
return;
} else if (result == LocalGroupRecoveryResult::Missing) {
if (ConversationSetupPending(directory)) {
if (!DiscardUncommittedConversationDirectory(directory)) {
_groupCreationState
= DesktopGroupCreationState::LocalFailure;
return;
}
} else if (QDir(directory).exists()) {
_vaultState = DesktopVaultState::SecurityBlocked;
_groupCreationState = DesktopGroupCreationState::LocalFailure;
return;
}
beginIndexedGroupJoin(conversation);
return;
} else if (result == LocalGroupRecoveryResult::Invalid) {
_vaultState = DesktopVaultState::SecurityBlocked;
_groupCreationState = DesktopGroupCreationState::LocalFailure;
return;
}
}
auto entries = QDir(AccountDirectory(_telegramUserIdBinding))
.entryList(
QDir::Dirs | QDir::NoDotAndDotDot | QDir::NoSymLinks,
QDir::Name);
auto cleanupFailed = false;
auto unexpectedDirectory = false;
entries.erase(
std::remove_if(
entries.begin(),
entries.end(),
[&](const QString &entry) {
const auto conversationId = DecodeConversationDirectory(entry);
if (!conversationId) {
return true;
}
const auto directory = ConversationDirectory(
_telegramUserIdBinding,
*conversationId);
if (QFileInfo::exists(directory + u"conversation.meta"_q)) {
if (ConversationSetupPending(directory)) {
(void)FinishConversationSetup(directory);
}
return false;
}
if (!ConversationSetupPending(directory)) {
unexpectedDirectory = true;
return false;
}
const auto discarded
= DiscardUncommittedConversationDirectory(directory);
cleanupFailed = cleanupFailed || !discarded;
return discarded;
}),
entries.end());
if (cleanupFailed) {
_groupCreationState = DesktopGroupCreationState::LocalFailure;
return;
} else if (unexpectedDirectory || entries.size() > 512) {
_vaultState = DesktopVaultState::SecurityBlocked;
_groupCreationState = DesktopGroupCreationState::LocalFailure;
return;
}
auto indexed = std::set<ConversationId>();
for (const auto &conversation : _vault->conversations) {
indexed.emplace(conversation.conversationId);
}
for (const auto &entry : entries) {
const auto conversationId = DecodeConversationDirectory(entry);
if (!conversationId || indexed.contains(*conversationId)) {
continue;
}
const auto result = restoreLocalGroup(*conversationId, nullptr);
if (result == LocalGroupRecoveryResult::Restored) {
publishNextBootstrapObject();
return;
} else if (result == LocalGroupRecoveryResult::Invalid) {
_vaultState = DesktopVaultState::SecurityBlocked;
_groupCreationState = DesktopGroupCreationState::LocalFailure;
return;
}
}
queueLoadedGroupDiscoveries();
startNextGroupDiscovery();
}
void DesktopService::beginIndexedGroupJoin(
const CloudVaultConversation &conversation) {
if (!vaultReady()
|| _pendingGroupJoin
|| _pendingGroupCreation
|| _groups.contains(conversation.conversationId)) {
return;
}
if (!IsProtectedGroupPeerBinding(
_session,
conversation.telegramPeerIdBinding)) {
_vaultState = DesktopVaultState::SecurityBlocked;
_groupCreationState = DesktopGroupCreationState::LocalFailure;
return;
}
_pendingGroupJoin = std::make_unique<PendingGroupJoin>(
_session,
conversation);
_groupCreationState = DesktopGroupCreationState::Preparing;
_pendingGroupJoin->sync =
std::make_unique<PublicBootstrapSyncController>(
conversation.conversationId,
conversation.telegramPeerIdBinding,
conversation.ownerAccountId,
_pendingGroupJoin->controlTransport,
_pendingGroupJoin->envelopeCodec,
_sha256,
[weak = base::weak_ptr(this)](
PublicBootstrapSyncCompletion result) {
if (weak) {
weak->applyPublicBootstrapSyncResult(std::move(result));
}
});
if (!_pendingGroupJoin->sync->startForJoin()) {
_pendingGroupJoin.reset();
_groupCreationState
= DesktopGroupCreationState::RetryableTransportError;
resumeDeferredGroupObservations();
}
}
void DesktopService::applyPublicBootstrapSyncResult(
PublicBootstrapSyncCompletion result) {
if (!vaultReady() || !_pendingGroupJoin) {
return;
}
_pendingGroupJoin->sync.reset();
if (result.status == PublicBootstrapSyncStatus::RetryableTransportError
|| result.status == PublicBootstrapSyncStatus::Missing) {
_pendingGroupJoin.reset();
_groupCreationState = DesktopGroupCreationState::RetryableTransportError;
resumeDeferredGroupObservations();
return;
} else if (result.status
== PublicBootstrapSyncStatus::PermanentTransportError) {
_pendingGroupJoin.reset();
_groupCreationState = DesktopGroupCreationState::PermanentTransportError;
resumeDeferredGroupObservations();
return;
} else if (result.status != PublicBootstrapSyncStatus::Verified
|| !result.verified) {
_pendingGroupJoin.reset();
_vaultState = DesktopVaultState::SecurityBlocked;
_groupCreationState = DesktopGroupCreationState::LocalFailure;
return;
}
auto conversation = _pendingGroupJoin->conversation;
auto verified = std::move(*result.verified);
auto objects = std::move(result.untrustedObjects);
if (!BootstrapMatchesConversation(verified, conversation)) {
_pendingGroupJoin.reset();
_vaultState = DesktopVaultState::SecurityBlocked;
_groupCreationState = DesktopGroupCreationState::LocalFailure;
return;
}
_pendingGroupJoin.reset();
if (!prepareGroupJoin(
std::move(conversation),
std::move(verified),
std::move(objects),
false)) {
_groupCreationState = DesktopGroupCreationState::LocalFailure;
resumeDeferredGroupObservations();
}
}
bool DesktopService::prepareGroupJoin(
CloudVaultConversation conversation,
VerifiedPublicGroupBootstrap verified,
std::vector<TelegramTransport::UntrustedObject> objects,
bool discovered) {
if (!vaultReady()
|| _pendingGroupCreation
|| _groups.contains(conversation.conversationId)
|| !BootstrapMatchesConversation(verified, conversation)) {
return false;
}
const auto accountId = DeriveAccountId(
_vault->identity.credential,
_sha256);
const auto clientId = RandomId<ClientId>();
auto localRecordKey = DeriveConversationLocalRecordKey(
_vault->masterKey,
_telegramUserIdBinding,
conversation.conversationId);
if (!accountId || !clientId || !localRecordKey) {
_groupCreationState = DesktopGroupCreationState::LocalFailure;
return false;
}
auto operation = std::make_unique<PendingGroupCreation>(
ConversationDirectory(
_telegramUserIdBinding,
conversation.conversationId),
std::move(*localRecordKey),
_session,
conversation.telegramPeerIdBinding,
conversation.conversationId,
_sha256);
if (operation->metadata.load(conversation.conversationId)
!= ConversationMetadataLoadResult::Missing
|| operation->journal.load(conversation.conversationId)
!= GroupBootstrapJournalLoadResult::Empty
|| operation->mlsState.load(conversation.conversationId)
!= MlsStateLoadResult::Missing
|| operation->archiveState.load(conversation.conversationId)
!= ArchiveStateLoadResult::Missing
|| operation->groupLedger.load(conversation.conversationId)
!= GroupLedgerLoadResult::Missing
|| operation->outbox.load() != PersistentOutboxLoadResult::Empty
|| operation->changeJournal.load(conversation.conversationId)
!= GroupChangeJournalLoadResult::Empty
|| operation->changeInbox.load(conversation.conversationId)
!= GroupChangeInboxLoadResult::Missing
|| operation->keyPackages.load(
conversation.conversationId,
conversation.telegramPeerIdBinding)
!= KeyPackagePoolLoadResult::Empty
|| operation->freshnessTrust.load(conversation.conversationId)
!= FreshnessTrustLoadResult::Missing
|| operation->inboundJournal.load()
!= InboundJournalLoadResult::Missing
|| operation->controlInboundJournal.load()
!= InboundJournalLoadResult::Missing
|| operation->contentStore.load()
!= ContentStoreLoadResult::Missing
|| operation->controlSyncState.load(conversation.conversationId)
!= ControlObservationStateLoadResult::Missing
|| operation->contentSyncState.load(conversation.conversationId)
!= ContentSyncStateLoadResult::Missing
|| operation->fileTransfer.load(conversation.conversationId)
!= FileTransferLoadResult::Empty) {
_groupCreationState = DesktopGroupCreationState::LocalFailure;
return false;
}
CleanupStagedProtectedSources(operation->directory, QString());
if (!BeginConversationSetup(operation->directory)) {
DiscardUncommittedConversationDirectory(operation->directory);
_groupCreationState = DesktopGroupCreationState::LocalFailure;
return false;
}
if (operation->groupLedger.initialize(
verified.genesis,
verified.state,
verified.checkpoint,
verified.ownerCredential)
!= GroupLedgerCommitResult::Committed
|| operation->freshnessTrust.initialize(false)
!= FreshnessTrustCommitResult::Committed) {
DiscardUncommittedConversationDirectory(operation->directory);
_groupCreationState = DesktopGroupCreationState::LocalFailure;
return false;
}
const auto staged = StagePublicJoinObjects(
objects,
conversation.conversationId,
conversation.telegramPeerIdBinding,
operation->envelopeCodec,
_sha256,
operation->changeInbox);
const auto reconstructed = (staged == PublicJoinInboxStageStatus::Staged)
? ReconstructPublicJoinObjects(
conversation.conversationId,
conversation.telegramPeerIdBinding,
operation->envelopeCodec,
operation->changeInbox)
: std::nullopt;
const auto createdAt = std::uint64_t(base::unixtime::now());
const auto catchup = reconstructed
? CatchUpPublicJoin(
*reconstructed,
conversation.conversationId,
conversation.telegramPeerIdBinding,
*accountId,
*clientId,
_vault->identity.credential,
createdAt,
operation->envelopeCodec,
_sha256,
operation->groupLedger,
operation->keyPackages)
: PublicJoinCatchupOutcome();
const auto caughtUpCheckpoint = operation->groupLedger.checkpoint();
const auto reachedIndexedCheckpoint = ValidConversationCheckpoint(
conversation.checkpoint,
conversation.conversationId)
&& caughtUpCheckpoint.generation >= conversation.checkpoint.generation
&& operation->groupLedger.checkpointAt(
conversation.checkpoint.generation) == conversation.checkpoint;
if (!reconstructed
|| (catchup.status != PublicJoinCatchupStatus::Waiting
&& catchup.status
!= PublicJoinCatchupStatus::UpdatedWaiting)
|| !reachedIndexedCheckpoint
|| !operation->changeInbox.discardAppliedTransitions(
caughtUpCheckpoint.generation)) {
DiscardUncommittedConversationDirectory(operation->directory);
_groupCreationState = DesktopGroupCreationState::LocalFailure;
return false;
}
const auto catchupAdvanced = caughtUpCheckpoint != conversation.checkpoint;
conversation.checkpoint = caughtUpCheckpoint;
auto keyPackage = PrepareClientKeyPackage({
.client = {
.conversationId = conversation.conversationId,
.accountId = *accountId,
.clientId = *clientId,
.telegramPeerIdBinding = conversation.telegramPeerIdBinding,
},
.currentGeneration = caughtUpCheckpoint.generation,
.telegramUserIdBinding = _telegramUserIdBinding,
.createdAt = createdAt,
.accountCredential = &_vault->identity.credential,
.accountSigningPrivateKey = &_vault->identity.signingPrivateKey,
},
OpenMlsBridge(),
MlsContextCodecV1(),
ClientKeyPackagePublicationCodecV1(),
operation->envelopeCodec,
_sha256);
if (keyPackage.status != PrepareClientKeyPackageStatus::Prepared
|| !keyPackage.entry
|| operation->keyPackages.add(std::move(*keyPackage.entry))
!= KeyPackagePoolMutationResult::Committed
|| operation->keyPackages.enqueuePending(
operation->outbox,
createdAt) != KeyPackagePoolEnqueueResult::Queued
|| operation->metadata.initialize({
.conversationId = conversation.conversationId,
.telegramPeerIdBinding = conversation.telegramPeerIdBinding,
.accountId = *accountId,
.clientId = *clientId,
}) != ConversationMetadataCommitResult::Committed) {
DiscardUncommittedConversationDirectory(operation->directory);
_groupCreationState = DesktopGroupCreationState::LocalFailure;
return false;
}
(void)FinishConversationSetup(operation->directory);
rememberProtectedPeerForPresentation(
conversation.telegramPeerIdBinding);
operation->conversation = conversation;
operation->joinTargetCheckpoint = conversation.checkpoint;
operation->freshnessGate = std::make_unique<FreshnessGate>(
verified.checkpoint,
false);
operation->vaultPreflightRequired = discovered || catchupAdvanced;
operation->phase = PendingGroupCreation::Phase::AwaitingAdmission;
_pendingGroupCreation = std::move(operation);
publishNextBootstrapObject();
return true;
}
bool DesktopService::queueFreshnessChallenge(
PendingGroupCreation &group) {
if (!vaultReady()
|| !group.freshnessGate
|| group.freshnessGate->state() != FreshnessState::Required) {
return false;
}
const auto metadata = group.metadata.metadata();
const auto nonce = RandomId<ChallengeNonce>();
const auto objectId = RandomId<ObjectId>();
if (!metadata
|| !nonce
|| !objectId
|| !group.freshnessGate->beginChallenge(*nonce)) {
return false;
}
const auto challenge = group.freshnessGate->challenge();
const auto envelope = challenge
? PrepareFreshnessChallengeEnvelope({
.challenge = *challenge,
.requesterAccountId = metadata->accountId,
.requesterClientId = metadata->clientId,
.telegramPeerIdBinding = group.telegramPeerIdBinding,
.objectId = *objectId,
.requesterSigningPrivateKey =
&_vault->identity.signingPrivateKey,
}, group.envelopeCodec, _sha256)
: std::nullopt;
if (!envelope || !group.outbox.appendSealed(*envelope)) {
group.freshnessGate->requireFreshness(
group.groupLedger.checkpoint());
return false;
}
return true;
}
bool DesktopService::resumeQueuedFreshnessChallenge(
PendingGroupCreation &group,
const EncodedEnvelope &encoded) {
if (!vaultReady()) {
return false;
}
const auto metadata = group.metadata.metadata();
const auto envelope = group.envelopeCodec.decode(encoded);
const auto challenge = envelope
? FreshnessChallengeCodecV1().decode(envelope->payload)
: std::nullopt;
if (!metadata
|| !group.freshnessGate
|| !envelope
|| !challenge
|| challenge->conversationId != group.conversationId
|| challenge->knownCheckpoint
!= group.freshnessGate->knownCheckpoint()
|| envelope->conversationId != group.conversationId
|| envelope->objectKind != ObjectKind::FreshnessChallenge
|| envelope->senderAccountId != metadata->accountId
|| envelope->senderClientId != metadata->clientId
|| envelope->telegramPeerIdBinding
!= group.telegramPeerIdBinding
|| envelope->epochOrGeneration
!= challenge->knownCheckpoint.generation
|| envelope->payloadHash != _sha256.digest(envelope->payload)) {
return false;
}
const auto state = group.freshnessGate->state();
return (state == FreshnessState::Required)
? group.freshnessGate->beginChallenge(challenge->nonce)
: (state == FreshnessState::WaitingForWitness
&& group.freshnessGate->challenge() == challenge);
}
bool DesktopService::processObservedFreshness(
ConversationId conversationId,
const std::vector<TelegramTransport::UntrustedObject> &objects) {
const auto i = _groups.find(conversationId);
if (!vaultReady()
|| i == end(_groups)
|| i->second->phase != PendingGroupCreation::Phase::Active) {
return false;
}
auto &group = *i->second;
const auto metadata = group.metadata.metadata();
if (!metadata || !group.freshnessGate) {
return false;
}
auto changed = false;
for (const auto &object : objects) {
if (group.freshnessGate->sendingAllowed()) {
const auto challenge = VerifyObservedFreshnessChallenge(
object,
conversationId,
group.telegramPeerIdBinding,
group.envelopeCodec,
group.groupLedger,
_sha256);
if (!challenge
|| (challenge->requesterAccountId == metadata->accountId
&& challenge->requesterClientId
== metadata->clientId)) {
continue;
}
const auto replay = group.controlInboundJournal.lookup(
conversationId,
challenge->objectId,
challenge->payloadHash);
if (replay == InboundJournalLookup::Accepted) {
continue;
} else if (replay == InboundJournalLookup::ObjectIdConflict) {
_vaultState = DesktopVaultState::SecurityBlocked;
_groupCreationState = DesktopGroupCreationState::LocalFailure;
setContentState(
conversationId,
DesktopContentState::SecurityBlocked);
return false;
} else if (replay == InboundJournalLookup::StorageError) {
_groupCreationState = DesktopGroupCreationState::LocalFailure;
setContentState(
conversationId,
DesktopContentState::LocalFailure);
return false;
}
const auto responseObjectId = FreshnessResponseObjectId(
challenge->objectId,
metadata->clientId,
_sha256);
const auto response = PrepareFreshnessResponseEnvelope({
.challenge = challenge->challenge,
.witnessCheckpoint = group.groupLedger.checkpoint(),
.witnessAccountId = metadata->accountId,
.witnessClientId = metadata->clientId,
.telegramPeerIdBinding = group.telegramPeerIdBinding,
.objectId = responseObjectId,
.witnessSigningPrivateKey =
&_vault->identity.signingPrivateKey,
}, group.envelopeCodec, _sha256);
const auto alreadyPublished = response && std::any_of(
begin(objects),
end(objects),
[&](const auto &candidate) {
return candidate.bytes == response->bytes;
});
const auto queued = response
? QueueFreshnessResponseOnce({
.conversationId = conversationId,
.challengeObjectId = challenge->objectId,
.challengePayloadHash = challenge->payloadHash,
.responseEnvelope = *response,
.responseAlreadyPublished = alreadyPublished,
}, group.outbox, group.controlInboundJournal)
: FreshnessResponseQueueResult::InvalidArguments;
if (queued == FreshnessResponseQueueResult::Queued) {
changed = true;
} else if (queued
== FreshnessResponseQueueResult::ObjectIdConflict) {
_vaultState = DesktopVaultState::SecurityBlocked;
_groupCreationState = DesktopGroupCreationState::LocalFailure;
setContentState(
conversationId,
DesktopContentState::SecurityBlocked);
return false;
} else if (queued
== FreshnessResponseQueueResult::InvalidArguments
|| queued
== FreshnessResponseQueueResult::PersistenceFailed) {
_groupCreationState = DesktopGroupCreationState::LocalFailure;
setContentState(
conversationId,
DesktopContentState::LocalFailure);
return false;
}
} else if (group.freshnessGate->state()
== FreshnessState::WaitingForWitness) {
const auto response = VerifyObservedFreshnessResponse(
object,
conversationId,
group.telegramPeerIdBinding,
group.envelopeCodec,
group.groupLedger,
_sha256);
if (!response) {
continue;
}
const auto verifier = AccountFreshnessResponseVerifier(
group.groupLedger);
const auto accepted = group.freshnessGate->acceptResponse(
response->response,
verifier);
if (accepted == FreshnessResponseResult::Accepted) {
const auto committed = group.freshnessTrust.confirm();
if (committed != FreshnessTrustCommitResult::Committed
&& committed
!= FreshnessTrustCommitResult::AlreadyCommitted) {
group.freshnessGate->requireFreshness(
group.groupLedger.checkpoint());
_groupCreationState
= DesktopGroupCreationState::LocalFailure;
return false;
}
_groupCreationState = DesktopGroupCreationState::Ready;
changed = true;
} else if (accepted == FreshnessResponseResult::ForkDetected) {
_vaultState = DesktopVaultState::SecurityBlocked;
_groupCreationState = DesktopGroupCreationState::LocalFailure;
return false;
} else if (accepted
== FreshnessResponseResult::ResynchronizationRequired) {
const auto target = group.freshnessGate
->resynchronizationTarget();
const auto knownTarget = target
? group.groupLedger.checkpointAt(target->generation)
: std::nullopt;
const auto caughtUp = target
&& knownTarget == target
&& group.freshnessGate
->completeResynchronization(
*target,
verifier)
&& group.freshnessGate->advanceTrustedCheckpoint(
group.groupLedger.checkpoint());
if (caughtUp) {
const auto committed = group.freshnessTrust.confirm();
if (committed != FreshnessTrustCommitResult::Committed
&& committed
!= FreshnessTrustCommitResult::AlreadyCommitted) {
group.freshnessGate->requireFreshness(
group.groupLedger.checkpoint());
_groupCreationState
= DesktopGroupCreationState::LocalFailure;
return false;
}
_groupCreationState = DesktopGroupCreationState::Ready;
} else {
_groupCreationState
= DesktopGroupCreationState::AwaitingFreshness;
}
changed = true;
}
}
}
return changed;
}
bool DesktopService::processObservedSafetyGossip(
ConversationId conversationId,
const std::vector<TelegramTransport::UntrustedObject> &objects) {
const auto i = _groups.find(conversationId);
if (!vaultReady()
|| i == end(_groups)
|| (i->second->phase != PendingGroupCreation::Phase::Active
&& i->second->phase != PendingGroupCreation::Phase::Removed)) {
return false;
}
auto &group = *i->second;
const auto metadata = group.metadata.metadata();
const auto state = group.groupLedger.state();
if (!metadata || !state) {
return false;
}
const auto sameGeneration = group.safetyWitnessGeneration
== state->generation();
auto witnesses = sameGeneration
? group.safetyWitnesses
: std::set<AccountId>();
auto ownCurrentGossipObserved = sameGeneration
&& group.ownSafetyGossipObserved;
for (const auto &object : objects) {
const auto observed = VerifyObservedSafetyGossip(
object,
conversationId,
group.telegramPeerIdBinding,
group.envelopeCodec,
group.groupLedger,
_sha256);
if (observed.result == SafetyGossipVerifyResult::ForkDetected
|| observed.result
== SafetyGossipVerifyResult::IdentityConflict
|| observed.result
== SafetyGossipVerifyResult::FutureCheckpoint) {
_vaultState = DesktopVaultState::SecurityBlocked;
_groupCreationState = DesktopGroupCreationState::LocalFailure;
setContentState(
conversationId,
DesktopContentState::SecurityBlocked);
return true;
} else if (observed.result != SafetyGossipVerifyResult::Verified
|| !observed.verified
|| observed.verified->gossip.checkpoint
!= group.groupLedger.checkpoint()) {
continue;
}
witnesses.emplace(observed.verified->gossip.reporterAccountId);
if (observed.verified->gossip.reporterAccountId
== metadata->accountId
&& observed.verified->gossip.reporterClientId
== metadata->clientId) {
ownCurrentGossipObserved = true;
}
}
witnesses.emplace(metadata->accountId);
const auto witnessChanged = group.safetyWitnessGeneration
!= state->generation()
|| group.safetyWitnesses != witnesses;
group.safetyWitnessGeneration = state->generation();
group.safetyWitnesses = std::move(witnesses);
group.ownSafetyGossipObserved = ownCurrentGossipObserved;
if (witnessChanged) {
notifySecurityRevision();
}
if (group.phase != PendingGroupCreation::Phase::Active
|| ownCurrentGossipObserved) {
return witnessChanged;
}
const auto gossipId = DeriveSafetyGossipObjectId(
group.groupLedger.checkpoint(),
metadata->accountId,
metadata->clientId,
_sha256);
if (!gossipId || group.outbox.contains(gossipId)) {
return witnessChanged;
}
const auto envelope = PrepareSafetyGossipEnvelope({
.gossipId = gossipId,
.reporterAccountId = metadata->accountId,
.reporterClientId = metadata->clientId,
.telegramPeerIdBinding = group.telegramPeerIdBinding,
.reporterSigningPrivateKey = &_vault->identity.signingPrivateKey,
}, group.groupLedger, group.envelopeCodec, _sha256);
if (!envelope || !group.outbox.appendSealed(*envelope)) {
setContentState(conversationId, DesktopContentState::LocalFailure);
return true;
}
return true;
}
bool DesktopService::synchronizeObservedGroupChanges(
ConversationId conversationId,
const std::vector<TelegramTransport::UntrustedObject> &objects) {
const auto i = _groups.find(conversationId);
if (!vaultReady()
|| i == end(_groups)
|| _pendingGroupCreation
|| i->second->phase != PendingGroupCreation::Phase::Active) {
return false;
}
auto &group = *i->second;
const auto metadata = group.metadata.metadata();
const auto accountId = DeriveAccountId(
_vault->identity.credential,
_sha256);
if (!metadata
|| !accountId
|| metadata->accountId != *accountId
|| !group.freshnessGate) {
_vaultState = DesktopVaultState::SecurityBlocked;
_groupCreationState = DesktopGroupCreationState::LocalFailure;
return true;
}
const auto synchronized = SynchronizeObservedGroupChanges(
objects,
{
.conversationId = conversationId,
.accountId = *accountId,
.clientId = metadata->clientId,
.telegramPeerIdBinding = group.telegramPeerIdBinding,
},
std::uint64_t(base::unixtime::now()),
group.envelopeCodec,
OpenMlsBridge(),
MlsContextCodecV1(),
MlsRosterCodecV1(),
GroupControlCodecV1(),
_sha256,
group.mlsState,
group.archiveState,
group.groupLedger,
group.changeJournal,
group.changeInbox);
if (synchronized.status == ObservedGroupChangeSyncStatus::NoChange) {
return false;
} else if (synchronized.status
== ObservedGroupChangeSyncStatus::WaitingForObjects
&& !synchronized.appliedTransitions) {
return false;
} else if (synchronized.status
== ObservedGroupChangeSyncStatus::ForkDetected
|| synchronized.status
== ObservedGroupChangeSyncStatus::InvalidState) {
_vaultState = DesktopVaultState::SecurityBlocked;
_groupCreationState = DesktopGroupCreationState::LocalFailure;
return true;
} else if (synchronized.status
== ObservedGroupChangeSyncStatus::PersistenceFailure) {
_groupCreationState = DesktopGroupCreationState::LocalFailure;
return true;
}
const auto checkpoint = group.groupLedger.checkpoint();
if (synchronized.status
== ObservedGroupChangeSyncStatus::LocalClientRemoved) {
group.phase = PendingGroupCreation::Phase::Removed;
group.freshnessGate->requireFreshness(checkpoint);
} else if (group.freshnessGate->state() == FreshnessState::Ready) {
if (!group.freshnessGate->advanceTrustedCheckpoint(checkpoint)) {
_vaultState = DesktopVaultState::SecurityBlocked;
_groupCreationState = DesktopGroupCreationState::LocalFailure;
return true;
}
} else if (group.freshnessGate->state()
== FreshnessState::ResynchronizationRequired) {
const auto target = group.freshnessGate->resynchronizationTarget();
const auto knownTarget = target
? group.groupLedger.checkpointAt(target->generation)
: std::nullopt;
const auto verifier = AccountFreshnessResponseVerifier(
group.groupLedger);
if (target
&& knownTarget == target
&& group.freshnessGate->completeResynchronization(
*target,
verifier)
&& group.freshnessGate->advanceTrustedCheckpoint(checkpoint)) {
const auto committed = group.freshnessTrust.confirm();
if (committed != FreshnessTrustCommitResult::Committed
&& committed
!= FreshnessTrustCommitResult::AlreadyCommitted) {
group.freshnessGate->requireFreshness(checkpoint);
_groupCreationState = DesktopGroupCreationState::LocalFailure;
return true;
}
}
}
group.conversation.checkpoint = checkpoint;
notifySecurityRevision();
_pendingGroupCreation = std::move(i->second);
_groups.erase(i);
_pendingGroupCreation->vaultPreflightRequired = true;
publishNextBootstrapObject();
return true;
}
void DesktopService::publishQueuedGroupOutbox(
ConversationId conversationId) {
const auto i = _groups.find(conversationId);
if (!vaultReady()
|| i == end(_groups)
|| _pendingGroupCreation
|| !i->second->outbox.size()) {
return;
}
if (i->second->phase == PendingGroupCreation::Phase::Active) {
pumpActiveOutbox(conversationId);
return;
}
_pendingGroupCreation = std::move(i->second);
_groups.erase(i);
_pendingGroupCreation->vaultPreflightRequired = false;
publishNextBootstrapObject();
}
DesktopService::QueuedContentRecoveryResult
DesktopService::recoverQueuedContentHistory(
PendingGroupCreation &group) {
// The adjacent content-envelope/descriptor pair is the cross-store WAL.
// Materialize its local history record before either object can be sent;
// regenerating the pair would reuse stable IDs with different ciphertext.
if (group.queuedContentReconciled) {
return QueuedContentRecoveryResult::Ready;
} else if (!group.outbox.loaded() || !group.contentStore.loaded()) {
return QueuedContentRecoveryResult::PersistenceFailed;
}
auto items = group.outbox.items(group.conversationId);
const auto itemsGuard = qScopeGuard([&] {
for (auto &item : items) {
CleanseOutboxItem(item);
}
});
auto recovered = false;
for (auto index = std::size_t(); index != items.size();) {
const auto &contentItem = items[index];
if (contentItem.stage != OutboxItemStage::Sealed
|| !contentItem.sealed) {
++index;
continue;
}
const auto envelope = group.envelopeCodec.decode(
*contentItem.sealed);
if (!envelope) {
return QueuedContentRecoveryResult::Invalid;
} else if (envelope->objectKind
!= ObjectKind::EncryptedMessageBody
&& envelope->objectKind
!= ObjectKind::EncryptedFileManifest) {
++index;
continue;
}
const auto encrypted = EncryptedArchivedContentCodecV1().decode(
envelope->payload);
if (!encrypted
|| envelope->payloadHash != _sha256.digest(envelope->payload)
|| encrypted->conversationId != group.conversationId
|| encrypted->contentObjectId != envelope->objectId
|| encrypted->objectKind != envelope->objectKind
|| encrypted->groupGeneration != envelope->epochOrGeneration
|| encrypted->senderAccountId != envelope->senderAccountId
|| encrypted->senderClientId != envelope->senderClientId
|| QByteArray(
reinterpret_cast<const char*>(encrypted->signature.data()),
encrypted->signature.size()) != envelope->authenticationData
|| index + 1 == items.size()) {
return QueuedContentRecoveryResult::Invalid;
}
const auto &eventItem = items[index + 1];
if (eventItem.draft.conversationId != group.conversationId
|| eventItem.draft.objectId != encrypted->eventObjectId) {
return QueuedContentRecoveryResult::Invalid;
}
if (eventItem.stage == OutboxItemStage::Sealed) {
if (!eventItem.sealed
|| !group.contentStore.contains(encrypted->eventObjectId)) {
return QueuedContentRecoveryResult::Invalid;
}
auto stored = group.contentStore.record(
encrypted->eventObjectId);
const auto storedGuard = qScopeGuard([&] {
if (stored) {
Cleanse(stored->plaintext);
}
});
if (!stored) {
return QueuedContentRecoveryResult::PersistenceFailed;
} else if (stored->conversationId != group.conversationId
|| stored->eventObjectId != encrypted->eventObjectId
|| stored->contentObjectId != encrypted->contentObjectId
|| stored->objectKind != encrypted->objectKind
|| stored->groupGeneration != encrypted->groupGeneration
|| stored->senderAccountId != encrypted->senderAccountId
|| stored->senderClientId != encrypted->senderClientId) {
return QueuedContentRecoveryResult::Invalid;
}
} else if (eventItem.stage == OutboxItemStage::Draft
&& !eventItem.sealed
&& eventItem.draft.authenticatedData
== QByteArray("TDE2E/archived-content/v1")) {
auto opened = OpenLiveArchivedContent(
group.conversationId,
group.telegramPeerIdBinding,
{
.conversationId = group.conversationId,
.eventObjectId = eventItem.draft.objectId,
.senderAccountId = envelope->senderAccountId,
.senderClientId = envelope->senderClientId,
.plaintext = eventItem.draft.plaintext,
},
*envelope,
group.groupLedger,
EncryptedArchivedContentCodecV1(),
ArchivedContentDescriptorCodecV1(),
_sha256);
const auto openedGuard = qScopeGuard([&] {
if (opened.content) {
Cleanse(opened.content->plaintext);
}
});
if (opened.status != ArchivedContentOpenStatus::Opened
|| !opened.content) {
return QueuedContentRecoveryResult::Invalid;
}
auto unixTime = std::uint64_t();
if (opened.content->objectKind
== ObjectKind::EncryptedMessageBody) {
auto body = ProtectedMessageBodyCodecV1().decodePlaintext(
opened.content->plaintext);
if (!body) {
return QueuedContentRecoveryResult::Invalid;
}
unixTime = body->unixTime;
Cleanse(body->textUtf8);
} else {
auto manifest = PrivateFileManifestCodecV1().decodePlaintext(
opened.content->plaintext);
if (!manifest
|| manifest->context.conversationId
!= group.conversationId) {
return QueuedContentRecoveryResult::Invalid;
}
unixTime = manifest->unixTime;
Cleanse(manifest->filenameUtf8);
Cleanse(manifest->mimeTypeUtf8);
if (manifest->preview) {
Cleanse(manifest->preview->jpegBytes);
}
}
const auto stored = group.contentStore.append({
.conversationId = group.conversationId,
.eventObjectId = opened.content->eventObjectId,
.contentObjectId = opened.content->contentObjectId,
.objectKind = opened.content->objectKind,
.groupGeneration = opened.content->groupGeneration,
.senderAccountId = opened.content->senderAccountId,
.senderClientId = opened.content->senderClientId,
.unixTime = unixTime,
.observedTelegramMessageId = 0,
.plaintext = opened.content->plaintext,
});
if (stored == ContentStoreAppendResult::Conflict
|| stored == ContentStoreAppendResult::InvalidRecord) {
return QueuedContentRecoveryResult::Invalid;
} else if (stored
== ContentStoreAppendResult::PersistenceFailed) {
return QueuedContentRecoveryResult::PersistenceFailed;
} else if (stored == ContentStoreAppendResult::Stored) {
recovered = true;
}
} else {
return QueuedContentRecoveryResult::Invalid;
}
index += 2;
}
group.queuedContentReconciled = true;
if (recovered) {
notifyContentRevision();
}
return recovered
? QueuedContentRecoveryResult::Recovered
: QueuedContentRecoveryResult::Ready;
}
bool DesktopService::initializeActivePipeline(
PendingGroupCreation &group) {
if (!vaultReady()) {
return false;
}
const auto recovered = recoverQueuedContentHistory(group);
if (recovered == QueuedContentRecoveryResult::Invalid) {
_vaultState = DesktopVaultState::SecurityBlocked;
return false;
} else if (recovered
== QueuedContentRecoveryResult::PersistenceFailed) {
return false;
}
if (group.applicationEngine
&& group.outboxCoordinator
&& group.uploadController) {
return group.applicationEngine->ready();
}
const auto metadata = group.metadata.metadata();
if (!metadata
|| !group.freshnessGate
|| group.phase != PendingGroupCreation::Phase::Active) {
return false;
}
group.applicationEngine = std::make_unique<OpenMlsApplicationEngine>(
OpenMlsClientContext{
.conversationId = group.conversationId,
.accountId = metadata->accountId,
.clientId = metadata->clientId,
.telegramPeerIdBinding = group.telegramPeerIdBinding,
},
OpenMlsBridge(),
MlsContextCodecV1(),
group.envelopeCodec,
_sha256,
group.mlsState);
if (!group.applicationEngine->ready()) {
group.applicationEngine.reset();
return false;
}
group.outboxCoordinator = std::make_unique<OutboxCoordinator>(
*group.freshnessGate,
group.outbox,
*group.applicationEngine);
const auto conversationId = group.conversationId;
group.uploadController = std::make_unique<OutboxUploadController>(
*group.outboxCoordinator,
group.transport,
[weak = base::weak_ptr(this), conversationId](ObjectId objectId) {
return weak && weak->prepareActiveUploadAcknowledgement(
conversationId,
objectId);
},
[weak = base::weak_ptr(this), conversationId](
UploadCompletion completion) {
if (weak) {
weak->completeActiveUpload(
conversationId,
std::move(completion));
}
});
return true;
}
void DesktopService::pumpActiveOutbox(ConversationId conversationId) {
const auto i = _groups.find(conversationId);
if (!vaultReady()
|| i == end(_groups)
|| i->second->phase != PendingGroupCreation::Phase::Active) {
return;
}
if (!initializeActivePipeline(*i->second)) {
setContentState(
conversationId,
(_vaultState.current() == DesktopVaultState::SecurityBlocked)
? DesktopContentState::SecurityBlocked
: DesktopContentState::LocalFailure);
return;
}
auto &group = *i->second;
if (_pendingGroupCreation
|| _pendingGroupJoin
|| _pendingGroupDiscovery) {
group.observationDirty = true;
return;
}
if (group.observation) {
setContentState(
conversationId,
DesktopContentState::Synchronizing);
return;
} else if (group.observationDirty && !group.outbox.size()) {
beginGroupObservation(conversationId);
const auto current = _groups.find(conversationId);
if (current == end(_groups)) {
return;
} else if (current->second->observation) {
setContentState(
conversationId,
DesktopContentState::Synchronizing);
return;
} else if (current->second->observationDirty) {
return;
}
}
if (const auto pending = group.fileTransfer.pending();
pending && pending->cancelRequested) {
if (group.fileFinalHashInProgress
|| group.uploadInProgress
|| (group.uploadController
&& group.uploadController->uploadInProgress())) {
setContentState(
conversationId,
DesktopContentState::Synchronizing);
} else if (const auto result = finishFileTransferCancellation(group);
result == FileTransferCancellationResult::RetryableCleanupFailure) {
setContentState(
conversationId,
DesktopContentState::Synchronizing);
scheduleFileTransferRetry(conversationId);
} else if (result == FileTransferCancellationResult::Failure) {
setContentState(
conversationId,
DesktopContentState::LocalFailure);
} else {
pumpActiveOutbox(conversationId);
}
return;
}
if (group.fileHashInProgress) {
setContentState(
conversationId,
DesktopContentState::Synchronizing);
return;
} else if (group.filePreparationSource
&& !group.outbox.size()
&& !group.uploadInProgress
&& (!group.uploadController
|| !group.uploadController->uploadInProgress())
&& !commitPreparedFileTransfer(conversationId)) {
return;
}
if (!group.freshnessGate->sendingAllowed()) {
if (group.freshnessGate->state() == FreshnessState::Required
&& !group.outbox.size()
&& !queueFreshnessChallenge(group)) {
setContentState(
conversationId,
DesktopContentState::LocalFailure);
return;
}
auto item = group.outbox.front(conversationId);
const auto itemGuard = qScopeGuard([&] {
if (item) {
CleanseOutboxItem(*item);
}
});
const auto envelope = (item && item->sealed)
? group.envelopeCodec.decode(*item->sealed)
: std::nullopt;
if (!item
|| item->stage != OutboxItemStage::Sealed
|| !item->sealed
|| !envelope
|| envelope->objectKind != ObjectKind::FreshnessChallenge) {
setContentState(
conversationId,
DesktopContentState::AwaitingFreshness);
return;
}
if (!resumeQueuedFreshnessChallenge(group, *item->sealed)) {
setContentState(
conversationId,
DesktopContentState::LocalFailure);
return;
}
if (group.uploadInProgress) {
return;
}
const auto objectId = item->sealed->objectId;
group.uploadInProgress = true;
group.transport.uploadExact(
*item->sealed,
[weak = base::weak_ptr(this), conversationId, objectId](
TelegramTransport::UploadResult result) {
if (!weak || !weak->vaultReady()) {
return;
}
const auto i = weak->_groups.find(conversationId);
if (i == end(weak->_groups)) {
return;
}
auto &group = *i->second;
group.uploadInProgress = false;
if (result == TelegramTransport::UploadResult::Accepted) {
if (group.fileTransfer.pending()) {
weak->resetFileTransferRetry(group);
}
if (!group.outbox.remove(objectId)) {
weak->setContentState(
conversationId,
DesktopContentState::LocalFailure);
return;
}
weak->setContentState(
conversationId,
DesktopContentState::AwaitingFreshness);
weak->beginGroupObservation(conversationId);
} else if (result
== TelegramTransport::UploadResult::RetryableError) {
weak->setContentState(
conversationId,
DesktopContentState::RetryableTransportError);
if (group.fileTransfer.pending()) {
weak->scheduleFileTransferRetry(conversationId);
}
} else {
weak->setContentState(
conversationId,
DesktopContentState::PermanentTransportError);
}
if (const auto pending = group.fileTransfer.pending();
pending && pending->cancelRequested) {
weak->pumpActiveOutbox(conversationId);
}
});
return;
}
if (const auto pending = group.fileTransfer.pending(); pending) {
if (!pending->manifestPublished) {
if (!queuePendingFileManifest(conversationId)) {
return;
}
} else if (!group.outbox.contains(pending->eventObjectId)
&& !group.outbox.contains(pending->contentObjectId)) {
(void)pumpFileTransfer(conversationId);
return;
}
}
const auto pumped = group.uploadController->pump();
switch (pumped) {
case UploadPumpResult::Empty:
setContentState(conversationId, DesktopContentState::Ready);
beginContentObservation(conversationId);
break;
case UploadPumpResult::Started:
case UploadPumpResult::UploadInProgress:
setContentState(conversationId, DesktopContentState::Synchronizing);
break;
case UploadPumpResult::AwaitingFreshness:
setContentState(
conversationId,
DesktopContentState::AwaitingFreshness);
break;
case UploadPumpResult::InvalidItem:
case UploadPumpResult::ProtectionFailed:
case UploadPumpResult::PersistenceFailed:
setContentState(conversationId, DesktopContentState::LocalFailure);
break;
}
}
bool DesktopService::prepareActiveUploadAcknowledgement(
ConversationId conversationId,
ObjectId objectId) {
const auto i = _groups.find(conversationId);
if (!vaultReady() || i == end(_groups)) {
return false;
}
return prepareFileManifestAcknowledgement(*i->second, objectId);
}
bool DesktopService::prepareFileManifestAcknowledgement(
PendingGroupCreation &group,
ObjectId objectId) {
const auto pending = group.fileTransfer.pending();
if (!pending
|| pending->manifestPublished
|| objectId != pending->eventObjectId) {
return true;
} else if (!group.outbox.contains(pending->eventObjectId)
|| group.outbox.contains(pending->contentObjectId)) {
return false;
}
const auto currentEpoch = group.archiveState.currentEpoch();
const auto archiveEpochGeneration = pending->archiveEpochGeneration
? pending->archiveEpochGeneration
: (currentEpoch ? currentEpoch->generation : 0);
const auto marked = group.fileTransfer.markManifestPublished(
archiveEpochGeneration);
return marked == FileTransferCommitResult::Committed
|| marked == FileTransferCommitResult::AlreadyCommitted;
}
bool DesktopService::commitPreparedFileTransfer(
ConversationId conversationId) {
const auto i = _groups.find(conversationId);
if (!vaultReady() || i == end(_groups)) {
return false;
}
auto &group = *i->second;
const auto metadata = group.metadata.metadata();
const auto state = group.groupLedger.state();
const auto epoch = group.archiveState.currentEpoch();
const auto source = group.filePreparationSource;
if (!metadata
|| !state
|| !epoch
|| !source
|| group.filePreparationPath.isEmpty()
|| group.filePreparationFilename.isEmpty()
|| group.filePreparationMimeType.isEmpty()
|| group.fileTransfer.pending()
|| group.outbox.size()
|| group.uploadInProgress
|| (group.uploadController
&& group.uploadController->uploadInProgress())
|| source->size > kMaximumProtectedFileSize) {
RemoveStagedProtectedSource(
_telegramUserIdBinding,
conversationId,
group.filePreparationPath);
group.filePreparationPath.clear();
group.filePreparationFilename.clear();
group.filePreparationMimeType.clear();
group.filePreparationSource.reset();
setContentState(conversationId, DesktopContentState::LocalFailure);
return false;
}
const auto material = GenerateFileEncryptionMaterial();
const auto eventObjectId = RandomId<ObjectId>();
const auto contentObjectId = RandomId<ObjectId>();
if (!material || !eventObjectId || !contentObjectId) {
RemoveStagedProtectedSource(
_telegramUserIdBinding,
conversationId,
group.filePreparationPath);
group.filePreparationPath.clear();
group.filePreparationFilename.clear();
group.filePreparationMimeType.clear();
group.filePreparationSource.reset();
setContentState(conversationId, DesktopContentState::LocalFailure);
return false;
}
const auto chunkCount = source->size
? std::uint32_t(1 + ((source->size - 1) / kDesktopFileChunkSize))
: 0;
const auto manifest = PrivateFileManifest{
.context = {
.conversationId = conversationId,
.fileId = material->fileId,
.plaintextSize = source->size,
.chunkSize = kDesktopFileChunkSize,
.chunkCount = chunkCount,
.noncePrefix = material->noncePrefix,
},
.key = FileEncryptionKey(std::array<std::uint8_t, 32>(
material->key.bytes())),
.plaintextHash = source->hash,
.unixTime = std::uint64_t(base::unixtime::now()),
.filenameUtf8 = group.filePreparationFilename.toUtf8(),
.mimeTypeUtf8 = group.filePreparationMimeType.toUtf8(),
.preview = source->preview,
};
auto manifestPlaintext = PrivateFileManifestCodecV1()
.encodePlaintext(manifest);
const auto manifestGuard = qScopeGuard([&] {
if (manifestPlaintext) {
Cleanse(*manifestPlaintext);
}
});
if (!manifestPlaintext) {
RemoveStagedProtectedSource(
_telegramUserIdBinding,
conversationId,
group.filePreparationPath);
group.filePreparationPath.clear();
group.filePreparationFilename.clear();
group.filePreparationMimeType.clear();
group.filePreparationSource.reset();
setContentState(conversationId, DesktopContentState::LocalFailure);
return false;
} else if (group.freshnessGate->state() == FreshnessState::Required
&& !queueFreshnessChallenge(group)) {
setContentState(conversationId, DesktopContentState::LocalFailure);
return false;
}
const auto sourcePath = group.filePreparationPath;
auto transfer = PendingFileTransfer{
.conversationId = conversationId,
.eventObjectId = *eventObjectId,
.contentObjectId = *contentObjectId,
.groupGeneration = state->generation(),
.archiveEpochGeneration = epoch->generation,
.manifestPublished = false,
.nextChunkIndex = 0,
.sourcePathUtf8 = group.filePreparationPath.toUtf8(),
.manifestPlaintext = *manifestPlaintext,
};
const auto queued = group.fileTransfer.begin(std::move(transfer));
if (queued != FileTransferCommitResult::Committed) {
RemoveStagedProtectedSource(
_telegramUserIdBinding,
conversationId,
sourcePath);
group.filePreparationPath.clear();
group.filePreparationFilename.clear();
group.filePreparationMimeType.clear();
group.filePreparationSource.reset();
setContentState(conversationId, DesktopContentState::LocalFailure);
return false;
}
if (!IsStagedProtectedSource(
_telegramUserIdBinding,
conversationId,
sourcePath)) {
group.localProtectedFilePaths.insert_or_assign(
*eventObjectId,
sourcePath);
}
group.filePreparationPath.clear();
group.filePreparationFilename.clear();
group.filePreparationMimeType.clear();
group.filePreparationSource.reset();
notifyFileTransferRevision();
return true;
}
DesktopService::FileTransferCancellationResult
DesktopService::finishFileTransferCancellation(
PendingGroupCreation &group) {
const auto pending = group.fileTransfer.pending();
const auto manifest = pending
? PrivateFileManifestCodecV1().decodePlaintext(
pending->manifestPlaintext)
: std::nullopt;
if (!pending
|| !pending->cancelRequested
|| !manifest
|| group.fileFinalHashInProgress
|| group.uploadInProgress
|| (group.uploadController
&& group.uploadController->uploadInProgress())
|| !group.outbox.removePair(
pending->eventObjectId,
pending->contentObjectId)) {
return FileTransferCancellationResult::Failure;
}
const auto sourcePath = QString::fromUtf8(pending->sourcePathUtf8);
if (!group.chunkStore.removeChunksBefore(
group.conversationId,
manifest->context.fileId,
manifest->context.chunkCount)) {
return FileTransferCancellationResult::RetryableCleanupFailure;
}
group.localProtectedFilePaths.erase(pending->eventObjectId);
const auto cleared = group.fileTransfer.clear();
if (cleared != FileTransferCommitResult::Committed
&& cleared != FileTransferCommitResult::AlreadyCommitted) {
return FileTransferCancellationResult::Failure;
}
resetFileTransferRetry(group);
RemoveStagedProtectedSource(
_telegramUserIdBinding,
group.conversationId,
sourcePath);
notifyFileTransferRevision();
return FileTransferCancellationResult::Finished;
}
void DesktopService::completeActiveUpload(
ConversationId conversationId,
UploadCompletion completion) {
const auto i = _groups.find(conversationId);
if (!vaultReady() || i == end(_groups)) {
return;
}
auto &group = *i->second;
if (!completion.outboxUpdated) {
setContentState(conversationId, DesktopContentState::LocalFailure);
return;
} else if (const auto pending = group.fileTransfer.pending();
pending && pending->cancelRequested) {
pumpActiveOutbox(conversationId);
return;
} else if (completion.transportResult
== TelegramTransport::UploadResult::RetryableError) {
setContentState(
conversationId,
DesktopContentState::RetryableTransportError);
if (group.fileTransfer.pending()) {
scheduleFileTransferRetry(conversationId);
}
return;
} else if (completion.transportResult
== TelegramTransport::UploadResult::PermanentError) {
setContentState(
conversationId,
DesktopContentState::PermanentTransportError);
return;
}
const auto receipt = group.mlsState.receipt(completion.objectId);
if (receipt) {
const auto envelope = group.envelopeCodec.decode(receipt->envelope);
const auto reconciled = envelope
? ReconcileObservedMlsReceipt(
*envelope,
group.envelopeCodec,
group.mlsState,
group.inboundJournal)
: ObservedMlsReceiptReconcileResult::ObjectIdConflict;
if (reconciled != ObservedMlsReceiptReconcileResult::Reconciled) {
const auto state = (reconciled
== ObservedMlsReceiptReconcileResult::ObjectIdConflict)
? DesktopContentState::SecurityBlocked
: DesktopContentState::LocalFailure;
setContentState(conversationId, state);
if (state == DesktopContentState::SecurityBlocked) {
_vaultState = DesktopVaultState::SecurityBlocked;
}
return;
}
}
if (group.fileTransfer.pending()) {
resetFileTransferRetry(group);
}
pumpActiveOutbox(conversationId);
}
bool DesktopService::pumpFileTransfer(ConversationId conversationId) {
const auto i = _groups.find(conversationId);
if (!vaultReady()
|| i == end(_groups)
|| i->second->phase != PendingGroupCreation::Phase::Active) {
return false;
}
auto &group = *i->second;
const auto pending = group.fileTransfer.pending();
if (!pending) {
return false;
} else if (!pending->manifestPublished) {
setContentState(conversationId, DesktopContentState::LocalFailure);
return true;
} else if (!group.freshnessGate
|| !group.freshnessGate->sendingAllowed()) {
setContentState(
conversationId,
DesktopContentState::AwaitingFreshness);
return true;
} else if (group.uploadInProgress
|| (group.uploadController
&& group.uploadController->uploadInProgress())) {
return true;
}
const auto manifest = PrivateFileManifestCodecV1().decodePlaintext(
pending->manifestPlaintext);
if (!manifest) {
setContentState(conversationId, DesktopContentState::LocalFailure);
return true;
} else if (!group.chunkStore.removeChunksBefore(
conversationId,
manifest->context.fileId,
pending->nextChunkIndex)) {
setContentState(
conversationId,
DesktopContentState::Synchronizing);
scheduleFileTransferRetry(conversationId);
return true;
} else if (pending->nextChunkIndex == manifest->context.chunkCount) {
(void)finalizeFileTransfer(conversationId);
return true;
}
auto source = QFile(QString::fromUtf8(pending->sourcePathUtf8));
const auto index = pending->nextChunkIndex;
const auto offset = std::uint64_t(manifest->context.chunkSize) * index;
const auto expected = (index + 1 < manifest->context.chunkCount)
? std::uint64_t(manifest->context.chunkSize)
: manifest->context.plaintextSize - offset;
if (!source.open(QIODevice::ReadOnly)
|| source.size() < 0
|| std::uint64_t(source.size()) != manifest->context.plaintextSize
|| !source.seek(qint64(offset))) {
setContentState(conversationId, DesktopContentState::LocalFailure);
return true;
}
auto plaintext = source.read(qint64(expected));
const auto prepared = IdempotentFileChunkProtector(
AesGcmFileChunkCipher(),
group.chunkStore).prepare(
manifest->key,
manifest->context,
index,
plaintext);
Cleanse(plaintext);
const auto metadata = group.metadata.metadata();
const auto envelope = (metadata && prepared.exactCiphertext)
? PrepareFileChunkEnvelope({
.conversationId = conversationId,
.senderAccountId = metadata->accountId,
.senderClientId = metadata->clientId,
.telegramPeerIdBinding = group.telegramPeerIdBinding,
.groupGeneration = pending->groupGeneration,
.fileId = manifest->context.fileId,
.chunkIndex = index,
.chunkCount = manifest->context.chunkCount,
.senderSigningPrivateKey = &_vault->identity.signingPrivateKey,
.exactCiphertext = *prepared.exactCiphertext,
}, group.envelopeCodec, _sha256)
: std::nullopt;
if (prepared.result != FileChunkPrepareResult::Ready || !envelope) {
setContentState(conversationId, DesktopContentState::LocalFailure);
return true;
}
group.uploadInProgress = true;
setContentState(conversationId, DesktopContentState::Synchronizing);
group.transport.uploadExact(
envelope->encoded,
[weak = base::weak_ptr(this), conversationId, index](
TelegramTransport::UploadResult result) {
if (weak) {
weak->completeFileChunkUpload(
conversationId,
index,
result);
}
});
return true;
}
void DesktopService::completeFileChunkUpload(
ConversationId conversationId,
std::uint32_t chunkIndex,
TelegramTransport::UploadResult result) {
const auto i = _groups.find(conversationId);
if (!vaultReady() || i == end(_groups)) {
return;
}
auto &group = *i->second;
group.uploadInProgress = false;
if (const auto pending = group.fileTransfer.pending();
pending && pending->cancelRequested) {
pumpActiveOutbox(conversationId);
return;
}
if (result == TelegramTransport::UploadResult::RetryableError) {
setContentState(
conversationId,
DesktopContentState::RetryableTransportError);
scheduleFileTransferRetry(conversationId);
return;
} else if (result == TelegramTransport::UploadResult::PermanentError) {
setContentState(
conversationId,
DesktopContentState::PermanentTransportError);
return;
}
const auto pending = group.fileTransfer.pending();
const auto manifest = pending
? PrivateFileManifestCodecV1().decodePlaintext(
pending->manifestPlaintext)
: std::nullopt;
if (!manifest) {
setContentState(conversationId, DesktopContentState::LocalFailure);
return;
}
if (group.fileTransfer.advance(chunkIndex)
!= FileTransferCommitResult::Committed) {
setContentState(conversationId, DesktopContentState::LocalFailure);
return;
}
resetFileTransferRetry(group);
(void)group.chunkStore.removeChunk(
conversationId,
manifest->context.fileId,
chunkIndex);
pumpActiveOutbox(conversationId);
}
void DesktopService::scheduleFileTransferRetry(
ConversationId conversationId) {
const auto i = _groups.find(conversationId);
auto group = (i != end(_groups)) ? i->second.get() : nullptr;
if (!group
&& _pendingGroupCreation
&& _pendingGroupCreation->conversationId == conversationId) {
group = _pendingGroupCreation.get();
}
if (!vaultReady() || !group || !group->fileTransfer.pending()) {
return;
}
const auto delay = FileRetryDelay(group->fileRetryAttempt);
group->fileRetryAttempt = std::min(group->fileRetryAttempt + 1, 6);
if (++group->fileRetryToken == 0) {
++group->fileRetryToken;
}
const auto token = group->fileRetryToken;
base::call_delayed(delay, [weak = base::weak_ptr(this),
conversationId,
token] {
if (!weak || !weak->vaultReady()) {
return;
}
const auto i = weak->_groups.find(conversationId);
auto group = (i != end(weak->_groups)) ? i->second.get() : nullptr;
const auto publishing = !group
&& weak->_pendingGroupCreation
&& weak->_pendingGroupCreation->conversationId == conversationId;
if (publishing) {
group = weak->_pendingGroupCreation.get();
}
if (!group
|| group->fileRetryToken != token
|| !group->fileTransfer.pending()) {
return;
}
if (publishing) {
weak->publishNextBootstrapObject();
} else {
weak->pumpActiveOutbox(conversationId);
}
});
}
void DesktopService::resetFileTransferRetry(
PendingGroupCreation &group) {
group.fileRetryAttempt = 0;
if (++group.fileRetryToken == 0) {
++group.fileRetryToken;
}
}
bool DesktopService::queuePendingFileManifest(
ConversationId conversationId) {
const auto i = _groups.find(conversationId);
if (!vaultReady() || i == end(_groups)) {
return false;
}
auto &group = *i->second;
const auto pending = group.fileTransfer.pending();
const auto manifest = pending
? PrivateFileManifestCodecV1().decodePlaintext(
pending->manifestPlaintext)
: std::nullopt;
auto source = QFile(pending
? QString::fromUtf8(pending->sourcePathUtf8)
: QString());
const auto sourceMatches = pending
&& manifest
&& source.open(QIODevice::ReadOnly)
&& source.size() >= 0
&& std::uint64_t(source.size())
== manifest->context.plaintextSize;
const auto metadata = group.metadata.metadata();
const auto epoch = (pending && pending->archiveEpochGeneration)
? group.archiveState.epoch(pending->archiveEpochGeneration)
: group.archiveState.currentEpoch();
if (!pending
|| !manifest
|| !sourceMatches
|| !metadata
|| !epoch
|| !group.groupLedger.wasClientActiveAt(
metadata->accountId,
metadata->clientId,
pending->groupGeneration)
|| !group.archiveState.epochWasActiveAt(
epoch->generation,
pending->groupGeneration)
|| !group.outboxCoordinator) {
setContentState(conversationId, DesktopContentState::LocalFailure);
return false;
}
const auto hasEvent = group.outbox.contains(pending->eventObjectId);
const auto hasContent = group.outbox.contains(pending->contentObjectId);
if (!hasEvent && !hasContent) {
const auto queued = QueueArchivedContent({
.conversationId = conversationId,
.eventObjectId = pending->eventObjectId,
.contentObjectId = pending->contentObjectId,
.objectKind = ObjectKind::EncryptedFileManifest,
.senderAccountId = metadata->accountId,
.senderClientId = metadata->clientId,
.telegramPeerIdBinding = group.telegramPeerIdBinding,
.groupGeneration = pending->groupGeneration,
.archiveEpochGeneration = epoch->generation,
.archiveEpochKey = &epoch->key,
.senderSigningPrivateKey = &_vault->identity.signingPrivateKey,
.plaintext = pending->manifestPlaintext,
.mlsContext = QByteArray("TDE2E/archived-content/v1"),
},
ArchiveEpochCrypto(),
EncryptedArchivedContentCodecV1(),
ArchivedContentDescriptorCodecV1(),
group.envelopeCodec,
_sha256,
*group.outboxCoordinator);
if (queued != ArchivedContentQueueResult::Queued) {
setContentState(
conversationId,
DesktopContentState::LocalFailure);
return false;
}
group.queuedContentReconciled = false;
}
const auto stored = group.contentStore.append({
.conversationId = conversationId,
.eventObjectId = pending->eventObjectId,
.contentObjectId = pending->contentObjectId,
.objectKind = ObjectKind::EncryptedFileManifest,
.groupGeneration = pending->groupGeneration,
.senderAccountId = metadata->accountId,
.senderClientId = metadata->clientId,
.unixTime = manifest->unixTime,
.observedTelegramMessageId = 0,
.plaintext = pending->manifestPlaintext,
});
if (stored == ContentStoreAppendResult::Conflict) {
_vaultState = DesktopVaultState::SecurityBlocked;
setContentState(conversationId, DesktopContentState::SecurityBlocked);
return false;
} else if (stored == ContentStoreAppendResult::InvalidRecord
|| stored == ContentStoreAppendResult::PersistenceFailed) {
setContentState(conversationId, DesktopContentState::LocalFailure);
return false;
}
if (stored == ContentStoreAppendResult::Stored) {
notifyContentRevision();
}
group.queuedContentReconciled = true;
return true;
}
bool DesktopService::finalizeFileTransfer(
ConversationId conversationId) {
const auto i = _groups.find(conversationId);
if (!vaultReady() || i == end(_groups)) {
return false;
}
auto &group = *i->second;
const auto pending = group.fileTransfer.pending();
const auto manifest = pending
? PrivateFileManifestCodecV1().decodePlaintext(
pending->manifestPlaintext)
: std::nullopt;
if (!pending
|| !pending->manifestPublished
|| !manifest
|| pending->nextChunkIndex != manifest->context.chunkCount) {
setContentState(conversationId, DesktopContentState::LocalFailure);
return false;
} else if (group.fileFinalHashInProgress) {
return true;
}
const auto eventObjectId = pending->eventObjectId;
const auto sourcePath = QString::fromUtf8(pending->sourcePathUtf8);
const auto operationEpoch = _operationEpoch;
const auto cancellation = std::make_shared<std::atomic_bool>(false);
group.fileFinalHashInProgress = true;
group.fileFinalHashCancellation = cancellation;
setContentState(conversationId, DesktopContentState::Synchronizing);
crl::async([weak = base::weak_ptr(this),
conversationId,
eventObjectId,
sourcePath,
cancellation,
operationEpoch] {
const auto source = HashFile(sourcePath, cancellation);
crl::on_main([weak,
conversationId,
eventObjectId,
sourcePath,
cancellation,
operationEpoch,
source] {
if (!weak || weak->_operationEpoch != operationEpoch) {
return;
}
const auto i = weak->_groups.find(conversationId);
if (i == end(weak->_groups)
|| i->second->fileFinalHashCancellation != cancellation) {
return;
}
auto &group = *i->second;
group.fileFinalHashInProgress = false;
group.fileFinalHashCancellation.reset();
const auto pending = group.fileTransfer.pending();
if (!pending
|| pending->eventObjectId != eventObjectId
|| QString::fromUtf8(pending->sourcePathUtf8) != sourcePath) {
return;
} else if (pending->cancelRequested) {
weak->pumpActiveOutbox(conversationId);
return;
}
const auto manifest = pending
? PrivateFileManifestCodecV1().decodePlaintext(
pending->manifestPlaintext)
: std::nullopt;
if (!manifest
|| pending->nextChunkIndex
!= manifest->context.chunkCount
|| !source
|| source->size != manifest->context.plaintextSize
|| source->hash != manifest->plaintextHash) {
weak->setContentState(
conversationId,
DesktopContentState::LocalFailure);
return;
}
if (!group.chunkStore.removeChunksBefore(
conversationId,
manifest->context.fileId,
manifest->context.chunkCount)) {
weak->setContentState(
conversationId,
DesktopContentState::Synchronizing);
weak->scheduleFileTransferRetry(conversationId);
return;
}
const auto cleared = group.fileTransfer.clear();
if (cleared != FileTransferCommitResult::Committed
&& cleared != FileTransferCommitResult::AlreadyCommitted) {
weak->setContentState(
conversationId,
DesktopContentState::LocalFailure);
return;
}
weak->resetFileTransferRetry(group);
RemoveStagedProtectedSource(
weak->_telegramUserIdBinding,
conversationId,
sourcePath);
weak->notifyFileTransferRevision();
weak->pumpActiveOutbox(conversationId);
});
});
return true;
}
void DesktopService::beginGroupObservation(ConversationId conversationId) {
const auto i = _groups.find(conversationId);
if (!vaultReady()
|| i == end(_groups)
|| (i->second->phase != PendingGroupCreation::Phase::Active
&& i->second->phase
!= PendingGroupCreation::Phase::AwaitingAdmission)) {
return;
}
auto &group = *i->second;
if (_pendingGroupCreation
|| _pendingGroupJoin
|| _pendingGroupDiscovery) {
group.observationDirty = true;
return;
}
if (group.observation) {
group.observationDirty = true;
return;
} else if (group.contentObservation
|| group.uploadInProgress
|| group.fileHashInProgress
|| group.fileFinalHashInProgress
|| group.pendingFileDownload
|| (group.outbox.size()
&& (!group.freshnessGate
|| group.freshnessGate->sendingAllowed()))) {
group.observationDirty = true;
return;
}
group.observationDirty = false;
group.observation = std::make_unique<PublicBootstrapSyncController>(
conversationId,
group.telegramPeerIdBinding,
group.conversation.ownerAccountId,
group.controlTransport,
group.envelopeCodec,
_sha256,
[weak = base::weak_ptr(this), conversationId](
PublicBootstrapSyncCompletion result) {
if (weak) {
weak->applyGroupObservation(
conversationId,
std::move(result));
}
});
const auto boundary = group.controlSyncState.newestObservedMessageId();
const auto started = boundary
? (group.phase == PendingGroupCreation::Phase::AwaitingAdmission)
? group.observation->startForJoinFromBoundary(boundary)
: group.observation->startFromBoundary(boundary)
: (group.phase == PendingGroupCreation::Phase::AwaitingAdmission)
? group.observation->startForJoin()
: group.observation->start();
if (!started) {
group.observation.reset();
group.observationDirty = true;
setContentState(
conversationId,
DesktopContentState::RetryableTransportError);
if (group.phase
== PendingGroupCreation::Phase::AwaitingAdmission) {
_groupCreationState
= DesktopGroupCreationState::RetryableTransportError;
}
scheduleGroupObservationRetry(conversationId);
}
}
void DesktopService::resumeDeferredGroupObservations() {
if (!vaultReady()
|| _pendingGroupCreation
|| _pendingGroupJoin
|| _pendingGroupDiscovery) {
return;
}
auto conversations = std::vector<ConversationId>();
for (const auto &[conversationId, group] : _groups) {
if (group->observationDirty && !group->observation) {
conversations.push_back(conversationId);
}
}
for (const auto conversationId : conversations) {
if (_pendingGroupCreation
|| _pendingGroupJoin
|| _pendingGroupDiscovery) {
return;
}
beginGroupObservation(conversationId);
}
}
void DesktopService::scheduleGroupObservationRetry(
ConversationId conversationId) {
const auto i = _groups.find(conversationId);
if (!vaultReady()
|| i == end(_groups)
|| i->second->observation
|| !i->second->observationDirty) {
return;
}
auto &group = *i->second;
const auto delay = FileRetryDelay(group.groupObservationRetryAttempt);
group.groupObservationRetryAttempt = std::min(
group.groupObservationRetryAttempt + 1,
6);
if (++group.groupObservationRetryToken == 0) {
++group.groupObservationRetryToken;
}
const auto token = group.groupObservationRetryToken;
base::call_delayed(delay, [weak = base::weak_ptr(this),
conversationId,
token] {
if (!weak || !weak->vaultReady()) {
return;
}
const auto i = weak->_groups.find(conversationId);
if (i == end(weak->_groups)
|| i->second->groupObservationRetryToken != token
|| i->second->observation
|| !i->second->observationDirty) {
return;
}
weak->beginGroupObservation(conversationId);
});
}
void DesktopService::resetGroupObservationRetry(
PendingGroupCreation &group) {
group.groupObservationRetryAttempt = 0;
if (++group.groupObservationRetryToken == 0) {
++group.groupObservationRetryToken;
}
}
void DesktopService::beginContentObservation(
ConversationId conversationId) {
const auto i = _groups.find(conversationId);
if (!vaultReady()
|| i == end(_groups)
|| (i->second->phase != PendingGroupCreation::Phase::Active
&& i->second->phase != PendingGroupCreation::Phase::Removed)) {
return;
}
auto &group = *i->second;
if (group.contentObservation
|| group.observation
|| group.observationDirty) {
group.contentObservationDirty = true;
return;
}
group.contentObservationDirty = false;
group.contentObservation =
std::make_unique<ObservedContentSyncController>(
conversationId,
group.telegramPeerIdBinding,
group.contentTransport,
_sha256,
[weak = base::weak_ptr(this), conversationId](
std::vector<TelegramTransport::UntrustedObject> objects) {
return weak
? weak->processObservedContentPage(
conversationId,
std::move(objects))
: ObservedContentPageResult::PersistenceFailed;
},
[weak = base::weak_ptr(this), conversationId](
ObservedContentSyncCompletion completion) {
if (weak) {
weak->applyContentObservation(
conversationId,
std::move(completion));
}
},
[weak = base::weak_ptr(this), conversationId](
const std::vector<
TelegramTransport::UntrustedObject> &objects,
std::size_t objectLimit) {
return weak
? weak->previewObservedFileManifests(
conversationId,
objects,
objectLimit)
: ObservedContentPageResult::PersistenceFailed;
});
setContentState(conversationId, DesktopContentState::Synchronizing);
if (!group.contentObservation->start(
group.contentSyncState.newestObservedMessageId())) {
group.contentObservation.reset();
setContentState(conversationId, DesktopContentState::LocalFailure);
}
}
ObservedContentPageResult DesktopService::previewObservedFileManifests(
ConversationId conversationId,
const std::vector<TelegramTransport::UntrustedObject> &objects,
std::size_t objectLimit) {
if (!vaultReady()) {
return ObservedContentPageResult::SecurityBlocked;
}
const auto i = _groups.find(conversationId);
if (i == end(_groups)) {
return ObservedContentPageResult::PersistenceFailed;
}
auto &group = *i->second;
const auto metadata = group.metadata.metadata();
if (!metadata) {
return ObservedContentPageResult::PersistenceFailed;
}
const auto processed = ProcessObservedFileManifestPreview(
objects,
objectLimit,
{
.conversationId = conversationId,
.accountId = metadata->accountId,
.clientId = metadata->clientId,
.telegramPeerIdBinding = group.telegramPeerIdBinding,
},
group.envelopeCodec,
_sha256,
group.archiveState,
group.groupLedger,
group.contentStore,
group.chunkStore);
if (processed.status == ObservedContentProcessStatus::SecurityBlocked) {
_vaultState = DesktopVaultState::SecurityBlocked;
setContentState(
conversationId,
DesktopContentState::SecurityBlocked);
return ObservedContentPageResult::SecurityBlocked;
} else if (processed.status
== ObservedContentProcessStatus::RetryRequired) {
return ObservedContentPageResult::RetryRequired;
} else if (processed.status
== ObservedContentProcessStatus::PersistenceFailed
|| processed.status == ObservedContentProcessStatus::InvalidState) {
setContentState(conversationId, DesktopContentState::LocalFailure);
return ObservedContentPageResult::PersistenceFailed;
}
if (processed.stats.manifestsStored) {
notifyContentRevision();
}
return ObservedContentPageResult::Persisted;
}
ObservedContentPageResult DesktopService::processObservedContentPage(
ConversationId conversationId,
std::vector<TelegramTransport::UntrustedObject> objects) {
if (!vaultReady()) {
return ObservedContentPageResult::SecurityBlocked;
}
const auto i = _groups.find(conversationId);
if (i == end(_groups)) {
return ObservedContentPageResult::PersistenceFailed;
}
auto &group = *i->second;
const auto metadata = group.metadata.metadata();
if (!metadata) {
return ObservedContentPageResult::PersistenceFailed;
}
const auto processed = ProcessObservedContentPage(
objects,
{
.conversationId = conversationId,
.accountId = metadata->accountId,
.clientId = metadata->clientId,
.telegramPeerIdBinding = group.telegramPeerIdBinding,
},
_telegramUserIdBinding,
group.envelopeCodec,
OpenMlsBridge(),
MlsContextCodecV1(),
_sha256,
group.mlsState,
group.archiveState,
group.groupLedger,
group.inboundJournal,
group.contentStore,
group.chunkStore);
if (processed.status == ObservedContentProcessStatus::SecurityBlocked) {
_vaultState = DesktopVaultState::SecurityBlocked;
setContentState(
conversationId,
DesktopContentState::SecurityBlocked);
return ObservedContentPageResult::SecurityBlocked;
} else if (processed.status
== ObservedContentProcessStatus::RetryRequired) {
return ObservedContentPageResult::RetryRequired;
} else if (processed.status
== ObservedContentProcessStatus::PersistenceFailed
|| processed.status == ObservedContentProcessStatus::InvalidState) {
setContentState(conversationId, DesktopContentState::LocalFailure);
return ObservedContentPageResult::PersistenceFailed;
}
if (processed.stats.messagesStored
|| processed.stats.manifestsStored) {
notifyContentRevision();
}
return ObservedContentPageResult::Persisted;
}
void DesktopService::applyContentObservation(
ConversationId conversationId,
ObservedContentSyncCompletion completion) {
if (!vaultReady()) {
return;
}
const auto i = _groups.find(conversationId);
if (i == end(_groups)) {
return;
}
auto &group = *i->second;
const auto rerun = group.contentObservationDirty;
group.contentObservationDirty = false;
group.contentObservation.reset();
const auto resumeControlObservation = qScopeGuard([&] {
const auto current = _groups.find(conversationId);
if (_vaultState.current() == DesktopVaultState::Ready
&& current != end(_groups)
&& current->second->observationDirty
&& !current->second->observation) {
beginGroupObservation(conversationId);
}
});
switch (completion.status) {
case ObservedContentSyncStatus::Complete:
if (completion.previousBoundaryMessageId
!= group.contentSyncState.newestObservedMessageId()
|| (completion.nextBoundaryMessageId
&& (!completion.newestObservedMessageId
|| completion.nextBoundaryMessageId
> completion.newestObservedMessageId))) {
_vaultState = DesktopVaultState::SecurityBlocked;
setContentState(
conversationId,
DesktopContentState::SecurityBlocked);
return;
}
if (completion.nextBoundaryMessageId) {
const auto committed = group.contentSyncState.advance(
completion.nextBoundaryMessageId);
if (committed == ContentSyncStateCommitResult::InvalidBoundary) {
_vaultState = DesktopVaultState::SecurityBlocked;
setContentState(
conversationId,
DesktopContentState::SecurityBlocked);
return;
} else if (committed
== ContentSyncStateCommitResult::PersistenceFailed) {
setContentState(
conversationId,
DesktopContentState::LocalFailure);
return;
}
}
setContentState(conversationId, DesktopContentState::Ready);
if (rerun) {
beginContentObservation(conversationId);
}
break;
case ObservedContentSyncStatus::RetryRequired:
case ObservedContentSyncStatus::RetryableTransportError:
setContentState(
conversationId,
DesktopContentState::RetryableTransportError);
break;
case ObservedContentSyncStatus::PermanentTransportError:
setContentState(
conversationId,
DesktopContentState::PermanentTransportError);
break;
case ObservedContentSyncStatus::SecurityBlocked:
case ObservedContentSyncStatus::InvalidPagination:
_vaultState = DesktopVaultState::SecurityBlocked;
setContentState(
conversationId,
DesktopContentState::SecurityBlocked);
break;
case ObservedContentSyncStatus::PersistenceFailed:
setContentState(conversationId, DesktopContentState::LocalFailure);
break;
case ObservedContentSyncStatus::Cancelled:
setContentState(conversationId, DesktopContentState::Idle);
break;
}
}
void DesktopService::applyGroupObservation(
ConversationId conversationId,
PublicBootstrapSyncCompletion result) {
if (!vaultReady()) {
return;
}
const auto i = _groups.find(conversationId);
if (i == end(_groups)) {
return;
}
const auto awaiting = i->second->phase
== PendingGroupCreation::Phase::AwaitingAdmission;
const auto rerun = i->second->observationDirty;
i->second->observationDirty = false;
i->second->observation.reset();
if (result.status == PublicBootstrapSyncStatus::ObjectConflict
|| result.status == PublicBootstrapSyncStatus::Ambiguous
|| result.status == PublicBootstrapSyncStatus::CapacityExceeded
|| result.status == PublicBootstrapSyncStatus::InvalidPagination) {
_vaultState = DesktopVaultState::SecurityBlocked;
_groupCreationState = DesktopGroupCreationState::LocalFailure;
return;
}
if (_pendingGroupCreation
|| _pendingGroupJoin
|| _pendingGroupDiscovery) {
i->second->observationDirty = true;
return;
}
if (result.status != PublicBootstrapSyncStatus::Verified
&& result.status != PublicBootstrapSyncStatus::Incremental) {
i->second->observationDirty = true;
const auto permanent = result.status
== PublicBootstrapSyncStatus::PermanentTransportError;
setContentState(
conversationId,
permanent
? DesktopContentState::PermanentTransportError
: DesktopContentState::RetryableTransportError);
if (awaiting) {
_groupCreationState = permanent
? DesktopGroupCreationState::PermanentTransportError
: DesktopGroupCreationState::RetryableTransportError;
}
if (!permanent) {
scheduleGroupObservationRetry(conversationId);
}
return;
}
resetGroupObservationRetry(*i->second);
auto changed = false;
if (awaiting) {
changed = completeObservedJoin(
conversationId,
result);
} else {
const auto processingFailed = [&] {
const auto state = contentState(conversationId);
return _vaultState.current()
== DesktopVaultState::SecurityBlocked
|| _groupCreationState.current()
== DesktopGroupCreationState::LocalFailure
|| state == DesktopContentState::SecurityBlocked
|| state == DesktopContentState::LocalFailure;
};
const auto synchronized = synchronizeObservedGroupChanges(
conversationId,
result.untrustedObjects);
if (synchronized
|| !_groups.contains(conversationId)
|| processingFailed()) {
return;
}
const auto safetyChanged = processObservedSafetyGossip(
conversationId,
result.untrustedObjects);
if (processingFailed()) {
return;
}
const auto freshnessChanged = processObservedFreshness(
conversationId,
result.untrustedObjects);
if (processingFailed()) {
return;
}
const auto grantAccepted = acceptObservedHistoryGrant(
conversationId,
result.untrustedObjects);
if (processingFailed()) {
return;
}
const auto admitted = admitObservedClient(conversationId);
if (processingFailed() || !_groups.contains(conversationId)) {
return;
}
changed = safetyChanged
|| freshnessChanged
|| grantAccepted
|| admitted;
auto &group = *_groups.find(conversationId)->second;
if ((result.previousBoundaryMessageId
&& result.previousBoundaryMessageId
!= group.controlSyncState
.newestObservedMessageId())
|| !result.newestObservedMessageId) {
_vaultState = DesktopVaultState::SecurityBlocked;
_groupCreationState = DesktopGroupCreationState::LocalFailure;
setContentState(
conversationId,
DesktopContentState::SecurityBlocked);
return;
}
auto nextBoundaryMessageId = result.previousBoundaryMessageId;
if (!nextBoundaryMessageId) {
if (result.untrustedObjects.empty()) {
_vaultState = DesktopVaultState::SecurityBlocked;
_groupCreationState = DesktopGroupCreationState::LocalFailure;
setContentState(
conversationId,
DesktopContentState::SecurityBlocked);
return;
}
const auto index = std::min(
kControlSyncOverlap,
result.untrustedObjects.size() - 1);
nextBoundaryMessageId = result.untrustedObjects[index]
.observedMessageId;
} else if (result.untrustedObjects.size() > kControlSyncOverlap) {
nextBoundaryMessageId = result.untrustedObjects[
kControlSyncOverlap].observedMessageId;
}
const auto committed = group.controlSyncState.advance(
nextBoundaryMessageId,
group.groupLedger.checkpoint(),
group.safetyWitnesses,
group.ownSafetyGossipObserved);
if (committed
== ControlObservationStateCommitResult::InvalidState) {
_vaultState = DesktopVaultState::SecurityBlocked;
_groupCreationState = DesktopGroupCreationState::LocalFailure;
setContentState(
conversationId,
DesktopContentState::SecurityBlocked);
return;
} else if (committed
== ControlObservationStateCommitResult::PersistenceFailed) {
_groupCreationState = DesktopGroupCreationState::LocalFailure;
setContentState(
conversationId,
DesktopContentState::LocalFailure);
return;
}
if (rerun) {
beginGroupObservation(conversationId);
return;
} else if (!admitted) {
publishQueuedGroupOutbox(conversationId);
}
}
if (!changed && rerun) {
beginGroupObservation(conversationId);
}
(void)startNextProtectedFileDownload(conversationId);
pumpActiveOutbox(conversationId);
startNextGroupDiscovery();
}
void DesktopService::handleNewTelegramItem(not_null<HistoryItem*> item) {
const auto peer = item->history()->peer;
if (!peer->isChat() && !peer->isMegagroup()) {
return;
}
const auto media = item->media();
const auto document = media ? media->document() : nullptr;
if (!document
|| !IsProtectedGroupCarrierMetadata(
document->filename(),
document->mimeString())) {
return;
}
const auto peerId = item->history()->peer->id.value;
if (!vaultReady()) {
return;
}
if (document->filename() == ProtectedContentCarrierFilename()) {
for (const auto &[conversationId, group] : _groups) {
if (group->telegramPeerIdBinding == peerId) {
beginContentObservation(conversationId);
}
}
return;
} else if (document->filename() != ProtectedControlCarrierFilename()
&& document->filename() != ProtectedLegacyCarrierFilename()) {
return;
}
auto conversations = std::vector<ConversationId>();
for (const auto &[conversationId, group] : _groups) {
if (group->telegramPeerIdBinding == peerId) {
conversations.push_back(conversationId);
}
}
for (const auto conversationId : conversations) {
beginGroupObservation(conversationId);
}
if (conversations.empty()) {
queueGroupDiscovery(peerId);
}
}
void DesktopService::queueLoadedGroupDiscoveries() {
if (!vaultReady()) {
return;
}
auto peerIds = std::vector<std::uint64_t>();
_session->data().enumerateGroups([&](not_null<PeerData*> peer) {
const auto history = _session->data().historyLoaded(peer);
if (history && history->hasE2ECloudGroupCarrier()) {
peerIds.push_back(peer->id.value);
}
});
for (const auto peerId : peerIds) {
queueGroupDiscovery(peerId);
}
}
void DesktopService::queueGroupDiscovery(
std::uint64_t telegramPeerIdBinding) {
const auto peer = telegramPeerIdBinding
? _session->data().peerLoaded(PeerId(telegramPeerIdBinding))
: nullptr;
if (!telegramPeerIdBinding
|| !peer
|| (!peer->isChat() && !peer->isMegagroup())
|| !_vault
|| _vaultState.current() != DesktopVaultState::Ready
|| std::any_of(
begin(_vault->conversations),
end(_vault->conversations),
[&](const CloudVaultConversation &conversation) {
return conversation.telegramPeerIdBinding
== telegramPeerIdBinding;
})) {
return;
}
_groupDiscoveryQueue.emplace(telegramPeerIdBinding);
startNextGroupDiscovery();
}
void DesktopService::startNextGroupDiscovery() {
const auto operationState = _groupCreationState.current();
if (_pendingGroupDiscovery
|| _pendingGroupCreation
|| _pendingGroupJoin
|| !vaultReady()
|| (operationState != DesktopGroupCreationState::Idle
&& operationState != DesktopGroupCreationState::Ready)
|| std::any_of(
begin(_groups),
end(_groups),
[](const auto &entry) {
return entry.second->observation
|| entry.second->observationDirty;
})) {
return;
}
while (!_groupDiscoveryQueue.empty()) {
const auto peerId = *_groupDiscoveryQueue.begin();
_groupDiscoveryQueue.erase(_groupDiscoveryQueue.begin());
if (std::any_of(
begin(_vault->conversations),
end(_vault->conversations),
[&](const CloudVaultConversation &conversation) {
return conversation.telegramPeerIdBinding == peerId;
})
|| std::any_of(
begin(_groups),
end(_groups),
[&](const auto &entry) {
return entry.second->telegramPeerIdBinding == peerId;
})) {
continue;
}
_pendingGroupDiscovery =
std::make_unique<PendingGroupDiscovery>(_session, peerId);
_pendingGroupDiscovery->sync =
std::make_unique<PublicBootstrapDiscoveryController>(
peerId,
_pendingGroupDiscovery->backend,
_pendingGroupDiscovery->envelopeCodec,
_sha256,
[weak = base::weak_ptr(this)](
PublicBootstrapSyncCompletion result) {
if (weak) {
weak->applyGroupDiscovery(std::move(result));
}
});
_groupCreationState = DesktopGroupCreationState::Preparing;
if (!_pendingGroupDiscovery->sync->start()) {
_pendingGroupDiscovery.reset();
_groupDiscoveryQueue.emplace(peerId);
_groupCreationState
= DesktopGroupCreationState::RetryableTransportError;
resumeDeferredGroupObservations();
return;
}
return;
}
}
void DesktopService::applyGroupDiscovery(
PublicBootstrapSyncCompletion result) {
if (!vaultReady() || !_pendingGroupDiscovery) {
return;
}
const auto peerId = _pendingGroupDiscovery->telegramPeerIdBinding;
_pendingGroupDiscovery->sync.reset();
_pendingGroupDiscovery.reset();
if (result.status == PublicBootstrapSyncStatus::ObjectConflict
|| result.status == PublicBootstrapSyncStatus::Ambiguous
|| result.status == PublicBootstrapSyncStatus::CapacityExceeded
|| result.status == PublicBootstrapSyncStatus::InvalidPagination) {
_groupCreationState = DesktopGroupCreationState::Ready;
resumeDeferredGroupObservations();
startNextGroupDiscovery();
return;
} else if (result.status
== PublicBootstrapSyncStatus::RetryableTransportError
|| result.status
== PublicBootstrapSyncStatus::PermanentTransportError) {
_groupDiscoveryQueue.emplace(peerId);
_groupCreationState = (result.status
== PublicBootstrapSyncStatus::PermanentTransportError)
? DesktopGroupCreationState::PermanentTransportError
: DesktopGroupCreationState::RetryableTransportError;
resumeDeferredGroupObservations();
return;
} else if (result.status != PublicBootstrapSyncStatus::Verified
|| !result.verified) {
_groupCreationState = DesktopGroupCreationState::Ready;
resumeDeferredGroupObservations();
startNextGroupDiscovery();
return;
}
auto verified = std::move(*result.verified);
auto objects = std::move(result.untrustedObjects);
if (verified.genesis.telegramPeerIdBinding != peerId
|| std::any_of(
begin(_vault->conversations),
end(_vault->conversations),
[&](const CloudVaultConversation &conversation) {
return conversation.conversationId
== verified.genesis.conversationId
|| conversation.telegramPeerIdBinding == peerId;
})) {
_groupCreationState = DesktopGroupCreationState::Ready;
resumeDeferredGroupObservations();
startNextGroupDiscovery();
return;
}
auto conversation = CloudVaultConversation{
.conversationId = verified.genesis.conversationId,
.telegramPeerIdBinding = peerId,
.checkpoint = verified.checkpoint,
.ownerAccountId = verified.genesis.ownerAccountId,
};
if (!prepareGroupJoin(
std::move(conversation),
std::move(verified),
std::move(objects),
true)) {
_groupCreationState = DesktopGroupCreationState::LocalFailure;
resumeDeferredGroupObservations();
startNextGroupDiscovery();
}
}
bool DesktopService::completeObservedJoin(
ConversationId conversationId,
const PublicBootstrapSyncCompletion &result) {
const auto i = _groups.find(conversationId);
if (!vaultReady()
|| i == end(_groups)
|| _pendingGroupCreation
|| i->second->phase
!= PendingGroupCreation::Phase::AwaitingAdmission) {
return false;
}
auto &group = *i->second;
const auto metadata = group.metadata.metadata();
const auto accountId = DeriveAccountId(
_vault->identity.credential,
_sha256);
if (!metadata
|| !accountId
|| metadata->accountId != *accountId) {
_vaultState = DesktopVaultState::SecurityBlocked;
_groupCreationState = DesktopGroupCreationState::LocalFailure;
return false;
}
const auto staged = StagePublicJoinObjects(
result.untrustedObjects,
conversationId,
group.telegramPeerIdBinding,
group.envelopeCodec,
_sha256,
group.changeInbox);
if (staged == PublicJoinInboxStageStatus::ForkDetected
|| staged == PublicJoinInboxStageStatus::InvalidState) {
_vaultState = DesktopVaultState::SecurityBlocked;
_groupCreationState = DesktopGroupCreationState::LocalFailure;
return false;
} else if (staged != PublicJoinInboxStageStatus::Staged) {
_groupCreationState = DesktopGroupCreationState::LocalFailure;
return false;
}
const auto objects = ReconstructPublicJoinObjects(
conversationId,
group.telegramPeerIdBinding,
group.envelopeCodec,
group.changeInbox);
if (!objects) {
_groupCreationState = DesktopGroupCreationState::LocalFailure;
return false;
}
const auto persistObservation = [&] {
const auto previous
= group.controlSyncState.newestObservedMessageId();
if (!result.newestObservedMessageId
|| result.newestObservedMessageId < previous
|| result.previousBoundaryMessageId != previous) {
_vaultState = DesktopVaultState::SecurityBlocked;
_groupCreationState = DesktopGroupCreationState::LocalFailure;
return false;
}
group.safetyWitnessGeneration
= group.groupLedger.checkpoint().generation;
group.safetyWitnesses = { metadata->accountId };
group.ownSafetyGossipObserved = false;
const auto committed = group.controlSyncState.advance(
result.newestObservedMessageId,
group.groupLedger.checkpoint(),
group.safetyWitnesses,
false);
if (committed == ControlObservationStateCommitResult::InvalidState) {
_vaultState = DesktopVaultState::SecurityBlocked;
_groupCreationState = DesktopGroupCreationState::LocalFailure;
return false;
} else if (committed
== ControlObservationStateCommitResult::PersistenceFailed) {
_groupCreationState = DesktopGroupCreationState::LocalFailure;
return false;
}
return true;
};
auto catchup = CatchUpPublicJoin(
*objects,
conversationId,
group.telegramPeerIdBinding,
*accountId,
metadata->clientId,
_vault->identity.credential,
std::uint64_t(base::unixtime::now()),
group.envelopeCodec,
_sha256,
group.groupLedger,
group.keyPackages);
if (catchup.status == PublicJoinCatchupStatus::Waiting) {
const auto current = group.groupLedger.checkpoint();
const auto target = group.joinTargetCheckpoint;
if (!ValidConversationCheckpoint(target, conversationId)) {
_vaultState = DesktopVaultState::SecurityBlocked;
_groupCreationState = DesktopGroupCreationState::LocalFailure;
} else if (current.generation >= target.generation) {
if (group.groupLedger.checkpointAt(target.generation) != target) {
_vaultState = DesktopVaultState::SecurityBlocked;
_groupCreationState = DesktopGroupCreationState::LocalFailure;
} else {
(void)persistObservation();
}
}
return false;
} else if (catchup.status == PublicJoinCatchupStatus::UpdatedWaiting) {
if (!group.changeInbox.discardAppliedTransitions(
group.groupLedger.checkpoint().generation)) {
_groupCreationState = DesktopGroupCreationState::LocalFailure;
return false;
}
const auto current = group.groupLedger.checkpoint();
const auto target = group.joinTargetCheckpoint;
if (!ValidConversationCheckpoint(target, conversationId)
|| (current.generation >= target.generation
&& group.groupLedger.checkpointAt(target.generation)
!= target)) {
_vaultState = DesktopVaultState::SecurityBlocked;
_groupCreationState = DesktopGroupCreationState::LocalFailure;
return false;
} else if (current.generation < target.generation) {
return false;
}
(void)persistObservation();
return false;
} else if (catchup.status == PublicJoinCatchupStatus::ForkDetected
|| catchup.status == PublicJoinCatchupStatus::InvalidState) {
_vaultState = DesktopVaultState::SecurityBlocked;
_groupCreationState = DesktopGroupCreationState::LocalFailure;
return false;
} else if (catchup.status
== PublicJoinCatchupStatus::PersistenceFailure
|| catchup.status != PublicJoinCatchupStatus::Ready
|| !catchup.bundle) {
_groupCreationState = DesktopGroupCreationState::LocalFailure;
return false;
}
const auto target = group.joinTargetCheckpoint;
if (!ValidConversationCheckpoint(target, conversationId)
|| group.groupLedger.checkpoint().generation < target.generation
|| group.groupLedger.checkpointAt(target.generation) != target) {
_vaultState = DesktopVaultState::SecurityBlocked;
_groupCreationState = DesktopGroupCreationState::LocalFailure;
return false;
}
auto bundle = std::move(*catchup.bundle);
const auto installed = InstallClientKeyPackageForWelcome(
bundle.targetKeyPackage,
std::uint64_t(base::unixtime::now()),
OpenMlsBridge(),
_sha256,
group.keyPackages,
group.mlsState);
if (installed != InstallClientKeyPackageStatus::Installed
&& installed != InstallClientKeyPackageStatus::AlreadyInstalled) {
_groupCreationState = DesktopGroupCreationState::LocalFailure;
return false;
}
auto prepared = PrepareOpenMlsInboundJoin({
.local = {
.conversationId = conversationId,
.accountId = *accountId,
.clientId = metadata->clientId,
.telegramPeerIdBinding = group.telegramPeerIdBinding,
},
.transitionEnvelope = std::move(bundle.transitionEnvelope),
.commitEnvelope = std::move(bundle.commitEnvelope),
.welcomeEnvelope = std::move(bundle.welcomeEnvelope),
.archiveDistributionEnvelope =
std::move(bundle.archiveDistributionEnvelope),
.targetCredential = std::move(bundle.targetCredential),
.targetKeyPackage = bundle.targetKeyPackage,
},
OpenMlsBridge(),
MlsContextCodecV1(),
MlsRosterCodecV1(),
GroupControlCodecV1(),
_sha256,
group.mlsState,
group.archiveState,
group.groupLedger);
if (prepared.status != OpenMlsInboundGroupChangeStatus::Prepared
|| !prepared.prepared) {
_groupCreationState = DesktopGroupCreationState::LocalFailure;
return false;
}
auto coordinator = GroupChangeTransactionCoordinator(
group.changeJournal,
group.mlsState,
group.archiveState,
group.groupLedger,
_sha256);
const auto applied = coordinator.apply(
std::move(prepared.prepared->transaction));
if (applied != GroupChangeApplyStatus::Applied
&& coordinator.recover() != GroupChangeApplyStatus::Recovered) {
_groupCreationState = DesktopGroupCreationState::LocalFailure;
return false;
}
const auto finalized = FinalizeClientKeyPackageWelcome(
bundle.targetKeyPackage,
OpenMlsBridge(),
_sha256,
group.keyPackages,
group.mlsState);
if (finalized != FinalizeClientKeyPackageStatus::Finalized
&& finalized != FinalizeClientKeyPackageStatus::AlreadyFinalized) {
_groupCreationState = DesktopGroupCreationState::LocalFailure;
return false;
}
group.phase = PendingGroupCreation::Phase::Active;
group.conversation.checkpoint = group.groupLedger.checkpoint();
group.freshnessGate->requireFreshness(group.conversation.checkpoint);
if (!group.changeInbox.discardAppliedTransitions(
group.groupLedger.checkpoint().generation)) {
_groupCreationState = DesktopGroupCreationState::LocalFailure;
return false;
}
(void)acceptObservedHistoryGrant(conversationId, *objects);
if (_vaultState.current() == DesktopVaultState::SecurityBlocked
|| _groupCreationState.current()
== DesktopGroupCreationState::LocalFailure
|| contentState(conversationId)
== DesktopContentState::SecurityBlocked
|| contentState(conversationId)
== DesktopContentState::LocalFailure) {
return false;
}
if (!group.changeInbox.discardJoinOnlyObjects()
|| !persistObservation()) {
_groupCreationState = DesktopGroupCreationState::LocalFailure;
return false;
}
notifySecurityRevision();
_pendingGroupCreation = std::move(i->second);
_groups.erase(i);
_pendingGroupCreation->vaultPreflightRequired = true;
publishNextBootstrapObject();
return true;
}
bool DesktopService::acceptObservedHistoryGrant(
ConversationId conversationId,
const std::vector<TelegramTransport::UntrustedObject> &objects) {
const auto i = _groups.find(conversationId);
if (!vaultReady() || i == end(_groups)) {
return false;
}
auto &group = *i->second;
const auto metadata = group.metadata.metadata();
const auto accountId = DeriveAccountId(
_vault->identity.credential,
_sha256);
if (!metadata || !accountId || metadata->accountId != *accountId) {
return false;
}
for (const auto &object : objects) {
const auto envelope = group.envelopeCodec.decodeUntrusted(
object.bytes);
if (!envelope
|| envelope->objectKind != ObjectKind::HistoryGrant
|| envelope->conversationId != conversationId
|| envelope->telegramPeerIdBinding
!= group.telegramPeerIdBinding
|| object.observedTelegramPeerIdBinding
!= group.telegramPeerIdBinding
|| object.observedMessageId <= 0
|| envelope->payloadHash != _sha256.digest(envelope->payload)) {
continue;
}
const auto grant = EncryptedHistoryGrantCodecV1().decode(
envelope->payload);
const auto state = grant
? group.groupLedger.stateAt(grant->groupGeneration)
: std::nullopt;
const auto issuer = state
? state->member(grant->issuerAccountId)
: nullptr;
if (!grant
|| grant->recipientAccountId != *accountId
|| grant->grantId != envelope->objectId
|| grant->issuerAccountId != envelope->senderAccountId
|| grant->issuerClientId != envelope->senderClientId
|| grant->groupGeneration != envelope->epochOrGeneration
|| envelope->authenticationData != QByteArray(
reinterpret_cast<const char*>(grant->signature.data()),
int(grant->signature.size()))
|| !issuer
|| issuer->telegramUserIdBinding
!= object.observedSenderTelegramUserIdBinding) {
continue;
}
const auto accepted = AcceptAuthorizedHistoryGrant({
.conversationId = conversationId,
.telegramPeerIdBinding = group.telegramPeerIdBinding,
.recipientAccountId = *accountId,
.recipientArchivePrivateKey =
&_vault->identity.archiveHpkePrivateKey,
.grant = &*grant,
},
group.groupLedger,
group.archiveState,
_sha256,
OpenMlsBridge());
if (accepted == HistoryGrantServiceStatus::Accepted
|| accepted == HistoryGrantServiceStatus::AlreadyAccepted) {
return true;
} else if (accepted == HistoryGrantServiceStatus::ArchiveConflict) {
_vaultState = DesktopVaultState::SecurityBlocked;
_groupCreationState = DesktopGroupCreationState::LocalFailure;
setContentState(
conversationId,
DesktopContentState::SecurityBlocked);
return false;
} else if (accepted
== HistoryGrantServiceStatus::PersistenceFailure) {
_groupCreationState = DesktopGroupCreationState::LocalFailure;
setContentState(
conversationId,
DesktopContentState::LocalFailure);
return false;
}
}
return false;
}
bool DesktopService::resumeObservedJoinHistory(
ConversationId conversationId) {
const auto i = _groups.find(conversationId);
if (!vaultReady() || i == end(_groups)) {
return false;
}
auto &group = *i->second;
const auto objects = ReconstructPublicJoinObjects(
conversationId,
group.telegramPeerIdBinding,
group.envelopeCodec,
group.changeInbox);
if (!objects) {
return false;
}
(void)acceptObservedHistoryGrant(conversationId, *objects);
if (_vaultState.current() == DesktopVaultState::SecurityBlocked
|| _groupCreationState.current()
== DesktopGroupCreationState::LocalFailure
|| contentState(conversationId)
== DesktopContentState::SecurityBlocked
|| contentState(conversationId)
== DesktopContentState::LocalFailure) {
return false;
}
return group.changeInbox.discardJoinOnlyObjects();
}
bool DesktopService::applyAdministrativeTransition(
ConversationId conversationId,
GroupTransition transition) {
const auto i = _groups.find(conversationId);
const auto operationState = _groupCreationState.current();
if (!vaultReady()
|| i == end(_groups)
|| (operationState != DesktopGroupCreationState::Idle
&& operationState != DesktopGroupCreationState::Ready)
|| _pendingGroupCreation
|| _pendingGroupJoin
|| _pendingGroupDiscovery) {
return false;
}
auto &group = *i->second;
const auto state = group.groupLedger.state();
const auto metadata = group.metadata.metadata();
const auto actorAccountId = DeriveAccountId(
_vault->identity.credential,
_sha256);
const auto removal = transition.kind == GroupTransitionKind::RemoveMember
|| transition.kind == GroupTransitionKind::RemoveClient;
const auto policy = transition.kind == GroupTransitionKind::SetRole
|| transition.kind == GroupTransitionKind::TransferOwnership
|| transition.kind == GroupTransitionKind::SetDefaultHistory
|| transition.kind == GroupTransitionKind::SetMemberHistory;
if ((!removal && !policy)
|| !state
|| !metadata
|| !actorAccountId
|| group.observation
|| group.observationDirty
|| group.contentObservation
|| group.phase != PendingGroupCreation::Phase::Active
|| group.fileHashInProgress
|| !group.filePreparationPath.isEmpty()
|| group.fileFinalHashInProgress
|| group.fileTransfer.pending()
|| group.pendingFileDownload
|| !group.fileDownloadQueue.empty()
|| group.uploadInProgress
|| group.outbox.size()
|| (group.uploadController
&& group.uploadController->uploadInProgress())
|| !group.freshnessGate
|| !group.freshnessGate->administrationAllowed()
|| metadata->accountId != *actorAccountId
|| !state->memberByClient(metadata->clientId)
|| state->generation()
== std::numeric_limits<std::uint64_t>::max()) {
return false;
}
const auto transitionId = RandomId<ObjectId>();
const auto commitObjectId = RandomId<ObjectId>();
const auto distributionObjectId = RandomId<ObjectId>();
const auto nextArchiveKey = ArchiveEpochCrypto().generateKey();
if (!transitionId
|| !commitObjectId
|| !distributionObjectId
|| !nextArchiveKey) {
return false;
}
transition.conversationId = conversationId;
transition.transitionId = *transitionId;
transition.previousGeneration = state->generation();
transition.generation = state->generation() + 1;
const auto context = OpenMlsClientContext{
.conversationId = conversationId,
.accountId = *actorAccountId,
.clientId = metadata->clientId,
.telegramPeerIdBinding = group.telegramPeerIdBinding,
};
auto prepared = removal
? PrepareOpenMlsRemoval({
.actor = context,
.currentGroupState = state,
.currentCheckpoint = group.groupLedger.checkpoint(),
.transition = transition,
.actorCredential = &_vault->identity.credential,
.actorSigningPrivateKey = &_vault->identity.signingPrivateKey,
.nextArchiveKey = &*nextArchiveKey,
.mlsCommitObjectId = *commitObjectId,
.archiveDistributionObjectId = *distributionObjectId,
},
OpenMlsBridge(),
MlsContextCodecV1(),
MlsRosterCodecV1(),
GroupControlCodecV1(),
group.envelopeCodec,
_sha256,
group.mlsState,
group.archiveState,
group.groupLedger)
: PrepareOpenMlsPolicyChange({
.actor = context,
.currentGroupState = state,
.currentCheckpoint = group.groupLedger.checkpoint(),
.transition = transition,
.actorCredential = &_vault->identity.credential,
.actorSigningPrivateKey = &_vault->identity.signingPrivateKey,
.nextArchiveKey = &*nextArchiveKey,
.mlsCommitObjectId = *commitObjectId,
.archiveDistributionObjectId = *distributionObjectId,
},
OpenMlsBridge(),
MlsContextCodecV1(),
MlsRosterCodecV1(),
GroupControlCodecV1(),
group.envelopeCodec,
_sha256,
group.mlsState,
group.archiveState,
group.groupLedger);
if (prepared.status != OpenMlsGroupChangePrepareStatus::Prepared
|| !prepared.prepared) {
return false;
}
auto &preparedChange = *prepared.prepared;
if (transition.kind == GroupTransitionKind::SetMemberHistory) {
const auto grantId = RandomId<ObjectId>();
auto grant = (grantId && preparedChange.transaction.archiveEpoch)
? CreateAdmissionHistoryGrant({
.grant = {
.conversationId = conversationId,
.grantId = *grantId,
.issuerAccountId = *actorAccountId,
.issuerClientId = metadata->clientId,
.recipientAccountId = transition.targetAccountId,
.telegramPeerIdBinding = group.telegramPeerIdBinding,
.groupGeneration = transition.generation,
.historyAccess = transition.historyAccess,
.issuerSigningPrivateKey =
&_vault->identity.signingPrivateKey,
},
.signedTransition =
&preparedChange.transaction.signedTransition,
.applied = &preparedChange.applied,
.admittedCredential = nullptr,
.archiveEpoch = &*preparedChange.transaction.archiveEpoch,
},
group.groupLedger,
group.archiveState,
_sha256,
OpenMlsBridge())
: CreateAuthorizedHistoryGrantOutcome();
const auto payload = grant.grant
? EncryptedHistoryGrantCodecV1().encode(*grant.grant)
: std::nullopt;
const auto envelope = (grantId && grant.grant && payload)
? group.envelopeCodec.encode({
.conversationId = conversationId,
.objectKind = ObjectKind::HistoryGrant,
.senderAccountId = *actorAccountId,
.senderClientId = metadata->clientId,
.telegramPeerIdBinding = group.telegramPeerIdBinding,
.epochOrGeneration = transition.generation,
.objectId = *grantId,
.payloadHash = _sha256.digest(*payload),
.payload = *payload,
.authenticationData = QByteArray(
reinterpret_cast<const char*>(
grant.grant->signature.data()),
int(grant.grant->signature.size())),
})
: std::nullopt;
if (!envelope) {
return false;
}
preparedChange.transaction.outboxEnvelopes.push_back(*envelope);
}
auto coordinator = GroupChangeTransactionCoordinator(
group.changeJournal,
group.mlsState,
group.archiveState,
group.groupLedger,
group.outbox,
_sha256);
if (coordinator.apply(std::move(preparedChange.transaction))
!= GroupChangeApplyStatus::Applied) {
return false;
}
group.conversation.checkpoint = group.groupLedger.checkpoint();
setContentState(conversationId, DesktopContentState::Synchronizing);
notifySecurityRevision();
_pendingGroupCreation = std::move(i->second);
_groups.erase(i);
_pendingGroupCreation->vaultPreflightRequired = true;
publishNextBootstrapObject();
return true;
}
bool DesktopService::admitObservedClient(
ConversationId conversationId) {
const auto i = _groups.find(conversationId);
if (!vaultReady() || i == end(_groups) || _pendingGroupCreation) {
return false;
}
auto &group = *i->second;
const auto state = group.groupLedger.state();
const auto metadata = group.metadata.metadata();
const auto actorAccountId = DeriveAccountId(
_vault->identity.credential,
_sha256);
if (!state
|| !metadata
|| !actorAccountId
|| !group.freshnessGate
|| !group.freshnessGate->administrationAllowed()
|| metadata->accountId != *actorAccountId
|| !state->memberByClient(metadata->clientId)) {
return false;
}
const auto durableObjects = ReconstructPublicJoinObjects(
conversationId,
group.telegramPeerIdBinding,
group.envelopeCodec,
group.changeInbox);
if (!durableObjects) {
_groupCreationState = DesktopGroupCreationState::LocalFailure;
return false;
}
for (const auto &object : *durableObjects) {
auto observed = VerifyObservedClientKeyPackage(
object,
conversationId,
group.telegramPeerIdBinding,
state->generation(),
std::uint64_t(base::unixtime::now()),
group.envelopeCodec,
_sha256);
if (!observed.verified) {
continue;
}
const auto &candidate = *observed.verified;
const auto targetAccountId = candidate.envelope.senderAccountId;
const auto targetClientId = candidate.envelope.senderClientId;
const auto existingMember = state->member(targetAccountId);
if (state->memberByClient(targetClientId)
|| (existingMember
&& existingMember->telegramUserIdBinding
!= candidate.telegramUserIdBinding)
|| (!existingMember
&& state->memberByTelegramUserId(
candidate.telegramUserIdBinding))) {
continue;
}
if (state->generation()
== std::numeric_limits<std::uint64_t>::max()) {
_vaultState = DesktopVaultState::SecurityBlocked;
_groupCreationState = DesktopGroupCreationState::LocalFailure;
return false;
}
const auto transitionId = RandomId<ObjectId>();
const auto commitObjectId = RandomId<ObjectId>();
const auto distributionObjectId = RandomId<ObjectId>();
const auto welcomeObjectId = RandomId<ObjectId>();
const auto historyGrantObjectId = RandomId<ObjectId>();
const auto nextArchiveKey = ArchiveEpochCrypto().generateKey();
if (!transitionId
|| !commitObjectId
|| !distributionObjectId
|| !welcomeObjectId
|| !historyGrantObjectId
|| !nextArchiveKey) {
return false;
}
const auto historyAccess = existingMember
? existingMember->historyAccess
: state->policy().defaultHistoryAccess;
const auto transition = GroupTransition{
.conversationId = conversationId,
.transitionId = *transitionId,
.previousGeneration = state->generation(),
.generation = state->generation() + 1,
.kind = existingMember
? GroupTransitionKind::AddClient
: GroupTransitionKind::AddMember,
.targetAccountId = targetAccountId,
.targetClientId = targetClientId,
.targetTelegramUserIdBinding = candidate.telegramUserIdBinding,
.targetRole = GroupRole::Member,
.targetAdminPermissions = 0,
.historyAccess = historyAccess,
};
auto prepared = PrepareOpenMlsAdmission({
.actor = {
.conversationId = conversationId,
.accountId = *actorAccountId,
.clientId = metadata->clientId,
.telegramPeerIdBinding = group.telegramPeerIdBinding,
},
.currentGroupState = state,
.currentCheckpoint = group.groupLedger.checkpoint(),
.transition = transition,
.actorCredential = &_vault->identity.credential,
.actorSigningPrivateKey = &_vault->identity.signingPrivateKey,
.targetCredential = &candidate.publication.accountCredential,
.targetClientAuthorization =
&candidate.publication.authorization,
.targetKeyPackage = candidate.publication.keyPackage,
.nextArchiveKey = &*nextArchiveKey,
.mlsCommitObjectId = *commitObjectId,
.archiveDistributionObjectId = *distributionObjectId,
.welcomeObjectId = *welcomeObjectId,
},
OpenMlsBridge(),
MlsContextCodecV1(),
MlsRosterCodecV1(),
GroupControlCodecV1(),
group.envelopeCodec,
_sha256,
group.mlsState,
group.archiveState,
group.groupLedger);
if (!prepared.prepared) {
continue;
}
auto &preparedChange = *prepared.prepared;
auto grant = preparedChange.transaction.archiveEpoch
? CreateAdmissionHistoryGrant({
.grant = {
.conversationId = conversationId,
.grantId = *historyGrantObjectId,
.issuerAccountId = *actorAccountId,
.issuerClientId = metadata->clientId,
.recipientAccountId = targetAccountId,
.telegramPeerIdBinding =
group.telegramPeerIdBinding,
.groupGeneration = transition.generation,
.historyAccess = historyAccess,
.issuerSigningPrivateKey =
&_vault->identity.signingPrivateKey,
},
.signedTransition =
&preparedChange.transaction.signedTransition,
.applied = &preparedChange.applied,
.admittedCredential =
&candidate.publication.accountCredential,
.archiveEpoch = &*preparedChange.transaction.archiveEpoch,
},
group.groupLedger,
group.archiveState,
_sha256,
OpenMlsBridge())
: CreateAuthorizedHistoryGrantOutcome();
const auto grantPayload = grant.grant
? EncryptedHistoryGrantCodecV1().encode(*grant.grant)
: std::nullopt;
const auto grantEnvelope = (grant.grant && grantPayload)
? group.envelopeCodec.encode({
.conversationId = conversationId,
.objectKind = ObjectKind::HistoryGrant,
.senderAccountId = *actorAccountId,
.senderClientId = metadata->clientId,
.telegramPeerIdBinding = group.telegramPeerIdBinding,
.epochOrGeneration = transition.generation,
.objectId = *historyGrantObjectId,
.payloadHash = _sha256.digest(*grantPayload),
.payload = *grantPayload,
.authenticationData = QByteArray(
reinterpret_cast<const char*>(
grant.grant->signature.data()),
int(grant.grant->signature.size())),
})
: std::nullopt;
if (!grantEnvelope) {
_groupCreationState = DesktopGroupCreationState::LocalFailure;
return false;
}
preparedChange.transaction.outboxEnvelopes.push_back(
*grantEnvelope);
auto coordinator = GroupChangeTransactionCoordinator(
group.changeJournal,
group.mlsState,
group.archiveState,
group.groupLedger,
group.outbox,
_sha256);
if (coordinator.apply(std::move(
preparedChange.transaction))
!= GroupChangeApplyStatus::Applied) {
_groupCreationState = DesktopGroupCreationState::LocalFailure;
return false;
}
group.conversation.checkpoint = group.groupLedger.checkpoint();
notifySecurityRevision();
_pendingGroupCreation = std::move(i->second);
_groups.erase(i);
_pendingGroupCreation->vaultPreflightRequired = true;
publishNextBootstrapObject();
return true;
}
return false;
}
DesktopService::LocalGroupRecoveryResult DesktopService::restoreLocalGroup(
ConversationId conversationId,
const CloudVaultConversation *indexedConversation) {
auto localRecordKey = DeriveConversationLocalRecordKey(
_vault->masterKey,
_telegramUserIdBinding,
conversationId);
if (!localRecordKey) {
return LocalGroupRecoveryResult::Invalid;
}
const auto directory = ConversationDirectory(
_telegramUserIdBinding,
conversationId);
auto metadataBlob = FileAtomicBlobStore(
directory + u"conversation.meta"_q);
auto metadataProtector = AesGcmLocalRecordProtector(
std::move(*localRecordKey));
auto metadata = PersistentConversationMetadata(
metadataBlob,
metadataProtector);
const auto metadataLoad = metadata.load(conversationId);
if (metadataLoad == ConversationMetadataLoadResult::Missing) {
return LocalGroupRecoveryResult::Missing;
} else if (metadataLoad != ConversationMetadataLoadResult::Loaded
|| !metadata.metadata()) {
return LocalGroupRecoveryResult::Invalid;
}
const auto accountId = DeriveAccountId(
_vault->identity.credential,
_sha256);
const auto &localMetadata = *metadata.metadata();
if (!accountId
|| localMetadata.accountId != *accountId
|| !IsProtectedGroupPeerBinding(
_session,
localMetadata.telegramPeerIdBinding)
|| (indexedConversation
&& indexedConversation->telegramPeerIdBinding
!= localMetadata.telegramPeerIdBinding)) {
return LocalGroupRecoveryResult::Invalid;
}
rememberProtectedPeerForPresentation(
localMetadata.telegramPeerIdBinding);
auto operationKey = DeriveConversationLocalRecordKey(
_vault->masterKey,
_telegramUserIdBinding,
conversationId);
if (!operationKey) {
return LocalGroupRecoveryResult::Invalid;
}
auto operation = std::make_unique<PendingGroupCreation>(
directory,
std::move(*operationKey),
_session,
localMetadata.telegramPeerIdBinding,
conversationId,
_sha256);
const auto metadataReload = operation->metadata.load(conversationId);
const auto journalLoad = operation->journal.load(conversationId);
const auto mlsLoad = operation->mlsState.load(conversationId);
const auto archiveLoad = operation->archiveState.load(conversationId);
const auto groupLoad = operation->groupLedger.load(conversationId);
const auto outboxLoad = operation->outbox.load();
const auto changeJournalLoad = operation->changeJournal.load(
conversationId);
const auto changeInboxLoad = operation->changeInbox.load(conversationId);
const auto keyPackageLoad = operation->keyPackages.load(
conversationId,
localMetadata.telegramPeerIdBinding);
const auto freshnessTrustLoad = operation->freshnessTrust.load(
conversationId);
const auto inboundJournalLoad = operation->inboundJournal.load();
const auto controlInboundJournalLoad = operation->controlInboundJournal.load();
const auto contentStoreLoad = operation->contentStore.load();
const auto controlSyncLoad = operation->controlSyncState.load(
conversationId);
const auto contentSyncLoad = operation->contentSyncState.load(
conversationId);
const auto fileTransferLoad = operation->fileTransfer.load(
conversationId);
if (metadataReload != ConversationMetadataLoadResult::Loaded
|| journalLoad == GroupBootstrapJournalLoadResult::StorageError
|| journalLoad
== GroupBootstrapJournalLoadResult::AuthenticationFailed
|| journalLoad == GroupBootstrapJournalLoadResult::InvalidSnapshot
|| mlsLoad == MlsStateLoadResult::StorageError
|| mlsLoad == MlsStateLoadResult::AuthenticationFailed
|| mlsLoad == MlsStateLoadResult::InvalidSnapshot
|| archiveLoad == ArchiveStateLoadResult::StorageError
|| archiveLoad == ArchiveStateLoadResult::AuthenticationFailed
|| archiveLoad == ArchiveStateLoadResult::InvalidSnapshot
|| groupLoad == GroupLedgerLoadResult::StorageError
|| groupLoad == GroupLedgerLoadResult::AuthenticationFailed
|| groupLoad == GroupLedgerLoadResult::InvalidSnapshot
|| outboxLoad == PersistentOutboxLoadResult::StorageError
|| outboxLoad == PersistentOutboxLoadResult::AuthenticationFailed
|| outboxLoad == PersistentOutboxLoadResult::InvalidSnapshot
|| changeJournalLoad == GroupChangeJournalLoadResult::StorageError
|| changeJournalLoad
== GroupChangeJournalLoadResult::AuthenticationFailed
|| changeJournalLoad == GroupChangeJournalLoadResult::InvalidSnapshot
|| changeInboxLoad == GroupChangeInboxLoadResult::StorageError
|| changeInboxLoad
== GroupChangeInboxLoadResult::AuthenticationFailed
|| changeInboxLoad == GroupChangeInboxLoadResult::InvalidSnapshot
|| keyPackageLoad == KeyPackagePoolLoadResult::StorageError
|| keyPackageLoad == KeyPackagePoolLoadResult::AuthenticationFailed
|| keyPackageLoad == KeyPackagePoolLoadResult::InvalidSnapshot
|| freshnessTrustLoad == FreshnessTrustLoadResult::StorageError
|| freshnessTrustLoad
== FreshnessTrustLoadResult::AuthenticationFailed
|| freshnessTrustLoad == FreshnessTrustLoadResult::InvalidSnapshot) {
return LocalGroupRecoveryResult::Invalid;
}
if (inboundJournalLoad == InboundJournalLoadResult::ReadFailed
|| inboundJournalLoad
== InboundJournalLoadResult::AuthenticationFailed
|| inboundJournalLoad == InboundJournalLoadResult::InvalidSnapshot
|| controlInboundJournalLoad == InboundJournalLoadResult::ReadFailed
|| controlInboundJournalLoad
== InboundJournalLoadResult::AuthenticationFailed
|| controlInboundJournalLoad
== InboundJournalLoadResult::InvalidSnapshot
|| contentStoreLoad == ContentStoreLoadResult::ReadFailed
|| contentStoreLoad == ContentStoreLoadResult::AuthenticationFailed
|| contentStoreLoad == ContentStoreLoadResult::InvalidSnapshot
|| controlSyncLoad == ControlObservationStateLoadResult::ReadFailed
|| controlSyncLoad
== ControlObservationStateLoadResult::AuthenticationFailed
|| controlSyncLoad
== ControlObservationStateLoadResult::InvalidSnapshot
|| contentSyncLoad == ContentSyncStateLoadResult::ReadFailed
|| contentSyncLoad
== ContentSyncStateLoadResult::AuthenticationFailed
|| contentSyncLoad == ContentSyncStateLoadResult::InvalidSnapshot) {
return LocalGroupRecoveryResult::Invalid;
}
if (fileTransferLoad == FileTransferLoadResult::ReadFailed
|| fileTransferLoad == FileTransferLoadResult::AuthenticationFailed
|| fileTransferLoad == FileTransferLoadResult::InvalidSnapshot) {
return LocalGroupRecoveryResult::Invalid;
}
const auto pendingFileTransfer = operation->fileTransfer.pending();
CleanupStagedProtectedSources(
operation->directory,
pendingFileTransfer
? QString::fromUtf8(pendingFileTransfer->sourcePathUtf8)
: QString());
if (journalLoad == GroupBootstrapJournalLoadResult::Pending) {
const auto envelopeCodec = EnvelopeCodecV1();
auto coordinator = GroupBootstrapTransactionCoordinator(
operation->journal,
operation->mlsState,
operation->archiveState,
operation->groupLedger,
operation->outbox,
envelopeCodec,
_sha256);
if (coordinator.recover() != GroupBootstrapApplyStatus::Recovered) {
return LocalGroupRecoveryResult::Invalid;
}
}
if (changeJournalLoad == GroupChangeJournalLoadResult::Pending) {
auto coordinator = GroupChangeTransactionCoordinator(
operation->changeJournal,
operation->mlsState,
operation->archiveState,
operation->groupLedger,
operation->outbox,
_sha256);
if (coordinator.recover() != GroupChangeApplyStatus::Recovered) {
return LocalGroupRecoveryResult::Invalid;
}
}
const auto receiptReconciliation = ReconcileMlsOutboxReceipts(
operation->mlsState,
operation->outbox,
operation->envelopeCodec,
operation->inboundJournal);
if (receiptReconciliation != MlsReceiptReconcileResult::Reconciled
&& receiptReconciliation != MlsReceiptReconcileResult::NothingToDo) {
return LocalGroupRecoveryResult::Invalid;
}
const auto state = operation->groupLedger.state();
const auto localMember = state ? state->member(*accountId) : nullptr;
const auto activeClient = localMember && std::find(
begin(localMember->clients),
end(localMember->clients),
localMetadata.clientId) != end(localMember->clients);
const auto removed = operation->mlsState.removed()
&& operation->mlsState.removalTombstone()
&& !activeClient;
const auto awaitingAdmission = !activeClient
&& !removed
&& !operation->mlsState.revision()
&& !operation->archiveState.revision()
&& !operation->keyPackages.entries().empty();
if (awaitingAdmission) {
if (!state) {
return LocalGroupRecoveryResult::Invalid;
}
const auto currentTime = std::uint64_t(base::unixtime::now());
const auto boundPackage = std::any_of(
begin(operation->keyPackages.entries()),
end(operation->keyPackages.entries()),
[&](const StoredClientKeyPackage &entry) {
const auto envelope = operation->envelopeCodec.decode(
entry.publicationEnvelope);
const auto publication = envelope
? VerifyClientKeyPackageEnvelope(
*envelope,
conversationId,
localMetadata.telegramPeerIdBinding,
state->generation(),
_sha256)
: VerifyClientKeyPackageEnvelopeOutcome();
const auto expectedObjectId = (envelope
&& publication.publication)
? DeriveClientKeyPackageObjectId(
conversationId,
localMetadata.accountId,
localMetadata.clientId,
state->generation(),
_telegramUserIdBinding,
publication.publication->accountCredential,
publication.publication->keyPackage,
_sha256)
: std::nullopt;
return envelope
&& publication.result
== ClientKeyPackageEnvelopeResult::Verified
&& publication.publication
&& expectedObjectId
&& envelope->objectId == *expectedObjectId
&& ClientAuthorizationUsableAt(
publication.publication->authorization,
currentTime);
});
if (!boundPackage) {
auto keyPackage = PrepareClientKeyPackage({
.client = {
.conversationId = conversationId,
.accountId = localMetadata.accountId,
.clientId = localMetadata.clientId,
.telegramPeerIdBinding
= localMetadata.telegramPeerIdBinding,
},
.currentGeneration = state->generation(),
.telegramUserIdBinding = _telegramUserIdBinding,
.createdAt = currentTime,
.accountCredential = &_vault->identity.credential,
.accountSigningPrivateKey
= &_vault->identity.signingPrivateKey,
},
OpenMlsBridge(),
MlsContextCodecV1(),
ClientKeyPackagePublicationCodecV1(),
operation->envelopeCodec,
_sha256);
if (keyPackage.status
!= PrepareClientKeyPackageStatus::Prepared
|| !keyPackage.entry
|| operation->keyPackages.add(std::move(*keyPackage.entry))
!= KeyPackagePoolMutationResult::Committed) {
return LocalGroupRecoveryResult::Invalid;
}
}
const auto queued = operation->keyPackages.enqueuePending(
operation->outbox,
currentTime);
if (queued != KeyPackagePoolEnqueueResult::Queued
&& queued != KeyPackagePoolEnqueueResult::NothingToDo) {
return LocalGroupRecoveryResult::Invalid;
}
}
const auto localIsGenesisOwner = state
&& state->generation() == 1
&& localMember
&& localMember->role == GroupRole::Owner
&& activeClient;
if (freshnessTrustLoad == FreshnessTrustLoadResult::Missing) {
if (operation->freshnessTrust.initialize(localIsGenesisOwner)
!= FreshnessTrustCommitResult::Committed) {
return LocalGroupRecoveryResult::Invalid;
}
}
if (!state
|| !operation->mlsState.loaded()
|| (awaitingAdmission
? archiveLoad != ArchiveStateLoadResult::Missing
: !operation->archiveState.loaded())
|| !operation->groupLedger.loaded()
|| !operation->groupLedger.revision()
|| !operation->outbox.loaded()
|| (!awaitingAdmission
&& ((!activeClient && !removed)
|| !operation->mlsState.revision()
|| !operation->archiveState.revision()))) {
return LocalGroupRecoveryResult::Invalid;
}
const auto owner = std::find_if(
begin(state->members()),
end(state->members()),
[](const GroupMember &member) {
return member.role == GroupRole::Owner;
});
if (owner == end(state->members())) {
return LocalGroupRecoveryResult::Invalid;
}
const auto genesisState = operation->groupLedger.stateAt(1);
if (!genesisState) {
return LocalGroupRecoveryResult::Invalid;
}
const auto genesisOwner = std::find_if(
begin(genesisState->members()),
end(genesisState->members()),
[](const GroupMember &member) {
return member.role == GroupRole::Owner;
});
if (genesisOwner == end(genesisState->members())) {
return LocalGroupRecoveryResult::Invalid;
}
if (controlSyncLoad == ControlObservationStateLoadResult::Loaded) {
const auto storedCheckpoint = operation->controlSyncState.checkpoint();
const auto expectedCheckpoint = operation->groupLedger.checkpointAt(
storedCheckpoint.generation);
if (!expectedCheckpoint
|| *expectedCheckpoint != storedCheckpoint
|| !operation->controlSyncState.safetyWitnesses().contains(
localMetadata.accountId)) {
return LocalGroupRecoveryResult::Invalid;
}
if (storedCheckpoint == operation->groupLedger.checkpoint()) {
operation->safetyWitnessGeneration = storedCheckpoint.generation;
operation->safetyWitnesses
= operation->controlSyncState.safetyWitnesses();
operation->ownSafetyGossipObserved
= operation->controlSyncState.ownSafetyGossipObserved();
} else {
operation->safetyWitnessGeneration = state->generation();
operation->safetyWitnesses = { localMetadata.accountId };
operation->ownSafetyGossipObserved = false;
}
} else if (controlSyncLoad
== ControlObservationStateLoadResult::LegacyLoaded) {
operation->safetyWitnessGeneration = state->generation();
operation->safetyWitnesses = { localMetadata.accountId };
operation->ownSafetyGossipObserved = false;
const auto migrated = operation->controlSyncState.advance(
operation->controlSyncState.newestObservedMessageId(),
operation->groupLedger.checkpoint(),
operation->safetyWitnesses,
false);
if (migrated != ControlObservationStateCommitResult::Committed) {
return LocalGroupRecoveryResult::Invalid;
}
}
const auto localConversation = CloudVaultConversation{
.conversationId = conversationId,
.telegramPeerIdBinding = localMetadata.telegramPeerIdBinding,
.checkpoint = operation->groupLedger.checkpoint(),
.ownerAccountId = genesisOwner->accountId,
};
if (awaitingAdmission) {
operation->phase = PendingGroupCreation::Phase::AwaitingAdmission;
} else if (removed) {
operation->phase = PendingGroupCreation::Phase::Removed;
} else {
operation->phase = PendingGroupCreation::Phase::Active;
}
operation->freshnessGate = std::make_unique<FreshnessGate>(
operation->groupLedger.checkpoint(),
operation->freshnessTrust.trusted() && !removed);
const auto indexedMatches = indexedConversation
&& indexedConversation->conversationId
== localConversation.conversationId
&& indexedConversation->telegramPeerIdBinding
== localConversation.telegramPeerIdBinding
&& indexedConversation->ownerAccountId
== localConversation.ownerAccountId;
const auto localAhead = indexedMatches
&& indexedConversation->checkpoint.generation
< localConversation.checkpoint.generation;
const auto indexedAhead = indexedMatches
&& awaitingAdmission
&& indexedConversation->checkpoint.generation
> localConversation.checkpoint.generation;
if (indexedConversation
&& *indexedConversation != localConversation
&& !localAhead
&& !indexedAhead) {
return LocalGroupRecoveryResult::Invalid;
}
operation->joinTargetCheckpoint = awaitingAdmission
? (indexedConversation
? indexedConversation->checkpoint
: localConversation.checkpoint)
: Checkpoint();
if (indexedConversation
&& !localAhead
&& operation->phase == PendingGroupCreation::Phase::Active) {
operation->conversation = *indexedConversation;
_groups[conversationId] = std::move(operation);
if (!resumeObservedJoinHistory(conversationId)
|| !initializeActivePipeline(*_groups[conversationId])) {
_groups.erase(conversationId);
return LocalGroupRecoveryResult::Invalid;
}
beginGroupObservation(conversationId);
beginContentObservation(conversationId);
pumpActiveOutbox(conversationId);
return LocalGroupRecoveryResult::Complete;
} else if (!operation->outbox.size()
&& !(removed && operation->fileTransfer.pending())) {
const auto requiresVaultUpdate = localAhead
|| (!indexedConversation
&& (awaitingAdmission || localIsGenesisOwner));
if (requiresVaultUpdate) {
operation->conversation = localConversation;
operation->vaultPreflightRequired = true;
_pendingGroupCreation = std::move(operation);
return LocalGroupRecoveryResult::Restored;
} else if (!indexedConversation) {
return LocalGroupRecoveryResult::Invalid;
}
operation->conversation = *indexedConversation;
if (!awaitingAdmission && !operation->freshnessTrust.trusted()) {
operation->vaultPreflightRequired = false;
_pendingGroupCreation = std::move(operation);
return LocalGroupRecoveryResult::Restored;
}
_groups[conversationId] = std::move(operation);
if (!awaitingAdmission
&& !removed
&& (!resumeObservedJoinHistory(conversationId)
|| !initializeActivePipeline(*_groups[conversationId]))) {
_groups.erase(conversationId);
return LocalGroupRecoveryResult::Invalid;
}
if (awaitingAdmission) {
_groupCreationState = DesktopGroupCreationState::AwaitingAdmission;
}
beginGroupObservation(conversationId);
if (!awaitingAdmission) {
beginContentObservation(conversationId);
}
return LocalGroupRecoveryResult::Complete;
}
operation->conversation = (indexedConversation && !localAhead)
? *indexedConversation
: localConversation;
operation->vaultPreflightRequired = !indexedConversation || localAhead;
_pendingGroupCreation = std::move(operation);
return LocalGroupRecoveryResult::Restored;
}
bool DesktopService::commitVaultAnchor(const UnlockedCloudVault &vault) {
const auto accountId = DeriveAccountId(
vault.identity.credential,
_sha256);
if (!accountId) {
return false;
}
const auto result = _vaultAnchor.commit({
.generation = vault.generation,
.blobDigest = vault.blobDigest,
.accountId = *accountId,
});
return result == CloudVaultAnchorCommitResult::Committed
|| result == CloudVaultAnchorCommitResult::AlreadyCommitted;
}
bool DesktopService::commitVaultAnchor(
const PreparedCloudVaultUpdate &update,
const AccountPrivateIdentity &identity) {
const auto accountId = DeriveAccountId(identity.credential, _sha256);
if (!accountId) {
return false;
}
const auto result = _vaultAnchor.commit({
.generation = update.generation,
.blobDigest = update.blobDigest,
.accountId = *accountId,
});
return result == CloudVaultAnchorCommitResult::Committed
|| result == CloudVaultAnchorCommitResult::AlreadyCommitted;
}
} // namespace E2ECloud