| // 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; |
| } |