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.