chromium/chrome/browser/smart_card/smart_card_reader_tracker_impl.cc

// 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.

#include "chrome/browser/smart_card/smart_card_reader_tracker_impl.h"

#include "base/time/time.h"

namespace {

constexpr base::TimeDelta kStatusChangeTimeout = base::Minutes(5);

template <class T>
bool CopyIfChanged(T& target, const T& source) {
  if (target != source) {
    target = source;
    return true;
  }
  return false;
}

template <class T>
std::unordered_set<T> ToUnorderedSet(const std::vector<T>& v) {
  return std::unordered_set<T>(v.begin(), v.end());
}

// Wrapper to handle the PC/SC nuisance that "no readers" is expressed as an
// error instead of an empty list.
std::vector<std::string> GetReadersFromResult(
    device::mojom::SmartCardListReadersResultPtr& result) {
  if (result->is_error()) {
    CHECK_EQ(result->get_error(),
             device::mojom::SmartCardError::kNoReadersAvailable);
    return std::vector<std::string>();
  }

  return result->get_readers();
}

}  // namespace

const base::TimeDelta SmartCardReaderTrackerImpl::kMinRefreshInterval =
    base::Seconds(3);

////////////////////////////////////////////////////////////////////////////////
// SmartCardReaderTrackerImpl::State

// Represents a state in the `SmartCardReaderTracker` state machine.
// It defines how the `SmartCardReaderTracker` reacts to calls to its public
// methods and to its callbacks.
class SmartCardReaderTrackerImpl::State {
 public:
  explicit State(base::WeakPtr<SmartCardReaderTrackerImpl> tracker)
      : tracker_(tracker) {}
  virtual ~State() = default;
  virtual std::string ToString() const = 0;
  // Called when SmartCardReaderTracker enters this state.
  // This will be the first method to be called after the constructor.
  virtual void Enter() {}

  // Called when SmartCardReaderTracker::Start() is called while
  // SmartCardReaderTracker is in this state.
  virtual void Start() {}

  // Called when SmartCardReaderTracker::Stop() is called while
  // SmartCardReaderTracker is in this state.
  virtual void Stop() {}

 protected:
  base::WeakPtr<SmartCardReaderTrackerImpl> tracker_;
};

////////////////////////////////////////////////////////////////////////////////
// SmartCardReaderTrackerImpl::Uninitialized declaration

// Initial state of `SmartCardReaderTracker`.
class SmartCardReaderTrackerImpl::Uninitialized
    : public SmartCardReaderTrackerImpl::State {
 public:
  explicit Uninitialized(base::WeakPtr<SmartCardReaderTrackerImpl> tracker)
      : State(tracker) {}
  std::string ToString() const override;
  void Start() override;
};

////////////////////////////////////////////////////////////////////////////////
// SmartCardReaderTrackerImpl::WaitReadersList declaration

// `SmartCardReaderTracker` called `ListReaders` and is waiting for its result.
class SmartCardReaderTrackerImpl::WaitReadersList
    : public SmartCardReaderTrackerImpl::State {
 public:
  WaitReadersList(base::WeakPtr<SmartCardReaderTrackerImpl> tracker,
                  mojo::Remote<device::mojom::SmartCardContext> context);
  WaitReadersList(
      base::WeakPtr<SmartCardReaderTrackerImpl> tracker,
      mojo::PendingRemote<device::mojom::SmartCardContext> pending_context);
  ~WaitReadersList() override;

  std::string ToString() const override;
  void Enter() override;

 private:
  void OnListReadersDone(device::mojom::SmartCardListReadersResultPtr result);
  void RemoveAbsentReadersFromTracker(
      const std::vector<std::string>& current_readers);
  std::vector<std::string> IdentifyNewReaders(
      const std::vector<std::string>& current_readers);

  mojo::Remote<device::mojom::SmartCardContext> context_;
  base::WeakPtrFactory<SmartCardReaderTrackerImpl::WaitReadersList>
      weak_ptr_factory_{this};
};

////////////////////////////////////////////////////////////////////////////////
// SmartCardReaderTrackerImpl::Tracking

