ZaStoGram_desktop/Telegram/SourceFiles/tests/test_e2e_cloud_vault.cpp
2026-08-02 14:18:37 +03:00

1017 lines
30 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/transport/cloud_vault_sync_controller.h"
#include "e2e_cloud/vault/argon2id_password_kdf.h"
#include "e2e_cloud/vault/cloud_vault.h"
#include "e2e_cloud/vault/cloud_vault_selection.h"
#include "e2e_cloud/vault/password_kdf.h"
#include "e2e_cloud/vault/password_vault.h"
#include "e2e_cloud/vault/persistent_cloud_vault_anchor.h"
#include <algorithm>
#include <array>
#include <cstdio>
#include <cstdint>
#include <memory>
#include <optional>
#include <utility>
#include <vector>
namespace {
using namespace E2ECloud;
template <typename Id>
[[nodiscard]] Id FilledId(std::uint8_t value) {
auto result = Id();
result.bytes.fill(value);
return result;
}
[[nodiscard]] int Fail(const char *message) {
std::fprintf(stderr, "%s\n", message);
return 1;
}
[[nodiscard]] Argon2idConfig MakeConfig() {
return {
.parameterVersion = 1,
.memoryKibibytes = 64 * 1024,
.iterations = 3,
.parallelism = 1,
};
}
[[nodiscard]] VaultMasterKey MakeMasterKey() {
auto result = VaultMasterKey();
for (auto i = std::size_t(0); i != result.size(); ++i) {
result[i] = std::uint8_t(i + 1);
}
return result;
}
class TestPasswordKdf final : public PasswordKdf {
public:
[[nodiscard]] std::optional<PasswordDerivedKey> deriveArgon2id(
const QByteArray &password,
const Argon2idParameters &parameters) const override {
++calls;
lastParameters = parameters;
if (fail || password.isEmpty()) {
return std::nullopt;
}
auto result = PasswordDerivedKey();
for (auto i = std::size_t(0); i != result.size(); ++i) {
result[i] = std::uint8_t(parameters.salt[i % 16]
+ std::uint8_t(password.constData()[i % password.size()])
+ i);
}
return result;
}
mutable int calls = 0;
mutable Argon2idParameters lastParameters;
bool fail = false;
};
class MemoryBlobStore final : public AtomicBlobStore {
public:
[[nodiscard]] BlobReadResult read() const override {
return error
? BlobReadResult{
.status = BlobReadStatus::Error,
.bytes = {},
}
: bytes
? BlobReadResult{
.status = BlobReadStatus::Found,
.bytes = *bytes,
}
: BlobReadResult{
.status = BlobReadStatus::Missing,
.bytes = {},
};
}
bool writeAtomic(const QByteArray &value) override {
if (failWrites) {
return false;
}
bytes = value;
return true;
}
std::optional<QByteArray> bytes;
bool error = false;
bool failWrites = false;
};
class TestCloudVaultRemote final : public CloudVaultRemote {
public:
void uploadExact(QByteArray, UploadCallback callback) override {
callback(Result::PermanentError);
}
void discover(DiscoveryCallback callback) override {
++discoveryCalls;
callback(discoveryResult, discoveryPresent);
}
void downloadPage(
QByteArray,
int,
DownloadCallback callback) override {
++downloadCalls;
if (asynchronousDownloads) {
downloadCallbacks.push_back(std::move(callback));
} else {
callback(downloadResult, std::move(downloadPageResult));
}
}
Result discoveryResult = Result::Accepted;
Result downloadResult = Result::PermanentError;
CarrierDownloadPage downloadPageResult;
std::vector<DownloadCallback> downloadCallbacks;
bool discoveryPresent = false;
bool asynchronousDownloads = false;
int discoveryCalls = 0;
int downloadCalls = 0;
};
struct CallbackLifetimeState {
bool insideCallback = false;
bool activeProbeDestroyed = false;
std::uint64_t lastProbeId = 0;
std::uint64_t activeProbeId = 0;
std::optional<CloudVaultSyncStatus> status;
};
class CallbackLifetimeProbe final {
public:
explicit CallbackLifetimeProbe(
std::shared_ptr<CallbackLifetimeState> state)
: _state(std::move(state))
, _id(_state ? ++_state->lastProbeId : 0) {
}
CallbackLifetimeProbe(const CallbackLifetimeProbe &other)
: _state(other._state)
, _id(_state ? ++_state->lastProbeId : 0) {
}
CallbackLifetimeProbe(CallbackLifetimeProbe &&other) noexcept = default;
CallbackLifetimeProbe &operator=(
const CallbackLifetimeProbe &) = delete;
CallbackLifetimeProbe &operator=(CallbackLifetimeProbe &&) = delete;
~CallbackLifetimeProbe() {
if (_state
&& _state->insideCallback
&& _state->activeProbeId == _id) {
_state->activeProbeDestroyed = true;
}
}
[[nodiscard]] std::shared_ptr<CallbackLifetimeState> state() const {
return _state;
}
[[nodiscard]] std::uint64_t id() const {
return _id;
}
private:
std::shared_ptr<CallbackLifetimeState> _state;
std::uint64_t _id = 0;
};
[[nodiscard]] int ScenarioVaultCompletionCanDestroyController() {
auto kdf = TestPasswordKdf();
auto sha256 = OpenSslSha256Provider();
auto codec = CloudVaultCodecV1(kdf, sha256);
auto selector = CloudVaultSelector(codec, sha256);
auto remote = TestCloudVaultRemote();
auto lifetime = std::make_shared<CallbackLifetimeState>();
auto controller = std::unique_ptr<CloudVaultSyncController>();
controller = std::make_unique<CloudVaultSyncController>(
777,
remote,
selector,
[&, probe = CallbackLifetimeProbe(lifetime)](
CloudVaultSyncCompletion result) {
const auto state = probe.state();
state->status = result.status;
state->activeProbeId = probe.id();
state->insideCallback = true;
controller.reset();
state->insideCallback = false;
});
const auto started = controller->startDiscovery();
if (!started
|| controller
|| lifetime->status != CloudVaultSyncStatus::Missing
|| lifetime->activeProbeDestroyed) {
return Fail("vault completion was destroyed during its own callback");
}
return 0;
}
[[nodiscard]] int ScenarioVaultDiscoveryFindsMissingIdentity() {
auto kdf = TestPasswordKdf();
auto sha256 = OpenSslSha256Provider();
auto codec = CloudVaultCodecV1(kdf, sha256);
auto selector = CloudVaultSelector(codec, sha256);
auto remote = TestCloudVaultRemote();
auto completion = std::optional<CloudVaultSyncCompletion>();
auto controller = CloudVaultSyncController(
777,
remote,
selector,
[&](CloudVaultSyncCompletion result) {
completion = std::move(result);
});
if (!controller.startDiscovery()
|| controller.running()
|| !completion
|| completion->status != CloudVaultSyncStatus::Missing
|| completion->pages != 1
|| completion->candidates != 0
|| kdf.calls != 0
|| remote.discoveryCalls != 1) {
return Fail("vault discovery did not identify a missing identity");
}
return 0;
}
[[nodiscard]] int ScenarioVaultDiscoveryFindsExistingAccount() {
auto kdf = TestPasswordKdf();
auto sha256 = OpenSslSha256Provider();
auto codec = CloudVaultCodecV1(kdf, sha256);
auto selector = CloudVaultSelector(codec, sha256);
auto remote = TestCloudVaultRemote();
remote.discoveryPresent = true;
auto completion = std::optional<CloudVaultSyncCompletion>();
auto controller = CloudVaultSyncController(
777,
remote,
selector,
[&](CloudVaultSyncCompletion result) {
completion = std::move(result);
});
if (!controller.startDiscovery()
|| controller.running()
|| !completion
|| completion->status != CloudVaultSyncStatus::Present
|| completion->pages != 1
|| completion->candidates != 1
|| kdf.calls != 0
|| remote.discoveryCalls != 1) {
return Fail("vault discovery requested a password before detection");
}
return 0;
}
[[nodiscard]] int ScenarioVaultSelectionCanCompleteAsynchronously() {
auto kdf = TestPasswordKdf();
auto sha256 = OpenSslSha256Provider();
auto codec = CloudVaultCodecV1(kdf, sha256);
auto selector = CloudVaultSelector(codec, sha256);
auto remote = TestCloudVaultRemote();
remote.downloadResult = CloudVaultRemote::Result::Accepted;
remote.downloadPageResult.complete = true;
auto completion = std::optional<CloudVaultSyncCompletion>();
auto deferred = CloudVaultSyncController::SelectionCompletion();
auto validArguments = false;
auto controller = CloudVaultSyncController(
777,
remote,
selector,
[&](CloudVaultSyncCompletion result) {
completion = std::move(result);
},
[&](
std::vector<QByteArray> candidates,
QByteArray password,
std::uint64_t telegramUserIdBinding,
std::optional<CloudVaultAnchor> localAnchor,
CloudVaultSyncController::SelectionCompletion callback) {
validArguments = candidates.empty()
&& password == QByteArray("password")
&& telegramUserIdBinding == 777
&& !localAnchor;
deferred = std::move(callback);
});
if (!controller.start(QByteArray("password"))
|| !controller.running()
|| completion
|| !deferred
|| !validArguments
|| kdf.calls
|| remote.downloadCalls != 1) {
return Fail("vault selection did not leave expensive work deferred");
}
deferred({
.status = CloudVaultSelectionStatus::Missing,
.vault = std::nullopt,
});
if (controller.running()
|| !completion
|| completion->status != CloudVaultSyncStatus::Missing
|| completion->pages != 1
|| completion->candidates != 0) {
return Fail("deferred vault selection did not finish cleanly");
}
return 0;
}
[[nodiscard]] int ScenarioVaultRestartIgnoresPreviousDownload() {
auto kdf = TestPasswordKdf();
auto sha256 = OpenSslSha256Provider();
auto codec = CloudVaultCodecV1(kdf, sha256);
auto selector = CloudVaultSelector(codec, sha256);
auto remote = TestCloudVaultRemote();
remote.asynchronousDownloads = true;
auto statuses = std::vector<CloudVaultSyncStatus>();
auto controller = CloudVaultSyncController(
777,
remote,
selector,
[&](CloudVaultSyncCompletion result) {
statuses.push_back(result.status);
});
if (!controller.start(QByteArray("first password"))
|| remote.downloadCallbacks.size() != 1) {
return Fail("vault restart fixture did not start");
}
controller.cancel();
if (!controller.start(QByteArray("second password"))
|| remote.downloadCallbacks.size() != 2) {
return Fail("vault controller could not restart after cancellation");
}
remote.downloadCallbacks[0](
CloudVaultRemote::Result::Accepted,
{ .untrustedObjects = {}, .nextCursor = {}, .complete = true });
if (!controller.running()
|| statuses != std::vector{ CloudVaultSyncStatus::Cancelled }
|| kdf.calls) {
return Fail("previous vault run completed the replacement run");
}
remote.downloadCallbacks[1](
CloudVaultRemote::Result::Accepted,
{ .untrustedObjects = {}, .nextCursor = {}, .complete = true });
if (controller.running()
|| statuses != std::vector{
CloudVaultSyncStatus::Cancelled,
CloudVaultSyncStatus::Missing }) {
return Fail("replacement vault run did not own its download callback");
}
return 0;
}
[[nodiscard]] int ScenarioVaultRoundTrip() {
auto kdf = TestPasswordKdf();
const auto vault = PasswordVault(kdf);
const auto expected = MakeMasterKey();
auto input = expected;
const auto wrapped = vault.wrap(
std::move(input),
QByteArray("correct horse battery staple"),
MakeConfig(),
9);
if (!wrapped
|| wrapped->size() != 114
|| input != VaultMasterKey()
|| kdf.calls != 1) {
return Fail("vault key was not wrapped with a consumed master key");
}
const auto opened = vault.unwrap(
*wrapped,
QByteArray("correct horse battery staple"));
if (!opened
|| opened->masterKey != expected
|| opened->generation != 9
|| opened->parameters != kdf.lastParameters
|| opened->parameters.memoryKibibytes
!= MakeConfig().memoryKibibytes
|| opened->parameters.iterations != MakeConfig().iterations
|| opened->parameters.parallelism != MakeConfig().parallelism) {
return Fail("vault key did not survive an authenticated round trip");
}
return 0;
}
[[nodiscard]] int ScenarioVaultRejectsWrongPasswordAndTampering() {
auto kdf = TestPasswordKdf();
const auto vault = PasswordVault(kdf);
auto key = MakeMasterKey();
const auto wrapped = vault.wrap(
std::move(key),
QByteArray("password one"),
MakeConfig(),
4);
if (!wrapped
|| vault.unwrap(*wrapped, QByteArray("password two"))) {
return Fail("vault accepted the wrong password");
}
for (const auto offset : { 0, 14, 42, 54, 66, 113 }) {
auto tampered = *wrapped;
tampered[offset] = char(std::uint8_t(tampered[offset]) ^ 1);
if (vault.unwrap(tampered, QByteArray("password one"))) {
return Fail("vault accepted tampered parameters or ciphertext");
}
}
return 0;
}
[[nodiscard]] int ScenarioVaultUsesFreshNonce() {
auto kdf = TestPasswordKdf();
const auto vault = PasswordVault(kdf);
auto firstKey = MakeMasterKey();
auto secondKey = MakeMasterKey();
const auto first = vault.wrap(
std::move(firstKey),
QByteArray("same password"),
MakeConfig(),
1);
const auto second = vault.wrap(
std::move(secondKey),
QByteArray("same password"),
MakeConfig(),
1);
const auto firstOpened = first
? vault.unwrap(*first, QByteArray("same password"))
: std::nullopt;
const auto secondOpened = second
? vault.unwrap(*second, QByteArray("same password"))
: std::nullopt;
if (!first
|| !second
|| first == second
|| !firstOpened
|| !secondOpened
|| firstOpened->parameters.salt == secondOpened->parameters.salt) {
return Fail("vault wrapping reused deterministic ciphertext");
}
return 0;
}
[[nodiscard]] int ScenarioVaultBoundsKdfBeforeDerivation() {
auto kdf = TestPasswordKdf();
const auto vault = PasswordVault(kdf);
auto key = MakeMasterKey();
auto config = MakeConfig();
config.memoryKibibytes = 32 * 1024;
if (vault.wrap(
std::move(key),
QByteArray("password"),
config,
1)
|| kdf.calls) {
return Fail("vault created a record with weak password work factors");
}
return 0;
}
[[nodiscard]] int ScenarioVaultRejectsExcessiveKdfBeforeDerivation() {
auto kdf = TestPasswordKdf();
const auto vault = PasswordVault(kdf);
auto key = MakeMasterKey();
const auto wrapped = vault.wrap(
std::move(key),
QByteArray("password"),
MakeConfig(),
1);
if (!wrapped) {
return Fail("vault KDF limit test setup failed");
}
auto excessive = *wrapped;
excessive[14] = char(0x00);
excessive[15] = char(0x10);
excessive[16] = char(0x00);
excessive[17] = char(0x00);
const auto callsBefore = kdf.calls;
if (vault.unwrap(excessive, QByteArray("password"))
|| kdf.calls != callsBefore) {
return Fail("vault performed an attacker-controlled excessive KDF");
}
return 0;
}
[[nodiscard]] int ScenarioArgon2idReferenceVector() {
auto parameters = Argon2idParameters{
.parameterVersion = 1,
.memoryKibibytes = 256,
.iterations = 2,
.parallelism = 1,
.salt = {},
};
const auto salt = QByteArray("somesalt12345678");
std::copy_n(
reinterpret_cast<const std::uint8_t*>(salt.constData()),
parameters.salt.size(),
parameters.salt.begin());
const auto result = Argon2idPasswordKdf().deriveArgon2id(
QByteArray("password"),
parameters);
const auto expected = PasswordDerivedKey{
0x81, 0x10, 0xe1, 0x16, 0x5e, 0xb0, 0xe1, 0x11,
0x4e, 0xe3, 0x7d, 0x5f, 0xf0, 0x17, 0x57, 0x3b,
0xa0, 0x08, 0x4b, 0x83, 0x66, 0xb4, 0x10, 0x8d,
0xb4, 0x47, 0x49, 0x95, 0x4b, 0x8d, 0x98, 0x71,
};
if (!result || *result != expected) {
return Fail("Argon2id provider did not match the reference vector");
}
return 0;
}
[[nodiscard]] int ScenarioCloudVaultRoundTripAndUpdate() {
auto kdf = TestPasswordKdf();
auto sha256 = OpenSslSha256Provider();
auto codec = CloudVaultCodecV1(kdf, sha256);
auto identity = GenerateAccountPrivateIdentity();
if (!identity) {
return Fail("account identity generation failed for cloud vault");
}
const auto expectedAccountId = DeriveAccountId(
identity->credential,
sha256);
auto created = codec.create(
777,
std::move(*identity),
QByteArray("correct horse battery staple"),
MakeConfig());
if (!created
|| created->unlocked.generation != 1
|| created->unlocked.previousBlobDigest
|| !created->unlocked.blobDigest
|| !created->unlocked.conversations.empty()) {
return Fail("initial cloud vault was not created");
}
auto unlocked = codec.unlock(
created->encoded,
QByteArray("correct horse battery staple"),
777);
const auto accountId = unlocked
? DeriveAccountId(unlocked->identity.credential, sha256)
: std::nullopt;
if (!unlocked
|| accountId != expectedAccountId
|| unlocked->blobDigest != created->unlocked.blobDigest
|| codec.unlock(
created->encoded,
QByteArray("wrong password"),
777)
|| codec.unlock(
created->encoded,
QByteArray("correct horse battery staple"),
778)) {
return Fail("cloud vault unlock did not enforce password and account");
}
const auto conversation = CloudVaultConversation{
.conversationId = FilledId<ConversationId>(3),
.telegramPeerIdBinding = 9001,
.checkpoint = {
.conversationId = FilledId<ConversationId>(3),
.generation = 8,
.stateHash = FilledId<Digest>(4),
},
.ownerAccountId = FilledId<AccountId>(5),
};
auto update = codec.prepareUpdate(*unlocked, { conversation });
if (!update
|| update->generation != 2
|| update->previousBlobDigest != unlocked->blobDigest
|| update->blobDigest == unlocked->blobDigest) {
return Fail("cloud vault update did not extend its digest chain");
}
const auto updateBytes = update->encoded;
if (!codec.applyPublished(*unlocked, std::move(*update))
|| unlocked->generation != 2
|| unlocked->conversations != std::vector{ conversation }) {
return Fail("published cloud vault update was not adopted");
}
auto reopened = codec.unlock(
updateBytes,
QByteArray("correct horse battery staple"),
777);
if (!reopened
|| reopened->generation != 2
|| reopened->previousBlobDigest != created->unlocked.blobDigest
|| reopened->conversations != std::vector{ conversation }) {
return Fail("updated cloud vault did not survive another installation");
}
return 0;
}
[[nodiscard]] int ScenarioCloudVaultRejectsTamperingAndKeyMismatch() {
auto kdf = TestPasswordKdf();
auto sha256 = OpenSslSha256Provider();
auto codec = CloudVaultCodecV1(kdf, sha256);
auto identity = GenerateAccountPrivateIdentity();
if (!identity) {
return Fail("account identity generation failed for tamper test");
}
identity->credential.signingPublicKey[0] ^= 1;
if (codec.create(
777,
std::move(*identity),
QByteArray("correct horse battery staple"),
MakeConfig())) {
return Fail("cloud vault accepted mismatched private identity keys");
}
auto validIdentity = GenerateAccountPrivateIdentity();
auto created = validIdentity
? codec.create(
777,
std::move(*validIdentity),
QByteArray("correct horse battery staple"),
MakeConfig())
: std::nullopt;
if (!created) {
return Fail("cloud vault tamper fixture could not be created");
}
for (const auto offset : std::array{
qsizetype(0),
qsizetype(20),
created->encoded.size() / 2,
created->encoded.size() - 1,
}) {
auto tampered = created->encoded;
tampered[offset] = char(std::uint8_t(tampered[offset]) ^ 1);
if (codec.unlock(
tampered,
QByteArray("correct horse battery staple"),
777)) {
return Fail("cloud vault accepted modified header or ciphertext");
}
}
return 0;
}
[[nodiscard]] int ScenarioCloudVaultRejectsDuplicatePeerBindings() {
auto kdf = TestPasswordKdf();
auto sha256 = OpenSslSha256Provider();
auto codec = CloudVaultCodecV1(kdf, sha256);
auto identity = GenerateAccountPrivateIdentity();
auto created = identity
? codec.create(
777,
std::move(*identity),
QByteArray("correct horse battery staple"),
MakeConfig())
: std::nullopt;
if (!created) {
return Fail("duplicate peer vault fixture could not be created");
}
const auto first = CloudVaultConversation{
.conversationId = FilledId<ConversationId>(3),
.telegramPeerIdBinding = 9001,
.checkpoint = {
.conversationId = FilledId<ConversationId>(3),
.generation = 1,
.stateHash = FilledId<Digest>(4),
},
.ownerAccountId = FilledId<AccountId>(5),
};
auto second = CloudVaultConversation{
.conversationId = FilledId<ConversationId>(6),
.telegramPeerIdBinding = first.telegramPeerIdBinding,
.checkpoint = {
.conversationId = FilledId<ConversationId>(6),
.generation = 1,
.stateHash = FilledId<Digest>(7),
},
.ownerAccountId = FilledId<AccountId>(8),
};
if (codec.prepareUpdate(created->unlocked, { first, second })) {
return Fail("cloud vault accepted duplicate Telegram peer bindings");
}
second.telegramPeerIdBinding = 9002;
if (!codec.prepareUpdate(created->unlocked, { first, second })) {
return Fail("cloud vault rejected distinct Telegram peer bindings");
}
return 0;
}
[[nodiscard]] int ScenarioCloudVaultSelectionDetectsForksAndGaps() {
auto kdf = TestPasswordKdf();
auto sha256 = OpenSslSha256Provider();
auto codec = CloudVaultCodecV1(kdf, sha256);
auto selector = CloudVaultSelector(codec, sha256);
auto identity = GenerateAccountPrivateIdentity();
auto created = identity
? codec.create(
777,
std::move(*identity),
QByteArray("selection password"),
MakeConfig())
: std::nullopt;
if (!created) {
return Fail("cloud vault selection fixture could not be created");
}
const auto accountId = DeriveAccountId(
created->unlocked.identity.credential,
sha256);
if (!accountId) {
return Fail("cloud vault selection account id was invalid");
}
const auto firstConversation = CloudVaultConversation{
.conversationId = FilledId<ConversationId>(3),
.telegramPeerIdBinding = 9001,
.checkpoint = {
.conversationId = FilledId<ConversationId>(3),
.generation = 1,
.stateHash = FilledId<Digest>(4),
},
.ownerAccountId = *accountId,
};
const auto secondConversation = CloudVaultConversation{
.conversationId = FilledId<ConversationId>(5),
.telegramPeerIdBinding = 9002,
.checkpoint = {
.conversationId = FilledId<ConversationId>(5),
.generation = 1,
.stateHash = FilledId<Digest>(6),
},
.ownerAccountId = *accountId,
};
const auto versionOne = created->encoded;
const auto versionOneAnchor = CloudVaultAnchor{
.generation = created->unlocked.generation,
.blobDigest = created->unlocked.blobDigest,
.accountId = *accountId,
};
auto firstUpdate = codec.prepareUpdate(
created->unlocked,
{ firstConversation });
auto competingUpdate = codec.prepareUpdate(
created->unlocked,
{ secondConversation });
if (!firstUpdate
|| !competingUpdate
|| firstUpdate->blobDigest == competingUpdate->blobDigest) {
return Fail("cloud vault fork fixtures were not distinct");
}
const auto versionTwo = firstUpdate->encoded;
const auto competingVersionTwo = competingUpdate->encoded;
const auto versionTwoAnchor = CloudVaultAnchor{
.generation = firstUpdate->generation,
.blobDigest = firstUpdate->blobDigest,
.accountId = *accountId,
};
auto competingVault = codec.unlock(
versionOne,
QByteArray("selection password"),
777);
if (!competingVault
|| !codec.applyPublished(
*competingVault,
std::move(*competingUpdate))) {
return Fail("cloud vault competing branch could not be adopted");
}
auto competingThirdUpdate = codec.prepareUpdate(
*competingVault,
{ secondConversation });
if (!competingThirdUpdate) {
return Fail("cloud vault competing third version was not created");
}
const auto competingVersionThree = competingThirdUpdate->encoded;
if (!codec.applyPublished(
created->unlocked,
std::move(*firstUpdate))) {
return Fail("cloud vault selection fixture update was not adopted");
}
auto secondUpdate = codec.prepareUpdate(
created->unlocked,
{ firstConversation, secondConversation });
if (!secondUpdate) {
return Fail("cloud vault third selection version was not created");
}
const auto versionThree = secondUpdate->encoded;
auto selected = selector.select(
{ versionOne, versionTwo, versionThree },
QByteArray("selection password"),
777,
versionOneAnchor);
if (selected.status != CloudVaultSelectionStatus::Selected
|| !selected.vault
|| selected.vault->generation != 3) {
return Fail("cloud vault selector did not follow the signed chain");
}
auto unanchored = selector.select(
{ versionOne, versionTwo, versionThree },
QByteArray("selection password"),
777);
if (unanchored.status != CloudVaultSelectionStatus::Selected
|| !unanchored.vault
|| unanchored.vault->generation != 3) {
return Fail("unanchored cloud vault rejected a complete chain");
}
auto forked = selector.select(
{ versionTwo, competingVersionTwo },
QByteArray("selection password"),
777,
versionOneAnchor);
if (forked.status != CloudVaultSelectionStatus::ForkDetected) {
return Fail("cloud vault selector silently chose a concurrent update");
}
auto unanchoredGap = selector.select(
{ versionTwo, versionThree },
QByteArray("selection password"),
777);
if (unanchoredGap.status != CloudVaultSelectionStatus::ChainGap) {
return Fail("unanchored cloud vault accepted a missing genesis");
}
auto unanchoredBranch = selector.select(
{ versionOne, versionTwo, competingVersionThree },
QByteArray("selection password"),
777);
if (unanchoredBranch.status != CloudVaultSelectionStatus::ChainGap) {
return Fail("unanchored cloud vault crossed competing branches");
}
auto gap = selector.select(
{ versionThree },
QByteArray("selection password"),
777,
versionOneAnchor);
if (gap.status != CloudVaultSelectionStatus::ChainGap) {
return Fail("cloud vault selector accepted a missing anchored version");
}
auto rollback = selector.select(
{ versionOne },
QByteArray("selection password"),
777,
versionTwoAnchor);
if (rollback.status != CloudVaultSelectionStatus::RollbackDetected) {
return Fail("cloud vault selector accepted an anchored rollback");
}
auto missing = selector.select(
{},
QByteArray("selection password"),
777,
versionTwoAnchor);
if (missing.status != CloudVaultSelectionStatus::RollbackDetected) {
return Fail("cloud vault selector accepted deletion after an anchor");
}
return 0;
}
[[nodiscard]] int ScenarioCloudVaultSelectionScalesPastOldLimit() {
auto kdf = TestPasswordKdf();
auto sha256 = OpenSslSha256Provider();
auto codec = CloudVaultCodecV1(kdf, sha256);
auto selector = CloudVaultSelector(codec, sha256);
auto identity = GenerateAccountPrivateIdentity();
auto created = identity
? codec.create(
777,
std::move(*identity),
QByteArray("selection scale password"),
MakeConfig())
: std::nullopt;
if (!created) {
return Fail("large cloud vault selection fixture could not be created");
}
const auto accountId = DeriveAccountId(
created->unlocked.identity.credential,
sha256);
if (!accountId) {
return Fail("large cloud vault selection account id was invalid");
}
const auto anchor = CloudVaultAnchor{
.generation = created->unlocked.generation,
.blobDigest = created->unlocked.blobDigest,
.accountId = *accountId,
};
auto versions = std::vector<QByteArray>{ created->encoded };
while (created->unlocked.generation != 300) {
auto update = codec.prepareUpdate(
created->unlocked,
created->unlocked.conversations);
if (!update) {
return Fail("large cloud vault chain could not be extended");
}
versions.push_back(update->encoded);
if (!codec.applyPublished(
created->unlocked,
std::move(*update))) {
return Fail("large cloud vault update could not be adopted");
}
}
const auto callsBeforeSelection = kdf.calls;
auto selected = selector.select(
std::move(versions),
QByteArray("selection scale password"),
777,
anchor);
if (selected.status != CloudVaultSelectionStatus::Selected
|| !selected.vault
|| selected.vault->generation != 300
|| kdf.calls != callsBeforeSelection + 1) {
return Fail("large cloud vault chain repeated KDF work or hit old cap");
}
return 0;
}
[[nodiscard]] int ScenarioPersistentCloudVaultAnchor() {
auto blob = MemoryBlobStore();
auto persistent = PersistentCloudVaultAnchor(blob, 777);
const auto accountId = FilledId<AccountId>(3);
const auto first = CloudVaultAnchor{
.generation = 4,
.blobDigest = FilledId<Digest>(5),
.accountId = accountId,
};
const auto second = CloudVaultAnchor{
.generation = 5,
.blobDigest = FilledId<Digest>(6),
.accountId = accountId,
};
if (persistent.load() != CloudVaultAnchorLoadResult::Missing
|| !persistent.loaded()
|| persistent.anchor()
|| persistent.commit(first)
!= CloudVaultAnchorCommitResult::Committed
|| persistent.commit(first)
!= CloudVaultAnchorCommitResult::AlreadyCommitted
|| persistent.anchor() != first) {
return Fail("cloud vault anchor did not commit idempotently");
}
auto reopened = PersistentCloudVaultAnchor(blob, 777);
if (reopened.load() != CloudVaultAnchorLoadResult::Loaded
|| reopened.anchor() != first
|| reopened.commit(CloudVaultAnchor{
.generation = 3,
.blobDigest = FilledId<Digest>(7),
.accountId = accountId,
}) != CloudVaultAnchorCommitResult::Conflict
|| reopened.commit(CloudVaultAnchor{
.generation = 4,
.blobDigest = FilledId<Digest>(8),
.accountId = accountId,
}) != CloudVaultAnchorCommitResult::Conflict
|| reopened.commit(CloudVaultAnchor{
.generation = 5,
.blobDigest = second.blobDigest,
.accountId = FilledId<AccountId>(9),
}) != CloudVaultAnchorCommitResult::Conflict) {
return Fail("cloud vault anchor accepted an invalid chain change");
}
blob.failWrites = true;
if (reopened.commit(second)
!= CloudVaultAnchorCommitResult::PersistenceFailed
|| reopened.anchor() != first) {
return Fail("failed cloud vault anchor write changed live state");
}
blob.failWrites = false;
if (reopened.commit(second) != CloudVaultAnchorCommitResult::Committed) {
return Fail("cloud vault anchor did not advance monotonically");
}
auto wrongUser = PersistentCloudVaultAnchor(blob, 778);
if (wrongUser.load() != CloudVaultAnchorLoadResult::InvalidSnapshot) {
return Fail("cloud vault anchor crossed Telegram accounts");
}
blob.bytes = QByteArray("invalid");
auto corrupted = PersistentCloudVaultAnchor(blob, 777);
if (corrupted.load() != CloudVaultAnchorLoadResult::InvalidSnapshot
|| corrupted.loaded()) {
return Fail("corrupt cloud vault anchor did not fail closed");
}
return 0;
}
} // namespace
int main(int, char *[]) {
for (const auto scenario : {
ScenarioVaultCompletionCanDestroyController,
ScenarioVaultDiscoveryFindsMissingIdentity,
ScenarioVaultDiscoveryFindsExistingAccount,
ScenarioVaultSelectionCanCompleteAsynchronously,
ScenarioVaultRestartIgnoresPreviousDownload,
ScenarioVaultRoundTrip,
ScenarioVaultRejectsWrongPasswordAndTampering,
ScenarioVaultUsesFreshNonce,
ScenarioVaultBoundsKdfBeforeDerivation,
ScenarioVaultRejectsExcessiveKdfBeforeDerivation,
ScenarioArgon2idReferenceVector,
ScenarioCloudVaultRoundTripAndUpdate,
ScenarioCloudVaultRejectsTamperingAndKeyMismatch,
ScenarioCloudVaultRejectsDuplicatePeerBindings,
ScenarioCloudVaultSelectionDetectsForksAndGaps,
ScenarioCloudVaultSelectionScalesPastOldLimit,
ScenarioPersistentCloudVaultAnchor,
}) {
if (const auto result = scenario()) {
return result;
}
}
return 0;
}