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

#ifndef CHROME_BROWSER_ASH_LOGIN_QUICK_UNLOCK_PIN_BACKEND_H_
#define CHROME_BROWSER_ASH_LOGIN_QUICK_UNLOCK_PIN_BACKEND_H_

#include <optional>
#include <string>

#include "base/functional/callback.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
#include "base/time/time.h"
#include "chromeos/ash/components/login/auth/public/auth_callbacks.h"
#include "chromeos/ash/components/login/auth/public/key.h"
#include "chromeos/ash/services/auth_factor_config/chrome_browser_delegates.h"
#include "components/prefs/pref_service.h"

class AccountId;
class PrefService;

namespace ash::quick_unlock {

class PinStorageCryptohome;
enum class Purpose;

// Provides high-level access to the user's PIN. The underlying storage can be
// either cryptohome or prefs.
class PinBackend : public ash::auth::PinBackendDelegate {
 public:
  using BoolCallback = base::OnceCallback<void(bool)>;
  using AvailabilityCallback =
      base::OnceCallback<void(bool, std::optional<base::Time>)>;

  // Creates the singleton object.
  // `local_state` must be non-null and must live until Shutdown() is called.
  // TODO(crbug.com/498416395): Use std::unique_ptr<PinBackend> for memory
  // management, and remove this.
  static void Initialize(PrefService* local_state);

  // Fetch the PinBackend instance.
  static PinBackend* GetInstance();

  // Cleans up internal states.
  // TODO(crbug.com/498416395): Refactor PinBackend to destroy the singleton
  // object, and remove this.
  static void Shutdown();

  // Computes a new salt.
  static std::string ComputeSalt();

  // Computes the secret for a given `pin` and `salt`.
  static std::string ComputeSecret(const std::string& pin,
                                   const std::string& salt,
                                   Key::KeyType key_type);

  PinBackend(const PinBackend&) = delete;
  PinBackend& operator=(const PinBackend&) = delete;

  ~PinBackend() override;

  // Check to see if the PinBackend supports login. This is true when the
  // cryptohome backend is available.
  void HasLoginSupport(BoolCallback result);

  // Check if the given account_id has a PIN registered.
  void IsSet(const AccountId& account_id, BoolCallback result);

  // Set the PIN for the given user.
  void Set(const AccountId& account_id,
           const std::string& auth_token,
           const std::string& pin,
           BoolCallback did_set) override;

  // Update the PIN for the given user.
  void UpdateCryptohomePin(const AccountId& account_id,
                           const std::string& auth_token,
                           const std::string& pin,
                           BoolCallback did_update) override;

  // Set the state of PIN auto submit for the given user. Called when enabling
  // auto submit through the confirmation dialog in Settings.
  void SetPinAutoSubmitEnabled(const AccountId& account_id,
                               const std::string& pin,
                               const bool enabled,
                               BoolCallback did_set);

  // Remove the given user's PIN.
  void Remove(const AccountId& account_id,
              const std::string& auth_token,
              BoolCallback did_remove) override;

  // Is PIN authentication available for the given account? Even if PIN is set,
  // it may not be available for authentication due to some additional
  // restrictions.
  void CanAuthenticate(const AccountId& account_id,
                       Purpose purpose,
                       AvailabilityCallback result_callback);

  // Try to check a pin `key` value for the given user. The `key` must be plain
  // text and not contain a salt. The `user_context` must not have an
  // associated auth session, and must have `IsUsingPin` set to true. The
  // UserContext passed to the `result` callback after the authentication
  // attempt is the same as the one that was passed to `TryAuthenticate`. In
  // particular, it does not have an associated auth session.
  void TryAuthenticate(std::unique_ptr<UserContext> user_context,
                       const Key& key,
                       Purpose purpose,
                       AuthOperationCallback result);

  // Interface for the lock/login screen to access the user's PIN length.
  // Ensures that the UI is always consistent with the pref values without the
  // need for individual observers.
  int GetExposedPinLength(const AccountId& account_id);

  // TODO(crbug.com/1104164) - Remove this once most users have their
  // preferences backfilled.
  // These values are persisted to logs. Entries should not be renumbered and
  // numeric values should never be reused. Must coincide with the enum
  // PinAutosubmitBackfillEvent on enums.xml
  enum class BackfillEvent {
    // Successfully set the user preference on the server
    kEnabled = 0,
    // Possible errors to keep track of.
    kDisabledDueToPolicy = 1,
    kDisabledDueToPinLength = 2,
    kMaxValue = kDisabledDueToPinLength,
  };

