// Copyright 2024 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_WEBAUTHN_AUTHENTICATOR_REQUEST_DIALOG_CONTROLLER_H_
#define CHROME_BROWSER_WEBAUTHN_AUTHENTICATOR_REQUEST_DIALOG_CONTROLLER_H_

#include <cstdint>
#include <memory>
#include <optional>
#include <string>
#include <vector>

#include "base/functional/callback_forward.h"
#include "base/gtest_prod_util.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
#include "base/scoped_observation.h"
#include "base/timer/timer.h"
#include "chrome/browser/ui/webauthn/passkey_upgrade_request_controller.h"
#include "chrome/browser/webauthn/authenticator_reference.h"
#include "chrome/browser/webauthn/authenticator_request_dialog_model.h"
#include "chrome/browser/webauthn/authenticator_transport.h"
#include "chrome/browser/webauthn/password_credential_fetcher.h"
#include "components/webauthn/core/browser/passkey_model.h"
#include "components/webauthn/core/browser/passkey_model_change.h"
#include "content/public/browser/authenticator_request_client_delegate.h"
#include "content/public/browser/global_routing_id.h"
#include "device/fido/cable/v2_constants.h"
#include "third_party/blink/public/mojom/credentialmanagement/credential_type_flags.mojom.h"
#include "url/gurl.h"

class PasskeyUpgradeRequestController;
class Profile;

namespace content {
class RenderFrameHost;
}  // namespace content