// Main state of `SmartCardReaderTracker`.
//
// `SmartCardReaderTrackerImpl::readers_` has an up to date state of all
// available smart card readers.
//
// It has a pending `SmartCardContext::GetStatusChange`request which returns
// once a change takes place.
class SmartCardReaderTrackerImpl::Tracking
    : public SmartCardReaderTrackerImpl::State {
 public:
  Tracking(base::WeakPtr<SmartCardReaderTrackerImpl> tracker,
           mojo::Remote<device::mojom::SmartCardContext> context)
      : State(tracker),
        context_(std::move(context)),
        last_start_time_(base::Time::Now()) {}

  std::string ToString() const override { return "Tracking"; }

  void Enter() override {
    CHECK(tracker_->CanTrack());

    std::vector<device::mojom::SmartCardReaderStateInPtr> reader_states;

    // Get notified when a known, existing, reader changes its state or is
    // removed from the system.
    for (auto const& [name, info] : tracker_->readers_) {
      reader_states.push_back(device::mojom::SmartCardReaderStateIn::New(
          name, CurrentStateFlagsFromInfo(info), info.event_count));
    }

    // Instead of waiting indefinitely until anything changes, wait
    // for some time and then expect to receive a timeout from PC/SC.
    // Useful as a way of telling whether the PC/SC backend is still "alive".
    context_->GetStatusChange(
        kStatusChangeTimeout, std::move(reader_states),
        base::BindOnce(
            &SmartCardReaderTrackerImpl::Tracking::OnGetStatusChangeDone,
            weak_ptr_factory_.GetWeakPtr()));
  }

  void Start() override {
    base::TimeDelta elapsed = base::Time::Now() - last_start_time_;

    if (elapsed < kMinRefreshInterval) {
      CHECK(!cancelled_);
      tracker_->FulfillRequests();
      return;
    }

    CancelGetStatusChange();
  }

  void Stop() override { CancelGetStatusChange(); }

 private:
  // Cancels the outstanding GetStatusChange() request.
  void CancelGetStatusChange() {
    if (!cancelled_) {
      context_->Cancel(
          base::BindOnce(&SmartCardReaderTrackerImpl::Tracking::OnCancelDone,
                         weak_ptr_factory_.GetWeakPtr()));

      cancelled_ = true;
    }
  }

  void OnGetStatusChangeDone(
      device::mojom::SmartCardStatusChangeResultPtr result) {
    if (result->is_error()) {
      const device::mojom::SmartCardError error = result->get_error();
      if (error == device::mojom::SmartCardError::kCancelled ||
          error == device::mojom::SmartCardError::kTimeout) {
        TrackChangesOrGiveUp();
      } else {
        tracker_->observer_list_.NotifyError(result->get_error());
        tracker_->ChangeState(std::make_unique<Uninitialized>(tracker_));
      }
      return;
    }

    tracker_->UpdateCache(result->get_reader_states());
    TrackChangesOrGiveUp();
  }

  void OnCancelDone(device::mojom::SmartCardResultPtr result) {
    CHECK(cancelled_);
    if (result->is_success()) {
      // Nothing to do here. OnGetStatusChangeDone will get called with
      // a device::mojom::SmartCardError::kCancelled.
      return;
    }
    // Cancelation has failed.
    // We have no other option than to use the cache.
    tracker_->FulfillRequests();
  }

  void TrackChangesOrGiveUp() {
    if (tracker_->CanTrack()) {
      tracker_->ChangeState(
          std::make_unique<WaitReadersList>(tracker_, std::move(context_)));
    } else {
      tracker_->ChangeState(std::make_unique<Uninitialized>(tracker_));
    }
  }

  mojo::Remote<device::mojom::SmartCardContext> context_;
  base::Time last_start_time_;
  bool cancelled_{false};
  base::WeakPtrFactory<SmartCardReaderTrackerImpl::Tracking> weak_ptr_factory_{
      this};
};

////////////////////////////////////////////////////////////////////////////////
// SmartCardReaderTrackerImpl::WaitInitialReaderStatus

