ZaStoGram_desktop/Telegram/SourceFiles/e2e_cloud/vault/cloud_vault.cpp
2026-08-02 11:49:34 +03:00

765 lines
22 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/vault/cloud_vault.h"
#include <openssl/crypto.h>
#include <openssl/evp.h>
#include <openssl/rand.h>
#include <algorithm>
#include <array>
#include <limits>
#include <set>
#include <utility>
namespace E2ECloud {
namespace {
inline constexpr auto kBlobMagic = std::array<std::uint8_t, 8>{
'T', 'D', 'E', '2', 'E', 'C', 'V', 'T',
};
inline constexpr auto kPlaintextMagic = std::array<std::uint8_t, 8>{
'T', 'D', 'E', '2', 'E', 'V', 'P', 'L',
};
inline constexpr auto kNonceSize = 12;
inline constexpr auto kTagSize = 16;
inline constexpr auto kMaximumWrappedKeySize = 4096;
inline constexpr auto kMaximumConversations = 4096;
inline constexpr auto kMaximumPlaintextSize = 2 * 1024 * 1024;
struct Reader {
const QByteArray &bytes;
int offset = 0;
};
void AppendUint16(QByteArray &result, std::uint16_t value) {
result.append(char(value >> 8));
result.append(char(value));
}
void AppendUint32(QByteArray &result, std::uint32_t value) {
result.append(char(value >> 24));
result.append(char(value >> 16));
result.append(char(value >> 8));
result.append(char(value));
}
void AppendUint64(QByteArray &result, std::uint64_t value) {
for (auto shift = 56; shift >= 0; shift -= 8) {
result.append(char(value >> shift));
}
}
template <typename Array>
void AppendArray(QByteArray &result, const Array &value) {
result.append(
reinterpret_cast<const char*>(value.data()),
int(value.size()));
}
void AppendBytes(QByteArray &result, const QByteArray &value) {
AppendUint32(result, std::uint32_t(value.size()));
result.append(value);
}
[[nodiscard]] bool ReadUint16(Reader &reader, std::uint16_t &value) {
if (reader.bytes.size() - reader.offset < 2) {
return false;
}
const auto data = reinterpret_cast<const std::uint8_t*>(
reader.bytes.constData() + reader.offset);
value = (std::uint16_t(data[0]) << 8) | std::uint16_t(data[1]);
reader.offset += 2;
return true;
}
[[nodiscard]] bool ReadUint32(Reader &reader, std::uint32_t &value) {
if (reader.bytes.size() - reader.offset < 4) {
return false;
}
const auto data = reinterpret_cast<const std::uint8_t*>(
reader.bytes.constData() + reader.offset);
value = (std::uint32_t(data[0]) << 24)
| (std::uint32_t(data[1]) << 16)
| (std::uint32_t(data[2]) << 8)
| std::uint32_t(data[3]);
reader.offset += 4;
return true;
}
[[nodiscard]] bool ReadUint64(Reader &reader, std::uint64_t &value) {
if (reader.bytes.size() - reader.offset < 8) {
return false;
}
value = 0;
for (auto i = 0; i != 8; ++i) {
value = (value << 8)
| std::uint8_t(reader.bytes[reader.offset + i]);
}
reader.offset += 8;
return true;
}
template <typename Array>
[[nodiscard]] bool ReadArray(Reader &reader, Array &value) {
const auto size = int(value.size());
if (reader.bytes.size() - reader.offset < size) {
return false;
}
std::copy_n(
reinterpret_cast<const std::uint8_t*>(
reader.bytes.constData() + reader.offset),
size,
value.data());
reader.offset += size;
return true;
}
[[nodiscard]] bool ReadBytes(
Reader &reader,
int maximumSize,
QByteArray &value) {
auto size = std::uint32_t();
if (!ReadUint32(reader, size)
|| size > std::uint32_t(maximumSize)
|| reader.bytes.size() - reader.offset < int(size)) {
return false;
}
value = QByteArray(reader.bytes.constData() + reader.offset, int(size));
reader.offset += int(size);
return true;
}
void Cleanse(QByteArray &bytes) {
if (!bytes.isEmpty()) {
OPENSSL_cleanse(bytes.data(), bytes.size());
}
bytes.clear();
}
template <typename Value>
void Cleanse(Value &value) {
OPENSSL_cleanse(value.data(), value.size());
}
[[nodiscard]] bool ValidConversations(
const std::vector<CloudVaultConversation> &conversations) {
if (conversations.size() > kMaximumConversations) {
return false;
}
auto telegramPeerBindings = std::set<std::uint64_t>();
for (auto i = std::size_t(0); i != conversations.size(); ++i) {
const auto &entry = conversations[i];
if (!entry.conversationId
|| !entry.telegramPeerIdBinding
|| !telegramPeerBindings.emplace(
entry.telegramPeerIdBinding).second
|| entry.checkpoint.conversationId != entry.conversationId
|| !entry.checkpoint.generation
|| !entry.checkpoint.stateHash
|| !entry.ownerAccountId
|| (i && !(conversations[i - 1].conversationId
< entry.conversationId))) {
return false;
}
}
return true;
}
[[nodiscard]] std::optional<QByteArray> EncodePlaintext(
std::uint64_t telegramUserIdBinding,
std::uint64_t generation,
Digest previousBlobDigest,
const AccountPrivateIdentity &identity,
const std::vector<CloudVaultConversation> &conversations) {
if (!telegramUserIdBinding
|| !generation
|| (generation == 1) != !previousBlobDigest
|| !ValidateAccountPrivateIdentity(identity)
|| !ValidConversations(conversations)) {
return std::nullopt;
}
const auto credential = AccountCredentialCodecV1().encode(
identity.credential);
if (!credential) {
return std::nullopt;
}
auto result = QByteArray();
AppendArray(result, kPlaintextMagic);
AppendUint16(result, 1);
AppendUint64(result, telegramUserIdBinding);
AppendUint64(result, generation);
AppendArray(result, previousBlobDigest.bytes);
AppendArray(result, identity.signingPrivateKey.bytes());
AppendArray(result, identity.archiveHpkePrivateKey.bytes());
result.append(*credential);
AppendUint32(result, std::uint32_t(conversations.size()));
for (const auto &entry : conversations) {
AppendArray(result, entry.conversationId.bytes);
AppendUint64(result, entry.telegramPeerIdBinding);
AppendUint64(result, entry.checkpoint.generation);
AppendArray(result, entry.checkpoint.stateHash.bytes);
AppendArray(result, entry.ownerAccountId.bytes);
}
const auto signature = SignAccountData(
identity.signingPrivateKey,
AccountSignatureDomain::VaultCheckpoint,
result);
if (!signature
|| result.size() + int(signature->size()) > kMaximumPlaintextSize) {
Cleanse(result);
return std::nullopt;
}
AppendArray(result, *signature);
return result;
}
struct DecodedPlaintext {
std::uint64_t telegramUserIdBinding = 0;
std::uint64_t generation = 0;
Digest previousBlobDigest;
AccountPrivateIdentity identity;
std::vector<CloudVaultConversation> conversations;
};
[[nodiscard]] std::optional<DecodedPlaintext> DecodePlaintext(
const QByteArray &plaintext) {
if (plaintext.size() < 8 + 2 + 8 + 8 + 32 + 32 + 32
+ kAccountCredentialEncodedSize + 4 + 64
|| plaintext.size() > kMaximumPlaintextSize) {
return std::nullopt;
}
auto reader = Reader{ plaintext };
auto magic = std::array<std::uint8_t, 8>();
auto version = std::uint16_t();
auto telegramUserIdBinding = std::uint64_t();
auto generation = std::uint64_t();
auto previousBlobDigest = Digest();
auto signingPrivate = std::array<std::uint8_t, 32>();
auto archivePrivate = std::array<std::uint8_t, 32>();
auto credentialBytes = QByteArray();
auto count = std::uint32_t();
if (!ReadArray(reader, magic)
|| !ReadUint16(reader, version)
|| !ReadUint64(reader, telegramUserIdBinding)
|| !ReadUint64(reader, generation)
|| !ReadArray(reader, previousBlobDigest.bytes)
|| !ReadArray(reader, signingPrivate)
|| !ReadArray(reader, archivePrivate)
|| reader.bytes.size() - reader.offset
< kAccountCredentialEncodedSize) {
Cleanse(signingPrivate);
Cleanse(archivePrivate);
return std::nullopt;
}
credentialBytes = QByteArray(
reader.bytes.constData() + reader.offset,
kAccountCredentialEncodedSize);
reader.offset += kAccountCredentialEncodedSize;
if (!ReadUint32(reader, count)
|| count > kMaximumConversations) {
Cleanse(signingPrivate);
Cleanse(archivePrivate);
return std::nullopt;
}
auto conversations = std::vector<CloudVaultConversation>();
conversations.reserve(count);
for (auto i = std::uint32_t(0); i != count; ++i) {
auto entry = CloudVaultConversation();
if (!ReadArray(reader, entry.conversationId.bytes)
|| !ReadUint64(reader, entry.telegramPeerIdBinding)
|| !ReadUint64(reader, entry.checkpoint.generation)
|| !ReadArray(reader, entry.checkpoint.stateHash.bytes)
|| !ReadArray(reader, entry.ownerAccountId.bytes)) {
Cleanse(signingPrivate);
Cleanse(archivePrivate);
return std::nullopt;
}
entry.checkpoint.conversationId = entry.conversationId;
conversations.push_back(entry);
}
auto signature = AccountSignature();
if (!ReadArray(reader, signature)
|| reader.offset != plaintext.size()) {
Cleanse(signingPrivate);
Cleanse(archivePrivate);
return std::nullopt;
}
const auto credential = AccountCredentialCodecV1().decode(credentialBytes);
auto signedBytes = QByteArray(
plaintext.constData(),
plaintext.size() - int(signature.size()));
auto identity = AccountPrivateIdentity{
.signingPrivateKey = SecureKey32(std::move(signingPrivate)),
.archiveHpkePrivateKey = SecureKey32(std::move(archivePrivate)),
.credential = credential.value_or(AccountCredentialPublic()),
};
const auto valid = magic == kPlaintextMagic
&& version == 1
&& telegramUserIdBinding
&& generation
&& (generation == 1) == !previousBlobDigest
&& credential
&& ValidConversations(conversations)
&& ValidateAccountPrivateIdentity(identity)
&& VerifyAccountSignature(
*credential,
AccountSignatureDomain::VaultCheckpoint,
signedBytes,
signature);
Cleanse(signedBytes);
if (!valid) {
return std::nullopt;
}
return DecodedPlaintext{
.telegramUserIdBinding = telegramUserIdBinding,
.generation = generation,
.previousBlobDigest = previousBlobDigest,
.identity = std::move(identity),
.conversations = std::move(conversations),
};
}
[[nodiscard]] std::optional<QByteArray> Encrypt(
const SecureKey32 &key,
const QByteArray &wrappedMasterKey,
std::uint64_t telegramUserIdBinding,
std::uint64_t generation,
const QByteArray &plaintext) {
if (!key.valid()
|| wrappedMasterKey.isEmpty()
|| wrappedMasterKey.size() > kMaximumWrappedKeySize
|| plaintext.isEmpty()
|| plaintext.size() > kMaximumPlaintextSize) {
return std::nullopt;
}
auto nonce = std::array<std::uint8_t, kNonceSize>();
if (RAND_bytes(nonce.data(), nonce.size()) != 1) {
return std::nullopt;
}
auto result = QByteArray();
AppendArray(result, kBlobMagic);
AppendUint16(result, 1);
AppendUint64(result, telegramUserIdBinding);
AppendUint64(result, generation);
AppendBytes(result, wrappedMasterKey);
AppendArray(result, nonce);
AppendUint32(result, std::uint32_t(plaintext.size()));
const auto headerSize = result.size();
result.resize(headerSize + plaintext.size() + kTagSize);
const auto context = EVP_CIPHER_CTX_new();
if (!context) {
return std::nullopt;
}
auto outputLength = 0;
auto totalLength = 0;
auto ok = EVP_EncryptInit_ex(
context,
EVP_aes_256_gcm(),
nullptr,
nullptr,
nullptr) == 1
&& EVP_CIPHER_CTX_ctrl(
context,
EVP_CTRL_GCM_SET_IVLEN,
nonce.size(),
nullptr) == 1
&& EVP_EncryptInit_ex(
context,
nullptr,
nullptr,
key.bytes().data(),
nonce.data()) == 1
&& EVP_EncryptUpdate(
context,
nullptr,
&outputLength,
reinterpret_cast<const unsigned char*>(result.constData()),
headerSize) == 1
&& EVP_EncryptUpdate(
context,
reinterpret_cast<unsigned char*>(result.data() + headerSize),
&outputLength,
reinterpret_cast<const unsigned char*>(plaintext.constData()),
plaintext.size()) == 1;
totalLength = outputLength;
ok = ok
&& EVP_EncryptFinal_ex(
context,
reinterpret_cast<unsigned char*>(
result.data() + headerSize + totalLength),
&outputLength) == 1
&& totalLength + outputLength == plaintext.size()
&& EVP_CIPHER_CTX_ctrl(
context,
EVP_CTRL_GCM_GET_TAG,
kTagSize,
result.data() + headerSize + plaintext.size()) == 1;
EVP_CIPHER_CTX_free(context);
if (!ok) {
Cleanse(result);
return std::nullopt;
}
return result;
}
struct BlobParts {
std::uint64_t telegramUserIdBinding = 0;
std::uint64_t generation = 0;
QByteArray wrappedMasterKey;
std::array<std::uint8_t, kNonceSize> nonce = {};
int headerSize = 0;
int ciphertextSize = 0;
};
[[nodiscard]] std::optional<BlobParts> ParseBlob(
const QByteArray &encoded) {
auto reader = Reader{ encoded };
auto magic = std::array<std::uint8_t, 8>();
auto version = std::uint16_t();
auto result = BlobParts();
auto ciphertextSize = std::uint32_t();
if (!ReadArray(reader, magic)
|| !ReadUint16(reader, version)
|| !ReadUint64(reader, result.telegramUserIdBinding)
|| !ReadUint64(reader, result.generation)
|| !ReadBytes(
reader,
kMaximumWrappedKeySize,
result.wrappedMasterKey)
|| !ReadArray(reader, result.nonce)
|| !ReadUint32(reader, ciphertextSize)
|| magic != kBlobMagic
|| version != 1
|| !result.telegramUserIdBinding
|| !result.generation
|| result.wrappedMasterKey.isEmpty()
|| !ciphertextSize
|| ciphertextSize > kMaximumPlaintextSize
|| encoded.size() - reader.offset
!= int(ciphertextSize) + kTagSize) {
return std::nullopt;
}
result.headerSize = reader.offset;
result.ciphertextSize = int(ciphertextSize);
return result;
}
[[nodiscard]] std::optional<QByteArray> Decrypt(
const QByteArray &encoded,
const BlobParts &parts,
const VaultMasterKey &masterKey) {
auto plaintext = QByteArray();
plaintext.resize(parts.ciphertextSize);
const auto context = EVP_CIPHER_CTX_new();
if (!context) {
return std::nullopt;
}
auto outputLength = 0;
auto totalLength = 0;
auto ok = EVP_DecryptInit_ex(
context,
EVP_aes_256_gcm(),
nullptr,
nullptr,
nullptr) == 1
&& EVP_CIPHER_CTX_ctrl(
context,
EVP_CTRL_GCM_SET_IVLEN,
parts.nonce.size(),
nullptr) == 1
&& EVP_DecryptInit_ex(
context,
nullptr,
nullptr,
masterKey.data(),
parts.nonce.data()) == 1
&& EVP_DecryptUpdate(
context,
nullptr,
&outputLength,
reinterpret_cast<const unsigned char*>(encoded.constData()),
parts.headerSize) == 1
&& EVP_DecryptUpdate(
context,
reinterpret_cast<unsigned char*>(plaintext.data()),
&outputLength,
reinterpret_cast<const unsigned char*>(
encoded.constData() + parts.headerSize),
parts.ciphertextSize) == 1;
totalLength = outputLength;
ok = ok
&& EVP_CIPHER_CTX_ctrl(
context,
EVP_CTRL_GCM_SET_TAG,
kTagSize,
const_cast<char*>(encoded.constData())
+ parts.headerSize + parts.ciphertextSize) == 1
&& EVP_DecryptFinal_ex(
context,
reinterpret_cast<unsigned char*>(plaintext.data()) + totalLength,
&outputLength) == 1
&& totalLength + outputLength == parts.ciphertextSize;
EVP_CIPHER_CTX_free(context);
if (!ok) {
Cleanse(plaintext);
return std::nullopt;
}
return plaintext;
}
[[nodiscard]] std::optional<UnlockedCloudVault> DecodeUnlockedVault(
const QByteArray &encoded,
BlobParts parts,
SecureKey32 &&masterKey,
std::uint64_t expectedTelegramUserIdBinding,
const Sha256Provider &sha256) {
if (!masterKey.valid()
|| parts.telegramUserIdBinding != expectedTelegramUserIdBinding) {
return std::nullopt;
}
auto plaintext = Decrypt(encoded, parts, masterKey.bytes());
if (!plaintext) {
return std::nullopt;
}
auto decoded = DecodePlaintext(*plaintext);
Cleanse(*plaintext);
const auto blobDigest = sha256.digest(encoded);
if (!decoded
|| !blobDigest
|| decoded->telegramUserIdBinding != parts.telegramUserIdBinding
|| decoded->generation != parts.generation) {
return std::nullopt;
}
return UnlockedCloudVault{
.telegramUserIdBinding = decoded->telegramUserIdBinding,
.generation = decoded->generation,
.previousBlobDigest = decoded->previousBlobDigest,
.blobDigest = blobDigest,
.masterKey = std::move(masterKey),
.wrappedMasterKey = std::move(parts.wrappedMasterKey),
.identity = std::move(decoded->identity),
.conversations = std::move(decoded->conversations),
};
}
[[nodiscard]] std::vector<CloudVaultConversation> SortedConversations(
std::vector<CloudVaultConversation> conversations) {
std::sort(
std::begin(conversations),
std::end(conversations),
[](const auto &a, const auto &b) {
return a.conversationId < b.conversationId;
});
return conversations;
}
} // namespace
CloudVaultCodecV1::CloudVaultCodecV1(
const PasswordKdf &passwordKdf,
const Sha256Provider &sha256)
: _passwordVault(passwordKdf)
, _sha256(sha256) {
}
std::optional<CreatedCloudVault> CloudVaultCodecV1::create(
std::uint64_t telegramUserIdBinding,
AccountPrivateIdentity &&identity,
QByteArray password,
Argon2idConfig config,
std::vector<CloudVaultConversation> conversations) const {
auto rawMasterKey = VaultMasterKey();
if (!telegramUserIdBinding
|| !ValidateAccountPrivateIdentity(identity)
|| RAND_bytes(rawMasterKey.data(), rawMasterKey.size()) != 1) {
Cleanse(password);
Cleanse(rawMasterKey);
return std::nullopt;
}
auto secureMasterKeyBytes = rawMasterKey;
auto wrapped = _passwordVault.wrap(
std::move(rawMasterKey),
std::move(password),
config,
1);
if (!wrapped) {
Cleanse(secureMasterKeyBytes);
return std::nullopt;
}
auto masterKey = SecureKey32(std::move(secureMasterKeyBytes));
conversations = SortedConversations(std::move(conversations));
const auto encoded = encode(
telegramUserIdBinding,
1,
{},
masterKey,
*wrapped,
identity,
conversations);
const auto blobDigest = encoded ? _sha256.digest(*encoded) : Digest();
if (!encoded || !blobDigest) {
return std::nullopt;
}
return CreatedCloudVault{
.encoded = *encoded,
.unlocked = {
.telegramUserIdBinding = telegramUserIdBinding,
.generation = 1,
.previousBlobDigest = {},
.blobDigest = blobDigest,
.masterKey = std::move(masterKey),
.wrappedMasterKey = *wrapped,
.identity = std::move(identity),
.conversations = std::move(conversations),
},
};
}
std::optional<UnlockedCloudVault> CloudVaultCodecV1::unlock(
const QByteArray &encoded,
QByteArray password,
std::uint64_t expectedTelegramUserIdBinding) const {
const auto parts = ParseBlob(encoded);
if (!parts
|| parts->telegramUserIdBinding != expectedTelegramUserIdBinding) {
Cleanse(password);
return std::nullopt;
}
auto unwrapped = _passwordVault.unwrap(
parts->wrappedMasterKey,
std::move(password));
if (!unwrapped) {
return std::nullopt;
}
auto secureMasterKey = SecureKey32(std::move(unwrapped->masterKey));
return DecodeUnlockedVault(
encoded,
std::move(*parts),
std::move(secureMasterKey),
expectedTelegramUserIdBinding,
_sha256);
}
std::optional<CloudVaultBlobHeader> CloudVaultCodecV1::inspect(
const QByteArray &encoded) const {
const auto parts = ParseBlob(encoded);
return parts
? std::optional<CloudVaultBlobHeader>({
.telegramUserIdBinding = parts->telegramUserIdBinding,
.generation = parts->generation,
.wrappedMasterKey = parts->wrappedMasterKey,
})
: std::nullopt;
}
std::optional<SecureKey32> CloudVaultCodecV1::unlockMasterKey(
const QByteArray &wrappedMasterKey,
QByteArray password) const {
auto unwrapped = _passwordVault.unwrap(
wrappedMasterKey,
std::move(password));
return unwrapped
? std::optional<SecureKey32>(
SecureKey32(std::move(unwrapped->masterKey)))
: std::nullopt;
}
std::optional<UnlockedCloudVault> CloudVaultCodecV1::unlockWithMasterKey(
const QByteArray &encoded,
const SecureKey32 &masterKey,
std::uint64_t expectedTelegramUserIdBinding) const {
auto parts = ParseBlob(encoded);
if (!parts) {
return std::nullopt;
}
auto clonedBytes = masterKey.bytes();
auto clonedKey = SecureKey32(std::move(clonedBytes));
return DecodeUnlockedVault(
encoded,
std::move(*parts),
std::move(clonedKey),
expectedTelegramUserIdBinding,
_sha256);
}
std::optional<PreparedCloudVaultUpdate> CloudVaultCodecV1::prepareUpdate(
const UnlockedCloudVault &vault,
std::vector<CloudVaultConversation> conversations) const {
if (vault.generation == std::numeric_limits<std::uint64_t>::max()
|| !vault.blobDigest
|| !vault.masterKey.valid()) {
return std::nullopt;
}
conversations = SortedConversations(std::move(conversations));
const auto generation = vault.generation + 1;
const auto encoded = encode(
vault.telegramUserIdBinding,
generation,
vault.blobDigest,
vault.masterKey,
vault.wrappedMasterKey,
vault.identity,
conversations);
const auto blobDigest = encoded ? _sha256.digest(*encoded) : Digest();
return (encoded && blobDigest)
? std::optional<PreparedCloudVaultUpdate>({
.encoded = *encoded,
.generation = generation,
.previousBlobDigest = vault.blobDigest,
.blobDigest = blobDigest,
.conversations = std::move(conversations),
})
: std::nullopt;
}
bool CloudVaultCodecV1::applyPublished(
UnlockedCloudVault &vault,
PreparedCloudVaultUpdate &&update) const {
if (update.generation != vault.generation + 1
|| update.previousBlobDigest != vault.blobDigest
|| !update.blobDigest
|| update.blobDigest != _sha256.digest(update.encoded)
|| !ValidConversations(update.conversations)) {
return false;
}
vault.generation = update.generation;
vault.previousBlobDigest = update.previousBlobDigest;
vault.blobDigest = update.blobDigest;
vault.conversations = std::move(update.conversations);
return true;
}
std::optional<QByteArray> CloudVaultCodecV1::encode(
std::uint64_t telegramUserIdBinding,
std::uint64_t generation,
Digest previousBlobDigest,
const SecureKey32 &masterKey,
const QByteArray &wrappedMasterKey,
const AccountPrivateIdentity &identity,
const std::vector<CloudVaultConversation> &conversations) const {
auto plaintext = EncodePlaintext(
telegramUserIdBinding,
generation,
previousBlobDigest,
identity,
conversations);
if (!plaintext) {
return std::nullopt;
}
auto result = Encrypt(
masterKey,
wrappedMasterKey,
telegramUserIdBinding,
generation,
*plaintext);
Cleanse(*plaintext);
return result;
}
} // namespace E2ECloud