diff options
Diffstat (limited to 'tde2e/td/e2e/Call.cpp')
| -rw-r--r-- | tde2e/td/e2e/Call.cpp | 792 |
1 files changed, 792 insertions, 0 deletions
diff --git a/tde2e/td/e2e/Call.cpp b/tde2e/td/e2e/Call.cpp new file mode 100644 index 000000000..c467329c8 --- /dev/null +++ b/tde2e/td/e2e/Call.cpp @@ -0,0 +1,792 @@ +// +// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2025 +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +#include "td/e2e/Call.h" + +#include "td/e2e/e2e_api.h" +#include "td/e2e/MessageEncryption.h" +#include "td/e2e/Mnemonic.h" + +#include "td/telegram/e2e_api.hpp" +#include "td/utils/algorithm.h" + +#include "td/utils/as.h" +#include "td/utils/common.h" +#include "td/utils/crypto.h" +#include "td/utils/logging.h" +#include "td/utils/misc.h" +#include "td/utils/overloaded.h" +#include "td/utils/Random.h" +#include "td/utils/SliceBuilder.h" +#include "td/utils/tl_helpers.h" +#include "td/utils/tl_parsers.h" + +#include <algorithm> +#include <limits> +#include <memory> +#include <tuple> +#include <utility> + +namespace tde2e_core { + +CallVerificationChain::State CallVerificationChain::get_state() const { + return state_; +} + +template <class F> +struct LambdaStorer { + const F &store_; +}; + +template <class F, class StorerT> +void store(const LambdaStorer<F> &lambda_storer, StorerT &storer) { + lambda_storer.store_(storer); +} + +template <class F> +std::string lambda_serialize(F &&f) { + return td::serialize(LambdaStorer<F>{std::forward<F>(f)}); +} + +void CallVerificationChain::on_new_main_block(const Blockchain &blockhain) { + state_ = Commit; + CHECK(blockhain.get_height() > height_); + height_ = td::narrow_cast<td::int32>(blockhain.get_height()); + last_block_hash_ = blockhain.last_block_hash_; + verification_state_ = {}; + verification_state_.height = height_; + + verification_words_ = + CallVerificationWords{height_, Mnemonic::generate_verification_words(last_block_hash_.as_slice())}; + auto &group_state = *blockhain.state_.group_state_; + committed_ = {}; + revealed_ = {}; + + participant_keys_ = {}; + for (auto &participant : group_state.participants) { + participant_keys_.emplace(participant.user_id, participant.public_key); + } + CHECK(participant_keys_.size() == group_state.participants.size()); + + commit_at_ = td::Timestamp::now(); + reveal_at_ = {}; + done_at_ = {}; + users_ = {}; + for (auto &participant : group_state.participants) { + users_[participant.user_id]; + } + + if (auto it = delayed_broadcasts_.find(height_); it != delayed_broadcasts_.end()) { + for (auto &[message, broadcast] : it->second) { + auto status = process_broadcast(std::move(message), std::move(broadcast)); + LOG_IF(ERROR, status.is_error()) << "Failed to process broadcast: " << status; + } + delayed_broadcasts_.erase(it); + } +} + +td::Status CallVerificationChain::try_apply_block(td::Slice message) { + // parse e2e::e2e_chain_GroupBroadcast + td::TlParser parser(message); + auto kv_broadcast = e2e::e2e_chain_GroupBroadcast::fetch(parser); + parser.fetch_end(); + TRY_STATUS(parser.get_status()); + + td::int32 chain_height{-1}; + downcast_call(*kv_broadcast, td::overloaded([&](auto &broadcast) { chain_height = broadcast.chain_height_; })); + + if (chain_height < height_) { + LOG(INFO) << "Skip old broadcast " << to_short_string(kv_broadcast); + // broadcast is too old + return td::Status::OK(); + } + + if (chain_height > height_) { + if (!delay_allowed_) { + return Error(E::InvalidBroadcast_InFuture, PSLICE() + << "broadcast_height=" << chain_height << " height=" << height_); + } + + LOG(INFO) << "Delay broadcast " << to_short_string(kv_broadcast); + delayed_broadcasts_[chain_height].emplace_back(message.str(), std::move(kv_broadcast)); + return td::Status::OK(); + } + + return process_broadcast(message.str(), std::move(kv_broadcast)); +} + +std::string CallVerificationChain::to_short_string(e2e::object_ptr<e2e::e2e_chain_GroupBroadcast> &broadcast) { + td::StringBuilder sb; + downcast_call(*broadcast, + td::overloaded([&](e2e::e2e_chain_groupBroadcastNonceCommit &commit) { sb << "CommitBroadcast"; }, + [&](e2e::e2e_chain_groupBroadcastNonceReveal &reveal) { sb << "RevealBroadcast"; })); + downcast_call(*broadcast, [&](auto &v) { + sb << "{height=" << v.chain_height_ << " user_id=" << v.user_id_; + auto it = participant_keys_.find(v.user_id_); + if (it != participant_keys_.end()) { + sb << " pk=" << it->second; + } else { + sb << " pk=?"; + } + sb << "}"; + }); + return sb.as_cslice().str(); +} + +td::Status CallVerificationChain::process_broadcast(std::string message, + e2e::object_ptr<e2e::e2e_chain_GroupBroadcast> broadcast) { + td::Status status; + td::UInt256 broadcast_chain_hash{}; + downcast_call(*broadcast, td::overloaded([&](auto &broadcast) { broadcast_chain_hash = broadcast.chain_hash_; })); + if (broadcast_chain_hash != last_block_hash_) { + status = Error(E::InvalidBroadcast_InvalidBlockHash); + } + if (status.is_ok()) { + downcast_call( + *broadcast, + td::overloaded([&](e2e::e2e_chain_groupBroadcastNonceCommit &commit) { status = process_broadcast(commit); }, + [&](e2e::e2e_chain_groupBroadcastNonceReveal &reveal) { status = process_broadcast(reveal); })); + } + + if (status.is_error()) { + LOG(ERROR) << "Failed broadcast\n" << to_short_string(broadcast) << "\n\t" << status; + } else { + LOG(DEBUG) << "Applied broadcast\n\t" << to_short_string(broadcast) << "\n\t" << *this; + } + return status; +} + +CallVerificationState CallVerificationChain::get_verification_state() const { + return verification_state_; +} + +CallVerificationWords CallVerificationChain::get_verification_words() const { + return verification_words_; +} + +td::Status CallVerificationChain::process_broadcast(e2e::e2e_chain_groupBroadcastNonceCommit &nonce_commit) { + CHECK(nonce_commit.chain_height_ == height_); + if (state_ != Commit) { + return Error(E::InvalidBroadcast_NotInCommit); + } + auto user_id = nonce_commit.user_id_; + auto it = participant_keys_.find(user_id); + if (it == participant_keys_.end()) { + return Error(E::InvalidBroadcast_UnknownUserId); + } + auto public_key = it->second; + if (!may_skip_signatures_validation_) { + TRY_STATUS(verify_signature(public_key, nonce_commit)); + } + + if (committed_.count(user_id) != 0) { + return Error(E::InvalidBroadcast_AlreadyApplied); + } + + committed_[user_id] = nonce_commit.nonce_hash_.as_slice().str(); + users_[user_id].receive_commit_at_ = td::Timestamp::now(); + + if (committed_.size() == participant_keys_.size()) { + state_ = Reveal; + reveal_at_ = td::Timestamp::now(); + } + + return td::Status::OK(); +} + +td::Status CallVerificationChain::process_broadcast(e2e::e2e_chain_groupBroadcastNonceReveal &nonce_reveal) { + CHECK(nonce_reveal.chain_height_ == height_); + if (state_ != Reveal) { + return Error(E::InvalidBroadcast_NotInReveal); + } + auto user_id = nonce_reveal.user_id_; + auto user_id_it = participant_keys_.find(user_id); + if (user_id_it == participant_keys_.end()) { + return Error(E::InvalidBroadcast_UnknownUserId); + } + auto public_key = user_id_it->second; + if (!may_skip_signatures_validation_) { + TRY_STATUS(verify_signature(public_key, nonce_reveal)); + } + + if (revealed_.count(user_id) != 0) { + return Error(E::InvalidBroadcast_AlreadyApplied); + } + + auto it = committed_.find(user_id); + CHECK(it != committed_.end()); + auto expected_nonce_hash = it->second; + auto received_nonce_hash = td::sha256(nonce_reveal.nonce_.as_slice()); + if (expected_nonce_hash != received_nonce_hash) { + return Error(E::InvalidBroadcast_InvalidReveal); + } + + revealed_[user_id] = nonce_reveal.nonce_.as_slice().str(); + users_[user_id].receive_reveal_at_ = td::Timestamp::now(); + + CHECK(!verification_state_.emoji_hash); + if (revealed_.size() == participant_keys_.size()) { + auto nonces = td::transform(revealed_, [](auto &p) { return p.second; }); + std::sort(nonces.begin(), nonces.end()); + + std::string full_nonce; + for (auto &nonce : nonces) { + full_nonce += nonce; + } + + verification_state_.emoji_hash = + MessageEncryption::hmac_sha512(full_nonce, last_block_hash_.as_slice()).as_slice().str(); + state_ = End; + done_at_ = td::Timestamp::now(); + } + return td::Status::OK(); +} + +CallEncryption::CallEncryption(td::int64 user_id, PrivateKey private_key) + : user_id_(user_id), private_key_(std::move(private_key)) { +} + +td::Status CallEncryption::add_shared_key(td::int32 epoch, td::UInt256 epoch_hash, td::SecureString key, + GroupStateRef group_state) { + sync(); + + TRY_RESULT(self, group_state->get_participant(private_key_.to_public_key())); + if (self.user_id != user_id_) { + // should not happen + return td::Status::Error("Wrong user identifier in state"); + } + + LOG(INFO) << "Add key from epoch: " << epoch; + epoch_by_hash_[epoch_hash] = epoch; + auto added = + epochs_.emplace(epoch, EpochInfo(epoch, epoch_hash, self.user_id, std::move(key), std::move(group_state))).second; + CHECK(added); + return td::Status::OK(); +} + +void CallEncryption::forget_shared_key(td::int32 epoch, td::UInt256 epoch_hash) { + sync(); + epochs_to_forget_.emplace(td::Timestamp::in(FORGET_EPOCH_DELAY), epoch); +} + +td::Result<std::string> CallEncryption::decrypt(td::int64 user_id, td::int32 channel_id, td::Slice encrypted_data) { + sync(); + if (user_id == user_id_) { + return td::Status::Error("Packet is encrypted by us"); + } + td::TlParser parser(encrypted_data); + auto head = static_cast<td::uint32>(parser.fetch_int()); + td::int32 epochs_n = head & 0xff; + auto version = (head >> 8) & 0xff; + auto reserved = head >> 16; + + if (version != 0) { + return td::Status::Error("Unsupported protocol version"); + } + if (reserved != 0) { + return td::Status::Error("Reserved part of head is not zero"); + } + + if (epochs_n > MAX_ACTIVE_EPOCHS) { + return td::Status::Error("Too many active epochs"); + } + + std::vector<td::UInt256> epoch_hashes(epochs_n); + for (auto &epoch : epoch_hashes) { + parse(epoch, parser); + } + auto unencrypted_header = encrypted_data.substr(0, encrypted_data.size() - parser.get_left_len()); + + std::vector<td::Slice> encrypted_headers; + for (td::int32 i = 0; i < epochs_n; i++) { + auto encrypted_header = parser.template fetch_string_raw<td::Slice>(32); + encrypted_headers.emplace_back(encrypted_header); + } + + auto encrypted_packet = parser.template fetch_string_raw<td::Slice>(parser.get_left_len()); + parser.fetch_end(); + TRY_STATUS(parser.get_status()); + + for (td::int32 i = 0; i < epochs_n; i++) { + auto epoch_hash = epoch_hashes[i]; + auto encrypted_header = encrypted_headers[i]; + if (auto it = epoch_by_hash_.find(epoch_hash); it != epoch_by_hash_.end()) { + auto it2 = epochs_.find(it->second); + if (it2 != epochs_.end()) { + auto &epoch_info = it2->second; + TRY_RESULT(one_time_secret, + MessageEncryption::decrypt_header(encrypted_header, encrypted_packet, epoch_info.secret_)); + return decrypt_packet_with_secret(user_id, channel_id, unencrypted_header, encrypted_packet, one_time_secret, + epoch_info.group_state_); + } + } + } + return Error(E::Decrypt_UnknownEpoch); +} + +td::Result<std::string> CallEncryption::encrypt(td::int32 channel_id, td::Slice decrypted_data) { + sync(); + + // use all active epochs + if (epochs_.empty()) { + return Error(E::Encrypt_UnknownEpoch); + } + auto epochs_n = td::narrow_cast<td::int32>(epochs_.size()); + + using td::store; + std::string header_a = lambda_serialize([&](auto &storer) { + store(epochs_n, storer); + for (auto &[epoch_i, epoch] : epochs_) { + store(epoch.epoch_hash_, storer); + } + }); + + td::SecureString one_time_secret(32, 0); + td::Random::secure_bytes(one_time_secret.as_mutable_slice()); + TRY_RESULT(encrypted_packet, encrypt_packet_with_secret(channel_id, header_a, decrypted_data, one_time_secret)); + + std::vector<td::SecureString> encrypted_headers; + for (auto &[epoch_i, epoch] : epochs_) { + TRY_RESULT(encrypted_header, MessageEncryption::encrypt_header(one_time_secret, encrypted_packet, epoch.secret_)); + encrypted_headers.emplace_back(std::move(encrypted_header)); + } + + std::string header_b = lambda_serialize([&](auto &storer) { + for (auto &encrypted_header : encrypted_headers) { + CHECK(encrypted_header.size() == 32); + storer.store_slice(encrypted_header); + } + }); + + //LOG(ERROR) << decrypted_data.size() << " -> " << header_a.size() << " + " << header_b.size() << " + " << encrypted_packet.size(); + return header_a + header_b + encrypted_packet; +} + +std::string add_magic(td::int32 magic, td::Slice header) { + std::string res(4 + header.size(), '\0'); + td::as<td::int32>(res.data()) = magic; + td::MutableSlice(res).substr(4).copy_from(header); + return res; +} + +td::Result<std::string> CallEncryption::encrypt_packet_with_secret(td::int32 channel_id, td::Slice unencrypted_part, + td::Slice packet, td::Slice one_time_secret) { + TRY_STATUS(validate_channel_id(channel_id)); + auto &seqno = seqno_[channel_id]; + if (seqno == std::numeric_limits<td::uint32>::max()) { + return td::Status::Error("Seqno overflow"); + } + seqno++; + + auto payload = lambda_serialize([&](auto &storer) { + using td::store; + store(static_cast<td::int32>(channel_id), storer); + store(seqno, storer); + storer.store_slice(packet); + }); + + // TODO: there is too much copies happening here. Almost all of them could be avoided + td::UInt256 large_msg_id{}; + auto encrypted_payload = MessageEncryption::encrypt_data( + payload, one_time_secret, add_magic(td::e2e_api::e2e_callPacket::ID, unencrypted_part), &large_msg_id); + auto to_sign = add_magic(td::e2e_api::e2e_callPacketLargeMsgId::ID, large_msg_id.as_slice()); + + TRY_RESULT(signature, private_key_.sign(to_sign)); + return encrypted_payload.as_slice().str() + signature.to_slice().str(); +} + +td::Result<std::string> CallEncryption::decrypt_packet_with_secret( + td::int64 expected_user_id, td::int32 expected_channel_id, td::Slice unencrypted_header, td::Slice encrypted_packet, + td::Slice one_time_secret, const GroupStateRef &group_state) { + TRY_RESULT(participant, group_state->get_participant(expected_user_id)); + if (encrypted_packet.size() < 64) { + return td::Status::Error("Not enough encryption data"); + } + TRY_RESULT(signature, Signature::from_slice(encrypted_packet.substr(encrypted_packet.size() - 64, 64))); + encrypted_packet.remove_suffix(64); + + td::UInt256 large_msg_id{}; + TRY_RESULT(payload_str, MessageEncryption::decrypt_data( + encrypted_packet, one_time_secret, + add_magic(td::e2e_api::e2e_callPacket::ID, unencrypted_header), &large_msg_id)); + // we know that this is packet created by some participant + + auto payload = td::Slice(payload_str); + auto to_verify = add_magic(td::e2e_api::e2e_callPacketLargeMsgId::ID, large_msg_id.as_slice()); + TRY_STATUS(participant.public_key.verify(to_verify, signature)); + + td::TlParser parser(payload); + td::int32 channel_id; + td::uint32 seqno{}; + parse(channel_id, parser); + TRY_STATUS(validate_channel_id(channel_id)); + parse(seqno, parser); + auto result = parser.template fetch_string_raw<std::string>(parser.get_left_len()); + parser.fetch_end(); + TRY_STATUS(parser.get_status()); + + if (channel_id != expected_channel_id) { + // currently ignore expected_channel_id + // return td::Status::Error("Channel identifier mismatch"); + } + TRY_STATUS(check_not_seen(participant.public_key, channel_id, seqno)); + mark_as_seen(participant.public_key, channel_id, seqno); + return result; +} + +td::Status CallEncryption::check_not_seen(const PublicKey &public_key, td::int32 channel_id, td::uint32 seqno) { + auto &s = seen_[std::make_pair(public_key, channel_id)]; + if (s.empty()) { + return td::Status::OK(); + } + auto value = seqno; + if (value < *s.begin()) { + return td::Status::Error("Message is too old"); + } + if (s.count(value) != 0) { + return td::Status::Error("Message is already processed"); + } + return td::Status::OK(); +} + +void CallEncryption::mark_as_seen(const PublicKey &public_key, td::int32 channel_id, td::uint32 seqno) { + auto value = seqno; + auto &s = seen_[std::make_pair(public_key, channel_id)]; + CHECK(s.insert(value).second); + while (s.size() > 1024 || (!s.empty() && *s.begin() + 1024 < seqno)) { + s.erase(s.begin()); + } +} + +void CallEncryption::sync() { + auto now = td::Timestamp::now(); + while (!epochs_to_forget_.empty() && + (epochs_to_forget_.front().first.is_in_past(now) || epochs_.size() > MAX_ACTIVE_EPOCHS)) { + auto epoch = epochs_to_forget_.front().second; + LOG(INFO) << "Forget key from epoch: " << epoch; + auto it = epochs_.find(epoch); + if (it != epochs_.end()) { + epoch_by_hash_.erase(it->second.epoch_hash_); + epochs_.erase(it); + } + epochs_to_forget_.pop(); + } +} + +td::Status CallEncryption::validate_channel_id(td::int32 channel_id) { + if (channel_id < 0 || channel_id > 1023) { + return Error(E::InvalidCallChannelId); + } + return td::Status::OK(); +} + +CallVerification CallVerification::create(td::int64 user_id, PrivateKey private_key, const Blockchain &blockchain) { + CallVerification result; + result.user_id_ = user_id; + result.private_key_ = std::move(private_key); + result.chain_.allow_delay(); + result.chain_.set_user_id(user_id); + result.on_new_main_block(blockchain); + return result; +} + +void CallVerification::on_new_main_block(const Blockchain &blockchain) { + auto nonce = generate_nonce(); + td::UInt256 nonce_hash; + td::sha256(nonce.as_mutable_slice(), nonce_hash.as_mutable_slice()); + + auto height = td::narrow_cast<td::int32>(blockchain.get_height()); + auto last_block_hash = blockchain.last_block_hash_; + auto nonce_commit_tl = e2e::e2e_chain_groupBroadcastNonceCommit({}, user_id_, height, last_block_hash, nonce_hash); + nonce_commit_tl.signature_ = sign(private_key_, nonce_commit_tl).move_as_ok().to_u512(); + auto nonce_commit = serialize_boxed(nonce_commit_tl); + + height_ = height; + last_block_hash_ = blockchain.last_block_hash_; + nonce_ = nonce; + sent_commit_ = true; + sent_reveal_ = false; + pending_outbound_messages_ = {nonce_commit}; + chain_.on_new_main_block(blockchain); +} + +CallVerificationWords CallVerification::get_verification_words() const { + return chain_.get_verification_words(); +} + +CallVerificationState CallVerification::get_verification_state() const { + return chain_.get_verification_state(); +} + +std::vector<std::string> CallVerification::pull_outbound_messages() { + std::vector<std::string> result; + std::swap(result, pending_outbound_messages_); + return result; +} + +td::Status CallVerification::receive_inbound_message(td::Slice message) { + TRY_STATUS(chain_.try_apply_block(message)); + + if (chain_.get_state() == CallVerificationChain::Reveal && !sent_reveal_) { + sent_reveal_ = true; + auto nonce_reveal_tl = e2e::e2e_chain_groupBroadcastNonceReveal({}, user_id_, height_, last_block_hash_, nonce_); + nonce_reveal_tl.signature_ = sign(private_key_, nonce_reveal_tl).move_as_ok().to_u512(); + auto nonce_reveal = serialize_boxed(nonce_reveal_tl); + CHECK(pending_outbound_messages_.empty()); + pending_outbound_messages_.push_back(nonce_reveal); + } + return td::Status::OK(); +} + +Call::Call(td::int64 user_id, PrivateKey pk, ClientBlockchain blockchain) + : user_id_(user_id) + , private_key_(std::move(pk)) + , blockchain_(std::move(blockchain)) + , call_encryption_(user_id, private_key_) { + CHECK(private_key_); + call_verification_ = CallVerification::create(user_id_, private_key_, blockchain_.get_inner_chain()); + LOG(INFO) << "Create call \n" << *this; +} + +td::Result<std::string> Call::create_zero_block(const PrivateKey &private_key, GroupStateRef group_state) { + TRY_RESULT(blockchain, ClientBlockchain::create_empty()); + TRY_RESULT(changes, make_changes_for_new_state(std::move(group_state))); + return blockchain.build_block(changes, private_key); +} + +td::Result<std::string> Call::create_self_add_block(const PrivateKey &private_key, td::Slice previous_block_server, + const GroupParticipant &self) { + TRY_RESULT(previous_block, Blockchain::from_server_to_local(previous_block_server.str())); + TRY_RESULT(blockchain, ClientBlockchain::create_from_block(previous_block, private_key.to_public_key())); + auto old_state = *blockchain.get_group_state(); + td::remove_if(old_state.participants, + [&self](const GroupParticipant &participant) { return participant.user_id == self.user_id; }); + old_state.participants.push_back(self); + auto new_group_state = std::make_shared<GroupState>(std::move(old_state)); + TRY_RESULT(changes, make_changes_for_new_state(std::move(new_group_state))); + return blockchain.build_block(changes, private_key); +} + +td::Result<Call> Call::create(td::int64 user_id, PrivateKey private_key, td::Slice last_block_server) { + TRY_RESULT(last_block, Blockchain::from_server_to_local(last_block_server.str())); + TRY_RESULT(blockchain, ClientBlockchain::create_from_block(last_block, private_key.to_public_key())); + auto call = Call(user_id, std::move(private_key), std::move(blockchain)); + TRY_STATUS(call.update_group_shared_key()); + return call; +} + +td::Result<std::string> Call::build_change_state(GroupStateRef new_group_state) const { + TRY_STATUS(get_status()); + TRY_RESULT(changes, make_changes_for_new_state(std::move(new_group_state))); + return blockchain_.build_block(changes, private_key_); +} + +td::Result<std::vector<Change>> Call::make_changes_for_new_state(GroupStateRef group_state) { + TRY_RESULT(e_private_key, PrivateKey::generate()); + td::SecureString group_shared_key(32); + td::Random::secure_bytes(group_shared_key.as_mutable_slice()); + + td::SecureString one_time_secret(32); + td::Random::secure_bytes(one_time_secret.as_mutable_slice()); + + auto encrypted_group_shared_key = MessageEncryption::encrypt_data(group_shared_key, one_time_secret); + + std::vector<td::int64> dst_user_id; + std::vector<std::string> dst_header; + for (auto &participant : group_state->participants) { + auto public_key = participant.public_key; + TRY_RESULT(shared_key, e_private_key.compute_shared_secret(public_key)); + dst_user_id.push_back(participant.user_id); + TRY_RESULT(header, MessageEncryption::encrypt_header(one_time_secret, encrypted_group_shared_key, shared_key)); + dst_header.push_back(header.as_slice().str()); + } + auto change_set_shared_key = Change{ChangeSetSharedKey{std::make_shared<GroupSharedKey>( + GroupSharedKey{e_private_key.to_public_key(), encrypted_group_shared_key.as_slice().str(), std::move(dst_user_id), + std::move(dst_header)})}}; + auto change_set_group_state = Change{ChangeSetGroupState{std::move(group_state)}}; + + return std::vector<Change>{std::move(change_set_group_state), std::move(change_set_shared_key)}; +} + +td::Result<td::int32> Call::get_height() const { + TRY_STATUS(get_status()); + return td::narrow_cast<td::int32>(blockchain_.get_height()); +} + +td::Result<GroupStateRef> Call::get_group_state() const { + TRY_STATUS(get_status()); + return blockchain_.get_group_state(); +} + +td::Status Call::apply_block(td::Slice server_block) { + TRY_STATUS(get_status()); + TRY_RESULT(block, Blockchain::from_server_to_local(server_block.str())); + auto status = do_apply_block(block); + if (status.is_error()) { + LOG(ERROR) << "Failed to apply block: " << status << "\n" << Block::from_tl_serialized(block); + status_ = std::move(status); + } else { + LOG(INFO) << "Block has been applied\n" << *this; + } + + return get_status(); +} +td::Status Call::do_apply_block(td::Slice block) { + TRY_RESULT(changes, blockchain_.try_apply_block(block)); + call_verification_.on_new_main_block(blockchain_.get_inner_chain()); + TRY_STATUS(update_group_shared_key()); + return td::Status::OK(); +} + +td::Result<td::SecureString> Call::decrypt_shared_key() { + auto group_shared_key = blockchain_.get_group_shared_key(); + for (size_t i = 0; i < group_shared_key->dest_user_id.size(); i++) { + if (group_shared_key->dest_user_id[i] == user_id_) { + TRY_RESULT(shared_key, private_key_.compute_shared_secret(group_shared_key->ek)); + TRY_RESULT(one_time_secret, + MessageEncryption::decrypt_header(group_shared_key->dest_header[i], + group_shared_key->encrypted_shared_key, shared_key)); + TRY_RESULT(decrypted_shared_key, + MessageEncryption::decrypt_data(group_shared_key->encrypted_shared_key, one_time_secret)); + if (decrypted_shared_key.size() != 32) { + return td::Status::Error("Invalid shared key (size != 32)"); + } + group_shared_key_ = td::SecureString( + MessageEncryption::hmac_sha512(group_shared_key_, blockchain_.get_last_block_hash().as_slice()) + .as_slice() + .substr(0, 32)); + return decrypted_shared_key; + } + } + return td::Status::Error("Could not find user_id in group_shared_key"); +} + +td::Status Call::update_group_shared_key() { + // NB: we drop key immediately, we don't want old key to be active due to some errors later + group_shared_key_ = {}; + call_encryption_.forget_shared_key(td::narrow_cast<td::int32>(blockchain_.get_height() - 1), + blockchain_.get_previous_block_hash()); + + auto group_state = blockchain_.get_group_state(); + + auto r_participant = group_state->get_participant(private_key_.to_public_key()); + if (r_participant.is_error()) { + return Error(E::InvalidCallGroupState_NotParticipant); + } + auto participant = r_participant.move_as_ok(); + if (participant.user_id != user_id_) { + return Error(E::InvalidCallGroupState_WrongUserId); + } + + TRY_RESULT_ASSIGN(group_shared_key_, decrypt_shared_key()); + + return call_encryption_.add_shared_key(td::narrow_cast<td::int32>(blockchain_.get_height()), + blockchain_.get_last_block_hash(), group_shared_key_.copy(), group_state); +} + +td::StringBuilder &operator<<(td::StringBuilder &sb, const CallVerificationChain &chain) { + sb << "Verification {height=" << chain.height_ << " state="; + switch (chain.state_) { + case CallVerificationChain::State::Commit: + sb << "commit"; + break; + case CallVerificationChain::State::Reveal: + sb << "reveal"; + break; + case CallVerificationChain::State::End: + sb << "done"; + break; + } + sb << " commit_n=" << chain.committed_.size() << " reveal_n=" << chain.revealed_.size() << "}"; + auto now = td::Timestamp::now(); + sb << "\n\t\t"; + sb << "commit->"; + if (chain.state_ == CallVerificationChain::State::Commit) { + sb << (now.at() - chain.commit_at_.at()) << "s->..."; + } else { + sb << (chain.reveal_at_.at() - chain.commit_at_.at()) << "s->reveal->"; + if (chain.state_ == CallVerificationChain::State::Reveal) { + sb << (now.at() - chain.reveal_at_.at()) << "s->..."; + } else { + sb << (chain.done_at_.at() - chain.reveal_at_.at()) << "s->done"; + } + } + auto it = chain.users_.find(chain.user_id_); + if (it != chain.users_.end()) { + const CallVerificationChain::UserState &self = it->second; + sb << "\n\t\tself:"; + if (self.receive_commit_at_) { + sb << " commit=" << self.receive_commit_at_.at() - chain.commit_at_.at() << "s"; + } else { + sb << " commit=" << now.at() - chain.commit_at_.at() << "s..."; + } + if (chain.state_ != CallVerificationChain::State::Commit) { + if (self.receive_reveal_at_) { + sb << " reveal=" << self.receive_reveal_at_.at() - chain.reveal_at_.at() << "s"; + } else { + sb << " reveal=" << now.at() - chain.reveal_at_.at() << "s..."; + } + } + } + + { + sb << "\n\t\t"; + sb << "commit ="; + auto users = td::transform(chain.users_, [&](auto &key) { + auto t = chain.users_.at(key.first).receive_commit_at_; + if (t) { + return std::make_tuple(-(t.at() - chain.commit_at_.at()), key.first, false); + } + return std::make_tuple(-(now.at() - chain.commit_at_.at()), key.first, true); + }); + std::sort(users.begin(), users.end()); + for (auto &user : users) { + sb << " " << std::get<1>(user) << ":" << -std::get<0>(user) << "s"; + if (std::get<2>(user)) { + sb << "..."; + } + } + } + if (chain.state_ != CallVerificationChain::State::Commit) { + sb << "\n\t\t"; + sb << "reveal ="; + auto users = td::transform(chain.users_, [&](auto &key) { + auto t = chain.users_.at(key.first).receive_reveal_at_; + if (t) { + return std::make_tuple(-(t.at() - chain.reveal_at_.at()), key.first, false); + } + return std::make_tuple(-(now.at() - chain.reveal_at_.at()), key.first, true); + }); + std::sort(users.begin(), users.end()); + for (auto &user : users) { + sb << " " << std::get<1>(user) << ":" << -std::get<0>(user) << "s"; + if (std::get<2>(user)) { + sb << "..."; + } + } + } + + return sb; +} + +td::StringBuilder &operator<<(td::StringBuilder &sb, const CallVerification &verification) { + return sb << verification.chain_; +} + +td::StringBuilder &operator<<(td::StringBuilder &sb, const Call &call) { + auto status = call.get_status(); + sb << "Call{" << call.blockchain_.get_height() << ":" << call.private_key_.to_public_key() << "}"; + if (status.is_error()) { + sb << "\nCALL_FAILED: " << call.status_; + } + auto group_state = call.blockchain_.get_group_state(); + sb << "\n\tusers=" << td::transform(group_state->participants, [](auto &p) { return p.user_id; }); + sb << "\n\tpkeys=" << td::transform(group_state->participants, [](auto &p) { return p.public_key; }); + sb << "\n\t" << call.call_verification_; + return sb; +} + +} // namespace tde2e_core |