// It's waiting for a `SmartCardContext::GetStatusChange` request to return
// with information on the available, but unknown to the tracker, smart card
// readers.
class SmartCardReaderTrackerImpl::WaitInitialReaderStatus
    : public SmartCardReaderTrackerImpl::State {
 public:
  explicit WaitInitialReaderStatus(
      base::WeakPtr<SmartCardReaderTrackerImpl> tracker,
      mojo::Remote<device::mojom::SmartCardContext> context,
      std::vector<std::string>& new_readers)
      : State(tracker),
        context_(std::move(context)),
        new_readers_(new_readers) {}
  std::string ToString() const override { return "WaitInitialReaderStatus"; }
  void Enter() override {
    CHECK(!new_readers_.empty());

    std::vector<device::mojom::SmartCardReaderStateInPtr> reader_states;

    for (const std::string& reader_name : new_readers_) {
      CHECK_EQ(tracker_->readers_.count(reader_name), size_t(0));

      auto state = device::mojom::SmartCardReaderStateIn::New();
      state->reader = reader_name;

      state->current_state = device::mojom::SmartCardReaderStateFlags::New(
          /*unaware=*/true, false, false, false, false, false, false, false,
          false, false, false);

      reader_states.push_back(std::move(state));
    }

    context_->GetStatusChange(
        base::TimeDelta::Max(), std::move(reader_states),
        base::BindOnce(&SmartCardReaderTrackerImpl::WaitInitialReaderStatus::
                           OnGetStatusChangeDone,
                       weak_ptr_factory_.GetWeakPtr()));
  }

 private:
  void OnGetStatusChangeDone(
      device::mojom::SmartCardStatusChangeResultPtr result) {
    if (result->is_error()) {
      tracker_->FailRequests(device::mojom::SmartCardError::kNoService);
      tracker_->observer_list_.NotifyError(result->get_error());
      tracker_->ChangeState(std::make_unique<Uninitialized>(tracker_));
      return;
    }

    tracker_->UpdateCache(result->get_reader_states());
    // The cache is now up to date and can therefore be used to fulfill
    // all pending requests.
    tracker_->FulfillRequests();

    if (tracker_->CanTrack()) {
      tracker_->ChangeState(
          std::make_unique<Tracking>(tracker_, std::move(context_)));
    } else {
      tracker_->ChangeState(std::make_unique<Uninitialized>(tracker_));
    }
  }

  mojo::Remote<device::mojom::SmartCardContext> context_;
  std::vector<std::string> new_readers_;
  base::WeakPtrFactory<SmartCardReaderTrackerImpl::WaitInitialReaderStatus>
      weak_ptr_factory_{this};
};

////////////////////////////////////////////////////////////////////////////////
// SmartCardReaderTrackerImpl::WaitReadersList implementation

SmartCardReaderTrackerImpl::WaitReadersList::WaitReadersList(
    base::WeakPtr<SmartCardReaderTrackerImpl> tracker,
    mojo::Remote<device::mojom::SmartCardContext> context)
    : State(tracker), context_(std::move(context)) {}

SmartCardReaderTrackerImpl::WaitReadersList::WaitReadersList(
    base::WeakPtr<SmartCardReaderTrackerImpl> tracker,
    mojo::PendingRemote<device::mojom::SmartCardContext> pending_context)
    : State(tracker), context_(std::move(pending_context)) {}

SmartCardReaderTrackerImpl::WaitReadersList::~WaitReadersList() = default;

std::string SmartCardReaderTrackerImpl::WaitReadersList::ToString() const {
  return "WaitReadersList";
}

void SmartCardReaderTrackerImpl::WaitReadersList::Enter() {
  context_->ListReaders(base::BindOnce(
      &SmartCardReaderTrackerImpl::WaitReadersList::OnListReadersDone,
      weak_ptr_factory_.GetWeakPtr()));
}

void SmartCardReaderTrackerImpl::WaitReadersList::OnListReadersDone(
    device::mojom::SmartCardListReadersResultPtr result) {
  if (result->is_error() &&
      result->get_error() !=
          device::mojom::SmartCardError::kNoReadersAvailable) {
    tracker_->FailRequests(result->get_error());
    tracker_->observer_list_.NotifyError(result->get_error());
    tracker_->ChangeState(std::make_unique<Uninitialized>(tracker_));
    return;
  }

  auto current_readers = GetReadersFromResult(result);

  RemoveAbsentReadersFromTracker(current_readers);

  if (current_readers.empty()) {
    // RemoveAbsentReadersFromTracker() should have emptied it.
    CHECK(tracker_->readers_.empty());

    // There are no readers to be tracked.
    tracker_->FulfillRequests();
    tracker_->ChangeState(std::make_unique<Uninitialized>(tracker_));
    return;
  }

  std::vector<std::string> new_readers = IdentifyNewReaders(current_readers);

  if (new_readers.empty()) {
    // We already know about all existing readers and their states.
    // The cache can be considered still up to date.
    tracker_->FulfillRequests();
    // And we can go straight to Tracking (skipping WaitInitialReaderStatus).
    tracker_->ChangeState(
        std::make_unique<Tracking>(tracker_, std::move(context_)));
    return;
  }

  tracker_->ChangeState(std::make_unique<WaitInitialReaderStatus>(
      tracker_, std::move(context_), new_readers));
}

