chromium/chrome/browser/ui/webui/ash/settings/integration_tests/os_settings_pin_setup_browsertest.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 "ash/constants/ash_features.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/public/cpp/in_session_auth_dialog_controller.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/test_future.h"
#include "base/values.h"
#include "chrome/browser/ash/login/quick_unlock/pin_storage_prefs.h"
#include "chrome/browser/ash/login/quick_unlock/quick_unlock_factory.h"
#include "chrome/browser/ash/login/quick_unlock/quick_unlock_storage.h"
#include "chrome/browser/ash/login/quick_unlock/quick_unlock_utils.h"
#include "chrome/browser/ash/profiles/profile_helper.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/webui/ash/settings/test_support/os_settings_lock_screen_browser_test_base.h"
#include "chrome/common/pref_names.h"
#include "chrome/test/data/webui/chromeos/settings/os_people_page/pin_settings_api.test-mojom-test-utils.h"
#include "chrome/test/data/webui/chromeos/settings/test_api.test-mojom-test-utils.h"
#include "chromeos/ash/components/osauth/impl/auth_hub_common.h"
#include "chromeos/ash/components/osauth/impl/auth_surface_registry.h"
#include "chromeos/ash/components/osauth/public/auth_engine_api.h"
#include "chromeos/ash/components/osauth/public/auth_parts.h"
#include "components/policy/core/browser/browser_policy_connector.h"
#include "components/policy/core/common/mock_configuration_policy_provider.h"
#include "components/policy/core/common/policy_map.h"
#include "components/policy/core/common/policy_types.h"
#include "components/prefs/pref_service.h"
#include "content/public/test/browser_test.h"

