chromium/chromeos/ash/services/auth_factor_config/auth_factor_config.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 "chromeos/ash/services/auth_factor_config/auth_factor_config.h"

#include <memory>

#include "ash/constants/ash_features.h"
#include "ash/constants/ash_pref_names.h"
#include "base/feature_list.h"
#include "chromeos/ash/components/cryptohome/auth_factor.h"
#include "chromeos/ash/components/login/auth/public/auth_factors_configuration.h"
#include "chromeos/ash/components/login/auth/public/user_context.h"
#include "chromeos/ash/components/osauth/public/auth_session_storage.h"
#include "chromeos/ash/services/auth_factor_config/auth_factor_config_utils.h"
#include "components/prefs/pref_service.h"
#include "components/user_manager/user_directory_integrity_manager.h"
#include "components/user_manager/user_manager.h"

namespace ash::auth {

AuthFactorConfig::AuthFactorConfig(
    QuickUnlockStorageDelegate* quick_unlock_storage,
    PrefService* local_state)
    : quick_unlock_storage_(quick_unlock_storage),
      local_state_(local_state),
      auth_factor_editor_(UserDataAuthClient::Get()) {
  CHECK(quick_unlock_storage_);
  CHECK(local_state_);
}

AuthFactorConfig::~AuthFactorConfig() = default;

// static
void AuthFactorConfig::RegisterPrefs(PrefRegistrySimple* registry) {
  const bool default_for_enterprise = base::FeatureList::IsEnabled(
      ash::features::kCryptohomeRecoveryByDefaultForEnterprise);
  registry->RegisterBooleanPref(ash::prefs::kRecoveryFactorBehavior,
                                default_for_enterprise);
}

void AuthFactorConfig::BindReceiver(
    mojo::PendingReceiver<mojom::AuthFactorConfig> receiver) {
  receivers_.Add(this, std::move(receiver));
}

void AuthFactorConfig::ObserveFactorChanges(
    mojo::PendingRemote<mojom::FactorObserver> observer) {
  observers_.Add(std::move(observer));
}

void AuthFactorConfig::NotifyFactorObserversAfterSuccess(
    AuthFactorSet changed_factors,
    const std::string& auth_token,
    std::unique_ptr<UserContext> context,
    base::OnceCallback<void(mojom::ConfigureResult)> callback) {
  CHECK(context);

  auth_factor_editor_.GetAuthFactorsConfiguration(
      std::move(context),
      base::BindOnce(&AuthFactorConfig::OnGetAuthFactorsConfiguration,
                     weak_factory_.GetWeakPtr(), changed_factors,
                     std::move(callback), auth_token));
}

void AuthFactorConfig::NotifyFactorObserversAfterFailure(
    const std::string& auth_token,
    std::unique_ptr<UserContext> context,
    base::OnceCallback<void()> callback) {
  CHECK(context);

  // The original callback, but with an additional ignored parameter so that we
  // can pass it to `OnGetAuthFactorsConfiguration`.
  base::OnceCallback<void(mojom::ConfigureResult)> ignore_param_callback =
      base::BindOnce([](base::OnceCallback<void()> callback,
                        mojom::ConfigureResult) { std::move(callback).Run(); },
                     std::move(callback));

  auth_factor_editor_.GetAuthFactorsConfiguration(
      std::move(context),
      base::BindOnce(&AuthFactorConfig::OnGetAuthFactorsConfiguration,
                     weak_factory_.GetWeakPtr(), AuthFactorSet::All(),
                     std::move(ignore_param_callback), auth_token));
}

void AuthFactorConfig::OnUserHasKnowledgeFactor(const UserContext& context) {
  if (add_knowledge_factor_callback_) {
    std::move(add_knowledge_factor_callback_).Run();
  }
  if (skip_user_integrity_notification_.value_or(false)) {
    return;
  }
  user_manager::UserDirectoryIntegrityManager(local_state_).ClearPrefs();
}

void AuthFactorConfig::IsSupported(const std::string& auth_token,
                                   mojom::AuthFactor factor,
                                   base::OnceCallback<void(bool)> callback) {
  ObtainContext(auth_token,
                base::BindOnce(&AuthFactorConfig::IsSupportedWithContext,
                               weak_factory_.GetWeakPtr(), auth_token, factor,
                               std::move(callback)));
}
void AuthFactorConfig::IsSupportedWithContext(
    const std::string& auth_token,
    mojom::AuthFactor factor,
    base::OnceCallback<void(bool)> callback,
    std::unique_ptr<UserContext> context) {
  if (!context) {
    LOG(ERROR) << "Invalid or expired auth token";
    std::move(callback).Run(false);
    return;
  }

  if (context->HasAuthFactorsConfiguration()) {
    const cryptohome::AuthFactorsSet cryptohome_supported_factors =
        context->GetAuthFactorsConfiguration().get_supported_factors();

    ash::AuthSessionStorage::Get()->Return(auth_token, std::move(context));

    switch (factor) {
      case mojom::AuthFactor::kRecovery: {
        std::move(callback).Run(cryptohome_supported_factors.Has(
            cryptohome::AuthFactorType::kRecovery));
        return;
      }
      case mojom::AuthFactor::kPin: {
        std::move(callback).Run(
            cryptohome_supported_factors.Has(cryptohome::AuthFactorType::kPin));
        return;
      }
      case mojom::AuthFactor::kLocalPassword:
      case mojom::AuthFactor::kGaiaPassword: {
        std::move(callback).Run(true);
        return;
      }
    }

    NOTREACHED_IN_MIGRATION();
  }

  auto split_callback = base::SplitOnceCallback(std::move(callback));

  FactorStatusCheckOperation retry = base::BindOnce(
      &AuthFactorConfig::IsSupportedWithContext, weak_factory_.GetWeakPtr(),
      auth_token, factor, std::move(split_callback.first));

  RefreshAuthFactorsConfiguration(
      std::move(context),
      base::BindOnce(
          &AuthFactorConfig::MaybeRetryFactorStatusCheckOnConfigRefresh,
          weak_factory_.GetWeakPtr(), std::move(retry),
          std::move(split_callback.second), auth_token));
}

void AuthFactorConfig::IsConfigured(const std::string& auth_token,
                                    mojom::AuthFactor factor,
                                    base::OnceCallback<void(bool)> callback) {
  ObtainContext(auth_token,
                base::BindOnce(&AuthFactorConfig::IsConfiguredWithContext,
                               weak_factory_.GetWeakPtr(), auth_token, factor,
                               std::move(callback)));
}

void AuthFactorConfig::IsConfiguredWithContext(
    const std::string& auth_token,
    mojom::AuthFactor factor,
    base::OnceCallback<void(bool)> callback,
    std::unique_ptr<UserContext> context) {
  if (!context) {
    LOG(ERROR) << "Invalid or expired auth token";
    std::move(callback).Run(false);
    return;
  }

  if (context->HasAuthFactorsConfiguration()) {
    const auto& config = context->GetAuthFactorsConfiguration();
    ash::AuthSessionStorage::Get()->Return(auth_token, std::move(context));

    switch (factor) {
      case mojom::AuthFactor::kRecovery: {
        std::move(callback).Run(
            config.HasConfiguredFactor(cryptohome::AuthFactorType::kRecovery));
        return;
      }
      case mojom::AuthFactor::kPin: {
        // We have to consider both cryptohome based PIN and legacy pref PIN.
        if (config.HasConfiguredFactor(cryptohome::AuthFactorType::kPin)) {
          std::move(callback).Run(true);
          return;
        }
        const auto* user = ::user_manager::UserManager::Get()->GetPrimaryUser();
        if (!user) {
          LOG(ERROR) << "No logged in user";
          std::move(callback).Run(false);
        }
        const PrefService* prefs = quick_unlock_storage_->GetPrefService(*user);
        if (!prefs) {
          LOG(ERROR) << "No pref service for user";
          std::move(callback).Run(false);
          return;
        }

        const bool has_prefs_pin =
            !prefs->GetString(prefs::kQuickUnlockPinSecret).empty() &&
            !prefs->GetString(prefs::kQuickUnlockPinSalt).empty();

        std::move(callback).Run(has_prefs_pin);
        return;
      }
      case mojom::AuthFactor::kGaiaPassword: {
        const cryptohome::AuthFactor* password_factor =
            config.FindFactorByType(cryptohome::AuthFactorType::kPassword);
        if (!password_factor) {
          std::move(callback).Run(false);
          return;
        }

        std::move(callback).Run(IsGaiaPassword(*password_factor));
        return;
      }
      case mojom::AuthFactor::kLocalPassword: {
        const cryptohome::AuthFactor* password_factor =
            config.FindFactorByType(cryptohome::AuthFactorType::kPassword);
        if (!password_factor) {
          std::move(callback).Run(false);
          return;
        }

        std::move(callback).Run(IsLocalPassword(*password_factor));
        return;
      }
    }

    NOTREACHED_IN_MIGRATION();
  }

  auto split_callback = base::SplitOnceCallback(std::move(callback));

  FactorStatusCheckOperation retry = base::BindOnce(
      &AuthFactorConfig::IsConfiguredWithContext, weak_factory_.GetWeakPtr(),
      auth_token, factor, std::move(split_callback.first));

  RefreshAuthFactorsConfiguration(
      std::move(context),
      base::BindOnce(
          &AuthFactorConfig::MaybeRetryFactorStatusCheckOnConfigRefresh,
          weak_factory_.GetWeakPtr(), std::move(retry),
          std::move(split_callback.second), auth_token));
}

void AuthFactorConfig::GetManagementType(
    const std::string& auth_token,
    mojom::AuthFactor factor,
    base::OnceCallback<void(mojom::ManagementType)> callback) {
  switch (factor) {
    case mojom::AuthFactor::kRecovery: {
      const auto* user = ::user_manager::UserManager::Get()->GetPrimaryUser();
      CHECK(user);
      const PrefService* prefs = quick_unlock_storage_->GetPrefService(*user);
      CHECK(prefs);
      const mojom::ManagementType result =
          prefs->IsManagedPreference(prefs::kRecoveryFactorBehavior)
              ? mojom::ManagementType::kUser
              : mojom::ManagementType::kNone;

      std::move(callback).Run(result);
      return;
    }
    case mojom::AuthFactor::kPin: {
      const auto* user = ::user_manager::UserManager::Get()->GetPrimaryUser();
      CHECK(user);
      const PrefService* prefs = quick_unlock_storage_->GetPrefService(*user);
      CHECK(prefs);

      if (prefs->IsManagedPreference(prefs::kQuickUnlockModeAllowlist) ||
          prefs->IsManagedPreference(prefs::kWebAuthnFactors)) {
        std::move(callback).Run(mojom::ManagementType::kUser);
      } else {
        std::move(callback).Run(mojom::ManagementType::kNone);
      }
      return;
    }
    case mojom::AuthFactor::kGaiaPassword:
    case mojom::AuthFactor::kLocalPassword: {
      // There are currently no policies related to Gaia/local passwords.
      std::move(callback).Run(mojom::ManagementType::kNone);
      return;
    }
  }

  NOTREACHED_IN_MIGRATION();
}

void AuthFactorConfig::IsEditable(const std::string& auth_token,
                                  mojom::AuthFactor factor,
                                  base::OnceCallback<void(bool)> callback) {
  ObtainContext(auth_token,
                base::BindOnce(&AuthFactorConfig::IsEditableWithContext,
                               weak_factory_.GetWeakPtr(), auth_token, factor,
                               std::move(callback)));
}
void AuthFactorConfig::IsEditableWithContext(
    const std::string& auth_token,
    mojom::AuthFactor factor,
    base::OnceCallback<void(bool)> callback,
    std::unique_ptr<UserContext> context) {
  if (!context) {
    LOG(ERROR) << "Invalid or expired auth token";
    std::move(callback).Run(false);
    return;
  }

  if (context->HasAuthFactorsConfiguration()) {
    const auto& config = context->GetAuthFactorsConfiguration();

    ash::AuthSessionStorage::Get()->Return(auth_token, std::move(context));

    switch (factor) {
      case mojom::AuthFactor::kRecovery: {
        const auto* user = ::user_manager::UserManager::Get()->GetPrimaryUser();
        CHECK(user);

        const PrefService* prefs = quick_unlock_storage_->GetPrefService(*user);
        CHECK(prefs);

        if (prefs->IsUserModifiablePreference(prefs::kRecoveryFactorBehavior)) {
          std::move(callback).Run(true);
          return;
        }

        const bool is_configured =
            config.HasConfiguredFactor(cryptohome::AuthFactorType::kRecovery);

        if (is_configured !=
            prefs->GetBoolean(prefs::kRecoveryFactorBehavior)) {
          std::move(callback).Run(true);
          return;
        }

        std::move(callback).Run(false);
        return;
      }
      case mojom::AuthFactor::kPin: {
        const auto* user = ::user_manager::UserManager::Get()->GetPrimaryUser();
        CHECK(user);
        const PrefService* prefs = quick_unlock_storage_->GetPrefService(*user);
        CHECK(prefs);

        // Lists of factors that are allowed for some purpose.
        const base::Value::List* pref_lists[] = {
            &prefs->GetList(prefs::kQuickUnlockModeAllowlist),
            &prefs->GetList(prefs::kWebAuthnFactors),
        };

        // Values in factor lists that match PINs.
        const base::Value pref_list_values[] = {
            base::Value("all"),
            base::Value("PIN"),
        };

        for (const auto* pref_list : pref_lists) {
          for (const auto& pref_list_value : pref_list_values) {
            if (base::Contains(*pref_list, pref_list_value)) {
              std::move(callback).Run(true);
              return;
            }
          }
        }

        std::move(callback).Run(false);
        return;
      }
      case mojom::AuthFactor::kGaiaPassword: {
        // TODO(b/290916811): Decide upon when to return true here. For now we
        // don't allow edits or removal of Gaia passwords once they're
        // configured, so we always return false.
        std::move(callback).Run(false);
        return;
      }
      case mojom::AuthFactor::kLocalPassword: {
        std::move(callback).Run(true);
        return;
      }
    }

    NOTREACHED_IN_MIGRATION();
  }

  auto split_callback = base::SplitOnceCallback(std::move(callback));

  FactorStatusCheckOperation retry = base::BindOnce(
      &AuthFactorConfig::IsConfiguredWithContext, weak_factory_.GetWeakPtr(),
      auth_token, factor, std::move(split_callback.first));

  RefreshAuthFactorsConfiguration(
      std::move(context),
      base::BindOnce(
          &AuthFactorConfig::MaybeRetryFactorStatusCheckOnConfigRefresh,
          weak_factory_.GetWeakPtr(), std::move(retry),
          std::move(split_callback.second), auth_token));
}

void AuthFactorConfig::ObtainContext(
    const std::string& auth_token,
    base::OnceCallback<void(std::unique_ptr<UserContext>)> callback) {
  if (!ash::AuthSessionStorage::Get()->IsValid(auth_token)) {
    std::move(callback).Run(nullptr);
    return;
  }
  ash::AuthSessionStorage::Get()->BorrowAsync(FROM_HERE, auth_token,
                                              std::move(callback));
}

void AuthFactorConfig::OnGetAuthFactorsConfiguration(
    AuthFactorSet changed_factors,
    base::OnceCallback<void(mojom::ConfigureResult)> callback,
    const std::string& auth_token,
    std::unique_ptr<UserContext> context,
    std::optional<AuthenticationError> error) {
  bool has_knowledge_factor =
      context->GetAuthFactorsConfiguration().HasConfiguredFactor(
          cryptohome::AuthFactorType::kPassword) ||
      context->GetAuthFactorsConfiguration().HasConfiguredFactor(
          cryptohome::AuthFactorType::kPin);

  ash::AuthSessionStorage::Get()->Return(auth_token, std::move(context));

  if (error.has_value()) {
    LOG(ERROR) << "Refreshing list of configured auth factors failed, code "
               << error->get_cryptohome_code();
    std::move(callback).Run(mojom::ConfigureResult::kFatalError);
    return;
  }

  if (has_knowledge_factor) {
    OnUserHasKnowledgeFactor(*context);
  }

  std::move(callback).Run(mojom::ConfigureResult::kSuccess);

  for (auto& observer : observers_) {
    for (const auto changed_factor : changed_factors) {
      observer->OnFactorChanged(changed_factor);
    }
  }
}

void AuthFactorConfig::SetAddKnowledgeFactorCallbackForTesting(
    base::OnceClosure callback) {
  add_knowledge_factor_callback_ = std::move(callback);
}

void AuthFactorConfig::SetSkipUserIntegrityNotificationForTesting(
    bool skip_notification) {
  skip_user_integrity_notification_ = skip_notification;
}

void AuthFactorConfig::RefreshAuthFactorsConfiguration(
    std::unique_ptr<UserContext> context,
    base::OnceCallback<void(std::unique_ptr<UserContext>)> continuation) {
  auth_factor_editor_.GetAuthFactorsConfiguration(
      std::move(context),
      base::BindOnce(&AuthFactorConfig::OnAuthFactorConfigurationRefreshed,
                     weak_factory_.GetWeakPtr(), std::move(continuation)));
}

void AuthFactorConfig::OnAuthFactorConfigurationRefreshed(
    base::OnceCallback<void(std::unique_ptr<UserContext>)> continuation,
    std::unique_ptr<UserContext> context,
    std::optional<AuthenticationError> error) {
  if (error.has_value()) {
    LOG(ERROR) << "Refreshing list of configured auth factors failed, code "
               << error->get_cryptohome_code();
  }

  std::move(continuation).Run(std::move(context));
}

void AuthFactorConfig::MaybeRetryFactorStatusCheckOnConfigRefresh(
    FactorStatusCheckOperation retry_operation,
    FactorStatusCheckResultCallback callback,
    const std::string& auth_token,
    std::unique_ptr<UserContext> context) {
  if (context->HasAuthFactorsConfiguration()) {
    std::move(retry_operation).Run(std::move(context));
    return;
  }

  // We don't have `AuthFactorsConfiguration`, and we have already retried
  // getting them, without success. Assume the factor is unavailable.
  LOG(ERROR) << "Could not obtain auth factor configuration, assuming factor "
                "unavailable";
  ash::AuthSessionStorage::Get()->Return(auth_token, std::move(context));
  std::move(callback).Run(false);
}

}  // namespace ash::auth