void SmartCardReaderTrackerImpl::WaitReadersList::
    RemoveAbsentReadersFromTracker(
        const std::vector<std::string>& current_readers) {
  auto current_readers_set = ToUnorderedSet(current_readers);

  for (auto it = tracker_->readers_.begin(); it != tracker_->readers_.end();) {
    std::string reader_name = it->first;
    if (current_readers_set.count(reader_name) == 0) {
      it = tracker_->readers_.erase(it);
      tracker_->observer_list_.NotifyReaderRemoved(reader_name);
    } else {
      ++it;
    }
  }
}

std::vector<std::string>
SmartCardReaderTrackerImpl::WaitReadersList::IdentifyNewReaders(
    const std::vector<std::string>& current_readers) {
  std::vector<std::string> new_readers;
  for (const std::string& reader_name : current_readers) {
    if (tracker_->readers_.count(reader_name) == 0) {
      new_readers.push_back(reader_name);
    }
  }
  return new_readers;
}

////////////////////////////////////////////////////////////////////////////////
// SmartCardReaderTrackerImpl::WaitContext

// State where the `SmartCardReaderTracker` is waiting for a
// `SmartCardContextFactory::CreateContext` call to return.
class SmartCardReaderTrackerImpl::WaitContext
    : public SmartCardReaderTrackerImpl::State {
 public:
  std::string ToString() const override { return "WaitContext"; }
  explicit WaitContext(base::WeakPtr<SmartCardReaderTrackerImpl> tracker)
      : State(tracker) {}

  void Enter() override {
    tracker_->context_factory_->CreateContext(base::BindOnce(
        &SmartCardReaderTrackerImpl::WaitContext::OnEstablishContextDone,
        weak_ptr_factory_.GetWeakPtr()));
  }

 private:
  void OnEstablishContextDone(
      device::mojom::SmartCardCreateContextResultPtr result) {
    if (result->is_error()) {
      tracker_->FailRequests(result->get_error());
      tracker_->observer_list_.NotifyError(result->get_error());
      tracker_->ChangeState(std::make_unique<Uninitialized>(tracker_));
      return;
    }

    tracker_->ChangeState(std::make_unique<WaitReadersList>(
        tracker_, std::move(result->get_context())));
  }

  base::WeakPtrFactory<SmartCardReaderTrackerImpl::WaitContext>
      weak_ptr_factory_{this};
};

////////////////////////////////////////////////////////////////////////////////
// SmartCardReaderTrackerImpl::Uninitialized implementation

std::string SmartCardReaderTrackerImpl::Uninitialized::ToString() const {
  return "Uninitialized";
}

void SmartCardReaderTrackerImpl::Uninitialized::Start() {
  tracker_->ChangeState(std::make_unique<WaitContext>(tracker_));
}

////////////////////////////////////////////////////////////////////////////////
// SmartCardReaderTrackerImpl

SmartCardReaderTrackerImpl::SmartCardReaderTrackerImpl(
    mojo::PendingRemote<device::mojom::SmartCardContextFactory> context_factory)
    : context_factory_(std::move(context_factory)) {
  state_ = std::make_unique<Uninitialized>(weak_factory_.GetWeakPtr());
}

SmartCardReaderTrackerImpl::~SmartCardReaderTrackerImpl() = default;

void SmartCardReaderTrackerImpl::Start(Observer* observer,
                                       StartCallback callback) {
  observer_list_.AddObserverIfMissing(observer);
  pending_get_readers_requests_.push(std::move(callback));
  state_->Start();
}

void SmartCardReaderTrackerImpl::Stop(Observer* observer) {
  observer_list_.RemoveObserver(observer);
  if (observer_list_.empty()) {
    state_->Stop();
  }
}

void SmartCardReaderTrackerImpl::ChangeState(
    std::unique_ptr<State> next_state) {
  CHECK(next_state);

  auto current_state = std::move(state_);

  VLOG(1) << "ChangeState: " << current_state->ToString() << " -> "
          << next_state->ToString();
  state_ = std::move(next_state);
  state_->Enter();

  // This method is invoked from inside `current_state`, so we postpone
  // destruction to ensure it has a chance to finish its current method
  // without crashing because it was deleted.
  base::SequencedTaskRunner::GetCurrentDefault()->DeleteSoon(
      FROM_HERE, std::move(current_state));
}

bool SmartCardReaderTrackerImpl::CanTrack() const {
  return !observer_list_.empty() && !readers_.empty();
}

void SmartCardReaderTrackerImpl::AddReader(
    const device::mojom::SmartCardReaderStateOut& state_out) {
  readers_.insert({state_out.reader, ReaderInfoFromMojoStateOut(state_out)});
  // NB: Not informing observers since there is no `OnReaderAdded` method.
  // See comment on the Observer class definition for details.
}

