Reland "[sync::test] Add single client integration test for custom passphrase"
This is a reland of 475bfe9da1df9ab55685e0a9ce1a62d8e50536f1. Fixed test
failures which happened because some DCHECKs had side effects.
Original change's description:
> [sync::test] Add single client integration test for custom passphrase
>
> Add a Sync integration test which exercises custom passphrase flows, including
> the handling and proper functioning of the newly added key derivation method
> (scrypt).
>
> Add an integration test helper file for encryption-related tasks and modify
> FakeServer and LoopbackServer to allow easier modification of persistent
> entities such as Nigori. Add passphrase-related functionality to
> ProfileSyncServiceHarness to give tests better control over how and when
> passphrase-based encryption is enabled.
>
> The test uses a gray-box approach, where it tests the client at the
> ProfileSyncService granularity, but injects and inspects entities on the (fake)
> server to ensure that encryption is performed properly. This is necessary
> because, when it comes to encryption, we are not interested merely in that it
> does not hinder existing functionality (e.g. two clients are syncing data
> properly in the presence of a custom passphrase), but also that it provides the
> expected security to the user. For this reason, we use our knowledge of the
> encryption architecture internals to ensure that the data committed to the
> server is encrypted in the expected way.
>
> Bug: 894148
> Change-Id: I728f7f18cc0db7b1da50f747a87a640877d0b023
> Reviewed-on: https://chromium-review.googlesource.com/c/1274205
> Commit-Queue: David Davidović <davidovic@google.com>
> Reviewed-by: vitaliii <vitaliii@chromium.org>
> Reviewed-by: Marc Treib <treib@chromium.org>
> Cr-Commit-Position: refs/heads/master@{#598912}
Bug: 894148
Change-Id: I752acff5815531c0c83ebefb43fb620a785e3ca3
Reviewed-on: https://chromium-review.googlesource.com/c/1278790
Commit-Queue: David Davidović <davidovic@google.com>
Reviewed-by: Marc Treib <treib@chromium.org>
Reviewed-by: vitaliii <vitaliii@chromium.org>
Cr-Commit-Position: refs/heads/master@{#599182}
diff --git a/chrome/browser/sync/test/integration/bookmarks_helper.cc b/chrome/browser/sync/test/integration/bookmarks_helper.cc
index 23547e2..b633d2c 100644
--- a/chrome/browser/sync/test/integration/bookmarks_helper.cc
+++ b/chrome/browser/sync/test/integration/bookmarks_helper.cc
@@ -45,6 +45,7 @@
#include "components/history/core/browser/history_db_task.h"
#include "components/history/core/browser/history_service.h"
#include "components/history/core/browser/history_types.h"
+#include "components/sync/test/fake_server/entity_builder_factory.h"
#include "components/sync_bookmarks/bookmark_change_processor.h"
#include "content/public/test/test_utils.h"
#include "testing/gtest/include/gtest/gtest.h"
@@ -957,6 +958,15 @@
return base::StringPrintf("Subsubfolder Name %d", i);
}
+std::unique_ptr<syncer::LoopbackServerEntity> CreateBookmarkServerEntity(
+ const std::string& title,
+ const GURL& url) {
+ fake_server::EntityBuilderFactory entity_builder_factory;
+ fake_server::BookmarkEntityBuilder bookmark_builder =
+ entity_builder_factory.NewBookmarkEntityBuilder(title);
+ return bookmark_builder.BuildBookmark(url);
+}
+
} // namespace bookmarks_helper
BookmarksMatchChecker::BookmarksMatchChecker()
@@ -1004,6 +1014,64 @@
return "Waiting for bookmark count to match";
}
+ServerBookmarksEqualityChecker::ServerBookmarksEqualityChecker(
+ browser_sync::ProfileSyncService* service,
+ fake_server::FakeServer* fake_server,
+ const std::vector<ExpectedBookmark>& expected_bookmarks,
+ syncer::Cryptographer* cryptographer)
+ : SingleClientStatusChangeChecker(service),
+ fake_server_(fake_server),
+ cryptographer_(cryptographer),
+ expected_bookmarks_(expected_bookmarks) {}
+
+bool ServerBookmarksEqualityChecker::IsExitConditionSatisfied() {
+ std::vector<sync_pb::SyncEntity> entities =
+ fake_server_->GetSyncEntitiesByModelType(syncer::BOOKMARKS);
+ if (expected_bookmarks_.size() != entities.size()) {
+ return false;
+ }
+
+ // Make a copy so we can remove bookmarks that were found.
+ std::vector<ExpectedBookmark> expected = expected_bookmarks_;
+ for (const sync_pb::SyncEntity& entity : entities) {
+ // If the cryptographer was provided, we expect the specifics to have
+ // encrypted data.
+ EXPECT_EQ(entity.specifics().has_encrypted(), cryptographer_ != nullptr);
+
+ sync_pb::BookmarkSpecifics actual_specifics;
+ if (entity.specifics().has_encrypted()) {
+ sync_pb::EntitySpecifics entity_specifics;
+ EXPECT_TRUE(cryptographer_->Decrypt(entity.specifics().encrypted(),
+ &entity_specifics));
+ actual_specifics = entity_specifics.bookmark();
+ } else {
+ actual_specifics = entity.specifics().bookmark();
+ }
+
+ auto it =
+ std::find_if(expected.begin(), expected.end(),
+ [actual_specifics](const ExpectedBookmark& bookmark) {
+ return actual_specifics.title() == bookmark.title &&
+ actual_specifics.url() == bookmark.url;
+ });
+ if (it != expected.end()) {
+ expected.erase(it);
+ } else {
+ ADD_FAILURE() << "Could not find expected bookmark with title '"
+ << actual_specifics.title() << "' and URL '"
+ << actual_specifics.url() << "'";
+ }
+ }
+
+ return true;
+}
+
+std::string ServerBookmarksEqualityChecker::GetDebugMessage() const {
+ return "Waiting for server-side bookmarks to match expected.";
+}
+
+ServerBookmarksEqualityChecker::~ServerBookmarksEqualityChecker() {}
+
namespace {
bool BookmarkCountsByUrlMatch(int profile,
diff --git a/chrome/browser/sync/test/integration/bookmarks_helper.h b/chrome/browser/sync/test/integration/bookmarks_helper.h
index a149ed80..ab030fee 100644
--- a/chrome/browser/sync/test/integration/bookmarks_helper.h
+++ b/chrome/browser/sync/test/integration/bookmarks_helper.h
@@ -5,13 +5,19 @@
#ifndef CHROME_BROWSER_SYNC_TEST_INTEGRATION_BOOKMARKS_HELPER_H_
#define CHROME_BROWSER_SYNC_TEST_INTEGRATION_BOOKMARKS_HELPER_H_
+#include <memory>
#include <string>
+#include <vector>
#include "base/compiler_specific.h"
#include "chrome/browser/sync/test/integration/await_match_status_change_checker.h"
#include "chrome/browser/sync/test/integration/multi_client_status_change_checker.h"
#include "chrome/browser/sync/test/integration/single_client_status_change_checker.h"
+#include "components/sync/base/cryptographer.h"
+#include "components/sync/engine_impl/loopback_server/loopback_server_entity.h"
+#include "components/sync/test/fake_server/fake_server.h"
#include "third_party/skia/include/core/SkColor.h"
+#include "url/gurl.h"
class GURL;
@@ -223,6 +229,12 @@
// Returns a subsubfolder name identifiable by |i|.
std::string IndexedSubsubfolderName(int i);
+// Creates a server-side entity representing a bookmark with the given title and
+// URL.
+std::unique_ptr<syncer::LoopbackServerEntity> CreateBookmarkServerEntity(
+ const std::string& title,
+ const GURL& url);
+
} // namespace bookmarks_helper
// Checker used to block until bookmarks match on all clients.
@@ -265,6 +277,37 @@
const int expected_count_;
};
+// Checker used to block until the bookmarks on the server match a given set of
+// expected bookmarks.
+class ServerBookmarksEqualityChecker : public SingleClientStatusChangeChecker {
+ public:
+ struct ExpectedBookmark {
+ std::string title;
+ GURL url;
+ };
+
+ // If a |cryptographer| is provided (i.e. is not nullptr), it is assumed that
+ // the server-side data should be encrypted, and the provided cryptographer
+ // will be used to decrypt the data prior to checking for equality.
+ ServerBookmarksEqualityChecker(
+ browser_sync::ProfileSyncService* service,
+ fake_server::FakeServer* fake_server,
+ const std::vector<ExpectedBookmark>& expected_bookmarks,
+ syncer::Cryptographer* cryptographer);
+
+ bool IsExitConditionSatisfied() override;
+ std::string GetDebugMessage() const override;
+
+ ~ServerBookmarksEqualityChecker() override;
+
+ private:
+ fake_server::FakeServer* fake_server_;
+ syncer::Cryptographer* cryptographer_;
+ const std::vector<ExpectedBookmark> expected_bookmarks_;
+
+ DISALLOW_COPY_AND_ASSIGN(ServerBookmarksEqualityChecker);
+};
+
// Checker used to block until the actual number of bookmarks with the given url
// match the expected count.
class BookmarksUrlChecker : public AwaitMatchStatusChangeChecker {
diff --git a/chrome/browser/sync/test/integration/encryption_helper.cc b/chrome/browser/sync/test/integration/encryption_helper.cc
new file mode 100644
index 0000000..66a1096
--- /dev/null
+++ b/chrome/browser/sync/test/integration/encryption_helper.cc
@@ -0,0 +1,176 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <string>
+#include <vector>
+
+#include "base/base64.h"
+#include "chrome/browser/sync/test/integration/encryption_helper.h"
+#include "components/browser_sync/profile_sync_service.h"
+#include "components/sync/base/passphrase_enums.h"
+#include "components/sync/base/system_encryptor.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace encryption_helper {
+
+bool GetServerNigori(fake_server::FakeServer* fake_server,
+ sync_pb::NigoriSpecifics* nigori) {
+ std::vector<sync_pb::SyncEntity> entity_list =
+ fake_server->GetPermanentSyncEntitiesByModelType(syncer::NIGORI);
+ if (entity_list.size() != 1U) {
+ return false;
+ }
+
+ *nigori = entity_list[0].specifics().nigori();
+ return true;
+}
+
+void InitCustomPassphraseCryptographerFromNigori(
+ const sync_pb::NigoriSpecifics& nigori,
+ syncer::Cryptographer* cryptographer,
+ const std::string& passphrase) {
+ sync_pb::EncryptedData keybag = nigori.encryption_keybag();
+ cryptographer->SetPendingKeys(keybag);
+
+ std::string decoded_salt;
+ switch (syncer::ProtoKeyDerivationMethodToEnum(
+ nigori.custom_passphrase_key_derivation_method())) {
+ case syncer::KeyDerivationMethod::PBKDF2_HMAC_SHA1_1003:
+ ASSERT_TRUE(cryptographer->DecryptPendingKeys(
+ {syncer::KeyDerivationParams::CreateForPbkdf2(), passphrase}));
+ break;
+ case syncer::KeyDerivationMethod::SCRYPT_8192_8_11:
+ ASSERT_TRUE(base::Base64Decode(
+ nigori.custom_passphrase_key_derivation_salt(), &decoded_salt));
+ ASSERT_TRUE(cryptographer->DecryptPendingKeys(
+ {syncer::KeyDerivationParams::CreateForScrypt(decoded_salt),
+ passphrase}));
+ break;
+ case syncer::KeyDerivationMethod::UNSUPPORTED:
+ // This test cannot pass since we wouldn't know how to decrypt data
+ // encrypted using an unsupported method.
+ FAIL() << "Unsupported key derivation method encountered: "
+ << nigori.custom_passphrase_key_derivation_method();
+ }
+}
+
+sync_pb::NigoriSpecifics CreateCustomPassphraseNigori(
+ const syncer::KeyParams& params) {
+ syncer::KeyDerivationMethod method = params.derivation_params.method();
+
+ sync_pb::NigoriSpecifics nigori;
+ nigori.set_keybag_is_frozen(true);
+ nigori.set_keystore_migration_time(1U);
+ nigori.set_encrypt_everything(true);
+ nigori.set_passphrase_type(sync_pb::NigoriSpecifics::CUSTOM_PASSPHRASE);
+ nigori.set_custom_passphrase_key_derivation_method(
+ EnumKeyDerivationMethodToProto(method));
+
+ std::string encoded_salt;
+ switch (method) {
+ case syncer::KeyDerivationMethod::PBKDF2_HMAC_SHA1_1003:
+ // Nothing to do; no further information needs to be extracted from
+ // Nigori.
+ break;
+ case syncer::KeyDerivationMethod::SCRYPT_8192_8_11:
+ base::Base64Encode(params.derivation_params.scrypt_salt(), &encoded_salt);
+ nigori.set_custom_passphrase_key_derivation_salt(encoded_salt);
+ break;
+ case syncer::KeyDerivationMethod::UNSUPPORTED:
+ ADD_FAILURE()
+ << "Unsupported method in KeyParams, cannot construct Nigori.";
+ break;
+ }
+
+ // Nigori also contains a keybag, which is an encrypted collection of all keys
+ // that the data might be encrypted with. To create it, we construct a
+ // cryptographer, add our key to it, and use GetKeys() to dump it to the
+ // keybag (in encrypted form). So, in our case, the keybag is simply the
+ // passphrase-derived key encrypted with itself. Note that this is usually
+ // also the case during normal Sync operation, and so the keybag from Nigori
+ // only helps the encryption machinery to know if a given key is correct (e.g.
+ // checking if a user's passphrase is correct is done by trying to decrypt the
+ // keybag using a key derived from that passphrase). However, in some migrated
+ // states, the keybag might also additionally contain an old, pre-migration
+ // key.
+ syncer::SystemEncryptor encryptor;
+ syncer::Cryptographer cryptographer(&encryptor);
+ bool add_key_result = cryptographer.AddKey(params);
+ DCHECK(add_key_result);
+ bool get_keys_result =
+ cryptographer.GetKeys(nigori.mutable_encryption_keybag());
+ DCHECK(get_keys_result);
+
+ return nigori;
+}
+
+sync_pb::EntitySpecifics GetEncryptedBookmarkEntitySpecifics(
+ const sync_pb::BookmarkSpecifics& bookmark_specifics,
+ const syncer::KeyParams& key_params) {
+ sync_pb::EntitySpecifics new_specifics;
+
+ sync_pb::EntitySpecifics wrapped_entity_specifics;
+ *wrapped_entity_specifics.mutable_bookmark() = bookmark_specifics;
+ syncer::SystemEncryptor encryptor;
+ syncer::Cryptographer cryptographer(&encryptor);
+ bool add_key_result = cryptographer.AddKey(key_params);
+ DCHECK(add_key_result);
+ bool encrypt_result = cryptographer.Encrypt(
+ wrapped_entity_specifics, new_specifics.mutable_encrypted());
+ DCHECK(encrypt_result);
+
+ new_specifics.mutable_bookmark()->set_title("encrypted");
+ new_specifics.mutable_bookmark()->set_url("encrypted");
+
+ return new_specifics;
+}
+
+void SetNigoriInFakeServer(fake_server::FakeServer* fake_server,
+ const sync_pb::NigoriSpecifics& nigori) {
+ std::string nigori_entity_id =
+ fake_server->GetTopLevelPermanentItemId(syncer::NIGORI);
+ ASSERT_NE(nigori_entity_id, "");
+ sync_pb::EntitySpecifics nigori_entity_specifics;
+ *nigori_entity_specifics.mutable_nigori() = nigori;
+ fake_server->ModifyEntitySpecifics(nigori_entity_id, nigori_entity_specifics);
+}
+
+} // namespace encryption_helper
+
+ServerNigoriChecker::ServerNigoriChecker(
+ browser_sync::ProfileSyncService* service,
+ fake_server::FakeServer* fake_server,
+ syncer::PassphraseType expected_passphrase_type)
+ : SingleClientStatusChangeChecker(service),
+ fake_server_(fake_server),
+ expected_passphrase_type_(expected_passphrase_type) {}
+
+bool ServerNigoriChecker::IsExitConditionSatisfied() {
+ std::vector<sync_pb::SyncEntity> nigori_entities =
+ fake_server_->GetPermanentSyncEntitiesByModelType(syncer::NIGORI);
+ EXPECT_LE(nigori_entities.size(), 1U);
+ return !nigori_entities.empty() &&
+ syncer::ProtoPassphraseTypeToEnum(
+ nigori_entities[0].specifics().nigori().passphrase_type()) ==
+ expected_passphrase_type_;
+}
+
+std::string ServerNigoriChecker::GetDebugMessage() const {
+ return "Waiting for a Nigori node with the proper passphrase type to become "
+ "available on the server.";
+}
+
+PassphraseRequiredStateChecker::PassphraseRequiredStateChecker(
+ browser_sync::ProfileSyncService* service,
+ bool desired_state)
+ : SingleClientStatusChangeChecker(service), desired_state_(desired_state) {}
+
+bool PassphraseRequiredStateChecker::IsExitConditionSatisfied() {
+ return service()->IsPassphraseRequiredForDecryption() == desired_state_;
+}
+
+std::string PassphraseRequiredStateChecker::GetDebugMessage() const {
+ return "Waiting until decryption passphrase is " +
+ std::string(desired_state_ ? "required" : "not required");
+}
diff --git a/chrome/browser/sync/test/integration/encryption_helper.h b/chrome/browser/sync/test/integration/encryption_helper.h
new file mode 100644
index 0000000..5b560c1
--- /dev/null
+++ b/chrome/browser/sync/test/integration/encryption_helper.h
@@ -0,0 +1,79 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_SYNC_TEST_INTEGRATION_ENCRYPTION_HELPER_H_
+#define CHROME_BROWSER_SYNC_TEST_INTEGRATION_ENCRYPTION_HELPER_H_
+
+#include <string>
+
+#include "chrome/browser/sync/test/integration/single_client_status_change_checker.h"
+#include "components/sync/base/cryptographer.h"
+#include "components/sync/protocol/nigori_specifics.pb.h"
+#include "components/sync/test/fake_server/fake_server.h"
+
+namespace encryption_helper {
+
+// Given a |fake_server|, fetches its Nigori node and writes it to the
+// proto pointed to by |nigori|. Returns false if the server does not contain
+// exactly one Nigori node.
+bool GetServerNigori(fake_server::FakeServer* fake_server,
+ sync_pb::NigoriSpecifics* nigori);
+
+// Given a |fake_server|, sets the Nigori instance stored in it to |nigori|.
+void SetNigoriInFakeServer(fake_server::FakeServer* fake_server,
+ const sync_pb::NigoriSpecifics& nigori);
+
+// Given a |nigori| with CUSTOM_PASSPHRASE passphrase type, initializes the
+// given |cryptographer| with the key described in it. Since the key inside the
+// Nigori is encrypted (by design), the provided |passphrase| will be used to
+// decrypt it. This function will fail the test (using ASSERT) if the Nigori is
+// not a custom passphrase one, or if the key cannot be decrypted.
+void InitCustomPassphraseCryptographerFromNigori(
+ const sync_pb::NigoriSpecifics& nigori,
+ syncer::Cryptographer* cryptographer,
+ const std::string& passphrase);
+
+// Returns an EntitySpecifics containing encrypted data corresponding to the
+// provided BookmarkSpecifics and encrypted using the given |key_params|.
+sync_pb::EntitySpecifics GetEncryptedBookmarkEntitySpecifics(
+ const sync_pb::BookmarkSpecifics& specifics,
+ const syncer::KeyParams& key_params);
+
+// Creates a NigoriSpecifics that describes encryption using a custom passphrase
+// with the given key parameters.
+sync_pb::NigoriSpecifics CreateCustomPassphraseNigori(
+ const syncer::KeyParams& params);
+
+} // namespace encryption_helper
+
+// Checker used to block until a Nigori with a given passphrase type is
+// available on the server.
+class ServerNigoriChecker : public SingleClientStatusChangeChecker {
+ public:
+ ServerNigoriChecker(browser_sync::ProfileSyncService* service,
+ fake_server::FakeServer* fake_server,
+ syncer::PassphraseType expected_passphrase_type);
+
+ bool IsExitConditionSatisfied() override;
+ std::string GetDebugMessage() const override;
+
+ private:
+ fake_server::FakeServer* fake_server_;
+ syncer::PassphraseType expected_passphrase_type_;
+};
+
+// Checker used to block until Sync requires or stops requiring a passphrase.
+class PassphraseRequiredStateChecker : public SingleClientStatusChangeChecker {
+ public:
+ PassphraseRequiredStateChecker(browser_sync::ProfileSyncService* service,
+ bool desired_state);
+
+ bool IsExitConditionSatisfied() override;
+ std::string GetDebugMessage() const override;
+
+ private:
+ bool desired_state_;
+};
+
+#endif // CHROME_BROWSER_SYNC_TEST_INTEGRATION_ENCRYPTION_HELPER_H_
diff --git a/chrome/browser/sync/test/integration/profile_sync_service_harness.cc b/chrome/browser/sync/test/integration/profile_sync_service_harness.cc
index 5814613..5e99091f 100644
--- a/chrome/browser/sync/test/integration/profile_sync_service_harness.cc
+++ b/chrome/browser/sync/test/integration/profile_sync_service_harness.cc
@@ -182,7 +182,7 @@
#endif // !OS_CHROMEOS
bool ProfileSyncServiceHarness::SetupSync() {
- bool result = SetupSync(syncer::UserSelectableTypes(), false);
+ bool result = SetupSync(syncer::UserSelectableTypes());
if (!result) {
LOG(ERROR) << profile_debug_name_ << ": SetupSync failed. Syncer status:\n"
<< GetServiceStatus();
@@ -193,7 +193,9 @@
}
bool ProfileSyncServiceHarness::SetupSyncForClearingServerData() {
- bool result = SetupSync(syncer::UserSelectableTypes(), true);
+ bool result = SetupSyncImpl(syncer::UserSelectableTypes(),
+ /*skip_passphrase_verification=*/true,
+ /*encryption_passphrase=*/base::nullopt);
if (!result) {
LOG(ERROR) << profile_debug_name_
<< ": SetupSyncForClear failed. Syncer status:\n"
@@ -204,8 +206,47 @@
return result;
}
-bool ProfileSyncServiceHarness::SetupSync(syncer::ModelTypeSet synced_datatypes,
- bool skip_passphrase_verification) {
+bool ProfileSyncServiceHarness::SetupSync(
+ syncer::ModelTypeSet synced_datatypes) {
+ return SetupSyncImpl(synced_datatypes, /*skip_passphrase_verification=*/false,
+ /*encryption_passphrase=*/base::nullopt);
+}
+
+bool ProfileSyncServiceHarness::SetupSyncWithEncryptionPassphrase(
+ syncer::ModelTypeSet synced_datatypes,
+ const std::string& passphrase) {
+ return SetupSyncImpl(synced_datatypes, /*skip_passphrase_verification=*/false,
+ passphrase);
+}
+
+bool ProfileSyncServiceHarness::SetupSyncWithDecryptionPassphrase(
+ syncer::ModelTypeSet synced_datatypes,
+ const std::string& passphrase) {
+ if (!SetupSyncImpl(synced_datatypes, /*skip_passphrase_verification=*/true,
+ /*encryption_passphrase=*/base::nullopt)) {
+ return false;
+ }
+
+ DVLOG(1) << "Setting decryption passphrase.";
+ if (!service_->SetDecryptionPassphrase(passphrase)) {
+ // This is not a fatal failure, as some tests intentionally pass an
+ // incorrect passphrase. If this happens, Sync will be set up but will have
+ // encountered cryptographer errors for the passphrase-encrypted datatypes.
+ LOG(INFO) << "SetDecryptionPassphrase() failed.";
+ }
+ // Since SetupSyncImpl() was called with skip_passphrase_verification == true,
+ // it will not have called FinishSyncSetup(). FinishSyncSetup() is in charge
+ // of calling ProfileSyncService::SetFirstSetupComplete(), and without that,
+ // Sync will still be in setup mode and Sync-the-feature will be disabled.
+ // Therefore, we call FinishSyncSetup() here explicitly.
+ FinishSyncSetup();
+ return true;
+}
+
+bool ProfileSyncServiceHarness::SetupSyncImpl(
+ syncer::ModelTypeSet synced_datatypes,
+ bool skip_passphrase_verification,
+ const base::Optional<std::string>& encryption_passphrase) {
DCHECK(!profile_->IsLegacySupervised())
<< "SetupSync should not be used for legacy supervised users.";
@@ -243,6 +284,12 @@
service()->OnUserChoseDatatypes(sync_everything, synced_datatypes);
}
+ if (encryption_passphrase.has_value()) {
+ service()->SetEncryptionPassphrase(
+ encryption_passphrase.value(),
+ syncer::SyncService::PassphraseType::EXPLICIT);
+ }
+
// Notify ProfileSyncService that we are done with configuration.
if (skip_passphrase_verification) {
sync_blocker_.reset();
diff --git a/chrome/browser/sync/test/integration/profile_sync_service_harness.h b/chrome/browser/sync/test/integration/profile_sync_service_harness.h
index dff401d..f38d1d5 100644
--- a/chrome/browser/sync/test/integration/profile_sync_service_harness.h
+++ b/chrome/browser/sync/test/integration/profile_sync_service_harness.h
@@ -11,6 +11,7 @@
#include "base/compiler_specific.h"
#include "base/macros.h"
+#include "base/optional.h"
#include "components/browser_sync/profile_sync_service.h"
#include "components/sync/base/model_type.h"
#include "components/sync/engine/cycle/sync_cycle_snapshot.h"
@@ -62,11 +63,21 @@
// StopSyncService(), StartSyncService() directly after.
bool SetupSyncForClearingServerData();
- // Both SetupSync and SetupSyncForClearingServerData call into this method.
- // Same as the above method, but enables sync only for the datatypes contained
- // in |synced_datatypes|.
- bool SetupSync(syncer::ModelTypeSet synced_datatypes,
- bool skip_passphrase_verification = false);
+ // Enables and configures sync only for the given |synced_datatypes|. Returns
+ // true only after sync has been fully initialized and authenticated, and we
+ // are ready to process changes.
+ bool SetupSync(syncer::ModelTypeSet synced_datatypes);
+
+ // Same as SetupSync(), but also sets the given encryption passphrase during
+ // setup.
+ bool SetupSyncWithEncryptionPassphrase(syncer::ModelTypeSet synced_datatypes,
+ const std::string& passphrase);
+
+ // Same as SetupSync(), but also sets the given decryption passphrase during
+ // setup. If the passphrase is incorrect, this method will still return true
+ // and Sync will be operational but with undecryptable datatypes disabled.
+ bool SetupSyncWithDecryptionPassphrase(syncer::ModelTypeSet synced_datatypes,
+ const std::string& passphrase);
// Signals that sync setup is complete, and that PSS may begin syncing.
// Typically SetupSync does this automatically, but if that returned false,
@@ -146,6 +157,16 @@
const std::string& password,
SigninType signin_type);
+ // If |encryption_passphrase| has a value, it will be set during setup. If
+ // not, no custom passphrase will be set. If |skip_passphrase_verification| is
+ // true and Sync requires a passphrase, FinishSyncSetup() will not be called,
+ // in order to give the caller a chance to provide the passphrase using
+ // SetDecryptionPassphrase(). After that, the caller needs to call
+ // FinishSyncSetup() manually.
+ bool SetupSyncImpl(syncer::ModelTypeSet synced_datatypes,
+ bool skip_passphrase_verification,
+ const base::Optional<std::string>& encryption_passphrase);
+
// Gets detailed status from |service_| in pretty-printable form.
std::string GetServiceStatus();
diff --git a/chrome/browser/sync/test/integration/single_client_custom_passphrase_sync_test.cc b/chrome/browser/sync/test/integration/single_client_custom_passphrase_sync_test.cc
new file mode 100644
index 0000000..d9d59a95
--- /dev/null
+++ b/chrome/browser/sync/test/integration/single_client_custom_passphrase_sync_test.cc
@@ -0,0 +1,364 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/sync/test/integration/bookmarks_helper.h"
+#include "chrome/browser/sync/test/integration/encryption_helper.h"
+#include "chrome/browser/sync/test/integration/sync_test.h"
+#include "components/browser_sync/profile_sync_service.h"
+#include "components/sync/base/cryptographer.h"
+#include "components/sync/base/passphrase_enums.h"
+#include "components/sync/base/sync_base_switches.h"
+#include "components/sync/base/system_encryptor.h"
+#include "components/sync/engine/sync_engine_switches.h"
+
+namespace {
+
+using bookmarks_helper::AddURL;
+using bookmarks_helper::CreateBookmarkServerEntity;
+using encryption_helper::CreateCustomPassphraseNigori;
+using encryption_helper::GetEncryptedBookmarkEntitySpecifics;
+using encryption_helper::GetServerNigori;
+using encryption_helper::InitCustomPassphraseCryptographerFromNigori;
+using encryption_helper::SetNigoriInFakeServer;
+using fake_server::FakeServer;
+using sync_pb::EncryptedData;
+using sync_pb::NigoriSpecifics;
+using sync_pb::SyncEntity;
+using syncer::Cryptographer;
+using syncer::KeyDerivationParams;
+using syncer::KeyParams;
+using syncer::LoopbackServerEntity;
+using syncer::ModelType;
+using syncer::ModelTypeSet;
+using syncer::PassphraseType;
+using syncer::ProtoPassphraseTypeToEnum;
+using syncer::SyncService;
+using syncer::SystemEncryptor;
+
+class DatatypeCommitCountingFakeServerObserver : public FakeServer::Observer {
+ public:
+ explicit DatatypeCommitCountingFakeServerObserver(FakeServer* fake_server)
+ : fake_server_(fake_server) {
+ fake_server->AddObserver(this);
+ }
+
+ void OnCommit(const std::string& committer_id,
+ ModelTypeSet committed_model_types) override {
+ for (ModelType type : committed_model_types) {
+ ++datatype_commit_counts_[type];
+ }
+ }
+
+ int GetCommitCountForDatatype(ModelType type) {
+ return datatype_commit_counts_[type];
+ }
+
+ ~DatatypeCommitCountingFakeServerObserver() override {
+ fake_server_->RemoveObserver(this);
+ }
+
+ private:
+ FakeServer* fake_server_;
+ std::map<syncer::ModelType, int> datatype_commit_counts_;
+};
+
+// These tests use a gray-box testing approach to verify that the data committed
+// to the server is encrypted properly, and that properly-encrypted data from
+// the server is successfully decrypted by the client. They also verify that the
+// key derivation methods are set, read and handled properly. They do not,
+// however, directly ensure that two clients syncing through the same account
+// will be able to access each others' data in the presence of a custom
+// passphrase. For this, a separate two-client test will be used.
+//
+// TODO(davidovic): Add two-client tests and update the above comment.
+class SingleClientCustomPassphraseSyncTest : public SyncTest {
+ public:
+ SingleClientCustomPassphraseSyncTest() : SyncTest(SINGLE_CLIENT) {}
+ ~SingleClientCustomPassphraseSyncTest() override {}
+
+ // Waits until the given set of bookmarks appears on the server, encrypted
+ // according to the server-side Nigori and with the given passphrase.
+ bool WaitForEncryptedServerBookmarks(
+ const std::vector<ServerBookmarksEqualityChecker::ExpectedBookmark>&
+ expected_bookmarks,
+ const std::string& passphrase) {
+ auto cryptographer = CreateCryptographerFromServerNigori(passphrase);
+ return ServerBookmarksEqualityChecker(GetSyncService(), GetFakeServer(),
+ expected_bookmarks,
+ cryptographer.get())
+ .Wait();
+ }
+
+ // Waits until the given set of bookmarks appears on the server, encrypted
+ // with the precise KeyParams given.
+ bool WaitForEncryptedServerBookmarks(
+ const std::vector<ServerBookmarksEqualityChecker::ExpectedBookmark>&
+ expected_bookmarks,
+ const KeyParams& key_params) {
+ auto cryptographer = CreateCryptographerWithKeyParams(key_params);
+ return ServerBookmarksEqualityChecker(GetSyncService(), GetFakeServer(),
+ expected_bookmarks,
+ cryptographer.get())
+ .Wait();
+ }
+
+ bool WaitForUnencryptedServerBookmarks(
+ const std::vector<ServerBookmarksEqualityChecker::ExpectedBookmark>&
+ expected_bookmarks) {
+ return ServerBookmarksEqualityChecker(GetSyncService(), GetFakeServer(),
+ expected_bookmarks,
+ /*cryptographer=*/nullptr)
+ .Wait();
+ }
+
+ bool WaitForNigori(PassphraseType expected_passphrase_type) {
+ return ServerNigoriChecker(GetSyncService(), GetFakeServer(),
+ expected_passphrase_type)
+ .Wait();
+ }
+
+ bool WaitForPassphraseRequiredState(bool desired_state) {
+ return PassphraseRequiredStateChecker(GetSyncService(), desired_state)
+ .Wait();
+ }
+
+ bool WaitForClientBookmarkWithTitle(std::string title) {
+ return BookmarksTitleChecker(/*profile_index=*/0, title,
+ /*expected_count=*/1)
+ .Wait();
+ }
+
+ browser_sync::ProfileSyncService* GetSyncService() {
+ return SyncTest::GetSyncService(0);
+ }
+
+ // When the cryptographer is initialized with a passphrase, it uses the key
+ // derivation method and other parameters from the server-side Nigori. Thus,
+ // checking that the server-side Nigori contains the desired key derivation
+ // method and checking that the server-side encrypted bookmarks can be
+ // decrypted using a cryptographer initialized with this function is
+ // sufficient to determine that a given key derivation method is being
+ // correctly used for encryption.
+ std::unique_ptr<Cryptographer> CreateCryptographerFromServerNigori(
+ const std::string& passphrase) {
+ NigoriSpecifics nigori;
+ EXPECT_TRUE(GetServerNigori(GetFakeServer(), &nigori));
+ EXPECT_EQ(ProtoPassphraseTypeToEnum(nigori.passphrase_type()),
+ PassphraseType::CUSTOM_PASSPHRASE);
+ auto cryptographer = std::make_unique<Cryptographer>(&system_encryptor_);
+ InitCustomPassphraseCryptographerFromNigori(nigori, cryptographer.get(),
+ passphrase);
+ return cryptographer;
+ }
+
+ // A cryptographer initialized with the given KeyParams has not "seen" the
+ // server-side Nigori, and so any data decryptable by such a cryptographer
+ // does not depend on external info.
+ std::unique_ptr<Cryptographer> CreateCryptographerWithKeyParams(
+ const KeyParams& key_params) {
+ auto cryptographer = std::make_unique<Cryptographer>(&system_encryptor_);
+ cryptographer->AddKey(key_params);
+ return cryptographer;
+ }
+
+ void SetScryptFeaturesState(bool force_disabled,
+ bool use_for_new_passphrases) {
+ std::vector<base::Feature> enabled_features;
+ std::vector<base::Feature> disabled_features;
+ if (force_disabled) {
+ enabled_features.push_back(
+ switches::kSyncForceDisableScryptForCustomPassphrase);
+ } else {
+ disabled_features.push_back(
+ switches::kSyncForceDisableScryptForCustomPassphrase);
+ }
+ if (use_for_new_passphrases) {
+ enabled_features.push_back(
+ switches::kSyncUseScryptForNewCustomPassphrases);
+ } else {
+ disabled_features.push_back(
+ switches::kSyncUseScryptForNewCustomPassphrases);
+ }
+ feature_list_.InitWithFeatures(enabled_features, disabled_features);
+ }
+
+ void InjectEncryptedServerBookmark(const std::string& title,
+ const GURL& url,
+ const KeyParams& key_params) {
+ std::unique_ptr<LoopbackServerEntity> server_entity =
+ CreateBookmarkServerEntity(title, url);
+ server_entity->SetSpecifics(GetEncryptedBookmarkEntitySpecifics(
+ server_entity->GetSpecifics().bookmark(), key_params));
+ GetFakeServer()->InjectEntity(std::move(server_entity));
+ }
+
+ private:
+ SystemEncryptor system_encryptor_;
+ base::test::ScopedFeatureList feature_list_;
+
+ DISALLOW_COPY_AND_ASSIGN(SingleClientCustomPassphraseSyncTest);
+};
+
+IN_PROC_BROWSER_TEST_F(SingleClientCustomPassphraseSyncTest,
+ CommitsEncryptedData) {
+ SetEncryptionPassphraseForClient(/*index=*/0, "hunter2");
+ ASSERT_TRUE(SetupSync());
+
+ ASSERT_TRUE(
+ AddURL(/*profile=*/0, "Hello world", GURL("https://google.com/")));
+ ASSERT_TRUE(
+ AddURL(/*profile=*/0, "Bookmark #2", GURL("https://example.com/")));
+ ASSERT_TRUE(WaitForNigori(PassphraseType::CUSTOM_PASSPHRASE));
+
+ EXPECT_TRUE(WaitForEncryptedServerBookmarks(
+ {{"Hello world", GURL("https://google.com/")},
+ {"Bookmark #2", GURL("https://example.com/")}},
+ /*passphrase=*/"hunter2"));
+}
+
+IN_PROC_BROWSER_TEST_F(SingleClientCustomPassphraseSyncTest,
+ CommitsEncryptedDataUsingPbkdf2WhenScryptDisabled) {
+ SetScryptFeaturesState(/*force_disabled=*/false,
+ /*use_for_new_passphrases=*/false);
+ SetEncryptionPassphraseForClient(/*index=*/0, "hunter2");
+ ASSERT_TRUE(SetupSync());
+ ASSERT_TRUE(AddURL(/*profile=*/0, "PBKDF2 encrypted",
+ GURL("https://google.com/pbkdf2-encrypted")));
+
+ ASSERT_TRUE(WaitForNigori(PassphraseType::CUSTOM_PASSPHRASE));
+ NigoriSpecifics nigori;
+ EXPECT_TRUE(GetServerNigori(GetFakeServer(), &nigori));
+ EXPECT_EQ(nigori.custom_passphrase_key_derivation_method(),
+ sync_pb::NigoriSpecifics::PBKDF2_HMAC_SHA1_1003);
+ EXPECT_TRUE(WaitForEncryptedServerBookmarks(
+ {{"PBKDF2 encrypted", GURL("https://google.com/pbkdf2-encrypted")}},
+ /*passphrase=*/"hunter2"));
+}
+
+IN_PROC_BROWSER_TEST_F(SingleClientCustomPassphraseSyncTest,
+ CommitsEncryptedDataUsingScryptWhenScryptEnabled) {
+ SetScryptFeaturesState(/*force_disabled=*/false,
+ /*use_for_new_passphrases=*/true);
+ SetEncryptionPassphraseForClient(/*index=*/0, "hunter2");
+ ASSERT_TRUE(SetupSync());
+
+ ASSERT_TRUE(AddURL(/*profile=*/0, "scrypt encrypted",
+ GURL("https://google.com/scrypt-encrypted")));
+
+ ASSERT_TRUE(WaitForNigori(PassphraseType::CUSTOM_PASSPHRASE));
+ NigoriSpecifics nigori;
+ EXPECT_TRUE(GetServerNigori(GetFakeServer(), &nigori));
+ EXPECT_EQ(nigori.custom_passphrase_key_derivation_method(),
+ sync_pb::NigoriSpecifics::SCRYPT_8192_8_11);
+ EXPECT_TRUE(WaitForEncryptedServerBookmarks(
+ {{"scrypt encrypted", GURL("https://google.com/scrypt-encrypted")}},
+ /*passphrase=*/"hunter2"));
+}
+
+IN_PROC_BROWSER_TEST_F(SingleClientCustomPassphraseSyncTest,
+ CanDecryptPbkdf2KeyEncryptedData) {
+ KeyParams key_params = {KeyDerivationParams::CreateForPbkdf2(), "hunter2"};
+ InjectEncryptedServerBookmark("PBKDF2-encrypted bookmark",
+ GURL("http://example.com/doesnt-matter"),
+ key_params);
+ SetNigoriInFakeServer(GetFakeServer(),
+ CreateCustomPassphraseNigori(key_params));
+ SetDecryptionPassphraseForClient(/*index=*/0, "hunter2");
+
+ ASSERT_TRUE(SetupSync());
+ EXPECT_TRUE(WaitForPassphraseRequiredState(/*desired_state=*/false));
+
+ EXPECT_TRUE(WaitForClientBookmarkWithTitle("PBKDF2-encrypted bookmark"));
+}
+
+IN_PROC_BROWSER_TEST_F(SingleClientCustomPassphraseSyncTest,
+ CanDecryptScryptKeyEncryptedDataWhenScryptNotDisabled) {
+ SetScryptFeaturesState(/*force_disabled=*/false,
+ /*used_for_new_passphrases_=*/false);
+ KeyParams key_params = {
+ KeyDerivationParams::CreateForScrypt("someConstantSalt"), "hunter2"};
+ InjectEncryptedServerBookmark("scypt-encrypted bookmark",
+ GURL("http://example.com/doesnt-matter"),
+ key_params);
+ SetNigoriInFakeServer(GetFakeServer(),
+ CreateCustomPassphraseNigori(key_params));
+ SetDecryptionPassphraseForClient(/*index=*/0, "hunter2");
+
+ ASSERT_TRUE(SetupSync());
+ EXPECT_TRUE(WaitForPassphraseRequiredState(/*desired_state=*/false));
+
+ EXPECT_TRUE(WaitForClientBookmarkWithTitle("scypt-encrypted bookmark"));
+}
+
+IN_PROC_BROWSER_TEST_F(SingleClientCustomPassphraseSyncTest,
+ CannotDecryptScryptKeyEncryptedDataWhenScryptDisabled) {
+ KeyParams key_params = {
+ KeyDerivationParams::CreateForScrypt("someConstantSalt"), "hunter2"};
+ sync_pb::NigoriSpecifics nigori = CreateCustomPassphraseNigori(key_params);
+ InjectEncryptedServerBookmark("scypt-encrypted bookmark",
+ GURL("http://example.com/doesnt-matter"),
+ key_params);
+ // Can only set feature state now because creating a Nigori and injecting an
+ // encrypted bookmark both require key derivation using scrypt.
+ SetScryptFeaturesState(/*force_disabled=*/true,
+ /*used_for_new_passphrases_=*/false);
+ SetNigoriInFakeServer(GetFakeServer(), nigori);
+ SetDecryptionPassphraseForClient(/*index=*/0, "hunter2");
+
+ ASSERT_TRUE(SetupSync());
+
+ EXPECT_TRUE(WaitForPassphraseRequiredState(/*desired_state=*/true));
+}
+
+IN_PROC_BROWSER_TEST_F(SingleClientCustomPassphraseSyncTest,
+ DoesNotLeakUnencryptedData) {
+ SetScryptFeaturesState(/*force_disabled=*/false,
+ /*use_for_new_passphrases=*/false);
+ SetEncryptionPassphraseForClient(/*index=*/0, "hunter2");
+ DatatypeCommitCountingFakeServerObserver observer(GetFakeServer());
+ ASSERT_TRUE(SetupSync());
+
+ ASSERT_TRUE(AddURL(/*profile=*/0, "Should be encrypted",
+ GURL("https://google.com/encrypted")));
+
+ ASSERT_TRUE(WaitForNigori(PassphraseType::CUSTOM_PASSPHRASE));
+ // If WaitForEncryptedServerBookmarks() succeeds, that means that a
+ // cryptographer initialized with only the key params was able to decrypt the
+ // data, so the data must be encrypted using a passphrase-derived key (and not
+ // e.g. a keystore key), because that cryptographer has never seen the
+ // server-side Nigori. Furthermore, if a bookmark commit has happened only
+ // once, we are certain that no bookmarks other than those we've verified to
+ // be encrypted have been committed.
+ EXPECT_TRUE(WaitForEncryptedServerBookmarks(
+ {{"Should be encrypted", GURL("https://google.com/encrypted")}},
+ {KeyDerivationParams::CreateForPbkdf2(), "hunter2"}));
+ EXPECT_EQ(observer.GetCommitCountForDatatype(syncer::BOOKMARKS), 1);
+}
+
+IN_PROC_BROWSER_TEST_F(SingleClientCustomPassphraseSyncTest,
+ ReencryptsDataWhenPassphraseIsSet) {
+ SetScryptFeaturesState(/*force_disabled=*/false,
+ /*use_for_new_passphrases=*/false);
+ ASSERT_TRUE(SetupSync());
+ ASSERT_TRUE(WaitForNigori(PassphraseType::KEYSTORE_PASSPHRASE));
+ ASSERT_TRUE(AddURL(/*profile=*/0, "Re-encryption is great",
+ GURL("https://google.com/re-encrypted")));
+ std::vector<ServerBookmarksEqualityChecker::ExpectedBookmark> expected = {
+ {"Re-encryption is great", GURL("https://google.com/re-encrypted")}};
+ ASSERT_TRUE(WaitForUnencryptedServerBookmarks(expected));
+
+ GetSyncService()->SetEncryptionPassphrase(
+ "hunter2", SyncService::PassphraseType::EXPLICIT);
+ ASSERT_TRUE(WaitForNigori(PassphraseType::CUSTOM_PASSPHRASE));
+
+ // If WaitForEncryptedServerBookmarks() succeeds, that means that a
+ // cryptographer initialized with only the key params was able to decrypt the
+ // data, so the data must be encrypted using a passphrase-derived key (and not
+ // e.g. the previous keystore key which was stored in the Nigori keybag),
+ // because that cryptographer has never seen the server-side Nigori.
+ EXPECT_TRUE(WaitForEncryptedServerBookmarks(
+ expected, {KeyDerivationParams::CreateForPbkdf2(), "hunter2"}));
+}
+
+} // namespace
diff --git a/chrome/browser/sync/test/integration/sync_test.cc b/chrome/browser/sync/test/integration/sync_test.cc
index 328a8ef8..cc0e9f0 100644
--- a/chrome/browser/sync/test/integration/sync_test.cc
+++ b/chrome/browser/sync/test/integration/sync_test.cc
@@ -643,6 +643,20 @@
fake_server_->RemoveObserver(fake_server_invalidation_services_[index]);
}
+void SyncTest::SetEncryptionPassphraseForClient(int index,
+ const std::string& passphrase) {
+ // Must be called before client initialization.
+ DCHECK(clients_.empty());
+ client_encryption_passphrases_[index] = passphrase;
+}
+
+void SyncTest::SetDecryptionPassphraseForClient(int index,
+ const std::string& passphrase) {
+ // Must be called before client initialization.
+ DCHECK(clients_.empty());
+ client_decryption_passphrases_[index] = passphrase;
+}
+
void SyncTest::SetupMockGaiaResponsesForProfile(Profile* profile) {
ChromeSigninClient* signin_client = static_cast<ChromeSigninClient*>(
ChromeSigninClientFactory::GetForProfile(profile));
@@ -782,8 +796,35 @@
// Sync each of the profiles.
for (; clientIndex < num_clients_; clientIndex++) {
+ ProfileSyncServiceHarness* client = GetClient(clientIndex);
DVLOG(1) << "Setting up " << clientIndex << " client";
- if (!GetClient(clientIndex)->SetupSync()) {
+
+ auto decryption_passphrase_it =
+ client_decryption_passphrases_.find(clientIndex);
+ auto encryption_passphrase_it =
+ client_encryption_passphrases_.find(clientIndex);
+ bool decryption_passphrase_provided =
+ (decryption_passphrase_it != client_decryption_passphrases_.end());
+ bool encryption_passphrase_provided =
+ (encryption_passphrase_it != client_encryption_passphrases_.end());
+ if (decryption_passphrase_provided && encryption_passphrase_provided) {
+ LOG(FATAL) << "Both an encryption and decryption passphrase were "
+ "provided for the client. This is disallowed.";
+ return false;
+ }
+
+ bool setup_succeeded;
+ if (encryption_passphrase_provided) {
+ setup_succeeded = client->SetupSyncWithEncryptionPassphrase(
+ syncer::UserSelectableTypes(), encryption_passphrase_it->second);
+ } else if (decryption_passphrase_provided) {
+ setup_succeeded = client->SetupSyncWithDecryptionPassphrase(
+ syncer::UserSelectableTypes(), decryption_passphrase_it->second);
+ } else {
+ setup_succeeded = client->SetupSync(syncer::UserSelectableTypes());
+ }
+
+ if (!setup_succeeded) {
LOG(FATAL) << "SetupSync() failed.";
return false;
}
diff --git a/chrome/browser/sync/test/integration/sync_test.h b/chrome/browser/sync/test/integration/sync_test.h
index ec474802..41e8ea51 100644
--- a/chrome/browser/sync/test/integration/sync_test.h
+++ b/chrome/browser/sync/test/integration/sync_test.h
@@ -292,6 +292,23 @@
// Stops notificatinos being sent to a client.
void DisableNotificationsForClient(int index);
+ // Sets a decryption passphrase to be used for a client. The passphrase will
+ // be provided to the client during initialization, before Sync starts. It is
+ // an error to provide both a decryption and encryption passphrases for one
+ // client.
+ void SetDecryptionPassphraseForClient(int index,
+ const std::string& passphrase);
+
+ // Sets an explicit encryption passphrase to be used for a client. The
+ // passphrase will be set for the client during initialization, before Sync
+ // starts. An encryption passphrase can be also enabled after initialization,
+ // but using this method ensures that Sync is never enabled when there is no
+ // passphrase, which allows tests to check for unencrypted data leaks. It is
+ // an error to provide both a decryption and encryption passphrases for one
+ // client.
+ void SetEncryptionPassphraseForClient(int index,
+ const std::string& passphrase);
+
// Sets up fake responses for kClientLoginUrl, kIssueAuthTokenUrl,
// kGetUserInfoUrl and kSearchDomainCheckUrl in order to mock out calls to
// GAIA servers.
@@ -443,6 +460,12 @@
// profile with the server.
std::vector<std::unique_ptr<ProfileSyncServiceHarness>> clients_;
+ // Mapping from client indexes to encryption passphrases to use for them.
+ std::map<int, std::string> client_encryption_passphrases_;
+
+ // Mapping from client indexes to decryption passphrases to use for them.
+ std::map<int, std::string> client_decryption_passphrases_;
+
// A set of objects to listen for commit activity and broadcast notifications
// of this activity to its peer sync clients.
std::vector<std::unique_ptr<P2PInvalidationForwarder>>
diff --git a/chrome/test/BUILD.gn b/chrome/test/BUILD.gn
index 0af9ed07..2b76bbc 100644
--- a/chrome/test/BUILD.gn
+++ b/chrome/test/BUILD.gn
@@ -5188,6 +5188,8 @@
"../browser/sync/test/integration/dictionary_helper.h",
"../browser/sync/test/integration/dictionary_load_observer.cc",
"../browser/sync/test/integration/dictionary_load_observer.h",
+ "../browser/sync/test/integration/encryption_helper.cc",
+ "../browser/sync/test/integration/encryption_helper.h",
"../browser/sync/test/integration/extension_settings_helper.cc",
"../browser/sync/test/integration/extension_settings_helper.h",
"../browser/sync/test/integration/extensions_helper.cc",
@@ -5323,6 +5325,7 @@
"../browser/sync/test/integration/single_client_apps_sync_test.cc",
"../browser/sync/test/integration/single_client_arc_package_sync_test.cc",
"../browser/sync/test/integration/single_client_bookmarks_sync_test.cc",
+ "../browser/sync/test/integration/single_client_custom_passphrase_sync_test.cc",
"../browser/sync/test/integration/single_client_dictionary_sync_test.cc",
"../browser/sync/test/integration/single_client_directory_sync_test.cc",
"../browser/sync/test/integration/single_client_extensions_sync_test.cc",
diff --git a/components/sync/engine_impl/loopback_server/loopback_server.cc b/components/sync/engine_impl/loopback_server/loopback_server.cc
index 29c3977..5a0be40 100644
--- a/components/sync/engine_impl/loopback_server/loopback_server.cc
+++ b/components/sync/engine_impl/loopback_server/loopback_server.cc
@@ -185,6 +185,7 @@
if (!top_level_entity) {
return false;
}
+ top_level_permanent_item_ids_[model_type] = top_level_entity->GetId();
SaveEntity(std::move(top_level_entity));
if (model_type == syncer::BOOKMARKS) {
@@ -200,6 +201,15 @@
return true;
}
+std::string LoopbackServer::GetTopLevelPermanentItemId(
+ syncer::ModelType model_type) {
+ auto it = top_level_permanent_item_ids_.find(model_type);
+ if (it == top_level_permanent_item_ids_.end()) {
+ return std::string();
+ }
+ return it->second;
+}
+
void LoopbackServer::UpdateEntityVersion(LoopbackServerEntity* entity) {
entity->SetVersion(++version_);
}
@@ -506,6 +516,22 @@
return sync_entities;
}
+std::vector<sync_pb::SyncEntity>
+LoopbackServer::GetPermanentSyncEntitiesByModelType(ModelType model_type) {
+ DCHECK(thread_checker_.CalledOnValidThread());
+ std::vector<sync_pb::SyncEntity> sync_entities;
+ for (const auto& kv : entities_) {
+ const LoopbackServerEntity& entity = *kv.second;
+ if (!entity.IsDeleted() && entity.IsPermanent() &&
+ entity.GetModelType() == model_type) {
+ sync_pb::SyncEntity sync_entity;
+ entity.SerializeAsProto(&sync_entity);
+ sync_entities.push_back(sync_entity);
+ }
+ }
+ return sync_entities;
+}
+
std::unique_ptr<base::DictionaryValue>
LoopbackServer::GetEntitiesAsDictionaryValue() {
DCHECK(thread_checker_.CalledOnValidThread());
diff --git a/components/sync/engine_impl/loopback_server/loopback_server.h b/components/sync/engine_impl/loopback_server/loopback_server.h
index 407f2c6e..680e9a1 100644
--- a/components/sync/engine_impl/loopback_server/loopback_server.h
+++ b/components/sync/engine_impl/loopback_server/loopback_server.h
@@ -88,6 +88,10 @@
// Inserts the default permanent items in |entities_|.
bool CreateDefaultPermanentItems();
+ // Returns an empty string if no top-level permanent item of the given type
+ // was created.
+ std::string GetTopLevelPermanentItemId(syncer::ModelType model_type);
+
std::string GenerateNewKeystoreKey() const;
// Saves a |entity| to |entities_|.
@@ -127,14 +131,20 @@
std::string GetStoreBirthday() const;
// Returns all entities stored by the server of the given |model_type|.
- // This method is only used in tests.
+ // Permanent entities are excluded. This method is only used in tests.
std::vector<sync_pb::SyncEntity> GetSyncEntitiesByModelType(
syncer::ModelType model_type);
+ // Returns a list of permanent entities of the given |model_type|. This method
+ // is only used in tests.
+ std::vector<sync_pb::SyncEntity> GetPermanentSyncEntitiesByModelType(
+ syncer::ModelType model_type);
+
// Creates a DicionaryValue representation of all entities present in the
// server. The dictionary keys are the strings generated by ModelTypeToString
// and the values are ListValues containing StringValue versions of entity
- // names. Used by test to verify the contents of the server state.
+ // names. Permanent entities are excluded. Used by test to verify the contents
+ // of the server state.
std::unique_ptr<base::DictionaryValue> GetEntitiesAsDictionaryValue();
// Modifies the entity on the server with the given |id|. The entity's
@@ -189,6 +199,7 @@
int64_t store_birthday_;
EntityMap entities_;
+ std::map<ModelType, std::string> top_level_permanent_item_ids_;
std::vector<std::string> keystore_keys_;
// The file used to store the local sync data.
diff --git a/components/sync/test/fake_server/fake_server.cc b/components/sync/test/fake_server/fake_server.cc
index c2b55fc..e2f9c67c 100644
--- a/components/sync/test/fake_server/fake_server.cc
+++ b/components/sync/test/fake_server/fake_server.cc
@@ -306,6 +306,18 @@
return loopback_server_->GetSyncEntitiesByModelType(model_type);
}
+std::vector<sync_pb::SyncEntity>
+FakeServer::GetPermanentSyncEntitiesByModelType(ModelType model_type) {
+ DCHECK(thread_checker_.CalledOnValidThread());
+ return loopback_server_->GetPermanentSyncEntitiesByModelType(model_type);
+}
+
+std::string FakeServer::GetTopLevelPermanentItemId(
+ syncer::ModelType model_type) {
+ DCHECK(thread_checker_.CalledOnValidThread());
+ return loopback_server_->GetTopLevelPermanentItemId(model_type);
+}
+
void FakeServer::InjectEntity(std::unique_ptr<LoopbackServerEntity> entity) {
DCHECK(thread_checker_.CalledOnValidThread());
DCHECK(entity->GetModelType() != syncer::AUTOFILL_WALLET_DATA)
diff --git a/components/sync/test/fake_server/fake_server.h b/components/sync/test/fake_server/fake_server.h
index fdede785..6a8c2928 100644
--- a/components/sync/test/fake_server/fake_server.h
+++ b/components/sync/test/fake_server/fake_server.h
@@ -81,10 +81,21 @@
// Returns all entities stored by the server of the given |model_type|.
// This method returns SyncEntity protocol buffer objects (instead of
// LoopbackServerEntity) so that callers can inspect datatype-specific data
- // (e.g., the URL of a session tab).
+ // (e.g., the URL of a session tab). Permanent entities are excluded.
std::vector<sync_pb::SyncEntity> GetSyncEntitiesByModelType(
syncer::ModelType model_type);
+ // Returns all permanent entities stored by the server of the given
+ // |model_type|. This method returns SyncEntity protocol buffer objects
+ // (instead of LoopbackServerEntity) so that callers can inspect
+ // datatype-specific data (e.g., the URL of a session tab).
+ std::vector<sync_pb::SyncEntity> GetPermanentSyncEntitiesByModelType(
+ syncer::ModelType model_type);
+
+ // Returns an empty string if no top-level permanent item of the given type
+ // was created.
+ std::string GetTopLevelPermanentItemId(syncer::ModelType model_type);
+
// Adds |entity| to the server's collection of entities. This method makes no
// guarantees that the added entity will result in successful server
// operations.