chromium/chrome/browser/sync/test/lacros/trusted_vault_keys_sharing_lacros_browsertest.cc

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

#include "base/files/file_path.h"
#include "base/test/scoped_feature_list.h"
#include "chrome/browser/ash/trusted_vault/trusted_vault_backend_service_ash.h"
#include "chrome/browser/ash/trusted_vault/trusted_vault_backend_service_factory_ash.h"
#include "chrome/browser/sync/sync_ui_util.h"
#include "chrome/browser/sync/test/integration/encryption_helper.h"
#include "chrome/browser/sync/test/integration/passwords_helper.h"
#include "chrome/browser/sync/test/integration/sync_test.h"
#include "chrome/browser/ui/webui/trusted_vault/trusted_vault_dialog_delegate.h"
#include "chrome/common/chrome_constants.h"
#include "chrome/grit/generated_resources.h"
#include "chromeos/crosapi/mojom/trusted_vault.mojom.h"
#include "chromeos/lacros/lacros_service.h"
#include "components/sync/service/sync_service_impl.h"
#include "components/sync/test/fake_server_nigori_helper.h"
#include "components/sync/test/nigori_test_utils.h"
#include "components/trusted_vault/features.h"
#include "components/trusted_vault/test/fake_crosapi_trusted_vault_backend.h"
#include "components/trusted_vault/test/fake_crosapi_trusted_vault_backend_service.h"
#include "components/trusted_vault/test/fake_trusted_vault_client.h"
#include "content/public/browser/browser_main_parts.h"
#include "content/public/test/browser_test.h"
#include "google_apis/gaia/gaia_switches.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/views/test/widget_test.h"
#include "ui/views/widget/any_widget_observer.h"

namespace {

using testing::ElementsAre;
using testing::Eq;
using testing::SizeIs;

MATCHER_P4(StatusLabelsMatch,
           message_type,
           status_label_string_id,
           button_string_id,
           action_type,
           "") {
  if (arg.message_type != message_type) {
    *result_listener << "Wrong message type, expected: "
                     << static_cast<int>(message_type)
                     << ", got: " << static_cast<int>(arg.message_type);
    return false;
  }
  if (arg.status_label_string_id != status_label_string_id) {
    *result_listener << "Wrong status label, expected: "
                     << status_label_string_id
                     << ", got: " << arg.status_label_string_id;
    return false;
  }
  if (arg.button_string_id != button_string_id) {
    *result_listener << "Wrong button string, expected: " << button_string_id
                     << ", got: " << arg.button_string_id;
    return false;
  }
  if (arg.action_type != action_type) {
    *result_listener << "Wrong action type, expected: "
                     << static_cast<int>(action_type)
                     << ", got: " << static_cast<int>(arg.action_type);
    return false;
  }
  return true;
}

class TrustedVaultKeysSharingLacrosBrowserTest : public SyncTest {
 public:
  TrustedVaultKeysSharingLacrosBrowserTest() : SyncTest(SINGLE_CLIENT) {
    override_features_.InitAndEnableFeature(
        trusted_vault::kChromeOSTrustedVaultClientShared);
    fake_crosapi_backend_service_ =
        std::make_unique<trusted_vault::FakeCrosapiTrustedVaultBackendService>(
            &chrome_sync_trusted_vault_client_ash_,
            &passkeys_trusted_vault_client_ash_);
  }

  ~TrustedVaultKeysSharingLacrosBrowserTest() override = default;

  base::FilePath GetProfileBaseName(int index) override {
    // TrustedVault keys sharing is enabled only for the main profile, so
    // SyncTest should setup sync using it.
    CHECK_EQ(index, 0);
    return base::FilePath(chrome::kInitialProfile);
  }

  // This test replaces production TrustedVaultBackend Crosapi interface with
  // a fake one. It needs to be done before connection between Ash
  // TrustedVaultBackend and Lacros TrustedVaultClient is established (during
  // creation of Lacros profile), but after LacrosService is initialized. Thus
  // relying on CreatedBrowserMainParts().
  void CreatedBrowserMainParts(
      content::BrowserMainParts* browser_main_parts) override {
    SyncTest::CreatedBrowserMainParts(browser_main_parts);

    // Replace the production TrustedVaultBackendService Crosapi with a fake
    // for testing.
    chromeos::LacrosService::Get()->InjectRemoteForTesting(
        fake_crosapi_backend_service_->BindNewPipeAndPassRemote());
  }

  void SetUpCommandLine(base::CommandLine* command_line) override {
    SyncTest::SetUpCommandLine(command_line);

    ASSERT_TRUE(embedded_test_server()->InitializeAndListen());
    const GURL& base_url = embedded_test_server()->base_url();
    command_line->AppendSwitchASCII(switches::kGaiaUrl, base_url.spec());
  }