namespace ash::settings {

namespace {

// PINs to be used for checking PIN verification logic.
const char kFirstPin[] = "111111";
const char kSecondPin[] = "222222";
const char kIncorrectPin[] = "333333";

// PINs to be used for checking minimal/maximum length PINs.
const size_t kMinimumPinLengthForTest = 5;
const size_t kMaximumPinLengthForTest = 10;
const char kMinimumLengthPin[] = "11223";
const char kMaximumLengthPin[] = "1122334455";

// A weak PIN used to verify that a warning is displayed.
const char kWeakPin[] = "111111";

// Twelve digit PINs are allowed for autosubmit, but no more.
const char kMaximumLengthPinForAutosubmit[] = "321321321321";
const char kTooLongPinForAutosubmit[] = "3213213213213";

// Name and value of the metric that records authentication on the lock screen
// page.
const char kPinUnlockUmaHistogramName[] = "Settings.PinUnlockSetup";
const base::HistogramBase::Sample kChoosePinOrPassword = 2;
const base::HistogramBase::Sample kEnterPin = 3;
const base::HistogramBase::Sample kConfirmPin = 4;

}  // namespace

enum class PinType {
  kPrefs,
  kCryptohome,
};

// Tests PIN-related settings in the ChromeOS settings page.
class OSSettingsPinSetupTest : public OSSettingsLockScreenBrowserTestBase,
                               public testing::WithParamInterface<PinType> {
 public:
  OSSettingsPinSetupTest() : pin_type_(GetParam()) {
    switch (pin_type_) {
      case PinType::kPrefs:
        cryptohome_->set_supports_low_entropy_credentials(false);
        break;
      case PinType::kCryptohome:
        cryptohome_->set_supports_low_entropy_credentials(true);
        break;
    }
  }

  void SetUpInProcessBrowserTestFixture() override {
    OSSettingsLockScreenBrowserTestBase::SetUpInProcessBrowserTestFixture();

    // Override the policy provider for testing. The `ON_CALL` lines here are
    // necessary because something inside the policy stack expects those return
    // values.
    ON_CALL(provider_, IsInitializationComplete(testing::_))
        .WillByDefault(testing::Return(true));
    ON_CALL(provider_, IsFirstPolicyLoadComplete(testing::_))
        .WillByDefault(testing::Return(true));
    policy::BrowserPolicyConnector::SetPolicyProviderForTesting(&provider_);
  }

  PrefService& Prefs() {
    PrefService* service =
        ProfileHelper::Get()->GetProfileByAccountId(GetAccountId())->GetPrefs();
    CHECK(service);
    return *service;
  }

  bool GetPinAutoSubmitState() {
    return Prefs().GetBoolean(::prefs::kPinUnlockAutosubmitEnabled);
  }

  // Returns whether or not a PIN is configured in the backend.
  bool IsPinConfigured() {
    switch (pin_type_) {
      case PinType::kPrefs:
        return !Prefs().GetString(prefs::kQuickUnlockPinSecret).empty() &&
               !Prefs().GetString(prefs::kQuickUnlockPinSalt).empty();
      case PinType::kCryptohome:
        return cryptohome_->HasPinFactor(GetAccountId());
    }
  }

  void SetPinLocked() {
    switch (pin_type_) {
      case PinType::kPrefs: {
        quick_unlock::QuickUnlockStorage* qus =
            quick_unlock::QuickUnlockFactory::GetForAccountId(GetAccountId());
        CHECK(qus);
        quick_unlock::PinStoragePrefs* psp = qus->pin_storage_prefs();
        CHECK(psp);
        // Make sure to add enough unlock attempts so that PIN is locked out.
        for (int i = 0;
             i != quick_unlock::PinStoragePrefs::kMaximumUnlockAttempts; ++i) {
          psp->AddUnlockAttempt();
        }
        CHECK(!psp->IsPinAuthenticationAvailable(quick_unlock::Purpose::kAny));
        break;
      }
      case PinType::kCryptohome: {
        cryptohome_->SetPinLocked(GetAccountId(), true);
        break;
      }
    }
  }

  mojom::PinSettingsApiAsyncWaiter GoToPinSettings(
      mojom::LockScreenSettingsAsyncWaiter& lock_screen_settings) {
    pin_settings_remote_ = mojo::Remote(lock_screen_settings.GoToPinSettings());
    return mojom::PinSettingsApiAsyncWaiter(pin_settings_remote_.get());
  }

  void SetPinDisabledPolicy(bool disabled) {
    policy::PolicyMap policies;
    base::Value policy_value{disabled ? base::Value::List()
                                      : base::Value::List().Append("PIN")};

    policies.Set("QuickUnlockModeAllowlist", policy::POLICY_LEVEL_MANDATORY,
                 policy::POLICY_SCOPE_USER, policy::POLICY_SOURCE_CLOUD,
                 policy_value.Clone(),
                 /*external_data_fetcher=*/nullptr);

    policies.Set("WebAuthnFactors", policy::POLICY_LEVEL_MANDATORY,
                 policy::POLICY_SCOPE_USER, policy::POLICY_SOURCE_CLOUD,
                 policy_value.Clone(),
                 /*external_data_fetcher=*/nullptr);

    provider_.UpdateChromePolicy(policies);
  }

 private:
  PinType pin_type_;
  mojo::Remote<mojom::PinSettingsApi> pin_settings_remote_;
  testing::NiceMock<policy::MockConfigurationPolicyProvider> provider_;
};

INSTANTIATE_TEST_SUITE_P(All,
                         OSSettingsPinSetupTest,
                         testing::Values(PinType::kPrefs,
                                         PinType::kCryptohome));

// The test fixture for tests that are supposed to be run with the cryptohome
// backend only. We need a separate but essentially identical class here so
// that we can use INSTANTIATE_TEST_SUITE_P with a different set of
// test::Values.
class OSSettingsPinSetupCryptohomeOnlyTest : public OSSettingsPinSetupTest {};

INSTANTIATE_TEST_SUITE_P(All,
                         OSSettingsPinSetupCryptohomeOnlyTest,
                         testing::Values(PinType::kCryptohome));

// Tests that adding a PIN works.
IN_PROC_BROWSER_TEST_P(OSSettingsPinSetupTest, AddPin) {
  auto lock_screen_settings = OpenLockScreenSettingsAndAuthenticate();
  auto pin_settings = GoToPinSettings(lock_screen_settings);

  pin_settings.AssertHasPin(false);
  EXPECT_EQ(false, IsPinConfigured());

  pin_settings.SetPin(kFirstPin);

  pin_settings.AssertHasPin(true);
  EXPECT_EQ(true, IsPinConfigured());
}

// Tests that changing a PIN works.
IN_PROC_BROWSER_TEST_P(OSSettingsPinSetupTest, ChangePin) {
  auto lock_screen_settings = OpenLockScreenSettingsAndAuthenticate();
  auto pin_settings = GoToPinSettings(lock_screen_settings);
  pin_settings.SetPin(kFirstPin);
  pin_settings.AssertHasPin(true);
  EXPECT_EQ(true, IsPinConfigured());

  pin_settings.SetPin(kSecondPin);

  pin_settings.AssertHasPin(true);
  EXPECT_EQ(true, IsPinConfigured());
}

// Tests that removing a PIN works.
IN_PROC_BROWSER_TEST_P(OSSettingsPinSetupTest, RemovePin) {
  auto lock_screen_settings = OpenLockScreenSettingsAndAuthenticate();
  auto pin_settings = GoToPinSettings(lock_screen_settings);
  pin_settings.SetPin(kFirstPin);
  pin_settings.AssertHasPin(true);
  EXPECT_EQ(true, IsPinConfigured());

  pin_settings.RemovePin();

  EXPECT_EQ(false, IsPinConfigured());
  pin_settings.AssertHasPin(false);
}

// Tests that PIN changes are persistent over relaunching os-settings.
IN_PROC_BROWSER_TEST_P(OSSettingsPinSetupTest, PinPersists) {
  {
    auto lock_screen_settings = OpenLockScreenSettingsAndAuthenticate();
    auto pin_settings = GoToPinSettings(lock_screen_settings);
    pin_settings.SetPin(kFirstPin);

    pin_settings.AssertHasPin(true);
    EXPECT_EQ(true, IsPinConfigured());
  }

  auto lock_screen_settings = OpenLockScreenSettingsAndAuthenticate();
  auto pin_settings = GoToPinSettings(lock_screen_settings);

  pin_settings.AssertHasPin(true);
  EXPECT_EQ(true, IsPinConfigured());
}

// Tests that nothing is persisted when cancelling the PIN setup dialog after
// entering the PIN only once.
IN_PROC_BROWSER_TEST_P(OSSettingsPinSetupTest, SetPinButCancelConfirmation) {
  auto lock_screen_settings = OpenLockScreenSettingsAndAuthenticate();
  auto pin_settings = GoToPinSettings(lock_screen_settings);
  pin_settings.AssertHasPin(false);
  EXPECT_EQ(false, IsPinConfigured());

  pin_settings.SetPinButCancelConfirmation(kFirstPin);

  pin_settings.AssertHasPin(false);
  EXPECT_EQ(false, IsPinConfigured());
}

// Tests that nothing is persisted during setup when the PIN that is entered
// the second time for confirmation does not match the first PIN. We should
// record this in UMA though.
IN_PROC_BROWSER_TEST_P(OSSettingsPinSetupTest, SetPinButFailConfirmation) {
  auto lock_screen_settings = OpenLockScreenSettingsAndAuthenticate();
  auto pin_settings = GoToPinSettings(lock_screen_settings);
  pin_settings.AssertHasPin(false);
  EXPECT_EQ(false, IsPinConfigured());

  pin_settings.SetPinButFailConfirmation(kFirstPin, kIncorrectPin);

  pin_settings.AssertHasPin(false);
  EXPECT_EQ(false, IsPinConfigured());
}

// Tests that an error message is displayed when setting PIN fails.
IN_PROC_BROWSER_TEST_P(OSSettingsPinSetupCryptohomeOnlyTest,
                       AddPinButInternalError) {
  auto lock_screen_settings = OpenLockScreenSettingsAndAuthenticate();
  auto pin_settings = GoToPinSettings(lock_screen_settings);

  pin_settings.AssertHasPin(false);
  EXPECT_EQ(false, IsPinConfigured());

  cryptohome_->SetNextOperationError(
      FakeUserDataAuthClient::Operation::kAddAuthFactor,
      cryptohome::ErrorWrapper::CreateFromErrorCodeOnly(
          ::user_data_auth::CRYPTOHOME_ADD_CREDENTIALS_FAILED));
  pin_settings.SetPinButInternalError(kFirstPin);

  pin_settings.AssertHasPin(false);
  EXPECT_EQ(false, IsPinConfigured());
}

// Tests that PIN setup UI validates minimal pin lengths.
IN_PROC_BROWSER_TEST_P(OSSettingsPinSetupTest, MinimumPinLength) {
  Prefs().SetInteger(prefs::kPinUnlockMinimumLength, kMinimumPinLengthForTest);

  auto lock_screen_settings = OpenLockScreenSettingsAndAuthenticate();
  auto pin_settings = GoToPinSettings(lock_screen_settings);

  // Check that a minimum length PIN is accepted, but that the PIN obtained by
  // removing the last digit is rejected.
  std::string too_short_pin{kMinimumLengthPin};
  too_short_pin.pop_back();

  // SetPinButTooShort checks that a warning is displayed.
  pin_settings.SetPinButTooShort(std::move(too_short_pin), kMinimumLengthPin);
}

// Tests that PIN setup UI validates maximal pin lengths.
IN_PROC_BROWSER_TEST_P(OSSettingsPinSetupTest, MaximumPinLength) {
  Prefs().SetInteger(prefs::kPinUnlockMaximumLength, kMaximumPinLengthForTest);

  auto lock_screen_settings = OpenLockScreenSettingsAndAuthenticate();
  auto pin_settings = GoToPinSettings(lock_screen_settings);

  // Check that a maximum length PIN is accepted, but that the PIN obtained by
  // duplicating the last digit is rejected.
  std::string too_long_pin{kMaximumLengthPin};
  too_long_pin += too_long_pin.back();

  // SetPinButTooLong checks that a warning is displayed.
  pin_settings.SetPinButTooLong(std::move(too_long_pin), kMaximumLengthPin);
}

// Tests that a warning is displayed when setting up a weak PIN, but that it is
// still possible.
IN_PROC_BROWSER_TEST_P(OSSettingsPinSetupTest, WeakPinWarning) {
  auto lock_screen_settings = OpenLockScreenSettingsAndAuthenticate();
  auto pin_settings = GoToPinSettings(lock_screen_settings);

  // SetPinWithWarning checks that a warning is displayed.
  pin_settings.SetPinWithWarning(kWeakPin);

  pin_settings.AssertHasPin(true);
  EXPECT_EQ(true, IsPinConfigured());
}

// Tests that the PIN setup dialog handles key events appropriately.
IN_PROC_BROWSER_TEST_P(OSSettingsPinSetupTest, PressKeysInPinSetupDialog) {
  auto lock_screen_settings = OpenLockScreenSettingsAndAuthenticate();
  auto pin_settings = GoToPinSettings(lock_screen_settings);

  pin_settings.CheckPinSetupDialogKeyInput();
}

// Tests that all relevant metrics are recorded when cancelling PIN setup
// immediately.
IN_PROC_BROWSER_TEST_P(OSSettingsPinSetupTest, SetPinMetricsCancelImmediately) {
  auto lock_screen_settings = OpenLockScreenSettingsAndAuthenticate();
  auto pin_settings = GoToPinSettings(lock_screen_settings);
  base::HistogramTester histograms;

  pin_settings.SetPinButCancelImmediately();

  // The UI doesn't wait for the asynchronous metrics calls to finish, which is
  // why we need the RunLoop here:
  base::RunLoop().RunUntilIdle();
  histograms.ExpectBucketCount(kPinUnlockUmaHistogramName, kChoosePinOrPassword,
                               1);
  histograms.ExpectTotalCount(kPinUnlockUmaHistogramName, 1);
}

// Tests that all relevant metrics are recorded when cancelling PIN setup in
// the confirmation step.
IN_PROC_BROWSER_TEST_P(OSSettingsPinSetupTest,
                       SetPinMetricsCancelConfirmation) {
  auto lock_screen_settings = OpenLockScreenSettingsAndAuthenticate();
  auto pin_settings = GoToPinSettings(lock_screen_settings);
  base::HistogramTester histograms;

  pin_settings.SetPinButCancelConfirmation(kFirstPin);

  // The UI doesn't wait for the asynchronous metrics calls to finish, which is
  // why we need the RunLoop here:
  base::RunLoop().RunUntilIdle();
  histograms.ExpectBucketCount(kPinUnlockUmaHistogramName, kChoosePinOrPassword,
                               1);
  histograms.ExpectBucketCount(kPinUnlockUmaHistogramName, kEnterPin, 1);
  histograms.ExpectTotalCount(kPinUnlockUmaHistogramName, 2);
}

// Tests that all relevant metrics are recorded when adding a PIN.
IN_PROC_BROWSER_TEST_P(OSSettingsPinSetupTest, SetPinMetricsFull) {
  auto lock_screen_settings = OpenLockScreenSettingsAndAuthenticate();
  auto pin_settings = GoToPinSettings(lock_screen_settings);
  base::HistogramTester histograms;

  pin_settings.SetPin(kFirstPin);

  // The UI doesn't wait for the asynchronous metrics calls to finish, which is
  // why we need the RunLoop here:
  base::RunLoop().RunUntilIdle();
  histograms.ExpectBucketCount(kPinUnlockUmaHistogramName, kChoosePinOrPassword,
                               1);
  histograms.ExpectBucketCount(kPinUnlockUmaHistogramName, kEnterPin, 1);
  histograms.ExpectBucketCount(kPinUnlockUmaHistogramName, kConfirmPin, 1);
  histograms.ExpectTotalCount(kPinUnlockUmaHistogramName, 3);
}

// Tests that the PIN control is disabled when PIN is disabled by policy.
IN_PROC_BROWSER_TEST_P(OSSettingsPinSetupTest, PinDisabledByPolicy) {
  SetPinDisabledPolicy(true);

  auto lock_screen_settings = OpenLockScreenSettingsAndAuthenticate();
  auto pin_settings = GoToPinSettings(lock_screen_settings);

  pin_settings.AssertDisabled(true);
}

// Tests that the PIN control is enabled when PIN is allowed policy.
IN_PROC_BROWSER_TEST_P(OSSettingsPinSetupTest, PinNotDisabledByPolicy) {
  SetPinDisabledPolicy(false);

  auto lock_screen_settings = OpenLockScreenSettingsAndAuthenticate();
  auto pin_settings = GoToPinSettings(lock_screen_settings);

  pin_settings.AssertDisabled(false);
}

// Tests that the PIN control gets disabled when the policy disabling PIN is
// activated while the settings page is opened.
IN_PROC_BROWSER_TEST_P(OSSettingsPinSetupTest, PinDisabledPolicyWhileOpen) {
  auto lock_screen_settings = OpenLockScreenSettingsAndAuthenticate();
  auto pin_settings = GoToPinSettings(lock_screen_settings);
  pin_settings.AssertDisabled(false);

  SetPinDisabledPolicy(true);

  pin_settings.AssertDisabled(true);
}

// Tests enabling and disabling autosubmit.
IN_PROC_BROWSER_TEST_P(OSSettingsPinSetupTest, Autosubmit) {
  auto lock_screen_settings = OpenLockScreenSettingsAndAuthenticate();
  auto pin_settings = GoToPinSettings(lock_screen_settings);

  // Set a pin. Autosubmit should be enabled.
  pin_settings.SetPin(kFirstPin);
  pin_settings.AssertPinAutosubmitEnabled(true);
  EXPECT_EQ(true, GetPinAutoSubmitState());

  // Change, remove and add pin again. Nothing of this should affect the pin
  // autosubmit pref.
  pin_settings.SetPin(kSecondPin);
  pin_settings.AssertPinAutosubmitEnabled(true);
  EXPECT_EQ(true, GetPinAutoSubmitState());

  pin_settings.RemovePin();
  pin_settings.AssertPinAutosubmitEnabled(true);
  EXPECT_EQ(true, GetPinAutoSubmitState());

  pin_settings.SetPin(kSecondPin);
  pin_settings.AssertPinAutosubmitEnabled(true);
  EXPECT_EQ(true, GetPinAutoSubmitState());

  // Disable pin autosubmit. This should turn the pref off, but the pin should
  // still be active.
  pin_settings.DisablePinAutosubmit();
  pin_settings.AssertPinAutosubmitEnabled(false);
  EXPECT_EQ(false, GetPinAutoSubmitState());
  EXPECT_EQ(true, IsPinConfigured());

  // Try to enable pin autosubmit using the wrong pin. This should not succeed.
  pin_settings.EnablePinAutosubmitIncorrectly(kIncorrectPin);
  pin_settings.AssertPinAutosubmitEnabled(false);
  EXPECT_EQ(false, GetPinAutoSubmitState());

  // Try to enable pin autosubmit using the correct pin. This should succeed.
  pin_settings.EnablePinAutosubmit(kSecondPin);
  pin_settings.AssertPinAutosubmitEnabled(true);
  EXPECT_EQ(true, GetPinAutoSubmitState());

  // Even after we have authenticated with the correct pin, we should be able
  // to remove the pin.
  pin_settings.RemovePin();
  pin_settings.AssertHasPin(false);
  EXPECT_EQ(false, IsPinConfigured());
}

// Tests the maximum length of PINs for autosubmit.
IN_PROC_BROWSER_TEST_P(OSSettingsPinSetupTest, MaximumLengthAutosubmit) {
  auto lock_screen_settings = OpenLockScreenSettingsAndAuthenticate();
  auto pin_settings = GoToPinSettings(lock_screen_settings);

  // Set a maximum length pin. Autosubmit should be enabled.
  pin_settings.SetPin(kMaximumLengthPinForAutosubmit);
  pin_settings.AssertPinAutosubmitEnabled(true);
  EXPECT_EQ(true, GetPinAutoSubmitState());
  // Remove the PIN again.
  pin_settings.RemovePin();

  // Set an overly long PIN. Autosubmit should be disabled, and we shouldn't be
  // able to turn it on.
  pin_settings.SetPin(kTooLongPinForAutosubmit);
  pin_settings.AssertPinAutosubmitEnabled(false);
  EXPECT_EQ(false, GetPinAutoSubmitState());

  pin_settings.EnablePinAutosubmitTooLong(kTooLongPinForAutosubmit);
  pin_settings.AssertPinAutosubmitEnabled(false);
  EXPECT_EQ(false, GetPinAutoSubmitState());
}

// Tests that the user is asked to reauthenticate when trying to enable PIN
// autosubmit but with a locked-out PIN.
IN_PROC_BROWSER_TEST_P(OSSettingsPinSetupTest, AutosubmitWithLockedPin) {
  auto go_to_lock_screen_settings_and_authenticate = [&]() {
    if (ash::features::IsUseAuthPanelInSessionEnabled()) {
      OpenLockScreenSettings();
      AuthenticateUsingPassword();
      return mojom::LockScreenSettingsAsyncWaiter{
          lock_screen_settings_remote_.get()};
    } else {
      return OpenLockScreenSettingsAndAuthenticate();
    }
  };

  auto lock_screen_settings = go_to_lock_screen_settings_and_authenticate();

  auto pin_settings = GoToPinSettings(lock_screen_settings);
  pin_settings.SetPin(kFirstPin);
  // We disable autosubmit so that we can try to reenable.
  pin_settings.DisablePinAutosubmit();
  SetPinLocked();

  pin_settings.TryEnablePinAutosubmit(kFirstPin);

  if (ash::features::IsUseAuthPanelInSessionEnabled()) {
    base::test::TestFuture<AuthSurfaceRegistry::AuthSurface> future;
    auto subscription =
        ash::AuthParts::Get()->GetAuthSurfaceRegistry()->RegisterShownCallback(
            future.GetCallback());

    auto surface = future.Get();
    ASSERT_EQ(surface, AuthSurfaceRegistry::AuthSurface::kInSession);

    base::RunLoop().RunUntilIdle();

    AuthenticateUsingPassword();

    base::RunLoop().RunUntilIdle();

    EXPECT_EQ(false, GetPinAutoSubmitState());
    pin_settings.AssertPinAutosubmitEnabled(false);
  } else {
    lock_screen_settings.AssertAuthenticated(false);

    lock_screen_settings.Authenticate(
        OSSettingsLockScreenBrowserTestBase::kPassword);
    EXPECT_EQ(false, GetPinAutoSubmitState());
    pin_settings.AssertPinAutosubmitEnabled(false);
  }
}

}  // namespace ash::settings