ZaStoGram_desktop/Telegram/SourceFiles/e2e_cloud/desktop/protected_conversation_box.cpp
2026-08-04 01:16:32 +03:00

826 lines
23 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/protected_conversation_box.h"
#include "e2e_cloud/desktop/desktop_service.h"
#include "e2e_cloud/files/private_file_manifest.h"
#include "core/application.h"
#include "core/file_utilities.h"
#include "data/data_document.h"
#include "data/data_session.h"
#include "lang/lang_keys.h"
#include "main/main_session.h"
#include "storage/storage_account.h"
#include "ui/boxes/confirm_box.h"
#include "ui/layers/generic_box.h"
#include "ui/toast/toast.h"
#include "ui/widgets/buttons.h"
#include "ui/widgets/labels.h"
#include "window/window_session_controller.h"
#include "styles/style_layers.h"
#include "styles/style_settings.h"
#include "styles/style_widgets.h"
#include <openssl/crypto.h>
#include <QtCore/QDir>
#include <QtCore/QFileInfo>
#include <QtCore/QScopeGuard>
#include <algorithm>
#include <cctype>
#include <limits>
#include <utility>
namespace E2ECloud {
namespace {
void CleanseRecords(std::vector<ProtectedContentRecord> &records) {
for (auto &record : records) {
if (!record.plaintext.isEmpty()) {
OPENSSL_cleanse(
record.plaintext.data(),
record.plaintext.size());
record.plaintext.clear();
}
}
}
template <typename Id>
[[nodiscard]] std::optional<Id> DecodeProtectedHistoryId(
const QByteArray &bytes) {
auto result = Id();
if (bytes.size() != int(result.bytes.size())) {
return std::nullopt;
}
std::copy_n(
reinterpret_cast<const std::uint8_t*>(bytes.constData()),
result.bytes.size(),
result.bytes.begin());
return result ? std::optional<Id>(result) : std::nullopt;
}
[[nodiscard]] QString ProtectedMediaCachePath(
not_null<Main::Session*> session,
ConversationId conversationId,
ObjectId eventObjectId,
const QString &filename) {
const auto conversationBytes = QByteArray(
reinterpret_cast<const char*>(conversationId.bytes.data()),
int(conversationId.bytes.size()));
auto directory = session->local().tempDirectory()
+ u"protected_media/"_q
+ QString::fromLatin1(conversationBytes.toHex())
+ u"/"_q;
if (!QDir().mkpath(directory)) {
return {};
}
const auto idBytes = QByteArray(
reinterpret_cast<const char*>(eventObjectId.bytes.data()),
int(eventObjectId.bytes.size()));
auto result = directory + QString::fromLatin1(idBytes.toHex());
const auto suffix = QFileInfo(filename).suffix().toLatin1();
const auto validSuffix = !suffix.isEmpty()
&& suffix.size() <= 16
&& std::all_of(
suffix.begin(),
suffix.end(),
[](char value) {
return std::isalnum(static_cast<unsigned char>(value));
});
if (validSuffix) {
result += u"."_q + QString::fromLatin1(suffix);
}
return result;
}
void CloseWhenVaultUnavailable(
not_null<Ui::GenericBox*> box,
not_null<DesktopService*> service) {
service->vaultStateValue() | rpl::on_next([=](DesktopVaultState state) {
if (state != DesktopVaultState::Ready) {
box->closeBox();
}
}, box->lifetime());
}
void CloseWhenSecurityChanges(
not_null<Ui::GenericBox*> box,
not_null<DesktopService*> service) {
service->securityRevisionValue(
) | rpl::skip(1) | rpl::on_next([=](std::uint64_t) {
box->closeBox();
}, box->lifetime());
}
void SaveProtectedRecord(
not_null<DesktopService*> service,
ConversationId conversationId,
ObjectId eventObjectId,
QString filename,
QWidget *guard) {
FileDialog::GetWritePath(
Core::App().getFileDialogParent(),
tr::lng_e2e_cloud_save_file(tr::now),
FileDialog::AllFilesFilter(),
std::move(filename),
crl::guard(guard, [=](QString &&path) {
if (!path.isEmpty()) {
(void)service->saveProtectedFile(
conversationId,
eventObjectId,
std::move(path),
crl::guard(guard, [](ProtectedFileSaveResult result) {
const auto text = (result
== ProtectedFileSaveResult::Saved)
? tr::lng_e2e_cloud_file_saved(tr::now)
: (result == ProtectedFileSaveResult::Busy)
? tr::lng_e2e_cloud_file_busy(tr::now)
: tr::lng_e2e_cloud_file_save_failed(tr::now);
Ui::Toast::Show({ .text = text });
}));
}
}));
}
void RebuildProtectedFiles(
not_null<Ui::VerticalLayout*> container,
DesktopService &service,
ConversationId conversationId,
not_null<std::size_t*> visibleLimit) {
container->clear();
const auto kind = ObjectKind::EncryptedFileManifest;
const auto total = service.protectedContentCount(conversationId, kind);
if (!total) {
container->add(
object_ptr<Ui::FlatLabel>(
container,
tr::lng_e2e_cloud_content_empty(),
st::boxLabel),
st::boxRowPadding);
return;
}
constexpr auto kVisiblePageSize = std::size_t(200);
const auto visible = std::min(*visibleLimit, total);
const auto first = total - visible;
if (first) {
const auto count = std::min(kVisiblePageSize, first);
const auto button = container->add(
object_ptr<Ui::SettingsButton>(
container,
tr::lng_e2e_cloud_show_older(
lt_count,
rpl::single(int(count)) | tr::to_count()),
st::settingsButton),
st::boxRowPadding,
style::al_top);
button->setClickedCallback([=, service = &service] {
*visibleLimit += count;
crl::on_main(container, [=] {
RebuildProtectedFiles(
container,
*service,
conversationId,
visibleLimit);
});
});
}
auto records = service.protectedContent(
conversationId,
first,
visible,
kind);
const auto recordsGuard = qScopeGuard([&] {
CleanseRecords(records);
});
if (records.size() != visible) {
container->add(
object_ptr<Ui::FlatLabel>(
container,
tr::lng_e2e_cloud_content_failed(),
st::boxLabel),
st::boxRowPadding);
return;
}
for (const auto &record : records) {
const auto manifest = PrivateFileManifestCodecV1()
.decodePlaintext(record.plaintext);
if (!manifest) {
continue;
}
const auto title = tr::lng_e2e_cloud_file(
tr::now,
lt_name,
QString::fromUtf8(manifest->filenameUtf8),
lt_size,
QString::number(manifest->context.plaintextSize));
const auto button = container->add(
object_ptr<Ui::SettingsButton>(
container,
rpl::single(title),
st::settingsButton),
st::boxRowPadding,
style::al_top);
const auto eventObjectId = record.eventObjectId;
const auto filename = QString::fromUtf8(manifest->filenameUtf8);
button->setClickedCallback([=, service = &service] {
SaveProtectedRecord(
service,
conversationId,
eventObjectId,
filename,
container.get());
});
}
}
[[nodiscard]] QString ContentStatusText(
const DesktopService &service,
ConversationId conversationId) {
const auto groups = service.protectedGroups();
const auto group = std::find_if(
begin(groups),
end(groups),
[&](const auto &value) {
return value.conversationId == conversationId;
});
if (group != end(groups) && group->removed) {
return tr::lng_e2e_cloud_content_removed(tr::now);
}
switch (service.contentState(conversationId)) {
case DesktopContentState::Idle:
case DesktopContentState::Ready:
return tr::lng_e2e_cloud_content_ready(tr::now);
case DesktopContentState::Synchronizing:
return tr::lng_e2e_cloud_content_syncing(tr::now);
case DesktopContentState::AwaitingFreshness:
return tr::lng_e2e_cloud_content_freshness(tr::now);
case DesktopContentState::RetryableTransportError:
return tr::lng_e2e_cloud_network_error(tr::now);
case DesktopContentState::PermanentTransportError:
case DesktopContentState::LocalFailure:
return tr::lng_e2e_cloud_content_failed(tr::now);
case DesktopContentState::SecurityBlocked:
return tr::lng_e2e_cloud_security_blocked(tr::now);
}
return tr::lng_e2e_cloud_content_failed(tr::now);
}
[[nodiscard]] QString RoleText(GroupRole role) {
switch (role) {
case GroupRole::Owner:
return tr::lng_e2e_cloud_role_owner(tr::now);
case GroupRole::Administrator:
return tr::lng_e2e_cloud_role_admin(tr::now);
case GroupRole::Member:
return tr::lng_e2e_cloud_role_member(tr::now);
}
return tr::lng_e2e_cloud_role_member(tr::now);
}
[[nodiscard]] QString HistoryAccessText(HistoryAccess historyAccess) {
switch (historyAccess.mode) {
case HistoryAccessMode::None:
return tr::lng_e2e_cloud_history_none(tr::now);
case HistoryAccessMode::FromJoin:
return tr::lng_e2e_cloud_history_from_join(tr::now);
case HistoryAccessMode::Since:
return tr::lng_e2e_cloud_history_since(tr::now);
case HistoryAccessMode::Full:
return tr::lng_e2e_cloud_history_full(tr::now);
}
return tr::lng_e2e_cloud_history_from_join(tr::now);
}
[[nodiscard]] QString MemberSecurityText(
const DesktopProtectedMember &member) {
auto text = tr::lng_e2e_cloud_member_security(
tr::now,
lt_user,
member.local
? tr::lng_e2e_cloud_you(tr::now)
: QString::number(member.telegramUserIdBinding),
lt_role,
RoleText(member.role),
lt_devices,
QString::number(member.clientCount),
lt_history,
HistoryAccessText(member.historyAccess),
lt_code,
member.accountSafetyCode);
if (!member.pairwiseSafetyCode.isEmpty()) {
text += u"\n"_q + tr::lng_e2e_cloud_pair_code(
tr::now,
lt_pair_code,
member.pairwiseSafetyCode);
}
return text;
}
[[nodiscard]] QString SecurityText(
const DesktopService &service,
ConversationId conversationId) {
const auto security = service.protectedSecurity(conversationId);
if (!security) {
return tr::lng_e2e_cloud_content_failed(tr::now);
}
auto sections = QStringList{
tr::lng_e2e_cloud_group_code(
tr::now,
lt_code,
security->groupSafetyCode),
tr::lng_e2e_cloud_witnesses(
tr::now,
lt_confirmed,
QString::number(security->witnessCount),
lt_total,
QString::number(security->memberCount)),
tr::lng_e2e_cloud_verify_hint(tr::now),
tr::lng_e2e_cloud_default_history_current(
tr::now,
lt_history,
HistoryAccessText(security->defaultHistoryAccess)),
};
for (const auto &member : security->members) {
sections.push_back(MemberSecurityText(member));
}
return sections.join(u"\n\n"_q);
}
template <typename Callback>
void AddSecurityAction(
not_null<Ui::GenericBox*> box,
QString title,
Callback &&callback) {
const auto button = box->addRow(
object_ptr<Ui::SettingsButton>(
box,
rpl::single(std::move(title)),
st::settingsButton),
style::al_top);
button->setClickedCallback(std::forward<Callback>(callback));
}
void ShowProtectedMemberSecurity(
not_null<Window::SessionController*> controller,
ConversationId conversationId,
AccountId accountId) {
controller->uiShow()->showBox(Box([=](not_null<Ui::GenericBox*> box) {
const auto service = &controller->session().e2eCloud();
CloseWhenVaultUnavailable(box, service);
CloseWhenSecurityChanges(box, service);
const auto security = service->protectedSecurity(conversationId);
const auto member = security
? std::find_if(
begin(security->members),
end(security->members),
[&](const auto &value) {
return value.accountId == accountId;
})
: std::vector<DesktopProtectedMember>::const_iterator();
if (!security || member == end(security->members)) {
box->setTitle(tr::lng_e2e_cloud_security());
box->addRow(object_ptr<Ui::FlatLabel>(
box,
tr::lng_e2e_cloud_content_failed(),
st::boxLabel));
box->addButton(tr::lng_close(), [=] { box->closeBox(); });
return;
}
box->setTitle(tr::lng_e2e_cloud_manage_member(
lt_user,
rpl::single(QString::number(member->telegramUserIdBinding))));
box->addRow(object_ptr<Ui::FlatLabel>(
box,
MemberSecurityText(*member),
st::boxLabel));
const auto apply = [=](auto callback) {
if (callback()) {
box->closeBox();
}
};
if (!member->local && security->canGrantHistory) {
AddSecurityAction(
box,
tr::lng_e2e_cloud_set_member_history(
tr::now,
lt_history,
tr::lng_e2e_cloud_history_none(tr::now)),
[=] {
apply([=] {
return service->setProtectedMemberHistory(
conversationId,
accountId,
{ .mode = HistoryAccessMode::None });
});
});
AddSecurityAction(
box,
tr::lng_e2e_cloud_set_member_history(
tr::now,
lt_history,
tr::lng_e2e_cloud_history_from_join(tr::now)),
[=] {
apply([=] {
return service->setProtectedMemberHistory(
conversationId,
accountId,
{ .mode = HistoryAccessMode::FromJoin });
});
});
}
if (!member->local && security->canGrantFullHistory) {
AddSecurityAction(
box,
tr::lng_e2e_cloud_set_member_history(
tr::now,
lt_history,
tr::lng_e2e_cloud_history_full(tr::now)),
[=] {
apply([=] {
return service->setProtectedMemberHistory(
conversationId,
accountId,
{ .mode = HistoryAccessMode::Full });
});
});
}
if (!member->local
&& member->role != GroupRole::Owner
&& security->canSetRoles) {
const auto promote = member->role == GroupRole::Member;
AddSecurityAction(
box,
promote
? tr::lng_e2e_cloud_promote_admin(tr::now)
: tr::lng_e2e_cloud_demote_member(tr::now),
[=] {
apply([=] {
return service->setProtectedMemberRole(
conversationId,
accountId,
promote
? GroupRole::Administrator
: GroupRole::Member);
});
});
}
if (!member->local
&& member->role != GroupRole::Owner
&& security->canRemoveMembers
&& (member->role != GroupRole::Administrator
|| security->canSetRoles)) {
AddSecurityAction(
box,
tr::lng_e2e_cloud_remove_member(tr::now),
[=] {
apply([=] {
return service->removeProtectedMember(
conversationId,
accountId);
});
});
}
box->addButton(tr::lng_close(), [=] { box->closeBox(); });
}));
}
void ShowProtectedSecurity(
not_null<Window::SessionController*> controller,
ConversationId conversationId) {
controller->uiShow()->showBox(Box([=](not_null<Ui::GenericBox*> box) {
const auto service = &controller->session().e2eCloud();
CloseWhenVaultUnavailable(box, service);
CloseWhenSecurityChanges(box, service);
box->setTitle(tr::lng_e2e_cloud_security());
const auto text = box->addRow(object_ptr<Ui::FlatLabel>(
box,
QString(),
st::boxLabel));
const auto refresh = [=] {
text->setText(SecurityText(*service, conversationId));
};
const auto security = service->protectedSecurity(conversationId);
if (security && security->canChangeDefaultHistory) {
const auto addDefault = [=](
HistoryAccessMode mode,
QString title) {
AddSecurityAction(box, std::move(title), [=] {
if (service->setProtectedDefaultHistory(
conversationId,
{ .mode = mode })) {
box->closeBox();
}
});
};
addDefault(
HistoryAccessMode::None,
tr::lng_e2e_cloud_set_default_history(
tr::now,
lt_history,
tr::lng_e2e_cloud_history_none(tr::now)));
addDefault(
HistoryAccessMode::FromJoin,
tr::lng_e2e_cloud_set_default_history(
tr::now,
lt_history,
tr::lng_e2e_cloud_history_from_join(tr::now)));
if (security->canGrantFullHistory) {
addDefault(
HistoryAccessMode::Full,
tr::lng_e2e_cloud_set_default_history(
tr::now,
lt_history,
tr::lng_e2e_cloud_history_full(tr::now)));
}
}
const auto canManage = security
&& (security->canSetRoles
|| security->canRemoveMembers
|| security->canGrantHistory);
if (canManage) {
for (const auto &member : security->members) {
if (member.local) {
continue;
}
AddSecurityAction(
box,
tr::lng_e2e_cloud_manage_member(
tr::now,
lt_user,
QString::number(member.telegramUserIdBinding)),
[=] {
ShowProtectedMemberSecurity(
controller,
conversationId,
member.accountId);
});
}
} else {
box->addRow(object_ptr<Ui::FlatLabel>(
box,
tr::lng_e2e_cloud_admin_freshness(),
st::boxLabel));
}
box->addButton(tr::lng_close(), [=] { box->closeBox(); });
refresh();
}));
}
void ShowProtectedFiles(
not_null<Window::SessionController*> controller,
ConversationId conversationId) {
controller->uiShow()->showBox(Box([=](not_null<Ui::GenericBox*> box) {
const auto service = &controller->session().e2eCloud();
CloseWhenVaultUnavailable(box, service);
box->setTitle(tr::lng_e2e_cloud_files());
const auto files = box->addRow(
object_ptr<Ui::VerticalLayout>(box),
style::margins());
const auto visible = box->lifetime().make_state<std::size_t>(200);
service->contentRevisionValue(
) | rpl::on_next([=](std::uint64_t) {
RebuildProtectedFiles(
files,
*service,
conversationId,
visible);
}, box->lifetime());
box->addButton(tr::lng_close(), [=] { box->closeBox(); });
}));
}
} // namespace
void OpenProtectedHistoryFile(
not_null<Window::SessionController*> controller,
not_null<DocumentData*> document,
FullMsgId context,
bool showInMediaView,
const QByteArray &conversationIdBytes,
const QByteArray &eventObjectIdBytes) {
const auto conversationId = DecodeProtectedHistoryId<ConversationId>(
conversationIdBytes);
const auto eventObjectId = DecodeProtectedHistoryId<ObjectId>(
eventObjectIdBytes);
if (!conversationId || !eventObjectId) {
Ui::Toast::Show({
.text = tr::lng_e2e_cloud_file_save_failed(tr::now),
});
return;
}
const auto download = [=](QString path, bool showSavedToast) {
if (path.isEmpty()) {
Ui::Toast::Show({
.text = tr::lng_e2e_cloud_file_save_failed(tr::now),
});
return;
}
if (document->uploading()) {
return;
}
document->status = FileReady;
document->uploadingData = std::make_unique<Data::UploadState>(
std::max(document->size, int64(1)));
document->owner().documentLoadProgress(document);
const auto savedPath = path;
(void)controller->session().e2eCloud().saveProtectedFile(
*conversationId,
*eventObjectId,
std::move(path),
crl::guard(controller, [=](ProtectedFileSaveResult result) {
const auto saved
= (result == ProtectedFileSaveResult::Saved);
document->uploadingData = nullptr;
document->status = saved
? FileReady
: FileDownloadFailed;
if (!saved || showSavedToast) {
Ui::Toast::Show({
.text = saved
? tr::lng_e2e_cloud_file_saved(tr::now)
: (result == ProtectedFileSaveResult::Busy)
? tr::lng_e2e_cloud_file_busy(tr::now)
: tr::lng_e2e_cloud_file_save_failed(
tr::now),
});
}
if (saved) {
document->setLocation(Core::FileLocation(savedPath));
document->owner().documentLoadDone(document);
controller->openDocument(
document,
showInMediaView,
{ .id = context });
} else {
document->owner().documentLoadFail(document, true);
}
}),
crl::guard(controller, [=](
std::uint64_t receivedBytes,
std::uint64_t totalBytes) {
if (!document->uploadingData || !totalBytes) {
return;
}
const auto maximum = std::uint64_t(
std::numeric_limits<int64>::max());
document->uploadingData->size = int64(
std::min(totalBytes, maximum));
document->uploadingData->offset = int64(std::min(
receivedBytes,
std::uint64_t(document->uploadingData->size)));
document->owner().documentLoadProgress(document);
}));
};
if (showInMediaView) {
download(
ProtectedMediaCachePath(
&controller->session(),
*conversationId,
*eventObjectId,
document->filename()),
false);
return;
}
FileDialog::GetWritePath(
Core::App().getFileDialogParent(),
tr::lng_e2e_cloud_save_file(tr::now),
FileDialog::AllFilesFilter(),
document->filename(),
crl::guard(controller, [=](QString &&path) {
if (!path.isEmpty()) {
download(std::move(path), true);
}
}));
}
void ShowProtectedGroupList(
not_null<Window::SessionController*> controller) {
controller->uiShow()->showBox(Box([=](not_null<Ui::GenericBox*> box) {
const auto service = &controller->session().e2eCloud();
CloseWhenVaultUnavailable(box, service);
box->setTitle(tr::lng_e2e_cloud_title());
const auto groups = service->protectedGroups();
if (groups.empty()) {
box->addRow(object_ptr<Ui::FlatLabel>(
box,
tr::lng_e2e_cloud_chats_empty(),
st::boxLabel));
}
for (const auto &group : groups) {
const auto title = group.title
+ u" · "_q
+ QString::number(group.contentCount)
+ u" · #"_q
+ QString::number(group.generation);
const auto button = box->addRow(
object_ptr<Ui::SettingsButton>(
box,
rpl::single(title),
st::settingsButton),
style::al_top);
button->setClickedCallback([=] {
box->closeBox();
ShowProtectedConversation(
controller,
group.conversationId);
});
}
box->addButton(tr::lng_close(), [=] { box->closeBox(); });
}));
}
void ShowProtectedConversation(
not_null<Window::SessionController*> controller,
ConversationId conversationId) {
controller->uiShow()->showBox(Box([=](not_null<Ui::GenericBox*> box) {
const auto service = &controller->session().e2eCloud();
CloseWhenVaultUnavailable(box, service);
const auto groups = service->protectedGroups();
const auto group = std::find_if(
begin(groups),
end(groups),
[&](const auto &value) {
return value.conversationId == conversationId;
});
box->setTitle((group != end(groups))
? group->title
: tr::lng_e2e_cloud_title(tr::now));
const auto status = box->addRow(object_ptr<Ui::FlatLabel>(
box,
QString(),
st::boxLabel));
const auto refreshStatus = [=] {
status->setText(ContentStatusText(*service, conversationId));
};
service->contentStateValue(
) | rpl::on_next([=](DesktopContentState) {
refreshStatus();
}, box->lifetime());
service->securityRevisionValue(
) | rpl::on_next([=](std::uint64_t) {
refreshStatus();
}, box->lifetime());
const auto cancelFile = box->addButton(
tr::lng_e2e_cloud_cancel_file(),
[=] {
box->uiShow()->showBox(Ui::MakeConfirmBox({
.text = tr::lng_e2e_cloud_cancel_file_sure(),
.confirmed = [=] {
const auto cancelled
= service->cancelProtectedFileTransfer(
conversationId);
Ui::Toast::Show({
.text = cancelled
? tr::lng_e2e_cloud_file_cancelled(tr::now)
: tr::lng_e2e_cloud_file_cancel_failed(
tr::now),
});
},
.confirmText = tr::lng_e2e_cloud_cancel_file(),
}));
});
const auto updateCancelFile = [=] {
const auto current = service->protectedGroups();
const auto found = std::find_if(
begin(current),
end(current),
[&](const auto &value) {
return value.conversationId == conversationId;
});
cancelFile->setDisabled(
found == end(current)
|| !found->active
|| !found->fileTransferPending);
};
service->contentStateValue(
) | rpl::on_next([=](DesktopContentState) {
updateCancelFile();
}, cancelFile->lifetime());
service->fileTransferRevisionValue(
) | rpl::on_next([=](std::uint64_t) {
refreshStatus();
updateCancelFile();
}, cancelFile->lifetime());
updateCancelFile();
box->addButton(tr::lng_e2e_cloud_files(), [=] {
ShowProtectedFiles(controller, conversationId);
});
box->addButton(tr::lng_e2e_cloud_security(), [=] {
ShowProtectedSecurity(controller, conversationId);
});
box->addButton(tr::lng_e2e_cloud_sync(), [=] {
service->synchronizeProtectedContent(conversationId);
});
box->addButton(tr::lng_close(), [=] { box->closeBox(); });
service->synchronizeProtectedContent(conversationId);
refreshStatus();
}));
}
} // namespace E2ECloud