| // Copyright 2023 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/webauthn/android/cable_registration_state.h" |
| |
| #include <array> |
| |
| #include "base/base64.h" |
| #include "base/compiler_specific.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback.h" |
| #include "crypto/random.h" |
| #include "device/fido/cable/v2_handshake.h" |
| #include "device/fido/features.h" |
| #include "third_party/boringssl/src/include/openssl/ec_key.h" |
| |
| using device::cablev2::authenticator::Registration; |
| |
| namespace webauthn::authenticator { |
| |
| RegistrationState::SystemInterface::~SystemInterface() = default; |
| |
| RegistrationState::RegistrationState(std::unique_ptr<SystemInterface> interface) |
| : interface_(std::move(interface)) {} |
| |
| RegistrationState::~RegistrationState() = default; |
| |
| void RegistrationState::Register() { |
| DCHECK(!linking_registration_); |
| DCHECK(!sync_registration_); |
| |
| linking_registration_ = interface_->NewRegistration( |
| device::cablev2::authenticator::Registration::Type::LINKING, |
| base::BindOnce(&RegistrationState::OnLinkingRegistrationReady, |
| base::Unretained(this)), |
| base::BindRepeating(&RegistrationState::OnEvent, base::Unretained(this))); |
| sync_registration_ = interface_->NewRegistration( |
| device::cablev2::authenticator::Registration::Type::SYNC, |
| base::BindOnce(&RegistrationState::OnSyncRegistrationReady, |
| base::Unretained(this)), |
| base::BindRepeating(&RegistrationState::OnEvent, base::Unretained(this))); |
| interface_->CanDeviceSupportCable(base::BindOnce( |
| &RegistrationState::OnDeviceSupportResult, base::Unretained(this))); |
| interface_->AmInWorkProfile(base::BindOnce( |
| &RegistrationState::OnWorkProfileResult, base::Unretained(this))); |
| } |
| |
| // have_data_for_sync returns true if this object has loaded enough state to |
| // put information into sync's DeviceInfo. |
| bool RegistrationState::have_data_for_sync() const { |
| return device_supports_cable_.has_value() && |
| am_in_work_profile_.has_value() && sync_registration_ != nullptr && |
| sync_registration_->contact_id() && have_play_services_data(); |
| } |
| |
| void RegistrationState::SignalSyncWhenReady() { |
| if (sync_registration_ && !sync_registration_->contact_id()) { |
| sync_registration_->PrepareContactID(); |
| } |
| if (!have_play_services_data() && !play_services_query_pending_) { |
| QueryPlayServices(); |
| } |
| signal_sync_when_ready_ = true; |
| } |
| |
| bool RegistrationState::have_play_services_data() const { |
| // If there's no result, then we're not ready. |
| if (!have_link_data_from_play_services_) { |
| return false; |
| } |
| // If there's a query already pending then the result must be stale and |
| // there's nothing more to do here. |
| if (play_services_query_pending_) { |
| return false; |
| } |
| const base::TimeDelta staleness = |
| base::TimeTicks::Now() - link_data_from_play_services_timeticks_; |
| return staleness < base::Hours(12); |
| } |
| |
| void RegistrationState::QueryPlayServices() { |
| DCHECK(!play_services_query_pending_); |
| |
| play_services_query_pending_ = true; |
| interface_->GetPrelinkFromPlayServices( |
| base::BindOnce(&RegistrationState::OnHavePlayServicesLinkingInformation, |
| base::Unretained(this))); |
| } |
| |
| void RegistrationState::OnHavePlayServicesLinkingInformation( |
| std::optional<std::vector<uint8_t>> cbor) { |
| DCHECK(play_services_query_pending_); |
| |
| play_services_query_pending_ = false; |
| link_data_from_play_services_ = std::move(cbor); |
| have_link_data_from_play_services_ = true; |
| link_data_from_play_services_timeticks_ = base::TimeTicks::Now(); |
| MaybeSignalSync(); |
| } |
| |
| void RegistrationState::OnLinkingRegistrationReady() { |
| MaybeFlushPendingEvent(); |
| } |
| |
| void RegistrationState::OnSyncRegistrationReady() { |
| MaybeSignalSync(); |
| } |
| |
| void RegistrationState::OnEvent(std::unique_ptr<Registration::Event> event) { |
| pending_event_ = std::move(event); |
| |
| MaybeFlushPendingEvent(); |
| } |
| |
| void RegistrationState::MaybeFlushPendingEvent() { |
| if (!pending_event_) { |
| return; |
| } |
| |
| if (pending_event_->source == Registration::Type::LINKING && |
| !pending_event_->contact_id) { |
| // This GCM message is from a QR-linked peer so it needs the contact ID |
| // to be processed. |
| pending_event_->contact_id = linking_registration_->contact_id(); |
| |
| if (!pending_event_->contact_id) { |
| // The contact ID isn't ready yet. Wait until it is. |
| linking_registration_->PrepareContactID(); |
| return; |
| } |
| } |
| |
| std::unique_ptr<Registration::Event> event(std::move(pending_event_)); |
| if (event->source == Registration::Type::SYNC) { |
| // If this is from a synced peer then we limit how old the keys can be. |
| // Clank will update its device information once per day (when launched) |
| // and we piggyback on that to transmit fresh keys. Therefore syncing |
| // peers should have reasonably recent information. |
| uint64_t id; |
| static_assert(std::tuple_size_v<decltype(event->pairing_id)> == sizeof(id)); |
| UNSAFE_TODO(memcpy(&id, event->pairing_id.data(), sizeof(id))); |
| |
| // A maximum age is enforced for sync secrets so that any leak of |
| // information isn't valid forever. The desktop ignores DeviceInfo |
| // records with information that is too old so this should never happen |
| // with honest clients. |
| if (id > std::numeric_limits<uint32_t>::max() || |
| device::cablev2::sync::IDIsMoreThanNPeriodsOld( |
| static_cast<uint32_t>(id), |
| device::cablev2::kMaxSyncInfoDaysForProducer)) { |
| return; |
| } |
| } |
| } |
| |
| void RegistrationState::MaybeSignalSync() { |
| if (!signal_sync_when_ready_ || !have_data_for_sync()) { |
| return; |
| } |
| signal_sync_when_ready_ = false; |
| |
| interface_->RefreshLocalDeviceInfo(); |
| } |
| |
| void RegistrationState::OnDeviceSupportResult(bool result) { |
| device_supports_cable_ = result; |
| MaybeSignalSync(); |
| } |
| |
| void RegistrationState::OnWorkProfileResult(bool result) { |
| am_in_work_profile_ = result; |
| MaybeSignalSync(); |
| } |
| |
| } // namespace webauthn::authenticator |