  void SetUpOnMainThread() override {
    SyncTest::SetUpOnMainThread();
    trusted_vault_widget_shown_waiter_ =
        std::make_unique<views::NamedWidgetShownWaiter>(
            views::test::AnyWidgetTestPasskey{},
            TrustedVaultDialogDelegate::kWidgetName);
  }

  bool SetupSyncAndTrustedVaultFakes() {
    if (!SetupSync()) {
      return false;
    }

    const CoreAccountInfo primary_account_info =
        GetSyncService(0)->GetAccountInfo();

    encryption_helper::SetupFakeTrustedVaultPages(
        primary_account_info.gaia, kTestTrustedVaultKey,
        /*trusted_vault_key_version=*/1, kTestRecoveryMethodPublicKey,
        embedded_test_server());

    embedded_test_server()->StartAcceptingConnections();

    fake_crosapi_backend_service_->chrome_sync_backend().SetPrimaryAccountInfo(
        GetSyncService(0)->GetAccountInfo());

    return true;
  }

  trusted_vault::FakeTrustedVaultClient& trusted_vault_client_ash() {
    return chrome_sync_trusted_vault_client_ash_;
  }

  bool WaitForTrustedVaultReauthCompletion() {
    CHECK(trusted_vault_widget_shown_waiter_);
    views::Widget* trusted_vault_widged =
        trusted_vault_widget_shown_waiter_->WaitIfNeededAndGet();
    views::test::WidgetDestroyedWaiter(trusted_vault_widged).Wait();
    return true;
  }

 protected:
  const std::vector<uint8_t> kTestTrustedVaultKey = {1, 2, 3};
  const std::vector<uint8_t> kTestRecoveryMethodPublicKey = {1, 2, 3, 4};

 private:
  base::test::ScopedFeatureList override_features_;

  std::unique_ptr<views::NamedWidgetShownWaiter>
      trusted_vault_widget_shown_waiter_;

