Index: sync/engine/model_type_sync_worker_impl_unittest.cc |
diff --git a/sync/engine/model_type_sync_worker_impl_unittest.cc b/sync/engine/model_type_sync_worker_impl_unittest.cc |
index 0c1fa13b1136a4c32eae5015ab07e897bbf736ef..df2ed66d71d4442ecc1997dedd19040795788f58 100644 |
--- a/sync/engine/model_type_sync_worker_impl_unittest.cc |
+++ b/sync/engine/model_type_sync_worker_impl_unittest.cc |
@@ -4,6 +4,7 @@ |
#include "sync/engine/model_type_sync_worker_impl.h" |
+#include "base/strings/stringprintf.h" |
#include "sync/engine/commit_contribution.h" |
#include "sync/engine/model_type_sync_proxy.h" |
#include "sync/internal_api/public/base/model_type.h" |
@@ -13,13 +14,18 @@ |
#include "sync/syncable/syncable_util.h" |
#include "sync/test/engine/mock_model_type_sync_proxy.h" |
#include "sync/test/engine/mock_nudge_handler.h" |
+#include "sync/test/engine/simple_cryptographer_provider.h" |
#include "sync/test/engine/single_type_mock_server.h" |
+#include "sync/test/fake_encryptor.h" |
#include "testing/gtest/include/gtest/gtest.h" |
static const std::string kTypeParentId = "PrefsRootNodeID"; |
static const syncer::ModelType kModelType = syncer::PREFERENCES; |
+// Special constant value taken from cryptographer.cc. |
+const char kNigoriKeyName[] = "nigori-key"; |
+ |
namespace syncer { |
// Tests the ModelTypeSyncWorkerImpl. |
@@ -65,8 +71,23 @@ class ModelTypeSyncWorkerImplTest : public ::testing::Test { |
// committing items right away. |
void NormalInitialize(); |
- // Initialize with a custom initial DataTypeState. |
- void InitializeWithState(const DataTypeState& state); |
+ // Initialize with some saved pending updates from the model thread. |
+ void InitializeWithPendingUpdates( |
+ const UpdateResponseDataList& initial_pending_updates); |
+ |
+ // Initialize with a custom initial DataTypeState and pending updates. |
+ void InitializeWithState(const DataTypeState& state, |
+ const UpdateResponseDataList& pending_updates); |
+ |
+ // Introduce a new key that the local cryptographer can't decrypt. |
+ void NewForeignEncryptionKey(); |
+ |
+ // Update the local cryptographer with all relevant keys. |
+ void UpdateLocalCryptographer(); |
+ |
+ // Use the Nth nigori instance to encrypt incoming updates. |
+ // The default value, zero, indicates no encryption. |
+ void SetUpdateEncryptionFilter(int n); |
// Modifications on the model thread that get sent to the worker under test. |
void CommitRequest(const std::string& tag, const std::string& value); |
@@ -79,6 +100,13 @@ class ModelTypeSyncWorkerImplTest : public ::testing::Test { |
const std::string& value); |
void TriggerTombstoneFromServer(int64 version_offset, const std::string& tag); |
+ // Delivers specified protos as updates. |
+ // |
+ // Does not update mock server state. Should be used as a last resort when |
+ // writing test cases that require entities that don't fit the normal sync |
+ // protocol. Try to use the other, higher level methods if possible. |
+ void DeliverRawUpdates(const SyncEntityList& update_list); |
+ |
// By default, this harness behaves as if all tasks posted to the model |
// thread are executed immediately. However, this is not necessarily true. |
// The model's TaskRunner has a queue, and the tasks we post to it could |
@@ -110,6 +138,7 @@ class ModelTypeSyncWorkerImplTest : public ::testing::Test { |
// be updated until the response is actually processed by the model thread. |
size_t GetNumModelThreadUpdateResponses() const; |
UpdateResponseDataList GetNthModelThreadUpdateResponse(size_t n) const; |
+ UpdateResponseDataList GetNthModelThreadPendingUpdates(size_t n) const; |
DataTypeState GetNthModelThreadUpdateState(size_t n) const; |
// Reads the latest update response datas on the model thread. |
@@ -144,7 +173,39 @@ class ModelTypeSyncWorkerImplTest : public ::testing::Test { |
static sync_pb::EntitySpecifics GenerateSpecifics(const std::string& tag, |
const std::string& value); |
+ // Returns a set of KeyParams for the cryptographer. Each input 'n' value |
+ // results in a different set of parameters. |
+ static KeyParams GetNthKeyParams(int n); |
+ |
+ // Returns the name for the given Nigori. |
+ // |
+ // Uses some 'white-box' knowledge to mimic the names that a real sync client |
+ // would generate. It's probably not necessary to do so, but it can't hurt. |
+ static std::string GetNigoriName(const Nigori& nigori); |
+ |
+ // Modifies the input/output parameter |specifics| by encrypting it with |
+ // a Nigori intialized with the specified KeyParams. |
+ static void EncryptUpdate(const KeyParams& params, |
+ sync_pb::EntitySpecifics* specifics); |
+ |
private: |
+ // An encryptor for our cryptographer. |
+ FakeEncryptor fake_encryptor_; |
+ |
+ // The cryptographer itself. |
+ Cryptographer cryptographer_; |
+ |
+ // A CryptographerProvider for the ModelTypeSyncWorkerImpl. |
+ SimpleCryptographerProvider cryptographer_provider_; |
+ |
+ // The number of the most recent foreign encryption key known to our |
+ // cryptographer. Note that not all of these will be decryptable. |
+ int foreign_encryption_key_index_; |
+ |
+ // The number of the encryption key used to encrypt incoming updates. A zero |
+ // value implies no encryption. |
+ int update_encryption_filter_index_; |
+ |
// The ModelTypeSyncWorkerImpl being tested. |
scoped_ptr<ModelTypeSyncWorkerImpl> worker_; |
@@ -163,7 +224,12 @@ class ModelTypeSyncWorkerImplTest : public ::testing::Test { |
}; |
ModelTypeSyncWorkerImplTest::ModelTypeSyncWorkerImplTest() |
- : mock_type_sync_proxy_(NULL), mock_server_(kModelType) { |
+ : cryptographer_(&fake_encryptor_), |
+ cryptographer_provider_(&cryptographer_), |
+ foreign_encryption_key_index_(0), |
+ update_encryption_filter_index_(0), |
+ mock_type_sync_proxy_(NULL), |
+ mock_server_(kModelType) { |
} |
ModelTypeSyncWorkerImplTest::~ModelTypeSyncWorkerImplTest() { |
@@ -175,10 +241,15 @@ void ModelTypeSyncWorkerImplTest::FirstInitialize() { |
GetSpecificsFieldNumberFromModelType(kModelType)); |
initial_state.next_client_id = 0; |
- InitializeWithState(initial_state); |
+ InitializeWithState(initial_state, UpdateResponseDataList()); |
} |
void ModelTypeSyncWorkerImplTest::NormalInitialize() { |
+ InitializeWithPendingUpdates(UpdateResponseDataList()); |
+} |
+ |
+void ModelTypeSyncWorkerImplTest::InitializeWithPendingUpdates( |
+ const UpdateResponseDataList& initial_pending_updates) { |
DataTypeState initial_state; |
initial_state.progress_marker.set_data_type_id( |
GetSpecificsFieldNumberFromModelType(kModelType)); |
@@ -188,21 +259,79 @@ void ModelTypeSyncWorkerImplTest::NormalInitialize() { |
initial_state.type_root_id = kTypeParentId; |
initial_state.initial_sync_done = true; |
- InitializeWithState(initial_state); |
+ InitializeWithState(initial_state, initial_pending_updates); |
mock_nudge_handler_.ClearCounters(); |
} |
void ModelTypeSyncWorkerImplTest::InitializeWithState( |
- const DataTypeState& state) { |
+ const DataTypeState& state, |
+ const UpdateResponseDataList& initial_pending_updates) { |
DCHECK(!worker_); |
// We don't get to own this object. The |worker_| keeps a scoped_ptr to it. |
mock_type_sync_proxy_ = new MockModelTypeSyncProxy(); |
scoped_ptr<ModelTypeSyncProxy> proxy(mock_type_sync_proxy_); |
- worker_.reset(new ModelTypeSyncWorkerImpl( |
- kModelType, state, &mock_nudge_handler_, proxy.Pass())); |
+ worker_.reset(new ModelTypeSyncWorkerImpl(kModelType, |
+ state, |
+ initial_pending_updates, |
+ &cryptographer_provider_, |
+ &mock_nudge_handler_, |
+ proxy.Pass())); |
+} |
+ |
+void ModelTypeSyncWorkerImplTest::NewForeignEncryptionKey() { |
+ foreign_encryption_key_index_++; |
+ |
+ sync_pb::NigoriKeyBag bag; |
+ |
+ for (int i = 0; i <= foreign_encryption_key_index_; ++i) { |
+ Nigori nigori; |
+ KeyParams params = GetNthKeyParams(i); |
+ nigori.InitByDerivation(params.hostname, params.username, params.password); |
+ |
+ sync_pb::NigoriKey* key = bag.add_key(); |
+ |
+ key->set_name(GetNigoriName(nigori)); |
+ nigori.ExportKeys(key->mutable_user_key(), |
+ key->mutable_encryption_key(), |
+ key->mutable_mac_key()); |
+ } |
+ |
+ // Re-create the last nigori from that loop. |
+ Nigori last_nigori; |
+ KeyParams params = GetNthKeyParams(foreign_encryption_key_index_); |
+ last_nigori.InitByDerivation( |
+ params.hostname, params.username, params.password); |
+ |
+ // Serialize and encrypt the bag with the last nigori. |
+ std::string serialized_bag; |
+ bag.SerializeToString(&serialized_bag); |
+ |
+ sync_pb::EncryptedData encrypted; |
+ encrypted.set_key_name(GetNigoriName(last_nigori)); |
+ last_nigori.Encrypt(serialized_bag, encrypted.mutable_blob()); |
+ |
+ // Update the cryptographer with new pending keys. |
+ cryptographer_.SetPendingKeys(encrypted); |
+ |
+ // Update the worker with the latest encryption key name. |
+ if (worker_) |
+ worker_->SetEncryptionKeyName(encrypted.key_name()); |
+} |
+ |
+void ModelTypeSyncWorkerImplTest::UpdateLocalCryptographer() { |
+ KeyParams params = GetNthKeyParams(foreign_encryption_key_index_); |
+ bool success = cryptographer_.DecryptPendingKeys(params); |
+ DCHECK(success); |
+ |
+ if (worker_) |
+ worker_->OnCryptographerStateChanged(); |
+} |
+ |
+void ModelTypeSyncWorkerImplTest::SetUpdateEncryptionFilter(int n) { |
+ update_encryption_filter_index_ = n; |
} |
void ModelTypeSyncWorkerImplTest::CommitRequest(const std::string& name, |
@@ -243,6 +372,12 @@ void ModelTypeSyncWorkerImplTest::TriggerUpdateFromServer( |
const std::string& value) { |
sync_pb::SyncEntity entity = mock_server_.UpdateFromServer( |
version_offset, GenerateTagHash(tag), GenerateSpecifics(tag, value)); |
+ |
+ if (update_encryption_filter_index_ != 0) { |
+ EncryptUpdate(GetNthKeyParams(update_encryption_filter_index_), |
+ entity.mutable_specifics()); |
+ } |
+ |
SyncEntityList entity_list; |
entity_list.push_back(&entity); |
@@ -255,11 +390,27 @@ void ModelTypeSyncWorkerImplTest::TriggerUpdateFromServer( |
worker_->ApplyUpdates(&dummy_status); |
} |
+void ModelTypeSyncWorkerImplTest::DeliverRawUpdates( |
+ const SyncEntityList& list) { |
+ sessions::StatusController dummy_status; |
+ worker_->ProcessGetUpdatesResponse(mock_server_.GetProgress(), |
+ mock_server_.GetContext(), |
+ list, |
+ &dummy_status); |
+ worker_->ApplyUpdates(&dummy_status); |
+} |
+ |
void ModelTypeSyncWorkerImplTest::TriggerTombstoneFromServer( |
int64 version_offset, |
const std::string& tag) { |
sync_pb::SyncEntity entity = |
mock_server_.TombstoneFromServer(version_offset, GenerateTagHash(tag)); |
+ |
+ if (update_encryption_filter_index_ != 0) { |
+ EncryptUpdate(GetNthKeyParams(update_encryption_filter_index_), |
+ entity.mutable_specifics()); |
+ } |
+ |
SyncEntityList entity_list; |
entity_list.push_back(&entity); |
@@ -346,6 +497,12 @@ ModelTypeSyncWorkerImplTest::GetNthModelThreadUpdateResponse(size_t n) const { |
return mock_type_sync_proxy_->GetNthUpdateResponse(n); |
} |
+UpdateResponseDataList |
+ModelTypeSyncWorkerImplTest::GetNthModelThreadPendingUpdates(size_t n) const { |
+ DCHECK_LT(n, GetNumModelThreadUpdateResponses()); |
+ return mock_type_sync_proxy_->GetNthPendingUpdates(n); |
+} |
+ |
DataTypeState ModelTypeSyncWorkerImplTest::GetNthModelThreadUpdateState( |
size_t n) const { |
DCHECK_LT(n, GetNumModelThreadUpdateResponses()); |
@@ -401,6 +558,7 @@ int ModelTypeSyncWorkerImplTest::GetNumInitialDownloadNudges() const { |
return mock_nudge_handler_.GetNumInitialDownloadNudges(); |
} |
+// static. |
std::string ModelTypeSyncWorkerImplTest::GenerateTagHash( |
const std::string& tag) { |
const std::string& client_tag_hash = |
@@ -408,6 +566,7 @@ std::string ModelTypeSyncWorkerImplTest::GenerateTagHash( |
return client_tag_hash; |
} |
+// static. |
sync_pb::EntitySpecifics ModelTypeSyncWorkerImplTest::GenerateSpecifics( |
const std::string& tag, |
const std::string& value) { |
@@ -417,6 +576,46 @@ sync_pb::EntitySpecifics ModelTypeSyncWorkerImplTest::GenerateSpecifics( |
return specifics; |
} |
+// static. |
+std::string ModelTypeSyncWorkerImplTest::GetNigoriName(const Nigori& nigori) { |
+ std::string name; |
+ if (!nigori.Permute(Nigori::Password, kNigoriKeyName, &name)) { |
+ NOTREACHED(); |
+ return std::string(); |
+ } |
+ |
+ return name; |
+} |
+ |
+// static. |
+KeyParams ModelTypeSyncWorkerImplTest::GetNthKeyParams(int n) { |
+ KeyParams params; |
+ params.hostname = std::string("localhost"); |
+ params.username = std::string("userX"); |
+ params.password = base::StringPrintf("pw%02d", n); |
+ return params; |
+} |
+ |
+// static. |
+void ModelTypeSyncWorkerImplTest::EncryptUpdate( |
+ const KeyParams& params, |
+ sync_pb::EntitySpecifics* specifics) { |
+ Nigori nigori; |
+ nigori.InitByDerivation(params.hostname, params.username, params.password); |
+ |
+ sync_pb::EntitySpecifics original_specifics = *specifics; |
+ std::string plaintext; |
+ original_specifics.SerializeToString(&plaintext); |
+ |
+ std::string encrypted; |
+ nigori.Encrypt(plaintext, &encrypted); |
+ |
+ specifics->Clear(); |
+ AddDefaultFieldValue(kModelType, specifics); |
+ specifics->mutable_encrypted()->set_key_name(GetNigoriName(nigori)); |
+ specifics->mutable_encrypted()->set_blob(encrypted); |
+} |
+ |
// Requests a commit and verifies the messages sent to the client and server as |
// a result. |
// |
@@ -600,6 +799,7 @@ TEST_F(ModelTypeSyncWorkerImplTest, TwoNewItemsCommittedSeparately) { |
EXPECT_EQ(2U, GetNumModelThreadCommitResponses()); |
} |
+// Test normal update receipt code path. |
TEST_F(ModelTypeSyncWorkerImplTest, ReceiveUpdates) { |
NormalInitialize(); |
@@ -625,4 +825,281 @@ TEST_F(ModelTypeSyncWorkerImplTest, ReceiveUpdates) { |
EXPECT_EQ("value1", update.specifics.preference().value()); |
} |
+// Test commit of encrypted updates. |
+TEST_F(ModelTypeSyncWorkerImplTest, EncryptedCommit) { |
+ NormalInitialize(); |
+ |
+ NewForeignEncryptionKey(); |
+ UpdateLocalCryptographer(); |
+ |
+ // Normal commit request stuff. |
+ CommitRequest("tag1", "value1"); |
+ DoSuccessfulCommit(); |
+ ASSERT_EQ(1U, GetNumCommitMessagesOnServer()); |
+ EXPECT_EQ(1, GetNthCommitMessageOnServer(0).commit().entries_size()); |
+ ASSERT_TRUE(HasCommitEntityOnServer("tag1")); |
+ const sync_pb::SyncEntity& tag1_entity = |
+ GetLatestCommitEntityOnServer("tag1"); |
+ |
+ EXPECT_TRUE(tag1_entity.specifics().has_encrypted()); |
+ |
+ // The title should be overwritten. |
+ EXPECT_EQ(tag1_entity.name(), "encrypted"); |
+ |
+ // The type should be set, but there should be no non-encrypted contents. |
+ EXPECT_TRUE(tag1_entity.specifics().has_preference()); |
+ EXPECT_FALSE(tag1_entity.specifics().preference().has_name()); |
+ EXPECT_FALSE(tag1_entity.specifics().preference().has_value()); |
+} |
+ |
+// Test items are not committed when encryption is required but unavailable. |
+TEST_F(ModelTypeSyncWorkerImplTest, EncryptionBlocksCommits) { |
+ NormalInitialize(); |
+ |
+ CommitRequest("tag1", "value1"); |
+ EXPECT_TRUE(WillCommit()); |
+ |
+ // We know encryption is in use on this account, but don't have the necessary |
+ // encryption keys. The worker should refuse to commit. |
+ NewForeignEncryptionKey(); |
+ EXPECT_FALSE(WillCommit()); |
+ |
+ // Once the cryptographer is returned to a normal state, we should be able to |
+ // commit again. |
+ EXPECT_EQ(1, GetNumCommitNudges()); |
+ UpdateLocalCryptographer(); |
+ EXPECT_EQ(2, GetNumCommitNudges()); |
+ EXPECT_TRUE(WillCommit()); |
+ |
+ // Verify the committed entity was properly encrypted. |
+ DoSuccessfulCommit(); |
+ ASSERT_EQ(1U, GetNumCommitMessagesOnServer()); |
+ EXPECT_EQ(1, GetNthCommitMessageOnServer(0).commit().entries_size()); |
+ ASSERT_TRUE(HasCommitEntityOnServer("tag1")); |
+ const sync_pb::SyncEntity& tag1_entity = |
+ GetLatestCommitEntityOnServer("tag1"); |
+ EXPECT_TRUE(tag1_entity.specifics().has_encrypted()); |
+ EXPECT_EQ(tag1_entity.name(), "encrypted"); |
+ EXPECT_TRUE(tag1_entity.specifics().has_preference()); |
+ EXPECT_FALSE(tag1_entity.specifics().preference().has_name()); |
+ EXPECT_FALSE(tag1_entity.specifics().preference().has_value()); |
+} |
+ |
+// Test the receipt of decryptable entities. |
+TEST_F(ModelTypeSyncWorkerImplTest, ReceiveDecryptableEntities) { |
+ NormalInitialize(); |
+ |
+ // Create a new Nigori and allow the cryptographer to decrypt it. |
+ NewForeignEncryptionKey(); |
+ UpdateLocalCryptographer(); |
+ |
+ // First, receive an unencrypted entry. |
+ TriggerUpdateFromServer(10, "tag1", "value1"); |
+ |
+ // Test some basic properties regarding the update. |
+ ASSERT_TRUE(HasUpdateResponseOnModelThread("tag1")); |
+ UpdateResponseData update1 = GetUpdateResponseOnModelThread("tag1"); |
+ EXPECT_EQ("tag1", update1.specifics.preference().name()); |
+ EXPECT_EQ("value1", update1.specifics.preference().value()); |
+ EXPECT_TRUE(update1.encryption_key_name.empty()); |
+ |
+ // Set received updates to be encrypted using the new nigori. |
+ SetUpdateEncryptionFilter(1); |
+ |
+ // This next update will be encrypted. |
+ TriggerUpdateFromServer(10, "tag2", "value2"); |
+ |
+ // Test its basic features and the value of encryption_key_name. |
+ ASSERT_TRUE(HasUpdateResponseOnModelThread("tag2")); |
+ UpdateResponseData update2 = GetUpdateResponseOnModelThread("tag2"); |
+ EXPECT_EQ("tag2", update2.specifics.preference().name()); |
+ EXPECT_EQ("value2", update2.specifics.preference().value()); |
+ EXPECT_FALSE(update2.encryption_key_name.empty()); |
+} |
+ |
+// Receive updates that are initially undecryptable, then ensure they get |
+// delivered to the model thread when decryption becomes possible. |
+TEST_F(ModelTypeSyncWorkerImplTest, ReceiveUndecryptableEntries) { |
+ NormalInitialize(); |
+ |
+ // Set a new encryption key. The model thread will be notified of the new |
+ // encryption key through a faked update response. |
+ NewForeignEncryptionKey(); |
+ EXPECT_EQ(1U, GetNumModelThreadUpdateResponses()); |
+ |
+ // Send an update using that new key. |
+ SetUpdateEncryptionFilter(1); |
+ TriggerUpdateFromServer(10, "tag1", "value1"); |
+ |
+ // At this point, the cryptographer does not have access to the key, so the |
+ // updates will be undecryptable. They'll be transfered to the model thread |
+ // for safe-keeping as pending updates. |
+ ASSERT_EQ(2U, GetNumModelThreadUpdateResponses()); |
+ UpdateResponseDataList updates_list = GetNthModelThreadUpdateResponse(1); |
+ EXPECT_EQ(0U, updates_list.size()); |
+ UpdateResponseDataList pending_updates = GetNthModelThreadPendingUpdates(1); |
+ EXPECT_EQ(1U, pending_updates.size()); |
+ |
+ // The update will be delivered as soon as decryption becomes possible. |
+ UpdateLocalCryptographer(); |
+ ASSERT_TRUE(HasUpdateResponseOnModelThread("tag1")); |
+ UpdateResponseData update = GetUpdateResponseOnModelThread("tag1"); |
+ EXPECT_EQ("tag1", update.specifics.preference().name()); |
+ EXPECT_EQ("value1", update.specifics.preference().value()); |
+ EXPECT_FALSE(update.encryption_key_name.empty()); |
+} |
+ |
+// Ensure that even encrypted updates can cause conflicts. |
+TEST_F(ModelTypeSyncWorkerImplTest, EncryptedUpdateOverridesPendingCommit) { |
+ NormalInitialize(); |
+ |
+ // Prepeare to commit an item. |
+ CommitRequest("tag1", "value1"); |
+ EXPECT_TRUE(WillCommit()); |
+ |
+ // Receive an encrypted update for that item. |
+ SetUpdateEncryptionFilter(1); |
+ TriggerUpdateFromServer(10, "tag1", "value1"); |
+ |
+ // The pending commit state should be cleared. |
+ EXPECT_FALSE(WillCommit()); |
+ |
+ // The encrypted update will be delivered to the model thread. |
+ ASSERT_EQ(1U, GetNumModelThreadUpdateResponses()); |
+ UpdateResponseDataList updates_list = GetNthModelThreadUpdateResponse(0); |
+ EXPECT_EQ(0U, updates_list.size()); |
+ UpdateResponseDataList pending_updates = GetNthModelThreadPendingUpdates(0); |
+ EXPECT_EQ(1U, pending_updates.size()); |
+} |
+ |
+// Test decryption of pending updates saved across a restart. |
+TEST_F(ModelTypeSyncWorkerImplTest, RestorePendingEntries) { |
+ // Create a fake pending update. |
+ UpdateResponseData update; |
+ |
+ update.client_tag_hash = GenerateTagHash("tag1"); |
+ update.id = "SomeID"; |
+ update.response_version = 100; |
+ update.ctime = base::Time::UnixEpoch() + base::TimeDelta::FromSeconds(10); |
+ update.mtime = base::Time::UnixEpoch() + base::TimeDelta::FromSeconds(11); |
+ update.non_unique_name = "encrypted"; |
+ update.deleted = false; |
+ |
+ update.specifics = GenerateSpecifics("tag1", "value1"); |
+ EncryptUpdate(GetNthKeyParams(1), &(update.specifics)); |
+ |
+ // Inject the update during ModelTypeSyncWorker initialization. |
+ UpdateResponseDataList saved_pending_updates; |
+ saved_pending_updates.push_back(update); |
+ InitializeWithPendingUpdates(saved_pending_updates); |
+ |
+ // Update will be undecryptable at first. |
+ EXPECT_EQ(0U, GetNumModelThreadUpdateResponses()); |
+ ASSERT_FALSE(HasUpdateResponseOnModelThread("tag1")); |
+ |
+ // Update the cryptographer so it can decrypt that update. |
+ NewForeignEncryptionKey(); |
+ UpdateLocalCryptographer(); |
+ |
+ // Verify the item gets decrypted and sent back to the model thread. |
+ ASSERT_TRUE(HasUpdateResponseOnModelThread("tag1")); |
+} |
+ |
+// Test decryption of pending updates saved across a restart. This test |
+// differs from the previous one in that the restored updates can be decrypted |
+// immediately after the ModelTypeSyncWorker is constructed. |
+TEST_F(ModelTypeSyncWorkerImplTest, RestoreApplicableEntries) { |
+ // Update the cryptographer so it can decrypt that update. |
+ NewForeignEncryptionKey(); |
+ UpdateLocalCryptographer(); |
+ |
+ // Create a fake pending update. |
+ UpdateResponseData update; |
+ update.client_tag_hash = GenerateTagHash("tag1"); |
+ update.id = "SomeID"; |
+ update.response_version = 100; |
+ update.ctime = base::Time::UnixEpoch() + base::TimeDelta::FromSeconds(10); |
+ update.mtime = base::Time::UnixEpoch() + base::TimeDelta::FromSeconds(11); |
+ update.non_unique_name = "encrypted"; |
+ update.deleted = false; |
+ |
+ update.specifics = GenerateSpecifics("tag1", "value1"); |
+ EncryptUpdate(GetNthKeyParams(1), &(update.specifics)); |
+ |
+ // Inject the update during ModelTypeSyncWorker initialization. |
+ UpdateResponseDataList saved_pending_updates; |
+ saved_pending_updates.push_back(update); |
+ InitializeWithPendingUpdates(saved_pending_updates); |
+ |
+ // Verify the item gets decrypted and sent back to the model thread. |
+ ASSERT_TRUE(HasUpdateResponseOnModelThread("tag1")); |
+} |
+ |
+// Test that undecryptable updates provide sufficient reason to not commit. |
+// |
+// This should be rare in practice. Usually the cryptographer will be in an |
+// unusable state when we receive undecryptable updates, and that alone will be |
+// enough to prevent all commits. |
+TEST_F(ModelTypeSyncWorkerImplTest, CommitBlockedByPending) { |
+ NormalInitialize(); |
+ |
+ // Prepeare to commit an item. |
+ CommitRequest("tag1", "value1"); |
+ EXPECT_TRUE(WillCommit()); |
+ |
+ // Receive an encrypted update for that item. |
+ SetUpdateEncryptionFilter(1); |
+ TriggerUpdateFromServer(10, "tag1", "value1"); |
+ |
+ // The pending commit state should be cleared. |
+ EXPECT_FALSE(WillCommit()); |
+ |
+ // The pending update will be delivered to the model thread. |
+ HasUpdateResponseOnModelThread("tag1"); |
+ |
+ // Pretend the update arrived too late to prevent another commit request. |
+ CommitRequest("tag1", "value2"); |
+ |
+ EXPECT_FALSE(WillCommit()); |
+} |
+ |
+// Verify that corrupted encrypted updates don't cause crashes. |
+TEST_F(ModelTypeSyncWorkerImplTest, ReceiveCorruptEncryption) { |
+ // Initialize the worker with basic encryption state. |
+ NormalInitialize(); |
+ NewForeignEncryptionKey(); |
+ UpdateLocalCryptographer(); |
+ |
+ // Manually create an update. |
+ sync_pb::SyncEntity entity; |
+ entity.set_client_defined_unique_tag(GenerateTagHash("tag1")); |
+ entity.set_id_string("SomeID"); |
+ entity.set_version(1); |
+ entity.set_ctime(1000); |
+ entity.set_mtime(1001); |
+ entity.set_name("encrypted"); |
+ entity.set_deleted(false); |
+ |
+ // Encrypt it. |
+ entity.mutable_specifics()->CopyFrom(GenerateSpecifics("tag1", "value1")); |
+ EncryptUpdate(GetNthKeyParams(1), entity.mutable_specifics()); |
+ |
+ // Replace a few bytes to corrupt it. |
+ entity.mutable_specifics()->mutable_encrypted()->mutable_blob()->replace( |
+ 0, 4, "xyz!"); |
+ |
+ SyncEntityList entity_list; |
+ entity_list.push_back(&entity); |
+ |
+ // If a corrupt update could trigger a crash, this is where it would happen. |
+ DeliverRawUpdates(entity_list); |
+ |
+ EXPECT_FALSE(HasUpdateResponseOnModelThread("tag1")); |
+ |
+ // Deliver a non-corrupt update to see if the everything still works. |
+ SetUpdateEncryptionFilter(1); |
+ TriggerUpdateFromServer(10, "tag1", "value1"); |
+ EXPECT_TRUE(HasUpdateResponseOnModelThread("tag1")); |
+} |
+ |
} // namespace syncer |