chromium/chrome/browser/certificate_provider/pin_dialog_manager.cc

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

#include "chrome/browser/certificate_provider/pin_dialog_manager.h"

#include <vector>

#include "base/containers/contains.h"
#include "base/functional/bind.h"
#include "base/logging.h"

namespace chromeos {

// Define timeout for issued sign_request_id.
constexpr base::TimeDelta kSignRequestIdTimeout = base::Minutes(10);

PinDialogManager::PinDialogManager() = default;

PinDialogManager::~PinDialogManager() = default;

void PinDialogManager::AddSignRequestId(
    const std::string& extension_id,
    int sign_request_id,
    const std::optional<AccountId>& authenticating_user_account_id) {
  ExtensionNameRequestIdPair key(extension_id, sign_request_id);
  sign_requests_.insert(
      std::make_pair(key, SignRequestState(/*begin_time=*/base::Time::Now(),
                                           authenticating_user_account_id)));
}

void PinDialogManager::RemoveSignRequest(const std::string& extension_id,
                                         int sign_request_id) {
  if (active_dialog_state_ &&
      active_dialog_state_->extension_id == extension_id &&
      active_dialog_state_->sign_request_id == sign_request_id) {
    CloseActiveDialog();
  }

  ExtensionNameRequestIdPair key(extension_id, sign_request_id);
  sign_requests_.erase(key);
}

int PinDialogManager::StoredSignRequestsForTesting() const {
  return sign_requests_.size();
}

PinDialogManager::RequestPinResult PinDialogManager::RequestPin(
    const std::string& extension_id,
    const std::string& extension_name,
    int sign_request_id,
    security_token_pin::CodeType code_type,
    security_token_pin::ErrorLabel error_label,
    int attempts_left,
    RequestPinCallback callback) {
  DCHECK_GE(attempts_left, -1);
  const bool accept_input = (attempts_left != 0);

  // Check the validity of sign_request_id.
  const SignRequestState* const sign_request_state =
      FindSignRequestState(extension_id, sign_request_id);
  if (!sign_request_state)
    return RequestPinResult::kInvalidId;

  // Start from sanity checks, as the extension might have issued this call
  // incorrectly.
  if (active_dialog_state_) {
    // The active dialog exists already, so we need to make sure it belongs to
    // the same extension and the user submitted some input.
    if (extension_id != active_dialog_state_->extension_id)
      return RequestPinResult::kOtherFlowInProgress;
    if (active_dialog_state_->request_pin_callback ||
        active_dialog_state_->stop_pin_request_callback) {
      // Extension requests a PIN without having received any input from its
      // previous request. Reject the new request.
      return RequestPinResult::kDialogDisplayedAlready;
    }
  } else {
    // Check that the sign request hasn't timed out yet.
    const base::Time current_time = base::Time::Now();
    if (current_time - sign_request_state->begin_time > kSignRequestIdTimeout)
      return RequestPinResult::kInvalidId;

    // A new dialog will be opened, so initialize the related internal state.
    active_dialog_state_.emplace(GetHostForNewDialog(), extension_id,
                                 extension_name, sign_request_id, code_type);
  }

  active_dialog_state_->request_pin_callback = std::move(callback);
  active_dialog_state_->host->ShowSecurityTokenPinDialog(
      extension_name, code_type, accept_input, error_label, attempts_left,
      sign_request_state->authenticating_user_account_id,
      base::BindOnce(&PinDialogManager::OnPinEntered,
                     weak_factory_.GetWeakPtr()),
      base::BindOnce(&PinDialogManager::OnPinDialogClosed,
                     weak_factory_.GetWeakPtr()));

  return RequestPinResult::kSuccess;
}

PinDialogManager::StopPinRequestResult
PinDialogManager::StopPinRequestWithError(
    const std::string& extension_id,
    security_token_pin::ErrorLabel error_label,
    StopPinRequestCallback callback) {
  DCHECK_NE(error_label, security_token_pin::ErrorLabel::kNone);

  // Perform sanity checks, as the extension might have issued this call
  // incorrectly.
  if (!active_dialog_state_ ||
      active_dialog_state_->extension_id != extension_id) {
    return StopPinRequestResult::kNoActiveDialog;
  }
  if (active_dialog_state_->request_pin_callback ||
      active_dialog_state_->stop_pin_request_callback) {
    return StopPinRequestResult::kNoUserInput;
  }

  const SignRequestState* const sign_request_state =
      FindSignRequestState(extension_id, active_dialog_state_->sign_request_id);
  if (!sign_request_state)
    return StopPinRequestResult::kNoActiveDialog;

  active_dialog_state_->stop_pin_request_callback = std::move(callback);
  active_dialog_state_->host->ShowSecurityTokenPinDialog(
      active_dialog_state_->extension_name, active_dialog_state_->code_type,
      /*enable_user_input=*/false, error_label,
      /*attempts_left=*/-1, sign_request_state->authenticating_user_account_id,
      base::BindOnce(&PinDialogManager::OnPinEntered,
                     weak_factory_.GetWeakPtr()),
      base::BindOnce(&PinDialogManager::OnPinDialogClosed,
                     weak_factory_.GetWeakPtr()));

  return StopPinRequestResult::kSuccess;
}

bool PinDialogManager::LastPinDialogClosed(
    const std::string& extension_id) const {
  auto iter = last_response_closed_.find(extension_id);
  return iter != last_response_closed_.end() && iter->second;
}

bool PinDialogManager::CloseDialog(const std::string& extension_id) {
  // Perform sanity checks, as the extension might have issued this call
  // incorrectly.
  if (!active_dialog_state_ ||
      extension_id != active_dialog_state_->extension_id) {
    LOG(ERROR) << "StopPinRequest called by unexpected extension: "
               << extension_id;
    return false;
  }

  CloseActiveDialog();
  return true;
}

void PinDialogManager::ExtensionUnloaded(const std::string& extension_id) {
  if (active_dialog_state_ &&
      active_dialog_state_->extension_id == extension_id) {
    CloseActiveDialog();
  }

  last_response_closed_[extension_id] = false;

  for (auto it = sign_requests_.cbegin(); it != sign_requests_.cend();) {
    if (it->first.first == extension_id)
      sign_requests_.erase(it++);
    else
      ++it;
  }
}

void PinDialogManager::AddPinDialogHost(
    SecurityTokenPinDialogHost* pin_dialog_host) {
  DCHECK(!base::Contains(added_dialog_hosts_, pin_dialog_host));
  added_dialog_hosts_.push_back(pin_dialog_host);
}

void PinDialogManager::RemovePinDialogHost(
    SecurityTokenPinDialogHost* pin_dialog_host) {
  if (active_dialog_state_ && active_dialog_state_->host == pin_dialog_host)
    CloseActiveDialog();
  DCHECK(base::Contains(added_dialog_hosts_, pin_dialog_host));
  std::erase(added_dialog_hosts_, pin_dialog_host);
}

PinDialogManager::SignRequestState::SignRequestState(
    base::Time begin_time,
    const std::optional<AccountId>& authenticating_user_account_id)
    : begin_time(begin_time),
      authenticating_user_account_id(authenticating_user_account_id) {}

PinDialogManager::SignRequestState::SignRequestState(const SignRequestState&) =
    default;
PinDialogManager::SignRequestState&
PinDialogManager::SignRequestState::operator=(const SignRequestState&) =
    default;

PinDialogManager::SignRequestState::~SignRequestState() = default;

PinDialogManager::ActiveDialogState::ActiveDialogState(
    SecurityTokenPinDialogHost* host,
    const std::string& extension_id,
    const std::string& extension_name,
    int sign_request_id,
    security_token_pin::CodeType code_type)
    : host(host),
      extension_id(extension_id),
      extension_name(extension_name),
      sign_request_id(sign_request_id),
      code_type(code_type) {}

PinDialogManager::ActiveDialogState::~ActiveDialogState() = default;

PinDialogManager::SignRequestState* PinDialogManager::FindSignRequestState(
    const std::string& extension_id,
    int sign_request_id) {
  const ExtensionNameRequestIdPair key(extension_id, sign_request_id);
  const auto sign_request_iter = sign_requests_.find(key);
  if (sign_request_iter == sign_requests_.end())
    return nullptr;
  return &sign_request_iter->second;
}

void PinDialogManager::OnPinEntered(const std::string& user_input) {
  DCHECK(!active_dialog_state_->stop_pin_request_callback);
  last_response_closed_[active_dialog_state_->extension_id] = false;
  if (active_dialog_state_->request_pin_callback)
    std::move(active_dialog_state_->request_pin_callback).Run(user_input);
}

void PinDialogManager::OnPinDialogClosed() {
  DCHECK(!active_dialog_state_->request_pin_callback ||
         !active_dialog_state_->stop_pin_request_callback);

  last_response_closed_[active_dialog_state_->extension_id] = true;
  if (active_dialog_state_->request_pin_callback) {
    std::move(active_dialog_state_->request_pin_callback)
        .Run(/*user_input=*/std::string());
  }
  if (active_dialog_state_->stop_pin_request_callback)
    std::move(active_dialog_state_->stop_pin_request_callback).Run();
  active_dialog_state_.reset();
}

SecurityTokenPinDialogHost* PinDialogManager::GetHostForNewDialog() {
  if (added_dialog_hosts_.empty())
    return &default_dialog_host_;
  return added_dialog_hosts_.back();
}

void PinDialogManager::CloseActiveDialog() {
  if (!active_dialog_state_)
    return;

  // Ignore any further callbacks from the host. Instead of relying on the host
  // to call the closing callback, run OnPinDialogClosed() below explicitly.
  weak_factory_.InvalidateWeakPtrs();

  active_dialog_state_->host->CloseSecurityTokenPinDialog();
  OnPinDialogClosed();
  DCHECK(!active_dialog_state_);
}

}  // namespace chromeos