chromium/chrome/browser/ash/sync/sync_explicit_passphrase_client_ash_unittest.cc

// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/ash/sync/sync_explicit_passphrase_client_ash.h"

#include "base/test/task_environment.h"
#include "base/test/test_future.h"
#include "chromeos/crosapi/mojom/sync.mojom-forward.h"
#include "chromeos/crosapi/mojom/sync.mojom-test-utils.h"
#include "components/sync/chromeos/explicit_passphrase_mojo_utils.h"
#include "components/sync/engine/nigori/key_derivation_params.h"
#include "components/sync/engine/nigori/nigori.h"
#include "components/sync/test/mock_sync_service.h"
#include "components/sync/test/sync_user_settings_mock.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace ash {

namespace {

using testing::Eq;
using testing::NotNull;
using testing::Return;

std::unique_ptr<syncer::Nigori> MakeTestNigoriKey() {
  return syncer::Nigori::CreateByDerivation(
      syncer::KeyDerivationParams::CreateForPbkdf2(), /*password=*/"password");
}

crosapi::mojom::NigoriKeyPtr MakeTestMojoNigoriKey() {
  std::unique_ptr<syncer::Nigori> nigori_key = MakeTestNigoriKey();
  return syncer::NigoriToMojo(*nigori_key);
}

class TestSyncExplicitPassphraseClientObserver
    : public crosapi::mojom::
          SyncExplicitPassphraseClientObserverInterceptorForTesting {
 public:
  TestSyncExplicitPassphraseClientObserver() = default;
  TestSyncExplicitPassphraseClientObserver(
      const TestSyncExplicitPassphraseClientObserver& other) = delete;
  TestSyncExplicitPassphraseClientObserver& operator=(
      const TestSyncExplicitPassphraseClientObserver& other) = delete;
  ~TestSyncExplicitPassphraseClientObserver() override = default;

  void Observe(SyncExplicitPassphraseClientAsh* client) {
    auto remote = receiver_.BindNewPipeAndPassRemote();
    client->AddObserver(std::move(remote));
  }

  int GetNumOnPassphraseRequiredCalls() const {
    return num_on_passphrase_required_calls_;
  }

  int GetNumOnPassphraseAvailableCalls() const {
    return num_on_passphrase_available_calls_;
  }

  // crosapi::mojom::SyncExplicitPassphraseClientObserverInterceptorForTesting
  // overrides:
  SyncExplicitPassphraseClientObserver* GetForwardingInterface() override {
    return this;
  }

  void OnPassphraseRequired() override { num_on_passphrase_required_calls_++; }

  void OnPassphraseAvailable() override {
    num_on_passphrase_available_calls_++;
  }

 private:
  int num_on_passphrase_required_calls_ = 0;
  int num_on_passphrase_available_calls_ = 0;

  mojo::Receiver<crosapi::mojom::SyncExplicitPassphraseClientObserver>
      receiver_{this};
};

class SyncExplicitPassphraseClientAshTest : public testing::Test {
 public:
  SyncExplicitPassphraseClientAshTest() : client_(&sync_service_) {
    sync_account_info_.gaia = "user1";
  }

  SyncExplicitPassphraseClientAshTest(
      const SyncExplicitPassphraseClientAshTest&) = delete;
  SyncExplicitPassphraseClientAshTest& operator=(
      const SyncExplicitPassphraseClientAshTest&) = delete;
  ~SyncExplicitPassphraseClientAshTest() override = default;

  void SetUp() override {
    ON_CALL(sync_service_, GetAccountInfo())
        .WillByDefault(Return(sync_account_info_));
    client_.BindReceiver(client_remote_.BindNewPipeAndPassReceiver());
  }

  SyncExplicitPassphraseClientAsh* client() { return &client_; }

  crosapi::mojom::NigoriKeyPtr GetDecryptionNigoriKey(
      crosapi::mojom::AccountKeyPtr key) const {
    base::test::TestFuture<crosapi::mojom::NigoriKeyPtr> future;
    client_remote_->GetDecryptionNigoriKey(std::move(key),
                                           future.GetCallback());
    return future.Take();
  }

  syncer::MockSyncService* sync_service() { return &sync_service_; }