 private:
  class CryptohomeBackendState {
   public:
    CryptohomeBackendState();
    CryptohomeBackendState(const CryptohomeBackendState&) = delete;
    CryptohomeBackendState& operator=(const CryptohomeBackendState&) = delete;
    ~CryptohomeBackendState();

    // If cryptohome backend is supported `cryptohome_backend_or_null` must be
    // non-null. Otherwiser, it must be nullptr.
    void Set(std::unique_ptr<PinStorageCryptohome> cryptohome_backend_or_null);

    // TODO(crbug.com/498416395): Removed with PinBackend::Shutdown.
    void Shutdown();

    // Returns true until `Set` is called.
    bool IsResolving() const;

    // `Set` must be called beforehand.
    bool IsSupported() const;

    // `Set` must be called beforehand.
    PinStorageCryptohome& GetCryptohomeBackend() const;

   private:
    std::optional<bool> is_supported_;
    std::unique_ptr<PinStorageCryptohome> cryptohome_backend_;
  };

  // Use Initialize().
  explicit PinBackend(PrefService* local_state);

  // Called when we know if the cryptohome supports PIN.
  void OnIsCryptohomeBackendSupported(bool is_supported);

  // Actions to be performed after an authentication attempt with Cryptohome.
  // The only use case right now is for PIN auto submit, where we might want to
  // expose the PIN length upon a successful attempt.
  void OnCryptohomeAuthenticationResponse(
      const Key& key,
      AuthOperationCallback result,
      std::unique_ptr<UserContext> user_context,
      std::optional<AuthenticationError> error);

  // Called after checking the user's PIN when enabling auto submit.
  // If the authentication was `success`ful, the `pin_length` will be
  // exposed in local state.
  void OnPinAutosubmitCheckComplete(size_t pin_length,
                                    BoolCallback result,
                                    std::unique_ptr<UserContext> user_context,
                                    std::optional<AuthenticationError> error);

  // Simple operations to be performed for PIN auto submit during the common
  // operations in PinBackend - Set, Remove, TryAuthenticate
  void SetWithContext(const AccountId& account_id,
                      const std::string& auth_token,
                      const std::string& pin,
                      BoolCallback did_set,
                      std::unique_ptr<UserContext> user_context);
  void RemoveWithContext(const AccountId& account_id,
                         const std::string& auth_token,
                         BoolCallback did_remove,
                         std::unique_ptr<UserContext> user_context);
  void UpdateCryptohomePinWithContext(
      const AccountId& account_id,
      const std::string& token,
      const std::string& pin,
      BoolCallback did_set,
      std::unique_ptr<UserContext> user_context);

  // When setting/updating a PIN. After every 'Set' operation the
  // exposed length can only be either the true PIN length, or zero.
  void UpdatePinAutosubmitOnSet(const AccountId& account_id, size_t pin_length);

  // Clears the exposed PIN length and resets the user setting.
  void UpdatePinAutosubmitOnRemove(const AccountId& account_id);

  // A successful authentication attempt will expose the pin length. This is
  // necessary when the preference is being set by policy. When the pref is
  // being controlled by the user -- through Settings --, the length is exposed
  // through a confirmation dialog immediately.
  void UpdatePinAutosubmitOnSuccessfulTryAuth(const AccountId& account_id,
                                              size_t pin_length);

  // PIN auto submit backfill operation for users with existing PINs.
  // TODO(crbug.com/1104164) - Remove this once most users have their
  // preferences backfilled.
  // Backfill PIN auto submit preferences for users who already have a pin, but
  // had it set up before the pin auto submit feature was released.
  void PinAutosubmitBackfill(const AccountId& account_id, size_t pin_length);

  // Updates the user context stored in the storage indexed by the supplied
  // `auth_token`, and runs the supplied `callback` with true iff the `error`
  // parameter is `nullopt`.
  static void OnAuthOperation(std::string auth_token,
                              BoolCallback callback,
                              std::unique_ptr<UserContext>,
                              std::optional<AuthenticationError>);

  raw_ptr<PrefService> local_state_;

  // Determining if the device supports cryptohome-based keys requires an async
  // dbus call to cryptohome. If we receive a request before we know which
  // backend to use, the request will be pushed to this list and invoked once
  // the backend configuration is determined.
  std::vector<base::OnceClosure> on_cryptohome_support_received_;

  CryptohomeBackendState cryptohome_state_;

  base::WeakPtrFactory<PinBackend> weak_ptr_factory_{this};
};

}  // namespace ash::quick_unlock

#endif  // CHROME_BROWSER_ASH_LOGIN_QUICK_UNLOCK_PIN_BACKEND_H_