// Encapsulates the logic behind the WebAuthn UI flow.
// flow. This is essentially a state machine going through the states defined in
// the `Step` enumeration.
class AuthenticatorRequestDialogController
    : public AuthenticatorRequestDialogModel::Observer,
      public webauthn::PasskeyModel::Observer,
      public PasskeyUpgradeRequestController::Delegate {
 public:
  using RequestCallback = device::FidoRequestHandlerBase::RequestCallback;
  using BlePermissionCallback = base::RepeatingCallback<void(
      device::FidoRequestHandlerBase::BlePermissionCallback)>;

  AuthenticatorRequestDialogController(
      AuthenticatorRequestDialogModel* model,
      content::RenderFrameHost* render_frame_host);

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

  ~AuthenticatorRequestDialogController() override;

  AuthenticatorRequestDialogModel* model() const;

  // AuthenticatorRequestDialogModel::Observer:
  void StartOver() override;
  void OnChromeProfileCreatePasskeyAccepted() override;
  void OnGPMRecoverSecurityDomainClosed() override;
  void ContinueWithFlowAfterBleAdapterPowered() override;
  void PowerOnBleAdapter() override;
  void OpenBlePreferences() override;
  void OpenGpmSettings() override;
  void OnOffTheRecordInterstitialAccepted() override;
  void CancelAuthenticatorRequest() override;
  void OnRequestComplete() override;
  void OnResidentCredentialConfirmed() override;
  void OnHavePIN(std::u16string pin) override;
  void OnGPMEnclaveEnabledStatusChanged(EnclaveEnabledStatus status) override;
  void OnAccountSelected(size_t index) override;
  void OnAccountPreselectedIndex(size_t index) override;
  void OnBioEnrollmentDone() override;
  void OnUserConfirmedPriorityMechanism() override;

  // webauthn::PasskeyModel::Observer:
  void OnPasskeysChanged(
      const std::vector<webauthn::PasskeyModelChange>& changes) override;
  void OnPasskeyModelShuttingDown() override;
  void OnPasskeyModelIsReady(bool is_ready) override;

  // PasskeyUpgradeRequestController::Delegate:
  void PasskeyUpgradeSucceeded() override;
  void PasskeyUpgradeFailed() override;

  // Returns whether the UI is in a state at which the |request_| member of
  // AuthenticatorImpl has completed processing. Note that the request callback
  // is only resolved after the UI is dismissed.
  bool is_request_complete() const;

  // Starts the UX flow, by either showing the transport or password selection
  // screen or the guided flow for the most likely transport.
  //
  // Valid action when at step: kNotStarted.
  void StartFlow(device::FidoRequestHandlerBase::TransportAvailabilityInfo
                     transport_availability,
                 PasswordCredentialFetcher::PasswordCredentials passwords);

  // Starts a modal WebAuthn flow (i.e. what you normally get if you call
  // WebAuthn with no mediation parameter) from a conditional request.
  //
  // Valid action when at step: kPasskeyAutofill.
  void TransitionToModalWebAuthnRequest();

  // Starts the UX flow. Tries to figure out the most likely transport to be
  // used, and starts the guided flow for that transport; or shows the manual
  // transport selection screen if the transport could not be uniquely
  // identified.
  //
  // Valid action when at step: kNotStarted.
  void StartGuidedFlowForMostLikelyTransportOrShowMechanismSelection();

  // Starts a flow for `transport`. Returns `true` if it started a flow, `false`
  // if it didn't and the mechanism selection screen should be shown instead.
  // This should only be called if `priority_mechanism_index_` is unset.
  bool StartGuidedFlowForHint(AuthenticatorTransport transport);

  // Proceeds straight to the platform authenticator prompt. If `type` is
  // `nullopt` then it actives the default platform authenticator. Otherwise it
  // actives the platform authenticator of the given type.
  void HideDialogAndDispatchToPlatformAuthenticator(
      std::optional<device::AuthenticatorType> type = std::nullopt);

  // Called when some caBLE event (e.g. receiving a BLE message, connecting to
  // the tunnel server, etc) happens.
  void OnCableEvent(device::cablev2::Event event);

  // Called when `cable_connecting_sheet_timer_` completes.
  void OnCableConnectingTimerComplete();

  // Ensures that the Bluetooth adapter is powered before executing |action|.
  //  -- If the adapter is powered, run |action| directly.
  //  -- If Chrome does not have Bluetooth permissions, show an error (macOS).
  //  -- If Chrome has not requested Bluetooth permissions yet, trigger a
  //     permission prompt (macOS).
  //  -- If the adapter is not powered, but Chrome can turn it automatically,
  //     then advanced to the flow to turn on Bluetooth automatically.
  //  -- Otherwise advanced to the manual Bluetooth power on flow.
  //
  // Valid action whenever contacting a phone or showing the QR code screen is
  // possible.
  void EnsureBleAdapterIsPoweredAndContinue(base::OnceClosure action);
  void OnBleStatusKnown(device::FidoRequestHandlerBase::BleStatus ble_status);

  // Tries if a USB device is present -- the user claims they plugged it in.
  //
  // Valid action when at step: kUsbInsert.
  void TryUsbDevice();

  // Tries to dispatch to the platform authenticator -- either because the
  // request requires it or because the user told us to. May show an error for
  // unrecognized credential, or an Incognito mode interstitial, or proceed
  // straight to the platform authenticator prompt.
  //
  // Valid action when at all steps.
  void StartPlatformAuthenticatorFlow();

  // To be called when Web Authentication request times-out.
  void OnRequestTimeout();

  // To be called when the user activates a security key that does not recognize
  // any of the allowed credentials (during a GetAssertion request).
  void OnActivatedKeyNotRegistered();

  // To be called when the user activates a security key that does recognize
  // one of excluded credentials (during a MakeCredential request).
  void OnActivatedKeyAlreadyRegistered();

  // To be called when the selected authenticator cannot currently handle PIN
  // requests because it needs a power-cycle due to too many failures.
  void OnSoftPINBlock();

  // To be called when the selected authenticator must be reset before
  // performing any PIN operations because of too many failures.
  void OnHardPINBlock();

  // To be called when the selected authenticator was removed while
  // waiting for a PIN to be entered.
  void OnAuthenticatorRemovedDuringPINEntry();

  // To be called when the selected authenticator doesn't have the requested
  // resident key capability.
  void OnAuthenticatorMissingResidentKeys();

  // To be called when the selected authenticator doesn't have the requested
  // user verification capability.
  void OnAuthenticatorMissingUserVerification();

  // To be called when the selected authenticator doesn't have the requested
  // large blob capability.
  void OnAuthenticatorMissingLargeBlob();

  // To be called when the selected authenticator doesn't support any of the
  // COSEAlgorithmIdentifiers requested by the RP.
  void OnNoCommonAlgorithms();

  // To be called when the selected authenticator cannot create a resident
  // credential because of insufficient storage.
  void OnAuthenticatorStorageFull();

  // To be called when the user denies consent, e.g. by canceling out of the
  // system's platform authenticator prompt.
  void OnUserConsentDenied();

  // To be called when the user clicks "Cancel" in the native Windows UI.
  // Returns true if the event was handled.
  bool OnWinUserCancelled();

  // To be called when a hybrid connection fails. Returns true if the event
  // was handled.
  bool OnHybridTransportError();

  // To be called when there are no passkeys from an internal authenticator.
  // This is a rare case but can happen when the user grants passkeys permission
  // on macOS as part of a request flow and then Chromium realises that the
  // request should never have been sent to iCloud Keychain in the first place.
  bool OnNoPasskeys();

  // To be called when the Bluetooth adapter status changes.
  void BluetoothAdapterStatusChanged(
      device::FidoRequestHandlerBase::BleStatus ble_status);

  void SetRequestCallback(RequestCallback request_callback);
  void SetAccountPreselectedCallback(
      content::AuthenticatorRequestClientDelegate::AccountPreselectedCallback
          callback);
  void SetBluetoothAdapterPowerOnCallback(
      base::RepeatingClosure bluetooth_adapter_power_on_callback);
  void SetRequestBlePermissionCallback(BlePermissionCallback callback);

  // Called when the user needs to retry user verification with the number of
  // |attempts| remaining.
  void OnRetryUserVerification(int attempts);

  // Adds or removes an authenticator to the list of known authenticators. The
  // first authenticator added with transport `kInternal` (or without a
  // transport) is considered to be the default platform authenticator.
  void AddAuthenticator(const device::FidoAuthenticator& authenticator);
  void RemoveAuthenticator(std::string_view authenticator_id);

  // SelectAccount is called to trigger an account selection dialog.
  void SelectAccount(
      std::vector<device::AuthenticatorGetAssertionResponse> responses,
      base::OnceCallback<void(device::AuthenticatorGetAssertionResponse)>
          callback);

  // OnAccountPreselected is called when the user selects a discoverable
  // credential from a platform authenticator prior to providing user
  // authentication. `crededential_id` must match one of the credentials in
  // `transport_availability_.recognized_credentials`. Returns the source of the
  // credential.
  //
  // Note: it's important not to pass a reference to `credential_id` here
  // because this function clears `model_->creds`, which is where such a
  // reference would often point.
  device::AuthenticatorType OnAccountPreselected(
      const std::vector<uint8_t> credential_id);

  void SetSelectedAuthenticatorForTesting(AuthenticatorReference authenticator);

  // StartTransportFlowForTesting moves the UI to focus on the given transport.
  // UI should use |mechanisms()| to enumerate the user-visible mechanisms and
  // use the callbacks therein.
  void StartTransportFlowForTesting(AuthenticatorTransport transport);

  // SetCurrentStepForTesting forces the model to the specified step. This
  // performs the little extra processing that this Controller does before
  // setting the model's Step. In most cases tests can set the Step on the
  // model directly.
  void SetCurrentStepForTesting(AuthenticatorRequestDialogModel::Step step);

  device::FidoRequestHandlerBase::TransportAvailabilityInfo&
  transport_availability_for_testing() {
    return transport_availability_;
  }
  std::vector<AuthenticatorReference>& saved_authenticators() {
    return ephemeral_state_.saved_authenticators_;
  }

  const base::flat_set<AuthenticatorTransport>& available_transports() {
    return transport_availability_.available_transports;
  }

  void CollectPIN(device::pin::PINEntryReason reason,
                  device::pin::PINEntryError error,
                  uint32_t min_pin_length,
                  int attempts,
                  base::OnceCallback<void(std::u16string)> provide_pin_cb);
  void FinishCollectToken();

  void StartInlineBioEnrollment(base::OnceClosure next_callback);
  void OnSampleCollected(int bio_samples_remaining);

  void set_is_non_webauthn_request(bool is_non_webauthn_request) {
    is_non_webauthn_request_ = is_non_webauthn_request;
  }

  void SetHints(
      const content::AuthenticatorRequestClientDelegate::Hints& hints) {
    hints_ = hints;
  }

  void set_cable_transport_info(
      const std::optional<std::string>& cable_qr_string);

  bool win_native_api_enabled() const {
    return transport_availability_.has_win_native_api_authenticator;
  }

  void set_allow_icloud_keychain(bool);
  void set_should_create_in_icloud_keychain(bool);

#if BUILDFLAG(IS_MAC)
  void RecordMacOsStartedHistogram();
  void RecordMacOsSuccessHistogram(device::FidoRequestType,
                                   device::AuthenticatorType);
  void set_is_active_profile_authenticator_user(bool);
  void set_has_icloud_drive_enabled(bool);
#endif

  void SetCredentialTypes(int types);

  content::AuthenticatorRequestClientDelegate::UIPresentation ui_presentation()
      const;
  void SetUIPresentation(
      content::AuthenticatorRequestClientDelegate::UIPresentation modality);

  void ConfigureEnclaveForUpgrade(
      device::FidoDiscoveryFactory* discovery_factory,
      bool cmtg_key_requested);

  base::WeakPtr<AuthenticatorRequestDialogController> GetWeakPtr();

 private:
  FRIEND_TEST_ALL_PREFIXES(AuthenticatorRequestDialogControllerTest,
                           DeduplicateAccounts);

  // Contains the state that will be reset when calling StartOver(). StartOver()
  // might be called at an arbitrary point of execution.
  struct EphemeralState {
    EphemeralState();
    EphemeralState(EphemeralState&&);
    EphemeralState& operator=(EphemeralState&&);
    ~EphemeralState();

    // Stores a list of |AuthenticatorReference| values such that a request can
    // be dispatched dispatched after some UI interaction. This is useful for
    // platform authenticators (and Windows) where dispatch to the authenticator
    // immediately results in modal UI to appear.
    std::vector<AuthenticatorReference> saved_authenticators_;

    // responses_ contains possible responses to select between after an
    // authenticator has responded to a request.
    std::vector<device::AuthenticatorGetAssertionResponse> responses_;

    // When a request has been dispatched to a platform authenticator, this
    // contains the `AuthenticatorType`. std::nullopt at all other times.
    std::optional<device::AuthenticatorType>
        dispatched_platform_authenticator_type_ = std::nullopt;

    // did_invoke_platform_despite_no_priority_mechanism_ is true if a platform
    // authenticator was triggered despite there not being a
    // `priority_mechanism_index_` set. For example, this can happen if there's
    // an allowlist match.
    bool did_invoke_platform_despite_no_priority_mechanism_ = false;
  };

  void ResetEphemeralState();

  void SetCurrentStep(AuthenticatorRequestDialogModel::Step step);

  // Requests that the step-by-step wizard flow commence, guiding the user
  // through using the Secutity Key with the given |transport|.
  //
  // Valid action when at step: kNotStarted. kMechanismSelection, and steps
  // where the other transports menu is shown, namely, kUsbInsertAndActivate,
  // kCableActivate.
  void StartGuidedFlowForTransport(AuthenticatorTransport transport);

  // Starts the hybrid flow. This flow starts with showing a QR code. In some
  // cases it can also display the user a message to insert a security key.
  void StartHybridFlow();

  // Displays a resident-key warning if needed and then calls
  // |HideDialogAndDispatchToNativeWindowsApi|.
  void StartWinNativeApi();

  void StartICloudKeychain();
  void StartEnclave();

  // Triggers gaia account reauth to restore sync to working order.
  void ReauthForSyncRestore();

  void StartAutofillRequest();
  void StartPasskeyUpgradeRequest();

  void DispatchRequestAsync(AuthenticatorReference* authenticator);

  // SortRecognizedCredentials sorts
  // `transport_availability_.recognized_credentials` into username order.
  void SortRecognizedCredentials();

  // PopulateMechanisms fills in |model_->mechanisms|.
  void PopulateMechanisms();

  // Adds a button that triggers Windows Hello with the specified string ID and
  // transport icon.
  void AddWindowsButton(int label, AuthenticatorTransport transport);

  // IndexOfPriorityMechanism returns the index, in |model_->mechanisms|, of the
  // Mechanism that should be triggered immediately, if any.
  std::optional<size_t> IndexOfPriorityMechanism();

  std::optional<size_t> IndexOfGetAssertionPriorityMechanism();
  std::optional<size_t> IndexOfImmediateGetPriorityMechanism();
  std::optional<size_t> IndexOfMakeCredentialPriorityMechanism();

  // Returns true if this request could pick the enclave authenticator by
  // default. This only makes sense for a create() call.
  bool CanDefaultToEnclave(Profile* profile);

  // Returns the render frame host associated with this request. May return
  // nullptr if the initiating frame or tab has been destroyed or detached.
  content::RenderFrameHost* MaybeGetRenderFrameHost() const;

  void PopulatePasswords();

  scoped_refptr<AuthenticatorRequestDialogModel> model_;

  // Identifier for the RenderFrameHost of the frame that initiated the current
  // request.
  EphemeralState ephemeral_state_;

  // is_non_webauthn_request_ is true if the current request came from Secure
  // Payment Confirmation, or from credit-card autofill.
  bool is_non_webauthn_request_ = false;

  // started_ records whether |StartFlow| has been called.
  bool started_ = false;

  // pending_step_ holds requested steps until the UI is shown. The UI is only
  // shown once the TransportAvailabilityInfo is available, but authenticators
  // may request, e.g., PIN entry prior to that.
  std::optional<AuthenticatorRequestDialogModel::Step> pending_step_;

  // after_off_the_record_interstitial_ contains the closure to run if the user
  // accepts the interstitial that warns that platform/caBLE authenticators may
  // record information even in incognito mode.
  base::OnceClosure after_off_the_record_interstitial_;

  // after_ble_adapter_powered_ contains the closure to run if the user
  // accepts the interstitial that requests to turn on the BLE adapter.
  base::OnceClosure after_ble_adapter_powered_;

  // This field is only filled out once the UX flow is started.
  device::FidoRequestHandlerBase::TransportAvailabilityInfo
      transport_availability_;

  PasswordCredentialFetcher::PasswordCredentials passwords_;

  content::AuthenticatorRequestClientDelegate::AccountPreselectedCallback
      account_preselected_callback_;
  RequestCallback request_callback_;
  base::RepeatingClosure bluetooth_adapter_power_on_callback_;

  // Triggers a permission prompt on macOS if ble_status is
  // kPendingPermissionRequest and returns the bluetooth status.
  BlePermissionCallback request_ble_permission_callback_;

  base::OnceClosure bio_enrollment_callback_;

  base::OnceCallback<void(std::u16string)> pin_callback_;

  base::OnceCallback<void(device::AuthenticatorGetAssertionResponse)>
      selection_callback_;

  // cable_connecting_sheet_timer_ is started when we start displaying
  // the "connecting..." sheet for a caBLE connection. To avoid flashing the UI,
  // the sheet won't be automatically replaced until it completes.
  base::OneShotTimer cable_connecting_sheet_timer_;

  // cable_connecting_ready_to_advance_ is set to true if we are ready to
  // advance the "connecting" sheet but are waiting for
  // `cable_connecting_sheet_timer_` to complete.
  bool cable_connecting_ready_to_advance_ = false;

  // allow_icloud_keychain_ is true if iCloud Keychain can be used for this
  // request. It is disabled for Secure Payment Confirmation and other non-
  // WebAuthn cases, for example.
  bool allow_icloud_keychain_ = false;

  // should_create_in_icloud_keychain is true if creation requests with
  // attachment=platform should default to iCloud Keychain rather than the
  // profile authenticator.
  bool should_create_in_icloud_keychain_ = false;

  // enclave_enabled_status_ determines whether Google Password Manager entries
  // should be offered both for `makeCredential` or `getAssertion`. If reauth
  // needed, instead of offering the actual passkeys or creating a passkey,
  // re-authentication to Google is offered.
  EnclaveEnabledStatus enclave_enabled_status_ =
      EnclaveEnabledStatus::kDisabled;

  // The RP's hints. See
  // https://w3c.github.io/webauthn/#enumdef-publickeycredentialhints
  content::AuthenticatorRequestClientDelegate::Hints hints_;

  // True when the priority mechanism was determined to be the enclave.
  bool enclave_was_priority_mechanism_ = false;

#if BUILDFLAG(IS_MAC)
  // did_record_macos_start_histogram_ is set to true if a histogram record of
  // starting the current request was made. Any later successful completion will
  // only be recorded if a start event was recorded first.
  bool did_record_macos_start_histogram_ = false;

  // has_icloud_drive_enabled_ is true if the current system has iCloud Drive
  // enabled. This is used as an approximation for whether iCloud Keychain
  // syncing is enabled.
  bool has_icloud_drive_enabled_ = false;
#endif

  // The credential types that are being asked for.
  int credential_types_ =
      static_cast<int>(blink::mojom::CredentialTypeFlags::kNone);

  const content::GlobalRenderFrameHostId frame_host_id_;

  base::ScopedObservation<webauthn::PasskeyModel,
                          webauthn::PasskeyModel::Observer>
      passkey_model_observation_{this};

  std::unique_ptr<PasskeyUpgradeRequestController>
      passkey_upgrade_request_controller_;

  base::WeakPtrFactory<AuthenticatorRequestDialogController> weak_factory_{
      this};
};

#endif  // CHROME_BROWSER_WEBAUTHN_AUTHENTICATOR_REQUEST_DIALOG_CONTROLLER_H_