  syncer::SyncUserSettingsMock* sync_user_settings() {
    return sync_service_.GetMockUserSettings();
  }

  const CoreAccountInfo& sync_account_info() const {
    return sync_account_info_;
  }

  crosapi::mojom::AccountKeyPtr GetSyncingAccountKey() const {
    crosapi::mojom::AccountKeyPtr account_key =
        crosapi::mojom::AccountKey::New();
    account_key->id = sync_account_info_.gaia;
    account_key->account_type = crosapi::mojom::AccountType::kGaia;
    return account_key;
  }

 private:
  base::test::SingleThreadTaskEnvironment task_environment_;

  testing::NiceMock<syncer::MockSyncService> sync_service_;

  SyncExplicitPassphraseClientAsh client_;
  mojo::Remote<crosapi::mojom::SyncExplicitPassphraseClient> client_remote_;

  CoreAccountInfo sync_account_info_;
};

TEST_F(SyncExplicitPassphraseClientAshTest, ShouldGetDecryptionKey) {
  ON_CALL(*sync_user_settings(), GetExplicitPassphraseDecryptionNigoriKey())
      .WillByDefault(MakeTestNigoriKey);

  crosapi::mojom::NigoriKeyPtr nigori_key =
      GetDecryptionNigoriKey(GetSyncingAccountKey());
  ASSERT_FALSE(nigori_key.is_null());

  crosapi::mojom::NigoriKeyPtr expected_nigori_key = MakeTestMojoNigoriKey();
  EXPECT_THAT(nigori_key->encryption_key,
              Eq(expected_nigori_key->encryption_key));
  EXPECT_THAT(nigori_key->mac_key, Eq(expected_nigori_key->mac_key));
}

TEST_F(SyncExplicitPassphraseClientAshTest,
       ShouldValidateAccountWhenGettingDecryptionKey) {
  crosapi::mojom::AccountKeyPtr wrong_account_key =
      crosapi::mojom::AccountKey::New();
  wrong_account_key->id = "user2";
  wrong_account_key->account_type = crosapi::mojom::AccountType::kGaia;

  crosapi::mojom::NigoriKeyPtr nigori_key =
      GetDecryptionNigoriKey(std::move(wrong_account_key));
  EXPECT_TRUE(nigori_key.is_null());
}

TEST_F(SyncExplicitPassphraseClientAshTest,
       ShouldHandleAbsenseOfKeyWhenGettingDecryptionKey) {
  crosapi::mojom::NigoriKeyPtr nigori_key =
      GetDecryptionNigoriKey(GetSyncingAccountKey());
  EXPECT_TRUE(nigori_key.is_null());
}

TEST_F(SyncExplicitPassphraseClientAshTest, ShouldSetDecryptionKey) {
  EXPECT_CALL(*sync_user_settings(),
              SetExplicitPassphraseDecryptionNigoriKey(NotNull()));
  client()->SetDecryptionNigoriKey(GetSyncingAccountKey(),
                                   MakeTestMojoNigoriKey());
}

TEST_F(SyncExplicitPassphraseClientAshTest,
       ShouldValidateAccountWhenSettingDecryptionKey) {
  crosapi::mojom::AccountKeyPtr wrong_account_key =
      crosapi::mojom::AccountKey::New();
  wrong_account_key->id = "user2";
  wrong_account_key->account_type = crosapi::mojom::AccountType::kGaia;

  EXPECT_CALL(*sync_user_settings(), SetExplicitPassphraseDecryptionNigoriKey)
      .Times(0);
  client()->SetDecryptionNigoriKey(std::move(wrong_account_key),
                                   MakeTestMojoNigoriKey());
}

TEST_F(SyncExplicitPassphraseClientAshTest,
       ShouldHandleNullKeyWhenSettingDecryptionKey) {
  EXPECT_CALL(*sync_user_settings(), SetExplicitPassphraseDecryptionNigoriKey)
      .Times(0);
  client()->SetDecryptionNigoriKey(GetSyncingAccountKey(), nullptr);
}

TEST_F(SyncExplicitPassphraseClientAshTest,
       ShouldHandleInvalidKeyWhenSettingDecryptionKey) {
  EXPECT_CALL(*sync_user_settings(), SetExplicitPassphraseDecryptionNigoriKey)
      .Times(0);

  crosapi::mojom::NigoriKeyPtr mojo_nigori_key =
      crosapi::mojom::NigoriKey::New();
  // Nigori deserialization fails with a wrong key length.
  mojo_nigori_key->encryption_key = {1, 2, 3};
  mojo_nigori_key->mac_key = {1, 2, 3};

  EXPECT_CALL(*sync_user_settings(), SetExplicitPassphraseDecryptionNigoriKey)
      .Times(0);
  client()->SetDecryptionNigoriKey(GetSyncingAccountKey(),
                                   std::move(mojo_nigori_key));
}

TEST_F(SyncExplicitPassphraseClientAshTest,
       ShouldNotifyObserverAboutPassphraseRequired) {
  TestSyncExplicitPassphraseClientObserver observer;
  observer.Observe(client());

  ASSERT_THAT(observer.GetNumOnPassphraseAvailableCalls(), Eq(0));
  ASSERT_THAT(observer.GetNumOnPassphraseRequiredCalls(), Eq(0));

  // Mimic entering passphrase required state.
  ON_CALL(*sync_user_settings(), IsPassphraseRequired())
      .WillByDefault(Return(true));
  client()->OnStateChanged(sync_service());
  client()->FlushMojoForTesting();
  EXPECT_THAT(observer.GetNumOnPassphraseAvailableCalls(), Eq(0));
  EXPECT_THAT(observer.GetNumOnPassphraseRequiredCalls(), Eq(1));
}

TEST_F(SyncExplicitPassphraseClientAshTest,
       ShouldNotifyObserverAboutPassphraseAvailable) {
  TestSyncExplicitPassphraseClientObserver observer;
  observer.Observe(client());

  ASSERT_THAT(observer.GetNumOnPassphraseAvailableCalls(), Eq(0));
  ASSERT_THAT(observer.GetNumOnPassphraseRequiredCalls(), Eq(0));

  // Mimic passphrase being entered by the user.
  ON_CALL(*sync_user_settings(), IsUsingExplicitPassphrase())
      .WillByDefault(Return(true));
  ON_CALL(*sync_user_settings(), IsPassphraseRequired())
      .WillByDefault(Return(false));
  client()->OnStateChanged(sync_service());
  client()->FlushMojoForTesting();
  EXPECT_THAT(observer.GetNumOnPassphraseAvailableCalls(), Eq(1));
  EXPECT_THAT(observer.GetNumOnPassphraseRequiredCalls(), Eq(0));
}

TEST_F(SyncExplicitPassphraseClientAshTest,
       ShouldNotifyNewObserverAboutPassphraseRequired) {
  // Mimic entering passphrase required state.
  ON_CALL(*sync_user_settings(), IsPassphraseRequired())
      .WillByDefault(Return(true));
  client()->OnStateChanged(sync_service());
  client()->FlushMojoForTesting();

  // Add new observer and ensure it's notified about passphrase required state.
  TestSyncExplicitPassphraseClientObserver observer;
  observer.Observe(client());
  client()->FlushMojoForTesting();
  EXPECT_THAT(observer.GetNumOnPassphraseAvailableCalls(), Eq(0));
  EXPECT_THAT(observer.GetNumOnPassphraseRequiredCalls(), Eq(1));
}

TEST_F(SyncExplicitPassphraseClientAshTest,
       ShouldNotifyNewObserverAboutPassphraseAvailable) {
  // Mimic passphrase being entered by the user.
  ON_CALL(*sync_user_settings(), IsUsingExplicitPassphrase())
      .WillByDefault(Return(true));
  ON_CALL(*sync_user_settings(), IsPassphraseRequired())
      .WillByDefault(Return(false));
  client()->OnStateChanged(sync_service());
  client()->FlushMojoForTesting();

  // Add new observer and ensure it's notified about passphrase available state.
  TestSyncExplicitPassphraseClientObserver observer;
  observer.Observe(client());
  client()->FlushMojoForTesting();
  EXPECT_THAT(observer.GetNumOnPassphraseAvailableCalls(), Eq(1));
  EXPECT_THAT(observer.GetNumOnPassphraseRequiredCalls(), Eq(0));
}

}  // namespace
}  // namespace ash