  trusted_vault::FakeTrustedVaultClient chrome_sync_trusted_vault_client_ash_{
      /*auto_complete_requests=*/true};
  trusted_vault::FakeTrustedVaultClient passkeys_trusted_vault_client_ash_{
      /*auto_complete_requests=*/true};
  std::unique_ptr<trusted_vault::FakeCrosapiTrustedVaultBackendService>
      fake_crosapi_backend_service_;
};

IN_PROC_BROWSER_TEST_F(TrustedVaultKeysSharingLacrosBrowserTest,
                       ShouldFetchKeys) {
  ASSERT_TRUE(SetupSyncAndTrustedVaultFakes());

  // Mimic that trusted vault key is already available in Ash.
  trusted_vault_client_ash().StoreKeys(GetSyncService(0)->GetAccountInfo().gaia,
                                       {kTestTrustedVaultKey},
                                       /*last_key_version=*/1);

  // Inject trusted vault Nigori server-side.
  fake_server::SetNigoriInFakeServer(
      syncer::BuildTrustedVaultNigoriSpecifics({kTestTrustedVaultKey}),
      GetFakeServer());

  // Inject a password encrypted with trusted vault key server-side.
  const syncer::KeyParamsForTesting trusted_vault_key_params =
      syncer::TrustedVaultKeyParamsForTesting(kTestTrustedVaultKey);
  const password_manager::PasswordForm password_form =
      passwords_helper::CreateTestPasswordForm(1);
  passwords_helper::InjectEncryptedServerPassword(
      password_form, trusted_vault_key_params.password,
      trusted_vault_key_params.derivation_params, GetFakeServer());

  // Lacros should be able to fetch keys from Ash and decrypt the passwords.
  EXPECT_TRUE(PasswordFormsChecker(0, {password_form}).Wait());
}

IN_PROC_BROWSER_TEST_F(TrustedVaultKeysSharingLacrosBrowserTest,
                       ShouldAcceptAndStoreTrustedVaultKeysFromTheWeb) {
  ASSERT_TRUE(SetupSyncAndTrustedVaultFakes());

  // Inject trusted vault Nigori server-side.
  fake_server::SetNigoriInFakeServer(
      syncer::BuildTrustedVaultNigoriSpecifics({kTestTrustedVaultKey}),
      GetFakeServer());

  // No keys available in Ash, eventually Lacros should enter
  // TrustedVaultKeyRequired state.
  EXPECT_TRUE(TrustedVaultKeyRequiredStateChecker(GetSyncService(0),
                                                  /*desired_state=*/true)
                  .Wait());

  // Verify that error has been shown to the user.
  // 1. In profile menu:
  EXPECT_THAT(
      GetAvatarSyncErrorType(GetProfile(0)),
      Eq(AvatarSyncErrorType::kTrustedVaultKeyMissingForPasswordsError));
  // 2. In settings:
  EXPECT_THAT(GetSyncStatusLabels(GetProfile(0)),
              StatusLabelsMatch(
                  SyncStatusMessageType::kPasswordsOnlySyncError,
                  IDS_SETTINGS_EMPTY_STRING, IDS_SYNC_STATUS_NEEDS_KEYS_BUTTON,
                  SyncStatusActionType::kRetrieveTrustedVaultKeys));

  // Simulate that user clicks on the error. Normally that triggers a reauth,
  // but this test bypass it (opens page that mimics the reauth completion and
  // closes automatically).
  OpenDialogForSyncKeyRetrieval(
      GetProfile(0), syncer::TrustedVaultUserActionTriggerForUMA::kProfileMenu);
  EXPECT_TRUE(WaitForTrustedVaultReauthCompletion());

  // Wait until trusted vault keys not required anymore, due to implementation
  // details this means that keys should be available in both Ash and Lacros.
  EXPECT_TRUE(TrustedVaultKeyRequiredStateChecker(GetSyncService(0),
                                                  /*desired_state=*/false)
                  .Wait());

  // Verify that errors disappeared from the UI.
  EXPECT_FALSE(GetAvatarSyncErrorType(GetProfile(0)).has_value());
  EXPECT_THAT(GetSyncStatusLabels(GetProfile(0)),
              StatusLabelsMatch(
                  SyncStatusMessageType::kSynced, IDS_SYNC_ACCOUNT_SYNCING,
                  IDS_SETTINGS_EMPTY_STRING, SyncStatusActionType::kNoAction));

  // Verify that keys are available in Ash.
  EXPECT_THAT(trusted_vault_client_ash().GetStoredKeys(
                  GetSyncService(0)->GetAccountInfo().gaia),
              ElementsAre(kTestTrustedVaultKey));
}

IN_PROC_BROWSER_TEST_F(TrustedVaultKeysSharingLacrosBrowserTest,
                       ShouldAddRecoveryMethodFromWeb) {
  ASSERT_TRUE(SetupSyncAndTrustedVaultFakes());

  // Inject trusted vault Nigori server-side and make it decryptable, degraded
  // recoverability is absent from the UI otherwise.
  fake_server::SetNigoriInFakeServer(
      syncer::BuildTrustedVaultNigoriSpecifics({kTestTrustedVaultKey}),
      GetFakeServer());
  trusted_vault_client_ash().StoreKeys(GetSyncService(0)->GetAccountInfo().gaia,
                                       {kTestTrustedVaultKey},
                                       /*last_key_version=*/1);

  // Enters degraded recoverability state.
  trusted_vault_client_ash().SetIsRecoveryMethodRequired(true);

  // Wait until Lacros is aware of degraded recoverability state.
  EXPECT_TRUE(
      TrustedVaultRecoverabilityDegradedStateChecker(GetSyncService(0), true)
          .Wait());

  // Verify that error has been shown to the user in profile menu:
  EXPECT_THAT(GetAvatarSyncErrorType(GetProfile(0)),
              Eq(AvatarSyncErrorType::
                     kTrustedVaultRecoverabilityDegradedForPasswordsError));
  // No errors expected in settings.
  EXPECT_THAT(GetSyncStatusLabels(GetProfile(0)),
              StatusLabelsMatch(
                  SyncStatusMessageType::kSynced, IDS_SYNC_ACCOUNT_SYNCING,
                  IDS_SETTINGS_EMPTY_STRING, SyncStatusActionType::kNoAction));

  // Simulate that user clicks on the error. Normally that triggers a reauth,
  // but this test bypass it (opens page that mimics the reauth completion and
  // closes automatically).
  OpenDialogForSyncKeyRecoverabilityDegraded(
      GetProfile(0), syncer::TrustedVaultUserActionTriggerForUMA::kProfileMenu);
  EXPECT_TRUE(WaitForTrustedVaultReauthCompletion());

  // Wait until degraded recoverability state is resolved.
  EXPECT_TRUE(
      TrustedVaultRecoverabilityDegradedStateChecker(GetSyncService(0), false)
          .Wait());

  // Verify that errors disappeared from the UI.
  EXPECT_FALSE(GetAvatarSyncErrorType(GetProfile(0)).has_value());

  // Verify that recovery method was added.
  const auto recovery_methods =
      trusted_vault_client_ash().server()->GetRecoveryMethods(
          GetSyncService(0)->GetAccountInfo().gaia);
  ASSERT_THAT(recovery_methods, SizeIs(1));
  EXPECT_THAT(recovery_methods[0].public_key, Eq(kTestRecoveryMethodPublicKey));
}

}  // namespace