void SmartCardReaderTrackerImpl::AddOrUpdateReader(
    const device::mojom::SmartCardReaderStateOut& state_out) {
  auto it = readers_.find(state_out.reader);
  if (it == readers_.end()) {
    AddReader(state_out);
  } else {
    ReaderInfo& info = it->second;
    if (UpdateInfoIfChanged(info, state_out)) {
      observer_list_.NotifyReaderChanged(info);
    }
  }
}

void SmartCardReaderTrackerImpl::RemoveReader(
    const device::mojom::SmartCardReaderStateOut& state_out) {
  auto it = readers_.find(state_out.reader);
  if (it == readers_.end()) {
    return;
  }

  readers_.erase(it);
  observer_list_.NotifyReaderRemoved(state_out.reader);
}

void SmartCardReaderTrackerImpl::GetReadersFromCache(StartCallback callback) {
  std::vector<ReaderInfo> reader_list;
  reader_list.reserve(readers_.size());

  for (const auto& [_, info] : readers_) {
    reader_list.push_back(info);
  }

  std::move(callback).Run(std::move(reader_list));
}

void SmartCardReaderTrackerImpl::UpdateCache(
    const std::vector<device::mojom::SmartCardReaderStateOutPtr>&
        reader_states) {
  for (const auto& reader_state : reader_states) {
    if (reader_state->event_state->unknown ||
        reader_state->event_state->ignore ||
        reader_state->event_state->unaware) {
      RemoveReader(*reader_state.get());
    } else {
      AddOrUpdateReader(*reader_state.get());
    }
  }
}

void SmartCardReaderTrackerImpl::FulfillRequests() {
  while (!pending_get_readers_requests_.empty()) {
    GetReadersFromCache(std::move(pending_get_readers_requests_.front()));
    pending_get_readers_requests_.pop();
  }
}

void SmartCardReaderTrackerImpl::FailRequests(
    device::mojom::SmartCardError error) {
  while (!pending_get_readers_requests_.empty()) {
    base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE,
        base::BindOnce(
            std::move(pending_get_readers_requests_.front()),
            std::optional<std::vector<SmartCardReaderTracker::ReaderInfo>>()));

    pending_get_readers_requests_.pop();
  }
}

// static
SmartCardReaderTracker::ReaderInfo
SmartCardReaderTrackerImpl::ReaderInfoFromMojoStateOut(
    const device::mojom::SmartCardReaderStateOut& state_out) {
  ReaderInfo info;
  info.name = state_out.reader;
  CopyStateFlagsIfChanged(info, *state_out.event_state.get());
  info.event_count = state_out.event_count;
  info.answer_to_reset = state_out.answer_to_reset;
  return info;
}

// static
bool SmartCardReaderTrackerImpl::CopyStateFlagsIfChanged(
    ReaderInfo& info,
    const device::mojom::SmartCardReaderStateFlags& state_flags) {
  bool changed = false;

  changed |= CopyIfChanged(info.unavailable, state_flags.unavailable);
  changed |= CopyIfChanged(info.empty, state_flags.empty);
  changed |= CopyIfChanged(info.present, state_flags.present);
  changed |= CopyIfChanged(info.exclusive, state_flags.exclusive);
  changed |= CopyIfChanged(info.inuse, state_flags.inuse);
  changed |= CopyIfChanged(info.mute, state_flags.mute);
  changed |= CopyIfChanged(info.unpowered, state_flags.unpowered);

  return changed;
}

// static
bool SmartCardReaderTrackerImpl::UpdateInfoIfChanged(
    ReaderInfo& info,
    const device::mojom::SmartCardReaderStateOut& state_out) {
  CHECK_EQ(state_out.reader, info.name);
  bool changed = false;

  changed |= CopyStateFlagsIfChanged(info, *state_out.event_state.get());
  changed |= CopyIfChanged(info.event_count, state_out.event_count);
  changed |= CopyIfChanged(info.answer_to_reset, state_out.answer_to_reset);

  return changed;
}

// static
device::mojom::SmartCardReaderStateFlagsPtr
SmartCardReaderTrackerImpl::CurrentStateFlagsFromInfo(const ReaderInfo& info) {
  auto flags = device::mojom::SmartCardReaderStateFlags::New();

  flags->unavailable = info.unavailable;
  flags->empty = info.empty;
  flags->present = info.present;
  flags->exclusive = info.exclusive;
  flags->inuse = info.inuse;
  flags->mute = info.mute;
  flags->unpowered = info.unpowered;

  return flags;
}