| // Copyright 2021 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/cablev2_devices.h" |
| |
| #include <algorithm> |
| #include <array> |
| #include <string> |
| #include <string_view> |
| #include <vector> |
| |
| #include "base/base64.h" |
| #include "base/containers/contains.h" |
| #include "base/i18n/time_formatting.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/time/time.h" |
| #include "chrome/browser/profiles/profile.h" |
| #include "chrome/browser/sync/device_info_sync_service_factory.h" |
| #include "components/sync_device_info/device_info.h" |
| #include "components/sync_device_info/device_info_sync_service.h" |
| #include "components/sync_device_info/device_info_tracker.h" |
| #include "device/fido/cable/cable_discovery_data.h" |
| #include "device/fido/cable/v2_constants.h" |
| #include "device/fido/cable/v2_handshake.h" |
| #include "third_party/icu/source/i18n/unicode/timezone.h" |
| |
| using device::cablev2::Pairing; |
| |
| namespace cablev2 { |
| |
| namespace { |
| |
| // NameForDisplay removes line-breaking characters from `raw_name` to ensure |
| // that the transport-selection UI isn't too badly broken by nonsense names. |
| static std::string NameForDisplay(std::string_view raw_name) { |
| std::u16string unicode_name = base::UTF8ToUTF16(raw_name); |
| std::u16string_view trimmed_name = |
| base::TrimWhitespace(unicode_name, base::TRIM_ALL); |
| // These are all the Unicode mandatory line-breaking characters |
| // (https://www.unicode.org/reports/tr14/tr14-32.html#Properties). |
| constexpr char16_t kLineTerminators[] = {0x0a, 0x0b, 0x0c, 0x0d, 0x85, 0x2028, |
| 0x2029, |
| // Array must be NUL terminated. |
| 0}; |
| std::u16string nonbreaking_name; |
| base::RemoveChars(trimmed_name, kLineTerminators, &nonbreaking_name); |
| return base::UTF16ToUTF8(nonbreaking_name); |
| } |
| |
| std::vector<std::unique_ptr<Pairing>> GetSyncedDevices(Profile* const profile) { |
| std::vector<std::unique_ptr<Pairing>> ret; |
| syncer::DeviceInfoSyncService* const sync_service = |
| DeviceInfoSyncServiceFactory::GetForProfile(profile); |
| if (!sync_service) { |
| return ret; |
| } |
| |
| syncer::DeviceInfoTracker* const tracker = |
| sync_service->GetDeviceInfoTracker(); |
| std::vector<const syncer::DeviceInfo*> devices = tracker->GetAllDeviceInfo(); |
| |
| const base::Time now = base::Time::Now(); |
| for (const syncer::DeviceInfo* device : devices) { |
| std::unique_ptr<Pairing> pairing = PairingFromSyncedDevice(device, now); |
| if (!pairing) { |
| continue; |
| } |
| ret.emplace_back(std::move(pairing)); |
| } |
| |
| return ret; |
| } |
| |
| } // namespace |
| |
| // PairingFromSyncedDevice extracts the caBLEv2 information from Sync's |
| // DeviceInfo (if any) into a caBLEv2 pairing. It may return nullptr. |
| std::unique_ptr<Pairing> PairingFromSyncedDevice( |
| const syncer::DeviceInfo* device, |
| const base::Time& now) { |
| const std::optional<syncer::DeviceInfo::PhoneAsASecurityKeyInfo>& |
| maybe_paask_info = device->paask_info(); |
| if (!maybe_paask_info) { |
| return nullptr; |
| } |
| const syncer::DeviceInfo::PhoneAsASecurityKeyInfo& paask_info = |
| *maybe_paask_info; |
| |
| if (device::cablev2::sync::IDIsMoreThanNPeriodsOld( |
| paask_info.id, device::cablev2::kMaxSyncInfoDaysForConsumer)) { |
| // Old entries are dropped as phones won't honor linking information that is |
| // excessively old. |
| return nullptr; |
| } |
| |
| auto pairing = std::make_unique<Pairing>(); |
| pairing->from_sync_deviceinfo = true; |
| pairing->name = NameForDisplay(device->client_name()); |
| |
| const std::optional<device::cablev2::tunnelserver::KnownDomainID> |
| tunnel_server_domain = device::cablev2::tunnelserver::ToKnownDomainID( |
| paask_info.tunnel_server_domain); |
| if (!tunnel_server_domain) { |
| // It's possible that a phone is running a more modern version of Chrome |
| // and uses an assigned tunnel server domain that is unknown to this code. |
| return nullptr; |
| } |
| |
| pairing->tunnel_server_domain = *tunnel_server_domain; |
| pairing->contact_id = paask_info.contact_id; |
| pairing->peer_public_key_x962 = paask_info.peer_public_key_x962; |
| pairing->secret.assign(paask_info.secret.begin(), paask_info.secret.end()); |
| pairing->last_updated = device->last_updated_timestamp(); |
| |
| // The pairing ID from sync is zero-padded to the standard length. |
| static_assert(device::cablev2::kPairingIDSize >= sizeof(paask_info.id), ""); |
| pairing->id.assign(device::cablev2::kPairingIDSize, 0); |
| base::span(pairing->id) |
| .copy_prefix_from(base::byte_span_from_ref(paask_info.id)); |
| |
| // The channel priority is only approximate and exists to help testing and |
| // development. I.e. we want the development or Canary install on a device to |
| // shadow the stable channel so that it's possible to test things. This code |
| // is matching the string generated by `FormatUserAgentForSync`. |
| const std::string& user_agent = device->sync_user_agent(); |
| if (user_agent.find("-devel") != std::string::npos) { |
| pairing->channel_priority = 5; |
| } else if (user_agent.find("(canary)") != std::string::npos) { |
| pairing->channel_priority = 4; |
| } else if (user_agent.find("(dev)") != std::string::npos) { |
| pairing->channel_priority = 3; |
| } else if (user_agent.find("(beta)") != std::string::npos) { |
| pairing->channel_priority = 2; |
| } else if (user_agent.find("(stable)") != std::string::npos) { |
| pairing->channel_priority = 1; |
| } else { |
| pairing->channel_priority = 0; |
| } |
| |
| return pairing; |
| } |
| |
| KnownDevices::KnownDevices() = default; |
| KnownDevices::~KnownDevices() = default; |
| |
| // static |
| std::unique_ptr<KnownDevices> KnownDevices::FromProfile(Profile* profile) { |
| if (profile->IsOffTheRecord()) { |
| // For Incognito windows we collect the devices from the parent profile. |
| // The `AuthenticatorRequestDialogController` will notice that it's an OTR |
| // profile and display a confirmation interstitial for makeCredential calls. |
| profile = profile->GetOriginalProfile(); |
| } |
| |
| auto ret = std::make_unique<KnownDevices>(); |
| ret->synced_devices = GetSyncedDevices(profile); |
| return ret; |
| } |
| |
| std::vector<std::string_view> KnownDevices::Names() const { |
| std::vector<std::string_view> names; |
| names.reserve(this->synced_devices.size()); |
| for (const std::unique_ptr<device::cablev2::Pairing>& device : |
| this->synced_devices) { |
| names.push_back(device->name); |
| } |
| return names; |
| } |
| |
| } // namespace cablev2 |