diff options
author | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2020-11-18 16:35:47 +0100 |
---|---|---|
committer | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2020-11-18 15:45:54 +0000 |
commit | 32f5a1c56531e4210bc4cf8d8c7825d66e081888 (patch) | |
tree | eeeec6822f4d738d8454525233fd0e2e3a659e6d /chromium/device | |
parent | 99677208ff3b216fdfec551fbe548da5520cd6fb (diff) | |
download | qtwebengine-chromium-32f5a1c56531e4210bc4cf8d8c7825d66e081888.tar.gz |
BASELINE: Update Chromium to 87.0.4280.67
Change-Id: Ib157360be8c2ffb2c73125751a89f60e049c1d54
Reviewed-by: Allan Sandfeld Jensen <allan.jensen@qt.io>
Diffstat (limited to 'chromium/device')
219 files changed, 10107 insertions, 6307 deletions
diff --git a/chromium/device/BUILD.gn b/chromium/device/BUILD.gn index 932276b8b9a..db0daed3d42 100644 --- a/chromium/device/BUILD.gn +++ b/chromium/device/BUILD.gn @@ -15,11 +15,20 @@ if (is_mac) { import("//build/config/mac/mac_sdk.gni") } +# This file depends on the legacy global sources assignment filter. It should +# be converted to check target platform before assigning source files to the +# sources variable. Remove this import and set_sources_assignment_filter call +# when the file has been converted. See https://crbug.com/1018739 for details. +import("//build/config/deprecated_default_sources_assignment_filter.gni") +set_sources_assignment_filter(deprecated_default_sources_assignment_filter) + is_linux_without_udev = (is_linux || is_chromeos) && !use_udev test("device_unittests") { sources = [ "base/synchronization/one_writer_seqlock_unittest.cc", + "bluetooth/adapter_unittest.cc", + "bluetooth/advertisement_unittest.cc", "bluetooth/bluetooth_adapter_android_unittest.cc", "bluetooth/bluetooth_adapter_mac_metrics_unittest.mm", "bluetooth/bluetooth_adapter_mac_unittest.mm", @@ -144,12 +153,14 @@ test("device_unittests") { "fido/ctap_request_unittest.cc", "fido/ctap_response_unittest.cc", "fido/fake_fido_discovery_unittest.cc", + "fido/fido_device_authenticator_unittest.cc", "fido/fido_device_discovery_unittest.cc", "fido/fido_parsing_utils_unittest.cc", "fido/fido_request_handler_unittest.cc", "fido/get_assertion_handler_unittest.cc", "fido/get_assertion_task_unittest.cc", "fido/hid/fido_hid_message_unittest.cc", + "fido/large_blob_unittest.cc", "fido/mac/browsing_data_deletion_unittest.mm", "fido/mac/credential_metadata_unittest.cc", "fido/mac/get_assertion_operation_unittest_mac.mm", diff --git a/chromium/device/base/BUILD.gn b/chromium/device/base/BUILD.gn index d51617965f7..73e821bb367 100644 --- a/chromium/device/base/BUILD.gn +++ b/chromium/device/base/BUILD.gn @@ -5,6 +5,13 @@ import("//build/config/features.gni") import("//device/vr/buildflags/buildflags.gni") +# This file depends on the legacy global sources assignment filter. It should +# be converted to check target platform before assigning source files to the +# sources variable. Remove this import and set_sources_assignment_filter call +# when the file has been converted. See https://crbug.com/1018739 for details. +import("//build/config/deprecated_default_sources_assignment_filter.gni") +set_sources_assignment_filter(deprecated_default_sources_assignment_filter) + component("base") { output_name = "device_base" diff --git a/chromium/device/base/features.cc b/chromium/device/base/features.cc index 899c3269372..1f7425f1975 100644 --- a/chromium/device/base/features.cc +++ b/chromium/device/base/features.cc @@ -8,10 +8,15 @@ namespace device { -#if defined(OS_MAC) || defined(OS_WIN) +#if defined(OS_MAC) const base::Feature kNewUsbBackend{"NewUsbBackend", base::FEATURE_DISABLED_BY_DEFAULT}; -#endif // defined(OS_MAC) || defined(OS_WIN) +#endif // defined(OS_MAC) + +#if defined(OS_WIN) +const base::Feature kNewUsbBackend{"NewUsbBackend", + base::FEATURE_ENABLED_BY_DEFAULT}; +#endif // defined(OS_WIN) #if defined(OS_WIN) const base::Feature kNewBLEWinImplementation{"NewBLEWinImplementation", @@ -39,16 +44,6 @@ const base::Feature kWebXrOrientationSensorDevice { }; #endif // BUILDFLAG(ENABLE_VR) namespace features { -#if BUILDFLAG(ENABLE_OCULUS_VR) -// Controls WebXR support for the Oculus Runtime. -const base::Feature kOculusVR{"OculusVR", base::FEATURE_DISABLED_BY_DEFAULT}; -#endif // ENABLE_OCULUS_VR - -#if BUILDFLAG(ENABLE_OPENVR) -// Controls WebXR support for the OpenVR Runtime. -const base::Feature kOpenVR{"OpenVR", base::FEATURE_DISABLED_BY_DEFAULT}; -#endif // ENABLE_OPENVR - #if BUILDFLAG(ENABLE_OPENXR) // Controls WebXR support for the OpenXR Runtime. const base::Feature kOpenXR{"OpenXR", base::FEATURE_ENABLED_BY_DEFAULT}; @@ -57,7 +52,7 @@ const base::Feature kOpenXR{"OpenXR", base::FEATURE_ENABLED_BY_DEFAULT}; #if BUILDFLAG(ENABLE_WINDOWS_MR) // Controls WebXR support for the Windows Mixed Reality Runtime. const base::Feature kWindowsMixedReality{"WindowsMixedReality", - base::FEATURE_ENABLED_BY_DEFAULT}; + base::FEATURE_DISABLED_BY_DEFAULT}; #endif // ENABLE_WINDOWS_MR } // namespace features } // namespace device diff --git a/chromium/device/base/features.h b/chromium/device/base/features.h index e017e526826..1d9143d4961 100644 --- a/chromium/device/base/features.h +++ b/chromium/device/base/features.h @@ -28,12 +28,6 @@ DEVICE_BASE_EXPORT extern const base::Feature kWebXrOrientationSensorDevice; // New features should be added to the device::features namespace. namespace features { -#if BUILDFLAG(ENABLE_OCULUS_VR) -DEVICE_BASE_EXPORT extern const base::Feature kOculusVR; -#endif // ENABLE_OCULUS_VR -#if BUILDFLAG(ENABLE_OPENVR) -DEVICE_BASE_EXPORT extern const base::Feature kOpenVR; -#endif // ENABLE_OPENVR #if BUILDFLAG(ENABLE_OPENXR) DEVICE_BASE_EXPORT extern const base::Feature kOpenXR; #endif // ENABLE_OPENXR diff --git a/chromium/device/base/synchronization/one_writer_seqlock.cc b/chromium/device/base/synchronization/one_writer_seqlock.cc index b3b8a71f80c..213b6301c05 100644 --- a/chromium/device/base/synchronization/one_writer_seqlock.cc +++ b/chromium/device/base/synchronization/one_writer_seqlock.cc @@ -8,10 +8,35 @@ namespace device { OneWriterSeqLock::OneWriterSeqLock() : sequence_(0) {} -base::subtle::Atomic32 OneWriterSeqLock::ReadBegin(uint32_t max_retries) const { - base::subtle::Atomic32 version; +void OneWriterSeqLock::AtomicWriterMemcpy(void* dest, + const void* src, + size_t size) { + DCHECK(!(reinterpret_cast<std::uintptr_t>(dest) % 4)); + DCHECK(!(reinterpret_cast<std::uintptr_t>(src) % 4)); + DCHECK(size % 4 == 0); + for (size_t i = 0; i < size / 4; ++i) { + reinterpret_cast<std::atomic<int32_t>*>(dest)[i].store( + reinterpret_cast<const int32_t*>(src)[i], std::memory_order_relaxed); + } +} + +void OneWriterSeqLock::AtomicReaderMemcpy(void* dest, + const void* src, + size_t size) { + DCHECK(!(reinterpret_cast<std::uintptr_t>(dest) % 4)); + DCHECK(!(reinterpret_cast<std::uintptr_t>(src) % 4)); + DCHECK(size % 4 == 0); + for (size_t i = 0; i < size / 4; ++i) { + reinterpret_cast<int32_t*>(dest)[i] = + reinterpret_cast<const std::atomic<int32_t>*>(src)[i].load( + std::memory_order_relaxed); + } +} + +int32_t OneWriterSeqLock::ReadBegin(uint32_t max_retries) const { + int32_t version; for (uint32_t i = 0; i <= max_retries; ++i) { - version = base::subtle::Acquire_Load(&sequence_); + version = sequence_.load(std::memory_order_acquire); // If the counter is even, then the associated data might be in a // consistent state, so we can try to read. @@ -27,16 +52,19 @@ base::subtle::Atomic32 OneWriterSeqLock::ReadBegin(uint32_t max_retries) const { return version; } -bool OneWriterSeqLock::ReadRetry(base::subtle::Atomic32 version) const { +bool OneWriterSeqLock::ReadRetry(int32_t version) const { // If the sequence number was updated then a read should be re-attempted. // -- Load fence, read membarrier - return base::subtle::Release_Load(&sequence_) != version; + atomic_thread_fence(std::memory_order_acquire); + return sequence_.load(std::memory_order_relaxed) != version; } void OneWriterSeqLock::WriteBegin() { // Increment the sequence number to odd to indicate the beginning of a write // update. - base::subtle::Barrier_AtomicIncrement(&sequence_, 1); + int32_t version = sequence_.fetch_add(1, std::memory_order_relaxed); + atomic_thread_fence(std::memory_order_release); + DCHECK((version & 1) == 0); // -- Store fence, write membarrier } @@ -44,7 +72,8 @@ void OneWriterSeqLock::WriteEnd() { // Increment the sequence to an even number to indicate the completion of // a write update. // -- Store fence, write membarrier - base::subtle::Barrier_AtomicIncrement(&sequence_, 1); + int32_t version = sequence_.fetch_add(1, std::memory_order_release); + DCHECK((version & 1) != 0); } } // namespace device diff --git a/chromium/device/base/synchronization/one_writer_seqlock.h b/chromium/device/base/synchronization/one_writer_seqlock.h index 0bd3e89b5f8..2ad75e8af57 100644 --- a/chromium/device/base/synchronization/one_writer_seqlock.h +++ b/chromium/device/base/synchronization/one_writer_seqlock.h @@ -5,6 +5,8 @@ #ifndef DEVICE_BASE_SYNCHRONIZATION_ONE_WRITER_SEQLOCK_H_ #define DEVICE_BASE_SYNCHRONIZATION_ONE_WRITER_SEQLOCK_H_ +#include <atomic> + #include "base/atomicops.h" #include "base/macros.h" #include "base/threading/platform_thread.h" @@ -36,16 +38,22 @@ namespace device { class OneWriterSeqLock { public: OneWriterSeqLock(); + // Copies data from src into dest using atomic stores. This should be used by + // writer of SeqLock. Data must be 4-byte aligned. + static void AtomicWriterMemcpy(void* dest, const void* src, size_t size); + // Copies data from src into dest using atomic loads. This should be used by + // readers of SeqLock. Data must be 4-byte aligned. + static void AtomicReaderMemcpy(void* dest, const void* src, size_t size); // ReadBegin returns |sequence_| when it is even, or when it has retried // |max_retries| times. Omitting |max_retries| results in ReadBegin not // returning until |sequence_| is even. - base::subtle::Atomic32 ReadBegin(uint32_t max_retries = UINT32_MAX) const; - bool ReadRetry(base::subtle::Atomic32 version) const; + int32_t ReadBegin(uint32_t max_retries = UINT32_MAX) const; + bool ReadRetry(int32_t version) const; void WriteBegin(); void WriteEnd(); private: - base::subtle::Atomic32 sequence_; + std::atomic<int32_t> sequence_; DISALLOW_COPY_AND_ASSIGN(OneWriterSeqLock); }; diff --git a/chromium/device/base/synchronization/one_writer_seqlock_unittest.cc b/chromium/device/base/synchronization/one_writer_seqlock_unittest.cc index fe88c42f693..938711c986d 100644 --- a/chromium/device/base/synchronization/one_writer_seqlock_unittest.cc +++ b/chromium/device/base/synchronization/one_writer_seqlock_unittest.cc @@ -5,8 +5,8 @@ #include "device/base/synchronization/one_writer_seqlock.h" #include <stdlib.h> +#include <atomic> -#include "base/atomic_ref_count.h" #include "base/macros.h" #include "base/third_party/dynamic_annotations/dynamic_annotations.h" #include "base/threading/platform_thread.h" @@ -18,7 +18,8 @@ namespace device { // Basic test to make sure that basic operation works correctly. struct TestData { - unsigned a, b, c; + // Data copies larger than a cache line. + uint32_t buffer[32]; }; class BasicSeqLockTestThread : public base::PlatformThread::Delegate { @@ -27,13 +28,13 @@ class BasicSeqLockTestThread : public base::PlatformThread::Delegate { void Init(OneWriterSeqLock* seqlock, TestData* data, - base::AtomicRefCount* ready) { + std::atomic<int>* ready) { seqlock_ = seqlock; data_ = data; ready_ = ready; } void ThreadMain() override { - while (ready_->IsZero()) { + while (!*ready_) { base::PlatformThread::YieldCurrentThread(); } @@ -42,24 +43,54 @@ class BasicSeqLockTestThread : public base::PlatformThread::Delegate { base::subtle::Atomic32 version; do { version = seqlock_->ReadBegin(); - copy = *data_; + OneWriterSeqLock::AtomicReaderMemcpy(©, data_, sizeof(TestData)); } while (seqlock_->ReadRetry(version)); - EXPECT_EQ(copy.a + 100, copy.b); - EXPECT_EQ(copy.c, copy.b + copy.a); + for (unsigned j = 1; j < 32; ++j) + EXPECT_EQ(copy.buffer[j], copy.buffer[0] + copy.buffer[j - 1]); } - ready_->Decrement(); + --(*ready_); } private: OneWriterSeqLock* seqlock_; TestData* data_; - base::AtomicRefCount* ready_; + std::atomic<int>* ready_; DISALLOW_COPY_AND_ASSIGN(BasicSeqLockTestThread); }; +class MaxRetriesSeqLockTestThread : public base::PlatformThread::Delegate { + public: + MaxRetriesSeqLockTestThread() = default; + + void Init(OneWriterSeqLock* seqlock, std::atomic<int>* ready) { + seqlock_ = seqlock; + ready_ = ready; + } + void ThreadMain() override { + while (!*ready_) { + base::PlatformThread::YieldCurrentThread(); + } + + for (unsigned i = 0; i < 10; ++i) { + base::subtle::Atomic32 version; + version = seqlock_->ReadBegin(100); + + EXPECT_NE(version & 1, 0); + } + + --*ready_; + } + + private: + OneWriterSeqLock* seqlock_; + std::atomic<int>* ready_; + + DISALLOW_COPY_AND_ASSIGN(MaxRetriesSeqLockTestThread); +}; + #if defined(OS_ANDROID) #define MAYBE_ManyThreads FLAKY_ManyThreads #else @@ -67,8 +98,8 @@ class BasicSeqLockTestThread : public base::PlatformThread::Delegate { #endif TEST(OneWriterSeqLockTest, MAYBE_ManyThreads) { OneWriterSeqLock seqlock; - TestData data = {0, 0, 0}; - base::AtomicRefCount ready(0); + TestData data; + std::atomic<int> ready(0); ANNOTATE_BENIGN_RACE_SIZED(&data, sizeof(data), "Racey reads are discarded"); @@ -76,24 +107,27 @@ TEST(OneWriterSeqLockTest, MAYBE_ManyThreads) { BasicSeqLockTestThread threads[kNumReaderThreads]; base::PlatformThreadHandle handles[kNumReaderThreads]; - for (unsigned i = 0; i < kNumReaderThreads; ++i) + for (uint32_t i = 0; i < kNumReaderThreads; ++i) threads[i].Init(&seqlock, &data, &ready); - for (unsigned i = 0; i < kNumReaderThreads; ++i) + for (uint32_t i = 0; i < kNumReaderThreads; ++i) ASSERT_TRUE(base::PlatformThread::Create(0, &threads[i], &handles[i])); // The main thread is the writer, and the spawned are readers. - unsigned counter = 0; + uint32_t counter = 0; for (;;) { + TestData new_data; + new_data.buffer[0] = counter++; + for (unsigned i = 1; i < 32; ++i) { + new_data.buffer[i] = new_data.buffer[0] + new_data.buffer[i - 1]; + } seqlock.WriteBegin(); - data.a = counter++; - data.b = data.a + 100; - data.c = data.b + data.a; + OneWriterSeqLock::AtomicWriterMemcpy(&data, &new_data, sizeof(TestData)); seqlock.WriteEnd(); if (counter == 1) - ready.Increment(kNumReaderThreads); + ready += kNumReaderThreads; - if (ready.IsZero()) + if (!ready) break; } @@ -101,4 +135,29 @@ TEST(OneWriterSeqLockTest, MAYBE_ManyThreads) { base::PlatformThread::Join(handles[i]); } +TEST(OneWriterSeqLockTest, MaxRetries) { + OneWriterSeqLock seqlock; + std::atomic<int> ready(0); + + static const unsigned kNumReaderThreads = 3; + MaxRetriesSeqLockTestThread threads[kNumReaderThreads]; + base::PlatformThreadHandle handles[kNumReaderThreads]; + + for (uint32_t i = 0; i < kNumReaderThreads; ++i) + threads[i].Init(&seqlock, &ready); + for (uint32_t i = 0; i < kNumReaderThreads; ++i) + ASSERT_TRUE(base::PlatformThread::Create(0, &threads[i], &handles[i])); + + // The main thread is the writer, and the spawned are readers. + seqlock.WriteBegin(); + ready += kNumReaderThreads; + while (ready) { + base::PlatformThread::YieldCurrentThread(); + } + seqlock.WriteEnd(); + + for (unsigned i = 0; i < kNumReaderThreads; ++i) + base::PlatformThread::Join(handles[i]); +} + } // namespace device diff --git a/chromium/device/bluetooth/BUILD.gn b/chromium/device/bluetooth/BUILD.gn index e4c977b4895..40539eb8592 100644 --- a/chromium/device/bluetooth/BUILD.gn +++ b/chromium/device/bluetooth/BUILD.gn @@ -12,6 +12,13 @@ if (is_chromeos) { use_real_dbus_clients = false } +# This file depends on the legacy global sources assignment filter. It should +# be converted to check target platform before assigning source files to the +# sources variable. Remove this import and set_sources_assignment_filter call +# when the file has been converted. See https://crbug.com/1018739 for details. +import("//build/config/deprecated_default_sources_assignment_filter.gni") +set_sources_assignment_filter(deprecated_default_sources_assignment_filter) + config("bluetooth_config") { if (is_win) { ldflags = [ @@ -36,6 +43,8 @@ source_set("deprecated_experimental_mojo") { "//device/bluetooth/public/mojom/gatt_result_type_converter.h", "adapter.cc", "adapter.h", + "advertisement.cc", + "advertisement.h", "device.cc", "device.h", "discovery_session.cc", @@ -107,8 +116,6 @@ component("bluetooth") { "bluetooth_adapter_android.h", "bluetooth_adapter_factory.cc", "bluetooth_adapter_factory.h", - "bluetooth_adapter_factory_wrapper.cc", - "bluetooth_adapter_factory_wrapper.h", "bluetooth_adapter_mac.h", "bluetooth_adapter_mac.mm", "bluetooth_adapter_mac_metrics.h", diff --git a/chromium/device/bluetooth/adapter.cc b/chromium/device/bluetooth/adapter.cc index dad05519932..13f0a70b818 100644 --- a/chromium/device/bluetooth/adapter.cc +++ b/chromium/device/bluetooth/adapter.cc @@ -12,6 +12,7 @@ #include "base/callback_helpers.h" #include "base/memory/ptr_util.h" #include "build/build_config.h" +#include "device/bluetooth/advertisement.h" #include "device/bluetooth/bluetooth_socket.h" #include "device/bluetooth/device.h" #include "device/bluetooth/discovery_session.h" @@ -20,6 +21,7 @@ #include "device/bluetooth/server_socket.h" #include "device/bluetooth/socket.h" #include "mojo/public/cpp/bindings/pending_remote.h" +#include "mojo/public/cpp/bindings/remote.h" #include "mojo/public/cpp/bindings/self_owned_receiver.h" namespace bluetooth { @@ -79,12 +81,37 @@ void Adapter::GetInfo(GetInfoCallback callback) { std::move(callback).Run(std::move(adapter_info)); } -void Adapter::SetClient(mojo::PendingRemote<mojom::AdapterClient> client, - SetClientCallback callback) { - client_.Bind(std::move(client)); +void Adapter::AddObserver(mojo::PendingRemote<mojom::AdapterObserver> observer, + AddObserverCallback callback) { + observers_.Add(std::move(observer)); std::move(callback).Run(); } +void Adapter::RegisterAdvertisement(const device::BluetoothUUID& service_uuid, + const std::vector<uint8_t>& service_data, + RegisterAdvertisementCallback callback) { + auto advertisement_data = + std::make_unique<device::BluetoothAdvertisement::Data>( + device::BluetoothAdvertisement::ADVERTISEMENT_TYPE_BROADCAST); + + auto uuid_list = std::make_unique<device::BluetoothAdvertisement::UUIDList>(); + uuid_list->push_back(service_uuid.value()); + advertisement_data->set_service_uuids(std::move(uuid_list)); + + auto service_data_map = + std::make_unique<device::BluetoothAdvertisement::ServiceData>(); + service_data_map->emplace(service_uuid.value(), service_data); + advertisement_data->set_service_data(std::move(service_data_map)); + + auto copyable_callback = base::AdaptCallbackForRepeating(std::move(callback)); + adapter_->RegisterAdvertisement( + std::move(advertisement_data), + base::BindOnce(&Adapter::OnRegisterAdvertisement, + weak_ptr_factory_.GetWeakPtr(), copyable_callback), + base::BindOnce(&Adapter::OnRegisterAdvertisementError, + weak_ptr_factory_.GetWeakPtr(), copyable_callback)); +} + void Adapter::SetDiscoverable(bool discoverable, SetDiscoverableCallback callback) { auto copyable_callback = base::AdaptCallbackForRepeating(std::move(callback)); @@ -132,7 +159,7 @@ void Adapter::CreateRfcommService(const std::string& service_name, const device::BluetoothUUID& service_uuid, CreateRfcommServiceCallback callback) { device::BluetoothAdapter::ServiceOptions service_options; - service_options.name = std::make_unique<std::string>(service_name); + service_options.name = service_name; auto copyable_callback = base::AdaptCallbackForRepeating(std::move(callback)); adapter_->CreateRfcommService( @@ -145,50 +172,47 @@ void Adapter::CreateRfcommService(const std::string& service_name, void Adapter::AdapterPresentChanged(device::BluetoothAdapter* adapter, bool present) { - if (client_) - client_->PresentChanged(present); + for (auto& observer : observers_) + observer->PresentChanged(present); } void Adapter::AdapterPoweredChanged(device::BluetoothAdapter* adapter, bool powered) { - if (client_) - client_->PoweredChanged(powered); + for (auto& observer : observers_) + observer->PoweredChanged(powered); } void Adapter::AdapterDiscoverableChanged(device::BluetoothAdapter* adapter, bool discoverable) { - if (client_) - client_->DiscoverableChanged(discoverable); + for (auto& observer : observers_) + observer->DiscoverableChanged(discoverable); } void Adapter::AdapterDiscoveringChanged(device::BluetoothAdapter* adapter, bool discovering) { - if (client_) - client_->DiscoveringChanged(discovering); + for (auto& observer : observers_) + observer->DiscoveringChanged(discovering); } void Adapter::DeviceAdded(device::BluetoothAdapter* adapter, device::BluetoothDevice* device) { - if (client_) { - auto device_info = Device::ConstructDeviceInfoStruct(device); - client_->DeviceAdded(std::move(device_info)); - } + auto device_info = Device::ConstructDeviceInfoStruct(device); + for (auto& observer : observers_) + observer->DeviceAdded(device_info->Clone()); } void Adapter::DeviceChanged(device::BluetoothAdapter* adapter, device::BluetoothDevice* device) { - if (client_) { - auto device_info = Device::ConstructDeviceInfoStruct(device); - client_->DeviceChanged(std::move(device_info)); - } + auto device_info = Device::ConstructDeviceInfoStruct(device); + for (auto& observer : observers_) + observer->DeviceChanged(device_info->Clone()); } void Adapter::DeviceRemoved(device::BluetoothAdapter* adapter, device::BluetoothDevice* device) { - if (client_) { - auto device_info = Device::ConstructDeviceInfoStruct(device); - client_->DeviceRemoved(std::move(device_info)); - } + auto device_info = Device::ConstructDeviceInfoStruct(device); + for (auto& observer : observers_) + observer->DeviceRemoved(device_info->Clone()); } void Adapter::OnGattConnected( @@ -207,6 +231,23 @@ void Adapter::OnConnectError( /*device=*/mojo::NullRemote()); } +void Adapter::OnRegisterAdvertisement( + RegisterAdvertisementCallback callback, + scoped_refptr<device::BluetoothAdvertisement> advertisement) { + mojo::PendingRemote<mojom::Advertisement> pending_advertisement; + mojo::MakeSelfOwnedReceiver( + std::make_unique<Advertisement>(std::move(advertisement)), + pending_advertisement.InitWithNewPipeAndPassReceiver()); + std::move(callback).Run(std::move(pending_advertisement)); +} + +void Adapter::OnRegisterAdvertisementError( + RegisterAdvertisementCallback callback, + device::BluetoothAdvertisement::ErrorCode error_code) { + DLOG(ERROR) << "Failed to register advertisement, error code: " << error_code; + std::move(callback).Run(/*advertisement=*/mojo::NullRemote()); +} + void Adapter::OnSetDiscoverable(SetDiscoverableCallback callback) { std::move(callback).Run(/*success=*/true); } diff --git a/chromium/device/bluetooth/adapter.h b/chromium/device/bluetooth/adapter.h index 329f95ad31a..f9d113afe42 100644 --- a/chromium/device/bluetooth/adapter.h +++ b/chromium/device/bluetooth/adapter.h @@ -11,12 +11,13 @@ #include "base/macros.h" #include "base/memory/ref_counted.h" #include "device/bluetooth/bluetooth_adapter.h" +#include "device/bluetooth/bluetooth_advertisement.h" #include "device/bluetooth/bluetooth_gatt_connection.h" #include "device/bluetooth/public/cpp/bluetooth_uuid.h" #include "device/bluetooth/public/mojom/adapter.mojom.h" #include "device/bluetooth/public/mojom/device.mojom-forward.h" #include "mojo/public/cpp/bindings/pending_remote.h" -#include "mojo/public/cpp/bindings/remote.h" +#include "mojo/public/cpp/bindings/remote_set.h" namespace bluetooth { @@ -35,8 +36,11 @@ class Adapter : public mojom::Adapter, ConnectToDeviceCallback callback) override; void GetDevices(GetDevicesCallback callback) override; void GetInfo(GetInfoCallback callback) override; - void SetClient(mojo::PendingRemote<mojom::AdapterClient> client, - SetClientCallback callback) override; + void AddObserver(mojo::PendingRemote<mojom::AdapterObserver> observer, + AddObserverCallback callback) override; + void RegisterAdvertisement(const device::BluetoothUUID& service_uuid, + const std::vector<uint8_t>& service_data, + RegisterAdvertisementCallback callback) override; void SetDiscoverable(bool discoverable, SetDiscoverableCallback callback) override; void SetName(const std::string& name, SetNameCallback callback) override; @@ -74,6 +78,13 @@ class Adapter : public mojom::Adapter, void OnConnectError(ConnectToDeviceCallback callback, device::BluetoothDevice::ConnectErrorCode error_code); + void OnRegisterAdvertisement( + RegisterAdvertisementCallback callback, + scoped_refptr<device::BluetoothAdvertisement> advertisement); + void OnRegisterAdvertisementError( + RegisterAdvertisementCallback callback, + device::BluetoothAdvertisement::ErrorCode error_code); + void OnSetDiscoverable(SetDiscoverableCallback callback); void OnSetDiscoverableError(SetDiscoverableCallback callback); @@ -98,8 +109,8 @@ class Adapter : public mojom::Adapter, // The current Bluetooth adapter. scoped_refptr<device::BluetoothAdapter> adapter_; - // The adapter client that listens to this service. - mojo::Remote<mojom::AdapterClient> client_; + // The adapter observers that listen to this service. + mojo::RemoteSet<mojom::AdapterObserver> observers_; base::WeakPtrFactory<Adapter> weak_ptr_factory_{this}; diff --git a/chromium/device/bluetooth/adapter_unittest.cc b/chromium/device/bluetooth/adapter_unittest.cc new file mode 100644 index 00000000000..e6e98d09a29 --- /dev/null +++ b/chromium/device/bluetooth/adapter_unittest.cc @@ -0,0 +1,126 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "device/bluetooth/adapter.h" + +#include "base/callback_helpers.h" +#include "base/run_loop.h" +#include "base/test/bind_test_util.h" +#include "base/test/task_environment.h" +#include "device/bluetooth/bluetooth_advertisement.h" +#include "device/bluetooth/test/mock_bluetooth_adapter.h" +#include "device/bluetooth/test/mock_bluetooth_advertisement.h" +#include "mojo/public/cpp/bindings/pending_remote.h" +#include "mojo/public/cpp/bindings/remote.h" +#include "testing/gtest/include/gtest/gtest.h" + +using ::testing::_; +using testing::NiceMock; +using testing::Return; + +namespace { + +const char kServiceId[] = "00000000-0000-0000-0000-000000000001"; +const char kDeviceServiceDataStr[] = "ServiceData"; + +std::vector<uint8_t> GetByteVector(const std::string& str) { + return std::vector<uint8_t>(str.begin(), str.end()); +} + +class MockBluetoothAdapterWithAdvertisements + : public device::MockBluetoothAdapter { + public: + void RegisterAdvertisement( + std::unique_ptr<device::BluetoothAdvertisement::Data> advertisement_data, + device::BluetoothAdapter::CreateAdvertisementCallback callback, + device::BluetoothAdapter::AdvertisementErrorCallback error_callback) + override { + last_register_advertisement_args_ = + std::make_pair(*advertisement_data->service_uuids(), + *advertisement_data->service_data()); + + if (should_advertisement_registration_succeed_) { + std::move(callback).Run( + base::MakeRefCounted<device::MockBluetoothAdvertisement>()); + } else { + std::move(error_callback) + .Run(device::BluetoothAdvertisement::ErrorCode:: + INVALID_ADVERTISEMENT_ERROR_CODE); + } + } + + bool should_advertisement_registration_succeed_ = true; + base::Optional<std::pair<device::BluetoothAdvertisement::UUIDList, + device::BluetoothAdvertisement::ServiceData>> + last_register_advertisement_args_; + + protected: + ~MockBluetoothAdapterWithAdvertisements() override = default; +}; + +} // namespace + +namespace bluetooth { + +class AdapterTest : public testing::Test { + public: + AdapterTest() = default; + ~AdapterTest() override = default; + AdapterTest(const AdapterTest&) = delete; + AdapterTest& operator=(const AdapterTest&) = delete; + + void SetUp() override { + mock_bluetooth_adapter_ = base::MakeRefCounted< + NiceMock<MockBluetoothAdapterWithAdvertisements>>(); + ON_CALL(*mock_bluetooth_adapter_, IsPresent()).WillByDefault(Return(true)); + ON_CALL(*mock_bluetooth_adapter_, IsPowered()).WillByDefault(Return(true)); + + adapter_ = std::make_unique<Adapter>(mock_bluetooth_adapter_); + } + + protected: + void VerifyRegisterAdvertisement(bool should_succeed) { + mock_bluetooth_adapter_->should_advertisement_registration_succeed_ = + should_succeed; + + auto service_data = GetByteVector(kDeviceServiceDataStr); + mojo::Remote<mojom::Advertisement> advertisement; + + base::RunLoop run_loop; + adapter_->RegisterAdvertisement( + device::BluetoothUUID(kServiceId), service_data, + base::BindLambdaForTesting([&](mojo::PendingRemote<mojom::Advertisement> + pending_advertisement) { + EXPECT_EQ(should_succeed, pending_advertisement.is_valid()); + run_loop.Quit(); + })); + run_loop.Run(); + + auto& uuid_list = + mock_bluetooth_adapter_->last_register_advertisement_args_->first; + EXPECT_EQ(1u, uuid_list.size()); + EXPECT_EQ(kServiceId, uuid_list[0]); + EXPECT_EQ( + service_data, + mock_bluetooth_adapter_->last_register_advertisement_args_->second.at( + kServiceId)); + } + + scoped_refptr<NiceMock<MockBluetoothAdapterWithAdvertisements>> + mock_bluetooth_adapter_; + std::unique_ptr<Adapter> adapter_; + + private: + base::test::TaskEnvironment task_environment_; +}; + +TEST_F(AdapterTest, TestRegisterAdvertisement_Success) { + VerifyRegisterAdvertisement(/*should_succeed=*/true); +} + +TEST_F(AdapterTest, TestRegisterAdvertisement_Error) { + VerifyRegisterAdvertisement(/*should_succeed=*/false); +} + +} // namespace bluetooth diff --git a/chromium/device/bluetooth/advertisement.cc b/chromium/device/bluetooth/advertisement.cc new file mode 100644 index 00000000000..cdb826529a1 --- /dev/null +++ b/chromium/device/bluetooth/advertisement.cc @@ -0,0 +1,46 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "device/bluetooth/advertisement.h" + +#include "base/bind.h" +#include "base/callback_helpers.h" + +namespace bluetooth { + +Advertisement::Advertisement( + scoped_refptr<device::BluetoothAdvertisement> bluetooth_advertisement) + : bluetooth_advertisement_(std::move(bluetooth_advertisement)) {} + +Advertisement::~Advertisement() { + Unregister(base::DoNothing()); +} + +void Advertisement::Unregister(UnregisterCallback callback) { + if (!bluetooth_advertisement_) + return; + + auto copyable_callback = base::AdaptCallbackForRepeating(std::move(callback)); + bluetooth_advertisement_->Unregister( + base::BindOnce(&Advertisement::OnUnregister, + weak_ptr_factory_.GetWeakPtr(), copyable_callback), + base::BindOnce(&Advertisement::OnUnregisterError, + weak_ptr_factory_.GetWeakPtr(), copyable_callback)); +} + +void Advertisement::OnUnregister(UnregisterCallback callback) { + bluetooth_advertisement_.reset(); + std::move(callback).Run(); +} + +void Advertisement::OnUnregisterError( + UnregisterCallback callback, + device::BluetoothAdvertisement::ErrorCode error_code) { + DLOG(ERROR) << "Failed to unregister advertisement, error code: " + << error_code; + bluetooth_advertisement_.reset(); + std::move(callback).Run(); +} + +} // namespace bluetooth diff --git a/chromium/device/bluetooth/advertisement.h b/chromium/device/bluetooth/advertisement.h new file mode 100644 index 00000000000..7746681d357 --- /dev/null +++ b/chromium/device/bluetooth/advertisement.h @@ -0,0 +1,43 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef DEVICE_BLUETOOTH_ADVERTISEMENT_H_ +#define DEVICE_BLUETOOTH_ADVERTISEMENT_H_ + +#include "base/memory/ref_counted.h" +#include "device/bluetooth/bluetooth_advertisement.h" +#include "device/bluetooth/public/mojom/adapter.mojom.h" + +namespace bluetooth { + +// Implementation of Mojo Advertisement in +// device/bluetooth/public/mojom/adapter.mojom. +// Uses the platform abstraction of //device/bluetooth. +// An instance of this class is constructed by Adapter and strongly bound to its +// MessagePipe. When the instance is destroyed, the underlying +// BluetoothAdvertisement is destroyed. +class Advertisement : public mojom::Advertisement { + public: + explicit Advertisement( + scoped_refptr<device::BluetoothAdvertisement> bluetooth_advertisement); + ~Advertisement() override; + Advertisement(const Advertisement&) = delete; + Advertisement& operator=(const Advertisement&) = delete; + + // mojom::Advertisement: + void Unregister(UnregisterCallback callback) override; + + private: + void OnUnregister(UnregisterCallback callback); + void OnUnregisterError(UnregisterCallback callback, + device::BluetoothAdvertisement::ErrorCode error_code); + + scoped_refptr<device::BluetoothAdvertisement> bluetooth_advertisement_; + + base::WeakPtrFactory<Advertisement> weak_ptr_factory_{this}; +}; + +} // namespace bluetooth + +#endif // DEVICE_BLUETOOTH_ADVERTISEMENT_H_ diff --git a/chromium/device/bluetooth/advertisement_unittest.cc b/chromium/device/bluetooth/advertisement_unittest.cc new file mode 100644 index 00000000000..2dd863c6da2 --- /dev/null +++ b/chromium/device/bluetooth/advertisement_unittest.cc @@ -0,0 +1,73 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "device/bluetooth/advertisement.h" + +#include "base/run_loop.h" +#include "base/test/task_environment.h" +#include "device/bluetooth/bluetooth_advertisement.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace { + +class FakeBluetoothAdvertisement : public device::BluetoothAdvertisement { + public: + // device::BluetoothAdvertisement: + void Unregister(SuccessCallback success_callback, + ErrorCallback error_callback) override { + called_unregister_ = true; + std::move(success_callback).Run(); + } + + bool called_unregister() { return called_unregister_; } + + private: + ~FakeBluetoothAdvertisement() override = default; + + bool called_unregister_ = false; +}; + +} // namespace + +namespace bluetooth { + +class AdvertisementTest : public testing::Test { + public: + AdvertisementTest() = default; + ~AdvertisementTest() override = default; + AdvertisementTest(const AdvertisementTest&) = delete; + AdvertisementTest& operator=(const AdvertisementTest&) = delete; + + void SetUp() override { + fake_bluetooth_advertisement_ = + base::MakeRefCounted<FakeBluetoothAdvertisement>(); + advertisement_ = + std::make_unique<Advertisement>(fake_bluetooth_advertisement_); + } + + protected: + scoped_refptr<FakeBluetoothAdvertisement> fake_bluetooth_advertisement_; + std::unique_ptr<Advertisement> advertisement_; + + private: + base::test::TaskEnvironment task_environment_; +}; + +TEST_F(AdvertisementTest, TestOnDestroyCallsUnregister) { + // When destroyed, |advertisement_| is expected to tear down its + // BluetoothAdvertisement. + ASSERT_FALSE(fake_bluetooth_advertisement_->called_unregister()); + advertisement_.reset(); + EXPECT_TRUE(fake_bluetooth_advertisement_->called_unregister()); +} + +TEST_F(AdvertisementTest, TestUnregister) { + ASSERT_FALSE(fake_bluetooth_advertisement_->called_unregister()); + base::RunLoop run_loop; + advertisement_->Unregister(run_loop.QuitClosure()); + run_loop.Run(); + EXPECT_TRUE(fake_bluetooth_advertisement_->called_unregister()); +} + +} // namespace bluetooth diff --git a/chromium/device/bluetooth/android/java/DEPS b/chromium/device/bluetooth/android/java/DEPS deleted file mode 100644 index 53896d8aa7c..00000000000 --- a/chromium/device/bluetooth/android/java/DEPS +++ /dev/null @@ -1,3 +0,0 @@ -include_rules = [ - "+components/location/android/java", -] diff --git a/chromium/device/bluetooth/android/java/src/org/chromium/device/bluetooth/ChromeBluetoothAdapter.java b/chromium/device/bluetooth/android/java/src/org/chromium/device/bluetooth/ChromeBluetoothAdapter.java deleted file mode 100644 index 4b782be0520..00000000000 --- a/chromium/device/bluetooth/android/java/src/org/chromium/device/bluetooth/ChromeBluetoothAdapter.java +++ /dev/null @@ -1,370 +0,0 @@ -// Copyright 2015 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package org.chromium.device.bluetooth; - -import android.annotation.TargetApi; -import android.bluetooth.BluetoothAdapter; -import android.bluetooth.le.ScanFilter; -import android.bluetooth.le.ScanSettings; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.os.Build; -import android.os.ParcelUuid; -import android.util.SparseArray; - -import org.chromium.base.Log; -import org.chromium.base.annotations.CalledByNative; -import org.chromium.base.annotations.JNIAdditionalImport; -import org.chromium.base.annotations.JNINamespace; -import org.chromium.base.annotations.NativeMethods; -import org.chromium.components.location.LocationUtils; - -import java.util.List; -import java.util.Map; - -/** - * Exposes android.bluetooth.BluetoothAdapter as necessary for C++ - * device::BluetoothAdapterAndroid, which implements the cross platform - * device::BluetoothAdapter. - * - * Lifetime is controlled by device::BluetoothAdapterAndroid. - */ -@JNINamespace("device") -@JNIAdditionalImport(Wrappers.class) -@TargetApi(Build.VERSION_CODES.M) -final class ChromeBluetoothAdapter extends BroadcastReceiver { - private static final String TAG = "Bluetooth"; - - private long mNativeBluetoothAdapterAndroid; - // mAdapter is final to ensure registerReceiver is followed by unregisterReceiver. - private final Wrappers.BluetoothAdapterWrapper mAdapter; - private ScanCallback mScanCallback; - - // --------------------------------------------------------------------------------------------- - // Construction and handler for C++ object destruction. - - /** - * Constructs a ChromeBluetoothAdapter. - * @param nativeBluetoothAdapterAndroid Is the associated C++ - * BluetoothAdapterAndroid pointer value. - * @param adapterWrapper Wraps the default android.bluetooth.BluetoothAdapter, - * but may be either null if an adapter is not available - * or a fake for testing. - */ - public ChromeBluetoothAdapter( - long nativeBluetoothAdapterAndroid, Wrappers.BluetoothAdapterWrapper adapterWrapper) { - mNativeBluetoothAdapterAndroid = nativeBluetoothAdapterAndroid; - mAdapter = adapterWrapper; - registerBroadcastReceiver(); - if (adapterWrapper == null) { - Log.i(TAG, "ChromeBluetoothAdapter created with no adapterWrapper."); - } else { - Log.i(TAG, "ChromeBluetoothAdapter created with provided adapterWrapper."); - } - } - - /** - * Handles C++ object being destroyed. - */ - @CalledByNative - private void onBluetoothAdapterAndroidDestruction() { - stopScan(); - mNativeBluetoothAdapterAndroid = 0; - unregisterBroadcastReceiver(); - } - - // --------------------------------------------------------------------------------------------- - // BluetoothAdapterAndroid methods implemented in java: - - // Implements BluetoothAdapterAndroid::Create. - @CalledByNative - private static ChromeBluetoothAdapter create( - long nativeBluetoothAdapterAndroid, Wrappers.BluetoothAdapterWrapper adapterWrapper) { - return new ChromeBluetoothAdapter(nativeBluetoothAdapterAndroid, adapterWrapper); - } - - // Implements BluetoothAdapterAndroid::GetAddress. - @CalledByNative - private String getAddress() { - if (isPresent()) { - return mAdapter.getAddress(); - } else { - return ""; - } - } - - // Implements BluetoothAdapterAndroid::GetName. - @CalledByNative - private String getName() { - if (isPresent()) { - return mAdapter.getName(); - } else { - return ""; - } - } - - // Implements BluetoothAdapterAndroid::IsPresent. - @CalledByNative - private boolean isPresent() { - return mAdapter != null; - } - - // Implements BluetoothAdapterAndroid::IsPowered. - @CalledByNative - private boolean isPowered() { - return isPresent() && mAdapter.isEnabled(); - } - - // Implements BluetoothAdapterAndroid::SetPowered. - @CalledByNative - private boolean setPowered(boolean powered) { - if (powered) { - return isPresent() && mAdapter.enable(); - } else { - return isPresent() && mAdapter.disable(); - } - } - - // Implements BluetoothAdapterAndroid::IsDiscoverable. - @CalledByNative - private boolean isDiscoverable() { - return isPresent() - && mAdapter.getScanMode() == BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE; - } - - // Implements BluetoothAdapterAndroid::IsDiscovering. - @CalledByNative - private boolean isDiscovering() { - return isPresent() && (mAdapter.isDiscovering() || mScanCallback != null); - } - - /** - * Starts a Low Energy scan. - * @param filters List of filters used to minimize number of devices returned - * @return True on success. - */ - @CalledByNative - private boolean startScan(List<ScanFilter> filters) { - Wrappers.BluetoothLeScannerWrapper scanner = mAdapter.getBluetoothLeScanner(); - - if (scanner == null) { - return false; - } - - if (!canScan()) { - return false; - } - - // scanMode note: SCAN_FAILED_FEATURE_UNSUPPORTED is caused (at least on some devices) if - // setReportDelay() is used or if SCAN_MODE_LOW_LATENCY isn't used. - int scanMode = ScanSettings.SCAN_MODE_LOW_LATENCY; - - assert mScanCallback == null; - mScanCallback = new ScanCallback(); - - try { - scanner.startScan(filters, scanMode, mScanCallback); - } catch (IllegalArgumentException e) { - Log.e(TAG, "Cannot start scan: " + e); - mScanCallback = null; - return false; - } catch (IllegalStateException e) { - Log.e(TAG, "Adapter is off. Cannot start scan: " + e); - mScanCallback = null; - return false; - } - return true; - } - - /** - * Stops the Low Energy scan. - * @return True if a scan was in progress. - */ - @CalledByNative - private boolean stopScan() { - if (mScanCallback == null) { - return false; - } - - try { - Wrappers.BluetoothLeScannerWrapper scanner = mAdapter.getBluetoothLeScanner(); - if (scanner != null) { - scanner.stopScan(mScanCallback); - } - } catch (IllegalArgumentException e) { - Log.e(TAG, "Cannot stop scan: " + e); - } catch (IllegalStateException e) { - Log.e(TAG, "Adapter is off. Cannot stop scan: " + e); - } - mScanCallback = null; - return true; - } - - // --------------------------------------------------------------------------------------------- - // Implementation details: - - /** - * @return true if Chromium has permission to scan for Bluetooth devices and location services - * are on. - */ - private boolean canScan() { - LocationUtils locationUtils = LocationUtils.getInstance(); - return locationUtils.hasAndroidLocationPermission() - && locationUtils.isSystemLocationSettingEnabled(); - } - - private void registerBroadcastReceiver() { - if (mAdapter != null) { - mAdapter.getContext().registerReceiver( - this, new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)); - } - } - - private void unregisterBroadcastReceiver() { - if (mAdapter != null) { - mAdapter.getContext().unregisterReceiver(this); - } - } - - /** - * Implements callbacks used during a Low Energy scan by notifying upon - * devices discovered or detecting a scan failure. - */ - private class ScanCallback extends Wrappers.ScanCallbackWrapper { - @Override - public void onBatchScanResult(List<Wrappers.ScanResultWrapper> results) { - Log.v(TAG, "onBatchScanResults"); - } - - @Override - public void onScanResult(int callbackType, Wrappers.ScanResultWrapper result) { - Log.v(TAG, "onScanResult %d %s %s", callbackType, result.getDevice().getAddress(), - result.getDevice().getName()); - - String[] uuid_strings; - List<ParcelUuid> uuids = result.getScanRecord_getServiceUuids(); - - if (uuids == null) { - uuid_strings = new String[] {}; - } else { - uuid_strings = new String[uuids.size()]; - for (int i = 0; i < uuids.size(); i++) { - uuid_strings[i] = uuids.get(i).toString(); - } - } - - String[] serviceDataKeys; - byte[][] serviceDataValues; - Map<ParcelUuid, byte[]> serviceData = result.getScanRecord_getServiceData(); - if (serviceData == null) { - serviceDataKeys = new String[] {}; - serviceDataValues = new byte[][] {}; - } else { - serviceDataKeys = new String[serviceData.size()]; - serviceDataValues = new byte[serviceData.size()][]; - int i = 0; - for (Map.Entry<ParcelUuid, byte[]> serviceDataItem : serviceData.entrySet()) { - serviceDataKeys[i] = serviceDataItem.getKey().toString(); - serviceDataValues[i++] = serviceDataItem.getValue(); - } - } - - int[] manufacturerDataKeys; - byte[][] manufacturerDataValues; - SparseArray<byte[]> manufacturerData = - result.getScanRecord_getManufacturerSpecificData(); - if (manufacturerData == null) { - manufacturerDataKeys = new int[] {}; - manufacturerDataValues = new byte[][] {}; - } else { - manufacturerDataKeys = new int[manufacturerData.size()]; - manufacturerDataValues = new byte[manufacturerData.size()][]; - for (int i = 0; i < manufacturerData.size(); i++) { - manufacturerDataKeys[i] = manufacturerData.keyAt(i); - manufacturerDataValues[i] = manufacturerData.valueAt(i); - } - } - - // Object can be destroyed, but Android keeps calling onScanResult. - if (mNativeBluetoothAdapterAndroid != 0) { - ChromeBluetoothAdapterJni.get().createOrUpdateDeviceOnScan( - mNativeBluetoothAdapterAndroid, ChromeBluetoothAdapter.this, - result.getDevice().getAddress(), result.getDevice(), - result.getScanRecord_getDeviceName(), result.getRssi(), uuid_strings, - result.getScanRecord_getTxPowerLevel(), serviceDataKeys, serviceDataValues, - manufacturerDataKeys, manufacturerDataValues, - result.getScanRecord_getAdvertiseFlags()); - } - } - - @Override - public void onScanFailed(int errorCode) { - Log.w(TAG, "onScanFailed: %d", errorCode); - ChromeBluetoothAdapterJni.get().onScanFailed( - mNativeBluetoothAdapterAndroid, ChromeBluetoothAdapter.this); - } - } - - @Override - public void onReceive(Context context, Intent intent) { - String action = intent.getAction(); - - if (isPresent() && BluetoothAdapter.ACTION_STATE_CHANGED.equals(action)) { - int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR); - - Log.w(TAG, "onReceive: BluetoothAdapter.ACTION_STATE_CHANGED: %s", - getBluetoothStateString(state)); - - switch (state) { - case BluetoothAdapter.STATE_ON: - ChromeBluetoothAdapterJni.get().onAdapterStateChanged( - mNativeBluetoothAdapterAndroid, ChromeBluetoothAdapter.this, true); - break; - case BluetoothAdapter.STATE_OFF: - ChromeBluetoothAdapterJni.get().onAdapterStateChanged( - mNativeBluetoothAdapterAndroid, ChromeBluetoothAdapter.this, false); - break; - default: - // do nothing - } - } - } - - private String getBluetoothStateString(int state) { - switch (state) { - case BluetoothAdapter.STATE_OFF: - return "STATE_OFF"; - case BluetoothAdapter.STATE_ON: - return "STATE_ON"; - case BluetoothAdapter.STATE_TURNING_OFF: - return "STATE_TURNING_OFF"; - case BluetoothAdapter.STATE_TURNING_ON: - return "STATE_TURNING_ON"; - default: - assert false; - return "illegal state: " + state; - } - } - - @NativeMethods - interface Natives { - // Binds to BluetoothAdapterAndroid::OnScanFailed. - void onScanFailed(long nativeBluetoothAdapterAndroid, ChromeBluetoothAdapter caller); - - // Binds to BluetoothAdapterAndroid::CreateOrUpdateDeviceOnScan. - void createOrUpdateDeviceOnScan(long nativeBluetoothAdapterAndroid, - ChromeBluetoothAdapter caller, String address, - Wrappers.BluetoothDeviceWrapper deviceWrapper, String localName, int rssi, - String[] advertisedUuids, int txPower, String[] serviceDataKeys, - Object[] serviceDataValues, int[] manufacturerDataKeys, - Object[] manufacturerDataValues, int advertiseFlags); - - // Binds to BluetoothAdapterAndroid::nativeOnAdapterStateChanged - void onAdapterStateChanged( - long nativeBluetoothAdapterAndroid, ChromeBluetoothAdapter caller, boolean powered); - } -} diff --git a/chromium/device/bluetooth/android/java/src/org/chromium/device/bluetooth/ChromeBluetoothDevice.java b/chromium/device/bluetooth/android/java/src/org/chromium/device/bluetooth/ChromeBluetoothDevice.java deleted file mode 100644 index e134b66cf13..00000000000 --- a/chromium/device/bluetooth/android/java/src/org/chromium/device/bluetooth/ChromeBluetoothDevice.java +++ /dev/null @@ -1,326 +0,0 @@ -// Copyright 2015 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package org.chromium.device.bluetooth; - -import android.annotation.TargetApi; -import android.bluetooth.BluetoothDevice; -import android.os.Build; - -import org.chromium.base.ContextUtils; -import org.chromium.base.Log; -import org.chromium.base.annotations.CalledByNative; -import org.chromium.base.annotations.JNIAdditionalImport; -import org.chromium.base.annotations.JNINamespace; -import org.chromium.base.annotations.NativeMethods; -import org.chromium.base.metrics.RecordHistogram; - -import java.util.HashMap; - -/** - * Exposes android.bluetooth.BluetoothDevice as necessary for C++ - * device::BluetoothDeviceAndroid. - * - * Lifetime is controlled by device::BluetoothDeviceAndroid. - */ -@JNINamespace("device") -@JNIAdditionalImport(Wrappers.class) -@TargetApi(Build.VERSION_CODES.M) -final class ChromeBluetoothDevice { - private static final String TAG = "Bluetooth"; - - private long mNativeBluetoothDeviceAndroid; - final Wrappers.BluetoothDeviceWrapper mDevice; - Wrappers.BluetoothGattWrapper mBluetoothGatt; - private final BluetoothGattCallbackImpl mBluetoothGattCallbackImpl; - final HashMap<Wrappers.BluetoothGattCharacteristicWrapper, - ChromeBluetoothRemoteGattCharacteristic> mWrapperToChromeCharacteristicsMap; - final HashMap<Wrappers.BluetoothGattDescriptorWrapper, ChromeBluetoothRemoteGattDescriptor> - mWrapperToChromeDescriptorsMap; - - private ChromeBluetoothDevice( - long nativeBluetoothDeviceAndroid, Wrappers.BluetoothDeviceWrapper deviceWrapper) { - mNativeBluetoothDeviceAndroid = nativeBluetoothDeviceAndroid; - mDevice = deviceWrapper; - mBluetoothGattCallbackImpl = new BluetoothGattCallbackImpl(); - mWrapperToChromeCharacteristicsMap = - new HashMap<Wrappers.BluetoothGattCharacteristicWrapper, - ChromeBluetoothRemoteGattCharacteristic>(); - mWrapperToChromeDescriptorsMap = new HashMap<Wrappers.BluetoothGattDescriptorWrapper, - ChromeBluetoothRemoteGattDescriptor>(); - Log.v(TAG, "ChromeBluetoothDevice created."); - } - - /** - * Handles C++ object being destroyed. - */ - @CalledByNative - private void onBluetoothDeviceAndroidDestruction() { - if (mBluetoothGatt != null) { - mBluetoothGatt.close(); - mBluetoothGatt = null; - } - mNativeBluetoothDeviceAndroid = 0; - } - - // --------------------------------------------------------------------------------------------- - // BluetoothDeviceAndroid methods implemented in java: - - // Implements BluetoothDeviceAndroid::Create. - @CalledByNative - private static ChromeBluetoothDevice create( - long nativeBluetoothDeviceAndroid, Wrappers.BluetoothDeviceWrapper deviceWrapper) { - return new ChromeBluetoothDevice(nativeBluetoothDeviceAndroid, deviceWrapper); - } - - // Implements BluetoothDeviceAndroid::GetBluetoothClass. - @CalledByNative - private int getBluetoothClass() { - return mDevice.getBluetoothClass_getDeviceClass(); - } - - // Implements BluetoothDeviceAndroid::GetAddress. - @CalledByNative - private String getAddress() { - return mDevice.getAddress(); - } - - // Implements BluetoothDeviceAndroid::GetName. - @CalledByNative - private String getName() { - return mDevice.getName(); - } - - // Implements BluetoothDeviceAndroid::IsPaired. - @CalledByNative - private boolean isPaired() { - return mDevice.getBondState() == BluetoothDevice.BOND_BONDED; - } - - // Implements BluetoothDeviceAndroid::CreateGattConnectionImpl. - @CalledByNative - private void createGattConnectionImpl() { - Log.i(TAG, "connectGatt"); - - if (mBluetoothGatt != null) mBluetoothGatt.close(); - - // autoConnect set to false as under experimentation using autoConnect failed to complete - // connections. - mBluetoothGatt = mDevice.connectGatt(ContextUtils.getApplicationContext(), - false /* autoConnect */, mBluetoothGattCallbackImpl, - // Prefer LE for dual-mode devices due to lower energy consumption. - BluetoothDevice.TRANSPORT_LE); - } - - // Implements BluetoothDeviceAndroid::DisconnectGatt. - @CalledByNative - private void disconnectGatt() { - Log.i(TAG, "BluetoothGatt.disconnect"); - if (mBluetoothGatt != null) mBluetoothGatt.disconnect(); - } - - // Implements callbacks related to a GATT connection. - private class BluetoothGattCallbackImpl extends Wrappers.BluetoothGattCallbackWrapper { - @Override - public void onConnectionStateChange(final int status, final int newState) { - Log.i(TAG, "onConnectionStateChange status:%d newState:%s", status, - (newState == android.bluetooth.BluetoothProfile.STATE_CONNECTED) - ? "Connected" - : "Disconnected"); - Wrappers.ThreadUtilsWrapper.getInstance().runOnUiThread(new Runnable() { - @Override - public void run() { - if (newState == android.bluetooth.BluetoothProfile.STATE_CONNECTED) { - RecordHistogram.recordSparseHistogram( - "Bluetooth.Web.Android.onConnectionStateChange.Status.Connected", - status); - mBluetoothGatt.discoverServices(); - } else if (newState == android.bluetooth.BluetoothProfile.STATE_DISCONNECTED) { - RecordHistogram.recordSparseHistogram( - "Bluetooth.Web.Android.onConnectionStateChange.Status.Disconnected", - status); - if (mBluetoothGatt != null) { - mBluetoothGatt.close(); - mBluetoothGatt = null; - } - } else { - RecordHistogram.recordSparseHistogram( - "Bluetooth.Web.Android.onConnectionStateChange.Status.InvalidState", - status); - } - if (mNativeBluetoothDeviceAndroid != 0) { - ChromeBluetoothDeviceJni.get().onConnectionStateChange( - mNativeBluetoothDeviceAndroid, ChromeBluetoothDevice.this, status, - newState == android.bluetooth.BluetoothProfile.STATE_CONNECTED); - } - } - }); - } - - @Override - public void onServicesDiscovered(final int status) { - Log.i(TAG, "onServicesDiscovered status:%d==%s", status, - status == android.bluetooth.BluetoothGatt.GATT_SUCCESS ? "OK" : "Error"); - Wrappers.ThreadUtilsWrapper.getInstance().runOnUiThread(new Runnable() { - @Override - public void run() { - if (mNativeBluetoothDeviceAndroid != 0) { - // When the device disconnects it deletes - // mBluetoothGatt, so we need to check it's not null. - if (mBluetoothGatt == null) { - RecordHistogram.recordSparseHistogram( - "Bluetooth.Web.Android.onServicesDiscovered.Status." - + "Disconnected", - status); - return; - } - RecordHistogram.recordSparseHistogram( - "Bluetooth.Web.Android.onServicesDiscovered.Status.Connected", - status); - - // TODO(crbug.com/576906): Update or replace existing GATT objects if they - // change after initial discovery. - for (Wrappers.BluetoothGattServiceWrapper service : - mBluetoothGatt.getServices()) { - // Create an adapter unique service ID. getInstanceId only differs - // between service instances with the same UUID on this device. - String serviceInstanceId = getAddress() + "/" - + service.getUuid().toString() + "," + service.getInstanceId(); - ChromeBluetoothDeviceJni.get().createGattRemoteService( - mNativeBluetoothDeviceAndroid, ChromeBluetoothDevice.this, - serviceInstanceId, service); - } - ChromeBluetoothDeviceJni.get().onGattServicesDiscovered( - mNativeBluetoothDeviceAndroid, ChromeBluetoothDevice.this); - } - } - }); - } - - @Override - public void onCharacteristicChanged( - final Wrappers.BluetoothGattCharacteristicWrapper characteristic) { - Log.i(TAG, "device onCharacteristicChanged."); - // Copy the characteristic's value for this event so that new notifications that - // arrive before the posted task runs do not affect this event's value. - final byte[] value = characteristic.getValue(); - Wrappers.ThreadUtilsWrapper.getInstance().runOnUiThread(new Runnable() { - @Override - public void run() { - ChromeBluetoothRemoteGattCharacteristic chromeCharacteristic = - mWrapperToChromeCharacteristicsMap.get(characteristic); - if (chromeCharacteristic == null) { - // Android events arriving with no Chrome object is expected rarely only - // when the event races object destruction. - Log.v(TAG, "onCharacteristicChanged when chromeCharacteristic == null."); - } else { - chromeCharacteristic.onCharacteristicChanged(value); - } - } - }); - } - - @Override - public void onCharacteristicRead( - final Wrappers.BluetoothGattCharacteristicWrapper characteristic, - final int status) { - Wrappers.ThreadUtilsWrapper.getInstance().runOnUiThread(new Runnable() { - @Override - public void run() { - ChromeBluetoothRemoteGattCharacteristic chromeCharacteristic = - mWrapperToChromeCharacteristicsMap.get(characteristic); - if (chromeCharacteristic == null) { - // Android events arriving with no Chrome object is expected rarely: only - // when the event races object destruction. - Log.v(TAG, "onCharacteristicRead when chromeCharacteristic == null."); - } else { - RecordHistogram.recordSparseHistogram( - "Bluetooth.Web.Android.onCharacteristicRead.Status", status); - chromeCharacteristic.onCharacteristicRead(status); - } - } - }); - } - - @Override - public void onCharacteristicWrite( - final Wrappers.BluetoothGattCharacteristicWrapper characteristic, - final int status) { - Wrappers.ThreadUtilsWrapper.getInstance().runOnUiThread(new Runnable() { - @Override - public void run() { - ChromeBluetoothRemoteGattCharacteristic chromeCharacteristic = - mWrapperToChromeCharacteristicsMap.get(characteristic); - if (chromeCharacteristic == null) { - // Android events arriving with no Chrome object is expected rarely: only - // when the event races object destruction. - Log.v(TAG, "onCharacteristicWrite when chromeCharacteristic == null."); - } else { - RecordHistogram.recordSparseHistogram( - "Bluetooth.Web.Android.onCharacteristicWrite.Status", status); - chromeCharacteristic.onCharacteristicWrite(status); - } - } - }); - } - - @Override - public void onDescriptorRead( - final Wrappers.BluetoothGattDescriptorWrapper descriptor, final int status) { - Wrappers.ThreadUtilsWrapper.getInstance().runOnUiThread(new Runnable() { - @Override - public void run() { - ChromeBluetoothRemoteGattDescriptor chromeDescriptor = - mWrapperToChromeDescriptorsMap.get(descriptor); - if (chromeDescriptor == null) { - // Android events arriving with no Chrome object is expected rarely: only - // when the event races object destruction. - Log.v(TAG, "onDescriptorRead when chromeDescriptor == null."); - } else { - RecordHistogram.recordSparseHistogram( - "Bluetooth.Web.Android.onDescriptorRead.Status", status); - chromeDescriptor.onDescriptorRead(status); - } - } - }); - } - - @Override - public void onDescriptorWrite( - final Wrappers.BluetoothGattDescriptorWrapper descriptor, final int status) { - Wrappers.ThreadUtilsWrapper.getInstance().runOnUiThread(new Runnable() { - @Override - public void run() { - ChromeBluetoothRemoteGattDescriptor chromeDescriptor = - mWrapperToChromeDescriptorsMap.get(descriptor); - if (chromeDescriptor == null) { - // Android events arriving with no Chrome object is expected rarely: only - // when the event races object destruction. - Log.v(TAG, "onDescriptorWrite when chromeDescriptor == null."); - } else { - RecordHistogram.recordSparseHistogram( - "Bluetooth.Web.Android.onDescriptorWrite.Status", status); - chromeDescriptor.onDescriptorWrite(status); - } - } - }); - } - } - - @NativeMethods - interface Natives { - // Binds to BluetoothDeviceAndroid::OnConnectionStateChange. - void onConnectionStateChange(long nativeBluetoothDeviceAndroid, - ChromeBluetoothDevice caller, int status, boolean connected); - - // Binds to BluetoothDeviceAndroid::CreateGattRemoteService. - void createGattRemoteService(long nativeBluetoothDeviceAndroid, - ChromeBluetoothDevice caller, String instanceId, - Wrappers.BluetoothGattServiceWrapper serviceWrapper); - - // Binds to BluetoothDeviceAndroid::GattServicesDiscovered. - void onGattServicesDiscovered( - long nativeBluetoothDeviceAndroid, ChromeBluetoothDevice caller); - } -} diff --git a/chromium/device/bluetooth/android/java/src/org/chromium/device/bluetooth/ChromeBluetoothRemoteGattCharacteristic.java b/chromium/device/bluetooth/android/java/src/org/chromium/device/bluetooth/ChromeBluetoothRemoteGattCharacteristic.java deleted file mode 100644 index 39156474261..00000000000 --- a/chromium/device/bluetooth/android/java/src/org/chromium/device/bluetooth/ChromeBluetoothRemoteGattCharacteristic.java +++ /dev/null @@ -1,196 +0,0 @@ -// Copyright 2015 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package org.chromium.device.bluetooth; - -import android.annotation.TargetApi; -import android.os.Build; - -import org.chromium.base.Log; -import org.chromium.base.annotations.CalledByNative; -import org.chromium.base.annotations.JNIAdditionalImport; -import org.chromium.base.annotations.JNINamespace; -import org.chromium.base.annotations.NativeMethods; - -import java.util.List; - -/** - * Exposes android.bluetooth.BluetoothGattCharacteristic as necessary - * for C++ device::BluetoothRemoteGattCharacteristicAndroid. - * - * Lifetime is controlled by - * device::BluetoothRemoteGattCharacteristicAndroid. - */ -@JNINamespace("device") -@JNIAdditionalImport(Wrappers.class) -@TargetApi(Build.VERSION_CODES.M) -final class ChromeBluetoothRemoteGattCharacteristic { - private static final String TAG = "Bluetooth"; - - private long mNativeBluetoothRemoteGattCharacteristicAndroid; - final Wrappers.BluetoothGattCharacteristicWrapper mCharacteristic; - final String mInstanceId; - final ChromeBluetoothDevice mChromeDevice; - - private ChromeBluetoothRemoteGattCharacteristic( - long nativeBluetoothRemoteGattCharacteristicAndroid, - Wrappers.BluetoothGattCharacteristicWrapper characteristicWrapper, String instanceId, - ChromeBluetoothDevice chromeDevice) { - mNativeBluetoothRemoteGattCharacteristicAndroid = - nativeBluetoothRemoteGattCharacteristicAndroid; - mCharacteristic = characteristicWrapper; - mInstanceId = instanceId; - mChromeDevice = chromeDevice; - - mChromeDevice.mWrapperToChromeCharacteristicsMap.put(characteristicWrapper, this); - - Log.v(TAG, "ChromeBluetoothRemoteGattCharacteristic created."); - } - - /** - * Handles C++ object being destroyed. - */ - @CalledByNative - private void onBluetoothRemoteGattCharacteristicAndroidDestruction() { - Log.v(TAG, "ChromeBluetoothRemoteGattCharacteristic Destroyed."); - if (mChromeDevice.mBluetoothGatt != null) { - mChromeDevice.mBluetoothGatt.setCharacteristicNotification(mCharacteristic, false); - } - mNativeBluetoothRemoteGattCharacteristicAndroid = 0; - mChromeDevice.mWrapperToChromeCharacteristicsMap.remove(mCharacteristic); - } - - void onCharacteristicChanged(byte[] value) { - Log.i(TAG, "onCharacteristicChanged"); - if (mNativeBluetoothRemoteGattCharacteristicAndroid != 0) { - ChromeBluetoothRemoteGattCharacteristicJni.get().onChanged( - mNativeBluetoothRemoteGattCharacteristicAndroid, - ChromeBluetoothRemoteGattCharacteristic.this, value); - } - } - - void onCharacteristicRead(int status) { - Log.i(TAG, "onCharacteristicRead status:%d==%s", status, - status == android.bluetooth.BluetoothGatt.GATT_SUCCESS ? "OK" : "Error"); - if (mNativeBluetoothRemoteGattCharacteristicAndroid != 0) { - ChromeBluetoothRemoteGattCharacteristicJni.get().onRead( - mNativeBluetoothRemoteGattCharacteristicAndroid, - ChromeBluetoothRemoteGattCharacteristic.this, status, - mCharacteristic.getValue()); - } - } - - void onCharacteristicWrite(int status) { - Log.i(TAG, "onCharacteristicWrite status:%d==%s", status, - status == android.bluetooth.BluetoothGatt.GATT_SUCCESS ? "OK" : "Error"); - if (mNativeBluetoothRemoteGattCharacteristicAndroid != 0) { - ChromeBluetoothRemoteGattCharacteristicJni.get().onWrite( - mNativeBluetoothRemoteGattCharacteristicAndroid, - ChromeBluetoothRemoteGattCharacteristic.this, status); - } - } - - // --------------------------------------------------------------------------------------------- - // BluetoothRemoteGattCharacteristicAndroid methods implemented in java: - - // Implements BluetoothRemoteGattCharacteristicAndroid::Create. - @CalledByNative - private static ChromeBluetoothRemoteGattCharacteristic create( - long nativeBluetoothRemoteGattCharacteristicAndroid, - Wrappers.BluetoothGattCharacteristicWrapper characteristicWrapper, String instanceId, - ChromeBluetoothDevice chromeDevice) { - return new ChromeBluetoothRemoteGattCharacteristic( - nativeBluetoothRemoteGattCharacteristicAndroid, characteristicWrapper, instanceId, - chromeDevice); - } - - // Implements BluetoothRemoteGattCharacteristicAndroid::GetUUID. - @CalledByNative - private String getUUID() { - return mCharacteristic.getUuid().toString(); - } - - // Implements BluetoothRemoteGattCharacteristicAndroid::GetProperties. - @CalledByNative - private int getProperties() { - // TODO(scheib): Must read Extended Properties Descriptor. crbug.com/548449 - return mCharacteristic.getProperties(); - } - - // Implements BluetoothRemoteGattCharacteristicAndroid::ReadRemoteCharacteristic. - @CalledByNative - private boolean readRemoteCharacteristic() { - if (!mChromeDevice.mBluetoothGatt.readCharacteristic(mCharacteristic)) { - Log.i(TAG, "readRemoteCharacteristic readCharacteristic failed."); - return false; - } - return true; - } - - // Implements BluetoothRemoteGattCharacteristicAndroid::WriteRemoteCharacteristic. - @CalledByNative - private boolean writeRemoteCharacteristic(byte[] value, int writeType) { - if (!mCharacteristic.setValue(value)) { - Log.i(TAG, "writeRemoteCharacteristic setValue failed."); - return false; - } - if (writeType != 0) { - mCharacteristic.setWriteType(writeType); - } - if (!mChromeDevice.mBluetoothGatt.writeCharacteristic(mCharacteristic)) { - Log.i(TAG, "writeRemoteCharacteristic writeCharacteristic failed."); - return false; - } - return true; - } - - // Enable or disable the notifications for this characteristic. - @CalledByNative - private boolean setCharacteristicNotification(boolean enabled) { - return mChromeDevice.mBluetoothGatt.setCharacteristicNotification(mCharacteristic, enabled); - } - - // Creates objects for all descriptors. Designed only to be called by - // BluetoothRemoteGattCharacteristicAndroid::EnsureDescriptorsCreated. - @CalledByNative - private void createDescriptors() { - List<Wrappers.BluetoothGattDescriptorWrapper> descriptors = - mCharacteristic.getDescriptors(); - // descriptorInstanceId ensures duplicate UUIDs have unique instance - // IDs. BluetoothGattDescriptor does not offer getInstanceId the way - // BluetoothGattCharacteristic does. - // - // TODO(crbug.com/576906) Do not reuse IDs upon onServicesDiscovered. - int instanceIdCounter = 0; - for (Wrappers.BluetoothGattDescriptorWrapper descriptor : descriptors) { - String descriptorInstanceId = - mInstanceId + "/" + descriptor.getUuid().toString() + ";" + instanceIdCounter++; - ChromeBluetoothRemoteGattCharacteristicJni.get().createGattRemoteDescriptor( - mNativeBluetoothRemoteGattCharacteristicAndroid, - ChromeBluetoothRemoteGattCharacteristic.this, descriptorInstanceId, descriptor, - mChromeDevice); - } - } - - @NativeMethods - interface Natives { - // Binds to BluetoothRemoteGattCharacteristicAndroid::OnChanged. - void onChanged(long nativeBluetoothRemoteGattCharacteristicAndroid, - ChromeBluetoothRemoteGattCharacteristic caller, byte[] value); - - // Binds to BluetoothRemoteGattCharacteristicAndroid::OnRead. - void onRead(long nativeBluetoothRemoteGattCharacteristicAndroid, - ChromeBluetoothRemoteGattCharacteristic caller, int status, byte[] value); - - // Binds to BluetoothRemoteGattCharacteristicAndroid::OnWrite. - void onWrite(long nativeBluetoothRemoteGattCharacteristicAndroid, - ChromeBluetoothRemoteGattCharacteristic caller, int status); - - // Binds to BluetoothRemoteGattCharacteristicAndroid::CreateGattRemoteDescriptor. - void createGattRemoteDescriptor(long nativeBluetoothRemoteGattCharacteristicAndroid, - ChromeBluetoothRemoteGattCharacteristic caller, String instanceId, - Wrappers.BluetoothGattDescriptorWrapper descriptorWrapper, - ChromeBluetoothDevice chromeBluetoothDevice); - } -} diff --git a/chromium/device/bluetooth/android/java/src/org/chromium/device/bluetooth/ChromeBluetoothRemoteGattDescriptor.java b/chromium/device/bluetooth/android/java/src/org/chromium/device/bluetooth/ChromeBluetoothRemoteGattDescriptor.java deleted file mode 100644 index 4214c4281be..00000000000 --- a/chromium/device/bluetooth/android/java/src/org/chromium/device/bluetooth/ChromeBluetoothRemoteGattDescriptor.java +++ /dev/null @@ -1,123 +0,0 @@ -// Copyright 2016 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package org.chromium.device.bluetooth; - -import org.chromium.base.Log; -import org.chromium.base.annotations.CalledByNative; -import org.chromium.base.annotations.JNIAdditionalImport; -import org.chromium.base.annotations.JNINamespace; -import org.chromium.base.annotations.NativeMethods; - -/** - * Exposes android.bluetooth.BluetoothGattDescriptor as necessary - * for C++ device::BluetoothRemoteGattDescriptorAndroid. - * - * Lifetime is controlled by device::BluetoothRemoteGattDescriptorAndroid. - */ -@JNINamespace("device") -@JNIAdditionalImport(Wrappers.class) -final class ChromeBluetoothRemoteGattDescriptor { - private static final String TAG = "Bluetooth"; - - private long mNativeBluetoothRemoteGattDescriptorAndroid; - final Wrappers.BluetoothGattDescriptorWrapper mDescriptor; - final ChromeBluetoothDevice mChromeDevice; - - private ChromeBluetoothRemoteGattDescriptor(long nativeBluetoothRemoteGattDescriptorAndroid, - Wrappers.BluetoothGattDescriptorWrapper descriptorWrapper, - ChromeBluetoothDevice chromeDevice) { - mNativeBluetoothRemoteGattDescriptorAndroid = nativeBluetoothRemoteGattDescriptorAndroid; - mDescriptor = descriptorWrapper; - mChromeDevice = chromeDevice; - - mChromeDevice.mWrapperToChromeDescriptorsMap.put(descriptorWrapper, this); - - Log.v(TAG, "ChromeBluetoothRemoteGattDescriptor created."); - } - - /** - * Handles C++ object being destroyed. - */ - @CalledByNative - private void onBluetoothRemoteGattDescriptorAndroidDestruction() { - Log.v(TAG, "ChromeBluetoothRemoteGattDescriptor Destroyed."); - mNativeBluetoothRemoteGattDescriptorAndroid = 0; - mChromeDevice.mWrapperToChromeDescriptorsMap.remove(mDescriptor); - } - - void onDescriptorRead(int status) { - Log.i(TAG, "onDescriptorRead status:%d==%s", status, - status == android.bluetooth.BluetoothGatt.GATT_SUCCESS ? "OK" : "Error"); - if (mNativeBluetoothRemoteGattDescriptorAndroid != 0) { - ChromeBluetoothRemoteGattDescriptorJni.get().onRead( - mNativeBluetoothRemoteGattDescriptorAndroid, - ChromeBluetoothRemoteGattDescriptor.this, status, mDescriptor.getValue()); - } - } - - void onDescriptorWrite(int status) { - Log.i(TAG, "onDescriptorWrite status:%d==%s", status, - status == android.bluetooth.BluetoothGatt.GATT_SUCCESS ? "OK" : "Error"); - if (mNativeBluetoothRemoteGattDescriptorAndroid != 0) { - ChromeBluetoothRemoteGattDescriptorJni.get().onWrite( - mNativeBluetoothRemoteGattDescriptorAndroid, - ChromeBluetoothRemoteGattDescriptor.this, status); - } - } - - // --------------------------------------------------------------------------------------------- - // BluetoothRemoteGattDescriptorAndroid methods implemented in java: - - // Implements BluetoothRemoteGattDescriptorAndroid::Create. - @CalledByNative - private static ChromeBluetoothRemoteGattDescriptor create( - long nativeBluetoothRemoteGattDescriptorAndroid, - Wrappers.BluetoothGattDescriptorWrapper descriptorWrapper, - ChromeBluetoothDevice chromeDevice) { - return new ChromeBluetoothRemoteGattDescriptor( - nativeBluetoothRemoteGattDescriptorAndroid, descriptorWrapper, chromeDevice); - } - - // Implements BluetoothRemoteGattDescriptorAndroid::GetUUID. - @CalledByNative - private String getUUID() { - return mDescriptor.getUuid().toString(); - } - - // Implements BluetoothRemoteGattDescriptorAndroid::ReadRemoteDescriptor. - @CalledByNative - private boolean readRemoteDescriptor() { - if (!mChromeDevice.mBluetoothGatt.readDescriptor(mDescriptor)) { - Log.i(TAG, "readRemoteDescriptor readDescriptor failed."); - return false; - } - return true; - } - - // Implements BluetoothRemoteGattDescriptorAndroid::WriteRemoteDescriptor. - @CalledByNative - private boolean writeRemoteDescriptor(byte[] value) { - if (!mDescriptor.setValue(value)) { - Log.i(TAG, "writeRemoteDescriptor setValue failed."); - return false; - } - if (!mChromeDevice.mBluetoothGatt.writeDescriptor(mDescriptor)) { - Log.i(TAG, "writeRemoteDescriptor writeDescriptor failed."); - return false; - } - return true; - } - - @NativeMethods - interface Natives { - // Binds to BluetoothRemoteGattDescriptorAndroid::OnRead. - void onRead(long nativeBluetoothRemoteGattDescriptorAndroid, - ChromeBluetoothRemoteGattDescriptor caller, int status, byte[] value); - - // Binds to BluetoothRemoteGattDescriptorAndroid::OnWrite. - void onWrite(long nativeBluetoothRemoteGattDescriptorAndroid, - ChromeBluetoothRemoteGattDescriptor caller, int status); - } -} diff --git a/chromium/device/bluetooth/android/java/src/org/chromium/device/bluetooth/ChromeBluetoothRemoteGattService.java b/chromium/device/bluetooth/android/java/src/org/chromium/device/bluetooth/ChromeBluetoothRemoteGattService.java deleted file mode 100644 index fd6803e1ece..00000000000 --- a/chromium/device/bluetooth/android/java/src/org/chromium/device/bluetooth/ChromeBluetoothRemoteGattService.java +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright 2015 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package org.chromium.device.bluetooth; - -import org.chromium.base.Log; -import org.chromium.base.annotations.CalledByNative; -import org.chromium.base.annotations.JNIAdditionalImport; -import org.chromium.base.annotations.JNINamespace; -import org.chromium.base.annotations.NativeMethods; - -import java.util.List; - -/** - * Exposes android.bluetooth.BluetoothGattService as necessary - * for C++ device::BluetoothRemoteGattServiceAndroid. - * - * Lifetime is controlled by - * device::BluetoothRemoteGattServiceAndroid. - */ -@JNINamespace("device") -@JNIAdditionalImport(Wrappers.class) -final class ChromeBluetoothRemoteGattService { - private static final String TAG = "Bluetooth"; - - private long mNativeBluetoothRemoteGattServiceAndroid; - final Wrappers.BluetoothGattServiceWrapper mService; - final String mInstanceId; - ChromeBluetoothDevice mChromeDevice; - - private ChromeBluetoothRemoteGattService(long nativeBluetoothRemoteGattServiceAndroid, - Wrappers.BluetoothGattServiceWrapper serviceWrapper, String instanceId, - ChromeBluetoothDevice chromeDevice) { - mNativeBluetoothRemoteGattServiceAndroid = nativeBluetoothRemoteGattServiceAndroid; - mService = serviceWrapper; - mInstanceId = instanceId; - mChromeDevice = chromeDevice; - Log.v(TAG, "ChromeBluetoothRemoteGattService created."); - } - - /** - * Handles C++ object being destroyed. - */ - @CalledByNative - private void onBluetoothRemoteGattServiceAndroidDestruction() { - mNativeBluetoothRemoteGattServiceAndroid = 0; - } - - // Implements BluetoothRemoteGattServiceAndroid::Create. - @CalledByNative - private static ChromeBluetoothRemoteGattService create( - long nativeBluetoothRemoteGattServiceAndroid, - Wrappers.BluetoothGattServiceWrapper serviceWrapper, String instanceId, - ChromeBluetoothDevice chromeDevice) { - return new ChromeBluetoothRemoteGattService( - nativeBluetoothRemoteGattServiceAndroid, serviceWrapper, instanceId, chromeDevice); - } - - // Implements BluetoothRemoteGattServiceAndroid::GetUUID. - @CalledByNative - private String getUUID() { - return mService.getUuid().toString(); - } - - // Creates objects for all characteristics. Designed only to be called by - // BluetoothRemoteGattServiceAndroid::EnsureCharacteristicsCreated. - @CalledByNative - private void createCharacteristics() { - List<Wrappers.BluetoothGattCharacteristicWrapper> characteristics = - mService.getCharacteristics(); - for (Wrappers.BluetoothGattCharacteristicWrapper characteristic : characteristics) { - // Create an adapter unique characteristic ID. getInstanceId only differs between - // characteristic instances with the same UUID on this service. - String characteristicInstanceId = mInstanceId + "/" - + characteristic.getUuid().toString() + "," + characteristic.getInstanceId(); - ChromeBluetoothRemoteGattServiceJni.get().createGattRemoteCharacteristic( - mNativeBluetoothRemoteGattServiceAndroid, ChromeBluetoothRemoteGattService.this, - characteristicInstanceId, characteristic, mChromeDevice); - } - } - - @NativeMethods - interface Natives { - // Binds to BluetoothRemoteGattServiceAndroid::CreateGattRemoteCharacteristic. - void createGattRemoteCharacteristic(long nativeBluetoothRemoteGattServiceAndroid, - ChromeBluetoothRemoteGattService caller, String instanceId, - Wrappers.BluetoothGattCharacteristicWrapper characteristicWrapper, - ChromeBluetoothDevice chromeBluetoothDevice); - } -} diff --git a/chromium/device/bluetooth/android/java/src/org/chromium/device/bluetooth/ChromeBluetoothScanFilterBuilder.java b/chromium/device/bluetooth/android/java/src/org/chromium/device/bluetooth/ChromeBluetoothScanFilterBuilder.java deleted file mode 100644 index e9e254670aa..00000000000 --- a/chromium/device/bluetooth/android/java/src/org/chromium/device/bluetooth/ChromeBluetoothScanFilterBuilder.java +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package org.chromium.device.bluetooth; - -import android.annotation.TargetApi; -import android.bluetooth.le.ScanFilter; -import android.os.Build; -import android.os.ParcelUuid; - -import org.chromium.base.annotations.CalledByNative; -import org.chromium.base.annotations.JNIAdditionalImport; -import org.chromium.base.annotations.JNINamespace; - -/** - * Exposes android.bluetooth.le.ScanFilter.Builder as necessary for C++. - * This class is used to implement - * BluetoothAdapterAndroid::CreateAndroidFilter() - */ -@JNINamespace("device") -@JNIAdditionalImport(Wrappers.class) -@TargetApi(Build.VERSION_CODES.M) -final class ChromeBluetoothScanFilterBuilder { - private ScanFilter.Builder mBuilder; - - /** - * Constructs a ChromeBluetoothScanFilter - */ - public ChromeBluetoothScanFilterBuilder() { - mBuilder = new ScanFilter.Builder(); - } - - // Creates and returns a new ChromeBluetoothScanFilterBuilder - @CalledByNative - private static ChromeBluetoothScanFilterBuilder create() { - return new ChromeBluetoothScanFilterBuilder(); - } - - @CalledByNative - private void setServiceUuid(String uuid) { - if (uuid != null) { - mBuilder.setServiceUuid(ParcelUuid.fromString(uuid)); - } - } - - @CalledByNative - private void setDeviceName(String deviceName) { - if (deviceName != null) { - mBuilder.setDeviceName(deviceName); - } - } - - @CalledByNative - public ScanFilter build() { - return mBuilder.build(); - } -} diff --git a/chromium/device/bluetooth/android/java/src/org/chromium/device/bluetooth/ChromeBluetoothScanFilterList.java b/chromium/device/bluetooth/android/java/src/org/chromium/device/bluetooth/ChromeBluetoothScanFilterList.java deleted file mode 100644 index 79d290cb495..00000000000 --- a/chromium/device/bluetooth/android/java/src/org/chromium/device/bluetooth/ChromeBluetoothScanFilterList.java +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package org.chromium.device.bluetooth; - -import android.annotation.TargetApi; -import android.bluetooth.le.ScanFilter; -import android.os.Build; - -import org.chromium.base.annotations.CalledByNative; -import org.chromium.base.annotations.JNIAdditionalImport; -import org.chromium.base.annotations.JNINamespace; - -import java.util.ArrayList; - -/** - * Allows for the creation of a Java ArrayList of the ScanFilter object. - */ -@JNINamespace("device") -@JNIAdditionalImport(Wrappers.class) -@TargetApi(Build.VERSION_CODES.M) -final class ChromeBluetoothScanFilterList { - ArrayList<ScanFilter> mFilters; - - /** - * Constructs a ChromeBluetoothScanFilterList - */ - public ChromeBluetoothScanFilterList() { - mFilters = new ArrayList<>(); - } - - @CalledByNative - private static ChromeBluetoothScanFilterList create() { - return new ChromeBluetoothScanFilterList(); - } - - @CalledByNative - private void addFilter(ScanFilter filter) { - mFilters.add(filter); - } - - @CalledByNative - public ArrayList<ScanFilter> getList() { - return mFilters; - } -} diff --git a/chromium/device/bluetooth/android/java/src/org/chromium/device/bluetooth/Wrappers.java b/chromium/device/bluetooth/android/java/src/org/chromium/device/bluetooth/Wrappers.java deleted file mode 100644 index 3829a8ff345..00000000000 --- a/chromium/device/bluetooth/android/java/src/org/chromium/device/bluetooth/Wrappers.java +++ /dev/null @@ -1,643 +0,0 @@ -// Copyright 2015 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package org.chromium.device.bluetooth; - -import android.Manifest; -import android.annotation.SuppressLint; -import android.annotation.TargetApi; -import android.bluetooth.BluetoothAdapter; -import android.bluetooth.BluetoothDevice; -import android.bluetooth.BluetoothGatt; -import android.bluetooth.BluetoothGattCallback; -import android.bluetooth.BluetoothGattCharacteristic; -import android.bluetooth.BluetoothGattDescriptor; -import android.bluetooth.BluetoothGattService; -import android.bluetooth.le.BluetoothLeScanner; -import android.bluetooth.le.ScanCallback; -import android.bluetooth.le.ScanFilter; -import android.bluetooth.le.ScanResult; -import android.bluetooth.le.ScanSettings; -import android.content.Context; -import android.content.pm.PackageManager; -import android.os.Build; -import android.os.ParcelUuid; -import android.util.SparseArray; - -import org.chromium.base.ContextUtils; -import org.chromium.base.Log; -import org.chromium.base.ThreadUtils; -import org.chromium.base.annotations.CalledByNative; -import org.chromium.base.annotations.JNINamespace; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; - -/** - * Wrapper classes around android.bluetooth.* classes that provide an - * indirection layer enabling fake implementations when running tests. - * - * Each Wrapper base class accepts an Android API object and passes through - * calls to it. When under test, Fake subclasses override all methods that - * pass through to the Android object and instead provide fake implementations. - */ -@JNINamespace("device") -@TargetApi(Build.VERSION_CODES.M) -class Wrappers { - private static final String TAG = "Bluetooth"; - - public static final int DEVICE_CLASS_UNSPECIFIED = 0x1F00; - - /** - * Wraps base.ThreadUtils. - * base.ThreadUtils has a set of static method to interact with the - * UI Thread. To be able to provide a set of test methods, ThreadUtilsWrapper - * uses the factory pattern. - */ - static class ThreadUtilsWrapper { - private static Factory sFactory; - - private static ThreadUtilsWrapper sInstance; - - protected ThreadUtilsWrapper() {} - - /** - * Returns the singleton instance of ThreadUtilsWrapper, creating it if needed. - */ - public static ThreadUtilsWrapper getInstance() { - if (sInstance == null) { - if (sFactory == null) { - sInstance = new ThreadUtilsWrapper(); - } else { - sInstance = sFactory.create(); - } - } - return sInstance; - } - - public void runOnUiThread(Runnable r) { - ThreadUtils.runOnUiThread(r); - } - - /** - * Instantiate this to explain how to create a ThreadUtilsWrapper instance in - * ThreadUtilsWrapper.getInstance(). - */ - public interface Factory { public ThreadUtilsWrapper create(); } - - /** - * Call this to use a different subclass of ThreadUtilsWrapper throughout the program. - */ - public static void setFactory(Factory factory) { - sFactory = factory; - sInstance = null; - } - } - - /** - * Wraps android.bluetooth.BluetoothAdapter. - */ - static class BluetoothAdapterWrapper { - private final BluetoothAdapter mAdapter; - protected final Context mContext; - protected BluetoothLeScannerWrapper mScannerWrapper; - - /** - * Creates a BluetoothAdapterWrapper using the default - * android.bluetooth.BluetoothAdapter. May fail if the default adapter - * is not available or if the application does not have sufficient - * permissions. - */ - @CalledByNative("BluetoothAdapterWrapper") - public static BluetoothAdapterWrapper createWithDefaultAdapter() { - final boolean hasMinAPI = Build.VERSION.SDK_INT >= Build.VERSION_CODES.M; - if (!hasMinAPI) { - Log.i(TAG, "BluetoothAdapterWrapper.create failed: SDK version (%d) too low.", - Build.VERSION.SDK_INT); - return null; - } - - final boolean hasPermissions = - ContextUtils.getApplicationContext().checkCallingOrSelfPermission( - Manifest.permission.BLUETOOTH) - == PackageManager.PERMISSION_GRANTED - && ContextUtils.getApplicationContext().checkCallingOrSelfPermission( - Manifest.permission.BLUETOOTH_ADMIN) - == PackageManager.PERMISSION_GRANTED; - if (!hasPermissions) { - Log.w(TAG, "BluetoothAdapterWrapper.create failed: Lacking Bluetooth permissions."); - return null; - } - - // Only Low Energy currently supported, see BluetoothAdapterAndroid class note. - final boolean hasLowEnergyFeature = - Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 - && ContextUtils.getApplicationContext().getPackageManager().hasSystemFeature( - PackageManager.FEATURE_BLUETOOTH_LE); - if (!hasLowEnergyFeature) { - Log.i(TAG, "BluetoothAdapterWrapper.create failed: No Low Energy support."); - return null; - } - - BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); - if (adapter == null) { - Log.i(TAG, "BluetoothAdapterWrapper.create failed: Default adapter not found."); - return null; - } else { - return new BluetoothAdapterWrapper(adapter, ContextUtils.getApplicationContext()); - } - } - - public BluetoothAdapterWrapper(BluetoothAdapter adapter, Context context) { - mAdapter = adapter; - mContext = context; - } - - public boolean disable() { - return mAdapter.disable(); - } - - public boolean enable() { - return mAdapter.enable(); - } - - @SuppressLint("HardwareIds") - public String getAddress() { - return mAdapter.getAddress(); - } - - public BluetoothLeScannerWrapper getBluetoothLeScanner() { - BluetoothLeScanner scanner = mAdapter.getBluetoothLeScanner(); - if (scanner == null) { - return null; - } - if (mScannerWrapper == null) { - mScannerWrapper = new BluetoothLeScannerWrapper(scanner); - } - return mScannerWrapper; - } - - public Context getContext() { - return mContext; - } - - public String getName() { - return mAdapter.getName(); - } - - public int getScanMode() { - return mAdapter.getScanMode(); - } - - public boolean isDiscovering() { - return mAdapter.isDiscovering(); - } - - public boolean isEnabled() { - return mAdapter.isEnabled(); - } - } - - /** - * Wraps android.bluetooth.BluetoothLeScanner. - */ - static class BluetoothLeScannerWrapper { - protected final BluetoothLeScanner mScanner; - private final HashMap<ScanCallbackWrapper, ForwardScanCallbackToWrapper> mCallbacks; - - public BluetoothLeScannerWrapper(BluetoothLeScanner scanner) { - mScanner = scanner; - mCallbacks = new HashMap<ScanCallbackWrapper, ForwardScanCallbackToWrapper>(); - } - - public void startScan( - List<ScanFilter> filters, int scanSettingsScanMode, ScanCallbackWrapper callback) { - ScanSettings settings = - new ScanSettings.Builder().setScanMode(scanSettingsScanMode).build(); - - ForwardScanCallbackToWrapper callbackForwarder = - new ForwardScanCallbackToWrapper(callback); - mCallbacks.put(callback, callbackForwarder); - - mScanner.startScan(filters, settings, callbackForwarder); - } - - public void stopScan(ScanCallbackWrapper callback) { - ForwardScanCallbackToWrapper callbackForwarder = mCallbacks.remove(callback); - mScanner.stopScan(callbackForwarder); - } - } - - /** - * Implements android.bluetooth.le.ScanCallback and forwards calls through to a - * provided ScanCallbackWrapper instance. - * - * This class is required so that Fakes can use ScanCallbackWrapper without - * it extending from ScanCallback. Fakes must function even on Android - * versions where ScanCallback class is not defined. - */ - static class ForwardScanCallbackToWrapper extends ScanCallback { - final ScanCallbackWrapper mWrapperCallback; - - ForwardScanCallbackToWrapper(ScanCallbackWrapper wrapperCallback) { - mWrapperCallback = wrapperCallback; - } - - @Override - public void onBatchScanResults(List<ScanResult> results) { - ArrayList<ScanResultWrapper> resultsWrapped = - new ArrayList<ScanResultWrapper>(results.size()); - for (ScanResult result : results) { - resultsWrapped.add(new ScanResultWrapper(result)); - } - mWrapperCallback.onBatchScanResult(resultsWrapped); - } - - @Override - public void onScanResult(int callbackType, ScanResult result) { - mWrapperCallback.onScanResult(callbackType, new ScanResultWrapper(result)); - } - - @Override - public void onScanFailed(int errorCode) { - mWrapperCallback.onScanFailed(errorCode); - } - } - - /** - * Wraps android.bluetooth.le.ScanCallback, being called by ScanCallbackImpl. - */ - abstract static class ScanCallbackWrapper { - public abstract void onBatchScanResult(List<ScanResultWrapper> results); - public abstract void onScanResult(int callbackType, ScanResultWrapper result); - public abstract void onScanFailed(int errorCode); - } - - /** - * Wraps android.bluetooth.le.ScanResult. - */ - static class ScanResultWrapper { - private final ScanResult mScanResult; - - public ScanResultWrapper(ScanResult scanResult) { - mScanResult = scanResult; - } - - public BluetoothDeviceWrapper getDevice() { - return new BluetoothDeviceWrapper(mScanResult.getDevice()); - } - - public int getRssi() { - return mScanResult.getRssi(); - } - - public List<ParcelUuid> getScanRecord_getServiceUuids() { - return mScanResult.getScanRecord().getServiceUuids(); - } - - public Map<ParcelUuid, byte[]> getScanRecord_getServiceData() { - return mScanResult.getScanRecord().getServiceData(); - } - - public SparseArray<byte[]> getScanRecord_getManufacturerSpecificData() { - return mScanResult.getScanRecord().getManufacturerSpecificData(); - } - - public int getScanRecord_getTxPowerLevel() { - return mScanResult.getScanRecord().getTxPowerLevel(); - } - - public String getScanRecord_getDeviceName() { - return mScanResult.getScanRecord().getDeviceName(); - } - - public int getScanRecord_getAdvertiseFlags() { - return mScanResult.getScanRecord().getAdvertiseFlags(); - } - } - - /** - * Wraps android.bluetooth.BluetoothDevice. - */ - static class BluetoothDeviceWrapper { - private final BluetoothDevice mDevice; - private final HashMap<BluetoothGattCharacteristic, BluetoothGattCharacteristicWrapper> - mCharacteristicsToWrappers; - private final HashMap<BluetoothGattDescriptor, BluetoothGattDescriptorWrapper> - mDescriptorsToWrappers; - - public BluetoothDeviceWrapper(BluetoothDevice device) { - mDevice = device; - mCharacteristicsToWrappers = - new HashMap<BluetoothGattCharacteristic, BluetoothGattCharacteristicWrapper>(); - mDescriptorsToWrappers = - new HashMap<BluetoothGattDescriptor, BluetoothGattDescriptorWrapper>(); - } - - public BluetoothGattWrapper connectGatt(Context context, boolean autoConnect, - BluetoothGattCallbackWrapper callback, int transport) { - return new BluetoothGattWrapper( - mDevice.connectGatt(context, autoConnect, - new ForwardBluetoothGattCallbackToWrapper(callback, this), transport), - this); - } - - public String getAddress() { - return mDevice.getAddress(); - } - - public int getBluetoothClass_getDeviceClass() { - if (mDevice == null || mDevice.getBluetoothClass() == null) { - // BluetoothDevice.getBluetoothClass() returns null if adapter has been powered off. - // Return DEVICE_CLASS_UNSPECIFIED in these cases. - return DEVICE_CLASS_UNSPECIFIED; - } - return mDevice.getBluetoothClass().getDeviceClass(); - } - - public int getBondState() { - return mDevice.getBondState(); - } - - public String getName() { - return mDevice.getName(); - } - } - - /** - * Wraps android.bluetooth.BluetoothGatt. - */ - static class BluetoothGattWrapper { - private final BluetoothGatt mGatt; - private final BluetoothDeviceWrapper mDeviceWrapper; - - BluetoothGattWrapper(BluetoothGatt gatt, BluetoothDeviceWrapper deviceWrapper) { - mGatt = gatt; - mDeviceWrapper = deviceWrapper; - } - - public void disconnect() { - mGatt.disconnect(); - } - - public void close() { - mGatt.close(); - } - - public void discoverServices() { - mGatt.discoverServices(); - } - - public List<BluetoothGattServiceWrapper> getServices() { - List<BluetoothGattService> services = mGatt.getServices(); - ArrayList<BluetoothGattServiceWrapper> servicesWrapped = - new ArrayList<BluetoothGattServiceWrapper>(services.size()); - for (BluetoothGattService service : services) { - servicesWrapped.add(new BluetoothGattServiceWrapper(service, mDeviceWrapper)); - } - return servicesWrapped; - } - - boolean readCharacteristic(BluetoothGattCharacteristicWrapper characteristic) { - return mGatt.readCharacteristic(characteristic.mCharacteristic); - } - - boolean setCharacteristicNotification( - BluetoothGattCharacteristicWrapper characteristic, boolean enable) { - return mGatt.setCharacteristicNotification(characteristic.mCharacteristic, enable); - } - - boolean writeCharacteristic(BluetoothGattCharacteristicWrapper characteristic) { - return mGatt.writeCharacteristic(characteristic.mCharacteristic); - } - - boolean readDescriptor(BluetoothGattDescriptorWrapper descriptor) { - return mGatt.readDescriptor(descriptor.mDescriptor); - } - - boolean writeDescriptor(BluetoothGattDescriptorWrapper descriptor) { - return mGatt.writeDescriptor(descriptor.mDescriptor); - } - } - - /** - * Implements android.bluetooth.BluetoothGattCallback and forwards calls through - * to a provided BluetoothGattCallbackWrapper instance. - * - * This class is required so that Fakes can use BluetoothGattCallbackWrapper - * without it extending from BluetoothGattCallback. Fakes must function even on - * Android versions where BluetoothGattCallback class is not defined. - */ - static class ForwardBluetoothGattCallbackToWrapper extends BluetoothGattCallback { - final BluetoothGattCallbackWrapper mWrapperCallback; - final BluetoothDeviceWrapper mDeviceWrapper; - - ForwardBluetoothGattCallbackToWrapper(BluetoothGattCallbackWrapper wrapperCallback, - BluetoothDeviceWrapper deviceWrapper) { - mWrapperCallback = wrapperCallback; - mDeviceWrapper = deviceWrapper; - } - - @Override - public void onCharacteristicChanged( - BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { - Log.i(TAG, "wrapper onCharacteristicChanged."); - mWrapperCallback.onCharacteristicChanged( - mDeviceWrapper.mCharacteristicsToWrappers.get(characteristic)); - } - - @Override - public void onCharacteristicRead( - BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { - mWrapperCallback.onCharacteristicRead( - mDeviceWrapper.mCharacteristicsToWrappers.get(characteristic), status); - } - - @Override - public void onCharacteristicWrite( - BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { - mWrapperCallback.onCharacteristicWrite( - mDeviceWrapper.mCharacteristicsToWrappers.get(characteristic), status); - } - - @Override - public void onDescriptorRead( - BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { - mWrapperCallback.onDescriptorRead( - mDeviceWrapper.mDescriptorsToWrappers.get(descriptor), status); - } - - @Override - public void onDescriptorWrite( - BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { - mWrapperCallback.onDescriptorWrite( - mDeviceWrapper.mDescriptorsToWrappers.get(descriptor), status); - } - - @Override - public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { - mWrapperCallback.onConnectionStateChange(status, newState); - } - - @Override - public void onServicesDiscovered(BluetoothGatt gatt, int status) { - mWrapperCallback.onServicesDiscovered(status); - } - } - - /** - * Wrapper alternative to android.bluetooth.BluetoothGattCallback allowing clients and Fakes to - * work on older SDK versions without having a dependency on the class not defined there. - * - * BluetoothGatt gatt parameters are omitted from methods as each call would - * need to look up the correct BluetoothGattWrapper instance. - * Client code should cache the BluetoothGattWrapper provided if - * necessary from the initial BluetoothDeviceWrapper.connectGatt - * call. - */ - abstract static class BluetoothGattCallbackWrapper { - public abstract void onCharacteristicChanged( - BluetoothGattCharacteristicWrapper characteristic); - public abstract void onCharacteristicRead( - BluetoothGattCharacteristicWrapper characteristic, int status); - public abstract void onCharacteristicWrite( - BluetoothGattCharacteristicWrapper characteristic, int status); - public abstract void onDescriptorRead( - BluetoothGattDescriptorWrapper descriptor, int status); - public abstract void onDescriptorWrite( - BluetoothGattDescriptorWrapper descriptor, int status); - public abstract void onConnectionStateChange(int status, int newState); - public abstract void onServicesDiscovered(int status); - } - - /** - * Wraps android.bluetooth.BluetoothGattService. - */ - static class BluetoothGattServiceWrapper { - private final BluetoothGattService mService; - private final BluetoothDeviceWrapper mDeviceWrapper; - - public BluetoothGattServiceWrapper( - BluetoothGattService service, BluetoothDeviceWrapper deviceWrapper) { - mService = service; - mDeviceWrapper = deviceWrapper; - } - - public List<BluetoothGattCharacteristicWrapper> getCharacteristics() { - List<BluetoothGattCharacteristic> characteristics = mService.getCharacteristics(); - ArrayList<BluetoothGattCharacteristicWrapper> characteristicsWrapped = - new ArrayList<BluetoothGattCharacteristicWrapper>(characteristics.size()); - for (BluetoothGattCharacteristic characteristic : characteristics) { - BluetoothGattCharacteristicWrapper characteristicWrapper = - mDeviceWrapper.mCharacteristicsToWrappers.get(characteristic); - if (characteristicWrapper == null) { - characteristicWrapper = - new BluetoothGattCharacteristicWrapper(characteristic, mDeviceWrapper); - mDeviceWrapper.mCharacteristicsToWrappers.put( - characteristic, characteristicWrapper); - } - characteristicsWrapped.add(characteristicWrapper); - } - return characteristicsWrapped; - } - - public int getInstanceId() { - return mService.getInstanceId(); - } - - public UUID getUuid() { - return mService.getUuid(); - } - } - - /** - * Wraps android.bluetooth.BluetoothGattCharacteristic. - */ - static class BluetoothGattCharacteristicWrapper { - final BluetoothGattCharacteristic mCharacteristic; - final BluetoothDeviceWrapper mDeviceWrapper; - - public BluetoothGattCharacteristicWrapper( - BluetoothGattCharacteristic characteristic, BluetoothDeviceWrapper deviceWrapper) { - mCharacteristic = characteristic; - mDeviceWrapper = deviceWrapper; - } - - public List<BluetoothGattDescriptorWrapper> getDescriptors() { - List<BluetoothGattDescriptor> descriptors = mCharacteristic.getDescriptors(); - - ArrayList<BluetoothGattDescriptorWrapper> descriptorsWrapped = - new ArrayList<BluetoothGattDescriptorWrapper>(descriptors.size()); - - for (BluetoothGattDescriptor descriptor : descriptors) { - BluetoothGattDescriptorWrapper descriptorWrapper = - mDeviceWrapper.mDescriptorsToWrappers.get(descriptor); - if (descriptorWrapper == null) { - descriptorWrapper = - new BluetoothGattDescriptorWrapper(descriptor, mDeviceWrapper); - mDeviceWrapper.mDescriptorsToWrappers.put(descriptor, descriptorWrapper); - } - descriptorsWrapped.add(descriptorWrapper); - } - return descriptorsWrapped; - } - - public int getInstanceId() { - return mCharacteristic.getInstanceId(); - } - - public int getProperties() { - return mCharacteristic.getProperties(); - } - - public UUID getUuid() { - return mCharacteristic.getUuid(); - } - - public byte[] getValue() { - return mCharacteristic.getValue(); - } - - public boolean setValue(byte[] value) { - return mCharacteristic.setValue(value); - } - - public void setWriteType(int writeType) { - mCharacteristic.setWriteType(writeType); - } - } - - /** - * Wraps android.bluetooth.BluetoothGattDescriptor. - */ - static class BluetoothGattDescriptorWrapper { - private final BluetoothGattDescriptor mDescriptor; - final BluetoothDeviceWrapper mDeviceWrapper; - - public BluetoothGattDescriptorWrapper( - BluetoothGattDescriptor descriptor, BluetoothDeviceWrapper deviceWrapper) { - mDescriptor = descriptor; - mDeviceWrapper = deviceWrapper; - } - - public BluetoothGattCharacteristicWrapper getCharacteristic() { - return mDeviceWrapper.mCharacteristicsToWrappers.get(mDescriptor.getCharacteristic()); - } - - public UUID getUuid() { - return mDescriptor.getUuid(); - } - - public byte[] getValue() { - return mDescriptor.getValue(); - } - - public boolean setValue(byte[] value) { - return mDescriptor.setValue(value); - } - } -} diff --git a/chromium/device/bluetooth/bluetooth_adapter.h b/chromium/device/bluetooth/bluetooth_adapter.h index 415c01c72a4..16e00340842 100644 --- a/chromium/device/bluetooth/bluetooth_adapter.h +++ b/chromium/device/bluetooth/bluetooth_adapter.h @@ -19,6 +19,7 @@ #include "base/containers/queue.h" #include "base/memory/ref_counted.h" #include "base/memory/weak_ptr.h" +#include "base/optional.h" #include "base/time/time.h" #include "build/build_config.h" #include "device/bluetooth/bluetooth_advertisement.h" @@ -311,9 +312,9 @@ class DEVICE_BLUETOOTH_EXPORT BluetoothAdapter ServiceOptions(); ~ServiceOptions(); - std::unique_ptr<int> channel; - std::unique_ptr<int> psm; - std::unique_ptr<std::string> name; + base::Optional<int> channel; + base::Optional<int> psm; + base::Optional<std::string> name; }; // The ErrorCallback is used for methods that can fail in which case it is diff --git a/chromium/device/bluetooth/bluetooth_adapter_factory_wrapper.cc b/chromium/device/bluetooth/bluetooth_adapter_factory_wrapper.cc deleted file mode 100644 index 3a3441fc48a..00000000000 --- a/chromium/device/bluetooth/bluetooth_adapter_factory_wrapper.cc +++ /dev/null @@ -1,150 +0,0 @@ -// Copyright 2016 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#include "device/bluetooth/bluetooth_adapter_factory_wrapper.h" - -#include <stddef.h> - -#include <utility> - -#include "base/bind.h" -#include "base/location.h" -#include "base/threading/thread_task_runner_handle.h" -#include "device/bluetooth/bluetooth_adapter_factory.h" - -namespace { - -static base::LazyInstance<device::BluetoothAdapterFactoryWrapper>::Leaky - g_bluetooth_adapter_factory_wrapper_singleton = LAZY_INSTANCE_INITIALIZER; - -} // namespace - -namespace device { - -BluetoothAdapterFactoryWrapper::~BluetoothAdapterFactoryWrapper() { - DCHECK(thread_checker_.CalledOnValidThread()); - // All observers should have been removed already. - DCHECK(adapter_observers_.empty()); - // Clear adapter. - set_adapter(scoped_refptr<BluetoothAdapter>()); -} - -// static -BluetoothAdapterFactoryWrapper& BluetoothAdapterFactoryWrapper::Get() { - return g_bluetooth_adapter_factory_wrapper_singleton.Get(); -} - -bool BluetoothAdapterFactoryWrapper::IsLowEnergySupported() { - DCHECK(thread_checker_.CalledOnValidThread()); - if (adapter_ != nullptr) { - return true; - } - return BluetoothAdapterFactory::Get()->IsLowEnergySupported(); -} - -void BluetoothAdapterFactoryWrapper::AcquireAdapter( - BluetoothAdapter::Observer* observer, - AcquireAdapterCallback callback) { - DCHECK(thread_checker_.CalledOnValidThread()); - DCHECK(!GetAdapter(observer)); - - AddAdapterObserver(observer); - if (adapter_) { - base::ThreadTaskRunnerHandle::Get()->PostTask( - FROM_HERE, base::BindOnce(std::move(callback), adapter_)); - return; - } - - DCHECK(BluetoothAdapterFactory::Get()->IsLowEnergySupported()); - BluetoothAdapterFactory::Get()->GetAdapter( - base::BindOnce(&BluetoothAdapterFactoryWrapper::OnGetAdapter, - weak_ptr_factory_.GetWeakPtr(), std::move(callback))); -} - -void BluetoothAdapterFactoryWrapper::ReleaseAdapter( - BluetoothAdapter::Observer* observer) { - DCHECK(thread_checker_.CalledOnValidThread()); - if (!HasAdapter(observer)) { - return; - } - RemoveAdapterObserver(observer); - if (adapter_observers_.empty()) - set_adapter(scoped_refptr<BluetoothAdapter>()); -} - -BluetoothAdapter* BluetoothAdapterFactoryWrapper::GetAdapter( - BluetoothAdapter::Observer* observer) { - DCHECK(thread_checker_.CalledOnValidThread()); - if (HasAdapter(observer)) { - return adapter_.get(); - } - return nullptr; -} - -void BluetoothAdapterFactoryWrapper::SetBluetoothAdapterForTesting( - scoped_refptr<BluetoothAdapter> mock_adapter) { - DCHECK(thread_checker_.CalledOnValidThread()); - set_adapter(std::move(mock_adapter)); -} - -BluetoothAdapterFactoryWrapper::BluetoothAdapterFactoryWrapper() { - DCHECK(thread_checker_.CalledOnValidThread()); -} - -void BluetoothAdapterFactoryWrapper::OnGetAdapter( - AcquireAdapterCallback continuation, - scoped_refptr<BluetoothAdapter> adapter) { - DCHECK(thread_checker_.CalledOnValidThread()); - - set_adapter(adapter); - std::move(continuation).Run(adapter_); -} - -bool BluetoothAdapterFactoryWrapper::HasAdapter( - BluetoothAdapter::Observer* observer) { - DCHECK(thread_checker_.CalledOnValidThread()); - - return base::Contains(adapter_observers_, observer); -} - -void BluetoothAdapterFactoryWrapper::AddAdapterObserver( - BluetoothAdapter::Observer* observer) { - DCHECK(thread_checker_.CalledOnValidThread()); - - auto iter = adapter_observers_.insert(observer); - DCHECK(iter.second); - if (adapter_) { - adapter_->AddObserver(observer); - } -} - -void BluetoothAdapterFactoryWrapper::RemoveAdapterObserver( - BluetoothAdapter::Observer* observer) { - DCHECK(thread_checker_.CalledOnValidThread()); - - size_t removed = adapter_observers_.erase(observer); - DCHECK(removed); - if (adapter_) { - adapter_->RemoveObserver(observer); - } -} - -void BluetoothAdapterFactoryWrapper::set_adapter( - scoped_refptr<BluetoothAdapter> adapter) { - DCHECK(thread_checker_.CalledOnValidThread()); - - if (adapter_.get()) { - for (BluetoothAdapter::Observer* observer : adapter_observers_) { - adapter_->RemoveObserver(observer); - } - } - adapter_ = adapter; - if (adapter_.get()) { - for (BluetoothAdapter::Observer* observer : adapter_observers_) { - adapter_->AddObserver(observer); - } - } -} - -} // namespace device diff --git a/chromium/device/bluetooth/bluetooth_adapter_factory_wrapper.h b/chromium/device/bluetooth/bluetooth_adapter_factory_wrapper.h deleted file mode 100644 index 74ce72d9300..00000000000 --- a/chromium/device/bluetooth/bluetooth_adapter_factory_wrapper.h +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright 2016 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#ifndef DEVICE_BLUETOOTH_BLUETOOTH_ADAPTER_FACTORY_WRAPPER_H_ -#define DEVICE_BLUETOOTH_BLUETOOTH_ADAPTER_FACTORY_WRAPPER_H_ - -#include <unordered_set> - -#include "base/lazy_instance.h" -#include "base/macros.h" -#include "base/threading/thread_checker.h" -#include "device/bluetooth/bluetooth_adapter.h" -#include "device/bluetooth/bluetooth_export.h" - -namespace device { - -// Wrapper around BluetoothAdapterFactory that allows us to change -// the underlying BluetoothAdapter object and have the observers -// observe the new instance of the object. -// TODO(ortuno): Once there is no need to swap the adapter to change its -// behavior observers should add/remove themselves to/from the adapter. -// http://crbug.com/603291 -class DEVICE_BLUETOOTH_EXPORT BluetoothAdapterFactoryWrapper { - public: - using AcquireAdapterCallback = - base::OnceCallback<void(scoped_refptr<BluetoothAdapter>)>; - - ~BluetoothAdapterFactoryWrapper(); - - static BluetoothAdapterFactoryWrapper& Get(); - - // Returns true if the platform supports Bluetooth Low Energy or if - // SetBluetoothAdapterForTesting has been called. - bool IsLowEnergySupported(); - - // Adds |observer| to the set of adapter observers. If another observer has - // acquired the adapter in the past it adds |observer| as an observer to that - // adapter, otherwise it gets a new adapter and adds |observer| to it. Runs - // |callback| with the adapter |observer| has been added to. - void AcquireAdapter(BluetoothAdapter::Observer* observer, - AcquireAdapterCallback callback); - // Removes |observer| from the list of adapter observers if |observer| - // has acquired the adapter in the past. If there are no more observers - // it deletes the reference to the adapter. - void ReleaseAdapter(BluetoothAdapter::Observer* observer); - - // Returns an adapter if |observer| has acquired an adapter in the past and - // this instance holds a reference to an adapter. Otherwise returns nullptr. - BluetoothAdapter* GetAdapter(BluetoothAdapter::Observer* observer); - - // Sets a new BluetoothAdapter to be returned by GetAdapter. When setting - // a new adapter all observers from the old adapter are removed and added - // to |mock_adapter|. - void SetBluetoothAdapterForTesting( - scoped_refptr<BluetoothAdapter> mock_adapter); - - private: - // friend LazyInstance to permit access to private constructor. - friend base::LazyInstanceTraitsBase<BluetoothAdapterFactoryWrapper>; - - BluetoothAdapterFactoryWrapper(); - - void OnGetAdapter(AcquireAdapterCallback continuation, - scoped_refptr<BluetoothAdapter> adapter); - - bool HasAdapter(BluetoothAdapter::Observer* observer); - void AddAdapterObserver(BluetoothAdapter::Observer* observer); - void RemoveAdapterObserver(BluetoothAdapter::Observer* observer); - - // Sets |adapter_| to a BluetoothAdapter instance and register observers, - // releasing references to previous |adapter_|. - void set_adapter(scoped_refptr<BluetoothAdapter> adapter); - - // A BluetoothAdapter instance representing an adapter of the system. - scoped_refptr<BluetoothAdapter> adapter_; - - // We keep a list of all observers so that when the adapter gets swapped, - // we can remove all observers from the old adapter and add them to the - // new adapter. - std::unordered_set<BluetoothAdapter::Observer*> adapter_observers_; - - // Should only be called on the UI thread. - base::ThreadChecker thread_checker_; - - // Weak pointer factory for generating 'this' pointers that might live longer - // than we do. - // Note: This should remain the last member so it'll be destroyed and - // invalidate its weak pointers before any other members are destroyed. - base::WeakPtrFactory<BluetoothAdapterFactoryWrapper> weak_ptr_factory_{this}; - - DISALLOW_COPY_AND_ASSIGN(BluetoothAdapterFactoryWrapper); -}; - -} // namespace device - -#endif // DEVICE_BLUETOOTH_BLUETOOTH_ADAPTER_FACTORY_WRAPPER_H_ diff --git a/chromium/device/bluetooth/bluetooth_adapter_winrt.cc b/chromium/device/bluetooth/bluetooth_adapter_winrt.cc index a51515dee10..5f2c36eb504 100644 --- a/chromium/device/bluetooth/bluetooth_adapter_winrt.cc +++ b/chromium/device/bluetooth/bluetooth_adapter_winrt.cc @@ -31,7 +31,6 @@ #include "base/task/post_task.h" #include "base/task/task_traits.h" #include "base/task/thread_pool.h" -#include "base/threading/scoped_thread_priority.h" #include "base/threading/thread_task_runner_handle.h" #include "base/win/core_winrt_util.h" #include "base/win/post_async_results.h" @@ -660,7 +659,9 @@ void BluetoothAdapterWinrt::Initialize(base::OnceClosure init_callback) { // Some of the initialization work requires loading libraries and should not // be run on the browser main thread. base::ThreadPool::PostTaskAndReplyWithResult( - FROM_HERE, {base::MayBlock(), base::TaskPriority::BEST_EFFORT}, + FROM_HERE, + {base::MayBlock(), base::TaskPriority::BEST_EFFORT, + base::ThreadPolicy::MUST_USE_FOREGROUND}, base::BindOnce(&BluetoothAdapterWinrt::PerformSlowInitTasks), base::BindOnce(&BluetoothAdapterWinrt::CompleteInitAgile, weak_ptr_factory_.GetWeakPtr(), std::move(init_callback))); @@ -700,10 +701,6 @@ void BluetoothAdapterWinrt::InitForTests( // static BluetoothAdapterWinrt::StaticsInterfaces BluetoothAdapterWinrt::PerformSlowInitTasks() { - // Mitigate the issues caused by loading DLLs on a background thread - // (http://crbug/973868). - SCOPED_MAY_LOAD_LIBRARY_AT_BACKGROUND_PRIORITY(); - if (!ResolveCoreWinRT()) return BluetoothAdapterWinrt::StaticsInterfaces(); diff --git a/chromium/device/bluetooth/bluetooth_classic_device_mac.h b/chromium/device/bluetooth/bluetooth_classic_device_mac.h index b7ed051bac5..68fab556112 100644 --- a/chromium/device/bluetooth/bluetooth_classic_device_mac.h +++ b/chromium/device/bluetooth/bluetooth_classic_device_mac.h @@ -33,6 +33,7 @@ class BluetoothClassicDeviceMac : public BluetoothDeviceMac { // BluetoothDevice override uint32_t GetBluetoothClass() const override; std::string GetAddress() const override; + AddressType GetAddressType() const override; VendorIDSource GetVendorIDSource() const override; uint16_t GetVendorID() const override; uint16_t GetProductID() const override; diff --git a/chromium/device/bluetooth/bluetooth_classic_device_mac.mm b/chromium/device/bluetooth/bluetooth_classic_device_mac.mm index 839aca94bc9..43bac24c872 100644 --- a/chromium/device/bluetooth/bluetooth_classic_device_mac.mm +++ b/chromium/device/bluetooth/bluetooth_classic_device_mac.mm @@ -85,6 +85,11 @@ std::string BluetoothClassicDeviceMac::GetAddress() const { return GetDeviceAddress(device_); } +BluetoothDevice::AddressType BluetoothClassicDeviceMac::GetAddressType() const { + NOTIMPLEMENTED(); + return ADDR_TYPE_UNKNOWN; +} + BluetoothDevice::VendorIDSource BluetoothClassicDeviceMac::GetVendorIDSource() const { return VENDOR_ID_UNKNOWN; diff --git a/chromium/device/bluetooth/bluetooth_device.h b/chromium/device/bluetooth/bluetooth_device.h index 643cad1fcfd..31aa41b3d95 100644 --- a/chromium/device/bluetooth/bluetooth_device.h +++ b/chromium/device/bluetooth/bluetooth_device.h @@ -60,6 +60,13 @@ class DEVICE_BLUETOOTH_EXPORT BluetoothDevice { VENDOR_ID_MAX_VALUE = VENDOR_ID_USB }; + // Possible values that may be returned by GetAddressType(). + enum AddressType { + ADDR_TYPE_UNKNOWN, + ADDR_TYPE_PUBLIC, + ADDR_TYPE_RANDOM, + }; + // The value returned if the RSSI or transmit power cannot be read. static const int kUnknownPower = 127; // The value returned if the appearance is not present. @@ -219,6 +226,10 @@ class DEVICE_BLUETOOTH_EXPORT BluetoothDevice { // a unique key to identify the device and copied where needed. virtual std::string GetAddress() const = 0; + // Returns the Bluetooth address type of the device. Currently available on + // Linux and Chrome OS. + virtual AddressType GetAddressType() const = 0; + // Returns the allocation source of the identifier returned by GetVendorID(), // where available, or VENDOR_ID_UNKNOWN where not. virtual VendorIDSource GetVendorIDSource() const = 0; diff --git a/chromium/device/bluetooth/bluetooth_device_android.cc b/chromium/device/bluetooth/bluetooth_device_android.cc index 9e25f6a0df4..27347ad06bb 100644 --- a/chromium/device/bluetooth/bluetooth_device_android.cc +++ b/chromium/device/bluetooth/bluetooth_device_android.cc @@ -67,6 +67,11 @@ std::string BluetoothDeviceAndroid::GetAddress() const { Java_ChromeBluetoothDevice_getAddress(AttachCurrentThread(), j_device_)); } +BluetoothDevice::AddressType BluetoothDeviceAndroid::GetAddressType() const { + NOTIMPLEMENTED(); + return ADDR_TYPE_UNKNOWN; +} + BluetoothDevice::VendorIDSource BluetoothDeviceAndroid::GetVendorIDSource() const { // Android API does not provide Vendor ID. diff --git a/chromium/device/bluetooth/bluetooth_device_android.h b/chromium/device/bluetooth/bluetooth_device_android.h index bfeff535c83..d6dc20f990a 100644 --- a/chromium/device/bluetooth/bluetooth_device_android.h +++ b/chromium/device/bluetooth/bluetooth_device_android.h @@ -50,6 +50,7 @@ class DEVICE_BLUETOOTH_EXPORT BluetoothDeviceAndroid final // BluetoothDevice: uint32_t GetBluetoothClass() const override; std::string GetAddress() const override; + AddressType GetAddressType() const override; VendorIDSource GetVendorIDSource() const override; uint16_t GetVendorID() const override; uint16_t GetProductID() const override; diff --git a/chromium/device/bluetooth/bluetooth_device_win.cc b/chromium/device/bluetooth/bluetooth_device_win.cc index 82b7e0f9ec0..17bcbabebe4 100644 --- a/chromium/device/bluetooth/bluetooth_device_win.cc +++ b/chromium/device/bluetooth/bluetooth_device_win.cc @@ -63,6 +63,11 @@ std::string BluetoothDeviceWin::GetAddress() const { return address_; } +BluetoothDevice::AddressType BluetoothDeviceWin::GetAddressType() const { + NOTIMPLEMENTED(); + return ADDR_TYPE_UNKNOWN; +} + BluetoothDevice::VendorIDSource BluetoothDeviceWin::GetVendorIDSource() const { return VENDOR_ID_UNKNOWN; diff --git a/chromium/device/bluetooth/bluetooth_device_win.h b/chromium/device/bluetooth/bluetooth_device_win.h index e55dbde0120..966925107c9 100644 --- a/chromium/device/bluetooth/bluetooth_device_win.h +++ b/chromium/device/bluetooth/bluetooth_device_win.h @@ -42,6 +42,7 @@ class DEVICE_BLUETOOTH_EXPORT BluetoothDeviceWin // BluetoothDevice override uint32_t GetBluetoothClass() const override; std::string GetAddress() const override; + AddressType GetAddressType() const override; VendorIDSource GetVendorIDSource() const override; uint16_t GetVendorID() const override; uint16_t GetProductID() const override; diff --git a/chromium/device/bluetooth/bluetooth_device_winrt.cc b/chromium/device/bluetooth/bluetooth_device_winrt.cc index 482c9f157e7..6ba09a2da58 100644 --- a/chromium/device/bluetooth/bluetooth_device_winrt.cc +++ b/chromium/device/bluetooth/bluetooth_device_winrt.cc @@ -208,6 +208,11 @@ std::string BluetoothDeviceWinrt::GetAddress() const { return address_; } +BluetoothDevice::AddressType BluetoothDeviceWinrt::GetAddressType() const { + NOTIMPLEMENTED(); + return ADDR_TYPE_UNKNOWN; +} + BluetoothDevice::VendorIDSource BluetoothDeviceWinrt::GetVendorIDSource() const { NOTIMPLEMENTED(); diff --git a/chromium/device/bluetooth/bluetooth_device_winrt.h b/chromium/device/bluetooth/bluetooth_device_winrt.h index 0062316191a..84cdbf9a15f 100644 --- a/chromium/device/bluetooth/bluetooth_device_winrt.h +++ b/chromium/device/bluetooth/bluetooth_device_winrt.h @@ -48,6 +48,7 @@ class DEVICE_BLUETOOTH_EXPORT BluetoothDeviceWinrt : public BluetoothDevice { // BluetoothDevice: uint32_t GetBluetoothClass() const override; std::string GetAddress() const override; + AddressType GetAddressType() const override; VendorIDSource GetVendorIDSource() const override; uint16_t GetVendorID() const override; uint16_t GetProductID() const override; diff --git a/chromium/device/bluetooth/bluetooth_low_energy_device_mac.h b/chromium/device/bluetooth/bluetooth_low_energy_device_mac.h index 83df27bc2fa..7adb0719af4 100644 --- a/chromium/device/bluetooth/bluetooth_low_energy_device_mac.h +++ b/chromium/device/bluetooth/bluetooth_low_energy_device_mac.h @@ -42,6 +42,7 @@ class DEVICE_BLUETOOTH_EXPORT BluetoothLowEnergyDeviceMac std::string GetIdentifier() const override; uint32_t GetBluetoothClass() const override; std::string GetAddress() const override; + AddressType GetAddressType() const override; BluetoothDevice::VendorIDSource GetVendorIDSource() const override; uint16_t GetVendorID() const override; uint16_t GetProductID() const override; diff --git a/chromium/device/bluetooth/bluetooth_low_energy_device_mac.mm b/chromium/device/bluetooth/bluetooth_low_energy_device_mac.mm index df268cfbc42..95c6888cac4 100644 --- a/chromium/device/bluetooth/bluetooth_low_energy_device_mac.mm +++ b/chromium/device/bluetooth/bluetooth_low_energy_device_mac.mm @@ -72,6 +72,12 @@ std::string BluetoothLowEnergyDeviceMac::GetAddress() const { return hash_address_; } +BluetoothDevice::AddressType BluetoothLowEnergyDeviceMac::GetAddressType() + const { + NOTIMPLEMENTED(); + return ADDR_TYPE_UNKNOWN; +} + BluetoothDevice::VendorIDSource BluetoothLowEnergyDeviceMac::GetVendorIDSource() const { return VENDOR_ID_UNKNOWN; diff --git a/chromium/device/bluetooth/bluetooth_socket_mac.mm b/chromium/device/bluetooth/bluetooth_socket_mac.mm index 8bf819d4c1d..7a73cf871f1 100644 --- a/chromium/device/bluetooth/bluetooth_socket_mac.mm +++ b/chromium/device/bluetooth/bluetooth_socket_mac.mm @@ -20,6 +20,7 @@ #include "base/mac/scoped_cftyperef.h" #include "base/memory/ref_counted.h" #include "base/numerics/safe_conversions.h" +#include "base/optional.h" #include "base/strings/string_number_conversions.h" #include "base/strings/stringprintf.h" #include "base/strings/sys_string_conversions.h" @@ -245,7 +246,7 @@ NSString* IntToNSString(int integer) { // corresponding to the provided |uuid|, |name|, and |protocol_definition|. Does // not include a service name in the definition if |name| is null. NSDictionary* BuildServiceDefinition(const BluetoothUUID& uuid, - const std::string* name, + const base::Optional<std::string>& name, NSArray* protocol_definition) { NSMutableDictionary* service_definition = [NSMutableDictionary dictionary]; @@ -290,8 +291,7 @@ NSDictionary* BuildRfcommServiceDefinition( }, ], ]; - return BuildServiceDefinition( - uuid, options.name.get(), rfcomm_protocol_definition); + return BuildServiceDefinition(uuid, options.name, rfcomm_protocol_definition); } // Returns a dictionary containing the Bluetooth L2CAP service definition @@ -310,8 +310,7 @@ NSDictionary* BuildL2capServiceDefinition( }, ], ]; - return BuildServiceDefinition( - uuid, options.name.get(), l2cap_protocol_definition); + return BuildServiceDefinition(uuid, options.name, l2cap_protocol_definition); } // Registers a Bluetooth service with the specified |service_definition| in the @@ -340,7 +339,7 @@ IOBluetoothSDPServiceRecord* RegisterService( // Returns true iff the |requested_channel_id| was registered in the RFCOMM // |service_record|. If it was, also updates |registered_channel_id| with the // registered value, as the requested id may have been left unspecified. -bool VerifyRfcommService(const int* requested_channel_id, +bool VerifyRfcommService(const base::Optional<int>& requested_channel_id, BluetoothRFCOMMChannelID* registered_channel_id, IOBluetoothSDPServiceRecord* service_record) { // Test whether the requested channel id was available. @@ -357,9 +356,9 @@ bool VerifyRfcommService(const int* requested_channel_id, return true; } -// Registers an RFCOMM service with the specified |uuid|, |options.channel_id|, +// Registers an RFCOMM service with the specified |uuid|, |options.channel|, // and |options.name| in the system SDP server. Automatically allocates a -// channel if |options.channel_id| is null. Does not specify a name if +// channel if |options.channel| is null. Does not specify a name if // |options.name| is null. Returns a handle to the registered service and // updates |registered_channel_id| to the actual channel id, or returns nil if // the service could not be registered. @@ -369,14 +368,14 @@ IOBluetoothSDPServiceRecord* RegisterRfcommService( BluetoothRFCOMMChannelID* registered_channel_id) { return RegisterService( BuildRfcommServiceDefinition(uuid, options), - base::BindOnce(&VerifyRfcommService, options.channel.get(), + base::BindOnce(&VerifyRfcommService, options.channel, registered_channel_id)); } // Returns true iff the |requested_psm| was registered in the L2CAP // |service_record|. If it was, also updates |registered_psm| with the // registered value, as the requested PSM may have been left unspecified. -bool VerifyL2capService(const int* requested_psm, +bool VerifyL2capService(const base::Optional<int>& requested_psm, BluetoothL2CAPPSM* registered_psm, IOBluetoothSDPServiceRecord* service_record) { // Test whether the requested PSM was available. @@ -404,7 +403,7 @@ IOBluetoothSDPServiceRecord* RegisterL2capService( BluetoothL2CAPPSM* registered_psm) { return RegisterService( BuildL2capServiceDefinition(uuid, options), - base::BindOnce(&VerifyL2capService, options.psm.get(), registered_psm)); + base::BindOnce(&VerifyL2capService, options.psm, registered_psm)); } } // namespace diff --git a/chromium/device/bluetooth/bluetooth_socket_net.cc b/chromium/device/bluetooth/bluetooth_socket_net.cc index 7c95d63b491..7408e62fde2 100644 --- a/chromium/device/bluetooth/bluetooth_socket_net.cc +++ b/chromium/device/bluetooth/bluetooth_socket_net.cc @@ -238,14 +238,43 @@ void BluetoothSocketNet::SendFrontWriteRequest() { if (!tcp_socket_) return; + if (pending_write_request_) { + // It is possible to enter this function while a write request is + // currently pending if the following sequence happens: + // + // 1) A single pending write is running and it is the last one in the queue. + // 2) A Send() call queues a DoSend() call on the sequence. + // 3) The pending write completes, queue length is zero, a call to + // SendFrontWriteRequest is queued on the sequence. + // 4) DoSend() runs on the sequence, queues a write request, and runs + // SendFrontWriteRequest() inline because the queue size is 1. + // 5) The immediate call for SendFrontWriteRequest() starts a write request + // and exits. + // 6) The next SendFrontWriteRequest() which was queued in step 3 now runs + // while the write request from 5 is still pending. + // + // At this point we have entered SendFrontWriteRequest() while we are + // waiting for a pending write. Previously the code did not handle this + // situation and would attempt to process the write request at the front + // of the queue twice which is both wrong from a data perspective and also + // triggers a CHECK in the Socket. + // + // The fix is to ensure we only process a new write request when there are + // no pending requests, so we exit early here and let OnSocketWriteComplete + // queue the next SendFrontWriteRequest. + return; + } + if (write_queue_.size() == 0) return; - WriteRequest* request = write_queue_.front().get(); + pending_write_request_ = std::move(write_queue_.front()); + write_queue_.pop(); + auto copyable_callback = base::AdaptCallbackForRepeating( base::BindOnce(&BluetoothSocketNet::OnSocketWriteComplete, this, - std::move(request->success_callback), - std::move(request->error_callback))); + std::move(pending_write_request_->success_callback), + std::move(pending_write_request_->error_callback))); net::NetworkTrafficAnnotationTag traffic_annotation = net::DefineNetworkTrafficAnnotation("bluetooth_socket", R"( semantics { @@ -270,9 +299,9 @@ void BluetoothSocketNet::SendFrontWriteRequest() { "DeviceAllowBluetooth policy can disable Bluetooth for ChromeOS, " "not implemented for other platforms." })"); - int send_result = - tcp_socket_->Write(request->buffer.get(), request->buffer_size, - copyable_callback, traffic_annotation); + int send_result = tcp_socket_->Write(pending_write_request_->buffer.get(), + pending_write_request_->buffer_size, + copyable_callback, traffic_annotation); // Write() will not have run |copyable_callback| if there is no pending I/O. if (send_result != net::ERR_IO_PENDING) copyable_callback.Run(send_result); @@ -284,7 +313,7 @@ void BluetoothSocketNet::OnSocketWriteComplete( int send_result) { DCHECK(socket_thread_->task_runner()->RunsTasksInCurrentSequence()); - write_queue_.pop(); + pending_write_request_.reset(); if (send_result >= net::OK) { std::move(success_callback).Run(send_result); diff --git a/chromium/device/bluetooth/bluetooth_socket_net.h b/chromium/device/bluetooth/bluetooth_socket_net.h index 9c7459df616..45c070af327 100644 --- a/chromium/device/bluetooth/bluetooth_socket_net.h +++ b/chromium/device/bluetooth/bluetooth_socket_net.h @@ -109,6 +109,7 @@ class BluetoothSocketNet : public BluetoothSocket { std::unique_ptr<net::TCPSocket> tcp_socket_; scoped_refptr<net::IOBufferWithSize> read_buffer_; base::queue<std::unique_ptr<WriteRequest>> write_queue_; + std::unique_ptr<WriteRequest> pending_write_request_; DISALLOW_COPY_AND_ASSIGN(BluetoothSocketNet); }; diff --git a/chromium/device/bluetooth/bluez/bluetooth_bluez_unittest.cc b/chromium/device/bluetooth/bluez/bluetooth_bluez_unittest.cc index 8d954537d90..2896da6ccd5 100644 --- a/chromium/device/bluetooth/bluez/bluetooth_bluez_unittest.cc +++ b/chromium/device/bluetooth/bluez/bluetooth_bluez_unittest.cc @@ -1804,6 +1804,33 @@ TEST_F(BluetoothBlueZTest, DeviceProperties) { EXPECT_EQ(0x0306, devices[idx]->GetDeviceID()); } +TEST_F(BluetoothBlueZTest, DeviceAddressType) { + GetAdapter(); + BluetoothAdapter::DeviceList devices = adapter_->GetDevices(); + ASSERT_EQ(2U, devices.size()); + + int idx = GetDeviceIndexByAddress( + devices, bluez::FakeBluetoothDeviceClient::kPairedDeviceAddress); + ASSERT_NE(-1, idx); + ASSERT_EQ(bluez::FakeBluetoothDeviceClient::kPairedDeviceAddress, + devices[idx]->GetAddress()); + + bluez::FakeBluetoothDeviceClient::Properties* properties = + fake_bluetooth_device_client_->GetProperties(dbus::ObjectPath( + bluez::FakeBluetoothDeviceClient::kPairedDevicePath)); + + properties->address_type.set_valid(false); + EXPECT_EQ(BluetoothDevice::ADDR_TYPE_UNKNOWN, devices[idx]->GetAddressType()); + + properties->address_type.set_valid(true); + + properties->address_type.ReplaceValue(bluetooth_device::kAddressTypePublic); + EXPECT_EQ(BluetoothDevice::ADDR_TYPE_PUBLIC, devices[idx]->GetAddressType()); + + properties->address_type.ReplaceValue(bluetooth_device::kAddressTypeRandom); + EXPECT_EQ(BluetoothDevice::ADDR_TYPE_RANDOM, devices[idx]->GetAddressType()); +} + TEST_F(BluetoothBlueZTest, DeviceClassChanged) { // Simulate a change of class of a device, as sometimes occurs // during discovery. diff --git a/chromium/device/bluetooth/bluez/bluetooth_device_bluez.cc b/chromium/device/bluetooth/bluez/bluetooth_device_bluez.cc index 0bf76e693c2..1cf95efcd91 100644 --- a/chromium/device/bluetooth/bluez/bluetooth_device_bluez.cc +++ b/chromium/device/bluetooth/bluez/bluetooth_device_bluez.cc @@ -302,6 +302,24 @@ std::string BluetoothDeviceBlueZ::GetAddress() const { return device::CanonicalizeBluetoothAddress(properties->address.value()); } +BluetoothDeviceBlueZ::AddressType BluetoothDeviceBlueZ::GetAddressType() const { + bluez::BluetoothDeviceClient::Properties* properties = + bluez::BluezDBusManager::Get()->GetBluetoothDeviceClient()->GetProperties( + object_path_); + DCHECK(properties); + + if (!properties->address_type.is_valid()) + return ADDR_TYPE_UNKNOWN; + + if (properties->address_type.value() == bluetooth_device::kAddressTypePublic) + return ADDR_TYPE_PUBLIC; + if (properties->address_type.value() == bluetooth_device::kAddressTypeRandom) + return ADDR_TYPE_RANDOM; + + LOG(WARNING) << "Unknown address type: " << properties->address_type.value(); + return ADDR_TYPE_UNKNOWN; +} + BluetoothDevice::VendorIDSource BluetoothDeviceBlueZ::GetVendorIDSource() const { VendorIDSource vendor_id_source = VENDOR_ID_UNKNOWN; diff --git a/chromium/device/bluetooth/bluez/bluetooth_device_bluez.h b/chromium/device/bluetooth/bluez/bluetooth_device_bluez.h index a0fe2f2ac04..500cb5d7c67 100644 --- a/chromium/device/bluetooth/bluez/bluetooth_device_bluez.h +++ b/chromium/device/bluetooth/bluez/bluetooth_device_bluez.h @@ -56,6 +56,7 @@ class DEVICE_BLUETOOTH_EXPORT BluetoothDeviceBlueZ uint32_t GetBluetoothClass() const override; device::BluetoothTransport GetType() const override; std::string GetAddress() const override; + AddressType GetAddressType() const override; VendorIDSource GetVendorIDSource() const override; uint16_t GetVendorID() const override; uint16_t GetProductID() const override; diff --git a/chromium/device/bluetooth/cast/bluetooth_device_cast.cc b/chromium/device/bluetooth/cast/bluetooth_device_cast.cc index fc92253f050..58f816b8c5f 100644 --- a/chromium/device/bluetooth/cast/bluetooth_device_cast.cc +++ b/chromium/device/bluetooth/cast/bluetooth_device_cast.cc @@ -84,6 +84,11 @@ std::string BluetoothDeviceCast::GetAddress() const { return address_; } +BluetoothDevice::AddressType BluetoothDeviceCast::GetAddressType() const { + NOTIMPLEMENTED(); + return ADDR_TYPE_UNKNOWN; +} + BluetoothDevice::VendorIDSource BluetoothDeviceCast::GetVendorIDSource() const { return VENDOR_ID_UNKNOWN; } @@ -356,11 +361,15 @@ void BluetoothDeviceCast::DisconnectGatt() { // The device is intentionally not disconnected. } -void BluetoothDeviceCast::OnConnect(bool success) { +void BluetoothDeviceCast::OnConnect( + chromecast::bluetooth::RemoteDevice::ConnectStatus status) { + bool success = + (status == chromecast::bluetooth::RemoteDevice::ConnectStatus::kSuccess); DVLOG(2) << __func__ << " success:" << success; pending_connect_ = false; - if (!success) + if (!success) { DidFailToConnectGatt(ERROR_FAILED); + } } } // namespace device diff --git a/chromium/device/bluetooth/cast/bluetooth_device_cast.h b/chromium/device/bluetooth/cast/bluetooth_device_cast.h index df4bfdddd96..479a81dec00 100644 --- a/chromium/device/bluetooth/cast/bluetooth_device_cast.h +++ b/chromium/device/bluetooth/cast/bluetooth_device_cast.h @@ -40,6 +40,7 @@ class BluetoothDeviceCast : public BluetoothDevice { uint32_t GetBluetoothClass() const override; BluetoothTransport GetType() const override; std::string GetAddress() const override; + AddressType GetAddressType() const override; VendorIDSource GetVendorIDSource() const override; uint16_t GetVendorID() const override; uint16_t GetProductID() const override; @@ -120,7 +121,7 @@ class BluetoothDeviceCast : public BluetoothDevice { void DisconnectGatt() override; // Called back from connect requests generated from CreateGattConnectionImpl. - void OnConnect(bool success); + void OnConnect(chromecast::bluetooth::RemoteDevice::ConnectStatus status); // Called in response to GetServices void OnGetServices( diff --git a/chromium/device/bluetooth/dbus/bluetooth_device_client.cc b/chromium/device/bluetooth/dbus/bluetooth_device_client.cc index 2f71e14259a..ae2f4f74408 100644 --- a/chromium/device/bluetooth/dbus/bluetooth_device_client.cc +++ b/chromium/device/bluetooth/dbus/bluetooth_device_client.cc @@ -185,6 +185,7 @@ BluetoothDeviceClient::Properties::Properties( const PropertyChangedCallback& callback) : dbus::PropertySet(object_proxy, interface_name, callback) { RegisterProperty(bluetooth_device::kAddressProperty, &address); + RegisterProperty(bluetooth_device::kAddressTypeProperty, &address_type); RegisterProperty(bluetooth_device::kNameProperty, &name); RegisterProperty(bluetooth_device::kIconProperty, &icon); RegisterProperty(bluetooth_device::kClassProperty, &bluetooth_class); @@ -638,6 +639,7 @@ class BluetoothDeviceClientImpl : public BluetoothDeviceClient, dbus::MessageReader reader(response); if (!ReadRecordsFromMessage(&reader, &records)) { std::move(callback).Run(ServiceRecordList()); + return; } std::move(callback).Run(records); diff --git a/chromium/device/bluetooth/dbus/bluetooth_device_client.h b/chromium/device/bluetooth/dbus/bluetooth_device_client.h index a6339b17c51..866fdf100bc 100644 --- a/chromium/device/bluetooth/dbus/bluetooth_device_client.h +++ b/chromium/device/bluetooth/dbus/bluetooth_device_client.h @@ -51,6 +51,9 @@ class DEVICE_BLUETOOTH_EXPORT BluetoothDeviceClient : public BluezDBusClient { // The Bluetooth device address of the device. Read-only. dbus::Property<std::string> address; + // The Bluetooth address type of the device. Read-only. + dbus::Property<std::string> address_type; + // The Bluetooth friendly name of the device. Read-only, to give a // different local name, use the |alias| property. dbus::Property<std::string> name; diff --git a/chromium/device/bluetooth/dbus/bluetooth_le_advertisement_service_provider.cc b/chromium/device/bluetooth/dbus/bluetooth_le_advertisement_service_provider.cc index ce85f4347d1..b36b4f16de4 100644 --- a/chromium/device/bluetooth/dbus/bluetooth_le_advertisement_service_provider.cc +++ b/chromium/device/bluetooth/dbus/bluetooth_le_advertisement_service_provider.cc @@ -172,6 +172,7 @@ class BluetoothAdvertisementServiceProviderImpl method_call, kErrorInvalidArgs, "No such property: '" + property_name + "'."); std::move(response_sender).Run(std::move(error_response)); + return; } writer.CloseContainer(&variant_writer); diff --git a/chromium/device/bluetooth/device.cc b/chromium/device/bluetooth/device.cc index fd3df58d5de..c9ce478ad91 100644 --- a/chromium/device/bluetooth/device.cc +++ b/chromium/device/bluetooth/device.cc @@ -45,6 +45,9 @@ mojom::DeviceInfoPtr Device::ConstructDeviceInfoStruct( device_info->rssi->value = device->GetInquiryRSSI().value(); } + for (auto const& it : device->GetServiceData()) + device_info->service_data_map.insert_or_assign(it.first, it.second); + return device_info; } diff --git a/chromium/device/bluetooth/public/mojom/adapter.mojom b/chromium/device/bluetooth/public/mojom/adapter.mojom index c21d2901aff..5d7fc04a77a 100644 --- a/chromium/device/bluetooth/public/mojom/adapter.mojom +++ b/chromium/device/bluetooth/public/mojom/adapter.mojom @@ -49,6 +49,22 @@ struct AdapterInfo { bool discovering; }; +// Represents an ongoing BLE advertisement. Releasing it will release the +// underlying object and stop the advertisement, but callers should prefer to +// let a call to Unregister() to finish first. +// Note: Methods which are declared [Sync] are for use by +// //chrome/services/sharing/nearby; all other usage of their synchronous +// signatures is strongly discouraged. +interface Advertisement { + // Use to gracefully stop advertising before destroying the message pipe. The + // reply callback can be used to synchronize an attempt to re-register an + // advertisement; attempting to register an advertisement without first + // releasing limited advertisement slots in the hardware may fail with a + // busy error. + [Sync] + Unregister() => (); +}; + // Represents a request to discover nearby devices. // Note: Methods which are declared [Sync] are for use by // //chrome/services/sharing/nearby; all other usage of their synchronous @@ -124,9 +140,25 @@ interface Adapter { [Sync] GetInfo() => (AdapterInfo info); - // Sets the client that listens for the adapter's events. + // Adds an observer that listens for the adapter's events. + [Sync] + AddObserver(pending_remote<AdapterObserver> observer) => (); + + // Requests the adapter to broadcast a BLE advertisement on |service_id| with + // the associated packet |service_data|. Returns null if advertisement is not + // registered successfully. + // Important notes: + // * This method registers a "non-connectable" advertisement. Any future + // effort to allow this API to support "connectable" advertisements would + // also require the addition of an API to support hosting a GATT server. + // * Bluetooth chips generally can only broadcast a few advertisements, + // sometimes even only one, simultaneously. This can be mitigated by + // operating systems "rotating" advertisements in the higher software layer, + // as Chrome OS does. Non-Chrome OS clients of this API are responsible for + // understanding their host OS's and/or hardware's limitations. [Sync] - SetClient(pending_remote<AdapterClient> client) => (); + RegisterAdvertisement(UUID service_id, array<uint8> service_data) => + (pending_remote<Advertisement>? advertisement); // Requests the local device to make itself discoverable to nearby remote // devices. @@ -164,7 +196,8 @@ interface Adapter { => (pending_remote<ServerSocket>? server_socket); }; -interface AdapterClient { +// Listener on Bluetooth events. Register as an observer via AddObserver(). +interface AdapterObserver { // Called when the presence of the adapter changes. PresentChanged(bool present); diff --git a/chromium/device/bluetooth/public/mojom/device.mojom b/chromium/device/bluetooth/public/mojom/device.mojom index a5b89173c37..dcf5b3a18ef 100644 --- a/chromium/device/bluetooth/public/mojom/device.mojom +++ b/chromium/device/bluetooth/public/mojom/device.mojom @@ -6,6 +6,13 @@ module bluetooth.mojom; import "device/bluetooth/public/mojom/uuid.mojom"; +// Important note: the byte arrays which can be accessed from this interface +// (including "service data", "characteristics", and "descriptors") are +// arbitrary binary blobs of data provided by a likely untrustworthy device. +// Clients are responsible for safely parsing this information; please see +// "The Rule of 2" (//docs/security/rule-of-2.md). C++ clients must parse these +// blobs in a sandboxed process. + // Values representing the possible properties of a characteristic, which // define how the characteristic can be used. Each of these properties serve // a role as defined in the Bluetooth Specification. @@ -60,6 +67,10 @@ struct DeviceInfo { string address; bool is_gatt_connected; RSSIWrapper? rssi; + + // Important: the blobs associated with each UUID are arbitrary and untrusted. + // Please refer to the note on "The Rule of 2" at the top of this file. + map<UUID, array<uint8>> service_data_map; }; struct ServiceInfo { @@ -72,12 +83,18 @@ struct CharacteristicInfo { string id; UUID uuid; uint32 properties; + + // Important: this blob is arbitrary and untrusted. Please refer to the note + // on "The Rule of 2" at the top of this file. array<uint8> last_known_value; }; struct DescriptorInfo { string id; UUID uuid; + + // Important: this blob is arbitrary and untrusted. Please refer to the note + // on "The Rule of 2" at the top of this file. array<uint8> last_known_value; }; @@ -120,12 +137,16 @@ interface Device { // Reads the value for the GATT Descriptor with |descriptor_id| in the GATT // Characteristic with |characteristic_id| in the GATT Service with // |service_id|. + // Important: the returned |value| blob is arbitrary and untrusted. Please + // refer to the note on "The Rule of 2" at the top of this file. ReadValueForDescriptor(string service_id, string characteristic_id, string descriptor_id) => (GattResult result, array<uint8>? value); // Writes the |value| for the GATT Descriptor with |descriptor_id| in the GATT // Characteristic with |characteristic_id| in the GATT Service with // |service_id|. + // Important: the returned |value| blob is arbitrary and untrusted. Please + // refer to the note on "The Rule of 2" at the top of this file. WriteValueForDescriptor(string service_id, string characteristic_id, string descriptor_id, array<uint8> value) => (GattResult result); }; diff --git a/chromium/device/fido/BUILD.gn b/chromium/device/fido/BUILD.gn index a5a5d64b764..ec3c52825ac 100644 --- a/chromium/device/fido/BUILD.gn +++ b/chromium/device/fido/BUILD.gn @@ -16,8 +16,11 @@ component("fido") { "cable/cable_discovery_data.h", "cable/noise.cc", "cable/noise.h", + "cable/v2_constants.h", "cable/v2_handshake.cc", "cable/v2_handshake.h", + "cable/websocket_adapter.cc", + "cable/websocket_adapter.h", "cbor_extract.cc", "ed25519_public_key.cc", "ed25519_public_key.h", @@ -113,8 +116,8 @@ component("fido") { "cable/fido_cable_handshake_handler.h", "cable/fido_tunnel_device.cc", "cable/fido_tunnel_device.h", - "cable/websocket_adapter.cc", - "cable/websocket_adapter.h", + "cable/v2_discovery.cc", + "cable/v2_discovery.h", "client_data.cc", "client_data.h", "credential_management.cc", @@ -160,6 +163,8 @@ component("fido") { "hid/fido_hid_message.h", "hid/fido_hid_packet.cc", "hid/fido_hid_packet.h", + "large_blob.cc", + "large_blob.h", "make_credential_request_handler.cc", "make_credential_request_handler.h", "make_credential_task.cc", @@ -261,6 +266,50 @@ component("fido") { } } +static_library("cablev2_registration") { + sources = [ + "cable/v2_registration.cc", + "cable/v2_registration.h", + ] + deps = [ + ":fido", + "//base", + "//components/cbor", + "//components/device_event_log", + "//components/gcm_driver", + "//components/gcm_driver/instance_id", + ] +} + +static_library("cablev2_authenticator") { + sources = [ + "cable/v2_authenticator.cc", + "cable/v2_authenticator.h", + ] + deps = [ + ":fido", + "//components/cbor", + "//components/device_event_log", + "//services/network/public/mojom", + ] +} + +static_library("cablev2_test_util") { + testonly = true + sources = [ + "cable/v2_test_util.cc", + "cable/v2_test_util.h", + ] + deps = [ + ":cablev2_authenticator", + ":fido", + "//components/cbor", + "//crypto", + "//services/network:test_support", + "//services/network/public/mojom", + ] +} + if (is_chromeos) { proto_library("u2f_proto") { sources = [ "//third_party/cros_system_api/dbus/u2f/u2f_interface.proto" ] diff --git a/chromium/device/fido/DEPS b/chromium/device/fido/DEPS index 690e920335a..abccfd8760a 100644 --- a/chromium/device/fido/DEPS +++ b/chromium/device/fido/DEPS @@ -1,10 +1,12 @@ include_rules = [ "+components/apdu", "+components/cbor", + "+components/gcm_driver", "+crypto", "+dbus", "+net/base", "+net/cert", + "+net/cookies", "+net/traffic_annotation", "+services/network", "+third_party/boringssl/src/include", diff --git a/chromium/device/fido/aoa/android_accessory_discovery.cc b/chromium/device/fido/aoa/android_accessory_discovery.cc index fec676d6941..d55fcd4154d 100644 --- a/chromium/device/fido/aoa/android_accessory_discovery.cc +++ b/chromium/device/fido/aoa/android_accessory_discovery.cc @@ -183,6 +183,7 @@ void AndroidAccessoryDiscovery::OnOpenAccessory( base::BindOnce(&AndroidAccessoryDiscovery::OnAccessoryConfigured, weak_factory_.GetWeakPtr(), std::move(device), interface_info)); + return; } OnAccessoryConfigured(std::move(device), interface_info, /*success=*/true); diff --git a/chromium/device/fido/authenticator_get_assertion_response.cc b/chromium/device/fido/authenticator_get_assertion_response.cc index f1c7b3ed829..c012d29acdd 100644 --- a/chromium/device/fido/authenticator_get_assertion_response.cc +++ b/chromium/device/fido/authenticator_get_assertion_response.cc @@ -97,6 +97,11 @@ AuthenticatorGetAssertionResponse::SetNumCredentials(uint8_t num_credentials) { return *this; } +void AuthenticatorGetAssertionResponse::set_large_blob_key( + const base::span<const uint8_t, kLargeBlobKeyLength> large_blob_key) { + large_blob_key_ = fido_parsing_utils::Materialize(large_blob_key); +} + base::Optional<base::span<const uint8_t>> AuthenticatorGetAssertionResponse::hmac_secret() const { if (hmac_secret_) { diff --git a/chromium/device/fido/authenticator_get_assertion_response.h b/chromium/device/fido/authenticator_get_assertion_response.h index 9cb3740c59d..1e2ddd7c1ba 100644 --- a/chromium/device/fido/authenticator_get_assertion_response.h +++ b/chromium/device/fido/authenticator_get_assertion_response.h @@ -67,6 +67,13 @@ class COMPONENT_EXPORT(DEVICE_FIDO) AuthenticatorGetAssertionResponse android_client_data_ext_ = data; } + base::Optional<std::array<uint8_t, kLargeBlobKeyLength>> large_blob_key() + const { + return large_blob_key_; + } + void set_large_blob_key( + const base::span<const uint8_t, kLargeBlobKeyLength> large_blob_key); + // hmac_secret contains the output of the hmac_secret extension. base::Optional<base::span<const uint8_t>> hmac_secret() const; void set_hmac_secret(std::vector<uint8_t>); @@ -92,6 +99,11 @@ class COMPONENT_EXPORT(DEVICE_FIDO) AuthenticatorGetAssertionResponse // authenticator output. base::Optional<std::vector<uint8_t>> android_client_data_ext_; + // The large blob key associated to the credential. This value is only + // returned if the assertion request contains the largeBlobKey extension on a + // capable authenticator and the credential has an associated large blob key. + base::Optional<std::array<uint8_t, kLargeBlobKeyLength>> large_blob_key_; + DISALLOW_COPY_AND_ASSIGN(AuthenticatorGetAssertionResponse); }; diff --git a/chromium/device/fido/authenticator_get_info_response.cc b/chromium/device/fido/authenticator_get_info_response.cc index fa5bd5efda1..ad0303ecf94 100644 --- a/chromium/device/fido/authenticator_get_info_response.cc +++ b/chromium/device/fido/authenticator_get_info_response.cc @@ -59,8 +59,6 @@ std::vector<uint8_t> AuthenticatorGetInfoResponse::EncodeToCBOR( case Ctap2Version::kCtap2_1: version_array.emplace_back(kCtap2_1Version); break; - case Ctap2Version::kUnknown: - NOTREACHED(); } } break; diff --git a/chromium/device/fido/authenticator_make_credential_response.cc b/chromium/device/fido/authenticator_make_credential_response.cc index bd3ec120a86..fc6399109f5 100644 --- a/chromium/device/fido/authenticator_make_credential_response.cc +++ b/chromium/device/fido/authenticator_make_credential_response.cc @@ -55,9 +55,11 @@ AuthenticatorMakeCredentialResponse::CreateFromU2fRegisterResponse( if (!fido_attestation_statement) return base::nullopt; - return AuthenticatorMakeCredentialResponse( + AuthenticatorMakeCredentialResponse response( transport_used, AttestationObject(std::move(authenticator_data), std::move(fido_attestation_statement))); + response.is_resident_key = false; + return response; } AuthenticatorMakeCredentialResponse::AuthenticatorMakeCredentialResponse( @@ -70,8 +72,9 @@ AuthenticatorMakeCredentialResponse::AuthenticatorMakeCredentialResponse( AuthenticatorMakeCredentialResponse::AuthenticatorMakeCredentialResponse( AuthenticatorMakeCredentialResponse&& that) = default; -AuthenticatorMakeCredentialResponse& AuthenticatorMakeCredentialResponse:: -operator=(AuthenticatorMakeCredentialResponse&& other) = default; +AuthenticatorMakeCredentialResponse& +AuthenticatorMakeCredentialResponse::operator=( + AuthenticatorMakeCredentialResponse&& other) = default; AuthenticatorMakeCredentialResponse::~AuthenticatorMakeCredentialResponse() = default; @@ -102,6 +105,11 @@ AuthenticatorMakeCredentialResponse::GetRpIdHash() const { return attestation_object_.rp_id_hash(); } +void AuthenticatorMakeCredentialResponse::set_large_blob_key( + const base::span<const uint8_t, kLargeBlobKeyLength> large_blob_key) { + large_blob_key_ = fido_parsing_utils::Materialize(large_blob_key); +} + std::vector<uint8_t> AsCTAPStyleCBORBytes( const AuthenticatorMakeCredentialResponse& response) { const AttestationObject& object = response.attestation_object(); @@ -116,6 +124,9 @@ std::vector<uint8_t> AsCTAPStyleCBORBytes( if (response.enterprise_attestation_returned) { map.emplace(4, true); } + if (response.large_blob_key()) { + map.emplace(5, cbor::Value(*response.large_blob_key())); + } auto encoded_bytes = cbor::Writer::Write(cbor::Value(std::move(map))); DCHECK(encoded_bytes); return std::move(*encoded_bytes); diff --git a/chromium/device/fido/authenticator_make_credential_response.h b/chromium/device/fido/authenticator_make_credential_response.h index 62fe331cc08..86fd2130632 100644 --- a/chromium/device/fido/authenticator_make_credential_response.h +++ b/chromium/device/fido/authenticator_make_credential_response.h @@ -78,12 +78,24 @@ class COMPONENT_EXPORT(DEVICE_FIDO) AuthenticatorMakeCredentialResponse android_client_data_ext_ = data; } + base::Optional<std::array<uint8_t, kLargeBlobKeyLength>> large_blob_key() + const { + return large_blob_key_; + } + void set_large_blob_key( + const base::span<const uint8_t, kLargeBlobKeyLength> large_blob_key); + // enterprise_attestation_returned is true if the authenticator indicated that // it returned an enterprise attestation. Note: U2F authenticators can // support enterprise/individual attestation but cannot indicate when they // have done so, so this will always be false in the U2F case. bool enterprise_attestation_returned = false; + // is_resident_key indicates whether the created credential is client-side + // discoverable. It is nullopt if no discoverable credential was requested, + // but the authenticator may have created one anyway. + base::Optional<bool> is_resident_key; + private: AttestationObject attestation_object_; @@ -95,6 +107,11 @@ class COMPONENT_EXPORT(DEVICE_FIDO) AuthenticatorMakeCredentialResponse // authenticator output. base::Optional<std::vector<uint8_t>> android_client_data_ext_; + // The large blob key associated to the credential. This value is only + // returned if the credential is created with the largeBlobKey extension on a + // capable authenticator. + base::Optional<std::array<uint8_t, kLargeBlobKeyLength>> large_blob_key_; + DISALLOW_COPY_AND_ASSIGN(AuthenticatorMakeCredentialResponse); }; diff --git a/chromium/device/fido/authenticator_selection_criteria.cc b/chromium/device/fido/authenticator_selection_criteria.cc index 50b892adc80..aa23d1cc56a 100644 --- a/chromium/device/fido/authenticator_selection_criteria.cc +++ b/chromium/device/fido/authenticator_selection_criteria.cc @@ -10,10 +10,10 @@ AuthenticatorSelectionCriteria::AuthenticatorSelectionCriteria() = default; AuthenticatorSelectionCriteria::AuthenticatorSelectionCriteria( AuthenticatorAttachment authenticator_attachment, - bool require_resident_key, + ResidentKeyRequirement resident_key, UserVerificationRequirement user_verification_requirement) : authenticator_attachment_(authenticator_attachment), - require_resident_key_(require_resident_key), + resident_key_(resident_key), user_verification_requirement_(user_verification_requirement) {} AuthenticatorSelectionCriteria::AuthenticatorSelectionCriteria( @@ -31,7 +31,7 @@ AuthenticatorSelectionCriteria& AuthenticatorSelectionCriteria::operator=( bool AuthenticatorSelectionCriteria::operator==( const AuthenticatorSelectionCriteria& other) const { return authenticator_attachment_ == other.authenticator_attachment_ && - require_resident_key_ == other.require_resident_key_ && + resident_key_ == other.resident_key_ && user_verification_requirement_ == other.user_verification_requirement_; } diff --git a/chromium/device/fido/authenticator_selection_criteria.h b/chromium/device/fido/authenticator_selection_criteria.h index 69cb8d7a255..d809eb3e4cb 100644 --- a/chromium/device/fido/authenticator_selection_criteria.h +++ b/chromium/device/fido/authenticator_selection_criteria.h @@ -7,18 +7,20 @@ #include "base/component_export.h" #include "device/fido/fido_constants.h" +#include "device/fido/fido_types.h" namespace device { // Represents authenticator properties the relying party can specify to restrict // the type of authenticator used in creating credentials. +// // https://w3c.github.io/webauthn/#authenticatorSelection class COMPONENT_EXPORT(DEVICE_FIDO) AuthenticatorSelectionCriteria { public: AuthenticatorSelectionCriteria(); AuthenticatorSelectionCriteria( AuthenticatorAttachment authenticator_attachment, - bool require_resident_key, + ResidentKeyRequirement resident_key, UserVerificationRequirement user_verification_requirement); AuthenticatorSelectionCriteria(const AuthenticatorSelectionCriteria& other); AuthenticatorSelectionCriteria(AuthenticatorSelectionCriteria&& other); @@ -33,7 +35,7 @@ class COMPONENT_EXPORT(DEVICE_FIDO) AuthenticatorSelectionCriteria { return authenticator_attachment_; } - bool require_resident_key() const { return require_resident_key_; } + ResidentKeyRequirement resident_key() const { return resident_key_; } UserVerificationRequirement user_verification_requirement() const { return user_verification_requirement_; @@ -43,8 +45,8 @@ class COMPONENT_EXPORT(DEVICE_FIDO) AuthenticatorSelectionCriteria { AuthenticatorAttachment attachment) { authenticator_attachment_ = attachment; } - void SetRequireResidentKeyForTesting(bool require) { - require_resident_key_ = require; + void SetResidentKeyForTesting(ResidentKeyRequirement resident_key) { + resident_key_ = resident_key; } void SetUserVerificationRequirementForTesting( UserVerificationRequirement uv) { @@ -54,7 +56,7 @@ class COMPONENT_EXPORT(DEVICE_FIDO) AuthenticatorSelectionCriteria { private: AuthenticatorAttachment authenticator_attachment_ = AuthenticatorAttachment::kAny; - bool require_resident_key_ = false; + ResidentKeyRequirement resident_key_ = ResidentKeyRequirement::kDiscouraged; UserVerificationRequirement user_verification_requirement_ = UserVerificationRequirement::kPreferred; }; diff --git a/chromium/device/fido/authenticator_supported_options.cc b/chromium/device/fido/authenticator_supported_options.cc index 7b9951d496d..f0d49481641 100644 --- a/chromium/device/fido/authenticator_supported_options.cc +++ b/chromium/device/fido/authenticator_supported_options.cc @@ -96,6 +96,10 @@ cbor::Value AsCBOR(const AuthenticatorSupportedOptions& options) { option_map.emplace(kEnterpriseAttestationKey, true); } + if (options.supports_large_blobs) { + option_map.emplace(kLargeBlobsKey, true); + } + return cbor::Value(std::move(option_map)); } diff --git a/chromium/device/fido/authenticator_supported_options.h b/chromium/device/fido/authenticator_supported_options.h index c93357a6cf5..2b0e115d71a 100644 --- a/chromium/device/fido/authenticator_supported_options.h +++ b/chromium/device/fido/authenticator_supported_options.h @@ -97,6 +97,9 @@ struct COMPONENT_EXPORT(DEVICE_FIDO) AuthenticatorSupportedOptions { // uninteresting to Chromium because we do not support the administrative // operation to configure it. Thus this member reduces to a boolean.) bool enterprise_attestation = false; + // Indicates whether the authenticator supports the authenticatorLargeBlobs + // command. + bool supports_large_blobs = false; }; COMPONENT_EXPORT(DEVICE_FIDO) diff --git a/chromium/device/fido/bio/enrollment.h b/chromium/device/fido/bio/enrollment.h index 92ff22a0831..77ee7dc4b77 100644 --- a/chromium/device/fido/bio/enrollment.h +++ b/chromium/device/fido/bio/enrollment.h @@ -145,7 +145,7 @@ struct BioEnrollmentRequest { ~BioEnrollmentRequest(); private: - BioEnrollmentRequest(Version); + explicit BioEnrollmentRequest(Version); }; struct COMPONENT_EXPORT(DEVICE_FIDO) BioEnrollmentResponse { diff --git a/chromium/device/fido/cable/cable_discovery_data.cc b/chromium/device/fido/cable/cable_discovery_data.cc index 005b551114b..71dffdc4349 100644 --- a/chromium/device/fido/cable/cable_discovery_data.cc +++ b/chromium/device/fido/cable/cable_discovery_data.cc @@ -7,6 +7,7 @@ #include <cstring> #include "base/time/time.h" +#include "components/cbor/values.h" #include "crypto/random.h" #include "device/fido/cable/v2_handshake.h" #include "device/fido/fido_parsing_utils.h" @@ -19,29 +20,6 @@ namespace device { -namespace { - -enum class QRValue : uint8_t { - QR_SECRET = 0, - IDENTITY_KEY_SEED = 1, -}; - -void DeriveQRValue(base::span<const uint8_t, 32> qr_generator_key, - const int64_t tick, - QRValue type, - base::span<uint8_t> out) { - uint8_t hkdf_input[sizeof(uint64_t) + 1]; - memcpy(hkdf_input, &tick, sizeof(uint64_t)); - hkdf_input[sizeof(uint64_t)] = base::strict_cast<uint8_t>(type); - - bool ok = HKDF(out.data(), out.size(), EVP_sha256(), qr_generator_key.data(), - qr_generator_key.size(), - /*salt=*/nullptr, 0, hkdf_input, sizeof(hkdf_input)); - DCHECK(ok); -} - -} // namespace - CableDiscoveryData::CableDiscoveryData() = default; CableDiscoveryData::CableDiscoveryData( @@ -57,39 +35,6 @@ CableDiscoveryData::CableDiscoveryData( v1->session_pre_key = session_pre_key; } -CableDiscoveryData::CableDiscoveryData( - base::span<const uint8_t, kCableQRSecretSize> qr_secret, - base::span<const uint8_t, kCableIdentityKeySeedSize> identity_key_seed) { - InitFromQRSecret(qr_secret); - v2->local_identity_seed = fido_parsing_utils::Materialize(identity_key_seed); -} - -// static -base::Optional<CableDiscoveryData> CableDiscoveryData::FromQRData( - base::span<const uint8_t, - kCableCompressedPublicKeySize + kCableQRSecretSize> qr_data) { - auto qr_secret = qr_data.subspan(kCableCompressedPublicKeySize); - CableDiscoveryData discovery_data; - discovery_data.InitFromQRSecret(base::span<const uint8_t, kCableQRSecretSize>( - qr_secret.data(), qr_secret.size())); - - bssl::UniquePtr<EC_GROUP> p256( - EC_GROUP_new_by_curve_name(NID_X9_62_prime256v1)); - bssl::UniquePtr<EC_POINT> point(EC_POINT_new(p256.get())); - if (!EC_POINT_oct2point(p256.get(), point.get(), qr_data.data(), - kCableCompressedPublicKeySize, /*ctx=*/nullptr)) { - return base::nullopt; - } - CableAuthenticatorIdentityKey& identity_key = - discovery_data.v2->peer_identity.emplace(); - CHECK_EQ(identity_key.size(), - EC_POINT_point2oct( - p256.get(), point.get(), POINT_CONVERSION_UNCOMPRESSED, - identity_key.data(), identity_key.size(), /*ctx=*/nullptr)); - - return discovery_data; -} - CableDiscoveryData::CableDiscoveryData(const CableDiscoveryData& data) = default; @@ -109,12 +54,6 @@ bool CableDiscoveryData::operator==(const CableDiscoveryData& other) const { v1->authenticator_eid == other.v1->authenticator_eid && v1->session_pre_key == other.v1->session_pre_key; - case CableDiscoveryData::Version::V2: - return v2->eid_gen_key == other.v2->eid_gen_key && - v2->psk_gen_key == other.v2->psk_gen_key && - v2->peer_identity == other.v2->peer_identity && - v2->peer_name == other.v2->peer_name; - case CableDiscoveryData::Version::INVALID: CHECK(false); return false; @@ -126,107 +65,59 @@ bool CableDiscoveryData::MatchV1(const CableEidArray& eid) const { return eid == v1->authenticator_eid; } -bool CableDiscoveryData::MatchV2(const CableEidArray& eid, - CableEidArray* out_eid) const { - DCHECK_EQ(version, Version::V2); - - // Attempt to decrypt the EID with the EID generator key and check whether - // it has a valid structure. - AES_KEY key; - CableEidArray& out = *out_eid; - CHECK(AES_set_decrypt_key(v2->eid_gen_key.data(), - /*bits=*/8 * v2->eid_gen_key.size(), &key) == 0); - static_assert(kCableEphemeralIdSize == AES_BLOCK_SIZE, - "EIDs are not AES blocks"); - AES_decrypt(/*in=*/eid.data(), /*out=*/out.data(), &key); - return cablev2::eid::IsValid(out); -} +namespace cablev2 { -// static -QRGeneratorKey CableDiscoveryData::NewQRKey() { - QRGeneratorKey key; - crypto::RandBytes(key.data(), key.size()); - return key; -} +Pairing::Pairing() = default; +Pairing::~Pairing() = default; // static -int64_t CableDiscoveryData::CurrentTimeTick() { - // The ticks are currently 256ms. - return base::TimeTicks::Now().since_origin().InMilliseconds() >> 8; -} +base::Optional<std::unique_ptr<Pairing>> Pairing::Parse( + const cbor::Value& cbor, + uint32_t tunnel_server_domain, + base::span<const uint8_t, kQRSeedSize> local_identity_seed, + base::span<const uint8_t, 32> handshake_hash) { + if (!cbor.is_map()) { + return base::nullopt; + } -// static -std::array<uint8_t, kCableQRSecretSize> CableDiscoveryData::DeriveQRSecret( - base::span<const uint8_t, 32> qr_generator_key, - const int64_t tick) { - std::array<uint8_t, kCableQRSecretSize> ret; - DeriveQRValue(qr_generator_key, tick, QRValue::QR_SECRET, ret); - return ret; -} + const cbor::Value::MapValue& map = cbor.GetMap(); + auto pairing = std::make_unique<Pairing>(); + + const std::array<cbor::Value::MapValue::const_iterator, 5> its = { + map.find(cbor::Value(1)), map.find(cbor::Value(2)), + map.find(cbor::Value(3)), map.find(cbor::Value(4)), + map.find(cbor::Value(6))}; + const cbor::Value::MapValue::const_iterator name_it = + map.find(cbor::Value(5)); + if (name_it == map.end() || !name_it->second.is_string() || + std::any_of( + &its[0], &its[its.size()], + [&map](const cbor::Value::MapValue::const_iterator& it) -> bool { + return it == map.end() || !it->second.is_bytestring(); + }) || + its[3]->second.GetBytestring().size() != + std::tuple_size<decltype(pairing->peer_public_key_x962)>::value) { + } -// static -CableIdentityKeySeed CableDiscoveryData::DeriveIdentityKeySeed( - base::span<const uint8_t, 32> qr_generator_key, - const int64_t tick) { - std::array<uint8_t, kCableIdentityKeySeedSize> ret; - DeriveQRValue(qr_generator_key, tick, QRValue::IDENTITY_KEY_SEED, ret); - return ret; -} + pairing->tunnel_server_domain = + tunnelserver::DecodeDomain(tunnel_server_domain), + pairing->contact_id = its[0]->second.GetBytestring(); + pairing->id = its[1]->second.GetBytestring(); + pairing->secret = its[2]->second.GetBytestring(); + const std::vector<uint8_t>& peer_public_key = its[3]->second.GetBytestring(); + std::copy(peer_public_key.begin(), peer_public_key.end(), + pairing->peer_public_key_x962.begin()); + pairing->name = name_it->second.GetString(); + + if (!VerifyPairingSignature(local_identity_seed, + pairing->peer_public_key_x962, handshake_hash, + its[4]->second.GetBytestring())) { + return base::nullopt; + } -// static -CableQRData CableDiscoveryData::DeriveQRData( - base::span<const uint8_t, 32> qr_generator_key, - const int64_t tick) { - auto identity_key_seed = DeriveIdentityKeySeed(qr_generator_key, tick); - bssl::UniquePtr<EC_GROUP> p256( - EC_GROUP_new_by_curve_name(NID_X9_62_prime256v1)); - bssl::UniquePtr<EC_KEY> identity_key(EC_KEY_derive_from_secret( - p256.get(), identity_key_seed.data(), identity_key_seed.size())); - const EC_POINT* public_key = EC_KEY_get0_public_key(identity_key.get()); - CableQRData qr_data; - static_assert( - qr_data.size() == kCableCompressedPublicKeySize + kCableQRSecretSize, - "this code needs to be updated"); - CHECK_EQ(kCableCompressedPublicKeySize, - EC_POINT_point2oct(p256.get(), public_key, - POINT_CONVERSION_COMPRESSED, qr_data.data(), - kCableCompressedPublicKeySize, /*ctx=*/nullptr)); - - auto qr_secret = CableDiscoveryData::DeriveQRSecret(qr_generator_key, tick); - memcpy(&qr_data.data()[kCableCompressedPublicKeySize], qr_secret.data(), - qr_secret.size()); - - return qr_data; + return pairing; } -CableDiscoveryData::V2Data::V2Data() = default; -CableDiscoveryData::V2Data::V2Data(const V2Data&) = default; -CableDiscoveryData::V2Data::~V2Data() = default; - -void CableDiscoveryData::InitFromQRSecret( - base::span<const uint8_t, kCableQRSecretSize> qr_secret) { - version = Version::V2; - v2.emplace(); - - static const char kEIDGen[] = "caBLE QR to EID generator key"; - bool ok = - HKDF(v2->eid_gen_key.data(), v2->eid_gen_key.size(), EVP_sha256(), - qr_secret.data(), qr_secret.size(), /*salt=*/nullptr, 0, - reinterpret_cast<const uint8_t*>(kEIDGen), sizeof(kEIDGen) - 1); - DCHECK(ok); - - static const char kPSKGen[] = "caBLE QR to PSK generator key"; - ok = HKDF(v2->psk_gen_key.data(), v2->psk_gen_key.size(), EVP_sha256(), - qr_secret.data(), qr_secret.size(), /*salt=*/nullptr, 0, - reinterpret_cast<const uint8_t*>(kPSKGen), sizeof(kPSKGen) - 1); - DCHECK(ok); - - static const char kTunnelIDGen[] = "caBLE QR to tunnel ID generator key"; - ok = HKDF(v2->tunnel_id_gen_key.data(), v2->tunnel_id_gen_key.size(), - EVP_sha256(), qr_secret.data(), qr_secret.size(), /*salt=*/nullptr, - 0, reinterpret_cast<const uint8_t*>(kTunnelIDGen), - sizeof(kTunnelIDGen) - 1); - DCHECK(ok); -} +} // namespace cablev2 } // namespace device diff --git a/chromium/device/fido/cable/cable_discovery_data.h b/chromium/device/fido/cable/cable_discovery_data.h index 54bde8a217a..14c9dfd617a 100644 --- a/chromium/device/fido/cable/cable_discovery_data.h +++ b/chromium/device/fido/cable/cable_discovery_data.h @@ -10,26 +10,22 @@ #include "base/component_export.h" #include "base/containers/span.h" +#include "base/optional.h" +#include "device/fido/cable/v2_constants.h" #include "device/fido/fido_constants.h" +namespace cbor { +class Value; +} + namespace device { constexpr size_t kCableEphemeralIdSize = 16; constexpr size_t kCableSessionPreKeySize = 32; -constexpr size_t kCableQRSecretSize = 16; constexpr size_t kCableNonceSize = 8; -constexpr size_t kCableIdentityKeySeedSize = 32; -constexpr size_t kCableCompressedPublicKeySize = - /* type byte */ 1 + /* field element */ (256 / 8); -constexpr size_t kCableQRDataSize = - kCableCompressedPublicKeySize + kCableQRSecretSize; using CableEidArray = std::array<uint8_t, kCableEphemeralIdSize>; using CableSessionPreKeyArray = std::array<uint8_t, kCableSessionPreKeySize>; -// QRGeneratorKey is a random, AES-256 key that is used by -// |CableDiscoveryData::DeriveQRKeyMaterial| to encrypt a coarse timestamp and -// generate QR secrets, EIDs, etc. -using QRGeneratorKey = std::array<uint8_t, 32>; // CableNonce is a nonce used in BLE handshaking. using CableNonce = std::array<uint8_t, 8>; // CableEidGeneratorKey is an AES-256 key that is used to encrypt a 64-bit nonce @@ -42,8 +38,6 @@ using CableTunnelIDGeneratorKey = std::array<uint8_t, 32>; // CableAuthenticatorIdentityKey is a P-256 public value used to authenticate a // paired phone. using CableAuthenticatorIdentityKey = std::array<uint8_t, kP256X962Length>; -using CableIdentityKeySeed = std::array<uint8_t, kCableIdentityKeySeedSize>; -using CableQRData = std::array<uint8_t, kCableQRDataSize>; // Encapsulates information required to discover Cable device per single // credential. When multiple credentials are enrolled to a single account @@ -56,32 +50,16 @@ struct COMPONENT_EXPORT(DEVICE_FIDO) CableDiscoveryData { enum class Version { INVALID, V1, - V2, }; CableDiscoveryData(Version version, const CableEidArray& client_eid, const CableEidArray& authenticator_eid, const CableSessionPreKeyArray& session_pre_key); - // Creates discovery data given a specific QR secret and identity key seed. - // This will be used on the QR-displaying-side of a QR handshake. See - // |DeriveQRSecret| and |DeriveIdentityKeySeed| for how to generate such - // secrets. - CableDiscoveryData( - base::span<const uint8_t, kCableQRSecretSize> qr_secret, - base::span<const uint8_t, kCableIdentityKeySeedSize> identity_key_seed); CableDiscoveryData(); CableDiscoveryData(const CableDiscoveryData& data); ~CableDiscoveryData(); - // Creates discovery data given QR data, which contains a compressed public - // key and the QR secret. This will be used by the QR-scanning-side of a QR - // handshake. Returns |nullopt| if the embedded elliptic-curve point is - // invalid. - static base::Optional<CableDiscoveryData> FromQRData( - base::span<const uint8_t, - kCableCompressedPublicKeySize + kCableQRSecretSize> qr_data); - CableDiscoveryData& operator=(const CableDiscoveryData& other); bool operator==(const CableDiscoveryData& other) const; @@ -89,38 +67,6 @@ struct COMPONENT_EXPORT(DEVICE_FIDO) CableDiscoveryData { // instance, which must be version one. bool MatchV1(const CableEidArray& candidate_eid) const; - // MatchV2 returns true if |candidate_eid| matches this caBLE discovery - // instance, which must be version two. If so, |*out_eid| is set to the value - // of the decrypted EID. - bool MatchV2(const CableEidArray& candidate_eid, - CableEidArray* out_eid) const; - - // NewQRKey returns a random key for QR generation. - static QRGeneratorKey NewQRKey(); - - // CurrentTimeTick returns the current time as used by QR generation. The size - // of these ticks is a purely local matter for Chromium. - static int64_t CurrentTimeTick(); - - // DeriveQRKeyMaterial returns a QR-secret given a generating key and a - // timestamp. - static std::array<uint8_t, kCableQRSecretSize> DeriveQRSecret( - base::span<const uint8_t, 32> qr_generator_key, - const int64_t tick); - - // DeriveIdentityKeySeed returns a seed that can be used to create a P-256 - // identity key for a handshake using |EC_KEY_derive_from_secret|. - static CableIdentityKeySeed DeriveIdentityKeySeed( - base::span<const uint8_t, 32> qr_generator_key, - const int64_t tick); - - // DeriveQRData returns the QR data, a combination of QR secret and public - // identity key. This is base64url-encoded and placed in a caBLE v2 QR code - // with a prefix prepended. - static CableQRData DeriveQRData( - base::span<const uint8_t, 32> qr_generator_key, - const int64_t tick); - // version indicates whether v1 or v2 data is contained in this object. // |INVALID| is not a valid version but is set as the default to catch any // cases where the version hasn't been set explicitly. @@ -132,30 +78,49 @@ struct COMPONENT_EXPORT(DEVICE_FIDO) CableDiscoveryData { CableSessionPreKeyArray session_pre_key; }; base::Optional<V1Data> v1; +}; - struct COMPONENT_EXPORT(DEVICE_FIDO) V2Data { - V2Data(); - V2Data(const V2Data&); - ~V2Data(); - - CableEidGeneratorKey eid_gen_key; - CablePskGeneratorKey psk_gen_key; - CableTunnelIDGeneratorKey tunnel_id_gen_key; - base::Optional<CableAuthenticatorIdentityKey> peer_identity; - base::Optional<CableIdentityKeySeed> local_identity_seed; - // peer_name is an authenticator-controlled, UTF8-valid string containing - // the self-reported, human-friendly name of a v2 authenticator. This need - // not be filled in when handshaking but an authenticator may provide it - // when offering long-term pairing data. - base::Optional<std::string> peer_name; - }; - base::Optional<V2Data> v2; - - private: - void InitFromQRSecret( - base::span<const uint8_t, kCableQRSecretSize> qr_secret); +namespace cablev2 { + +// Pairing represents information previously received from a caBLEv2 +// authenticator that enables future interactions to skip scanning a QR code. +struct COMPONENT_EXPORT(DEVICE_FIDO) Pairing { + Pairing(); + ~Pairing(); + Pairing(const Pairing&) = delete; + Pairing& operator=(const Pairing&) = delete; + + // Parse builds a |Pairing| from an authenticator message. The signature + // within the structure is validated by using |local_identity_seed| and + // |handshake_hash|. + static base::Optional<std::unique_ptr<Pairing>> Parse( + const cbor::Value& cbor, + uint32_t tunnel_server_domain, + base::span<const uint8_t, kQRSeedSize> local_identity_seed, + base::span<const uint8_t, 32> handshake_hash); + + // tunnel_server_domain is known to be a valid hostname as it's constructed + // from the 22-bit value in the BLE advert rather than being parsed as a + // string from the authenticator. + std::string tunnel_server_domain; + // contact_id is an opaque value that is sent to the tunnel service in order + // to identify the caBLEv2 authenticator. + std::vector<uint8_t> contact_id; + // id is an opaque identifier that is sent via the tunnel service, to the + // authenticator, to identify this specific pairing. + std::vector<uint8_t> id; + // secret is the shared secret that authenticates the desktop to the + // authenticator. + std::vector<uint8_t> secret; + // peer_public_key_x962 is the authenticator's public key. + std::array<uint8_t, kP256X962Length> peer_public_key_x962; + // name is a human-friendly name for the authenticator, specified by that + // authenticator. (For example "Pixel 3".) + std::string name; }; +} // namespace cablev2 + } // namespace device #endif // DEVICE_FIDO_CABLE_CABLE_DISCOVERY_DATA_H_ diff --git a/chromium/device/fido/cable/fido_ble_connection_unittest.cc b/chromium/device/fido/cable/fido_ble_connection_unittest.cc index 3abfca15fb3..a07c6924844 100644 --- a/chromium/device/fido/cable/fido_ble_connection_unittest.cc +++ b/chromium/device/fido/cable/fido_ble_connection_unittest.cc @@ -226,8 +226,7 @@ class FidoBleConnectionTest : public ::testing::Test { EXPECT_CALL(*fido_device_, GetAddress) .WillRepeatedly(::testing::Return(new_address)); for (auto& observer : adapter_->GetObservers()) - observer.DeviceAddressChanged(adapter_.get(), fido_device_, - std::move(old_address)); + observer.DeviceAddressChanged(adapter_.get(), fido_device_, old_address); } void SetNextReadControlPointLengthReponse(bool success, diff --git a/chromium/device/fido/cable/fido_cable_discovery.cc b/chromium/device/fido/cable/fido_cable_discovery.cc index f4e8c2fb901..d1d4ca3ecc1 100644 --- a/chromium/device/fido/cable/fido_cable_discovery.cc +++ b/chromium/device/fido/cable/fido_cable_discovery.cc @@ -126,24 +126,6 @@ enum class FidoCableDiscovery::CableV1DiscoveryEvent : int { kMaxValue = kScanningStoppedUnexpectedly, }; -// FidoCableDiscovery::Result ------------------------------------------------- - -FidoCableDiscovery::Result::Result() = default; - -FidoCableDiscovery::Result::Result( - const CableDiscoveryData& in_discovery_data, - const CableEidArray& in_eid, - base::Optional<CableEidArray> in_decrypted_eid, - base::Optional<int> in_ticks_back) - : discovery_data(in_discovery_data), - eid(in_eid), - decrypted_eid(std::move(in_decrypted_eid)), - ticks_back(in_ticks_back) {} - -FidoCableDiscovery::Result::Result(const Result& other) = default; - -FidoCableDiscovery::Result::~Result() = default; - // FidoCableDiscovery::ObservedDeviceData ------------------------------------- FidoCableDiscovery::ObservedDeviceData::ObservedDeviceData() = default; @@ -153,17 +135,11 @@ FidoCableDiscovery::ObservedDeviceData::~ObservedDeviceData() = default; FidoCableDiscovery::FidoCableDiscovery( std::vector<CableDiscoveryData> discovery_data, - base::Optional<QRGeneratorKey> qr_generator_key, - base::Optional< - base::RepeatingCallback<void(std::unique_ptr<CableDiscoveryData>)>> - pairing_callback, - network::mojom::NetworkContext* network_context) + FidoDeviceDiscovery::BLEObserver* ble_observer) : FidoDeviceDiscovery( FidoTransportProtocol::kCloudAssistedBluetoothLowEnergy), discovery_data_(std::move(discovery_data)), - qr_generator_key_(std::move(qr_generator_key)), - pairing_callback_(std::move(pairing_callback)), - network_context_(network_context) { + ble_observer_(ble_observer) { // Windows currently does not support multiple EIDs, thus we ignore any extra // discovery data. // TODO(https://crbug.com/837088): Add support for multiple EIDs on Windows. @@ -212,7 +188,6 @@ FidoCableDiscovery::CreateV1HandshakeHandler( device, nonce, discovery_data.v1->session_pre_key); } - case CableDiscoveryData::Version::V2: case CableDiscoveryData::Version::INVALID: CHECK(false); return nullptr; @@ -296,8 +271,11 @@ void FidoCableDiscovery::DeviceChanged(BluetoothAdapter* adapter, void FidoCableDiscovery::DeviceRemoved(BluetoothAdapter* adapter, BluetoothDevice* device) { - if (IsCableDevice(device) && GetCableDiscoveryData(device)) { - const auto& device_address = device->GetAddress(); + const auto& device_address = device->GetAddress(); + if (IsCableDevice(device) && + // It only matters if V1 devices are "removed" because V2 devices do not + // transport data over BLE. + base::Contains(active_devices_, device_address)) { FIDO_LOG(DEBUG) << "caBLE device removed: " << device_address; RemoveDevice(FidoCableDevice::GetIdForAddress(device_address)); } @@ -476,18 +454,21 @@ void FidoCableDiscovery::CableDeviceFound(BluetoothAdapter* adapter, return; } - base::Optional<Result> result = GetCableDiscoveryData(device); - if (!result || base::Contains(active_authenticator_eids_, result->eid)) { + base::Optional<V1DiscoveryDataAndEID> v1_match = + GetCableDiscoveryData(device); + if (!v1_match) { return; } - FIDO_LOG(EVENT) << "Found new caBLE device."; - if (result->discovery_data.version == CableDiscoveryData::Version::V1) { - RecordCableV1DiscoveryEventOnce( - CableV1DiscoveryEvent::kFirstCableDeviceFound); + if (base::Contains(active_authenticator_eids_, v1_match->second)) { + return; } + active_authenticator_eids_.insert(v1_match->second); active_devices_.insert(device_address); - active_authenticator_eids_.insert(result->eid); + + FIDO_LOG(EVENT) << "Found new caBLEv1 device."; + RecordCableV1DiscoveryEventOnce( + CableV1DiscoveryEvent::kFirstCableDeviceFound); #if defined(OS_CHROMEOS) || defined(OS_LINUX) // Speed up GATT service discovery on ChromeOS/BlueZ. @@ -501,41 +482,21 @@ void FidoCableDiscovery::CableDeviceFound(BluetoothAdapter* adapter, } #endif // defined(OS_CHROMEOS) || defined(OS_LINUX) - switch (result->discovery_data.version) { - case CableDiscoveryData::Version::V1: { - auto cable_device = - std::make_unique<FidoCableDevice>(adapter, device_address); - cable_device->set_observer(this); - - std::unique_ptr<FidoCableHandshakeHandler> handshake_handler = - CreateV1HandshakeHandler(cable_device.get(), result->discovery_data, - result->eid); - auto* const handshake_handler_ptr = handshake_handler.get(); - active_handshakes_.emplace_back(std::move(cable_device), - std::move(handshake_handler)); - - StopAdvertisements( - base::BindOnce(&FidoCableDiscovery::ConductEncryptionHandshake, - weak_factory_.GetWeakPtr(), handshake_handler_ptr, - result->discovery_data.version)); - break; - } + auto cable_device = + std::make_unique<FidoCableDevice>(adapter, device_address); + cable_device->set_observer(this); - case CableDiscoveryData::Version::V2: { - if (!base::FeatureList::IsEnabled(device::kWebAuthPhoneSupport) || - !network_context_) { - return; - } - AddDevice(std::make_unique<cablev2::FidoTunnelDevice>( - network_context_, *result->discovery_data.v2, result->eid, - *result->decrypted_eid)); - break; - } + std::unique_ptr<FidoCableHandshakeHandler> handshake_handler = + CreateV1HandshakeHandler(cable_device.get(), v1_match->first, + v1_match->second); + auto* const handshake_handler_ptr = handshake_handler.get(); + active_handshakes_.emplace_back(std::move(cable_device), + std::move(handshake_handler)); - case CableDiscoveryData::Version::INVALID: - CHECK(false); - return; - } + StopAdvertisements( + base::BindOnce(&FidoCableDiscovery::ConductEncryptionHandshake, + weak_factory_.GetWeakPtr(), handshake_handler_ptr, + v1_match->first.version)); } void FidoCableDiscovery::ConductEncryptionHandshake( @@ -581,8 +542,8 @@ void FidoCableDiscovery::ValidateAuthenticatorHandshakeMessage( } } -base::Optional<FidoCableDiscovery::Result> -FidoCableDiscovery::GetCableDiscoveryData(const BluetoothDevice* device) const { +base::Optional<FidoCableDiscovery::V1DiscoveryDataAndEID> +FidoCableDiscovery::GetCableDiscoveryData(const BluetoothDevice* device) { base::Optional<CableEidArray> maybe_eid_from_service_data = MaybeGetEidFromServiceData(device); std::vector<CableEidArray> uuids = GetUUIDs(device); @@ -607,13 +568,12 @@ FidoCableDiscovery::GetCableDiscoveryData(const BluetoothDevice* device) const { FIDO_LOG(DEBUG) << "New caBLE device " << address << ":"; } - base::Optional<FidoCableDiscovery::Result> result; + base::Optional<FidoCableDiscovery::V1DiscoveryDataAndEID> result; if (maybe_eid_from_service_data.has_value()) { result = GetCableDiscoveryDataFromAuthenticatorEid(*maybe_eid_from_service_data); FIDO_LOG(DEBUG) << " Service data: " << ResultDebugString(*maybe_eid_from_service_data, result); - } else { FIDO_LOG(DEBUG) << " Service data: <none>"; } @@ -621,10 +581,14 @@ FidoCableDiscovery::GetCableDiscoveryData(const BluetoothDevice* device) const { if (!uuids.empty()) { FIDO_LOG(DEBUG) << " UUIDs:"; for (const auto& uuid : uuids) { - auto discovery_data = GetCableDiscoveryDataFromAuthenticatorEid(uuid); - FIDO_LOG(DEBUG) << " " << ResultDebugString(uuid, discovery_data); - if (!result && discovery_data) { - result = discovery_data; + auto eid_result = GetCableDiscoveryDataFromAuthenticatorEid(uuid); + FIDO_LOG(DEBUG) << " " << ResultDebugString(uuid, eid_result); + if (!result && eid_result) { + result = std::move(eid_result); + } + + if (ble_observer_) { + ble_observer_->OnBLEAdvertSeen(device->GetAddress(), uuid); } } } @@ -678,51 +642,13 @@ std::vector<CableEidArray> FidoCableDiscovery::GetUUIDs( return ret; } -base::Optional<FidoCableDiscovery::Result> +base::Optional<FidoCableDiscovery::V1DiscoveryDataAndEID> FidoCableDiscovery::GetCableDiscoveryDataFromAuthenticatorEid( - CableEidArray authenticator_eid) const { + CableEidArray authenticator_eid) { for (const auto& candidate : discovery_data_) { if (candidate.version == CableDiscoveryData::Version::V1 && candidate.MatchV1(authenticator_eid)) { - return Result(candidate, authenticator_eid, base::nullopt, base::nullopt); - } - } - - if (qr_generator_key_) { - // Attempt to match |authenticator_eid| as the result of scanning a QR code. - const int64_t current_tick = CableDiscoveryData::CurrentTimeTick(); - // kNumPreviousTicks is the number of previous ticks that will be accepted - // as valid. Ticks are currently 256ms so the value of sixteen translates to - // about four seconds. - constexpr int kNumPreviousTicks = 16; - - for (int i = 0; i < kNumPreviousTicks; i++) { - auto qr_secret = CableDiscoveryData::DeriveQRSecret(*qr_generator_key_, - current_tick - i); - auto identity_key_seed = CableDiscoveryData::DeriveIdentityKeySeed( - *qr_generator_key_, current_tick - i); - CableDiscoveryData candidate(qr_secret, identity_key_seed); - CableEidArray decrypted; - if (candidate.MatchV2(authenticator_eid, &decrypted)) { - return Result(candidate, authenticator_eid, decrypted, i); - } - } - - if (base::Contains(noted_obsolete_eids_, authenticator_eid)) { - std::array<uint8_t, kCableIdentityKeySeedSize> dummy_seed; - for (int i = kNumPreviousTicks; i < 2 * kNumPreviousTicks; i++) { - auto qr_secret = CableDiscoveryData::DeriveQRSecret(*qr_generator_key_, - current_tick - i); - CableDiscoveryData candidate(qr_secret, dummy_seed); - CableEidArray decrypted; - if (candidate.MatchV2(authenticator_eid, &decrypted)) { - noted_obsolete_eids_.insert(authenticator_eid); - FIDO_LOG(DEBUG) - << "(EID " << base::HexEncode(authenticator_eid) << " is " << i - << " ticks old and would be valid but for the cutoff)"; - break; - } - } + return V1DiscoveryDataAndEID(candidate, authenticator_eid); } } @@ -748,7 +674,7 @@ void FidoCableDiscovery::StartInternal() { // static std::string FidoCableDiscovery::ResultDebugString( const CableEidArray& eid, - const base::Optional<FidoCableDiscovery::Result>& result) { + const base::Optional<FidoCableDiscovery::V1DiscoveryDataAndEID>& result) { static const uint8_t kAppleContinuity[16] = { 0xd0, 0x61, 0x1e, 0x78, 0xbb, 0xb4, 0x45, 0x91, 0xa5, 0xf8, 0x48, 0x79, 0x10, 0xae, 0x43, 0x66, @@ -789,22 +715,8 @@ std::string FidoCableDiscovery::ResultDebugString( return ret; } - switch (result->discovery_data.version) { - case CableDiscoveryData::Version::V1: - ret += " (version one match"; - break; - case CableDiscoveryData::Version::V2: - ret += " (version two match"; - break; - case CableDiscoveryData::Version::INVALID: - NOTREACHED(); - } - - if (!result->ticks_back) { - ret += " against pairing data)"; - } else { - ret += " from QR, " + base::NumberToString(*result->ticks_back) + - " tick(s) ago)"; + if (result) { + ret += " (version one match)"; } return ret; diff --git a/chromium/device/fido/cable/fido_cable_discovery.h b/chromium/device/fido/cable/fido_cable_discovery.h index 7104c5756e7..c7fbc54c8df 100644 --- a/chromium/device/fido/cable/fido_cable_discovery.h +++ b/chromium/device/fido/cable/fido_cable_discovery.h @@ -21,14 +21,9 @@ #include "device/bluetooth/bluetooth_adapter.h" #include "device/fido/cable/cable_discovery_data.h" #include "device/fido/cable/fido_cable_device.h" +#include "device/fido/cable/v2_constants.h" #include "device/fido/fido_device_discovery.h" -namespace network { -namespace mojom { -class NetworkContext; -} -} // namespace network - namespace device { class BluetoothDevice; @@ -40,13 +35,8 @@ class COMPONENT_EXPORT(DEVICE_FIDO) FidoCableDiscovery public BluetoothAdapter::Observer, public FidoCableDevice::Observer { public: - FidoCableDiscovery( - std::vector<CableDiscoveryData> discovery_data, - base::Optional<QRGeneratorKey> qr_generator_key, - base::Optional< - base::RepeatingCallback<void(std::unique_ptr<CableDiscoveryData>)>> - pairing_callback, - network::mojom::NetworkContext* network_context); + FidoCableDiscovery(std::vector<CableDiscoveryData> discovery_data, + FidoDeviceDiscovery::BLEObserver* ble_observer); ~FidoCableDiscovery() override; // FidoDeviceDiscovery: @@ -66,26 +56,10 @@ class COMPONENT_EXPORT(DEVICE_FIDO) FidoCableDiscovery private: enum class CableV1DiscoveryEvent : int; - // Result represents a successful match of a received EID against a specific - // |FidoDiscoveryData|. - struct Result { - Result(); - Result(const CableDiscoveryData& in_discovery_data, - const CableEidArray& in_eid, - base::Optional<CableEidArray> decrypted_eid, - base::Optional<int> ticks_back); - Result(const Result&); - ~Result(); - - CableDiscoveryData discovery_data; - CableEidArray eid; - - base::Optional<CableEidArray> decrypted_eid; - // ticks_back is either |base::nullopt|, if the Result is from established - // discovery pairings, or else contains the number of QR ticks back in time - // against which the match was found. - base::Optional<int> ticks_back; - }; + // V1DiscoveryDataAndEID represents a match against caBLEv1 pairing data. It + // contains the CableDiscoveryData that matched and the BLE EID that triggered + // the match. + using V1DiscoveryDataAndEID = std::pair<CableDiscoveryData, CableEidArray>; // ObservedDeviceData contains potential EIDs observed from a BLE device. This // information is kept in order to de-duplicate device-log entries and make @@ -103,8 +77,9 @@ class COMPONENT_EXPORT(DEVICE_FIDO) FidoCableDiscovery // ResultDebugString returns a string containing a hex dump of |eid| and a // description of |result|, if present. - static std::string ResultDebugString(const CableEidArray& eid, - const base::Optional<Result>& result); + static std::string ResultDebugString( + const CableEidArray& eid, + const base::Optional<V1DiscoveryDataAndEID>& result); static base::Optional<CableEidArray> MaybeGetEidFromServiceData( const BluetoothDevice* device); static std::vector<CableEidArray> GetUUIDs(const BluetoothDevice* device); @@ -138,10 +113,10 @@ class COMPONENT_EXPORT(DEVICE_FIDO) FidoCableDiscovery FidoCableHandshakeHandler* handshake_handler, base::Optional<std::vector<uint8_t>> handshake_response); - base::Optional<Result> GetCableDiscoveryData( - const BluetoothDevice* device) const; - base::Optional<Result> GetCableDiscoveryDataFromAuthenticatorEid( - CableEidArray authenticator_eid) const; + base::Optional<V1DiscoveryDataAndEID> GetCableDiscoveryData( + const BluetoothDevice* device); + base::Optional<V1DiscoveryDataAndEID> + GetCableDiscoveryDataFromAuthenticatorEid(CableEidArray authenticator_eid); void RecordCableV1DiscoveryEventOnce(CableV1DiscoveryEvent event); // FidoDeviceDiscovery: @@ -165,18 +140,19 @@ class COMPONENT_EXPORT(DEVICE_FIDO) FidoCableDiscovery std::unique_ptr<BluetoothDiscoverySession> discovery_session_; std::vector<CableDiscoveryData> discovery_data_; + FidoDeviceDiscovery::BLEObserver* const ble_observer_; // active_authenticator_eids_ contains authenticator EIDs for which a // handshake is currently running. Further advertisements for the same EIDs // will be ignored. std::set<CableEidArray> active_authenticator_eids_; - // active_devices_ contains the BLE addresses of devices for which a handshake - // is already running. Further advertisements from these devices will be - // ignored. However, devices may rotate their BLE address at will so this is - // not completely effective. + // active_devices_ contains the BLE addresses of devices for which a + // handshake is already running. Further advertisements from these devices + // will be ignored. However, devices may rotate their BLE address at will so + // this is not completely effective. std::set<std::string> active_devices_; - base::Optional<QRGeneratorKey> qr_generator_key_; + base::Optional<std::array<uint8_t, cablev2::kQRKeySize>> qr_generator_key_; // Note that on Windows, |advertisements_| is the only reference holder of // BluetoothAdvertisement. @@ -186,20 +162,12 @@ class COMPONENT_EXPORT(DEVICE_FIDO) FidoCableDiscovery std::vector<std::pair<std::unique_ptr<FidoCableDevice>, std::unique_ptr<FidoCableHandshakeHandler>>> active_handshakes_; - base::Optional< - base::RepeatingCallback<void(std::unique_ptr<CableDiscoveryData>)>> - pairing_callback_; - network::mojom::NetworkContext* const network_context_; // observed_devices_ caches the information from observed caBLE devices so // that the device-log isn't spammed. - mutable base::flat_map<std::string, std::unique_ptr<ObservedDeviceData>> + base::flat_map<std::string, std::unique_ptr<ObservedDeviceData>> observed_devices_; - // noted_obsolete_eids_ remembers QR-code EIDs that have been logged as - // valid-but-expired in order to avoid spamming the device-log. - mutable base::flat_set<CableEidArray> noted_obsolete_eids_; - bool has_v1_discovery_data_ = false; base::flat_set<CableV1DiscoveryEvent> recorded_events_; diff --git a/chromium/device/fido/cable/fido_cable_discovery_unittest.cc b/chromium/device/fido/cable/fido_cable_discovery_unittest.cc index 9ac27ec3d42..d45b9d83f6e 100644 --- a/chromium/device/fido/cable/fido_cable_discovery_unittest.cc +++ b/chromium/device/fido/cable/fido_cable_discovery_unittest.cc @@ -326,9 +326,7 @@ class FakeFidoCableDiscovery : public FidoCableDiscovery { explicit FakeFidoCableDiscovery( std::vector<CableDiscoveryData> discovery_data) : FidoCableDiscovery(std::move(discovery_data), - BogusQRGeneratorKey(), - /*pairing_callback=*/base::nullopt, - /*network_context=*/nullptr) {} + /*ble_observer=*/nullptr) {} ~FakeFidoCableDiscovery() override = default; private: @@ -342,12 +340,6 @@ class FakeFidoCableDiscovery : public FidoCableDiscovery { return std::make_unique<FakeHandshakeHandler>( device, nonce, discovery_data.v1->session_pre_key); } - - static std::array<uint8_t, 32> BogusQRGeneratorKey() { - std::array<uint8_t, 32> ret; - memset(ret.data(), 0, ret.size()); - return ret; - } }; } // namespace diff --git a/chromium/device/fido/cable/fido_tunnel_device.cc b/chromium/device/fido/cable/fido_tunnel_device.cc index 457971a0ba5..f557e4590d9 100644 --- a/chromium/device/fido/cable/fido_tunnel_device.cc +++ b/chromium/device/fido/cable/fido_tunnel_device.cc @@ -5,16 +5,43 @@ #include "device/fido/cable/fido_tunnel_device.h" #include "base/strings/string_number_conversions.h" +#include "base/threading/thread_task_runner_handle.h" +#include "components/cbor/values.h" +#include "components/cbor/writer.h" #include "components/device_event_log/device_event_log.h" #include "crypto/random.h" +#include "device/fido/cable/cable_discovery_data.h" +#include "device/fido/cbor_extract.h" #include "device/fido/fido_constants.h" +#include "device/fido/fido_parsing_utils.h" #include "net/traffic_annotation/network_traffic_annotation.h" +#include "third_party/boringssl/src/include/openssl/aes.h" #include "third_party/boringssl/src/include/openssl/digest.h" #include "third_party/boringssl/src/include/openssl/hkdf.h" +using device::cbor_extract::IntKey; +using device::cbor_extract::Is; +using device::cbor_extract::StepOrByte; +using device::cbor_extract::Stop; + namespace device { namespace cablev2 { +namespace { + +std::array<uint8_t, 8> RandomId() { + std::array<uint8_t, 8> ret; + crypto::RandBytes(ret); + return ret; +} + +} // namespace + +FidoTunnelDevice::QRInfo::QRInfo() = default; +FidoTunnelDevice::QRInfo::~QRInfo() = default; +FidoTunnelDevice::PairedInfo::PairedInfo() = default; +FidoTunnelDevice::PairedInfo::~PairedInfo() = default; + constexpr net::NetworkTrafficAnnotationTag kTrafficAnnotation = net::DefineNetworkTrafficAnnotation("cablev2_websocket_from_client", R"( semantics { @@ -40,34 +67,37 @@ constexpr net::NetworkTrafficAnnotationTag kTrafficAnnotation = "triggered by significant user action." policy_exception_justification: "No policy provided because the operation is triggered by " - " significant user action." + " significant user action. No background activity occurs." })"); FidoTunnelDevice::FidoTunnelDevice( network::mojom::NetworkContext* network_context, - const CableDiscoveryData::V2Data& v2data, + base::OnceCallback<void(std::unique_ptr<Pairing>)> pairing_callback, + base::span<const uint8_t> secret, + base::span<const uint8_t, kQRSeedSize> local_identity_seed, const CableEidArray& eid, const CableEidArray& decrypted_eid) - : v2data_(v2data) { + : info_(absl::in_place_type<QRInfo>), id_(RandomId()) { DCHECK(eid::IsValid(decrypted_eid)); - crypto::RandBytes(id_); - const eid::Components components = eid::ToComponents(decrypted_eid); - nonce_and_eid_.first = components.nonce; - nonce_and_eid_.second = eid; + + QRInfo& info = absl::get<QRInfo>(info_); + info.pairing_callback = std::move(pairing_callback); + info.eid = eid; + info.local_identity_seed = + fido_parsing_utils::Materialize(local_identity_seed); + info.tunnel_server_domain = components.tunnel_server_domain; + + info.psk = Derive<EXTENT(info.psk)>(secret, components.nonce, + DerivedValueType::kPSK); std::array<uint8_t, 16> tunnel_id; - bool ok = HKDF(tunnel_id.data(), tunnel_id.size(), EVP_sha256(), - v2data_.tunnel_id_gen_key.data(), - v2data_.tunnel_id_gen_key.size(), components.nonce.data(), - components.nonce.size(), /*info=*/nullptr, 0); - DCHECK(ok); - - const GURL url(cablev2::tunnelserver::GetURL( - components.tunnel_server_domain, cablev2::tunnelserver::Action::kConnect, - tunnel_id)); - FIDO_LOG(DEBUG) << "Connecting caBLEv2 tunnel: " << url - << " shard: " << static_cast<int>(components.shard_id); + tunnel_id = Derive<EXTENT(tunnel_id)>(secret, components.nonce, + DerivedValueType::kTunnelID); + + const GURL url(tunnelserver::GetConnectURL(components.tunnel_server_domain, + components.routing_id, tunnel_id)); + FIDO_LOG(DEBUG) << GetId() << ": connecting caBLEv2 tunnel: " << url; websocket_client_ = std::make_unique<device::cablev2::WebSocketAdapter>( base::BindOnce(&FidoTunnelDevice::OnTunnelReady, base::Unretained(this)), @@ -84,20 +114,102 @@ FidoTunnelDevice::FidoTunnelDevice( mojo::NullRemote()); } +FidoTunnelDevice::FidoTunnelDevice( + network::mojom::NetworkContext* network_context, + std::unique_ptr<Pairing> pairing) + : info_(absl::in_place_type<PairedInfo>), id_(RandomId()) { + uint8_t client_nonce[kClientNonceSize]; + crypto::RandBytes(client_nonce); + + cbor::Value::MapValue client_payload; + client_payload.emplace(1, pairing->id); + client_payload.emplace(2, base::span<const uint8_t>(client_nonce)); + const base::Optional<std::vector<uint8_t>> client_payload_bytes = + cbor::Writer::Write(cbor::Value(std::move(client_payload))); + CHECK(client_payload_bytes.has_value()); + const std::string client_payload_hex = base::HexEncode(*client_payload_bytes); + + PairedInfo& info = absl::get<PairedInfo>(info_); + info.eid_encryption_key = Derive<EXTENT(info.eid_encryption_key)>( + pairing->secret, client_nonce, DerivedValueType::kEIDKey); + info.peer_identity = pairing->peer_public_key_x962; + info.secret = pairing->secret; + + const GURL url = tunnelserver::GetContactURL(pairing->tunnel_server_domain, + pairing->contact_id); + FIDO_LOG(DEBUG) << GetId() << ": connecting caBLEv2 tunnel: " << url; + + websocket_client_ = std::make_unique<device::cablev2::WebSocketAdapter>( + base::BindOnce(&FidoTunnelDevice::OnTunnelReady, base::Unretained(this)), + base::BindRepeating(&FidoTunnelDevice::OnTunnelData, + base::Unretained(this))); + std::vector<network::mojom::HttpHeaderPtr> headers; + headers.emplace_back(network::mojom::HttpHeader::New( + kCableClientPayloadHeader, client_payload_hex)); + network_context->CreateWebSocket( + url, {kCableWebSocketProtocol}, net::SiteForCookies(), + net::IsolationInfo(), std::move(headers), + network::mojom::kBrowserProcessId, + /*render_frame_id=*/0, url::Origin::Create(url), + network::mojom::kWebSocketOptionBlockAllCookies, + net::MutableNetworkTrafficAnnotationTag(kTrafficAnnotation), + websocket_client_->BindNewHandshakeClientPipe(), mojo::NullRemote(), + mojo::NullRemote()); +} + FidoTunnelDevice::~FidoTunnelDevice() { DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); } +bool FidoTunnelDevice::MatchEID(const CableEidArray& eid) { + PairedInfo& info = absl::get<PairedInfo>(info_); + + AES_KEY key; + CHECK(AES_set_decrypt_key(info.eid_encryption_key.data(), + /*bits=*/8 * info.eid_encryption_key.size(), + &key) == 0); + CableEidArray plaintext; + static_assert(EXTENT(plaintext) == AES_BLOCK_SIZE, "EIDs are not AES blocks"); + AES_decrypt(/*in=*/eid.data(), /*out=*/plaintext.data(), &key); + + if (!eid::IsValid(plaintext)) { + return false; + } + + const eid::Components components = eid::ToComponents(plaintext); + static_assert(EXTENT(components.routing_id) == 3, ""); + if (components.routing_id[0] || components.routing_id[1] || + components.routing_id[2]) { + return false; + } + + info.eid = eid; + info.psk = Derive<EXTENT(*info.psk)>(info.secret, components.nonce, + DerivedValueType::kPSK); + + if (state_ == State::kWaitingForEID) { + // The handshake message has already been received. It can now be answered. + ProcessHandshake(*info.handshake_message); + } + + return true; +} + FidoDevice::CancelToken FidoTunnelDevice::DeviceTransact( std::vector<uint8_t> command, DeviceCallback callback) { DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); DCHECK(!callback_); - pending_message_ = std::move(command); - callback_ = std::move(callback); - if (state_ == State::kHandshakeProcessed || state_ == State::kReady) { - MaybeFlushPendingMessage(); + if (state_ == State::kError) { + base::SequencedTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::BindOnce(std::move(callback), base::nullopt)); + } else { + pending_message_ = std::move(command); + callback_ = std::move(callback); + if (state_ == State::kHandshakeProcessed || state_ == State::kReady) { + MaybeFlushPendingMessage(); + } } // TODO: cancelation would be useful, but it depends on the GMSCore action @@ -124,12 +236,14 @@ base::WeakPtr<FidoDevice> FidoTunnelDevice::GetWeakPtr() { return weak_factory_.GetWeakPtr(); } -void FidoTunnelDevice::OnTunnelReady(bool ok, - base::Optional<uint8_t> shard_id) { +void FidoTunnelDevice::OnTunnelReady( + bool ok, + base::Optional<std::array<uint8_t, kRoutingIdSize>> routing_id) { DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); DCHECK_EQ(State::kConnecting, state_); if (!ok) { + FIDO_LOG(DEBUG) << GetId() << ": tunnel failed to connect"; OnError(); return; } @@ -148,29 +262,15 @@ void FidoTunnelDevice::OnTunnelData( switch (state_) { case State::kError: + break; + case State::kConnecting: - NOTREACHED(); + case State::kWaitingForEID: + OnError(); break; case State::kConnected: { - std::vector<uint8_t> response; - base::Optional<std::pair<std::unique_ptr<Crypter>, std::vector<uint8_t>>> - result(cablev2::RespondToHandshake( - v2data_.psk_gen_key, nonce_and_eid_, v2data_.local_identity_seed, - base::nullopt, *data, &response)); - if (!result || result->second.empty()) { - FIDO_LOG(ERROR) << "caBLEv2 handshake failed"; - OnError(); - return; - } - - FIDO_LOG(DEBUG) << "caBLEv2 handshake successful"; - websocket_client_->Write(response); - crypter_ = std::move(result->first); - getinfo_response_bytes_ = std::move(result->second); - state_ = State::kHandshakeProcessed; - - MaybeFlushPendingMessage(); + ProcessHandshake(*data); break; } @@ -179,18 +279,34 @@ void FidoTunnelDevice::OnTunnelData( // information. std::vector<uint8_t> decrypted; if (!crypter_->Decrypt(*data, &decrypted)) { - FIDO_LOG(ERROR) << "decryption failed for caBLE pairing message"; + FIDO_LOG(ERROR) << GetId() + << ": decryption failed for caBLE pairing message"; OnError(); return; } base::Optional<cbor::Value> payload = DecodePaddedCBORMap(decrypted); - if (!payload) { - FIDO_LOG(ERROR) << "decode failed for caBLE pairing message"; + if (!payload || !payload->is_map()) { + FIDO_LOG(ERROR) << GetId() + << ": decode failed for caBLE pairing message"; OnError(); return; } - // TODO: pairing not yet handled. + // The map may be empty if the peer doesn't wish to send pairing + // information. + if (!payload->GetMap().empty()) { + QRInfo& info = absl::get<QRInfo>(info_); + base::Optional<std::unique_ptr<Pairing>> maybe_pairing = + Pairing::Parse(*payload, info.tunnel_server_domain, + info.local_identity_seed, *info.handshake_hash); + if (!maybe_pairing) { + FIDO_LOG(ERROR) << GetId() << ": invalid caBLE pairing message"; + OnError(); + return; + } + + std::move(info.pairing_callback).Run(std::move(*maybe_pairing)); + } state_ = State::kReady; break; @@ -204,7 +320,7 @@ void FidoTunnelDevice::OnTunnelData( std::vector<uint8_t> plaintext; if (!crypter_->Decrypt(*data, &plaintext)) { - FIDO_LOG(ERROR) << "decryption failed for caBLE message"; + FIDO_LOG(ERROR) << GetId() << ": decryption failed for caBLE message"; OnError(); return; } @@ -215,6 +331,57 @@ void FidoTunnelDevice::OnTunnelData( } } +void FidoTunnelDevice::ProcessHandshake(base::span<const uint8_t> data) { + DCHECK(state_ == State::kWaitingForEID || state_ == State::kConnected); + + std::vector<uint8_t> response; + base::Optional<ResponderResult> result; + + if (auto* info = absl::get_if<QRInfo>(&info_)) { + base::Optional<ResponderResult> inner_result(cablev2::RespondToHandshake( + info->psk, info->eid, info->local_identity_seed, base::nullopt, data, + &response)); + if (inner_result) { + result.emplace(std::move(*inner_result)); + } + state_ = State::kHandshakeProcessed; + } else if (auto* info = absl::get_if<PairedInfo>(&info_)) { + if (!info->eid) { + DCHECK_EQ(state_, State::kConnected); + state_ = State::kWaitingForEID; + info->handshake_message = fido_parsing_utils::Materialize(data); + return; + } + + base::Optional<ResponderResult> inner_result( + cablev2::RespondToHandshake(*info->psk, *info->eid, + /*local_identity=*/base::nullopt, + info->peer_identity, data, &response)); + if (inner_result) { + result.emplace(std::move(*inner_result)); + } + state_ = State::kReady; + } else { + CHECK(false); + } + + if (!result || result->getinfo_bytes.empty()) { + FIDO_LOG(ERROR) << GetId() << ": caBLEv2 handshake failed"; + OnError(); + return; + } + + FIDO_LOG(DEBUG) << GetId() << ": caBLEv2 handshake successful"; + websocket_client_->Write(response); + if (auto* info = absl::get_if<QRInfo>(&info_)) { + info->handshake_hash = result->handshake_hash; + } + crypter_ = std::move(result->crypter); + getinfo_response_bytes_ = std::move(result->getinfo_bytes); + + MaybeFlushPendingMessage(); +} + void FidoTunnelDevice::OnError() { state_ = State::kError; websocket_client_.reset(); @@ -238,7 +405,8 @@ void FidoTunnelDevice::MaybeFlushPendingMessage() { reply.push_back(static_cast<uint8_t>(CtapDeviceResponseCode::kSuccess)); reply.insert(reply.end(), getinfo_response_bytes_.begin(), getinfo_response_bytes_.end()); - std::move(callback_).Run(std::move(reply)); + base::SequencedTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::BindOnce(std::move(callback_), std::move(reply))); } else if (crypter_->Encrypt(&pending)) { websocket_client_->Write(pending); } diff --git a/chromium/device/fido/cable/fido_tunnel_device.h b/chromium/device/fido/cable/fido_tunnel_device.h index 6e9e8be8d14..df878df6c02 100644 --- a/chromium/device/fido/cable/fido_tunnel_device.h +++ b/chromium/device/fido/cable/fido_tunnel_device.h @@ -7,11 +7,12 @@ #include <vector> +#include "base/callback_forward.h" #include "base/sequence_checker.h" -#include "device/fido/cable/cable_discovery_data.h" -#include "device/fido/cable/v2_handshake.h" +#include "device/fido/cable/v2_constants.h" #include "device/fido/cable/websocket_adapter.h" #include "device/fido/fido_device.h" +#include "third_party/abseil-cpp/absl/types/variant.h" namespace network { namespace mojom { @@ -24,15 +25,30 @@ namespace cablev2 { class Crypter; class WebSocketAdapter; +struct Pairing; class COMPONENT_EXPORT(DEVICE_FIDO) FidoTunnelDevice : public FidoDevice { public: + // This constructor is used for QR-initiated connections. + FidoTunnelDevice( + network::mojom::NetworkContext* network_context, + base::OnceCallback<void(std::unique_ptr<Pairing>)> pairing_callback, + base::span<const uint8_t> secret, + base::span<const uint8_t, kQRSeedSize> local_identity_seed, + const CableEidArray& eid, + const CableEidArray& decrypted_eid); + + // This constructor is used for pairing-initiated connections. FidoTunnelDevice(network::mojom::NetworkContext* network_context, - const CableDiscoveryData::V2Data& v2data, - const CableEidArray& eid, - const CableEidArray& decrypted_eid); + std::unique_ptr<Pairing> pairing); + ~FidoTunnelDevice() override; + // MatchEID is only valid for a pairing-initiated connection. It returns true + // if the given |eid| matched this pending tunnel and thus this device is now + // ready. + bool MatchEID(const CableEidArray& eid); + // FidoDevice: CancelToken DeviceTransact(std::vector<uint8_t> command, DeviceCallback callback) override; @@ -45,20 +61,51 @@ class COMPONENT_EXPORT(DEVICE_FIDO) FidoTunnelDevice : public FidoDevice { enum class State { kConnecting, kConnected, + kWaitingForEID, kHandshakeProcessed, kReady, kError, }; - void OnTunnelReady(bool ok, base::Optional<uint8_t> shard_id); + struct QRInfo { + QRInfo(); + ~QRInfo(); + QRInfo(const QRInfo&) = delete; + QRInfo& operator=(const QRInfo&) = delete; + + CableEidArray eid; + std::array<uint8_t, 32> psk; + base::OnceCallback<void(std::unique_ptr<Pairing>)> pairing_callback; + std::array<uint8_t, kQRSeedSize> local_identity_seed; + uint32_t tunnel_server_domain; + base::Optional<HandshakeHash> handshake_hash; + }; + + struct PairedInfo { + PairedInfo(); + ~PairedInfo(); + PairedInfo(const PairedInfo&) = delete; + PairedInfo& operator=(const PairedInfo&) = delete; + + std::array<uint8_t, 32> eid_encryption_key; + std::array<uint8_t, kP256X962Length> peer_identity; + std::vector<uint8_t> secret; + base::Optional<CableEidArray> eid; + base::Optional<std::array<uint8_t, 32>> psk; + base::Optional<std::vector<uint8_t>> handshake_message; + }; + + void OnTunnelReady( + bool ok, + base::Optional<std::array<uint8_t, kRoutingIdSize>> routing_id); void OnTunnelData(base::Optional<base::span<const uint8_t>> data); + void ProcessHandshake(base::span<const uint8_t> data); void OnError(); void MaybeFlushPendingMessage(); State state_ = State::kConnecting; - std::array<uint8_t, 8> id_; - const CableDiscoveryData::V2Data v2data_; - cablev2::NonceAndEID nonce_and_eid_; + absl::variant<QRInfo, PairedInfo> info_; + const std::array<uint8_t, 8> id_; std::unique_ptr<WebSocketAdapter> websocket_client_; std::unique_ptr<Crypter> crypter_; std::vector<uint8_t> getinfo_response_bytes_; diff --git a/chromium/device/fido/cable/noise.cc b/chromium/device/fido/cable/noise.cc index 7926500005f..a2bdf416e41 100644 --- a/chromium/device/fido/cable/noise.cc +++ b/chromium/device/fido/cable/noise.cc @@ -124,6 +124,10 @@ base::Optional<std::vector<uint8_t>> Noise::DecryptAndHash( return plaintext; } +std::array<uint8_t, 32> Noise::handshake_hash() const { + return h_; +} + void Noise::MixHashPoint(const EC_POINT* point) { uint8_t x962[kP256X962Length]; bssl::UniquePtr<EC_GROUP> p256( diff --git a/chromium/device/fido/cable/noise.h b/chromium/device/fido/cable/noise.h index 0372f452b82..2cd584074cf 100644 --- a/chromium/device/fido/cable/noise.h +++ b/chromium/device/fido/cable/noise.h @@ -41,6 +41,7 @@ class COMPONENT_EXPORT(DEVICE_FIDO) Noise { std::vector<uint8_t> EncryptAndHash(base::span<const uint8_t> plaintext); base::Optional<std::vector<uint8_t>> DecryptAndHash( base::span<const uint8_t> ciphertext); + std::array<uint8_t, 32> handshake_hash() const; // MaxHashPoint calls |MixHash| with the uncompressed, X9.62 serialization of // |point|. diff --git a/chromium/device/fido/cable/v2_authenticator.cc b/chromium/device/fido/cable/v2_authenticator.cc new file mode 100644 index 00000000000..8e4225fe74c --- /dev/null +++ b/chromium/device/fido/cable/v2_authenticator.cc @@ -0,0 +1,887 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "device/fido/cable/v2_authenticator.h" + +#include "base/memory/weak_ptr.h" +#include "base/sequence_checker.h" +#include "base/strings/string_number_conversions.h" +#include "components/cbor/diagnostic_writer.h" +#include "components/cbor/reader.h" +#include "components/cbor/values.h" +#include "components/cbor/writer.h" +#include "components/device_event_log/device_event_log.h" +#include "crypto/random.h" +#include "device/fido/cable/v2_handshake.h" +#include "device/fido/cable/websocket_adapter.h" +#include "device/fido/cbor_extract.h" +#include "device/fido/fido_constants.h" +#include "device/fido/fido_parsing_utils.h" +#include "net/base/isolation_info.h" +#include "net/cookies/site_for_cookies.h" +#include "net/traffic_annotation/network_traffic_annotation.h" +#include "services/network/public/mojom/network_context.mojom.h" +#include "third_party/boringssl/src/include/openssl/aes.h" +#include "third_party/boringssl/src/include/openssl/ec_key.h" +#include "third_party/boringssl/src/include/openssl/obj.h" + +namespace device { +namespace cablev2 { +namespace authenticator { + +using device::CtapDeviceResponseCode; +using device::CtapRequestCommand; +using device::cbor_extract::IntKey; +using device::cbor_extract::Is; +using device::cbor_extract::Map; +using device::cbor_extract::StepOrByte; +using device::cbor_extract::Stop; +using device::cbor_extract::StringKey; + +namespace { + +constexpr net::NetworkTrafficAnnotationTag kTrafficAnnotation = + net::DefineNetworkTrafficAnnotation("cablev2_websocket_from_authenticator", + R"(semantics { + sender: "Phone as a Security Key" + description: + "Chrome on a phone can communicate with other devices for the " + "purpose of using the phone as a security key. This WebSocket " + "connection is made to a Google service that aids in the exchange " + "of data with the other device. The service carries only " + "end-to-end encrypted data where the keys are shared directly " + "between the two devices via QR code and Bluetooth broadcast." + trigger: + "The user scans a QR code, displayed on the other device, and " + "confirms their desire to communicate with it." + data: "Only encrypted data that the service does not have the keys " + "for." + destination: GOOGLE_OWNED_SERVICE + } + policy { + cookies_allowed: NO + setting: "Not controlled by a setting because the operation is " + "triggered by significant user action." + policy_exception_justification: + "No policy provided because the operation is triggered by " + " significant user action. No background activity occurs." + })"); + +// kTunnelServer is the hardcoded tunnel server that phones will use for network +// communication. This specifies a Google service and the short domain need is +// necessary to fit within a BLE advert. +constexpr uint32_t kTunnelServer = device::cablev2::tunnelserver::EncodeDomain( + "xyi3", + device::cablev2::tunnelserver::TLD::COM); + +struct MakeCredRequest { + const std::vector<uint8_t>* client_data_hash; + const std::string* rp_id; + const std::vector<uint8_t>* user_id; + const cbor::Value::ArrayValue* cred_params; + const cbor::Value::ArrayValue* excluded_credentials; + const std::string* origin; + const std::vector<uint8_t>* challenge; +}; + +static constexpr StepOrByte<MakeCredRequest> kMakeCredParseSteps[] = { + // clang-format off + ELEMENT(Is::kRequired, MakeCredRequest, client_data_hash), + IntKey<MakeCredRequest>(1), + + Map<MakeCredRequest>(), + IntKey<MakeCredRequest>(2), + ELEMENT(Is::kRequired, MakeCredRequest, rp_id), + StringKey<MakeCredRequest>(), 'i', 'd', '\0', + Stop<MakeCredRequest>(), + + Map<MakeCredRequest>(), + IntKey<MakeCredRequest>(3), + ELEMENT(Is::kRequired, MakeCredRequest, user_id), + StringKey<MakeCredRequest>(), 'i', 'd', '\0', + Stop<MakeCredRequest>(), + + ELEMENT(Is::kRequired, MakeCredRequest, cred_params), + IntKey<MakeCredRequest>(4), + ELEMENT(Is::kOptional, MakeCredRequest, excluded_credentials), + IntKey<MakeCredRequest>(5), + + // TODO: remove once the FIDO API can handle clientDataJSON + Map<MakeCredRequest>(), + IntKey<MakeCredRequest>(6), + Map<MakeCredRequest>(), + StringKey<MakeCredRequest>(), + 'g', 'o', 'o', 'g', 'l', 'e', 'A', 'n', 'd', 'r', 'o', 'i', 'd', + 'C', 'l', 'i', 'e', 'n', 't', 'D', 'a', 't', 'a', '\0', + ELEMENT(Is::kRequired, MakeCredRequest, origin), + IntKey<MakeCredRequest>(2), + + ELEMENT(Is::kRequired, MakeCredRequest, challenge), + IntKey<MakeCredRequest>(3), + Stop<MakeCredRequest>(), + Stop<MakeCredRequest>(), + + Stop<MakeCredRequest>(), + // clang-format on +}; + +struct AttestationObject { + const std::string* fmt; + const std::vector<uint8_t>* auth_data; + const cbor::Value* statement; +}; + +static constexpr StepOrByte<AttestationObject> kAttObjParseSteps[] = { + // clang-format off + ELEMENT(Is::kRequired, AttestationObject, fmt), + StringKey<AttestationObject>(), 'f', 'm', 't', '\0', + + ELEMENT(Is::kRequired, AttestationObject, auth_data), + StringKey<AttestationObject>(), 'a', 'u', 't', 'h', 'D', 'a', 't', 'a', + '\0', + + ELEMENT(Is::kRequired, AttestationObject, statement), + StringKey<AttestationObject>(), 'a', 't', 't', 'S', 't', 'm', 't', '\0', + Stop<AttestationObject>(), + // clang-format on +}; + +struct GetAssertionRequest { + const std::string* rp_id; + const std::vector<uint8_t>* client_data_hash; + const cbor::Value::ArrayValue* allowed_credentials; + const std::string* origin; + const std::vector<uint8_t>* challenge; +}; + +static constexpr StepOrByte<GetAssertionRequest> kGetAssertionParseSteps[] = { + // clang-format off + ELEMENT(Is::kRequired, GetAssertionRequest, rp_id), + IntKey<GetAssertionRequest>(1), + + ELEMENT(Is::kRequired, GetAssertionRequest, client_data_hash), + IntKey<GetAssertionRequest>(2), + + ELEMENT(Is::kOptional, GetAssertionRequest, allowed_credentials), + IntKey<GetAssertionRequest>(3), + + // TODO: remove once the FIDO API can handle clientDataJSON + Map<GetAssertionRequest>(), + IntKey<GetAssertionRequest>(4), + Map<GetAssertionRequest>(), + StringKey<GetAssertionRequest>(), + 'g', 'o', 'o', 'g', 'l', 'e', 'A', 'n', 'd', 'r', 'o', 'i', 'd', + 'C', 'l', 'i', 'e', 'n', 't', 'D', 'a', 't', 'a', '\0', + ELEMENT(Is::kRequired, GetAssertionRequest, origin), + IntKey<GetAssertionRequest>(2), + + ELEMENT(Is::kRequired, GetAssertionRequest, challenge), + IntKey<GetAssertionRequest>(3), + Stop<GetAssertionRequest>(), + Stop<GetAssertionRequest>(), + + Stop<GetAssertionRequest>(), + // clang-format on +}; + +// BuildGetInfoResponse returns a CBOR-encoded getInfo response. +std::vector<uint8_t> BuildGetInfoResponse() { + std::array<uint8_t, device::kAaguidLength> aaguid{}; + std::vector<cbor::Value> versions; + versions.emplace_back("FIDO_2_0"); + std::vector<cbor::Value> extensions; + extensions.emplace_back(device::kExtensionAndroidClientData); + // TODO: should be based on whether a screen-lock is enabled. + cbor::Value::MapValue options; + options.emplace("uv", true); + + cbor::Value::MapValue response_map; + response_map.emplace(1, std::move(versions)); + response_map.emplace(2, std::move(extensions)); + response_map.emplace(3, aaguid); + response_map.emplace(4, std::move(options)); + + return cbor::Writer::Write(cbor::Value(std::move(response_map))).value(); +} + +std::array<uint8_t, device::cablev2::kNonceSize> RandomNonce() { + std::array<uint8_t, device::cablev2::kNonceSize> ret; + crypto::RandBytes(ret); + return ret; +} + +using GeneratePairingDataCallback = base::OnceCallback<std::vector<uint8_t>( + base::span<const uint8_t, device::kP256X962Length> peer_public_key_x962, + device::cablev2::HandshakeHash)>; + +// TunnelTransport is a transport that uses WebSockets to talk to a cloud +// service and uses BLE adverts to show proximity. +class TunnelTransport : public Transport { + public: + TunnelTransport( + Platform* platform, + network::mojom::NetworkContext* network_context, + base::span<const uint8_t> secret, + base::span<const uint8_t, device::kP256X962Length> peer_identity, + GeneratePairingDataCallback generate_pairing_data) + : platform_(platform), + nonce_(RandomNonce()), + tunnel_id_(device::cablev2::Derive<EXTENT(tunnel_id_)>( + secret, + nonce_, + DerivedValueType::kTunnelID)), + eid_key_(device::cablev2::Derive<EXTENT(eid_key_)>( + secret, + base::span<const uint8_t>(), + device::cablev2::DerivedValueType::kEIDKey)), + network_context_(network_context), + peer_identity_(device::fido_parsing_utils::Materialize(peer_identity)), + generate_pairing_data_(std::move(generate_pairing_data)) { + DCHECK_EQ(state_, State::kNone); + + state_ = State::kConnecting; + + std::array<uint8_t, device::cablev2::kPSKSize> psk; + psk = device::cablev2::Derive<EXTENT(psk)>( + secret, nonce_, device::cablev2::DerivedValueType::kPSK); + handshaker_ = std::make_unique<device::cablev2::HandshakeInitiator>( + psk, peer_identity, /*local_identity=*/nullptr); + + websocket_client_ = std::make_unique<device::cablev2::WebSocketAdapter>( + base::BindOnce(&TunnelTransport::OnTunnelReady, base::Unretained(this)), + base::BindRepeating(&TunnelTransport::OnTunnelData, + base::Unretained(this))); + target_ = device::cablev2::tunnelserver::GetNewTunnelURL(kTunnelServer, + tunnel_id_); + } + + TunnelTransport( + Platform* platform, + network::mojom::NetworkContext* network_context, + base::span<const uint8_t> secret, + base::span<const uint8_t, device::cablev2::kClientNonceSize> client_nonce, + std::array<uint8_t, device::cablev2::kRoutingIdSize> routing_id, + base::span<const uint8_t, 16> tunnel_id, + bssl::UniquePtr<EC_KEY> local_identity) + : platform_(platform), + nonce_(RandomNonce()), + tunnel_id_(fido_parsing_utils::Materialize(tunnel_id)), + eid_key_(device::cablev2::Derive<EXTENT(eid_key_)>( + secret, + client_nonce, + device::cablev2::DerivedValueType::kEIDKey)), + network_context_(network_context) { + DCHECK_EQ(state_, State::kNone); + + state_ = State::kConnectingPaired; + + std::array<uint8_t, device::cablev2::kPSKSize> psk; + psk = device::cablev2::Derive<EXTENT(psk)>( + secret, nonce_, device::cablev2::DerivedValueType::kPSK); + handshaker_ = std::make_unique<device::cablev2::HandshakeInitiator>( + psk, /*peer_identity=*/base::nullopt, std::move(local_identity)); + + websocket_client_ = std::make_unique<device::cablev2::WebSocketAdapter>( + base::BindOnce(&TunnelTransport::OnTunnelReady, base::Unretained(this)), + base::BindRepeating(&TunnelTransport::OnTunnelData, + base::Unretained(this))); + target_ = device::cablev2::tunnelserver::GetConnectURL( + kTunnelServer, routing_id, tunnel_id); + } + + // Transport: + + void StartReading( + base::RepeatingCallback<void(base::Optional<std::vector<uint8_t>>)> + read_callback) override { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + DCHECK(!read_callback_); + + read_callback_ = std::move(read_callback); + + network_context_->CreateWebSocket( + target_, {device::kCableWebSocketProtocol}, net::SiteForCookies(), + net::IsolationInfo(), /*headers=*/{}, network::mojom::kBrowserProcessId, + /*render_frame_id=*/0, url::Origin::Create(target_), + network::mojom::kWebSocketOptionBlockAllCookies, + net::MutableNetworkTrafficAnnotationTag(kTrafficAnnotation), + websocket_client_->BindNewHandshakeClientPipe(), mojo::NullRemote(), + mojo::NullRemote()); + FIDO_LOG(DEBUG) << "Creating WebSocket to " << target_.spec(); + } + + void Write(std::vector<uint8_t> data) override { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + DCHECK_EQ(state_, kReady); + + if (!crypter_->Encrypt(&data)) { + FIDO_LOG(ERROR) << "Failed to encrypt response"; + return; + } + websocket_client_->Write(data); + } + + private: + enum State { + kNone, + kConnecting, + kConnectingPaired, + kConnected, + kConnectedPaired, + kReady, + }; + + void OnTunnelReady( + bool ok, + base::Optional<std::array<uint8_t, device::cablev2::kRoutingIdSize>> + routing_id) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + DCHECK(state_ == State::kConnecting || state_ == State::kConnectingPaired); + + if (ok && state_ == State::kConnecting && !routing_id) { + FIDO_LOG(ERROR) << "Tunnel server did not specify routing ID"; + ok = false; + } + + if (!ok) { + FIDO_LOG(ERROR) << "Failed to connect to tunnel server"; + read_callback_.Run(base::nullopt); + return; + } + + FIDO_LOG(DEBUG) << "WebSocket connection established."; + + if (state_ == State::kConnecting) { + state_ = State::kConnected; + } else { + DCHECK_EQ(state_, State::kConnectingPaired); + state_ = State::kConnectedPaired; + } + + static constexpr std::array<uint8_t, device::cablev2::kRoutingIdSize> + kZeroRoutingID = {0, 0, 0}; + const device::CableEidArray eid = + StartAdvertising(routing_id.value_or(kZeroRoutingID)); + std::vector<uint8_t> msg = + handshaker_->BuildInitialMessage(eid, BuildGetInfoResponse()); + websocket_client_->Write(msg); + } + + void OnTunnelData(base::Optional<base::span<const uint8_t>> msg) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + if (!msg) { + FIDO_LOG(DEBUG) << "WebSocket tunnel closed"; + read_callback_.Run(base::nullopt); + return; + } + + switch (state_) { + case State::kConnectedPaired: + case State::kConnected: { + base::Optional<std::pair<std::unique_ptr<device::cablev2::Crypter>, + device::cablev2::HandshakeHash>> + result = handshaker_->ProcessResponse(*msg); + handshaker_.reset(); + if (!result) { + FIDO_LOG(ERROR) << "caBLE handshake failure"; + read_callback_.Run(base::nullopt); + return; + } + FIDO_LOG(DEBUG) << "caBLE handshake complete"; + crypter_ = std::move(result->first); + + if (state_ == State::kConnected) { + std::vector<uint8_t> pairing_data = + std::move(generate_pairing_data_) + .Run(*peer_identity_, result->second); + if (!crypter_->Encrypt(&pairing_data)) { + FIDO_LOG(ERROR) << "failed to encode pairing data"; + return; + } + + websocket_client_->Write(pairing_data); + } + + state_ = State::kReady; + break; + } + + case State::kReady: { + std::vector<uint8_t> plaintext; + if (!crypter_->Decrypt(*msg, &plaintext)) { + FIDO_LOG(ERROR) << "failed to decrypt caBLE message"; + read_callback_.Run(base::nullopt); + return; + } + + read_callback_.Run(plaintext); + break; + } + + default: + NOTREACHED(); + } + } + + device::CableEidArray StartAdvertising( + std::array<uint8_t, device::cablev2::kRoutingIdSize> routing_id) { + const device::cablev2::eid::Components components{ + .tunnel_server_domain = kTunnelServer, + .routing_id = routing_id, + .nonce = nonce_, + }; + const device::CableEidArray eid_plaintext = + device::cablev2::eid::FromComponents(components); + + AES_KEY key; + CHECK(AES_set_encrypt_key(eid_key_.data(), + /*bits=*/8 * eid_key_.size(), &key) == 0); + std::array<uint8_t, AES_BLOCK_SIZE> eid; + static_assert(EXTENT(eid_plaintext) == AES_BLOCK_SIZE, + "EIDs are not AES blocks"); + AES_encrypt(/*in=*/eid_plaintext.data(), /*out=*/eid.data(), &key); + + ble_advert_ = platform_->SendBLEAdvert(eid); + return eid; + } + + Platform* const platform_; + State state_ = State::kNone; + const std::array<uint8_t, kNonceSize> nonce_; + const std::array<uint8_t, kTunnelIdSize> tunnel_id_; + const std::array<uint8_t, kEIDKeySize> eid_key_; + std::unique_ptr<WebSocketAdapter> websocket_client_; + std::unique_ptr<HandshakeInitiator> handshaker_; + std::unique_ptr<Crypter> crypter_; + network::mojom::NetworkContext* const network_context_; + const base::Optional<std::array<uint8_t, kP256X962Length>> peer_identity_; + GeneratePairingDataCallback generate_pairing_data_; + GURL target_; + std::unique_ptr<Platform::BLEAdvert> ble_advert_; + base::RepeatingCallback<void(base::Optional<std::vector<uint8_t>>)> + read_callback_; + + SEQUENCE_CHECKER(sequence_checker_); +}; + +class CTAP2Processor : public Transaction { + public: + CTAP2Processor(std::unique_ptr<Transport> transport, + std::unique_ptr<Platform> platform, + Transaction::CompleteCallback complete_callback) + : transport_(std::move(transport)), + platform_(std::move(platform)), + complete_callback_(std::move(complete_callback)) { + transport_->StartReading( + base::BindRepeating(&CTAP2Processor::OnData, base::Unretained(this))); + } + + private: + void OnData(base::Optional<std::vector<uint8_t>> msg) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + if (!msg) { + FIDO_LOG(ERROR) << "Closing transaction due to transport EOF"; + std::move(complete_callback_).Run(); + return; + } + + base::Optional<std::vector<uint8_t>> response = ProcessCTAPMessage(*msg); + if (!response) { + // Fatal error. + // TODO: need to signal this to the UI. + std::move(complete_callback_).Run(); + return; + } + + if (response->empty()) { + // Response is pending. + return; + } + + transport_->Write(std::move(*response)); + } + + base::Optional<std::vector<uint8_t>> ProcessCTAPMessage( + base::span<const uint8_t> message_bytes) { + if (message_bytes.empty()) { + return base::nullopt; + } + const auto command = message_bytes[0]; + const auto cbor_bytes = message_bytes.subspan(1); + + base::Optional<cbor::Value> payload; + if (!cbor_bytes.empty()) { + payload = cbor::Reader::Read(cbor_bytes); + if (!payload) { + FIDO_LOG(ERROR) << "CBOR decoding failed for " + << base::HexEncode(cbor_bytes); + return base::nullopt; + } + FIDO_LOG(DEBUG) << "<- (" << base::HexEncode(&command, 1) << ") " + << cbor::DiagnosticWriter::Write(*payload); + } else { + FIDO_LOG(DEBUG) << "<- (" << base::HexEncode(&command, 1) + << ") <no payload>"; + } + + switch (command) { + case static_cast<uint8_t>( + device::CtapRequestCommand::kAuthenticatorGetInfo): { + if (payload) { + FIDO_LOG(ERROR) << "getInfo command incorrectly contained payload"; + return base::nullopt; + } + + base::Optional<std::vector<uint8_t>> response = BuildGetInfoResponse(); + if (!response) { + return base::nullopt; + } + response->insert( + response->begin(), + static_cast<uint8_t>(CtapDeviceResponseCode::kSuccess)); + return response; + } + + case static_cast<uint8_t>( + device::CtapRequestCommand::kAuthenticatorMakeCredential): { + if (!payload || !payload->is_map()) { + FIDO_LOG(ERROR) << "Invalid makeCredential payload"; + return base::nullopt; + } + + MakeCredRequest make_cred_request; + if (!device::cbor_extract::Extract<MakeCredRequest>( + &make_cred_request, kMakeCredParseSteps, payload->GetMap())) { + FIDO_LOG(ERROR) << "Failed to parse makeCredential request"; + return base::nullopt; + } + + std::vector<int> algorithms; + if (!device::cbor_extract::ForEachPublicKeyEntry( + *make_cred_request.cred_params, cbor::Value("alg"), + base::BindRepeating( + [](std::vector<int>* out, + const cbor::Value& value) -> bool { + if (!value.is_integer()) { + return false; + } + const int64_t alg = value.GetInteger(); + + if (alg > std::numeric_limits<int>::max() || + alg < std::numeric_limits<int>::min()) { + return false; + } + out->push_back(static_cast<int>(alg)); + return true; + }, + base::Unretained(&algorithms)))) { + return base::nullopt; + } + + std::vector<std::vector<uint8_t>> excluded_credential_ids; + if (make_cred_request.excluded_credentials && + !device::cbor_extract::ForEachPublicKeyEntry( + *make_cred_request.excluded_credentials, cbor::Value("id"), + base::BindRepeating( + [](std::vector<std::vector<uint8_t>>* out, + const cbor::Value& value) -> bool { + if (!value.is_bytestring()) { + return false; + } + out->push_back(value.GetBytestring()); + return true; + }, + base::Unretained(&excluded_credential_ids)))) { + return base::nullopt; + } + + // TODO: plumb the rk flag through once GmsCore supports resident + // keys. This will require support for optional maps in |Extract|. + platform_->MakeCredential( + *make_cred_request.origin, *make_cred_request.rp_id, + *make_cred_request.challenge, *make_cred_request.user_id, + algorithms, excluded_credential_ids, + /*resident_key_required=*/false, + base::BindOnce(&CTAP2Processor::OnMakeCredentialResponse, + weak_factory_.GetWeakPtr())); + return std::vector<uint8_t>(); + } + + case static_cast<uint8_t>( + device::CtapRequestCommand::kAuthenticatorGetAssertion): { + if (!payload || !payload->is_map()) { + FIDO_LOG(ERROR) << "Invalid makeCredential payload"; + return base::nullopt; + } + GetAssertionRequest get_assertion_request; + if (!device::cbor_extract::Extract<GetAssertionRequest>( + &get_assertion_request, kGetAssertionParseSteps, + payload->GetMap())) { + FIDO_LOG(ERROR) << "Failed to parse getAssertion request"; + return base::nullopt; + } + + std::vector<std::vector<uint8_t>> allowed_credential_ids; + if (get_assertion_request.allowed_credentials && + !device::cbor_extract::ForEachPublicKeyEntry( + *get_assertion_request.allowed_credentials, cbor::Value("id"), + base::BindRepeating( + [](std::vector<std::vector<uint8_t>>* out, + const cbor::Value& value) -> bool { + if (!value.is_bytestring()) { + return false; + } + out->push_back(value.GetBytestring()); + return true; + }, + base::Unretained(&allowed_credential_ids)))) { + return base::nullopt; + } + + platform_->GetAssertion( + *get_assertion_request.origin, *get_assertion_request.rp_id, + *get_assertion_request.challenge, allowed_credential_ids, + base::BindOnce(&CTAP2Processor::OnGetAssertionResponse, + weak_factory_.GetWeakPtr())); + + return std::vector<uint8_t>(); + } + + default: + FIDO_LOG(ERROR) << "Received unknown command " + << static_cast<unsigned>(command); + return base::nullopt; + } + } + + void OnMakeCredentialResponse(uint32_t ctap_status, + base::span<const uint8_t> client_data_json, + base::span<const uint8_t> attestation_object) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + DCHECK_LE(ctap_status, 0xFFu); + + std::vector<uint8_t> response = {base::checked_cast<uint8_t>(ctap_status)}; + if (ctap_status == static_cast<uint8_t>(CtapDeviceResponseCode::kSuccess)) { + // TODO: pass response parameters from the Java side. + base::Optional<cbor::Value> cbor_attestation_object = + cbor::Reader::Read(attestation_object); + if (!cbor_attestation_object || !cbor_attestation_object->is_map()) { + FIDO_LOG(ERROR) << "invalid CBOR attestation object"; + return; + } + + AttestationObject attestation_object; + if (!device::cbor_extract::Extract<AttestationObject>( + &attestation_object, kAttObjParseSteps, + cbor_attestation_object->GetMap())) { + FIDO_LOG(ERROR) << "attestation object parse failed"; + return; + } + + cbor::Value::MapValue response_map; + response_map.emplace(1, base::StringPiece(*attestation_object.fmt)); + response_map.emplace( + 2, base::span<const uint8_t>(*attestation_object.auth_data)); + response_map.emplace(3, attestation_object.statement->Clone()); + response_map.emplace(device::kAndroidClientDataExtOutputKey, + client_data_json); + + base::Optional<std::vector<uint8_t>> response_payload = + cbor::Writer::Write(cbor::Value(std::move(response_map))); + if (!response_payload) { + return; + } + response.insert(response.end(), response_payload->begin(), + response_payload->end()); + } + + transport_->Write(std::move(response)); + } + + void OnGetAssertionResponse(uint32_t ctap_status, + base::span<const uint8_t> client_data_json, + base::span<const uint8_t> credential_id, + base::span<const uint8_t> authenticator_data, + base::span<const uint8_t> signature) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + DCHECK_LE(ctap_status, 0xFFu); + std::vector<uint8_t> response = {base::checked_cast<uint8_t>(ctap_status)}; + + if (ctap_status == static_cast<uint8_t>(CtapDeviceResponseCode::kSuccess)) { + cbor::Value::MapValue credential_descriptor; + credential_descriptor.emplace("type", device::kPublicKey); + credential_descriptor.emplace("id", credential_id); + cbor::Value::ArrayValue transports; + transports.emplace_back("internal"); + transports.emplace_back("cable"); + credential_descriptor.emplace("transports", std::move(transports)); + cbor::Value::MapValue response_map; + response_map.emplace(1, std::move(credential_descriptor)); + response_map.emplace(2, authenticator_data); + response_map.emplace(3, signature); + // TODO: add user entity to support resident keys. + response_map.emplace(device::kAndroidClientDataExtOutputKey, + client_data_json); + + base::Optional<std::vector<uint8_t>> response_payload = + cbor::Writer::Write(cbor::Value(std::move(response_map))); + if (!response_payload) { + return; + } + response.insert(response.end(), response_payload->begin(), + response_payload->end()); + } + + transport_->Write(std::move(response)); + } + + const std::unique_ptr<Transport> transport_; + const std::unique_ptr<Platform> platform_; + Transaction::CompleteCallback complete_callback_; + SEQUENCE_CHECKER(sequence_checker_); + base::WeakPtrFactory<CTAP2Processor> weak_factory_{this}; +}; + +static bssl::UniquePtr<EC_KEY> IdentityKey( + base::span<const uint8_t, 32> root_secret) { + std::array<uint8_t, 32> seed; + seed = device::cablev2::Derive<EXTENT(seed)>( + root_secret, /*nonce=*/base::span<uint8_t>(), + device::cablev2::DerivedValueType::kIdentityKeySeed); + bssl::UniquePtr<EC_GROUP> p256( + EC_GROUP_new_by_curve_name(NID_X9_62_prime256v1)); + return bssl::UniquePtr<EC_KEY>( + EC_KEY_derive_from_secret(p256.get(), seed.data(), seed.size())); +} + +class PairingDataGenerator { + public: + static base::OnceCallback< + std::vector<uint8_t>(base::span<const uint8_t, device::kP256X962Length>, + device::cablev2::HandshakeHash)> + GetClosure(base::span<const uint8_t, kRootSecretSize> root_secret, + const std::string& name, + base::Optional<std::vector<uint8_t>> contact_id) { + auto* generator = + new PairingDataGenerator(root_secret, name, std::move(contact_id)); + return base::BindOnce(&PairingDataGenerator::Generate, + base::Owned(generator)); + } + + private: + PairingDataGenerator(base::span<const uint8_t, kRootSecretSize> root_secret, + const std::string& name, + base::Optional<std::vector<uint8_t>> contact_id) + : root_secret_(fido_parsing_utils::Materialize(root_secret)), + name_(name), + contact_id_(std::move(contact_id)) {} + + std::vector<uint8_t> Generate( + base::span<const uint8_t, device::kP256X962Length> peer_public_key_x962, + device::cablev2::HandshakeHash handshake_hash) { + cbor::Value::MapValue map; + + if (contact_id_) { + map.emplace(1, std::move(*contact_id_)); + + std::array<uint8_t, device::cablev2::kNonceSize> pairing_id; + crypto::RandBytes(pairing_id); + + map.emplace(2, pairing_id); + + std::array<uint8_t, 32> paired_secret; + paired_secret = device::cablev2::Derive<EXTENT(paired_secret)>( + root_secret_, pairing_id, + device::cablev2::DerivedValueType::kPairedSecret); + + map.emplace(3, paired_secret); + + bssl::UniquePtr<EC_KEY> identity_key(IdentityKey(root_secret_)); + device::CableAuthenticatorIdentityKey public_key; + CHECK_EQ( + public_key.size(), + EC_POINT_point2oct(EC_KEY_get0_group(identity_key.get()), + EC_KEY_get0_public_key(identity_key.get()), + POINT_CONVERSION_UNCOMPRESSED, public_key.data(), + public_key.size(), /*ctx=*/nullptr)); + + map.emplace(4, public_key); + map.emplace(5, name_); + + map.emplace( + 6, device::cablev2::CalculatePairingSignature( + identity_key.get(), peer_public_key_x962, handshake_hash)); + } + + std::vector<uint8_t> empty_vector; + return device::cablev2::EncodePaddedCBORMap(std::move(map)) + .value_or(empty_vector); + } + + const std::array<uint8_t, kRootSecretSize> root_secret_; + const std::string name_; + base::Optional<std::vector<uint8_t>> contact_id_; +}; + +} // namespace + +Platform::BLEAdvert::~BLEAdvert() = default; +Platform::~Platform() = default; +Transport::~Transport() = default; +Transaction::~Transaction() = default; + +std::unique_ptr<Transaction> TransactWithPlaintextTransport( + std::unique_ptr<Platform> platform, + std::unique_ptr<Transport> transport, + Transaction::CompleteCallback complete_callback) { + return std::make_unique<CTAP2Processor>( + std::move(transport), std::move(platform), std::move(complete_callback)); +} + +std::unique_ptr<Transaction> TransactFromQRCode( + std::unique_ptr<Platform> platform, + network::mojom::NetworkContext* network_context, + base::span<const uint8_t, kRootSecretSize> root_secret, + const std::string& authenticator_name, + base::span<const uint8_t, 16> qr_secret, + base::span<const uint8_t, kP256X962Length> peer_identity, + base::Optional<std::vector<uint8_t>> contact_id, + Transaction::CompleteCallback complete_callback) { + auto generate_pairing_data = PairingDataGenerator::GetClosure( + root_secret, authenticator_name, contact_id); + + Platform* const platform_ptr = platform.get(); + return std::make_unique<CTAP2Processor>( + std::make_unique<TunnelTransport>(platform_ptr, network_context, + qr_secret, peer_identity, + std::move(generate_pairing_data)), + std::move(platform), std::move(complete_callback)); +} + +std::unique_ptr<Transaction> TransactFromFCM( + std::unique_ptr<Platform> platform, + network::mojom::NetworkContext* network_context, + base::span<const uint8_t, kRootSecretSize> root_secret, + std::array<uint8_t, kRoutingIdSize> routing_id, + base::span<const uint8_t, kTunnelIdSize> tunnel_id, + base::span<const uint8_t> pairing_id, + base::span<const uint8_t, kClientNonceSize> client_nonce, + Transaction::CompleteCallback complete_callback) { + std::array<uint8_t, 32> paired_secret; + paired_secret = Derive<EXTENT(paired_secret)>( + root_secret, pairing_id, DerivedValueType::kPairedSecret); + + Platform* const platform_ptr = platform.get(); + return std::make_unique<CTAP2Processor>( + std::make_unique<TunnelTransport>(platform_ptr, network_context, + paired_secret, client_nonce, routing_id, + tunnel_id, IdentityKey(root_secret)), + std::move(platform), std::move(complete_callback)); +} + +} // namespace authenticator +} // namespace cablev2 +} // namespace device diff --git a/chromium/device/fido/cable/v2_authenticator.h b/chromium/device/fido/cable/v2_authenticator.h new file mode 100644 index 00000000000..28810230582 --- /dev/null +++ b/chromium/device/fido/cable/v2_authenticator.h @@ -0,0 +1,128 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef DEVICE_FIDO_CABLE_V2_AUTHENTICATOR_H_ +#define DEVICE_FIDO_CABLE_V2_AUTHENTICATOR_H_ + +#include <string> +#include <vector> + +#include <stdint.h> + +#include "base/callback_forward.h" +#include "base/containers/span.h" +#include "base/optional.h" +#include "device/fido/cable/v2_constants.h" +#include "device/fido/fido_constants.h" +#include "services/network/public/mojom/network_context.mojom-forward.h" + +namespace device { +namespace cablev2 { +namespace authenticator { + +// Platform abstracts the actions taken by the platform, i.e. the +// credential-store operations themselves, plus an interface for BLE +// advertising. +class Platform { + public: + // BLEAdvert represents a currently-transmitting advert. Destroying the object + // stops the transmission. + class BLEAdvert { + public: + virtual ~BLEAdvert(); + }; + + virtual ~Platform(); + + using MakeCredentialCallback = + base::OnceCallback<void(uint32_t status, + base::span<const uint8_t> client_data_json, + base::span<const uint8_t> attestation_obj)>; + using GetAssertionCallback = + base::OnceCallback<void(uint32_t status, + base::span<const uint8_t> client_data_json, + base::span<const uint8_t> cred_id, + base::span<const uint8_t> auth_data, + base::span<const uint8_t> sig)>; + + virtual void MakeCredential( + const std::string& origin, + const std::string& rp_id, + base::span<const uint8_t> challenge, + base::span<const uint8_t> user_id, + base::span<const int> algorithms, + base::span<const std::vector<uint8_t>> excluded_cred_ids, + bool resident_key_required, + MakeCredentialCallback callback) = 0; + + virtual void GetAssertion( + const std::string& origin, + const std::string& rp_id, + base::span<const uint8_t> challenge, + base::span<const std::vector<uint8_t>> allowed_cred_ids, + GetAssertionCallback callback) = 0; + + virtual std::unique_ptr<BLEAdvert> SendBLEAdvert( + base::span<uint8_t, 16> payload) = 0; +}; + +// Transport abstracts a way of transmitting to, and receiving from, the peer. +// The framing of messages must be preserved. +class Transport { + public: + virtual ~Transport(); + + // StartReading requests that the given callback be called whenever a message + // arrives from the peer. + virtual void StartReading( + base::RepeatingCallback<void(base::Optional<std::vector<uint8_t>>)> + read_callback) = 0; + virtual void Write(std::vector<uint8_t> data) = 0; +}; + +// A Transaction is a handle to an ongoing caBLEv2 transaction with a peer. +class Transaction { + public: + using CompleteCallback = base::OnceCallback<void()>; + + virtual ~Transaction(); +}; + +// TransactWithPlaintextTransport allows an arbitrary transport to be used for a +// caBLEv2 transaction. +std::unique_ptr<Transaction> TransactWithPlaintextTransport( + std::unique_ptr<Platform> platform, + std::unique_ptr<Transport> transport, + Transaction::CompleteCallback complete_callback); + +// TransactFromQRCode starts a network-based transaction based on the decoded +// contents of a QR code. +std::unique_ptr<Transaction> TransactFromQRCode( + std::unique_ptr<Platform> platform, + network::mojom::NetworkContext* network_context, + base::span<const uint8_t, kRootSecretSize> root_secret, + const std::string& authenticator_name, + // TODO: name this constant. + base::span<const uint8_t, 16> qr_secret, + base::span<const uint8_t, kP256X962Length> peer_identity, + base::Optional<std::vector<uint8_t>> contact_id, + Transaction::CompleteCallback complete_callback); + +// TransactFromQRCode starts a network-based transaction based on the decoded +// contents of a cloud message. +std::unique_ptr<Transaction> TransactFromFCM( + std::unique_ptr<Platform> platform, + network::mojom::NetworkContext* network_context, + base::span<const uint8_t, kRootSecretSize> root_secret, + std::array<uint8_t, kRoutingIdSize> routing_id, + base::span<const uint8_t, kTunnelIdSize> tunnel_id, + base::span<const uint8_t> pairing_id, + base::span<const uint8_t, kClientNonceSize> client_nonce, + Transaction::CompleteCallback complete_callback); + +} // namespace authenticator +} // namespace cablev2 +} // namespace device + +#endif // DEVICE_FIDO_CABLE_V2_AUTHENTICATOR_H_ diff --git a/chromium/device/fido/cable/v2_constants.h b/chromium/device/fido/cable/v2_constants.h new file mode 100644 index 00000000000..3d2dddf94b2 --- /dev/null +++ b/chromium/device/fido/cable/v2_constants.h @@ -0,0 +1,44 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef DEVICE_FIDO_CABLE_V2_CONSTANTS_H_ +#define DEVICE_FIDO_CABLE_V2_CONSTANTS_H_ + +namespace device { +namespace cablev2 { + +// kNonceSize is the number of bytes of nonce in the BLE advert. +constexpr size_t kNonceSize = 8; +// kClientNonceSize is the number of bytes of nonce sent by the client, via the +// tunnel server, for a pairing-based handshake. +constexpr size_t kClientNonceSize = 16; +// kRoutingIdSize is the number of bytes of routing information in the BLE +// advert. +constexpr size_t kRoutingIdSize = 3; +// kTunnelIdSize is the number of bytes of opaque tunnel ID, used to identify a +// specific tunnel to the tunnel service. +constexpr size_t kTunnelIdSize = 16; +// kEIDKeySize is the size of the AES key used to encrypt BLE adverts. +constexpr size_t kEIDKeySize = 32; +// kPSKSize is the size of the Noise pre-shared key used in handshakes. +constexpr size_t kPSKSize = 32; +// kRootSecretSize is the size of the main key maintained by authenticators. +constexpr size_t kRootSecretSize = 32; +// kQRKeySize is the size of the private key data that generates a QR code. It +// consists of a 256-bit seed value that's used to genertate the P-256 private +// key and a 128-bit secret. +constexpr size_t kQRSecretSize = 16; +constexpr size_t kQRSeedSize = 32; +constexpr size_t kQRKeySize = kQRSeedSize + kQRSecretSize; +// kCompressedPublicKeySize is the size of a compressed X9.62 public key. +constexpr size_t kCompressedPublicKeySize = + /* type byte */ 1 + /* field element */ (256 / 8); +// kQRDataSize is the size of the (unencoded) QR payload. It's a compressed +// public key followed by the QR secret. +constexpr size_t kQRDataSize = kCompressedPublicKeySize + kQRSecretSize; + +} // namespace cablev2 +} // namespace device + +#endif // DEVICE_FIDO_CABLE_V2_CONSTANTS_H_ diff --git a/chromium/device/fido/cable/v2_discovery.cc b/chromium/device/fido/cable/v2_discovery.cc new file mode 100644 index 00000000000..794f06f8370 --- /dev/null +++ b/chromium/device/fido/cable/v2_discovery.cc @@ -0,0 +1,119 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "device/fido/cable/v2_discovery.h" + +#include "base/bind.h" +#include "base/callback.h" +#include "base/strings/string_number_conversions.h" +#include "components/device_event_log/device_event_log.h" +#include "device/fido/cable/fido_tunnel_device.h" +#include "device/fido/cable/v2_handshake.h" +#include "device/fido/fido_parsing_utils.h" +#include "third_party/boringssl/src/include/openssl/aes.h" + +namespace device { +namespace cablev2 { + +Discovery::Discovery( + network::mojom::NetworkContext* network_context, + base::span<const uint8_t, kQRKeySize> qr_generator_key, + std::vector<std::unique_ptr<Pairing>> pairings, + base::Optional<base::RepeatingCallback<void(std::unique_ptr<Pairing>)>> + pairing_callback) + : FidoDeviceDiscovery( + FidoTransportProtocol::kCloudAssistedBluetoothLowEnergy), + network_context_(network_context), + local_identity_seed_(fido_parsing_utils::Materialize( + base::span<const uint8_t, kQRSeedSize>(qr_generator_key.data(), + kQRSeedSize))), + qr_secret_(fido_parsing_utils::Materialize( + base::span<const uint8_t, kQRSecretSize>( + qr_generator_key.data() + kQRSeedSize, + kQRSecretSize))), + eid_key_(Derive<EXTENT(eid_key_)>(qr_secret_, + base::span<const uint8_t>(), + DerivedValueType::kEIDKey)), + pairings_(std::move(pairings)), + pairing_callback_(std::move(pairing_callback)) { + static_assert(EXTENT(qr_generator_key) == kQRSecretSize + kQRSeedSize, ""); +} + +Discovery::~Discovery() = default; + +void Discovery::StartInternal() { + DCHECK(!started_); + + for (auto& pairing : pairings_) { + tunnels_pending_advert_.emplace_back(std::make_unique<FidoTunnelDevice>( + network_context_, std::move(pairing))); + } + pairings_.clear(); + + started_ = true; + NotifyDiscoveryStarted(true); + + std::vector<CableEidArray> pending_eids(std::move(pending_eids_)); + for (const auto& eid : pending_eids) { + OnBLEAdvertSeen("", eid); + } +} + +void Discovery::OnBLEAdvertSeen(const std::string& address, + const CableEidArray& eid) { + if (!started_) { + pending_eids_.push_back(eid); + return; + } + + if (base::Contains(observed_eids_, eid)) { + return; + } + observed_eids_.insert(eid); + + // Check whether the EID satisfies any pending tunnels. + for (std::vector<std::unique_ptr<FidoTunnelDevice>>::iterator i = + tunnels_pending_advert_.begin(); + i != tunnels_pending_advert_.end(); i++) { + if (!(*i)->MatchEID(eid)) { + continue; + } + + FIDO_LOG(DEBUG) << " (" << base::HexEncode(eid) + << " matches pending tunnel)"; + std::unique_ptr<FidoTunnelDevice> device(std::move(*i)); + tunnels_pending_advert_.erase(i); + AddDevice(std::move(device)); + return; + } + + // Check whether the EID matches a QR code. + AES_KEY aes_key; + CHECK(AES_set_decrypt_key(eid_key_.data(), + /*bits=*/8 * eid_key_.size(), &aes_key) == 0); + CableEidArray plaintext; + static_assert(EXTENT(plaintext) == AES_BLOCK_SIZE, "EIDs are not AES blocks"); + AES_decrypt(/*in=*/eid.data(), /*out=*/plaintext.data(), &aes_key); + if (cablev2::eid::IsValid(plaintext)) { + FIDO_LOG(DEBUG) << " (" << base::HexEncode(eid) << " matches QR code)"; + AddDevice(std::make_unique<cablev2::FidoTunnelDevice>( + network_context_, + base::BindOnce(&Discovery::AddPairing, weak_factory_.GetWeakPtr()), + qr_secret_, local_identity_seed_, eid, plaintext)); + return; + } + + FIDO_LOG(DEBUG) << " (" << base::HexEncode(eid) << ": no v2 match)"; +} + +void Discovery::AddPairing(std::unique_ptr<Pairing> pairing) { + if (!pairing_callback_) { + return; + } + + pairing_callback_->Run(std::move(pairing)); +} + +} // namespace cablev2 +} // namespace device diff --git a/chromium/device/fido/cable/v2_discovery.h b/chromium/device/fido/cable/v2_discovery.h new file mode 100644 index 00000000000..a3607cd7545 --- /dev/null +++ b/chromium/device/fido/cable/v2_discovery.h @@ -0,0 +1,74 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef DEVICE_FIDO_CABLE_V2_DISCOVERY_H_ +#define DEVICE_FIDO_CABLE_V2_DISCOVERY_H_ + +#include <memory> +#include <vector> + +#include "base/callback.h" +#include "base/component_export.h" +#include "base/containers/flat_set.h" +#include "base/containers/span.h" +#include "base/memory/weak_ptr.h" +#include "base/optional.h" +#include "device/fido/cable/cable_discovery_data.h" +#include "device/fido/cable/v2_constants.h" +#include "device/fido/fido_device_discovery.h" +#include "services/network/public/mojom/network_context.mojom-forward.h" + +namespace device { +namespace cablev2 { + +struct Pairing; +class FidoTunnelDevice; + +// Discovery creates caBLEv2 devices, either based on |pairings|, or when a BLE +// advert is seen that matches |qr_generator_key|. It does not actively scan for +// BLE adverts itself. Rather it depends on |OnBLEAdvertSeen| getting called. +class COMPONENT_EXPORT(DEVICE_FIDO) Discovery + : public FidoDeviceDiscovery, + public FidoDeviceDiscovery::BLEObserver { + public: + Discovery( + network::mojom::NetworkContext* network_context, + base::span<const uint8_t, kQRKeySize> qr_generator_key, + std::vector<std::unique_ptr<Pairing>> pairings, + // pairing_callback will be called when a QR-initiated connection receives + // pairing information from the peer. + base::Optional<base::RepeatingCallback<void(std::unique_ptr<Pairing>)>> + pairing_callback); + ~Discovery() override; + Discovery(const Discovery&) = delete; + Discovery& operator=(const Discovery&) = delete; + + // FidoDeviceDiscovery: + void StartInternal() override; + + // BLEObserver: + void OnBLEAdvertSeen(const std::string& address, + const CableEidArray& eid) override; + + private: + void AddPairing(std::unique_ptr<Pairing> pairing); + + network::mojom::NetworkContext* const network_context_; + const std::array<uint8_t, kQRSeedSize> local_identity_seed_; + const std::array<uint8_t, kQRSecretSize> qr_secret_; + const std::array<uint8_t, kEIDKeySize> eid_key_; + std::vector<std::unique_ptr<Pairing>> pairings_; + const base::Optional<base::RepeatingCallback<void(std::unique_ptr<Pairing>)>> + pairing_callback_; + std::vector<std::unique_ptr<FidoTunnelDevice>> tunnels_pending_advert_; + base::flat_set<CableEidArray> observed_eids_; + bool started_ = false; + std::vector<CableEidArray> pending_eids_; + base::WeakPtrFactory<Discovery> weak_factory_{this}; +}; + +} // namespace cablev2 +} // namespace device + +#endif // DEVICE_FIDO_CABLE_V2_DISCOVERY_H_ diff --git a/chromium/device/fido/cable/v2_handshake.cc b/chromium/device/fido/cable/v2_handshake.cc index 41c12e9e66a..b2be4ffb7bd 100644 --- a/chromium/device/fido/cable/v2_handshake.cc +++ b/chromium/device/fido/cable/v2_handshake.cc @@ -7,9 +7,11 @@ #include <array> #include <type_traits> +#include "base/base64url.h" #include "base/bits.h" #include "base/numerics/safe_math.h" #include "base/strings/string_number_conversions.h" +#include "base/strings/string_piece.h" #include "components/cbor/reader.h" #include "components/cbor/values.h" #include "components/cbor/writer.h" @@ -23,10 +25,15 @@ #include "third_party/boringssl/src/include/openssl/ec_key.h" #include "third_party/boringssl/src/include/openssl/ecdh.h" #include "third_party/boringssl/src/include/openssl/hkdf.h" +#include "third_party/boringssl/src/include/openssl/hmac.h" +#include "third_party/boringssl/src/include/openssl/mem.h" #include "third_party/boringssl/src/include/openssl/obj.h" #include "third_party/boringssl/src/include/openssl/sha.h" #include "url/gurl.h" +namespace device { +namespace cablev2 { + namespace { // Maximum value of a sequence number. Exceeding this causes all operations to @@ -47,16 +54,39 @@ bool ConstructNonce(uint32_t counter, base::span<uint8_t, 12> out_nonce) { return true; } -} // namespace +std::array<uint8_t, 32> PairingSignature( + const EC_KEY* identity_key, + base::span<const uint8_t, kP256X962Length> peer_public_key_x962, + base::span<const uint8_t, std::tuple_size<HandshakeHash>::value> + handshake_hash) { + const EC_GROUP* const p256 = EC_KEY_get0_group(identity_key); + bssl::UniquePtr<EC_POINT> peer_public_key(EC_POINT_new(p256)); + CHECK(EC_POINT_oct2point(p256, peer_public_key.get(), + peer_public_key_x962.data(), + peer_public_key_x962.size(), + /*ctx=*/nullptr)); + uint8_t shared_secret[32]; + CHECK(ECDH_compute_key(shared_secret, sizeof(shared_secret), + peer_public_key.get(), identity_key, + /*kdf=*/nullptr) == sizeof(shared_secret)); + + std::array<uint8_t, SHA256_DIGEST_LENGTH> expected_signature; + unsigned expected_signature_len = 0; + CHECK(HMAC(EVP_sha256(), /*key=*/shared_secret, sizeof(shared_secret), + handshake_hash.data(), handshake_hash.size(), + expected_signature.data(), &expected_signature_len) != nullptr); + CHECK_EQ(expected_signature_len, EXTENT(expected_signature)); + return expected_signature; +} -namespace device { -namespace cablev2 { +} // namespace namespace tunnelserver { -GURL GetURL(uint32_t domain, Action action, base::span<const uint8_t, 16> id) { - std::string ret = "wss://"; +std::string DecodeDomain(uint32_t domain) { static const char kBase32Chars[33] = "abcdefghijklmnopqrstuvwxyz234567"; + + std::string ret; ret.push_back(kBase32Chars[(domain >> 17) & 0x1f]); ret.push_back(kBase32Chars[(domain >> 12) & 0x1f]); ret.push_back(kBase32Chars[(domain >> 7) & 0x1f]); @@ -66,72 +96,113 @@ GURL GetURL(uint32_t domain, Action action, base::span<const uint8_t, 16> id) { static const char kTLDs[4][5] = {"com", "org", "net", "info"}; ret += kTLDs[domain & 3]; - switch (action) { - case Action::kNew: - ret += "/cable/new/"; - break; - case Action::kConnect: - ret += "/cable/connect/"; - break; - } + return ret; +} + +GURL GetNewTunnelURL(uint32_t domain, base::span<const uint8_t, 16> id) { + std::string ret = "wss://" + DecodeDomain(domain) + "/cable/new/"; + + ret += base::HexEncode(id); + const GURL url(ret); + DCHECK(url.is_valid()); + return url; +} +GURL GetConnectURL(uint32_t domain, + std::array<uint8_t, kRoutingIdSize> routing_id, + base::span<const uint8_t, 16> id) { + std::string ret = "wss://" + DecodeDomain(domain) + "/cable/connect/"; + + ret += base::HexEncode(routing_id); + ret += "/"; ret += base::HexEncode(id); + const GURL url(ret); DCHECK(url.is_valid()); return url; } + +GURL GetContactURL(const std::string& tunnel_server, + base::span<const uint8_t> contact_id) { + std::string contact_id_base64; + base::Base64UrlEncode( + base::StringPiece(reinterpret_cast<const char*>(contact_id.data()), + contact_id.size()), + base::Base64UrlEncodePolicy::OMIT_PADDING, &contact_id_base64); + GURL ret(std::string("wss://") + tunnel_server + "/cable/contact/" + + contact_id_base64); + DCHECK(ret.is_valid()); + return ret; +} + } // namespace tunnelserver namespace eid { CableEidArray FromComponents(const Components& components) { DCHECK_EQ(components.tunnel_server_domain >> 22, 0u); - DCHECK_EQ(components.shard_id >> 6, 0); - const uint32_t header = components.tunnel_server_domain | - (static_cast<uint32_t>(components.shard_id) << 22); CableEidArray eid; - constexpr size_t eid_size = - std::tuple_size<std::remove_reference<decltype(eid)>::type>::value; - memset(eid.data(), 0, eid.size()); - static_assert(eid_size >= sizeof(header), "EID too small"); - memcpy(eid.data(), &header, sizeof(header)); - static_assert(eid_size == 6 + kNonceSize, "EID wrong size"); - static_assert( - std::tuple_size<decltype(components.nonce)>::value == kNonceSize, - "Nonce wrong size"); - memcpy(eid.data() + 6, components.nonce.data(), kNonceSize); + static_assert(EXTENT(eid) == 16, ""); + eid[0] = components.tunnel_server_domain; + eid[1] = components.tunnel_server_domain >> 8; + eid[2] = components.tunnel_server_domain >> 16; + static_assert(EXTENT(components.routing_id) == 3, ""); + eid[3] = components.routing_id[0]; + eid[4] = components.routing_id[1]; + eid[5] = components.routing_id[2]; + eid[6] = 0; + eid[7] = 0; + static_assert(EXTENT(eid) == 8 + kNonceSize, ""); + memcpy(&eid[8], components.nonce.data(), kNonceSize); + return eid; } bool IsValid(const CableEidArray& eid) { - static_assert( - std::tuple_size<std::remove_reference<decltype(eid)>::type>::value >= 6, - "EID too small"); - return (eid[3] & 0xc0) == 0 && eid[4] == 0 && eid[5] == 0; + static_assert(EXTENT(eid) >= 8, ""); + return eid[6] == 0 && eid[7] == 0 && (eid[2] >> 6) == 0; } Components ToComponents(const CableEidArray& eid) { DCHECK(IsValid(eid)); - constexpr size_t eid_size = - std::tuple_size<std::remove_reference<decltype(eid)>::type>::value; Components ret; - uint32_t header; - static_assert(eid_size >= sizeof(header), "EID too small"); - memcpy(&header, eid.data(), sizeof(header)); - ret.shard_id = (header >> 22) & 0x3f; - ret.tunnel_server_domain = header & 0x3fffff; - static_assert(eid_size == 6 + std::tuple_size<decltype(ret.nonce)>::value, - "EID too small"); - memcpy(ret.nonce.data(), eid.data() + 6, ret.nonce.size()); + static_assert(EXTENT(eid) == 16, ""); + ret.tunnel_server_domain = static_cast<uint32_t>(eid[0]) | + (static_cast<uint32_t>(eid[1]) << 8) | + ((static_cast<uint32_t>(eid[2]) & 0x3f) << 16); + ret.routing_id[0] = eid[3]; + ret.routing_id[1] = eid[4]; + ret.routing_id[2] = eid[5]; + + static_assert(EXTENT(eid) == 8 + kNonceSize, ""); + memcpy(ret.nonce.data(), &eid[8], kNonceSize); return ret; } } // namespace eid +namespace internal { + +void Derive(uint8_t* out, + size_t out_len, + base::span<const uint8_t> secret, + base::span<const uint8_t> nonce, + DerivedValueType type) { + static_assert(sizeof(DerivedValueType) <= sizeof(uint32_t), ""); + const uint32_t type32 = static_cast<uint32_t>(type); + + HKDF(out, out_len, EVP_sha256(), secret.data(), secret.size(), + /*salt=*/nonce.data(), nonce.size(), + /*info=*/reinterpret_cast<const uint8_t*>(&type32), sizeof(type32)); +} + +} // namespace internal + base::Optional<std::vector<uint8_t>> EncodePaddedCBORMap( cbor::Value::MapValue map) { + // TODO: this should pad to 1K, not 256 bytes. base::Optional<std::vector<uint8_t>> cbor_bytes = cbor::Writer::Write(cbor::Value(std::move(map))); if (!cbor_bytes) { @@ -273,15 +344,12 @@ bool Crypter::IsCounterpartyOfForTesting(const Crypter& other) const { } HandshakeInitiator::HandshakeInitiator( - base::span<const uint8_t, 32> psk_gen_key, - base::span<const uint8_t, kNonceSize> nonce, + base::span<const uint8_t, 32> psk, base::Optional<base::span<const uint8_t, kP256X962Length>> peer_identity, bssl::UniquePtr<EC_KEY> local_identity) - : local_identity_(std::move(local_identity)) { + : psk_(fido_parsing_utils::Materialize(psk)), + local_identity_(std::move(local_identity)) { DCHECK(peer_identity.has_value() ^ static_cast<bool>(local_identity_)); - HKDF(psk_.data(), psk_.size(), EVP_sha256(), psk_gen_key.data(), - psk_gen_key.size(), /*salt=*/nonce.data(), nonce.size(), - /*info=*/nullptr, 0); if (peer_identity) { peer_identity_ = fido_parsing_utils::Materialize<kP256X962Length>(*peer_identity); @@ -359,8 +427,8 @@ std::vector<uint8_t> HandshakeInitiator::BuildInitialMessage( return handshake_message; } -base::Optional<std::unique_ptr<Crypter>> HandshakeInitiator::ProcessResponse( - base::span<const uint8_t> response) { +base::Optional<std::pair<std::unique_ptr<Crypter>, HandshakeHash>> +HandshakeInitiator::ProcessResponse(base::span<const uint8_t> response) { if (response.size() < kP256X962Length) { FIDO_LOG(DEBUG) << "Handshake response truncated (" << response.size() << " bytes)"; @@ -405,15 +473,23 @@ base::Optional<std::unique_ptr<Crypter>> HandshakeInitiator::ProcessResponse( std::array<uint8_t, 32> read_key, write_key; std::tie(write_key, read_key) = noise_.traffic_keys(); - return std::make_unique<cablev2::Crypter>(read_key, write_key); + return std::make_pair(std::make_unique<cablev2::Crypter>(read_key, write_key), + noise_.handshake_hash()); } -base::Optional<std::pair<std::unique_ptr<Crypter>, std::vector<uint8_t>>> -RespondToHandshake( - base::span<const uint8_t, 32> psk_gen_key, - const NonceAndEID& nonce_and_eid, - base::Optional<base::span<const uint8_t, kCableIdentityKeySeedSize>> - identity_seed, +ResponderResult::ResponderResult(std::unique_ptr<Crypter> in_crypter, + std::vector<uint8_t> in_getinfo_bytes, + HandshakeHash in_handshake_hash) + : crypter(std::move(in_crypter)), + getinfo_bytes(std::move(in_getinfo_bytes)), + handshake_hash(in_handshake_hash) {} +ResponderResult::~ResponderResult() = default; +ResponderResult::ResponderResult(ResponderResult&&) = default; + +base::Optional<ResponderResult> RespondToHandshake( + base::span<const uint8_t, 32> psk, + base::span<const uint8_t, kCableEphemeralIdSize> eid, + base::Optional<base::span<const uint8_t, kQRSeedSize>> identity_seed, base::Optional<base::span<const uint8_t, kP256X962Length>> peer_identity, base::span<const uint8_t> in, std::vector<uint8_t>* out_response) { @@ -435,9 +511,8 @@ RespondToHandshake( } Noise noise; - uint8_t prologue[1 + kCableEphemeralIdSize]; - DCHECK_EQ(nonce_and_eid.second.size(), kCableEphemeralIdSize); - memcpy(&prologue[1], nonce_and_eid.second.data(), kCableEphemeralIdSize); + uint8_t prologue[1 + EXTENT(eid)]; + memcpy(&prologue[1], eid.data(), eid.size()); if (identity) { noise.Init(device::Noise::HandshakeType::kNKpsk0); prologue[0] = 0; @@ -450,12 +525,6 @@ RespondToHandshake( noise.MixHash(*peer_identity); } - std::array<uint8_t, 32> psk; - HKDF(psk.data(), psk.size(), EVP_sha256(), psk_gen_key.data(), - psk_gen_key.size(), - /*salt=*/nonce_and_eid.first.data(), nonce_and_eid.first.size(), - /*info=*/nullptr, 0); - noise.MixKeyAndHash(psk); noise.MixHash(peer_point_bytes); noise.MixKey(peer_point_bytes); @@ -541,9 +610,39 @@ RespondToHandshake( std::array<uint8_t, 32> read_key, write_key; std::tie(read_key, write_key) = noise.traffic_keys(); - return std::make_pair( + return base::make_optional<ResponderResult>( std::make_unique<Crypter>(read_key, write_key), - std::vector<uint8_t>(getinfo_it->second.GetBytestring())); + getinfo_it->second.GetBytestring(), noise.handshake_hash()); +} + +bool VerifyPairingSignature( + base::span<const uint8_t, kQRSeedSize> identity_seed, + base::span<const uint8_t, kP256X962Length> peer_public_key_x962, + base::span<const uint8_t, std::tuple_size<HandshakeHash>::value> + handshake_hash, + base::span<const uint8_t> signature) { + bssl::UniquePtr<EC_GROUP> p256( + EC_GROUP_new_by_curve_name(NID_X9_62_prime256v1)); + bssl::UniquePtr<EC_KEY> identity_key(EC_KEY_derive_from_secret( + p256.get(), identity_seed.data(), identity_seed.size())); + + std::array<uint8_t, SHA256_DIGEST_LENGTH> expected_signature = + PairingSignature(identity_key.get(), peer_public_key_x962, + handshake_hash); + return signature.size() == EXTENT(expected_signature) && + CRYPTO_memcmp(expected_signature.data(), signature.data(), + EXTENT(expected_signature)) == 0; +} + +std::vector<uint8_t> CalculatePairingSignature( + const EC_KEY* identity_key, + base::span<const uint8_t, kP256X962Length> peer_public_key_x962, + base::span<const uint8_t, std::tuple_size<HandshakeHash>::value> + handshake_hash) { + std::array<uint8_t, SHA256_DIGEST_LENGTH> expected_signature = + PairingSignature(identity_key, peer_public_key_x962, handshake_hash); + return std::vector<uint8_t>(expected_signature.begin(), + expected_signature.end()); } } // namespace cablev2 diff --git a/chromium/device/fido/cable/v2_handshake.h b/chromium/device/fido/cable/v2_handshake.h index 8027edb402e..3e66a497e40 100644 --- a/chromium/device/fido/cable/v2_handshake.h +++ b/chromium/device/fido/cable/v2_handshake.h @@ -16,6 +16,7 @@ #include "components/cbor/values.h" #include "device/fido/cable/cable_discovery_data.h" #include "device/fido/cable/noise.h" +#include "device/fido/cable/v2_constants.h" #include "device/fido/fido_constants.h" #include "third_party/boringssl/src/include/openssl/base.h" @@ -24,8 +25,6 @@ class GURL; namespace device { namespace cablev2 { -constexpr size_t kNonceSize = 10; - namespace tunnelserver { // Base32Ord converts |c| into its base32 value, as defined in @@ -62,26 +61,41 @@ constexpr uint32_t EncodeDomain(const char label[5], TLD tld) { tld_value; } -// Action enumerates the two possible requests that can be made of a tunnel -// server: to create a new tunnel or to connect to an existing one. -enum class Action { - kNew, - kConnect, -}; +// DecodeDomain converts a 22-bit tunnel server domain (as encoded by +// |EncodeDomain|) into a string in dotted form. +COMPONENT_EXPORT(DEVICE_FIDO) std::string DecodeDomain(uint32_t domain); + +// GetNewTunnelURL converts a 22-bit tunnel server domain (as encoded by +// |EncodeDomain|), and a tunnel ID, into a WebSockets-based URL for creating a +// new tunnel. +COMPONENT_EXPORT(DEVICE_FIDO) +GURL GetNewTunnelURL(uint32_t domain, base::span<const uint8_t, 16> id); + +// GetConnectURL converts a 22-bit tunnel server domain (as encoded by +// |EncodeDomain|), a routing-ID, and a tunnel ID, into a WebSockets-based URL +// for connecting to an existing tunnel. +COMPONENT_EXPORT(DEVICE_FIDO) +GURL GetConnectURL(uint32_t domain, + std::array<uint8_t, kRoutingIdSize> routing_id, + base::span<const uint8_t, 16> id); -// GetURL converts a 22-bit tunnel server domain (as encoded by |EncodeDomain|), -// an action, and a tunnel ID, into a WebSockets-based URL. +// GetContactURL gets a URL for contacting a previously-paired authenticator. +// The |tunnel_server| is assumed to be a valid domain name and should have been +// taken from a previous call to |DecodeDomain|. COMPONENT_EXPORT(DEVICE_FIDO) -GURL GetURL(uint32_t domain, Action action, base::span<const uint8_t, 16> id); +GURL GetContactURL(const std::string& tunnel_server, + base::span<const uint8_t> contact_id); } // namespace tunnelserver namespace eid { +// TODO(agl): this could probably be a class. + // Components contains the parts of a decrypted EID. struct Components { - uint8_t shard_id; uint32_t tunnel_server_domain; + std::array<uint8_t, kRoutingIdSize> routing_id; std::array<uint8_t, kNonceSize> nonce; }; @@ -102,6 +116,39 @@ Components ToComponents(const CableEidArray& eid); } // namespace eid +// DerivedValueType enumerates the different types of values that might be +// derived in caBLEv2 from some secret. The values this this enum are protocol +// constants and thus must not change over time. +enum class DerivedValueType : uint32_t { + kEIDKey = 1, + kTunnelID = 2, + kPSK = 3, + kPairedSecret = 4, + kIdentityKeySeed = 5, +}; + +namespace internal { +COMPONENT_EXPORT(DEVICE_FIDO) +void Derive(uint8_t* out, + size_t out_len, + base::span<const uint8_t> secret, + base::span<const uint8_t> nonce, + DerivedValueType type); +} // namespace internal + +// Derive derives a sub-secret from a secret and nonce. It is not possible to +// learn anything about |secret| from the value of the sub-secret, assuming that +// |secret| has sufficient size to prevent full enumeration of the +// possibilities. +template <size_t N> +std::array<uint8_t, N> Derive(base::span<const uint8_t> secret, + base::span<const uint8_t> nonce, + DerivedValueType type) { + std::array<uint8_t, N> ret; + internal::Derive(ret.data(), N, secret, nonce, type); + return ret; +} + // EncodePaddedCBORMap encodes the given map and pads it to 256 bytes in such a // way that |DecodePaddedCBORMap| can decode it. The padding is done on the // assumption that the returned bytes will be encrypted and the encoded size of @@ -117,12 +164,6 @@ COMPONENT_EXPORT(DEVICE_FIDO) base::Optional<cbor::Value> DecodePaddedCBORMap( base::span<const uint8_t> input); -// NonceAndEID contains both the random nonce chosen for an advert, as well as -// the EID that was generated from it. -typedef std::pair<std::array<uint8_t, kNonceSize>, - std::array<uint8_t, device::kCableEphemeralIdSize>> - NonceAndEID; - // Crypter handles the post-handshake encryption of CTAP2 messages. class COMPONENT_EXPORT(DEVICE_FIDO) Crypter { public: @@ -153,18 +194,20 @@ class COMPONENT_EXPORT(DEVICE_FIDO) Crypter { uint32_t write_sequence_num_ = 0; }; +// HandshakeHash is the hashed transcript of a handshake. This can be used as a +// channel-binding value. See +// http://www.noiseprotocol.org/noise.html#channel-binding. +using HandshakeHash = std::array<uint8_t, 32>; + // HandshakeInitiator starts a caBLE v2 handshake and processes the single // response message from the other party. The handshake is always initiated from // the phone. class COMPONENT_EXPORT(DEVICE_FIDO) HandshakeInitiator { public: HandshakeInitiator( - // psk_gen_key is either derived from QR-code secrets or comes from - // pairing data. - base::span<const uint8_t, 32> psk_gen_key, - // nonce is randomly generated per advertisement and ensures that BLE - // adverts are non-deterministic. - base::span<const uint8_t, kNonceSize> nonce, + // psk is derived from the connection nonce and either QR-code secrets + // pairing secrets. + base::span<const uint8_t, 32> psk, // peer_identity, if not nullopt, specifies that this is a QR handshake // and then contains a P-256 public key for the peer. Otherwise this is a // paired handshake. @@ -190,9 +233,9 @@ class COMPONENT_EXPORT(DEVICE_FIDO) HandshakeInitiator { // ProcessResponse processes the handshake response from the peer. If // successful it returns a |Crypter| for protecting future messages on the - // connection. - base::Optional<std::unique_ptr<Crypter>> ProcessResponse( - base::span<const uint8_t> response); + // connection and a handshake transcript for signing over if needed. + base::Optional<std::pair<std::unique_ptr<Crypter>, HandshakeHash>> + ProcessResponse(base::span<const uint8_t> response); private: Noise noise_; @@ -203,21 +246,36 @@ class COMPONENT_EXPORT(DEVICE_FIDO) HandshakeInitiator { bssl::UniquePtr<EC_KEY> ephemeral_key_; }; -// RespondToHandshake responds to a caBLE v2 handshake started by a peer. It -// returns a Crypter for encrypting and decrypting future messages, as well as -// the getInfo response from the phone. +// ResponderResult is the result of a successful handshake from the responder's +// side. It contains a Crypter for protecting future messages, the contents of +// the getInfo response given by the peer, and a hash of the handshake +// transcript. +struct COMPONENT_EXPORT(DEVICE_FIDO) ResponderResult { + ResponderResult(std::unique_ptr<Crypter>, + std::vector<uint8_t> getinfo_bytes, + HandshakeHash); + ~ResponderResult(); + ResponderResult(const ResponderResult&) = delete; + ResponderResult(ResponderResult&&); + ResponderResult& operator=(const ResponderResult&) = delete; + + std::unique_ptr<Crypter> crypter; + std::vector<uint8_t> getinfo_bytes; + const HandshakeHash handshake_hash; +}; + +// RespondToHandshake responds to a caBLE v2 handshake started by a peer. COMPONENT_EXPORT(DEVICE_FIDO) -base::Optional<std::pair<std::unique_ptr<Crypter>, std::vector<uint8_t>>> -RespondToHandshake( - // For the first two arguments see |HandshakeInitiator| comments about - // |psk_gen_key| and |nonce|, and the |BuildInitialMessage| comment about - // |eid|. - base::span<const uint8_t, 32> psk_gen_key, - const NonceAndEID& nonce_and_eid, +base::Optional<ResponderResult> RespondToHandshake( + // psk is derived from the connection nonce and either QR-code secrets or + // pairing secrets. + base::span<const uint8_t, 32> psk, + // eid is the EID that was advertised for this handshake. This is checked + // as part of the handshake. + base::span<const uint8_t, kCableEphemeralIdSize> eid, // identity_seed, if not nullopt, specifies that this is a QR handshake and // contains the seed for QR key for this client. - base::Optional<base::span<const uint8_t, kCableIdentityKeySeedSize>> - identity_seed, + base::Optional<base::span<const uint8_t, kQRSeedSize>> identity_seed, // peer_identity, which must be non-nullopt iff |identity| is nullopt, // contains the peer's public key as taken from the pairing data. base::Optional<base::span<const uint8_t, kP256X962Length>> peer_identity, @@ -226,6 +284,27 @@ RespondToHandshake( // out_response is set to the response handshake message, if successful. std::vector<uint8_t>* out_response); +// VerifyPairingSignature checks that |signature| is a valid signature of +// |handshake_hash| by |peer_public_key_x962|. This is used by a phone to prove +// possession of |peer_public_key_x962| since the |handshake_hash| encloses +// random values generated by the desktop and thus is a fresh value. +COMPONENT_EXPORT(DEVICE_FIDO) +bool VerifyPairingSignature( + base::span<const uint8_t, kQRSeedSize> identity_seed, + base::span<const uint8_t, kP256X962Length> peer_public_key_x962, + base::span<const uint8_t, std::tuple_size<HandshakeHash>::value> + handshake_hash, + base::span<const uint8_t> signature); + +// CalculatePairingSignature generates a value that will satisfy +// |VerifyPairingSignature|. +COMPONENT_EXPORT(DEVICE_FIDO) +std::vector<uint8_t> CalculatePairingSignature( + const EC_KEY* identity_key, + base::span<const uint8_t, kP256X962Length> peer_public_key_x962, + base::span<const uint8_t, std::tuple_size<HandshakeHash>::value> + handshake_hash); + } // namespace cablev2 } // namespace device diff --git a/chromium/device/fido/cable/v2_handshake_fuzzer.cc b/chromium/device/fido/cable/v2_handshake_fuzzer.cc index 6dd63595a23..bdc47dd9b66 100644 --- a/chromium/device/fido/cable/v2_handshake_fuzzer.cc +++ b/chromium/device/fido/cable/v2_handshake_fuzzer.cc @@ -17,13 +17,11 @@ namespace device { namespace { -constexpr std::array<uint8_t, 32> kTestPSKGeneratorKey = { +constexpr std::array<uint8_t, 32> kTestPSK = { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, }; -constexpr std::array<uint8_t, cablev2::kNonceSize> kTestNonce = { - 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; constexpr std::array<uint8_t, 16> kTestEphemeralID = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}; constexpr std::array<uint8_t, 65> kTestPeerIdentity = { @@ -66,16 +64,13 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* raw_data, size_t size) { } if (initiate) { - cablev2::HandshakeInitiator handshaker(kTestPSKGeneratorKey, kTestNonce, - peer_identity, std::move(local_key)); + cablev2::HandshakeInitiator handshaker(kTestPSK, peer_identity, + std::move(local_key)); handshaker.BuildInitialMessage(kTestEphemeralID, kTestGetInfoBytes); handshaker.ProcessResponse(input); } else { - cablev2::NonceAndEID nonce_and_eid; - nonce_and_eid.first = kTestNonce; - nonce_and_eid.second = kTestEphemeralID; std::vector<uint8_t> response; - cablev2::RespondToHandshake(kTestPSKGeneratorKey, nonce_and_eid, local_seed, + cablev2::RespondToHandshake(kTestPSK, kTestEphemeralID, local_seed, peer_identity, input, &response); } diff --git a/chromium/device/fido/cable/v2_handshake_unittest.cc b/chromium/device/fido/cable/v2_handshake_unittest.cc index e6a7b2b7f80..6a146a08302 100644 --- a/chromium/device/fido/cable/v2_handshake_unittest.cc +++ b/chromium/device/fido/cable/v2_handshake_unittest.cc @@ -3,8 +3,8 @@ // found in the LICENSE file. #include "device/fido/cable/v2_handshake.h" -#include "base/rand_util.h" #include "components/cbor/values.h" +#include "crypto/random.h" #include "testing/gtest/include/gtest/gtest.h" #include "third_party/boringssl/src/include/openssl/ec.h" #include "third_party/boringssl/src/include/openssl/ec_key.h" @@ -21,21 +21,21 @@ TEST(CableV2Encoding, TunnelServerURLs) { constexpr uint32_t encoded = tunnelserver::EncodeDomain("abcd", tunnelserver::TLD::NET); uint8_t tunnel_id[16] = {0}; - const GURL url = - tunnelserver::GetURL(encoded, tunnelserver::Action::kNew, tunnel_id); + const GURL url = tunnelserver::GetNewTunnelURL(encoded, tunnel_id); EXPECT_TRUE(url.spec().find("//abcd.net/") != std::string::npos) << url; } TEST(CableV2Encoding, EIDs) { eid::Components components; components.tunnel_server_domain = 0x010203; - components.shard_id = 42; - base::RandBytes(components.nonce.data(), components.nonce.size()); + components.routing_id = {9, 10, 11}; + crypto::RandBytes(components.nonce); CableEidArray eid = eid::FromComponents(components); + EXPECT_TRUE(eid::IsValid(eid)); eid::Components components2 = eid::ToComponents(eid); - EXPECT_EQ(components.shard_id, components2.shard_id); + EXPECT_EQ(components.routing_id, components2.routing_id); EXPECT_EQ(components.tunnel_server_domain, components2.tunnel_server_domain); EXPECT_EQ(components.nonce, components2.nonce); @@ -68,13 +68,56 @@ TEST(CableV2Encoding, PaddedCBOR) { EXPECT_EQ(1u, decoded->GetMap().size()); } +std::array<uint8_t, kP256X962Length> PublicKeyOf(const EC_KEY* private_key) { + std::array<uint8_t, kP256X962Length> ret; + CHECK_EQ(ret.size(), + EC_POINT_point2oct(EC_KEY_get0_group(private_key), + EC_KEY_get0_public_key(private_key), + POINT_CONVERSION_UNCOMPRESSED, ret.data(), + ret.size(), /*ctx=*/nullptr)); + return ret; +} + +TEST(CableV2Encoding, HandshakeSignatures) { + static const uint8_t kSeed0[kQRSeedSize] = {0}; + static const uint8_t kSeed1[kQRSeedSize] = {1}; + + bssl::UniquePtr<EC_GROUP> group( + EC_GROUP_new_by_curve_name(NID_X9_62_prime256v1)); + bssl::UniquePtr<EC_KEY> authenticator_key( + EC_KEY_derive_from_secret(group.get(), kSeed0, sizeof(kSeed0))); + bssl::UniquePtr<EC_KEY> client_key( + EC_KEY_derive_from_secret(group.get(), kSeed1, sizeof(kSeed1))); + + const std::array<uint8_t, kP256X962Length> authenticator_public_key = + PublicKeyOf(authenticator_key.get()); + const std::array<uint8_t, kP256X962Length> client_public_key = + PublicKeyOf(client_key.get()); + + HandshakeHash handshake_hash = {1}; + + std::vector<uint8_t> signature = CalculatePairingSignature( + authenticator_key.get(), client_public_key, handshake_hash); + EXPECT_TRUE(VerifyPairingSignature(kSeed1, authenticator_public_key, + handshake_hash, signature)); + + handshake_hash[0] ^= 1; + EXPECT_FALSE(VerifyPairingSignature(kSeed1, authenticator_public_key, + handshake_hash, signature)); + handshake_hash[0] ^= 1; + + signature[0] ^= 1; + EXPECT_FALSE(VerifyPairingSignature(kSeed1, authenticator_public_key, + handshake_hash, signature)); + signature[0] ^= 1; +} + class CableV2HandshakeTest : public ::testing::Test { public: CableV2HandshakeTest() { - std::fill(psk_gen_key_.begin(), psk_gen_key_.end(), 0); - std::fill(nonce_and_eid_.first.begin(), nonce_and_eid_.first.end(), 1); - std::fill(nonce_and_eid_.second.begin(), nonce_and_eid_.second.end(), 2); - std::fill(identity_seed_.begin(), identity_seed_.end(), 3); + std::fill(psk_.begin(), psk_.end(), 0); + std::fill(eid_.begin(), eid_.end(), 1); + std::fill(identity_seed_.begin(), identity_seed_.end(), 2); bssl::UniquePtr<EC_GROUP> group( EC_GROUP_new_by_curve_name(NID_X9_62_prime256v1)); @@ -88,11 +131,11 @@ class CableV2HandshakeTest : public ::testing::Test { } protected: - std::array<uint8_t, 32> psk_gen_key_; - NonceAndEID nonce_and_eid_; + std::array<uint8_t, 32> psk_; + CableEidArray eid_; bssl::UniquePtr<EC_KEY> identity_key_; std::array<uint8_t, kP256X962Length> identity_public_; - std::array<uint8_t, kCableIdentityKeySeedSize> identity_seed_; + std::array<uint8_t, kQRSeedSize> identity_seed_; }; TEST_F(CableV2HandshakeTest, MessageEncrytion) { @@ -123,34 +166,33 @@ TEST_F(CableV2HandshakeTest, MessageEncrytion) { } TEST_F(CableV2HandshakeTest, QRHandshake) { - std::array<uint8_t, 32> wrong_psk_gen_key = psk_gen_key_; - wrong_psk_gen_key[0] ^= 1; + std::array<uint8_t, 32> wrong_psk = psk_; + wrong_psk[0] ^= 1; uint8_t kGetInfoBytes[] = {1, 2, 3, 4, 5}; for (const bool use_correct_key : {false, true}) { - HandshakeInitiator initiator( - use_correct_key ? psk_gen_key_ : wrong_psk_gen_key, - nonce_and_eid_.first, identity_public_, - /*local_identity=*/nullptr); + HandshakeInitiator initiator(use_correct_key ? psk_ : wrong_psk, + identity_public_, + /*local_identity=*/nullptr); std::vector<uint8_t> message = - initiator.BuildInitialMessage(nonce_and_eid_.second, kGetInfoBytes); + initiator.BuildInitialMessage(eid_, kGetInfoBytes); std::vector<uint8_t> response; - base::Optional<std::pair<std::unique_ptr<Crypter>, std::vector<uint8_t>>> - responder_result(RespondToHandshake( - psk_gen_key_, nonce_and_eid_, identity_seed_, - /*peer_identity=*/base::nullopt, message, &response)); + base::Optional<ResponderResult> responder_result(RespondToHandshake( + psk_, eid_, identity_seed_, + /*peer_identity=*/base::nullopt, message, &response)); ASSERT_EQ(responder_result.has_value(), use_correct_key); if (!use_correct_key) { continue; } - base::Optional<std::unique_ptr<Crypter>> initiator_result( - initiator.ProcessResponse(response)); + base::Optional<std::pair<std::unique_ptr<Crypter>, HandshakeHash>> + initiator_result(initiator.ProcessResponse(response)); ASSERT_TRUE(initiator_result.has_value()); - EXPECT_TRUE(responder_result->first->IsCounterpartyOfForTesting( - *initiator_result.value())); - ASSERT_EQ(responder_result->second.size(), sizeof(kGetInfoBytes)); - EXPECT_EQ(0, memcmp(responder_result->second.data(), kGetInfoBytes, + EXPECT_EQ(initiator_result->second, responder_result->handshake_hash); + EXPECT_TRUE(responder_result->crypter->IsCounterpartyOfForTesting( + *initiator_result->first)); + ASSERT_EQ(responder_result->getinfo_bytes.size(), sizeof(kGetInfoBytes)); + EXPECT_EQ(0, memcmp(responder_result->getinfo_bytes.data(), kGetInfoBytes, sizeof(kGetInfoBytes))); } } @@ -166,30 +208,28 @@ TEST_F(CableV2HandshakeTest, PairedHandshake) { EC_KEY* const key = use_correct_key ? identity_key_.get() : wrong_key.get(); EC_KEY_up_ref(key); - HandshakeInitiator initiator(psk_gen_key_, nonce_and_eid_.first, + HandshakeInitiator initiator(psk_, /*peer_identity=*/base::nullopt, bssl::UniquePtr<EC_KEY>(key)); std::vector<uint8_t> message = - initiator.BuildInitialMessage(nonce_and_eid_.second, kGetInfoBytes); + initiator.BuildInitialMessage(eid_, kGetInfoBytes); std::vector<uint8_t> response; - base::Optional<std::pair<std::unique_ptr<Crypter>, std::vector<uint8_t>>> - responder_result(RespondToHandshake(psk_gen_key_, nonce_and_eid_, - /*identity_seed=*/base::nullopt, - identity_public_, message, - &response)); + base::Optional<ResponderResult> responder_result(RespondToHandshake( + psk_, eid_, + /*identity_seed=*/base::nullopt, identity_public_, message, &response)); ASSERT_EQ(responder_result.has_value(), use_correct_key); if (!use_correct_key) { continue; } - base::Optional<std::unique_ptr<Crypter>> initiator_result( - initiator.ProcessResponse(response)); + base::Optional<std::pair<std::unique_ptr<Crypter>, HandshakeHash>> + initiator_result(initiator.ProcessResponse(response)); ASSERT_TRUE(initiator_result.has_value()); - EXPECT_TRUE(responder_result->first->IsCounterpartyOfForTesting( - *initiator_result.value())); - ASSERT_EQ(responder_result->second.size(), sizeof(kGetInfoBytes)); - EXPECT_EQ(0, memcmp(responder_result->second.data(), kGetInfoBytes, + EXPECT_TRUE(responder_result->crypter->IsCounterpartyOfForTesting( + *initiator_result->first)); + ASSERT_EQ(responder_result->getinfo_bytes.size(), sizeof(kGetInfoBytes)); + EXPECT_EQ(0, memcmp(responder_result->getinfo_bytes.data(), kGetInfoBytes, sizeof(kGetInfoBytes))); } } diff --git a/chromium/device/fido/cable/v2_registration.cc b/chromium/device/fido/cable/v2_registration.cc new file mode 100644 index 00000000000..1c26b620ff1 --- /dev/null +++ b/chromium/device/fido/cable/v2_registration.cc @@ -0,0 +1,177 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "device/fido/cable/v2_registration.h" + +#include "base/strings/string_number_conversions.h" +#include "components/cbor/reader.h" +#include "components/cbor/values.h" +#include "components/device_event_log/device_event_log.h" +#include "components/gcm_driver/gcm_app_handler.h" +#include "components/gcm_driver/gcm_driver.h" +#include "components/gcm_driver/instance_id/instance_id.h" +#include "components/gcm_driver/instance_id/instance_id_driver.h" +#include "device/fido/fido_parsing_utils.h" + +namespace device { +namespace cablev2 { +namespace authenticator { + +namespace { + +static const char kFCMAppId[] = "chrome.android.features.cablev2_authenticator"; +static const char kFCMSenderId[] = "141743603694"; + +class FCMHandler : public gcm::GCMAppHandler, public Registration { + public: + FCMHandler(instance_id::InstanceIDDriver* instance_id_driver, + base::RepeatingCallback<void(std::unique_ptr<Registration::Event>)> + event_callback) + : event_callback_(std::move(event_callback)), + instance_id_driver_(instance_id_driver), + instance_id_(instance_id_driver->GetInstanceID(kFCMAppId)) { + gcm::GCMDriver* const gcm_driver = instance_id_->gcm_driver(); + CHECK(gcm_driver->GetAppHandler(kFCMAppId) == nullptr); + instance_id_->gcm_driver()->AddAppHandler(kFCMAppId, this); + + instance_id_->GetToken( + kFCMSenderId, instance_id::kGCMScope, + /*time_to_live=*/base::TimeDelta(), /*options=*/{}, + /*flags=*/{}, + base::BindOnce(&FCMHandler::GetTokenComplete, base::Unretained(this))); + } + + ~FCMHandler() override { + instance_id_->gcm_driver()->RemoveAppHandler(kFCMAppId); + instance_id_driver_->RemoveInstanceID(kFCMAppId); + } + + // Registration: + + base::Optional<std::vector<uint8_t>> contact_id() const override { + if (!registration_token_) { + return base::nullopt; + } + return std::vector<uint8_t>(registration_token_->begin(), + registration_token_->end()); + } + + // GCMAppHandler: + + void OnMessage(const std::string& app_id, + const gcm::IncomingMessage& message) override { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + DCHECK_EQ(app_id, kFCMAppId); + DCHECK_EQ(message.sender_id, kFCMSenderId); + + if (app_id != kFCMAppId || message.sender_id != kFCMSenderId) { + FIDO_LOG(ERROR) << "Discarding FCM message from " << message.sender_id; + return; + } + + base::Optional<std::unique_ptr<Registration::Event>> event = + MessageToEvent(message.data); + if (!event) { + FIDO_LOG(ERROR) << "Failed to decode FCM message. Ignoring."; + return; + } + + event_callback_.Run(std::move(*event)); + } + + void ShutdownHandler() override {} + void OnStoreReset() override {} + void OnMessagesDeleted(const std::string& app_id) override {} + void OnSendError( + const std::string& app_id, + const gcm::GCMClient::SendErrorDetails& send_error_details) override {} + void OnSendAcknowledged(const std::string& app_id, + const std::string& message_id) override {} + void OnMessageDecryptionFailed(const std::string& app_id, + const std::string& message_id, + const std::string& error_message) override {} + bool CanHandle(const std::string& app_id) const override { return false; } + + private: + void GetTokenComplete(const std::string& token, + instance_id::InstanceID::Result result) { + DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + + if (result != instance_id::InstanceID::SUCCESS) { + FIDO_LOG(ERROR) << "Getting FCM token failed: " + << static_cast<int>(result); + return; + } + + FIDO_LOG(ERROR) << __func__ << " " << token; + registration_token_ = token; + } + + static base::Optional<std::unique_ptr<Registration::Event>> MessageToEvent( + const gcm::MessageData& data) { + auto event = std::make_unique<Registration::Event>(); + gcm::MessageData::const_iterator it = data.find("caBLE.tunnelID"); + if (it == data.end() || + !base::HexStringToSpan(it->second, event->tunnel_id)) { + return base::nullopt; + } + + it = data.find("caBLE.routingID"); + if (it == data.end() || + !base::HexStringToSpan(it->second, event->routing_id)) { + return base::nullopt; + } + + std::vector<uint8_t> payload_bytes; + it = data.find("caBLE.clientPayload"); + if (it == data.end() || + !base::HexStringToBytes(it->second, &payload_bytes)) { + return base::nullopt; + } + + base::Optional<cbor::Value> payload = cbor::Reader::Read(payload_bytes); + if (!payload || !payload->is_map()) { + return base::nullopt; + } + + const cbor::Value::MapValue& map = payload->GetMap(); + cbor::Value::MapValue::const_iterator cbor_it = map.find(cbor::Value(1)); + if (cbor_it == map.end() || !cbor_it->second.is_bytestring()) { + return base::nullopt; + } + event->pairing_id = cbor_it->second.GetBytestring(); + + if (!fido_parsing_utils::CopyCBORBytestring(&event->client_nonce, map, 2)) { + return base::nullopt; + } + + return event; + } + + base::RepeatingCallback<void(std::unique_ptr<Registration::Event>)> + event_callback_; + instance_id::InstanceIDDriver* const instance_id_driver_; + instance_id::InstanceID* const instance_id_; + base::Optional<std::string> registration_token_; + + SEQUENCE_CHECKER(sequence_checker_); +}; + +} // namespace + +Registration::~Registration() = default; +Registration::Event::Event() = default; +Registration::Event::~Event() = default; + +std::unique_ptr<Registration> Register( + instance_id::InstanceIDDriver* instance_id_driver, + base::RepeatingCallback<void(std::unique_ptr<Registration::Event>)> + event_callback) { + return std::make_unique<FCMHandler>(instance_id_driver, + std::move(event_callback)); +} + +} // namespace authenticator +} // namespace cablev2 +} // namespace device diff --git a/chromium/device/fido/cable/v2_registration.h b/chromium/device/fido/cable/v2_registration.h new file mode 100644 index 00000000000..90461edc385 --- /dev/null +++ b/chromium/device/fido/cable/v2_registration.h @@ -0,0 +1,64 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef DEVICE_FIDO_CABLE_V2_REGISTRATION_H_ +#define DEVICE_FIDO_CABLE_V2_REGISTRATION_H_ + +#include <stdint.h> + +#include <array> +#include <memory> +#include <vector> + +#include "base/callback_forward.h" +#include "base/optional.h" +#include "device/fido/cable/v2_constants.h" + +namespace instance_id { +class InstanceIDDriver; +} + +namespace device { +namespace cablev2 { +namespace authenticator { + +// Registration represents a subscription to events from the tunnel service. +class Registration { + public: + // An Event contains the information sent by the tunnel service when a peer is + // trying to connect. + struct Event { + Event(); + ~Event(); + Event(const Event&) = delete; + Event& operator=(const Event&) = delete; + + std::array<uint8_t, kTunnelIdSize> tunnel_id; + std::array<uint8_t, kRoutingIdSize> routing_id; + std::vector<uint8_t> pairing_id; + std::array<uint8_t, kClientNonceSize> client_nonce; + }; + + virtual ~Registration(); + + // contact_id returns an opaque token that may be placed in pairing data for + // desktops to later connect to. |nullopt| will be returned if the value is + // not yet ready. + virtual base::Optional<std::vector<uint8_t>> contact_id() const = 0; +}; + +// Register subscribes to the tunnel service and returns a |Registration|. This +// should only be called once in an address space. Subsequent calls may CHECK. +// The |event_callback| is called, on the same thread, whenever a paired device +// requests a tunnel. +std::unique_ptr<Registration> Register( + instance_id::InstanceIDDriver* instance_id_driver, + base::RepeatingCallback<void(std::unique_ptr<Registration::Event>)> + event_callback); + +} // namespace authenticator +} // namespace cablev2 +} // namespace device + +#endif // DEVICE_FIDO_CABLE_V2_REGISTRATION_H_ diff --git a/chromium/device/fido/cable/v2_test_util.cc b/chromium/device/fido/cable/v2_test_util.cc new file mode 100644 index 00000000000..1504cd22637 --- /dev/null +++ b/chromium/device/fido/cable/v2_test_util.cc @@ -0,0 +1,483 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "device/fido/cable/v2_test_util.h" + +#include <string> +#include <vector> + +#include "base/base64url.h" +#include "base/bind.h" +#include "base/check.h" +#include "base/memory/weak_ptr.h" +#include "base/strings/string_number_conversions.h" +#include "components/cbor/reader.h" +#include "components/cbor/values.h" +#include "components/cbor/writer.h" +#include "crypto/random.h" +#include "device/fido/cable/v2_authenticator.h" +#include "device/fido/cable/v2_discovery.h" +#include "device/fido/fido_constants.h" +#include "device/fido/virtual_ctap2_device.h" +#include "services/network/test/test_network_context.h" +#include "url/gurl.h" + +namespace device { +namespace cablev2 { +namespace { + +// TestNetworkContext intercepts WebSocket creation calls and simulates a +// caBLEv2 tunnel server. +class TestNetworkContext : public network::TestNetworkContext { + public: + using ContactCallback = base::RepeatingCallback<void( + base::span<const uint8_t, kTunnelIdSize> tunnel_id, + base::span<const uint8_t> pairing_id, + base::span<const uint8_t, kClientNonceSize> client_nonce)>; + + explicit TestNetworkContext(ContactCallback contact_callback) + : contact_callback_(std::move(contact_callback)) {} + + void CreateWebSocket( + const GURL& url, + const std::vector<std::string>& requested_protocols, + const net::SiteForCookies& site_for_cookies, + const net::IsolationInfo& isolation_info, + std::vector<network::mojom::HttpHeaderPtr> additional_headers, + int32_t process_id, + int32_t render_frame_id, + const url::Origin& origin, + uint32_t options, + const net::MutableNetworkTrafficAnnotationTag& traffic_annotation, + mojo::PendingRemote<network::mojom::WebSocketHandshakeClient> + handshake_client, + mojo::PendingRemote<network::mojom::AuthenticationHandler> auth_handler, + mojo::PendingRemote<network::mojom::TrustedHeaderClient> header_client) + override { + CHECK(url.has_path()); + + base::StringPiece path = url.path_piece(); + static const char kNewPrefix[] = "/cable/new/"; + static const char kConnectPrefix[] = "/cable/connect/"; + static const char kContactPrefix[] = "/cable/contact/"; + if (path.find(kNewPrefix) == 0) { + path.remove_prefix(sizeof(kNewPrefix) - 1); + CHECK(!base::Contains(connections_, path.as_string())); + connections_.emplace(path.as_string(), std::make_unique<Connection>( + Connection::Type::NEW, + std::move(handshake_client))); + } else if (path.find(kConnectPrefix) == 0) { + path.remove_prefix(sizeof(kConnectPrefix) - 1); + // The first part of |path| will be a hex-encoded routing ID followed by a + // '/'. Skip it. + constexpr size_t kRoutingIdComponentSize = 2 * kRoutingIdSize + 1; + CHECK_GE(path.size(), kRoutingIdComponentSize); + path.remove_prefix(kRoutingIdComponentSize); + + const auto it = connections_.find(path.as_string()); + CHECK(it != connections_.end()) << "Unknown tunnel requested"; + it->second->set_peer(std::make_unique<Connection>( + Connection::Type::CONNECT, std::move(handshake_client))); + } else if (path.find(kContactPrefix) == 0) { + path.remove_prefix(sizeof(kContactPrefix) - 1); + + CHECK_EQ(additional_headers.size(), 1u); + CHECK_EQ(additional_headers[0]->name, device::kCableClientPayloadHeader); + std::vector<uint8_t> client_payload_bytes; + CHECK(base::HexStringToBytes(additional_headers[0]->value, + &client_payload_bytes)); + + base::Optional<cbor::Value> client_payload = + cbor::Reader::Read(client_payload_bytes); + const cbor::Value::MapValue& map = client_payload->GetMap(); + + uint8_t tunnel_id[kTunnelIdSize]; + crypto::RandBytes(tunnel_id); + + connections_.emplace( + base::HexEncode(tunnel_id), + std::make_unique<Connection>(Connection::Type::CONTACT, + std::move(handshake_client))); + + const std::vector<uint8_t>& client_nonce_vec = + map.find(cbor::Value(2))->second.GetBytestring(); + base::span<const uint8_t, kClientNonceSize> client_nonce( + client_nonce_vec.data(), client_nonce_vec.size()); + + contact_callback_.Run( + tunnel_id, + /*pairing_id=*/map.find(cbor::Value(1))->second.GetBytestring(), + client_nonce); + } else { + CHECK(false) << "unexpected path: " << path; + } + } + + private: + class Connection : public network::mojom::WebSocket { + public: + enum class Type { + NEW, + CONNECT, + CONTACT, + }; + + Connection(Type type, + mojo::PendingRemote<network::mojom::WebSocketHandshakeClient> + pending_handshake_client) + : type_(type), + in_watcher_(FROM_HERE, mojo::SimpleWatcher::ArmingPolicy::MANUAL), + out_watcher_(FROM_HERE, mojo::SimpleWatcher::ArmingPolicy::MANUAL), + handshake_client_(std::move(pending_handshake_client)) { + MojoCreateDataPipeOptions options; + memset(&options, 0, sizeof(options)); + options.struct_size = sizeof(options); + options.flags = MOJO_CREATE_DATA_PIPE_FLAG_NONE; + options.element_num_bytes = sizeof(uint8_t); + options.capacity_num_bytes = 1 << 16; + + CHECK_EQ(mojo::CreateDataPipe(&options, &in_producer_, &in_), + MOJO_RESULT_OK); + CHECK_EQ(mojo::CreateDataPipe(&options, &out_, &out_consumer_), + MOJO_RESULT_OK); + + in_watcher_.Watch(in_.get(), MOJO_HANDLE_SIGNAL_READABLE, + MOJO_TRIGGER_CONDITION_SIGNALS_SATISFIED, + base::BindRepeating(&Connection::OnInPipeReady, + base::Unretained(this))); + out_watcher_.Watch(out_.get(), MOJO_HANDLE_SIGNAL_WRITABLE, + MOJO_TRIGGER_CONDITION_SIGNALS_SATISFIED, + base::BindRepeating(&Connection::OnOutPipeReady, + base::Unretained(this))); + base::SequencedTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::BindOnce(&Connection::CompleteConnection, + base::Unretained(this))); + } + + void SendMessage(network::mojom::WebSocketMessageType type, + uint64_t length) override { + if (!peer_ || !peer_->connected_) { + pending_messages_.emplace_back(std::make_tuple(type, length)); + } else { + peer_->client_receiver_->OnDataFrame(/*final=*/true, type, length); + } + + if (length > 0) { + buffer_.resize(buffer_.size() + length); + OnInPipeReady(MOJO_RESULT_OK, mojo::HandleSignalsState()); + } + } + + void StartReceiving() override {} + void StartClosingHandshake(uint16_t code, + const std::string& reason) override { + CHECK(false); + } + + void set_peer(std::unique_ptr<Connection> peer) { + CHECK(!peer_); + peer_ownership_ = std::move(peer); + peer_ = peer_ownership_.get(); + peer_->set_nonowning_peer(this); + + Flush(); + } + + private: + // name is useful when adding debugging messages. The first party to a + // tunnel is "A" and the second is "B". + const char* name() const { + switch (type_) { + case Type::NEW: + case Type::CONTACT: + return "A"; + case Type::CONNECT: + return "B"; + } + } + + void set_nonowning_peer(Connection* peer) { + CHECK(!peer_); + peer_ = peer; + Flush(); + } + + void CompleteConnection() { + CHECK(!connected_); + auto response = network::mojom::WebSocketHandshakeResponse::New(); + response->selected_protocol = device::kCableWebSocketProtocol; + + if (type_ == Type::NEW) { + auto header = network::mojom::HttpHeader::New(); + header->name = device::kCableRoutingIdHeader; + std::array<uint8_t, kRoutingIdSize> routing_id = {42}; + header->value = base::HexEncode(routing_id); + response->headers.push_back(std::move(header)); + } + + handshake_client_->OnConnectionEstablished( + socket_.BindNewPipeAndPassRemote(), + client_receiver_.BindNewPipeAndPassReceiver(), std::move(response), + std::move(out_consumer_), std::move(in_producer_)); + + connected_ = true; + if (peer_) { + peer_->Flush(); + } + } + + void Flush() { + if (!peer_->connected_) { + return; + } + + for (const auto& pending_message : pending_messages_) { + peer_->client_receiver_->OnDataFrame( + /*final=*/true, pending_message.first, pending_message.second); + } + + if (!buffer_.empty()) { + peer_->out_watcher_.Arm(); + } + } + + void OnInPipeReady(MojoResult, const mojo::HandleSignalsState&) { + const size_t todo = buffer_.size() - buffer_i_; + CHECK_GT(todo, 0u); + + // We CHECK that the message fits into Mojo's 32-bit lengths because we + // don't expect anything that large in unittests. + uint32_t todo_32 = todo; + CHECK_LE(todo, std::numeric_limits<decltype(todo_32)>::max()); + + const MojoResult result = in_->ReadData( + &buffer_.data()[buffer_i_], &todo_32, MOJO_READ_DATA_FLAG_NONE); + if (result == MOJO_RESULT_OK) { + buffer_i_ += todo_32; + CHECK_LE(buffer_i_, buffer_.size()); + + if (peer_ && buffer_i_ > 0) { + peer_->OnOutPipeReady(MOJO_RESULT_OK, mojo::HandleSignalsState()); + } + + if (buffer_i_ < buffer_.size()) { + in_watcher_.Arm(); + } else { + // TODO + } + } else if (result == MOJO_RESULT_SHOULD_WAIT) { + in_watcher_.Arm(); + } else { + CHECK(false) << static_cast<int>(result); + } + } + + void OnOutPipeReady(MojoResult, const mojo::HandleSignalsState&) { + const size_t todo = peer_->buffer_.size(); + if (todo == 0) { + return; + } + + uint32_t todo_32 = todo; + const MojoResult result = out_->WriteData(peer_->buffer_.data(), &todo_32, + MOJO_WRITE_DATA_FLAG_NONE); + if (result == MOJO_RESULT_OK) { + if (todo_32 == todo) { + peer_->buffer_.clear(); + peer_->buffer_i_ = 0; + } else { + const size_t new_length = todo - todo_32; + memmove(peer_->buffer_.data(), &peer_->buffer_.data()[todo_32], + new_length); + peer_->buffer_.resize(new_length); + peer_->buffer_i_ -= todo_32; + } + + if (!peer_->buffer_.empty()) { + out_watcher_.Arm(); + } + } else if (result == MOJO_RESULT_SHOULD_WAIT) { + out_watcher_.Arm(); + } else { + CHECK(false) << static_cast<int>(result); + } + } + + const Type type_; + bool connected_ = false; + std::unique_ptr<Connection> peer_ownership_; + std::vector<uint8_t> buffer_; + std::vector<std::pair<network::mojom::WebSocketMessageType, uint64_t>> + pending_messages_; + size_t buffer_i_ = 0; + mojo::SimpleWatcher in_watcher_; + mojo::SimpleWatcher out_watcher_; + Connection* peer_ = nullptr; + mojo::Remote<network::mojom::WebSocketHandshakeClient> handshake_client_; + mojo::Remote<network::mojom::WebSocketClient> client_receiver_; + mojo::Receiver<network::mojom::WebSocket> socket_{this}; + mojo::ScopedDataPipeConsumerHandle in_; + mojo::ScopedDataPipeProducerHandle in_producer_; + mojo::ScopedDataPipeProducerHandle out_; + mojo::ScopedDataPipeConsumerHandle out_consumer_; + }; + + std::map<std::string, std::unique_ptr<Connection>> connections_; + const ContactCallback contact_callback_; +}; + +class DummyBLEAdvert + : public device::cablev2::authenticator::Platform::BLEAdvert {}; + +// TestPlatform implements the platform support for caBLEv2 by forwarding +// messages to the given |VirtualCtap2Device|. +class TestPlatform : public authenticator::Platform { + public: + TestPlatform(Discovery* discovery, device::VirtualCtap2Device* ctap2_device) + : discovery_(discovery), ctap2_device_(ctap2_device) {} + + void MakeCredential(const std::string& origin, + const std::string& rp_id, + base::span<const uint8_t> challenge, + base::span<const uint8_t> user_id, + base::span<const int> algorithms, + base::span<const std::vector<uint8_t>> excluded_cred_ids, + bool resident_key_required, + MakeCredentialCallback callback) override { + std::string challenge_b64; + base::Base64UrlEncode( + base::StringPiece(reinterpret_cast<const char*>(challenge.data()), + challenge.size()), + base::Base64UrlEncodePolicy::OMIT_PADDING, &challenge_b64); + + std::string client_data_json = base::StringPrintf( + R"({"type": "webauthn.create", "challenge": "%s", "origin": "%s", + "androidPackageName": "com.chrome.unittest"})", + challenge_b64.c_str(), origin.c_str()); + std::vector<device::PublicKeyCredentialParams::CredentialInfo> cred_infos; + for (const auto& algo : algorithms) { + device::PublicKeyCredentialParams::CredentialInfo cred_info; + cred_info.algorithm = algo; + cred_infos.push_back(cred_info); + } + + device::CtapMakeCredentialRequest request( + client_data_json, device::PublicKeyCredentialRpEntity(rp_id), + device::PublicKeyCredentialUserEntity( + device::fido_parsing_utils::Materialize(user_id), + /*name=*/base::nullopt, /*display_name=*/base::nullopt, + /*icon_url=*/base::nullopt), + device::PublicKeyCredentialParams(std::move(cred_infos))); + + std::pair<device::CtapRequestCommand, base::Optional<cbor::Value>> + request_cbor = AsCTAPRequestValuePair(request); + + ctap2_device_->DeviceTransact( + ToCTAP2Command(std::move(request_cbor)), + base::BindOnce(&TestPlatform::OnMakeCredentialResult, + weak_factory_.GetWeakPtr(), std::move(client_data_json), + std::move(callback))); + } + + void GetAssertion(const std::string& origin, + const std::string& rp_id, + base::span<const uint8_t> challenge, + base::span<const std::vector<uint8_t>> allowed_cred_ids, + GetAssertionCallback callback) override { + NOTREACHED(); + } + + std::unique_ptr<authenticator::Platform::BLEAdvert> SendBLEAdvert( + base::span<uint8_t, 16> payload) override { + base::SequencedTaskRunnerHandle::Get()->PostTask( + FROM_HERE, + base::BindOnce( + [](Discovery* discovery, std::array<uint8_t, 16> payload) { + discovery->OnBLEAdvertSeen( + /*address=*/"", payload); + }, + base::Unretained(discovery_), + device::fido_parsing_utils::Materialize<EXTENT(payload)>(payload))); + return std::make_unique<DummyBLEAdvert>(); + } + + private: + std::vector<uint8_t> ToCTAP2Command( + const std::pair<device::CtapRequestCommand, base::Optional<cbor::Value>>& + parts) { + std::vector<uint8_t> ret; + + if (parts.second.has_value()) { + base::Optional<std::vector<uint8_t>> cbor_bytes = + cbor::Writer::Write(std::move(*parts.second)); + ret.swap(*cbor_bytes); + } + + ret.insert(ret.begin(), static_cast<uint8_t>(parts.first)); + return ret; + } + + void OnMakeCredentialResult(std::string client_data_json, + MakeCredentialCallback callback, + base::Optional<std::vector<uint8_t>> result) { + if (!result || result->empty()) { + std::move(callback).Run( + static_cast<uint32_t>(device::CtapDeviceResponseCode::kCtap2ErrOther), + base::span<const uint8_t>(), base::span<const uint8_t>()); + return; + } + const base::span<const uint8_t> payload = *result; + + if (payload.size() == 1 || + payload[0] != + static_cast<uint8_t>(device::CtapDeviceResponseCode::kSuccess)) { + std::move(callback).Run(payload[0], base::span<const uint8_t>(), + base::span<const uint8_t>()); + return; + } + + base::Optional<cbor::Value> v = cbor::Reader::Read(payload.subspan(1)); + const cbor::Value::MapValue& in_map = v->GetMap(); + + cbor::Value::MapValue out_map; + out_map.emplace("fmt", in_map.find(cbor::Value(1))->second.GetString()); + out_map.emplace("authData", + in_map.find(cbor::Value(2))->second.GetBytestring()); + out_map.emplace("attStmt", in_map.find(cbor::Value(3))->second.GetMap()); + + base::Optional<std::vector<uint8_t>> attestation_obj = + cbor::Writer::Write(cbor::Value(std::move(out_map))); + + std::move(callback).Run( + static_cast<uint32_t>(device::CtapDeviceResponseCode::kSuccess), + base::span<const uint8_t>( + reinterpret_cast<const uint8_t*>(client_data_json.data()), + client_data_json.size()), + *attestation_obj); + } + + Discovery* const discovery_; + device::VirtualCtap2Device* const ctap2_device_; + base::WeakPtrFactory<TestPlatform> weak_factory_{this}; +}; + +} // namespace + +std::unique_ptr<network::mojom::NetworkContext> NewMockTunnelServer( + ContactCallback contact_callback) { + return std::make_unique<TestNetworkContext>(std::move(contact_callback)); +} + +namespace authenticator { + +std::unique_ptr<authenticator::Platform> NewMockPlatform( + Discovery* discovery, + device::VirtualCtap2Device* ctap2_device) { + return std::make_unique<TestPlatform>(discovery, ctap2_device); +} + +} // namespace authenticator + +} // namespace cablev2 +} // namespace device diff --git a/chromium/device/fido/cable/v2_test_util.h b/chromium/device/fido/cable/v2_test_util.h new file mode 100644 index 00000000000..701a14a71b0 --- /dev/null +++ b/chromium/device/fido/cable/v2_test_util.h @@ -0,0 +1,52 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef DEVICE_FIDO_CABLE_V2_TEST_UTIL_H_ +#define DEVICE_FIDO_CABLE_V2_TEST_UTIL_H_ + +#include <memory> + +#include "base/callback_forward.h" +#include "base/containers/span.h" +#include "device/fido/cable/v2_constants.h" +#include "services/network/public/mojom/network_context.mojom-forward.h" + +namespace device { + +class VirtualCtap2Device; + +namespace cablev2 { + +class Discovery; + +// ContactCallback is called when a mock tunnel server (see +// |NewMockTunnelServer|) is asked to contact a phone. This simulates a tunnel +// server using a cloud messaging solution to wake a device. +using ContactCallback = base::RepeatingCallback<void( + base::span<const uint8_t, kTunnelIdSize> tunnel_id, + base::span<const uint8_t> pairing_id, + base::span<const uint8_t, kClientNonceSize> client_nonce)>; + +// NewMockTunnelServer returns a |NetworkContext| that implements WebSocket +// requests and simulates a tunnel server. +std::unique_ptr<network::mojom::NetworkContext> NewMockTunnelServer( + ContactCallback contact_callback); + +namespace authenticator { + +class Platform; + +// NewMockPlatform returns a |Platform| that implements the makeCredential +// operation by forwarding it to |ctap2_device|. Transmitted BLE adverts are +// forwarded to |discovery|. +std::unique_ptr<Platform> NewMockPlatform( + Discovery* discovery, + device::VirtualCtap2Device* ctap2_device); + +} // namespace authenticator + +} // namespace cablev2 +} // namespace device + +#endif // DEVICE_FIDO_CABLE_V2_TEST_UTIL_H_ diff --git a/chromium/device/fido/cable/websocket_adapter.cc b/chromium/device/fido/cable/websocket_adapter.cc index 4c10120cc0e..b522f58d1cf 100644 --- a/chromium/device/fido/cable/websocket_adapter.cc +++ b/chromium/device/fido/cable/websocket_adapter.cc @@ -20,7 +20,9 @@ static constexpr size_t kMaxIncomingMessageSize = 1 << 20; WebSocketAdapter::WebSocketAdapter(TunnelReadyCallback on_tunnel_ready, TunnelDataCallback on_tunnel_data) : on_tunnel_ready_(std::move(on_tunnel_ready)), - on_tunnel_data_(std::move(on_tunnel_data)) {} + on_tunnel_data_(std::move(on_tunnel_data)), + read_pipe_watcher_(FROM_HERE, mojo::SimpleWatcher::ArmingPolicy::MANUAL) { +} WebSocketAdapter::~WebSocketAdapter() = default; @@ -68,61 +70,78 @@ void WebSocketAdapter::OnConnectionEstablished( return; } - base::Optional<uint8_t> shard_id; + base::Optional<std::array<uint8_t, kRoutingIdSize>> routing_id; for (const auto& header : response->headers) { if (base::EqualsCaseInsensitiveASCII(header->name.c_str(), - kCableShardIdHeader)) { - unsigned u; - if (!base::StringToUint(header->value, &u) || shard_id > 63) { - FIDO_LOG(ERROR) << "Invalid shard ID from tunnel server"; + kCableRoutingIdHeader)) { + if (routing_id.has_value() || + !base::HexStringToSpan(header->value, routing_id.emplace())) { + FIDO_LOG(ERROR) << "Invalid routing ID from tunnel server: " + << header->value; return; } - shard_id = u; - break; } } socket_remote_.Bind(std::move(socket)); read_pipe_ = std::move(readable); + read_pipe_watcher_.Watch( + read_pipe_.get(), MOJO_HANDLE_SIGNAL_READABLE, + MOJO_TRIGGER_CONDITION_SIGNALS_SATISFIED, + base::BindRepeating(&WebSocketAdapter::OnDataPipeReady, + base::Unretained(this))); write_pipe_ = std::move(writable); client_receiver_.Bind(std::move(client_receiver)); + + // |handshake_receiver_| will disconnect soon. In order to catch network + // process crashes, we switch to watching |client_receiver_|. + handshake_receiver_.set_disconnect_handler(base::DoNothing()); + client_receiver_.set_disconnect_handler(base::BindOnce( + &WebSocketAdapter::OnMojoPipeDisconnect, base::Unretained(this))); + socket_remote_->StartReceiving(); - std::move(on_tunnel_ready_).Run(true, shard_id); + std::move(on_tunnel_ready_).Run(true, routing_id); } void WebSocketAdapter::OnDataFrame(bool finish, network::mojom::WebSocketMessageType type, uint64_t data_len) { DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); + DCHECK_EQ(pending_message_i_, pending_message_.size()); + DCHECK(!pending_message_finished_); + + if (data_len == 0) { + if (finish) { + FlushPendingMessage(); + } + return; + } const size_t old_size = pending_message_.size(); const size_t new_size = old_size + data_len; if (type != network::mojom::WebSocketMessageType::BINARY || data_len > std::numeric_limits<uint32_t>::max() || new_size < old_size || new_size > kMaxIncomingMessageSize) { - FIDO_LOG(ERROR) << "invalid WebSocket frame"; + FIDO_LOG(ERROR) << "invalid WebSocket frame (type: " + << static_cast<int>(type) << ", len: " << data_len << ")"; Close(); return; } - if (data_len > 0) { - pending_message_.resize(new_size); - uint32_t data_len_32 = data_len; - if (read_pipe_->ReadData(&pending_message_.data()[old_size], &data_len_32, - MOJO_READ_DATA_FLAG_ALL_OR_NONE) != - MOJO_RESULT_OK) { - FIDO_LOG(ERROR) << "reading WebSocket frame failed"; - Close(); - return; - } - DCHECK_EQ(static_cast<size_t>(data_len_32), data_len); - } - - if (finish) { - on_tunnel_data_.Run(pending_message_); - pending_message_.resize(0); - } + // The network process sends the |OnDataFrame| message before writing to + // |read_pipe_|. Therefore we cannot depend on the message bytes being + // immediately available in |read_pipe_| without a race. Thus + // |read_pipe_watcher_| is used to wait for the data if needed. + + pending_message_.resize(new_size); + pending_message_finished_ = finish; + // Suspend more |OnDataFrame| callbacks until frame's data has been read. The + // network service has successfully read |data_len| bytes before calling this + // function so there's no I/O errors to worry about while reading; we know + // that the bytes are coming. + client_receiver_.Pause(); + OnDataPipeReady(MOJO_RESULT_OK, mojo::HandleSignalsState()); } void WebSocketAdapter::OnDropChannel(bool was_clean, @@ -137,6 +156,41 @@ void WebSocketAdapter::OnClosingHandshake() { DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); } +void WebSocketAdapter::OnDataPipeReady(MojoResult, + const mojo::HandleSignalsState&) { + const size_t todo = pending_message_.size() - pending_message_i_; + DCHECK_GT(todo, 0u); + + // Truncation to 32-bits cannot overflow because |pending_message_.size()| is + // bound by |kMaxIncomingMessageSize| when it is resized in |OnDataFrame|. + uint32_t todo_32 = static_cast<uint32_t>(todo); + static_assert( + kMaxIncomingMessageSize <= std::numeric_limits<decltype(todo_32)>::max(), + ""); + const MojoResult result = + read_pipe_->ReadData(&pending_message_.data()[pending_message_i_], + &todo_32, MOJO_READ_DATA_FLAG_NONE); + if (result == MOJO_RESULT_OK) { + pending_message_i_ += todo_32; + DCHECK_LE(pending_message_i_, pending_message_.size()); + + if (pending_message_i_ < pending_message_.size()) { + read_pipe_watcher_.Arm(); + } else { + client_receiver_.Resume(); + if (pending_message_finished_) { + FlushPendingMessage(); + } + } + } else if (result == MOJO_RESULT_SHOULD_WAIT) { + read_pipe_watcher_.Arm(); + } else { + FIDO_LOG(ERROR) << "reading WebSocket frame failed: " + << static_cast<int>(result); + Close(); + } +} + void WebSocketAdapter::OnMojoPipeDisconnect() { DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_); @@ -160,5 +214,14 @@ void WebSocketAdapter::Close() { on_tunnel_data_.Run(base::nullopt); } +void WebSocketAdapter::FlushPendingMessage() { + std::vector<uint8_t> message; + message.swap(pending_message_); + pending_message_i_ = 0; + pending_message_finished_ = false; + + on_tunnel_data_.Run(message); +} + } // namespace cablev2 } // namespace device diff --git a/chromium/device/fido/cable/websocket_adapter.h b/chromium/device/fido/cable/websocket_adapter.h index c898fc7cd09..40289bd0bd7 100644 --- a/chromium/device/fido/cable/websocket_adapter.h +++ b/chromium/device/fido/cable/websocket_adapter.h @@ -12,6 +12,7 @@ #include "base/containers/span.h" #include "base/optional.h" #include "base/sequence_checker.h" +#include "device/fido/cable/v2_handshake.h" #include "services/network/public/mojom/network_context.mojom.h" namespace device { @@ -24,14 +25,13 @@ class COMPONENT_EXPORT(DEVICE_FIDO) WebSocketAdapter : public network::mojom::WebSocketHandshakeClient, network::mojom::WebSocketClient { public: - using TunnelReadyCallback = - base::OnceCallback<void(bool, base::Optional<uint8_t>)>; + using TunnelReadyCallback = base::OnceCallback< + void(bool, base::Optional<std::array<uint8_t, kRoutingIdSize>>)>; using TunnelDataCallback = base::RepeatingCallback<void(base::Optional<base::span<const uint8_t>>)>; WebSocketAdapter( // on_tunnel_ready is called once with a boolean that indicates whether - // the WebSocket successfully connected and an optional shard ID taken - // from the X-caBLE-Shard header in the HTTP response, if any. + // the WebSocket successfully connected and an optional routing ID. TunnelReadyCallback on_tunnel_ready, // on_tunnel_ready is called repeatedly, after successful connection, with // the contents of WebSocket messages. Framing is preserved so a single @@ -72,11 +72,22 @@ class COMPONENT_EXPORT(DEVICE_FIDO) WebSocketAdapter private: void OnMojoPipeDisconnect(); + void OnDataPipeReady(MojoResult result, + const mojo::HandleSignalsState& state); void Close(); + void FlushPendingMessage(); bool closed_ = false; + // pending_message_ contains a partial message that is being reassembled. std::vector<uint8_t> pending_message_; + // pending_message_i_ contains the number of valid bytes of + // |pending_message_|. + size_t pending_message_i_ = 0; + // pending_message_finished_ is true if |pending_message_| is the full size of + // an application frame and thus should be passed up once filled with bytes. + bool pending_message_finished_ = false; + TunnelReadyCallback on_tunnel_ready_; const TunnelDataCallback on_tunnel_data_; mojo::Receiver<network::mojom::WebSocketHandshakeClient> handshake_receiver_{ @@ -84,6 +95,7 @@ class COMPONENT_EXPORT(DEVICE_FIDO) WebSocketAdapter mojo::Receiver<network::mojom::WebSocketClient> client_receiver_{this}; mojo::Remote<network::mojom::WebSocket> socket_remote_; mojo::ScopedDataPipeConsumerHandle read_pipe_; + mojo::SimpleWatcher read_pipe_watcher_; mojo::ScopedDataPipeProducerHandle write_pipe_; SEQUENCE_CHECKER(sequence_checker_); }; diff --git a/chromium/device/fido/client_data.cc b/chromium/device/fido/client_data.cc index 5ee8a6dc6cd..eb04d50fb61 100644 --- a/chromium/device/fido/client_data.cc +++ b/chromium/device/fido/client_data.cc @@ -102,7 +102,7 @@ std::string SerializeCollectedClientDataToJson( // unreasonably specific assumptions about the clientData JSON. This is // done in the fashion of // https://tools.ietf.org/html/draft-ietf-tls-grease - ret.append(R"(,"extra_keys_may_be_added_here":")"); + ret.append(R"(,"other_keys_can_be_added_here":")"); ret.append( "do not compare clientDataJSON against a template. See " "https://goo.gl/yabPex\""); diff --git a/chromium/device/fido/credential_management.cc b/chromium/device/fido/credential_management.cc index 3e9a9b6aa76..9ec1fdfc85c 100644 --- a/chromium/device/fido/credential_management.cc +++ b/chromium/device/fido/credential_management.cc @@ -15,31 +15,11 @@ namespace device { -namespace { -std::array<uint8_t, 16> MakePINAuth(base::span<const uint8_t> pin_token, - base::span<const uint8_t> pin_auth_bytes) { - DCHECK(!pin_token.empty() && !pin_auth_bytes.empty()); - std::array<uint8_t, SHA256_DIGEST_LENGTH> hmac; - unsigned hmac_len; - CHECK(HMAC(EVP_sha256(), pin_token.data(), pin_token.size(), - pin_auth_bytes.data(), pin_auth_bytes.size(), hmac.data(), - &hmac_len)); - DCHECK_EQ(hmac.size(), static_cast<size_t>(hmac_len)); - std::array<uint8_t, 16> pin_auth; - std::copy(hmac.begin(), hmac.begin() + 16, pin_auth.begin()); - return pin_auth; -} -} // namespace - CredentialManagementRequest::CredentialManagementRequest( Version version_, CredentialManagementSubCommand subcommand_, - base::Optional<cbor::Value::MapValue> params_, - base::Optional<std::array<uint8_t, 16>> pin_auth_) - : version(version_), - subcommand(subcommand_), - params(std::move(params_)), - pin_auth(std::move(pin_auth_)) {} + base::Optional<cbor::Value::MapValue> params_) + : version(version_), subcommand(subcommand_), params(std::move(params_)) {} CredentialManagementRequest::CredentialManagementRequest( CredentialManagementRequest&&) = default; CredentialManagementRequest& CredentialManagementRequest::operator=( @@ -49,25 +29,25 @@ CredentialManagementRequest::~CredentialManagementRequest() = default; // static CredentialManagementRequest CredentialManagementRequest::ForGetCredsMetadata( Version version, - base::span<const uint8_t> pin_token) { - return CredentialManagementRequest( + const pin::TokenResponse& token) { + CredentialManagementRequest request( version, CredentialManagementSubCommand::kGetCredsMetadata, - /*params=*/base::nullopt, - MakePINAuth(pin_token, - {{static_cast<uint8_t>( - CredentialManagementSubCommand::kGetCredsMetadata)}})); + /*params=*/base::nullopt); + request.pin_auth = token.PinAuth({{static_cast<uint8_t>( + CredentialManagementSubCommand::kGetCredsMetadata)}}); + return request; } // static CredentialManagementRequest CredentialManagementRequest::ForEnumerateRPsBegin( Version version, - base::span<const uint8_t> pin_token) { - return CredentialManagementRequest( + const pin::TokenResponse& token) { + CredentialManagementRequest request( version, CredentialManagementSubCommand::kEnumerateRPsBegin, - /*params=*/base::nullopt, - MakePINAuth(pin_token, - {{static_cast<uint8_t>( - CredentialManagementSubCommand::kEnumerateRPsBegin)}})); + /*params=*/base::nullopt); + request.pin_auth = token.PinAuth({{static_cast<uint8_t>( + CredentialManagementSubCommand::kEnumerateRPsBegin)}}); + return request; } // static @@ -75,30 +55,30 @@ CredentialManagementRequest CredentialManagementRequest::ForEnumerateRPsGetNext( Version version) { return CredentialManagementRequest( version, CredentialManagementSubCommand::kEnumerateRPsGetNextRP, - /*params=*/base::nullopt, - /*pin_auth=*/base::nullopt); + /*params=*/base::nullopt); } // static CredentialManagementRequest CredentialManagementRequest::ForEnumerateCredentialsBegin( Version version, - base::span<const uint8_t> pin_token, + const pin::TokenResponse& token, std::array<uint8_t, kRpIdHashLength> rp_id_hash) { cbor::Value::MapValue params_map; params_map.emplace( static_cast<int>(CredentialManagementRequestParamKey::kRPIDHash), std::move(rp_id_hash)); - base::Optional<std::vector<uint8_t>> pin_auth_bytes = - cbor::Writer::Write(cbor::Value(params_map)); - DCHECK(pin_auth_bytes); - pin_auth_bytes->insert( - pin_auth_bytes->begin(), + std::vector<uint8_t> pin_auth_bytes = + *cbor::Writer::Write(cbor::Value(params_map)); + CredentialManagementRequest request( + version, CredentialManagementSubCommand::kEnumerateCredentialsBegin, + std::move(params_map)); + pin_auth_bytes.insert( + pin_auth_bytes.begin(), static_cast<uint8_t>( CredentialManagementSubCommand::kEnumerateCredentialsBegin)); - return CredentialManagementRequest( - version, CredentialManagementSubCommand::kEnumerateCredentialsBegin, - std::move(params_map), MakePINAuth(pin_token, *pin_auth_bytes)); + request.pin_auth = token.PinAuth(pin_auth_bytes); + return request; } // static @@ -107,27 +87,28 @@ CredentialManagementRequest::ForEnumerateCredentialsGetNext(Version version) { return CredentialManagementRequest( version, CredentialManagementSubCommand::kEnumerateCredentialsGetNextCredential, - /*params=*/base::nullopt, /*pin_auth=*/base::nullopt); + /*params=*/base::nullopt); } // static CredentialManagementRequest CredentialManagementRequest::ForDeleteCredential( Version version, - base::span<const uint8_t> pin_token, + const pin::TokenResponse& token, const PublicKeyCredentialDescriptor& credential_id) { cbor::Value::MapValue params_map; params_map.emplace( static_cast<int>(CredentialManagementRequestParamKey::kCredentialID), AsCBOR(credential_id)); - base::Optional<std::vector<uint8_t>> pin_auth_bytes = - cbor::Writer::Write(cbor::Value(params_map)); - DCHECK(pin_auth_bytes); - pin_auth_bytes->insert( - pin_auth_bytes->begin(), - static_cast<uint8_t>(CredentialManagementSubCommand::kDeleteCredential)); - return CredentialManagementRequest( + std::vector<uint8_t> pin_auth_bytes = + *cbor::Writer::Write(cbor::Value(params_map)); + CredentialManagementRequest request( version, CredentialManagementSubCommand::kDeleteCredential, - std::move(params_map), MakePINAuth(pin_token, *pin_auth_bytes)); + std::move(params_map)); + pin_auth_bytes.insert( + pin_auth_bytes.begin(), + static_cast<uint8_t>(CredentialManagementSubCommand::kDeleteCredential)); + request.pin_auth = token.PinAuth(pin_auth_bytes); + return request; } // static @@ -349,8 +330,9 @@ AggregatedEnumerateCredentialsResponse::AggregatedEnumerateCredentialsResponse( : rp(std::move(rp_)), credentials() {} AggregatedEnumerateCredentialsResponse::AggregatedEnumerateCredentialsResponse( AggregatedEnumerateCredentialsResponse&&) = default; -AggregatedEnumerateCredentialsResponse& AggregatedEnumerateCredentialsResponse:: -operator=(AggregatedEnumerateCredentialsResponse&&) = default; +AggregatedEnumerateCredentialsResponse& +AggregatedEnumerateCredentialsResponse::operator=( + AggregatedEnumerateCredentialsResponse&&) = default; AggregatedEnumerateCredentialsResponse:: ~AggregatedEnumerateCredentialsResponse() = default; diff --git a/chromium/device/fido/credential_management.h b/chromium/device/fido/credential_management.h index 7f24fae7735..b0d3725e200 100644 --- a/chromium/device/fido/credential_management.h +++ b/chromium/device/fido/credential_management.h @@ -8,6 +8,7 @@ #include "base/component_export.h" #include "base/optional.h" #include "device/fido/fido_constants.h" +#include "device/fido/pin.h" #include "device/fido/public_key_credential_descriptor.h" #include "device/fido/public_key_credential_rp_entity.h" #include "device/fido/public_key_credential_user_entity.h" @@ -96,40 +97,36 @@ struct CredentialManagementRequest { static CredentialManagementRequest ForGetCredsMetadata( Version version, - base::span<const uint8_t> pin_token); + const pin::TokenResponse& token); static CredentialManagementRequest ForEnumerateRPsBegin( Version version, - base::span<const uint8_t> pin_token); + const pin::TokenResponse& token); static CredentialManagementRequest ForEnumerateRPsGetNext(Version version); static CredentialManagementRequest ForEnumerateCredentialsBegin( Version version, - base::span<const uint8_t> pin_token, + const pin::TokenResponse& token, std::array<uint8_t, kRpIdHashLength> rp_id_hash); static CredentialManagementRequest ForEnumerateCredentialsGetNext( Version version); static CredentialManagementRequest ForDeleteCredential( Version version, - base::span<const uint8_t> pin_token, + const pin::TokenResponse& token, const PublicKeyCredentialDescriptor& credential_id); + CredentialManagementRequest(Version version, + CredentialManagementSubCommand subcommand, + base::Optional<cbor::Value::MapValue> params); CredentialManagementRequest(CredentialManagementRequest&&); CredentialManagementRequest& operator=(CredentialManagementRequest&&); + CredentialManagementRequest(const CredentialManagementRequest&) = delete; + CredentialManagementRequest& operator=(const CredentialManagementRequest&) = + delete; ~CredentialManagementRequest(); Version version; CredentialManagementSubCommand subcommand; base::Optional<cbor::Value::MapValue> params; - base::Optional<std::array<uint8_t, 16>> pin_auth; - - private: - CredentialManagementRequest() = delete; - CredentialManagementRequest(Version version, - CredentialManagementSubCommand subcommand, - base::Optional<cbor::Value::MapValue> params, - base::Optional<std::array<uint8_t, 16>> pin_auth); - CredentialManagementRequest(const CredentialManagementRequest&) = delete; - CredentialManagementRequest& operator=(const CredentialManagementRequest&) = - delete; + base::Optional<std::vector<uint8_t>> pin_auth; }; struct CredentialsMetadataResponse { diff --git a/chromium/device/fido/credential_management_handler.cc b/chromium/device/fido/credential_management_handler.cc index 8d8c6fa44bc..409bb4c1fff 100644 --- a/chromium/device/fido/credential_management_handler.cc +++ b/chromium/device/fido/credential_management_handler.cc @@ -157,7 +157,7 @@ void CredentialManagementHandler::OnHavePINToken( } state_ = State::kReady; - pin_token_ = response->token(); + pin_token_ = response; std::move(ready_callback_).Run(); } diff --git a/chromium/device/fido/credential_management_handler.h b/chromium/device/fido/credential_management_handler.h index 757f3991879..63424609649 100644 --- a/chromium/device/fido/credential_management_handler.h +++ b/chromium/device/fido/credential_management_handler.h @@ -127,7 +127,7 @@ class COMPONENT_EXPORT(DEVICE_FIDO) CredentialManagementHandler State state_ = State::kWaitingForTouch; FidoAuthenticator* authenticator_ = nullptr; - base::Optional<std::vector<uint8_t>> pin_token_; + base::Optional<pin::TokenResponse> pin_token_; ReadyCallback ready_callback_; GetPINCallback get_pin_callback_; diff --git a/chromium/device/fido/ctap_get_assertion_request.h b/chromium/device/fido/ctap_get_assertion_request.h index 3e777f27aa0..bec63882032 100644 --- a/chromium/device/fido/ctap_get_assertion_request.h +++ b/chromium/device/fido/ctap_get_assertion_request.h @@ -19,6 +19,7 @@ #include "device/fido/cable/cable_discovery_data.h" #include "device/fido/client_data.h" #include "device/fido/fido_constants.h" +#include "device/fido/large_blob.h" #include "device/fido/pin.h" #include "device/fido/public_key_credential_descriptor.h" @@ -57,6 +58,10 @@ struct COMPONENT_EXPORT(DEVICE_FIDO) CtapGetAssertionOptions { // it will be the first element and all others will have |credential_id|s. // Elements are sorted by |credential_id|s, where present. std::vector<PRFInput> prf_inputs; + + // large_blob_operation indicates whether we should attempt to read or write a + // large blob after a successful assertion. + LargeBlobOperation large_blob_operation; }; // Object that encapsulates request parameters for AuthenticatorGetAssertion as @@ -121,6 +126,7 @@ struct COMPONENT_EXPORT(DEVICE_FIDO) CtapGetAssertionRequest { base::Optional<std::array<uint8_t, crypto::kSHA256Length>> alternative_application_parameter; base::Optional<HMACSecret> hmac_secret; + bool large_blob_key = false; bool is_incognito_mode = false; bool is_u2f_only = false; diff --git a/chromium/device/fido/ctap_make_credential_request.cc b/chromium/device/fido/ctap_make_credential_request.cc index bce0ccf50d6..0b78bf9bbb9 100644 --- a/chromium/device/fido/ctap_make_credential_request.cc +++ b/chromium/device/fido/ctap_make_credential_request.cc @@ -156,6 +156,11 @@ base::Optional<CtapMakeCredentialRequest> CtapMakeCredentialRequest::Parse( return base::nullopt; } request.android_client_data_ext = std::move(*android_client_data_ext); + } else if (extension_name == kExtensionLargeBlobKey) { + if (!extension.second.is_bool() || !extension.second.GetBool()) { + return base::nullopt; + } + request.large_blob_key = true; } } } @@ -272,6 +277,10 @@ AsCTAPRequestValuePair(const CtapMakeCredentialRequest& request) { extensions[cbor::Value(kExtensionHmacSecret)] = cbor::Value(true); } + if (request.large_blob_key) { + extensions[cbor::Value(kExtensionLargeBlobKey)] = cbor::Value(true); + } + if (request.cred_protect) { extensions.emplace(kExtensionCredProtect, static_cast<int64_t>(*request.cred_protect)); diff --git a/chromium/device/fido/ctap_make_credential_request.h b/chromium/device/fido/ctap_make_credential_request.h index 42dba7dfa84..bb88af3ce21 100644 --- a/chromium/device/fido/ctap_make_credential_request.h +++ b/chromium/device/fido/ctap_make_credential_request.h @@ -72,9 +72,12 @@ struct COMPONENT_EXPORT(DEVICE_FIDO) CtapMakeCredentialRequest { AuthenticatorAttachment authenticator_attachment = AuthenticatorAttachment::kAny; bool resident_key_required = false; - // hmac_secret_ indicates whether the "hmac-secret" extension should be + // hmac_secret indicates whether the "hmac-secret" extension should be // asserted to CTAP2 authenticators. bool hmac_secret = false; + // large_blob_key indicates whether a large blob key should be associated to + // the new credential through the "largeBlobKey" extension. + bool large_blob_key = false; // If true, instruct the request handler only to dispatch this request via // U2F. diff --git a/chromium/device/fido/device_response_converter.cc b/chromium/device/fido/device_response_converter.cc index 417de7f8a0d..c2ea71f5831 100644 --- a/chromium/device/fido/device_response_converter.cc +++ b/chromium/device/fido/device_response_converter.cc @@ -40,13 +40,14 @@ ProtocolVersion ConvertStringToProtocolVersion(base::StringPiece version) { return ProtocolVersion::kUnknown; } -Ctap2Version ConvertStringToCtap2Version(base::StringPiece version) { +base::Optional<Ctap2Version> ConvertStringToCtap2Version( + base::StringPiece version) { if (version == kCtap2Version) return Ctap2Version::kCtap2_0; if (version == kCtap2_1Version) return Ctap2Version::kCtap2_1; - return Ctap2Version::kUnknown; + return base::nullopt; } // Converts a CBOR unsigned integer value to a uint32_t. The conversion is @@ -67,7 +68,7 @@ CtapDeviceResponseCode GetResponseCode(base::span<const uint8_t> buffer) { return CtapDeviceResponseCode::kCtap2ErrInvalidCBOR; auto code = static_cast<CtapDeviceResponseCode>(buffer[0]); - return base::Contains(GetCtapResponseCodeList(), code) + return base::Contains(kCtapResponseCodeList, code) ? code : CtapDeviceResponseCode::kCtap2ErrInvalidCBOR; } @@ -120,6 +121,16 @@ ReadCTAPMakeCredentialResponse(FidoTransportProtocol transport_used, } } + it = decoded_map.find(CBOR(5)); + if (it != decoded_map.end()) { + if (!it->second.is_bytestring() || + it->second.GetBytestring().size() != kLargeBlobKeyLength) { + return base::nullopt; + } + response.set_large_blob_key( + base::make_span<kLargeBlobKeyLength>(it->second.GetBytestring())); + } + return response; } @@ -179,6 +190,16 @@ base::Optional<AuthenticatorGetAssertionResponse> ReadCTAPGetAssertionResponse( } } + it = response_map.find(CBOR(0x0B)); + if (it != response_map.end()) { + if (!it->second.is_bytestring() || + it->second.GetBytestring().size() != kLargeBlobKeyLength) { + return base::nullopt; + } + response.set_large_blob_key( + base::make_span<kLargeBlobKeyLength>(it->second.GetBytestring())); + } + return response; } @@ -224,21 +245,28 @@ base::Optional<AuthenticatorGetInfoResponse> ReadCTAPGetInfoResponse( return base::nullopt; } - auto protocol = ConvertStringToProtocolVersion(version_string); + ProtocolVersion protocol = ConvertStringToProtocolVersion(version_string); if (protocol == ProtocolVersion::kUnknown) { FIDO_LOG(DEBUG) << "Unexpected protocol version received."; continue; } if (protocol == ProtocolVersion::kCtap2) { - ctap2_versions.insert(ConvertStringToCtap2Version(version_string)); + base::Optional<Ctap2Version> ctap2_version = + ConvertStringToCtap2Version(version_string); + if (ctap2_version) { + ctap2_versions.insert(*ctap2_version); + } } protocol_versions.insert(protocol); } - if (protocol_versions.empty()) + if (protocol_versions.empty() || + (base::Contains(protocol_versions, ProtocolVersion::kCtap2) && + ctap2_versions.empty())) { return base::nullopt; + } it = response_map.find(CBOR(3)); if (it == response_map.end() || !it->second.is_bytestring() || @@ -404,6 +432,14 @@ base::Optional<AuthenticatorGetInfoResponse> ReadCTAPGetInfoResponse( options.enterprise_attestation = option_map_it->second.GetBool(); } + option_map_it = option_map.find(CBOR(kLargeBlobsKey)); + if (option_map_it != option_map.end()) { + if (!option_map_it->second.is_bool() || !options.supports_resident_key) { + return base::nullopt; + } + options.supports_large_blobs = option_map_it->second.GetBool(); + } + response.options = std::move(options); } diff --git a/chromium/device/fido/fake_fido_discovery.cc b/chromium/device/fido/fake_fido_discovery.cc index 46dd2cd9af9..952760c926d 100644 --- a/chromium/device/fido/fake_fido_discovery.cc +++ b/chromium/device/fido/fake_fido_discovery.cc @@ -79,23 +79,23 @@ FakeFidoDiscovery* FakeFidoDiscoveryFactory::ForgeNextPlatformDiscovery( return next_platform_discovery_.get(); } -std::unique_ptr<FidoDiscoveryBase> FakeFidoDiscoveryFactory::Create( - FidoTransportProtocol transport) { +std::vector<std::unique_ptr<FidoDiscoveryBase>> +FakeFidoDiscoveryFactory::Create(FidoTransportProtocol transport) { switch (transport) { case FidoTransportProtocol::kUsbHumanInterfaceDevice: - return std::move(next_hid_discovery_); + return SingleDiscovery(std::move(next_hid_discovery_)); case FidoTransportProtocol::kNearFieldCommunication: - return std::move(next_nfc_discovery_); + return SingleDiscovery(std::move(next_nfc_discovery_)); case FidoTransportProtocol::kBluetoothLowEnergy: case FidoTransportProtocol::kAndroidAccessory: - return nullptr; + return {}; case FidoTransportProtocol::kCloudAssistedBluetoothLowEnergy: - return std::move(next_cable_discovery_); + return SingleDiscovery(std::move(next_cable_discovery_)); case FidoTransportProtocol::kInternal: - return std::move(next_platform_discovery_); + return SingleDiscovery(std::move(next_platform_discovery_)); } NOTREACHED(); - return nullptr; + return {}; } } // namespace test diff --git a/chromium/device/fido/fake_fido_discovery.h b/chromium/device/fido/fake_fido_discovery.h index 365db9ccfa6..ee5aa1f9813 100644 --- a/chromium/device/fido/fake_fido_discovery.h +++ b/chromium/device/fido/fake_fido_discovery.h @@ -111,7 +111,7 @@ class FakeFidoDiscoveryFactory : public device::FidoDiscoveryFactory { StartMode mode = StartMode::kManual); // device::FidoDiscoveryFactory: - std::unique_ptr<FidoDiscoveryBase> Create( + std::vector<std::unique_ptr<FidoDiscoveryBase>> Create( FidoTransportProtocol transport) override; private: diff --git a/chromium/device/fido/fake_fido_discovery_unittest.cc b/chromium/device/fido/fake_fido_discovery_unittest.cc index 670db181ab6..6161e32e283 100644 --- a/chromium/device/fido/fake_fido_discovery_unittest.cc +++ b/chromium/device/fido/fake_fido_discovery_unittest.cc @@ -147,10 +147,11 @@ TEST_F(FakeFidoDiscoveryFactoryTest, ForgesUsbFactoryFunction) { fake_fido_discovery_factory_.ForgeNextHidDiscovery(); ASSERT_EQ(FidoTransportProtocol::kUsbHumanInterfaceDevice, injected_fake_discovery->transport()); - auto produced_discovery = fake_fido_discovery_factory_.Create( - FidoTransportProtocol::kUsbHumanInterfaceDevice); - EXPECT_TRUE(produced_discovery); - EXPECT_EQ(injected_fake_discovery, produced_discovery.get()); + std::vector<std::unique_ptr<FidoDiscoveryBase>> produced_discoveries = + fake_fido_discovery_factory_.Create( + FidoTransportProtocol::kUsbHumanInterfaceDevice); + ASSERT_EQ(produced_discoveries.size(), 1u); + EXPECT_EQ(injected_fake_discovery, produced_discoveries[0].get()); } #endif diff --git a/chromium/device/fido/fido_authenticator.cc b/chromium/device/fido/fido_authenticator.cc index 653b48f0c42..8f442c9b931 100644 --- a/chromium/device/fido/fido_authenticator.cc +++ b/chromium/device/fido/fido_authenticator.cc @@ -73,19 +73,19 @@ FidoAuthenticator::WillNeedPINToGetAssertion( } void FidoAuthenticator::GetCredentialsMetadata( - base::span<const uint8_t> pin_token, + const pin::TokenResponse& pin_token, GetCredentialsMetadataCallback callback) { NOTREACHED(); } void FidoAuthenticator::EnumerateCredentials( - base::span<const uint8_t> pin_token, + const pin::TokenResponse& pin_token, EnumerateCredentialsCallback callback) { NOTREACHED(); } void FidoAuthenticator::DeleteCredential( - base::span<const uint8_t> pin_token, + const pin::TokenResponse& pin_token, const PublicKeyCredentialDescriptor& credential_id, DeleteCredentialCallback callback) { NOTREACHED(); @@ -128,6 +128,21 @@ void FidoAuthenticator::BioEnrollDelete(const pin::TokenResponse&, NOTREACHED(); } +void FidoAuthenticator::WriteLargeBlob( + const std::vector<uint8_t>& large_blob, + const LargeBlobKey& large_blob_key, + const base::Optional<pin::TokenResponse> pin_uv_auth_token, + base::OnceCallback<void(CtapDeviceResponseCode)> callback) { + NOTREACHED(); +} + +void FidoAuthenticator::ReadLargeBlob( + const std::vector<LargeBlobKey>& large_blob_keys, + const base::Optional<pin::TokenResponse> pin_uv_auth_token, + LargeBlobReadCallback callback) { + NOTREACHED(); +} + base::Optional<base::span<const int32_t>> FidoAuthenticator::GetAlgorithms() { return base::nullopt; } diff --git a/chromium/device/fido/fido_authenticator.h b/chromium/device/fido/fido_authenticator.h index ad984032d61..5154563046e 100644 --- a/chromium/device/fido/fido_authenticator.h +++ b/chromium/device/fido/fido_authenticator.h @@ -5,6 +5,7 @@ #ifndef DEVICE_FIDO_FIDO_AUTHENTICATOR_H_ #define DEVICE_FIDO_FIDO_AUTHENTICATOR_H_ +#include <cstdint> #include <string> #include "base/callback_forward.h" @@ -20,8 +21,10 @@ #include "device/fido/authenticator_supported_options.h" #include "device/fido/bio/enrollment.h" #include "device/fido/credential_management.h" +#include "device/fido/fido_constants.h" #include "device/fido/fido_request_handler_base.h" #include "device/fido/fido_transport_protocol.h" +#include "device/fido/large_blob.h" namespace device { @@ -70,6 +73,10 @@ class COMPONENT_EXPORT(DEVICE_FIDO) FidoAuthenticator { using BioEnrollmentCallback = base::OnceCallback<void(CtapDeviceResponseCode, base::Optional<BioEnrollmentResponse>)>; + using LargeBlobReadCallback = base::OnceCallback<void( + CtapDeviceResponseCode, + base::Optional<std::vector<std::pair<LargeBlobKey, std::vector<uint8_t>>>> + callback)>; FidoAuthenticator() = default; virtual ~FidoAuthenticator() = default; @@ -175,12 +182,12 @@ class COMPONENT_EXPORT(DEVICE_FIDO) FidoAuthenticator { const CtapGetAssertionRequest& request, const FidoRequestHandlerBase::Observer* observer); - virtual void GetCredentialsMetadata(base::span<const uint8_t> pin_token, + virtual void GetCredentialsMetadata(const pin::TokenResponse& pin_token, GetCredentialsMetadataCallback callback); - virtual void EnumerateCredentials(base::span<const uint8_t> pin_token, + virtual void EnumerateCredentials(const pin::TokenResponse& pin_token, EnumerateCredentialsCallback callback); virtual void DeleteCredential( - base::span<const uint8_t> pin_token, + const pin::TokenResponse& pin_token, const PublicKeyCredentialDescriptor& credential_id, DeleteCredentialCallback callback); @@ -202,6 +209,21 @@ class COMPONENT_EXPORT(DEVICE_FIDO) FidoAuthenticator { std::vector<uint8_t> template_id, BioEnrollmentCallback); + // Large blob commands. + // Attempts to write a |large_blob| into the credential. If there is an + // existing credential for the |large_blob_key|, it will be overwritten. + virtual void WriteLargeBlob( + const std::vector<uint8_t>& large_blob, + const LargeBlobKey& large_blob_key, + base::Optional<pin::TokenResponse> pin_uv_auth_token, + base::OnceCallback<void(CtapDeviceResponseCode)> callback); + // Attempts to read large blobs from the credential encrypted with + // |large_blob_keys|. Returns a map of keys to their blobs. + virtual void ReadLargeBlob( + const std::vector<LargeBlobKey>& large_blob_keys, + base::Optional<pin::TokenResponse> pin_uv_auth_token, + LargeBlobReadCallback callback); + // GetAlgorithms returns the list of supported COSEAlgorithmIdentifiers, or // |nullopt| if this is unknown and thus all requests should be tried in case // they work. diff --git a/chromium/device/fido/fido_constants.cc b/chromium/device/fido/fido_constants.cc index 47f1d3d66ba..84b41eae320 100644 --- a/chromium/device/fido/fido_constants.cc +++ b/chromium/device/fido/fido_constants.cc @@ -36,6 +36,7 @@ const char kBioEnrollmentPreviewMapKey[] = "userVerificationMgmtPreview"; const char kPinUvTokenMapKey[] = "pinUvAuthToken"; const char kDefaultCredProtectKey[] = "defaultCredProtect"; const char kEnterpriseAttestationKey[] = "ep"; +const char kLargeBlobsKey[] = "largeBlobs"; const base::TimeDelta kDeviceTimeout = base::TimeDelta::FromSeconds(20); const base::TimeDelta kU2fRetryDelay = base::TimeDelta::FromMilliseconds(200); @@ -71,6 +72,7 @@ const char kCtap2_1Version[] = "FIDO_2_1"; const char kExtensionHmacSecret[] = "hmac-secret"; const char kExtensionCredProtect[] = "credProtect"; const char kExtensionAndroidClientData[] = "googleAndroidClientData"; +const char kExtensionLargeBlobKey[] = "largeBlobKey"; const base::TimeDelta kBleDevicePairingModeWaitingInterval = base::TimeDelta::FromSeconds(2); diff --git a/chromium/device/fido/fido_constants.h b/chromium/device/fido/fido_constants.h index e9d5e833177..043d4fceb8b 100644 --- a/chromium/device/fido/fido_constants.h +++ b/chromium/device/fido/fido_constants.h @@ -40,6 +40,10 @@ constexpr size_t kClientDataHashLength = 32; // https://www.w3.org/TR/webauthn/#sec-authenticator-data constexpr size_t kRpIdHashLength = 32; +// Length of the key used to encrypt large blobs. +// TODO(nsatragno): add a link to the spec once it's published. +constexpr size_t kLargeBlobKeyLength = 32; + // Max length for the user handle: // https://www.w3.org/TR/webauthn/#user-handle constexpr size_t kUserHandleMaxLength = 64; @@ -84,7 +88,7 @@ enum class CtapDeviceResponseCode : uint8_t { kCtap2ErrLimitExceeded = 0x15, kCtap2ErrUnsupportedExtension = 0x16, kCtap2ErrTooManyElements = 0x17, - kCtap2ErrExtensionNotSupported = 0x18, + kCtap2ErrLargeBlobStorageFull = 0x18, kCtap2ErrCredentialExcluded = 0x19, kCtap2ErrProcesssing = 0x21, kCtap2ErrInvalidCredential = 0x22, @@ -112,6 +116,8 @@ enum class CtapDeviceResponseCode : uint8_t { kCtap2ErrPinTokenExpired = 0x38, kCtap2ErrRequestTooLarge = 0x39, kCtap2ErrUvBlocked = 0x3C, + kCtap2ErrIntegrityFailure = 0x3D, + kCtap2ErrUvInvalid = 0x3F, kCtap2ErrOther = 0x7F, kCtap2ErrSpecLast = 0xDF, kCtap2ErrExtensionFirst = 0xE0, @@ -120,57 +126,59 @@ enum class CtapDeviceResponseCode : uint8_t { kCtap2ErrVendorLast = 0xFF }; -constexpr std::array<CtapDeviceResponseCode, 49> GetCtapResponseCodeList() { - return {CtapDeviceResponseCode::kSuccess, - CtapDeviceResponseCode::kCtap1ErrInvalidCommand, - CtapDeviceResponseCode::kCtap1ErrInvalidParameter, - CtapDeviceResponseCode::kCtap1ErrInvalidLength, - CtapDeviceResponseCode::kCtap1ErrInvalidSeq, - CtapDeviceResponseCode::kCtap1ErrTimeout, - CtapDeviceResponseCode::kCtap1ErrChannelBusy, - CtapDeviceResponseCode::kCtap1ErrLockRequired, - CtapDeviceResponseCode::kCtap1ErrInvalidChannel, - CtapDeviceResponseCode::kCtap2ErrCBORUnexpectedType, - CtapDeviceResponseCode::kCtap2ErrInvalidCBOR, - CtapDeviceResponseCode::kCtap2ErrMissingParameter, - CtapDeviceResponseCode::kCtap2ErrLimitExceeded, - CtapDeviceResponseCode::kCtap2ErrUnsupportedExtension, - CtapDeviceResponseCode::kCtap2ErrTooManyElements, - CtapDeviceResponseCode::kCtap2ErrExtensionNotSupported, - CtapDeviceResponseCode::kCtap2ErrCredentialExcluded, - CtapDeviceResponseCode::kCtap2ErrProcesssing, - CtapDeviceResponseCode::kCtap2ErrInvalidCredential, - CtapDeviceResponseCode::kCtap2ErrUserActionPending, - CtapDeviceResponseCode::kCtap2ErrOperationPending, - CtapDeviceResponseCode::kCtap2ErrNoOperations, - CtapDeviceResponseCode::kCtap2ErrUnsupportedAlgorithm, - CtapDeviceResponseCode::kCtap2ErrOperationDenied, - CtapDeviceResponseCode::kCtap2ErrKeyStoreFull, - CtapDeviceResponseCode::kCtap2ErrNotBusy, - CtapDeviceResponseCode::kCtap2ErrNoOperationPending, - CtapDeviceResponseCode::kCtap2ErrUnsupportedOption, - CtapDeviceResponseCode::kCtap2ErrInvalidOption, - CtapDeviceResponseCode::kCtap2ErrKeepAliveCancel, - CtapDeviceResponseCode::kCtap2ErrNoCredentials, - CtapDeviceResponseCode::kCtap2ErrUserActionTimeout, - CtapDeviceResponseCode::kCtap2ErrNotAllowed, - CtapDeviceResponseCode::kCtap2ErrPinInvalid, - CtapDeviceResponseCode::kCtap2ErrPinBlocked, - CtapDeviceResponseCode::kCtap2ErrPinAuthInvalid, - CtapDeviceResponseCode::kCtap2ErrPinAuthBlocked, - CtapDeviceResponseCode::kCtap2ErrPinNotSet, - CtapDeviceResponseCode::kCtap2ErrPinRequired, - CtapDeviceResponseCode::kCtap2ErrPinPolicyViolation, - CtapDeviceResponseCode::kCtap2ErrPinTokenExpired, - CtapDeviceResponseCode::kCtap2ErrRequestTooLarge, - CtapDeviceResponseCode::kCtap2ErrUvBlocked, - CtapDeviceResponseCode::kCtap2ErrOther, - CtapDeviceResponseCode::kCtap2ErrSpecLast, - CtapDeviceResponseCode::kCtap2ErrExtensionFirst, - CtapDeviceResponseCode::kCtap2ErrExtensionLast, - CtapDeviceResponseCode::kCtap2ErrVendorFirst, - CtapDeviceResponseCode::kCtap2ErrVendorLast}; -} +constexpr std::array<CtapDeviceResponseCode, 51> kCtapResponseCodeList{ + CtapDeviceResponseCode::kSuccess, + CtapDeviceResponseCode::kCtap1ErrInvalidCommand, + CtapDeviceResponseCode::kCtap1ErrInvalidParameter, + CtapDeviceResponseCode::kCtap1ErrInvalidLength, + CtapDeviceResponseCode::kCtap1ErrInvalidSeq, + CtapDeviceResponseCode::kCtap1ErrTimeout, + CtapDeviceResponseCode::kCtap1ErrChannelBusy, + CtapDeviceResponseCode::kCtap1ErrLockRequired, + CtapDeviceResponseCode::kCtap1ErrInvalidChannel, + CtapDeviceResponseCode::kCtap2ErrCBORUnexpectedType, + CtapDeviceResponseCode::kCtap2ErrInvalidCBOR, + CtapDeviceResponseCode::kCtap2ErrMissingParameter, + CtapDeviceResponseCode::kCtap2ErrLimitExceeded, + CtapDeviceResponseCode::kCtap2ErrUnsupportedExtension, + CtapDeviceResponseCode::kCtap2ErrTooManyElements, + CtapDeviceResponseCode::kCtap2ErrLargeBlobStorageFull, + CtapDeviceResponseCode::kCtap2ErrCredentialExcluded, + CtapDeviceResponseCode::kCtap2ErrProcesssing, + CtapDeviceResponseCode::kCtap2ErrInvalidCredential, + CtapDeviceResponseCode::kCtap2ErrUserActionPending, + CtapDeviceResponseCode::kCtap2ErrOperationPending, + CtapDeviceResponseCode::kCtap2ErrNoOperations, + CtapDeviceResponseCode::kCtap2ErrUnsupportedAlgorithm, + CtapDeviceResponseCode::kCtap2ErrOperationDenied, + CtapDeviceResponseCode::kCtap2ErrKeyStoreFull, + CtapDeviceResponseCode::kCtap2ErrNotBusy, + CtapDeviceResponseCode::kCtap2ErrNoOperationPending, + CtapDeviceResponseCode::kCtap2ErrUnsupportedOption, + CtapDeviceResponseCode::kCtap2ErrInvalidOption, + CtapDeviceResponseCode::kCtap2ErrKeepAliveCancel, + CtapDeviceResponseCode::kCtap2ErrNoCredentials, + CtapDeviceResponseCode::kCtap2ErrUserActionTimeout, + CtapDeviceResponseCode::kCtap2ErrNotAllowed, + CtapDeviceResponseCode::kCtap2ErrPinInvalid, + CtapDeviceResponseCode::kCtap2ErrPinBlocked, + CtapDeviceResponseCode::kCtap2ErrPinAuthInvalid, + CtapDeviceResponseCode::kCtap2ErrPinAuthBlocked, + CtapDeviceResponseCode::kCtap2ErrPinNotSet, + CtapDeviceResponseCode::kCtap2ErrPinRequired, + CtapDeviceResponseCode::kCtap2ErrPinPolicyViolation, + CtapDeviceResponseCode::kCtap2ErrPinTokenExpired, + CtapDeviceResponseCode::kCtap2ErrRequestTooLarge, + CtapDeviceResponseCode::kCtap2ErrUvBlocked, + CtapDeviceResponseCode::kCtap2ErrIntegrityFailure, + CtapDeviceResponseCode::kCtap2ErrUvInvalid, + CtapDeviceResponseCode::kCtap2ErrOther, + CtapDeviceResponseCode::kCtap2ErrSpecLast, + CtapDeviceResponseCode::kCtap2ErrExtensionFirst, + CtapDeviceResponseCode::kCtap2ErrExtensionLast, + CtapDeviceResponseCode::kCtap2ErrVendorFirst, + CtapDeviceResponseCode::kCtap2ErrVendorLast, +}; // Commands supported by CTAPHID device as specified in // https://fidoalliance.org/specs/fido-v2.0-rd-20170927/fido-client-to-authenticator-protocol-v2.0-rd-20170927.html#ctaphid-commands @@ -228,6 +236,7 @@ enum class CtapRequestCommand : uint8_t { kAuthenticatorClientPin = 0x06, kAuthenticatorReset = 0x07, kAuthenticatorBioEnrollment = 0x09, + kAuthenticatorLargeBlobs = 0x0C, kAuthenticatorBioEnrollmentPreview = 0x40, kAuthenticatorCredentialManagement = 0x0a, kAuthenticatorCredentialManagementPreview = 0x41, @@ -325,6 +334,7 @@ COMPONENT_EXPORT(DEVICE_FIDO) extern const char kBioEnrollmentPreviewMapKey[]; COMPONENT_EXPORT(DEVICE_FIDO) extern const char kPinUvTokenMapKey[]; extern const char kDefaultCredProtectKey[]; extern const char kEnterpriseAttestationKey[]; +extern const char kLargeBlobsKey[]; // HID transport specific constants. constexpr uint32_t kHidBroadcastChannel = 0xffffffff; @@ -368,8 +378,19 @@ constexpr char kCableWebSocketProtocol[] = "fido.cable"; // kCableShardIdHeader is the name of an HTTP header that is sent in the reply // from the tunnel server and which specifies the server's chosen shard number. +// TODO(agl): remove. Only being kept around to allow things to compile. constexpr char kCableShardIdHeader[] = "X-caBLE-Shard"; +// kCableRoutingIdHeader is the name of an HTTP header that is sent in the reply +// from the tunnel server and which specifies the server's chosen routing ID +// which other parties can use to reach the same tunnel server. +constexpr char kCableRoutingIdHeader[] = "X-caBLE-Routing-ID"; + +// kCableClientPayloadHeader is the name of an HTTP header that is to +// the tunnel server when performing a state-assisted handshake and which +// includes the client's nonce and pairing ID. +constexpr char kCableClientPayloadHeader[] = "X-caBLE-Client-Payload"; + // Maximum wait time before client error outs on device. COMPONENT_EXPORT(DEVICE_FIDO) extern const base::TimeDelta kDeviceTimeout; @@ -400,11 +421,11 @@ COMPONENT_EXPORT(DEVICE_FIDO) extern const char kCableAuthenticatorHelloMessage[]; COMPONENT_EXPORT(DEVICE_FIDO) extern const char kCableClientHelloMessage[]; -enum class Ctap2Version { - kUnknown = 0, - kCtap2_0 = 1, - kCtap2_1 = 2, -}; +// The list of CTAP versions returned in the getInfo response for different +// minor versions. +constexpr Ctap2Version kCtap2Versions2_0[] = {Ctap2Version::kCtap2_0}; +constexpr Ctap2Version kCtap2Versions2_1[] = {Ctap2Version::kCtap2_0, + Ctap2Version::kCtap2_1}; // Protocol version strings. // https://fidoalliance.org/specs/fido-v2.0-ps-20190130/fido-client-to-authenticator-protocol-v2.0-ps-20190130.html#authenticatorGetInfo @@ -419,6 +440,7 @@ COMPONENT_EXPORT(DEVICE_FIDO) extern const char kExtensionHmacSecret[]; COMPONENT_EXPORT(DEVICE_FIDO) extern const char kExtensionCredProtect[]; COMPONENT_EXPORT(DEVICE_FIDO) extern const char kExtensionAndroidClientData[]; +COMPONENT_EXPORT(DEVICE_FIDO) extern const char kExtensionLargeBlobKey[]; // Maximum number of seconds the browser waits for Bluetooth authenticator to // send packets that advertises that the device is in pairing mode before diff --git a/chromium/device/fido/fido_device_authenticator.cc b/chromium/device/fido/fido_device_authenticator.cc index bf6405e1cd7..0690ea31b66 100644 --- a/chromium/device/fido/fido_device_authenticator.cc +++ b/chromium/device/fido/fido_device_authenticator.cc @@ -4,6 +4,7 @@ #include "device/fido/fido_device_authenticator.h" +#include <algorithm> #include <numeric> #include <utility> @@ -16,9 +17,11 @@ #include "device/fido/ctap_get_assertion_request.h" #include "device/fido/ctap_make_credential_request.h" #include "device/fido/features.h" +#include "device/fido/fido_constants.h" #include "device/fido/fido_device.h" #include "device/fido/fido_parsing_utils.h" #include "device/fido/get_assertion_task.h" +#include "device/fido/large_blob.h" #include "device/fido/make_credential_task.h" #include "device/fido/pin.h" #include "device/fido/u2f_command_constructor.h" @@ -338,7 +341,6 @@ FidoDeviceAuthenticator::WillNeedPINToMakeCredential( : MakeCredentialPINDisposition::kNoPIN; } - // CTAP 2.0 requires a PIN for credential creation once a PIN has been set. // Thus, if fallback to U2F isn't possible, a PIN will be needed if set. const bool u2f_fallback_possible = @@ -436,7 +438,7 @@ FidoDeviceAuthenticator::WillNeedPINToGetAssertion( } void FidoDeviceAuthenticator::GetCredentialsMetadata( - base::span<const uint8_t> pin_token, + const pin::TokenResponse& pin_token, GetCredentialsMetadataCallback callback) { DCHECK(Options()->supports_credential_management || Options()->supports_credential_management_preview); @@ -448,11 +450,12 @@ void FidoDeviceAuthenticator::GetCredentialsMetadata( } struct FidoDeviceAuthenticator::EnumerateCredentialsState { - EnumerateCredentialsState() = default; + explicit EnumerateCredentialsState(pin::TokenResponse pin_token_) + : pin_token(pin_token_) {} EnumerateCredentialsState(EnumerateCredentialsState&&) = default; EnumerateCredentialsState& operator=(EnumerateCredentialsState&&) = default; - std::vector<uint8_t> pin_token; + pin::TokenResponse pin_token; bool is_first_rp = true; bool is_first_credential = true; size_t rp_count; @@ -463,13 +466,12 @@ struct FidoDeviceAuthenticator::EnumerateCredentialsState { }; void FidoDeviceAuthenticator::EnumerateCredentials( - base::span<const uint8_t> pin_token, + const pin::TokenResponse& pin_token, EnumerateCredentialsCallback callback) { DCHECK(Options()->supports_credential_management || Options()->supports_credential_management_preview); - EnumerateCredentialsState state; - state.pin_token = fido_parsing_utils::Materialize(pin_token); + EnumerateCredentialsState state(pin_token); state.callback = std::move(callback); RunOperation<CredentialManagementRequest, EnumerateRPsResponse>( CredentialManagementRequest::ForEnumerateRPsBegin( @@ -626,7 +628,7 @@ void FidoDeviceAuthenticator::OnEnumerateCredentialsDone( } void FidoDeviceAuthenticator::DeleteCredential( - base::span<const uint8_t> pin_token, + const pin::TokenResponse& pin_token, const PublicKeyCredentialDescriptor& credential_id, DeleteCredentialCallback callback) { DCHECK(Options()->supports_credential_management || @@ -699,14 +701,201 @@ void FidoDeviceAuthenticator::BioEnrollCancel(BioEnrollmentCallback callback) { } void FidoDeviceAuthenticator::BioEnrollEnumerate( - const pin::TokenResponse& response, + const pin::TokenResponse& pin_token, BioEnrollmentCallback callback) { RunOperation<BioEnrollmentRequest, BioEnrollmentResponse>( BioEnrollmentRequest::ForEnumerate( - GetBioEnrollmentRequestVersion(*Options()), std::move(response)), + GetBioEnrollmentRequestVersion(*Options()), std::move(pin_token)), std::move(callback), base::BindOnce(&BioEnrollmentResponse::Parse)); } +void FidoDeviceAuthenticator::WriteLargeBlob( + const std::vector<uint8_t>& large_blob, + const LargeBlobKey& large_blob_key, + const base::Optional<pin::TokenResponse> pin_uv_auth_token, + base::OnceCallback<void(CtapDeviceResponseCode)> callback) { + auto pin_uv_auth_token_copy = pin_uv_auth_token; + FetchLargeBlobArray( + pin_uv_auth_token_copy, LargeBlobArrayReader(), + base::BindOnce(&FidoDeviceAuthenticator::OnHaveLargeBlobArrayForWrite, + weak_factory_.GetWeakPtr(), large_blob, large_blob_key, + std::move(pin_uv_auth_token), std::move(callback))); +} + +void FidoDeviceAuthenticator::ReadLargeBlob( + const std::vector<LargeBlobKey>& large_blob_keys, + const base::Optional<pin::TokenResponse> pin_uv_auth_token, + LargeBlobReadCallback callback) { + FetchLargeBlobArray( + std::move(pin_uv_auth_token), LargeBlobArrayReader(), + base::BindOnce(&FidoDeviceAuthenticator::OnHaveLargeBlobArrayForRead, + weak_factory_.GetWeakPtr(), large_blob_keys, + std::move(callback))); +} + +void FidoDeviceAuthenticator::FetchLargeBlobArray( + const base::Optional<pin::TokenResponse> pin_uv_auth_token, + LargeBlobArrayReader large_blob_array_reader, + base::OnceCallback<void(CtapDeviceResponseCode, + base::Optional<LargeBlobArrayReader>)> callback) { + size_t bytes_to_read = max_large_blob_fragment_length(); + LargeBlobsRequest request = + LargeBlobsRequest::ForRead(bytes_to_read, large_blob_array_reader.size()); + if (pin_uv_auth_token) { + request.SetPinParam(*pin_uv_auth_token); + } + RunOperation<LargeBlobsRequest, LargeBlobsResponse>( + std::move(request), + base::BindOnce(&FidoDeviceAuthenticator::OnReadLargeBlobFragment, + weak_factory_.GetWeakPtr(), bytes_to_read, + std::move(large_blob_array_reader), + std::move(pin_uv_auth_token), std::move(callback)), + base::BindOnce(&LargeBlobsResponse::ParseForRead, bytes_to_read)); +} + +void FidoDeviceAuthenticator::OnReadLargeBlobFragment( + const size_t bytes_requested, + LargeBlobArrayReader large_blob_array_reader, + const base::Optional<pin::TokenResponse> pin_uv_auth_token, + base::OnceCallback<void(CtapDeviceResponseCode, + base::Optional<LargeBlobArrayReader>)> callback, + CtapDeviceResponseCode status, + base::Optional<LargeBlobsResponse> response) { + if (status != CtapDeviceResponseCode::kSuccess) { + std::move(callback).Run(status, base::nullopt); + return; + } + + DCHECK(response && response->config()); + large_blob_array_reader.Append(*response->config()); + + if (response->config()->size() == bytes_requested) { + // More data may be available, read the next fragment. + FetchLargeBlobArray(std::move(pin_uv_auth_token), + std::move(large_blob_array_reader), + std::move(callback)); + return; + } + + std::move(callback).Run(CtapDeviceResponseCode::kSuccess, + std::move(large_blob_array_reader)); +} + +void FidoDeviceAuthenticator::OnHaveLargeBlobArrayForWrite( + const std::vector<uint8_t>& large_blob, + const LargeBlobKey& large_blob_key, + const base::Optional<pin::TokenResponse> pin_uv_auth_token, + base::OnceCallback<void(CtapDeviceResponseCode)> callback, + CtapDeviceResponseCode status, + base::Optional<LargeBlobArrayReader> large_blob_array_reader) { + if (status != CtapDeviceResponseCode::kSuccess) { + std::move(callback).Run(status); + return; + } + + base::Optional<std::vector<LargeBlobData>> large_blob_array = + large_blob_array_reader->Materialize(); + if (!large_blob_array) { + // The large blob array is corrupted. Replace it completely with a new one. + // TODO(nsatragno): but maybe we want to do something else like trying + // again? It might have been corrupted while transported. Decide when we + // have hardware to test. + large_blob_array.emplace(); + return; + } + + auto existing_large_blob = + std::find_if(large_blob_array->begin(), large_blob_array->end(), + [&large_blob_key](const LargeBlobData& blob) { + return blob.Decrypt(large_blob_key); + }); + + LargeBlobData new_large_blob_data(large_blob_key, large_blob); + if (existing_large_blob != large_blob_array->end()) { + *existing_large_blob = std::move(new_large_blob_data); + } else { + large_blob_array->emplace_back(std::move(new_large_blob_data)); + } + + WriteLargeBlobArray(std::move(pin_uv_auth_token), + LargeBlobArrayWriter(*large_blob_array), + std::move(callback)); +} + +void FidoDeviceAuthenticator::WriteLargeBlobArray( + const base::Optional<pin::TokenResponse> pin_uv_auth_token, + LargeBlobArrayWriter large_blob_array_writer, + base::OnceCallback<void(CtapDeviceResponseCode)> callback) { + LargeBlobArrayFragment fragment = + large_blob_array_writer.Pop(max_large_blob_fragment_length()); + + LargeBlobsRequest request = LargeBlobsRequest::ForWrite( + std::move(fragment), large_blob_array_writer.size()); + if (pin_uv_auth_token) { + request.SetPinParam(*pin_uv_auth_token); + } + RunOperation<LargeBlobsRequest, LargeBlobsResponse>( + std::move(request), + base::BindOnce(&FidoDeviceAuthenticator::OnWriteLargeBlobFragment, + weak_factory_.GetWeakPtr(), + std::move(large_blob_array_writer), + std::move(pin_uv_auth_token), std::move(callback)), + base::BindOnce(&LargeBlobsResponse::ParseForWrite)); +} + +void FidoDeviceAuthenticator::OnWriteLargeBlobFragment( + LargeBlobArrayWriter large_blob_array_writer, + const base::Optional<pin::TokenResponse> pin_uv_auth_token, + base::OnceCallback<void(CtapDeviceResponseCode)> callback, + CtapDeviceResponseCode status, + base::Optional<LargeBlobsResponse> response) { + if (status != CtapDeviceResponseCode::kSuccess) { + std::move(callback).Run(status); + return; + } + + if (large_blob_array_writer.has_remaining_fragments()) { + WriteLargeBlobArray(std::move(pin_uv_auth_token), + std::move(large_blob_array_writer), + std::move(callback)); + return; + } + + std::move(callback).Run(CtapDeviceResponseCode::kSuccess); +} + +void FidoDeviceAuthenticator::OnHaveLargeBlobArrayForRead( + const std::vector<LargeBlobKey>& large_blob_keys, + LargeBlobReadCallback callback, + CtapDeviceResponseCode status, + base::Optional<LargeBlobArrayReader> large_blob_array_reader) { + if (status != CtapDeviceResponseCode::kSuccess) { + std::move(callback).Run(status, base::nullopt); + return; + } + + base::Optional<std::vector<LargeBlobData>> large_blob_array = + large_blob_array_reader->Materialize(); + if (!large_blob_array) { + std::move(callback).Run(CtapDeviceResponseCode::kCtap2ErrIntegrityFailure, + base::nullopt); + return; + } + + std::vector<std::pair<LargeBlobKey, std::vector<uint8_t>>> result; + for (const LargeBlobData& blob : *large_blob_array) { + for (const LargeBlobKey& key : large_blob_keys) { + base::Optional<std::vector<uint8_t>> plaintext = blob.Decrypt(key); + if (plaintext) { + result.emplace_back(std::make_pair(key, std::move(*plaintext))); + break; + } + } + } + + std::move(callback).Run(CtapDeviceResponseCode::kSuccess, std::move(result)); +} + base::Optional<base::span<const int32_t>> FidoDeviceAuthenticator::GetAlgorithms() { if (device_->supported_protocol() == ProtocolVersion::kU2f) { @@ -860,6 +1049,13 @@ void FidoDeviceAuthenticator::OnHaveEphemeralKeyForUvToken( base::BindOnce(&pin::TokenResponse::Parse, std::move(shared_key))); } +size_t FidoDeviceAuthenticator::max_large_blob_fragment_length() { + return device_->device_info()->max_msg_size + ? *device_->device_info()->max_msg_size - + kLargeBlobReadEncodingOverhead + : kLargeBlobDefaultMaxFragmentLength; +} + base::WeakPtr<FidoAuthenticator> FidoDeviceAuthenticator::GetWeakPtr() { return weak_factory_.GetWeakPtr(); } diff --git a/chromium/device/fido/fido_device_authenticator.h b/chromium/device/fido/fido_device_authenticator.h index 771df599a00..06e875ba5cc 100644 --- a/chromium/device/fido/fido_device_authenticator.h +++ b/chromium/device/fido/fido_device_authenticator.h @@ -17,7 +17,10 @@ #include "build/build_config.h" #include "device/fido/ctap2_device_operation.h" #include "device/fido/fido_authenticator.h" +#include "device/fido/fido_constants.h" #include "device/fido/fido_request_handler_base.h" +#include "device/fido/large_blob.h" +#include "device/fido/pin.h" namespace device { @@ -70,11 +73,11 @@ class COMPONENT_EXPORT(DEVICE_FIDO) FidoDeviceAuthenticator const CtapGetAssertionRequest& request, const FidoRequestHandlerBase::Observer* observer) override; - void GetCredentialsMetadata(base::span<const uint8_t> pin_token, + void GetCredentialsMetadata(const pin::TokenResponse& pin_token, GetCredentialsMetadataCallback callback) override; - void EnumerateCredentials(base::span<const uint8_t> pin_token, + void EnumerateCredentials(const pin::TokenResponse& pin_token, EnumerateCredentialsCallback callback) override; - void DeleteCredential(base::span<const uint8_t> pin_token, + void DeleteCredential(const pin::TokenResponse& pin_token, const PublicKeyCredentialDescriptor& credential_id, DeleteCredentialCallback callback) override; @@ -93,6 +96,14 @@ class COMPONENT_EXPORT(DEVICE_FIDO) FidoDeviceAuthenticator void BioEnrollDelete(const pin::TokenResponse&, std::vector<uint8_t> template_id, BioEnrollmentCallback) override; + void WriteLargeBlob( + const std::vector<uint8_t>& large_blob, + const LargeBlobKey& large_blob_key, + base::Optional<pin::TokenResponse> pin_uv_auth_token, + base::OnceCallback<void(CtapDeviceResponseCode)> callback) override; + void ReadLargeBlob(const std::vector<LargeBlobKey>& large_blob_keys, + base::Optional<pin::TokenResponse> pin_uv_auth_token, + LargeBlobReadCallback callback) override; base::Optional<base::span<const int32_t>> GetAlgorithms() override; void Reset(ResetCallback callback) override; @@ -168,6 +179,42 @@ class COMPONENT_EXPORT(DEVICE_FIDO) FidoDeviceAuthenticator CtapDeviceResponseCode status, base::Optional<pin::KeyAgreementResponse> key); + void FetchLargeBlobArray( + base::Optional<pin::TokenResponse> pin_uv_auth_token, + LargeBlobArrayReader large_blob_array_reader, + base::OnceCallback<void(CtapDeviceResponseCode, + base::Optional<LargeBlobArrayReader>)> callback); + void WriteLargeBlobArray( + base::Optional<pin::TokenResponse> pin_uv_auth_token, + LargeBlobArrayWriter large_blob_array_writer, + base::OnceCallback<void(CtapDeviceResponseCode)> callback); + void OnReadLargeBlobFragment( + const size_t bytes_requested, + LargeBlobArrayReader large_blob_array_reader, + base::Optional<pin::TokenResponse> pin_uv_auth_token, + base::OnceCallback<void(CtapDeviceResponseCode, + base::Optional<LargeBlobArrayReader>)> callback, + CtapDeviceResponseCode status, + base::Optional<LargeBlobsResponse> response); + void OnWriteLargeBlobFragment( + LargeBlobArrayWriter large_blob_array_writer, + base::Optional<pin::TokenResponse> pin_uv_auth_token, + base::OnceCallback<void(CtapDeviceResponseCode)> callback, + CtapDeviceResponseCode status, + base::Optional<LargeBlobsResponse> response); + void OnHaveLargeBlobArrayForWrite( + const std::vector<uint8_t>& large_blob, + const LargeBlobKey& large_blob_key, + base::Optional<pin::TokenResponse> pin_uv_auth_token, + base::OnceCallback<void(CtapDeviceResponseCode)> callback, + CtapDeviceResponseCode status, + base::Optional<LargeBlobArrayReader> large_blob_array_reader); + void OnHaveLargeBlobArrayForRead( + const std::vector<LargeBlobKey>& large_blob_keys, + LargeBlobReadCallback callback, + CtapDeviceResponseCode status, + base::Optional<LargeBlobArrayReader> large_blob_array_reader); + template <typename... Args> void TaskClearProxy(base::OnceCallback<void(Args...)> callback, Args... args); template <typename... Args> @@ -195,6 +242,8 @@ class COMPONENT_EXPORT(DEVICE_FIDO) FidoDeviceAuthenticator CtapDeviceResponseCode status, base::Optional<EnumerateCredentialsResponse> response); + size_t max_large_blob_fragment_length(); + const std::unique_ptr<FidoDevice> device_; base::Optional<AuthenticatorSupportedOptions> options_; std::unique_ptr<FidoTask> task_; diff --git a/chromium/device/fido/fido_device_authenticator_unittest.cc b/chromium/device/fido/fido_device_authenticator_unittest.cc new file mode 100644 index 00000000000..5ba52e756f7 --- /dev/null +++ b/chromium/device/fido/fido_device_authenticator_unittest.cc @@ -0,0 +1,218 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "device/fido/fido_device_authenticator.h" + +#include <memory> + +#include "base/memory/scoped_refptr.h" +#include "base/optional.h" +#include "base/test/task_environment.h" +#include "device/fido/fido_constants.h" +#include "device/fido/fido_parsing_utils.h" +#include "device/fido/large_blob.h" +#include "device/fido/pin.h" +#include "device/fido/test_callback_receiver.h" +#include "device/fido/virtual_ctap2_device.h" +#include "device/fido/virtual_fido_device.h" +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace device { + +namespace { + +using WriteCallback = + device::test::ValueCallbackReceiver<CtapDeviceResponseCode>; +using ReadCallback = device::test::StatusAndValueCallbackReceiver< + CtapDeviceResponseCode, + base::Optional<std::vector<std::pair<LargeBlobKey, std::vector<uint8_t>>>>>; +using PinCallback = device::test::StatusAndValueCallbackReceiver< + CtapDeviceResponseCode, + base::Optional<pin::TokenResponse>>; + +constexpr LargeBlobKey kDummyKey1 = {{0x01}}; +constexpr LargeBlobKey kDummyKey2 = {{0x02}}; +constexpr std::array<uint8_t, 4> kSmallBlob1 = {'r', 'o', 's', 'a'}; +constexpr std::array<uint8_t, 4> kSmallBlob2 = {'l', 'u', 'm', 'a'}; +constexpr std::array<uint8_t, 4> kSmallBlob3 = {'s', 't', 'a', 'r'}; +constexpr size_t kMaxStorageSize = 4096; +constexpr char kPin[] = "1234"; + +class FidoDeviceAuthenticatorTest : public testing::Test { + public: + void SetUp() override { + VirtualCtap2Device::Config config; + config.pin_support = true; + config.large_blob_support = true; + config.resident_key_support = true; + config.available_large_blob_storage = kMaxStorageSize; + config.pin_uv_auth_token_support = true; + config.ctap2_versions = {Ctap2Version::kCtap2_1}; + + authenticator_state_ = base::MakeRefCounted<VirtualFidoDevice::State>(); + auto virtual_device = + std::make_unique<VirtualCtap2Device>(authenticator_state_, config); + virtual_device_ = virtual_device.get(); + authenticator_ = + std::make_unique<FidoDeviceAuthenticator>(std::move(virtual_device)); + + device::test::TestCallbackReceiver<> callback; + authenticator_->InitializeAuthenticator(callback.callback()); + callback.WaitForCallback(); + } + + protected: + scoped_refptr<VirtualFidoDevice::State> authenticator_state_; + std::unique_ptr<FidoDeviceAuthenticator> authenticator_; + VirtualCtap2Device* virtual_device_; + + private: + base::test::SingleThreadTaskEnvironment task_environment_; +}; + +TEST_F(FidoDeviceAuthenticatorTest, TestReadEmptyLargeBlob) { + ReadCallback callback; + authenticator_->ReadLargeBlob({kDummyKey1}, base::nullopt, + callback.callback()); + + callback.WaitForCallback(); + EXPECT_EQ(CtapDeviceResponseCode::kSuccess, callback.status()); + EXPECT_EQ(0u, callback.value()->size()); +} + +TEST_F(FidoDeviceAuthenticatorTest, TestReadInvalidLargeBlob) { + authenticator_state_->large_blob[0] += 1; + ReadCallback callback; + authenticator_->ReadLargeBlob({kDummyKey1}, base::nullopt, + callback.callback()); + + callback.WaitForCallback(); + EXPECT_EQ(CtapDeviceResponseCode::kCtap2ErrIntegrityFailure, + callback.status()); + EXPECT_FALSE(callback.value()); +} + +// Test reading and writing a blob that fits in a single fragment. +TEST_F(FidoDeviceAuthenticatorTest, TestWriteSmallBlob) { + std::vector<uint8_t> small_blob = + fido_parsing_utils::Materialize(kSmallBlob1); + WriteCallback write_callback; + authenticator_->WriteLargeBlob(small_blob, {kDummyKey1}, base::nullopt, + write_callback.callback()); + + write_callback.WaitForCallback(); + ASSERT_EQ(CtapDeviceResponseCode::kSuccess, write_callback.value()); + + ReadCallback read_callback; + authenticator_->ReadLargeBlob({kDummyKey1}, base::nullopt, + read_callback.callback()); + read_callback.WaitForCallback(); + ASSERT_EQ(CtapDeviceResponseCode::kSuccess, read_callback.status()); + auto large_blob_array = read_callback.value(); + ASSERT_TRUE(large_blob_array); + ASSERT_EQ(1u, large_blob_array->size()); + EXPECT_EQ(kDummyKey1, large_blob_array->at(0).first); + EXPECT_EQ(small_blob, large_blob_array->at(0).second); +} + +// Test reading and writing a blob that must fit in multiple fragments. +TEST_F(FidoDeviceAuthenticatorTest, TestWriteLargeBlob) { + std::vector<uint8_t> large_blob; + large_blob.reserve(2048); + for (size_t i = 0; i < large_blob.capacity(); ++i) { + large_blob.emplace_back(i % 0xFF); + } + + WriteCallback write_callback; + authenticator_->WriteLargeBlob(large_blob, {kDummyKey1}, base::nullopt, + write_callback.callback()); + + write_callback.WaitForCallback(); + ASSERT_EQ(CtapDeviceResponseCode::kSuccess, write_callback.value()); + + ReadCallback read_callback; + authenticator_->ReadLargeBlob({kDummyKey1}, base::nullopt, + read_callback.callback()); + read_callback.WaitForCallback(); + ASSERT_EQ(CtapDeviceResponseCode::kSuccess, read_callback.status()); + auto large_blob_array = read_callback.value(); + ASSERT_TRUE(large_blob_array); + ASSERT_EQ(1u, large_blob_array->size()); + EXPECT_EQ(kDummyKey1, large_blob_array->at(0).first); + EXPECT_EQ(large_blob, large_blob_array->at(0).second); +} + +// Test reading and writing a blob using a PinUvAuthToken. +TEST_F(FidoDeviceAuthenticatorTest, TestWriteSmallBlobWithToken) { + virtual_device_->SetPin(kPin); + PinCallback pin_callback; + authenticator_->GetPINToken(kPin, {pin::Permissions::kLargeBlobWrite}, + /*rp_id=*/base::nullopt, pin_callback.callback()); + pin_callback.WaitForCallback(); + ASSERT_EQ(CtapDeviceResponseCode::kSuccess, pin_callback.status()); + pin::TokenResponse pin_token = *pin_callback.value(); + + std::vector<uint8_t> small_blob = + fido_parsing_utils::Materialize(kSmallBlob1); + WriteCallback write_callback; + authenticator_->WriteLargeBlob(small_blob, {kDummyKey1}, pin_token, + write_callback.callback()); + write_callback.WaitForCallback(); + ASSERT_EQ(CtapDeviceResponseCode::kSuccess, write_callback.value()); + + ReadCallback read_callback; + authenticator_->ReadLargeBlob({kDummyKey1}, pin_token, + read_callback.callback()); + read_callback.WaitForCallback(); + ASSERT_EQ(CtapDeviceResponseCode::kSuccess, read_callback.status()); + auto large_blob_array = read_callback.value(); + ASSERT_TRUE(large_blob_array); + ASSERT_EQ(1u, large_blob_array->size()); + EXPECT_EQ(kDummyKey1, large_blob_array->at(0).first); + EXPECT_EQ(small_blob, large_blob_array->at(0).second); +} + +// Test updating a large blob in an array with multiple entries corresponding to +// other keys. +TEST_F(FidoDeviceAuthenticatorTest, TestUpdateLargeBlob) { + WriteCallback write_callback1; + authenticator_->WriteLargeBlob(fido_parsing_utils::Materialize(kSmallBlob1), + {kDummyKey1}, base::nullopt, + write_callback1.callback()); + write_callback1.WaitForCallback(); + ASSERT_EQ(CtapDeviceResponseCode::kSuccess, write_callback1.value()); + + WriteCallback write_callback2; + std::vector<uint8_t> small_blob2 = + fido_parsing_utils::Materialize(kSmallBlob2); + authenticator_->WriteLargeBlob(small_blob2, {kDummyKey2}, base::nullopt, + write_callback2.callback()); + write_callback2.WaitForCallback(); + ASSERT_EQ(CtapDeviceResponseCode::kSuccess, write_callback2.value()); + + // Update the first entry. + WriteCallback write_callback3; + std::vector<uint8_t> small_blob3 = + fido_parsing_utils::Materialize(kSmallBlob3); + authenticator_->WriteLargeBlob(small_blob3, {kDummyKey1}, base::nullopt, + write_callback3.callback()); + write_callback3.WaitForCallback(); + ASSERT_EQ(CtapDeviceResponseCode::kSuccess, write_callback3.value()); + + ReadCallback read_callback; + authenticator_->ReadLargeBlob({kDummyKey1, kDummyKey2}, base::nullopt, + read_callback.callback()); + read_callback.WaitForCallback(); + ASSERT_EQ(CtapDeviceResponseCode::kSuccess, read_callback.status()); + auto large_blob_array = read_callback.value(); + ASSERT_TRUE(large_blob_array); + EXPECT_THAT(*large_blob_array, testing::UnorderedElementsAre( + std::make_pair(kDummyKey1, small_blob3), + std::make_pair(kDummyKey2, small_blob2))); +} + +} // namespace + +} // namespace device diff --git a/chromium/device/fido/fido_device_discovery.cc b/chromium/device/fido/fido_device_discovery.cc index a581c4ecb26..64eb23acc79 100644 --- a/chromium/device/fido/fido_device_discovery.cc +++ b/chromium/device/fido/fido_device_discovery.cc @@ -14,6 +14,7 @@ namespace device { +FidoDeviceDiscovery::BLEObserver::~BLEObserver() = default; FidoDeviceDiscovery::Observer::~Observer() = default; FidoDeviceDiscovery::FidoDeviceDiscovery(FidoTransportProtocol transport) diff --git a/chromium/device/fido/fido_device_discovery.h b/chromium/device/fido/fido_device_discovery.h index f935f6fef12..6bf00bfab94 100644 --- a/chromium/device/fido/fido_device_discovery.h +++ b/chromium/device/fido/fido_device_discovery.h @@ -27,6 +27,15 @@ class FidoDeviceAuthenticator; class COMPONENT_EXPORT(DEVICE_FIDO) FidoDeviceDiscovery : public FidoDiscoveryBase { public: + // BLEObserver is an interface for discoveries that watch for BLE adverts. + class BLEObserver { + public: + virtual ~BLEObserver(); + + virtual void OnBLEAdvertSeen(const std::string& address, + const std::array<uint8_t, 16>& eid) = 0; + }; + enum class State { kIdle, kStarting, diff --git a/chromium/device/fido/fido_discovery_factory.cc b/chromium/device/fido/fido_discovery_factory.cc index 6fec78fb0e2..35ea31f118e 100644 --- a/chromium/device/fido/fido_discovery_factory.cc +++ b/chromium/device/fido/fido_discovery_factory.cc @@ -8,6 +8,7 @@ #include "device/bluetooth/bluetooth_adapter_factory.h" #include "device/fido/aoa/android_accessory_discovery.h" #include "device/fido/cable/fido_cable_discovery.h" +#include "device/fido/cable/v2_discovery.h" #include "device/fido/features.h" #include "device/fido/fido_discovery_base.h" @@ -35,39 +36,60 @@ namespace device { FidoDiscoveryFactory::FidoDiscoveryFactory() = default; FidoDiscoveryFactory::~FidoDiscoveryFactory() = default; -std::unique_ptr<FidoDiscoveryBase> FidoDiscoveryFactory::Create( +std::vector<std::unique_ptr<FidoDiscoveryBase>> FidoDiscoveryFactory::Create( FidoTransportProtocol transport) { switch (transport) { case FidoTransportProtocol::kUsbHumanInterfaceDevice: - return std::make_unique<FidoHidDiscovery>(hid_ignore_list_); + return SingleDiscovery( + std::make_unique<FidoHidDiscovery>(hid_ignore_list_)); case FidoTransportProtocol::kBluetoothLowEnergy: - return nullptr; + return {}; case FidoTransportProtocol::kCloudAssistedBluetoothLowEnergy: if (device::BluetoothAdapterFactory::Get()->IsLowEnergySupported() && (cable_data_.has_value() || qr_generator_key_.has_value())) { - return std::make_unique<FidoCableDiscovery>( - cable_data_.value_or(std::vector<CableDiscoveryData>()), - qr_generator_key_, cable_pairing_callback_, network_context_); + std::unique_ptr<cablev2::Discovery> v2_discovery; + if (qr_generator_key_.has_value()) { + v2_discovery = std::make_unique<cablev2::Discovery>( + network_context_, *qr_generator_key_, std::move(v2_pairings_), + std::move(cable_pairing_callback_)); + } + std::unique_ptr<FidoDiscoveryBase> v1_discovery = + std::make_unique<FidoCableDiscovery>( + cable_data_.value_or(std::vector<CableDiscoveryData>()), + v2_discovery ? v2_discovery.get() : nullptr); + + std::vector<std::unique_ptr<FidoDiscoveryBase>> ret; + if (v2_discovery) { + ret.emplace_back(std::move(v2_discovery)); + } + ret.emplace_back(std::move(v1_discovery)); + return ret; } - return nullptr; + return {}; case FidoTransportProtocol::kNearFieldCommunication: // TODO(https://crbug.com/825949): Add NFC support. - return nullptr; - case FidoTransportProtocol::kInternal: + return {}; + case FidoTransportProtocol::kInternal: { #if defined(OS_MAC) || defined(OS_CHROMEOS) - return MaybeCreatePlatformDiscovery(); + std::unique_ptr<FidoDiscoveryBase> discovery = + MaybeCreatePlatformDiscovery(); + if (discovery) { + return SingleDiscovery(std::move(discovery)); + } + return {}; #else - return nullptr; + return {}; #endif + } case FidoTransportProtocol::kAndroidAccessory: if (usb_device_manager_) { - return std::make_unique<AndroidAccessoryDiscovery>( - std::move(usb_device_manager_.value())); + return SingleDiscovery(std::make_unique<AndroidAccessoryDiscovery>( + std::move(usb_device_manager_.value()))); } - return nullptr; + return {}; } NOTREACHED() << "Unhandled transport type"; - return nullptr; + return {}; } bool FidoDiscoveryFactory::IsTestOverride() { @@ -76,9 +98,12 @@ bool FidoDiscoveryFactory::IsTestOverride() { void FidoDiscoveryFactory::set_cable_data( std::vector<CableDiscoveryData> cable_data, - base::Optional<QRGeneratorKey> qr_generator_key) { + const base::Optional<std::array<uint8_t, cablev2::kQRKeySize>>& + qr_generator_key, + std::vector<std::unique_ptr<cablev2::Pairing>> v2_pairings) { cable_data_ = std::move(cable_data); qr_generator_key_ = std::move(qr_generator_key); + v2_pairings_ = std::move(v2_pairings); } void FidoDiscoveryFactory::set_usb_device_manager( @@ -92,7 +117,7 @@ void FidoDiscoveryFactory::set_network_context( } void FidoDiscoveryFactory::set_cable_pairing_callback( - base::RepeatingCallback<void(std::unique_ptr<CableDiscoveryData>)> + base::RepeatingCallback<void(std::unique_ptr<cablev2::Pairing>)> pairing_callback) { cable_pairing_callback_.emplace(std::move(pairing_callback)); } @@ -102,6 +127,19 @@ void FidoDiscoveryFactory::set_hid_ignore_list( hid_ignore_list_ = std::move(hid_ignore_list); } +// static +std::vector<std::unique_ptr<FidoDiscoveryBase>> +FidoDiscoveryFactory::SingleDiscovery( + std::unique_ptr<FidoDiscoveryBase> discovery) { + if (!discovery) { + return {}; + } + + std::vector<std::unique_ptr<FidoDiscoveryBase>> ret; + ret.emplace_back(std::move(discovery)); + return ret; +} + #if defined(OS_WIN) void FidoDiscoveryFactory::set_win_webauthn_api(WinWebAuthnApi* api) { win_webauthn_api_ = api; diff --git a/chromium/device/fido/fido_discovery_factory.h b/chromium/device/fido/fido_discovery_factory.h index 9c6de139323..a8494d7d239 100644 --- a/chromium/device/fido/fido_discovery_factory.h +++ b/chromium/device/fido/fido_discovery_factory.h @@ -12,6 +12,7 @@ #include "base/optional.h" #include "build/build_config.h" #include "device/fido/cable/cable_discovery_data.h" +#include "device/fido/cable/v2_constants.h" #include "device/fido/fido_device_discovery.h" #include "device/fido/fido_discovery_base.h" #include "device/fido/fido_request_handler_base.h" @@ -38,10 +39,10 @@ class COMPONENT_EXPORT(DEVICE_FIDO) FidoDiscoveryFactory { FidoDiscoveryFactory(); virtual ~FidoDiscoveryFactory(); - // Instantiates a FidoDiscoveryBase for the given transport. + // Instantiates one or more FidoDiscoveryBases for the given transport. // // FidoTransportProtocol::kUsbHumanInterfaceDevice is not valid on Android. - virtual std::unique_ptr<FidoDiscoveryBase> Create( + virtual std::vector<std::unique_ptr<FidoDiscoveryBase>> Create( FidoTransportProtocol transport); // Returns whether the current instance is an override injected by the @@ -49,8 +50,11 @@ class COMPONENT_EXPORT(DEVICE_FIDO) FidoDiscoveryFactory { virtual bool IsTestOverride(); // set_cable_data configures caBLE obtained via a WebAuthn extension. - void set_cable_data(std::vector<CableDiscoveryData> cable_data, - base::Optional<QRGeneratorKey> qr_generator_key); + void set_cable_data( + std::vector<CableDiscoveryData> cable_data, + const base::Optional<std::array<uint8_t, cablev2::kQRKeySize>>& + qr_generator_key, + std::vector<std::unique_ptr<cablev2::Pairing>> v2_pairings); void set_usb_device_manager(mojo::Remote<device::mojom::UsbDeviceManager>); @@ -60,7 +64,7 @@ class COMPONENT_EXPORT(DEVICE_FIDO) FidoDiscoveryFactory { // called when a QR handshake results in a phone wishing to pair with this // browser. void set_cable_pairing_callback( - base::RepeatingCallback<void(std::unique_ptr<CableDiscoveryData>)>); + base::RepeatingCallback<void(std::unique_ptr<cablev2::Pairing>)>); void set_hid_ignore_list(base::flat_set<VidPid> hid_ignore_list); @@ -84,6 +88,10 @@ class COMPONENT_EXPORT(DEVICE_FIDO) FidoDiscoveryFactory { WinWebAuthnApi* win_webauthn_api() const; #endif // defined(OS_WIN) + protected: + static std::vector<std::unique_ptr<FidoDiscoveryBase>> SingleDiscovery( + std::unique_ptr<FidoDiscoveryBase> discovery); + private: #if defined(OS_MAC) || defined(OS_CHROMEOS) std::unique_ptr<FidoDiscoveryBase> MaybeCreatePlatformDiscovery() const; @@ -96,9 +104,10 @@ class COMPONENT_EXPORT(DEVICE_FIDO) FidoDiscoveryFactory { usb_device_manager_; network::mojom::NetworkContext* network_context_ = nullptr; base::Optional<std::vector<CableDiscoveryData>> cable_data_; - base::Optional<QRGeneratorKey> qr_generator_key_; + base::Optional<std::array<uint8_t, cablev2::kQRKeySize>> qr_generator_key_; + std::vector<std::unique_ptr<cablev2::Pairing>> v2_pairings_; base::Optional< - base::RepeatingCallback<void(std::unique_ptr<CableDiscoveryData>)>> + base::RepeatingCallback<void(std::unique_ptr<cablev2::Pairing>)>> cable_pairing_callback_; #if defined(OS_WIN) WinWebAuthnApi* win_webauthn_api_ = nullptr; diff --git a/chromium/device/fido/fido_parsing_utils.h b/chromium/device/fido/fido_parsing_utils.h index 431361483e3..6304cf9f841 100644 --- a/chromium/device/fido/fido_parsing_utils.h +++ b/chromium/device/fido/fido_parsing_utils.h @@ -139,6 +139,17 @@ bool CopyCBORBytestring(std::array<uint8_t, N>* out, return ExtractArray(bytestring, /*pos=*/0, out); } +constexpr std::array<uint8_t, 4> Uint32LittleEndian(uint32_t value) { + return {value & 0xFF, value >> 8 & 0xFF, value >> 16 & 0xFF, + value >> 24 & 0xFF}; +} + +constexpr std::array<uint8_t, 8> Uint64LittleEndian(uint64_t value) { + return {value & 0xFF, value >> 8 & 0xFF, value >> 16 & 0xFF, + value >> 24 & 0xFF, value >> 32 & 0xFF, value >> 40 & 0xFF, + value >> 48 & 0xFF, value >> 56 & 0xFF}; +} + } // namespace fido_parsing_utils } // namespace device diff --git a/chromium/device/fido/fido_request_handler_base.cc b/chromium/device/fido/fido_request_handler_base.cc index a7f687605d5..7eb2d155745 100644 --- a/chromium/device/fido/fido_request_handler_base.cc +++ b/chromium/device/fido/fido_request_handler_base.cc @@ -62,9 +62,9 @@ void FidoRequestHandlerBase::InitDiscoveries( const base::flat_set<FidoTransportProtocol>& available_transports) { transport_availability_info_.available_transports = available_transports; for (const auto transport : available_transports) { - std::unique_ptr<FidoDiscoveryBase> discovery = + std::vector<std::unique_ptr<FidoDiscoveryBase>> discoveries = fido_discovery_factory->Create(transport); - if (discovery == nullptr) { + if (discoveries.empty()) { // This can occur in tests when a ScopedVirtualU2fDevice is in effect and // HID transports are not configured or when caBLE discovery data isn't // available. @@ -72,8 +72,10 @@ void FidoRequestHandlerBase::InitDiscoveries( continue; } - discovery->set_observer(this); - discoveries_.push_back(std::move(discovery)); + for (auto& discovery : discoveries) { + discovery->set_observer(this); + discoveries_.emplace_back(std::move(discovery)); + } } // Check if the platform supports BLE before trying to get a power manager. diff --git a/chromium/device/fido/fido_types.h b/chromium/device/fido/fido_types.h index c620387ddc3..c896e809247 100644 --- a/chromium/device/fido/fido_types.h +++ b/chromium/device/fido/fido_types.h @@ -5,18 +5,25 @@ #ifndef DEVICE_FIDO_FIDO_TYPES_H_ #define DEVICE_FIDO_FIDO_TYPES_H_ -// The definitions below are for mojo-mappable types that need to be -// transferred from Blink. Types that have mojo equivalents are better placed -// in fido_constants.h. +// The definitions below are for mojo-mappable types that need to be transferred +// from Blink. Types that do not have mojo equivalents are better placed in +// fido_constants.h. namespace device { +// ProtocolVersion is the major protocol version of an authenticator device. enum class ProtocolVersion { kCtap2, kU2f, kUnknown, }; +// Ctap2Version distinguishes different minor versions of the CTAP2 protocol. +enum class Ctap2Version { + kCtap2_0, + kCtap2_1, +}; + enum class CredentialType { kPublicKey }; // Authenticator attachment constraint passed on from the relying party as a @@ -29,6 +36,16 @@ enum class AuthenticatorAttachment { kCrossPlatform, }; +// A constraint on whether a client-side discoverable (resident) credential +// should be created during registration. +// +// https://w3c.github.io/webauthn/#enum-residentKeyRequirement +enum class ResidentKeyRequirement { + kDiscouraged, + kPreferred, + kRequired, +}; + // User verification constraint passed on from the relying party as a parameter // for AuthenticatorSelectionCriteria and for CtapGetAssertion request. // https://w3c.github.io/webauthn/#enumdef-userverificationrequirement diff --git a/chromium/device/fido/get_assertion_request_handler.cc b/chromium/device/fido/get_assertion_request_handler.cc index 51a0175422e..7adcf5b6cf8 100644 --- a/chromium/device/fido/get_assertion_request_handler.cc +++ b/chromium/device/fido/get_assertion_request_handler.cc @@ -337,11 +337,8 @@ void GetAssertionRequestHandler::DispatchRequest( CtapGetAssertionRequest request(request_); if (request.user_verification != UserVerificationRequirement::kDiscouraged && authenticator->CanGetUvToken()) { - FIDO_LOG(DEBUG) << "Getting UV token from " - << authenticator->GetDisplayName(); - authenticator->GetUvToken( - request_.rp_id, - base::BindOnce(&GetAssertionRequestHandler::OnHaveUvToken, + authenticator->GetUvRetries( + base::BindOnce(&GetAssertionRequestHandler::OnStartUvTokenOrFallback, weak_factory_.GetWeakPtr(), authenticator)); return; } @@ -703,6 +700,40 @@ void GetAssertionRequestHandler::OnHavePINToken( DispatchRequestWithToken(std::move(*response)); } +void GetAssertionRequestHandler::OnStartUvTokenOrFallback( + FidoAuthenticator* authenticator, + CtapDeviceResponseCode status, + base::Optional<pin::RetriesResponse> response) { + size_t retries; + if (status != CtapDeviceResponseCode::kSuccess) { + FIDO_LOG(ERROR) << "OnStartUvTokenOrFallback() failed for " + << authenticator_->GetDisplayName() + << ", assuming authenticator locked."; + retries = 0; + } else { + retries = response->retries; + } + + if (retries == 0) { + if (authenticator->WillNeedPINToGetAssertion(request_, observer()) == + PINDisposition::kUsePINForFallback) { + authenticator->GetTouch(base::BindOnce( + &GetAssertionRequestHandler::StartPINFallbackForInternalUv, + weak_factory_.GetWeakPtr(), authenticator)); + return; + } + authenticator->GetTouch(base::BindOnce( + &GetAssertionRequestHandler::TerminateUnsatisfiableRequestPostTouch, + weak_factory_.GetWeakPtr(), authenticator)); + } + + base::Optional<std::string> rp_id(request_.rp_id); + authenticator->GetUvToken( + std::move(rp_id), + base::BindOnce(&GetAssertionRequestHandler::OnHaveUvToken, + weak_factory_.GetWeakPtr(), authenticator)); +} + void GetAssertionRequestHandler::OnUvRetriesResponse( CtapDeviceResponseCode status, base::Optional<pin::RetriesResponse> response) { @@ -746,29 +777,19 @@ void GetAssertionRequestHandler::OnHaveUvToken( return; } - if (status == CtapDeviceResponseCode::kCtap2ErrPinInvalid || + if (status == CtapDeviceResponseCode::kCtap2ErrUvInvalid || status == CtapDeviceResponseCode::kCtap2ErrOperationDenied || status == CtapDeviceResponseCode::kCtap2ErrUvBlocked) { if (status == CtapDeviceResponseCode::kCtap2ErrUvBlocked) { - // This error is returned immediately without user interaction. Ask for a - // touch and fall back to PIN or terminate the request if the device does - // not support PIN. - FIDO_LOG(DEBUG) << "Internal UV blocked for " - << authenticator->GetDisplayName() - << ", falling back to PIN."; if (authenticator->WillNeedPINToGetAssertion(request_, observer()) == PINDisposition::kUsePINForFallback) { - authenticator->GetTouch(base::BindOnce( - &GetAssertionRequestHandler::StartPINFallbackForInternalUv, - weak_factory_.GetWeakPtr(), authenticator)); + StartPINFallbackForInternalUv(authenticator); return; } - authenticator->GetTouch(base::BindOnce( - &GetAssertionRequestHandler::TerminateUnsatisfiableRequestPostTouch, - weak_factory_.GetWeakPtr(), authenticator)); + TerminateUnsatisfiableRequestPostTouch(authenticator); return; } - DCHECK(status == CtapDeviceResponseCode::kCtap2ErrPinInvalid || + DCHECK(status == CtapDeviceResponseCode::kCtap2ErrUvInvalid || status == CtapDeviceResponseCode::kCtap2ErrOperationDenied); CancelActiveAuthenticators(authenticator->GetId()); authenticator_ = authenticator; diff --git a/chromium/device/fido/get_assertion_request_handler.h b/chromium/device/fido/get_assertion_request_handler.h index 6933ebacb2c..06535a2d385 100644 --- a/chromium/device/fido/get_assertion_request_handler.h +++ b/chromium/device/fido/get_assertion_request_handler.h @@ -106,6 +106,9 @@ class COMPONENT_EXPORT(DEVICE_FIDO) GetAssertionRequestHandler void OnHavePIN(std::string pin); void OnHavePINToken(CtapDeviceResponseCode status, base::Optional<pin::TokenResponse> response); + void OnStartUvTokenOrFallback(FidoAuthenticator* authenticator, + CtapDeviceResponseCode status, + base::Optional<pin::RetriesResponse> response); void OnUvRetriesResponse(CtapDeviceResponseCode status, base::Optional<pin::RetriesResponse> response); void OnHaveUvToken(FidoAuthenticator* authenticator, diff --git a/chromium/device/fido/hid/fake_hid_impl_for_testing.h b/chromium/device/fido/hid/fake_hid_impl_for_testing.h index 15e87abae1b..894efab9b7a 100644 --- a/chromium/device/fido/hid/fake_hid_impl_for_testing.h +++ b/chromium/device/fido/hid/fake_hid_impl_for_testing.h @@ -111,7 +111,8 @@ class FakeFidoHidManager : public device::mojom::HidManager { mojo::PendingRemote<mojom::HidConnectionClient> connection_client, mojo::PendingRemote<mojom::HidConnectionWatcher> watcher, ConnectCallback callback) override; - void AddReceiver(mojo::PendingReceiver<device::mojom::HidManager> receiver); + void AddReceiver( + mojo::PendingReceiver<device::mojom::HidManager> receiver) override; void AddDevice(device::mojom::HidDeviceInfoPtr device); void AddDeviceAndSetConnection( device::mojom::HidDeviceInfoPtr device, diff --git a/chromium/device/fido/large_blob.cc b/chromium/device/fido/large_blob.cc new file mode 100644 index 00000000000..b69b4b72ee9 --- /dev/null +++ b/chromium/device/fido/large_blob.cc @@ -0,0 +1,299 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "device/fido/large_blob.h" +#include "base/containers/span.h" +#include "components/cbor/reader.h" +#include "components/cbor/writer.h" +#include "crypto/aead.h" +#include "crypto/random.h" +#include "crypto/sha2.h" +#include "device/fido/fido_parsing_utils.h" +#include "device/fido/pin.h" + +namespace device { + +namespace { +// The number of bytes the large blob validation hash is truncated to. +constexpr size_t kTruncatedHashBytes = 16; +constexpr std::array<uint8_t, 4> kLargeBlobADPrefix = {'b', 'l', 'o', 'b'}; +constexpr size_t kAssociatedDataLength = kLargeBlobADPrefix.size() + 8; + +std::array<uint8_t, kAssociatedDataLength> GenerateLargeBlobAdditionalData( + size_t size) { + std::array<uint8_t, kAssociatedDataLength> additional_data; + const std::array<uint8_t, 8>& size_array = + fido_parsing_utils::Uint64LittleEndian(size); + std::copy(kLargeBlobADPrefix.begin(), kLargeBlobADPrefix.end(), + additional_data.begin()); + std::copy(size_array.begin(), size_array.end(), + additional_data.begin() + kLargeBlobADPrefix.size()); + return additional_data; +} + +} // namespace + +LargeBlobArrayFragment::LargeBlobArrayFragment(const std::vector<uint8_t> bytes, + const size_t offset) + : bytes(std::move(bytes)), offset(offset) {} +LargeBlobArrayFragment::~LargeBlobArrayFragment() = default; +LargeBlobArrayFragment::LargeBlobArrayFragment(LargeBlobArrayFragment&&) = + default; + +bool VerifyLargeBlobArrayIntegrity(base::span<const uint8_t> large_blob_array) { + if (large_blob_array.size() <= kTruncatedHashBytes) { + return false; + } + const size_t trail_offset = large_blob_array.size() - kTruncatedHashBytes; + std::array<uint8_t, crypto::kSHA256Length> large_blob_hash = + crypto::SHA256Hash(large_blob_array.subspan(0, trail_offset)); + + base::span<const uint8_t> large_blob_trail = + large_blob_array.subspan(trail_offset); + return std::equal(large_blob_hash.begin(), + large_blob_hash.begin() + kTruncatedHashBytes, + large_blob_trail.begin(), large_blob_trail.end()); +} + +// static +LargeBlobsRequest LargeBlobsRequest::ForRead(size_t bytes, size_t offset) { + DCHECK_GT(bytes, 0u); + LargeBlobsRequest request; + request.get_ = bytes; + request.offset_ = offset; + return request; +} + +// static +LargeBlobsRequest LargeBlobsRequest::ForWrite(LargeBlobArrayFragment fragment, + size_t length) { + LargeBlobsRequest request; + if (fragment.offset == 0) { + request.length_ = length; + } + request.offset_ = fragment.offset; + request.set_ = std::move(fragment.bytes); + return request; +} + +LargeBlobsRequest::LargeBlobsRequest() = default; +LargeBlobsRequest::LargeBlobsRequest(LargeBlobsRequest&& other) = default; +LargeBlobsRequest::~LargeBlobsRequest() = default; + +void LargeBlobsRequest::SetPinParam( + const pin::TokenResponse& pin_uv_auth_token) { + pin_uv_auth_protocol_ = pin::kProtocolVersion; + std::vector<uint8_t> pin_auth(pin::kPinUvAuthTokenSafetyPadding.begin(), + pin::kPinUvAuthTokenSafetyPadding.end()); + pin_auth.insert(pin_auth.end(), kLargeBlobPinPrefix.begin(), + kLargeBlobPinPrefix.end()); + const std::array<uint8_t, 4> offset_array = + fido_parsing_utils::Uint32LittleEndian(offset_); + pin_auth.insert(pin_auth.end(), offset_array.begin(), offset_array.end()); + if (set_) { + pin_auth.insert(pin_auth.end(), set_->begin(), set_->end()); + } + pin_uv_auth_param_ = pin_uv_auth_token.PinAuth(pin_auth); +} + +// static +base::Optional<LargeBlobsResponse> LargeBlobsResponse::ParseForRead( + const size_t bytes_to_read, + const base::Optional<cbor::Value>& cbor_response) { + if (!cbor_response || !cbor_response->is_map()) { + return base::nullopt; + } + + const cbor::Value::MapValue& map = cbor_response->GetMap(); + auto it = + map.find(cbor::Value(static_cast<int>(LargeBlobsResponseKey::kConfig))); + if (it == map.end() || !it->second.is_bytestring()) { + return base::nullopt; + } + + const std::vector<uint8_t>& config = it->second.GetBytestring(); + if (config.size() > bytes_to_read) { + return base::nullopt; + } + + return LargeBlobsResponse(std::move(config)); +} + +// static +base::Optional<LargeBlobsResponse> LargeBlobsResponse::ParseForWrite( + const base::Optional<cbor::Value>& cbor_response) { + // For writing, we expect an empty response. + if (cbor_response) { + return base::nullopt; + } + + return LargeBlobsResponse(); +} + +LargeBlobsResponse::LargeBlobsResponse( + base::Optional<std::vector<uint8_t>> config) + : config_(std::move(config)) {} +LargeBlobsResponse::LargeBlobsResponse(LargeBlobsResponse&& other) = default; +LargeBlobsResponse& LargeBlobsResponse::operator=(LargeBlobsResponse&& other) = + default; +LargeBlobsResponse::~LargeBlobsResponse() = default; + +std::pair<CtapRequestCommand, base::Optional<cbor::Value>> +AsCTAPRequestValuePair(const LargeBlobsRequest& request) { + cbor::Value::MapValue map; + if (request.get_) { + map.emplace(static_cast<int>(LargeBlobsRequestKey::kGet), *request.get_); + } + if (request.set_) { + map.emplace(static_cast<int>(LargeBlobsRequestKey::kSet), *request.set_); + } + map.emplace(static_cast<int>(LargeBlobsRequestKey::kOffset), request.offset_); + if (request.length_) { + map.emplace(static_cast<int>(LargeBlobsRequestKey::kLength), + *request.length_); + } + if (request.pin_uv_auth_param_) { + map.emplace(static_cast<int>(LargeBlobsRequestKey::kPinUvAuthParam), + *request.pin_uv_auth_param_); + } + if (request.pin_uv_auth_protocol_) { + map.emplace(static_cast<int>(LargeBlobsRequestKey::kPinUvAuthProtocol), + *request.pin_uv_auth_protocol_); + } + return std::make_pair(CtapRequestCommand::kAuthenticatorLargeBlobs, + cbor::Value(std::move(map))); +} + +// static. +base::Optional<LargeBlobData> LargeBlobData::Parse(const cbor::Value& value) { + if (!value.is_map()) { + return base::nullopt; + } + const cbor::Value::MapValue& map = value.GetMap(); + auto ciphertext_it = + map.find(cbor::Value(static_cast<int>(LargeBlobDataKeys::kCiphertext))); + if (ciphertext_it == map.end() || !ciphertext_it->second.is_bytestring()) { + return base::nullopt; + } + auto nonce_it = + map.find(cbor::Value(static_cast<int>(LargeBlobDataKeys::kNonce))); + if (nonce_it == map.end() || !nonce_it->second.is_bytestring() || + nonce_it->second.GetBytestring().size() != kLargeBlobArrayNonceLength) { + return base::nullopt; + } + auto orig_size_it = + map.find(cbor::Value(static_cast<int>(LargeBlobDataKeys::kOrigSize))); + if (orig_size_it == map.end() || !orig_size_it->second.is_unsigned()) { + return base::nullopt; + } + return LargeBlobData(ciphertext_it->second.GetBytestring(), + base::make_span<kLargeBlobArrayNonceLength>( + nonce_it->second.GetBytestring()), + orig_size_it->second.GetUnsigned()); +} + +LargeBlobData::LargeBlobData( + std::vector<uint8_t> ciphertext, + base::span<const uint8_t, kLargeBlobArrayNonceLength> nonce, + int64_t orig_size) + : ciphertext_(std::move(ciphertext)), orig_size_(std::move(orig_size)) { + std::copy(nonce.begin(), nonce.end(), nonce_.begin()); +} +LargeBlobData::LargeBlobData(LargeBlobKey key, std::vector<uint8_t> blob) { + orig_size_ = blob.size(); + crypto::Aead aead(crypto::Aead::AeadAlgorithm::AES_256_GCM); + aead.Init(key); + crypto::RandBytes(nonce_); + ciphertext_ = + aead.Seal(blob, nonce_, GenerateLargeBlobAdditionalData(orig_size_)); +} +LargeBlobData::LargeBlobData(LargeBlobData&&) = default; +LargeBlobData& LargeBlobData::operator=(LargeBlobData&&) = default; +LargeBlobData::~LargeBlobData() = default; + +bool LargeBlobData::operator==(const LargeBlobData& other) const { + return ciphertext_ == other.ciphertext_ && nonce_ == other.nonce_ && + orig_size_ == other.orig_size_; +} + +base::Optional<std::vector<uint8_t>> LargeBlobData::Decrypt( + LargeBlobKey key) const { + crypto::Aead aead(crypto::Aead::AeadAlgorithm::AES_256_GCM); + aead.Init(key); + return aead.Open(ciphertext_, nonce_, + GenerateLargeBlobAdditionalData(orig_size_)); +} + +cbor::Value::MapValue LargeBlobData::AsCBOR() const { + cbor::Value::MapValue map; + map.emplace(static_cast<int>(LargeBlobDataKeys::kCiphertext), ciphertext_); + map.emplace(static_cast<int>(LargeBlobDataKeys::kNonce), nonce_); + map.emplace(static_cast<int>(LargeBlobDataKeys::kOrigSize), orig_size_); + return map; +} + +LargeBlobArrayReader::LargeBlobArrayReader() = default; +LargeBlobArrayReader::LargeBlobArrayReader(LargeBlobArrayReader&&) = default; +LargeBlobArrayReader::~LargeBlobArrayReader() = default; + +void LargeBlobArrayReader::Append(const std::vector<uint8_t>& fragment) { + bytes_.insert(bytes_.end(), fragment.begin(), fragment.end()); +} + +base::Optional<std::vector<LargeBlobData>> LargeBlobArrayReader::Materialize() { + if (!VerifyLargeBlobArrayIntegrity(bytes_)) { + return base::nullopt; + } + + base::span<const uint8_t> cbor_bytes = + base::make_span(bytes_.data(), bytes_.size() - kTruncatedHashBytes); + base::Optional<cbor::Value> cbor = cbor::Reader::Read(cbor_bytes); + if (!cbor || !cbor->is_array()) { + return base::nullopt; + } + + std::vector<LargeBlobData> large_blob_array; + const cbor::Value::ArrayValue& array = cbor->GetArray(); + for (const cbor::Value& value : array) { + base::Optional<LargeBlobData> large_blob_data = LargeBlobData::Parse(value); + if (!large_blob_data) { + continue; + } + + large_blob_array.emplace_back(std::move(*large_blob_data)); + } + + return large_blob_array; +} + +LargeBlobArrayWriter::LargeBlobArrayWriter( + const std::vector<LargeBlobData>& large_blob_array) { + cbor::Value::ArrayValue array; + for (const LargeBlobData& large_blob_data : large_blob_array) { + array.emplace_back(large_blob_data.AsCBOR()); + } + bytes_ = *cbor::Writer::Write(cbor::Value(array)); + + std::array<uint8_t, crypto::kSHA256Length> large_blob_hash = + crypto::SHA256Hash(bytes_); + bytes_.insert(bytes_.end(), large_blob_hash.begin(), + large_blob_hash.begin() + kTruncatedHashBytes); + DCHECK(VerifyLargeBlobArrayIntegrity(bytes_)); +} +LargeBlobArrayWriter::LargeBlobArrayWriter(LargeBlobArrayWriter&&) = default; +LargeBlobArrayWriter::~LargeBlobArrayWriter() = default; + +LargeBlobArrayFragment LargeBlobArrayWriter::Pop(size_t length) { + CHECK(has_remaining_fragments()); + length = std::min(length, bytes_.size() - offset_); + + LargeBlobArrayFragment fragment{ + fido_parsing_utils::Materialize( + base::make_span(bytes_.data() + offset_, length)), + offset_}; + offset_ += length; + return fragment; +} + +} // namespace device diff --git a/chromium/device/fido/large_blob.h b/chromium/device/fido/large_blob.h new file mode 100644 index 00000000000..dba7ef0298a --- /dev/null +++ b/chromium/device/fido/large_blob.h @@ -0,0 +1,203 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef DEVICE_FIDO_LARGE_BLOB_H_ +#define DEVICE_FIDO_LARGE_BLOB_H_ + +#include <cstdint> +#include <cstdlib> +#include <vector> + +#include "base/component_export.h" +#include "device/fido/fido_constants.h" +#include "device/fido/pin.h" + +namespace device { + +// https://drafts.fidoalliance.org/fido-2/stable-links-to-latest/fido-client-to-authenticator-protocol.html#largeBlobsRW +enum class LargeBlobsRequestKey : uint8_t { + kGet = 0x01, + kSet = 0x02, + kOffset = 0x03, + kLength = 0x04, + kPinUvAuthParam = 0x05, + kPinUvAuthProtocol = 0x06, +}; + +// https://drafts.fidoalliance.org/fido-2/stable-links-to-latest/fido-client-to-authenticator-protocol.html#largeBlobsRW +enum class LargeBlobsResponseKey : uint8_t { + kConfig = 0x01, +}; + +// https://drafts.fidoalliance.org/fido-2/stable-links-to-latest/fido-client-to-authenticator-protocol.html#large-blob +enum class LargeBlobDataKeys : uint8_t { + kCiphertext = 0x01, + kNonce = 0x02, + kOrigSize = 0x03, +}; + +enum class LargeBlobOperation { + kNone, + kRead, + kWrite, +}; + +using LargeBlobKey = std::array<uint8_t, kLargeBlobKeyLength>; + +constexpr size_t kLargeBlobDefaultMaxFragmentLength = 960; +constexpr size_t kLargeBlobReadEncodingOverhead = 64; +constexpr size_t kLargeBlobArrayNonceLength = 12; +constexpr std::array<uint8_t, 2> kLargeBlobPinPrefix = {0x0c, 0x00}; + +struct COMPONENT_EXPORT(DEVICE_FIDO) LargeBlobArrayFragment { + LargeBlobArrayFragment(std::vector<uint8_t> bytes, size_t offset); + ~LargeBlobArrayFragment(); + LargeBlobArrayFragment(const LargeBlobArrayFragment&) = delete; + LargeBlobArrayFragment operator=(const LargeBlobArrayFragment&) = delete; + LargeBlobArrayFragment(LargeBlobArrayFragment&&); + const std::vector<uint8_t> bytes; + const size_t offset; +}; + +COMPONENT_EXPORT(DEVICE_FIDO) +bool VerifyLargeBlobArrayIntegrity(base::span<const uint8_t> large_blob_array); + +class LargeBlobsRequest { + public: + ~LargeBlobsRequest(); + LargeBlobsRequest(const LargeBlobsRequest&) = delete; + LargeBlobsRequest operator=(const LargeBlobsRequest&) = delete; + LargeBlobsRequest(LargeBlobsRequest&& other); + + static LargeBlobsRequest ForRead(size_t bytes, size_t offset); + static LargeBlobsRequest ForWrite(LargeBlobArrayFragment fragment, + size_t length); + + void SetPinParam(const pin::TokenResponse& pin_uv_auth_token); + + friend std::pair<CtapRequestCommand, base::Optional<cbor::Value>> + AsCTAPRequestValuePair(const LargeBlobsRequest& request); + + private: + LargeBlobsRequest(); + + base::Optional<int64_t> get_; + base::Optional<std::vector<uint8_t>> set_; + int64_t offset_ = 0; + base::Optional<int64_t> length_; + base::Optional<std::vector<uint8_t>> pin_uv_auth_param_; + base::Optional<int64_t> pin_uv_auth_protocol_; +}; + +class LargeBlobsResponse { + public: + LargeBlobsResponse(const LargeBlobsResponse&) = delete; + LargeBlobsResponse operator=(const LargeBlobsResponse&) = delete; + LargeBlobsResponse(LargeBlobsResponse&& other); + LargeBlobsResponse& operator=(LargeBlobsResponse&&); + ~LargeBlobsResponse(); + + static base::Optional<LargeBlobsResponse> ParseForRead( + size_t bytes_to_read, + const base::Optional<cbor::Value>& cbor_response); + static base::Optional<LargeBlobsResponse> ParseForWrite( + const base::Optional<cbor::Value>& cbor_response); + + base::Optional<std::vector<uint8_t>> config() { return config_; } + + private: + explicit LargeBlobsResponse( + base::Optional<std::vector<uint8_t>> config = base::nullopt); + + base::Optional<std::vector<uint8_t>> config_; +}; + +// Represents the large-blob map structure +// https://drafts.fidoalliance.org/fido-2/stable-links-to-latest/fido-client-to-authenticator-protocol.html#large-blob +class COMPONENT_EXPORT(DEVICE_FIDO) LargeBlobData { + public: + static base::Optional<LargeBlobData> Parse(const cbor::Value& cbor_response); + + LargeBlobData(LargeBlobKey key, std::vector<uint8_t> blob); + LargeBlobData(const LargeBlobData&) = delete; + LargeBlobData operator=(const LargeBlobData&) = delete; + LargeBlobData(LargeBlobData&&); + LargeBlobData& operator=(LargeBlobData&&); + ~LargeBlobData(); + bool operator==(const LargeBlobData&) const; + + base::Optional<std::vector<uint8_t>> Decrypt(LargeBlobKey key) const; + cbor::Value::MapValue AsCBOR() const; + + private: + LargeBlobData(std::vector<uint8_t> ciphertext, + base::span<const uint8_t, kLargeBlobArrayNonceLength> nonce, + int64_t orig_size); + std::vector<uint8_t> ciphertext_; + std::array<uint8_t, kLargeBlobArrayNonceLength> nonce_; + int64_t orig_size_; +}; + +// Reading large blob arrays is done in chunks. This class provides facilities +// to assemble together those chunks. +class COMPONENT_EXPORT(DEVICE_FIDO) LargeBlobArrayReader { + public: + LargeBlobArrayReader(); + LargeBlobArrayReader(const LargeBlobArrayReader&) = delete; + LargeBlobArrayReader operator=(const LargeBlobArrayReader&) = delete; + LargeBlobArrayReader(LargeBlobArrayReader&&); + ~LargeBlobArrayReader(); + + // Appends a fragment to the large blob array. + void Append(const std::vector<uint8_t>& fragment); + + // Verifies the integrity of the large blob array. This should be called after + // all fragments have been |Append|ed. + // If successful, parses and returns the array. + base::Optional<std::vector<LargeBlobData>> Materialize(); + + // Returns the current size of the array fragments. + size_t size() const { return bytes_.size(); } + + private: + std::vector<uint8_t> bytes_; +}; + +// Writing large blob arrays is done in chunks. This class provides facilities +// to divide a blob into chunks. +class COMPONENT_EXPORT(DEVICE_FIDO) LargeBlobArrayWriter { + public: + explicit LargeBlobArrayWriter( + const std::vector<LargeBlobData>& large_blob_array); + LargeBlobArrayWriter(const LargeBlobArrayWriter&) = delete; + LargeBlobArrayWriter operator=(const LargeBlobArrayWriter&) = delete; + LargeBlobArrayWriter(LargeBlobArrayWriter&&); + ~LargeBlobArrayWriter(); + + // Extracts a fragment with |length|. Can only be called if + // has_remaining_fragments() is true. + LargeBlobArrayFragment Pop(size_t length); + + // Returns the current size of the array fragments. + size_t size() const { return bytes_.size(); } + + // Returns true if there are remaining fragments to be written, false + // otherwise. + bool has_remaining_fragments() const { return offset_ < size(); } + + void set_bytes_for_testing(std::vector<uint8_t> bytes) { + bytes_ = std::move(bytes); + } + + private: + std::vector<uint8_t> bytes_; + size_t offset_ = 0; +}; + +std::pair<CtapRequestCommand, base::Optional<cbor::Value>> +AsCTAPRequestValuePair(const LargeBlobsRequest& request); + +} // namespace device + +#endif // DEVICE_FIDO_LARGE_BLOB_H_ diff --git a/chromium/device/fido/large_blob_unittest.cc b/chromium/device/fido/large_blob_unittest.cc new file mode 100644 index 00000000000..7fda7edd8ed --- /dev/null +++ b/chromium/device/fido/large_blob_unittest.cc @@ -0,0 +1,144 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "device/fido/large_blob.h" + +#include "base/optional.h" +#include "device/fido/fido_parsing_utils.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace device { + +namespace { + +class FidoLargeBlobTest : public testing::Test {}; + +// An empty CBOR array (0x80) followed by LEFT(SHA-256(h'80'), 16). +const std::array<uint8_t, 17> kValidEmptyLargeBlobArray = { + 0x80, 0x76, 0xbe, 0x8b, 0x52, 0x8d, 0x00, 0x75, 0xf7, + 0xaa, 0xe9, 0x8d, 0x6f, 0xa5, 0x7a, 0x6d, 0x3c}; + +// Something that is not an empty CBOR array (0x10) followed by +// LEFT(SHA-256(h'10'), 16). +const std::array<uint8_t, 17> kInvalidLargeBlobArray = { + 0x10, 0xc5, 0x55, 0xea, 0xb4, 0x5d, 0x08, 0x84, 0x5a, + 0xe9, 0xf1, 0x0d, 0x45, 0x2a, 0x99, 0xbf, 0xcb}; + +// An "valid" CBOR large blob array with two entries. The first entry is not a +// valid large blob map structure. The second entry is valid. +const std::array<uint8_t, 45> kValidLargeBlobArray = { + 0x82, 0xA2, 0x02, 0x42, 0x11, 0x11, 0x03, 0x02, 0xA3, 0x01, 0x42, 0x22, + 0x22, 0x02, 0x4C, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, + 0x30, 0x31, 0x32, 0x03, 0x02, 0x9b, 0x33, 0x75, 0x6c, 0x0a, 0x84, 0xdf, + 0x32, 0xcc, 0xd0, 0xc8, 0x96, 0xea, 0xa7, 0x99, 0x13}; + +TEST_F(FidoLargeBlobTest, VerifyLargeBlobArrayIntegrityValid) { + std::vector<uint8_t> large_blob_array = + fido_parsing_utils::Materialize(kValidEmptyLargeBlobArray); + EXPECT_TRUE(VerifyLargeBlobArrayIntegrity(large_blob_array)); +} + +TEST_F(FidoLargeBlobTest, VerifyLargeBlobArrayIntegrityInvalid) { + std::vector<uint8_t> large_blob_array = + fido_parsing_utils::Materialize(kValidEmptyLargeBlobArray); + large_blob_array[0] += 1; + EXPECT_FALSE(VerifyLargeBlobArrayIntegrity(large_blob_array)); + + large_blob_array = fido_parsing_utils::Materialize(kValidEmptyLargeBlobArray); + large_blob_array.erase(large_blob_array.begin()); + EXPECT_FALSE(VerifyLargeBlobArrayIntegrity(large_blob_array)); +} + +TEST_F(FidoLargeBlobTest, LargeBlobArrayReader_MaterializeEmpty) { + LargeBlobArrayReader large_blob_array_reader; + large_blob_array_reader.Append( + fido_parsing_utils::Materialize(kValidEmptyLargeBlobArray)); + EXPECT_EQ(0u, large_blob_array_reader.Materialize()->size()); +} + +TEST_F(FidoLargeBlobTest, LargeBlobArrayReader_MaterializeInvalidCbor) { + LargeBlobArrayReader large_blob_array_reader; + large_blob_array_reader.Append( + fido_parsing_utils::Materialize(kInvalidLargeBlobArray)); + EXPECT_FALSE(large_blob_array_reader.Materialize()); +} + +TEST_F(FidoLargeBlobTest, LargeBlobArrayReader_MaterializeInvalidHash) { + std::vector<uint8_t> large_blob_array = + fido_parsing_utils::Materialize(kValidEmptyLargeBlobArray); + large_blob_array[0] += 1; + LargeBlobArrayReader large_blob_array_reader; + large_blob_array_reader.Append( + fido_parsing_utils::Materialize(large_blob_array)); + EXPECT_FALSE(large_blob_array_reader.Materialize()); +} + +TEST_F(FidoLargeBlobTest, LargeBlobArrayReader_MaterializeValid) { + LargeBlobArrayReader large_blob_array_reader; + large_blob_array_reader.Append( + fido_parsing_utils::Materialize(kValidLargeBlobArray)); + std::vector<LargeBlobData> vector = *large_blob_array_reader.Materialize(); + EXPECT_EQ(1u, vector.size()); +} + +// Test popping the large blob array in a fragment size that does not evenly +// divide the length of the array. +TEST_F(FidoLargeBlobTest, LargeBlobArrayWriter_PopUnevenly) { + const size_t fragment_size = 8; + const size_t expected_fragments = + kValidLargeBlobArray.size() / fragment_size + 1; + size_t fragments = 0; + ASSERT_NE(0u, kValidLargeBlobArray.size() % fragment_size); + + LargeBlobArrayWriter large_blob_array_writer({}); + std::vector<uint8_t> large_blob_array = + fido_parsing_utils::Materialize(kValidLargeBlobArray); + large_blob_array_writer.set_bytes_for_testing(large_blob_array); + std::vector<uint8_t> reconstructed; + EXPECT_TRUE(large_blob_array_writer.has_remaining_fragments()); + while (large_blob_array_writer.has_remaining_fragments()) { + LargeBlobArrayFragment fragment = + large_blob_array_writer.Pop(fragment_size); + ++fragments; + reconstructed.insert(reconstructed.end(), fragment.bytes.begin(), + fragment.bytes.end()); + EXPECT_EQ(fragments != expected_fragments, + large_blob_array_writer.has_remaining_fragments()); + } + + EXPECT_EQ(expected_fragments, fragments); + EXPECT_EQ(large_blob_array, reconstructed); +} + +// Test popping the large blob array in a fragment size that evenly divides the +// length of the array. +TEST_F(FidoLargeBlobTest, LargeBlobArrayFragments_PopEvenly) { + const size_t fragment_size = 9; + const size_t expected_fragments = kValidLargeBlobArray.size() / fragment_size; + size_t fragments = 0; + ASSERT_EQ(0u, kValidLargeBlobArray.size() % fragment_size); + + LargeBlobArrayWriter large_blob_array_writer({}); + std::vector<uint8_t> large_blob_array = + fido_parsing_utils::Materialize(kValidLargeBlobArray); + large_blob_array_writer.set_bytes_for_testing(large_blob_array); + std::vector<uint8_t> reconstructed; + EXPECT_TRUE(large_blob_array_writer.has_remaining_fragments()); + while (large_blob_array_writer.has_remaining_fragments()) { + LargeBlobArrayFragment fragment = + large_blob_array_writer.Pop(fragment_size); + ++fragments; + reconstructed.insert(reconstructed.end(), fragment.bytes.begin(), + fragment.bytes.end()); + EXPECT_EQ(fragments != expected_fragments, + large_blob_array_writer.has_remaining_fragments()); + } + + EXPECT_EQ(expected_fragments, fragments); + EXPECT_EQ(large_blob_array, reconstructed); +} + +} // namespace + +} // namespace device diff --git a/chromium/device/fido/mac/browsing_data_deletion_unittest.mm b/chromium/device/fido/mac/browsing_data_deletion_unittest.mm index 518b517c5cb..84948030afc 100644 --- a/chromium/device/fido/mac/browsing_data_deletion_unittest.mm +++ b/chromium/device/fido/mac/browsing_data_deletion_unittest.mm @@ -54,12 +54,13 @@ const std::vector<uint8_t> kUserId = {10, 11, 12, 13, 14, 15}; // credentials in the non-legacy keychain that are tagged with the keychain // access group used in this test. base::ScopedCFTypeRef<CFMutableDictionaryRef> BaseQuery() { - base::ScopedCFTypeRef<CFMutableDictionaryRef> query( - CFDictionaryCreateMutable(kCFAllocatorDefault, 0, nullptr, nullptr)); + base::ScopedCFTypeRef<CFMutableDictionaryRef> query(CFDictionaryCreateMutable( + kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, + &kCFTypeDictionaryValueCallBacks)); CFDictionarySetValue(query, kSecClass, kSecClassKey); base::ScopedCFTypeRef<CFStringRef> access_group_ref( base::SysUTF8ToCFStringRef(kKeychainAccessGroup)); - CFDictionarySetValue(query, kSecAttrAccessGroup, access_group_ref.release()); + CFDictionarySetValue(query, kSecAttrAccessGroup, access_group_ref); CFDictionarySetValue(query, kSecAttrNoLegacy, @YES); CFDictionarySetValue(query, kSecReturnAttributes, @YES); CFDictionarySetValue(query, kSecMatchLimit, kSecMatchLimitAll); diff --git a/chromium/device/fido/mac/credential_store.mm b/chromium/device/fido/mac/credential_store.mm index ebcce170ce5..a373dfafc6e 100644 --- a/chromium/device/fido/mac/credential_store.mm +++ b/chromium/device/fido/mac/credential_store.mm @@ -32,8 +32,9 @@ namespace { base::ScopedCFTypeRef<CFMutableDictionaryRef> DefaultKeychainQuery( const AuthenticatorConfig& config, const std::string& rp_id) { - base::ScopedCFTypeRef<CFMutableDictionaryRef> query( - CFDictionaryCreateMutable(kCFAllocatorDefault, 0, nullptr, nullptr)); + base::ScopedCFTypeRef<CFMutableDictionaryRef> query(CFDictionaryCreateMutable( + kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, + &kCFTypeDictionaryValueCallBacks)); CFDictionarySetValue(query, kSecClass, kSecClassKey); CFDictionarySetValue(query, kSecAttrAccessGroup, base::SysUTF8ToNSString(config.keychain_access_group)); @@ -79,8 +80,9 @@ QueryKeychainItemsForProfile(const std::string& keychain_access_group, // keychain access group. std::vector<base::ScopedCFTypeRef<CFDictionaryRef>> result; - base::ScopedCFTypeRef<CFMutableDictionaryRef> query( - CFDictionaryCreateMutable(kCFAllocatorDefault, 0, nullptr, nullptr)); + base::ScopedCFTypeRef<CFMutableDictionaryRef> query(CFDictionaryCreateMutable( + kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, + &kCFTypeDictionaryValueCallBacks)); CFDictionarySetValue(query, kSecClass, kSecClassKey); CFDictionarySetValue(query, kSecAttrAccessGroup, base::SysUTF8ToNSString(keychain_access_group)); @@ -178,7 +180,9 @@ bool DoDeleteWebAuthnCredentials(const std::string& keychain_access_group, continue; } base::ScopedCFTypeRef<CFMutableDictionaryRef> delete_query( - CFDictionaryCreateMutable(kCFAllocatorDefault, 0, nullptr, nullptr)); + CFDictionaryCreateMutable(kCFAllocatorDefault, 0, + &kCFTypeDictionaryKeyCallBacks, + &kCFTypeDictionaryValueCallBacks)); CFDictionarySetValue(delete_query, kSecClass, kSecClassKey); CFDictionarySetValue(delete_query, kSecAttrApplicationLabel, sec_attr_app_label); @@ -233,7 +237,9 @@ TouchIdCredentialStore::CreateCredential( CredentialMetadata::FromPublicKeyCredentialUserEntity(user, is_resident)); base::ScopedCFTypeRef<CFMutableDictionaryRef> params( - CFDictionaryCreateMutable(kCFAllocatorDefault, 0, nullptr, nullptr)); + CFDictionaryCreateMutable(kCFAllocatorDefault, 0, + &kCFTypeDictionaryKeyCallBacks, + &kCFTypeDictionaryValueCallBacks)); CFDictionarySetValue(params, kSecAttrKeyType, kSecAttrKeyTypeECSECPrimeRandom); CFDictionarySetValue(params, kSecAttrKeySizeInBits, @256); diff --git a/chromium/device/fido/mac/make_credential_operation.mm b/chromium/device/fido/mac/make_credential_operation.mm index fc27e4413fa..c8e0e310271 100644 --- a/chromium/device/fido/mac/make_credential_operation.mm +++ b/chromium/device/fido/mac/make_credential_operation.mm @@ -149,6 +149,7 @@ void MakeCredentialOperation::PromptTouchIdDone(bool success) { std::make_unique<PackedAttestationStatement>( CoseAlgorithmIdentifier::kEs256, std::move(*signature), /*x509_certificates=*/std::vector<std::vector<uint8_t>>()))); + response.is_resident_key = request_.resident_key_required; std::move(callback_).Run(CtapDeviceResponseCode::kSuccess, std::move(response)); } diff --git a/chromium/device/fido/mac/touch_id_context.mm b/chromium/device/fido/mac/touch_id_context.mm index f9af4eb253a..82ee3860776 100644 --- a/chromium/device/fido/mac/touch_id_context.mm +++ b/chromium/device/fido/mac/touch_id_context.mm @@ -79,7 +79,9 @@ bool CanCreateSecureEnclaveKeyPair() { // bindings. Instead, attempt to create an ephemeral key pair in the secure // enclave. base::ScopedCFTypeRef<CFMutableDictionaryRef> params( - CFDictionaryCreateMutable(kCFAllocatorDefault, 0, nullptr, nullptr)); + CFDictionaryCreateMutable(kCFAllocatorDefault, 0, + &kCFTypeDictionaryKeyCallBacks, + &kCFTypeDictionaryValueCallBacks)); CFDictionarySetValue(params, kSecAttrKeyType, kSecAttrKeyTypeECSECPrimeRandom); CFDictionarySetValue(params, kSecAttrKeySizeInBits, @256); diff --git a/chromium/device/fido/make_credential_handler_unittest.cc b/chromium/device/fido/make_credential_handler_unittest.cc index 3801a68a00f..2de00096f4b 100644 --- a/chromium/device/fido/make_credential_handler_unittest.cc +++ b/chromium/device/fido/make_credential_handler_unittest.cc @@ -64,14 +64,8 @@ class FidoMakeCredentialHandlerTest : public ::testing::Test { platform_discovery_ = fake_discovery_factory_->ForgeNextPlatformDiscovery(); } - std::unique_ptr<MakeCredentialRequestHandler> CreateMakeCredentialHandler() { - return CreateMakeCredentialHandlerWithAuthenticatorSelectionCriteria( - AuthenticatorSelectionCriteria()); - } - - std::unique_ptr<MakeCredentialRequestHandler> - CreateMakeCredentialHandlerWithAuthenticatorSelectionCriteria( - AuthenticatorSelectionCriteria authenticator_selection_criteria) { + std::unique_ptr<MakeCredentialRequestHandler> CreateMakeCredentialHandler( + AuthenticatorSelectionCriteria authenticator_selection_criteria = {}) { ForgeDiscoveries(); PublicKeyCredentialRpEntity rp(test_data::kRelyingPartyId); PublicKeyCredentialUserEntity user( @@ -83,13 +77,13 @@ class FidoMakeCredentialHandlerTest : public ::testing::Test { test_data::kClientDataJson, std::move(rp), std::move(user), std::move(credential_params)); - MakeCredentialRequestHandler::Options options; + MakeCredentialRequestHandler::Options options( + authenticator_selection_criteria); options.allow_skipping_pin_touch = true; auto handler = std::make_unique<MakeCredentialRequestHandler>( fake_discovery_factory_.get(), supported_transports_, - std::move(request_parameter), - std::move(authenticator_selection_criteria), options, cb_.callback()); + std::move(request_parameter), std::move(options), cb_.callback()); if (pending_mock_platform_device_) { platform_discovery_->AddDevice(std::move(pending_mock_platform_device_)); platform_discovery_->WaitForCallToStartAndSimulateSuccess(); @@ -189,10 +183,9 @@ TEST_F(FidoMakeCredentialHandlerTest, TestU2fRegister) { TEST_F(FidoMakeCredentialHandlerTest, U2fRegisterWithUserVerificationRequired) { auto request_handler = - CreateMakeCredentialHandlerWithAuthenticatorSelectionCriteria( - AuthenticatorSelectionCriteria( - AuthenticatorAttachment::kAny, /*require_resident_key=*/false, - UserVerificationRequirement::kRequired)); + CreateMakeCredentialHandler(AuthenticatorSelectionCriteria( + AuthenticatorAttachment::kAny, ResidentKeyRequirement::kDiscouraged, + UserVerificationRequirement::kRequired)); discovery()->WaitForCallToStartAndSimulateSuccess(); auto device = MockFidoDevice::MakeU2fWithGetInfoExpectation(); @@ -208,10 +201,9 @@ TEST_F(FidoMakeCredentialHandlerTest, U2fRegisterWithUserVerificationRequired) { TEST_F(FidoMakeCredentialHandlerTest, U2fRegisterWithResidentKeyRequirement) { auto request_handler = - CreateMakeCredentialHandlerWithAuthenticatorSelectionCriteria( - AuthenticatorSelectionCriteria( - AuthenticatorAttachment::kAny, /*require_resident_key=*/true, - UserVerificationRequirement::kPreferred)); + CreateMakeCredentialHandler(AuthenticatorSelectionCriteria( + AuthenticatorAttachment::kAny, ResidentKeyRequirement::kRequired, + UserVerificationRequirement::kPreferred)); discovery()->WaitForCallToStartAndSimulateSuccess(); auto device = MockFidoDevice::MakeU2fWithGetInfoExpectation(); @@ -227,10 +219,9 @@ TEST_F(FidoMakeCredentialHandlerTest, U2fRegisterWithResidentKeyRequirement) { TEST_F(FidoMakeCredentialHandlerTest, UserVerificationRequirementNotMet) { auto request_handler = - CreateMakeCredentialHandlerWithAuthenticatorSelectionCriteria( - AuthenticatorSelectionCriteria( - AuthenticatorAttachment::kAny, /*require_resident_key=*/false, - UserVerificationRequirement::kRequired)); + CreateMakeCredentialHandler(AuthenticatorSelectionCriteria( + AuthenticatorAttachment::kAny, ResidentKeyRequirement::kDiscouraged, + UserVerificationRequirement::kRequired)); discovery()->WaitForCallToStartAndSimulateSuccess(); auto device = MockFidoDevice::MakeCtapWithGetInfoExpectation( @@ -247,12 +238,10 @@ TEST_F(FidoMakeCredentialHandlerTest, UserVerificationRequirementNotMet) { } TEST_F(FidoMakeCredentialHandlerTest, CrossPlatformAttachment) { - auto request_handler = - CreateMakeCredentialHandlerWithAuthenticatorSelectionCriteria( - AuthenticatorSelectionCriteria( - AuthenticatorAttachment::kCrossPlatform, - /*require_resident_key=*/false, - UserVerificationRequirement::kPreferred)); + auto request_handler = CreateMakeCredentialHandler( + AuthenticatorSelectionCriteria(AuthenticatorAttachment::kCrossPlatform, + ResidentKeyRequirement::kDiscouraged, + UserVerificationRequirement::kPreferred)); // kCloudAssistedBluetoothLowEnergy not yet supported for MakeCredential. ExpectAllowedTransportsForRequestAre( @@ -271,12 +260,10 @@ TEST_F(FidoMakeCredentialHandlerTest, PlatformAttachment) { EXPECT_CALL(*platform_device, Cancel(_)); set_mock_platform_device(std::move(platform_device)); - auto request_handler = - CreateMakeCredentialHandlerWithAuthenticatorSelectionCriteria( - AuthenticatorSelectionCriteria( - AuthenticatorAttachment::kPlatform, - /*require_resident_key=*/false, - UserVerificationRequirement::kRequired)); + auto request_handler = CreateMakeCredentialHandler( + AuthenticatorSelectionCriteria(AuthenticatorAttachment::kPlatform, + ResidentKeyRequirement::kDiscouraged, + UserVerificationRequirement::kRequired)); ExpectAllowedTransportsForRequestAre(request_handler.get(), {FidoTransportProtocol::kInternal}); @@ -284,10 +271,9 @@ TEST_F(FidoMakeCredentialHandlerTest, PlatformAttachment) { TEST_F(FidoMakeCredentialHandlerTest, ResidentKeyRequirementNotMet) { auto request_handler = - CreateMakeCredentialHandlerWithAuthenticatorSelectionCriteria( - AuthenticatorSelectionCriteria( - AuthenticatorAttachment::kAny, /*require_resident_key=*/true, - UserVerificationRequirement::kPreferred)); + CreateMakeCredentialHandler(AuthenticatorSelectionCriteria( + AuthenticatorAttachment::kAny, ResidentKeyRequirement::kRequired, + UserVerificationRequirement::kPreferred)); discovery()->WaitForCallToStartAndSimulateSuccess(); auto device = MockFidoDevice::MakeCtapWithGetInfoExpectation( @@ -399,10 +385,9 @@ TEST_F(FidoMakeCredentialHandlerTest, ResidentKeyCancelOtherAuthenticator) { // most important: we don't want a stray touch to create a resident credential // on a second authenticator. auto request_handler = - CreateMakeCredentialHandlerWithAuthenticatorSelectionCriteria( - AuthenticatorSelectionCriteria( - AuthenticatorAttachment::kAny, /*require_resident_key=*/true, - UserVerificationRequirement::kRequired)); + CreateMakeCredentialHandler(AuthenticatorSelectionCriteria( + AuthenticatorAttachment::kAny, ResidentKeyRequirement::kRequired, + UserVerificationRequirement::kRequired)); discovery()->WaitForCallToStartAndSimulateSuccess(); auto device1 = MockFidoDevice::MakeCtapWithGetInfoExpectation(); @@ -433,10 +418,9 @@ TEST_F(FidoMakeCredentialHandlerTest, ResidentKeyCancel) { // request handler is deleted. When a user cancels, we don't want a stray // touch creating a resident key. auto request_handler = - CreateMakeCredentialHandlerWithAuthenticatorSelectionCriteria( - AuthenticatorSelectionCriteria( - AuthenticatorAttachment::kAny, /*require_resident_key=*/true, - UserVerificationRequirement::kRequired)); + CreateMakeCredentialHandler(AuthenticatorSelectionCriteria( + AuthenticatorAttachment::kAny, ResidentKeyRequirement::kRequired, + UserVerificationRequirement::kRequired)); auto delete_request_handler = [&request_handler]() { base::ThreadTaskRunnerHandle::Get()->PostTask( @@ -464,12 +448,10 @@ TEST_F(FidoMakeCredentialHandlerTest, ResidentKeyCancel) { TEST_F(FidoMakeCredentialHandlerTest, AuthenticatorSelectionCriteriaSatisfiedByCrossPlatformDevice) { set_supported_transports({FidoTransportProtocol::kUsbHumanInterfaceDevice}); - auto request_handler = - CreateMakeCredentialHandlerWithAuthenticatorSelectionCriteria( - AuthenticatorSelectionCriteria( - AuthenticatorAttachment::kCrossPlatform, - /*require_resident_key=*/true, - UserVerificationRequirement::kRequired)); + auto request_handler = CreateMakeCredentialHandler( + AuthenticatorSelectionCriteria(AuthenticatorAttachment::kCrossPlatform, + ResidentKeyRequirement::kRequired, + UserVerificationRequirement::kRequired)); discovery()->WaitForCallToStartAndSimulateSuccess(); auto device = MockFidoDevice::MakeCtapWithGetInfoExpectation(); @@ -504,11 +486,9 @@ TEST_F(FidoMakeCredentialHandlerTest, set_mock_platform_device(std::move(platform_device)); auto request_handler = - CreateMakeCredentialHandlerWithAuthenticatorSelectionCriteria( - AuthenticatorSelectionCriteria( - AuthenticatorAttachment::kPlatform, - /*require_resident_key=*/true, - UserVerificationRequirement::kRequired)); + CreateMakeCredentialHandler(AuthenticatorSelectionCriteria( + AuthenticatorAttachment::kPlatform, ResidentKeyRequirement::kRequired, + UserVerificationRequirement::kRequired)); callback().WaitForCallback(); EXPECT_EQ(MakeCredentialStatus::kSuccess, callback().status()); @@ -522,12 +502,10 @@ TEST_F(FidoMakeCredentialHandlerTest, // its GetInfo response is rejected. TEST_F(FidoMakeCredentialHandlerTest, CrossPlatformAuthenticatorPretendingToBePlatform) { - auto request_handler = - CreateMakeCredentialHandlerWithAuthenticatorSelectionCriteria( - AuthenticatorSelectionCriteria( - AuthenticatorAttachment::kCrossPlatform, - /*require_resident_key=*/false, - UserVerificationRequirement::kPreferred)); + auto request_handler = CreateMakeCredentialHandler( + AuthenticatorSelectionCriteria(AuthenticatorAttachment::kCrossPlatform, + ResidentKeyRequirement::kDiscouraged, + UserVerificationRequirement::kPreferred)); discovery()->WaitForCallToStartAndSimulateSuccess(); auto device = MockFidoDevice::MakeCtapWithGetInfoExpectation( @@ -553,11 +531,9 @@ TEST_F(FidoMakeCredentialHandlerTest, set_mock_platform_device(std::move(platform_device)); auto request_handler = - CreateMakeCredentialHandlerWithAuthenticatorSelectionCriteria( - AuthenticatorSelectionCriteria( - AuthenticatorAttachment::kPlatform, - /*require_resident_key=*/true, - UserVerificationRequirement::kRequired)); + CreateMakeCredentialHandler(AuthenticatorSelectionCriteria( + AuthenticatorAttachment::kPlatform, ResidentKeyRequirement::kRequired, + UserVerificationRequirement::kRequired)); task_environment_.FastForwardUntilNoTasksRemain(); EXPECT_FALSE(callback().was_called()); @@ -569,22 +545,19 @@ TEST_F(FidoMakeCredentialHandlerTest, SupportedTransportsAreOnlyNfc) { }; set_supported_transports(kNfc); - auto request_handler = - CreateMakeCredentialHandlerWithAuthenticatorSelectionCriteria( - AuthenticatorSelectionCriteria( - AuthenticatorAttachment::kCrossPlatform, - /*require_resident_key=*/false, - UserVerificationRequirement::kPreferred)); + auto request_handler = CreateMakeCredentialHandler( + AuthenticatorSelectionCriteria(AuthenticatorAttachment::kCrossPlatform, + ResidentKeyRequirement::kDiscouraged, + UserVerificationRequirement::kPreferred)); ExpectAllowedTransportsForRequestAre(request_handler.get(), kNfc); } TEST_F(FidoMakeCredentialHandlerTest, IncorrectRpIdHash) { auto request_handler = - CreateMakeCredentialHandlerWithAuthenticatorSelectionCriteria( - AuthenticatorSelectionCriteria( - AuthenticatorAttachment::kAny, /*require_resident_key=*/false, - UserVerificationRequirement::kPreferred)); + CreateMakeCredentialHandler(AuthenticatorSelectionCriteria( + AuthenticatorAttachment::kAny, ResidentKeyRequirement::kDiscouraged, + UserVerificationRequirement::kPreferred)); discovery()->WaitForCallToStartAndSimulateSuccess(); auto device = MockFidoDevice::MakeCtapWithGetInfoExpectation(); @@ -609,10 +582,9 @@ TEST_F(FidoMakeCredentialHandlerTest, state->fingerprints_enrolled = true; auto request_handler = - CreateMakeCredentialHandlerWithAuthenticatorSelectionCriteria( - AuthenticatorSelectionCriteria( - AuthenticatorAttachment::kAny, /*require_resident_key=*/true, - UserVerificationRequirement::kPreferred)); + CreateMakeCredentialHandler(AuthenticatorSelectionCriteria( + AuthenticatorAttachment::kAny, ResidentKeyRequirement::kRequired, + UserVerificationRequirement::kPreferred)); discovery()->WaitForCallToStartAndSimulateSuccess(); discovery()->AddDevice(std::make_unique<VirtualCtap2Device>( @@ -629,10 +601,9 @@ TEST_F(FidoMakeCredentialHandlerTest, MakeCredentialFailsForIncompatibleResidentKeyOption) { auto device = std::make_unique<VirtualCtap2Device>(); auto request_handler = - CreateMakeCredentialHandlerWithAuthenticatorSelectionCriteria( - AuthenticatorSelectionCriteria( - AuthenticatorAttachment::kAny, /*require_resident_key=*/true, - UserVerificationRequirement::kPreferred)); + CreateMakeCredentialHandler(AuthenticatorSelectionCriteria( + AuthenticatorAttachment::kAny, ResidentKeyRequirement::kRequired, + UserVerificationRequirement::kPreferred)); discovery()->WaitForCallToStartAndSimulateSuccess(); discovery()->AddDevice(std::move(device)); @@ -655,12 +626,10 @@ TEST_F(FidoMakeCredentialHandlerTest, CtapDeviceResponseCode::kCtap2ErrOperationDenied); set_mock_platform_device(std::move(platform_device)); - auto request_handler = - CreateMakeCredentialHandlerWithAuthenticatorSelectionCriteria( - AuthenticatorSelectionCriteria( - AuthenticatorAttachment::kPlatform, - /*require_resident_key=*/false, - UserVerificationRequirement::kPreferred)); + auto request_handler = CreateMakeCredentialHandler( + AuthenticatorSelectionCriteria(AuthenticatorAttachment::kPlatform, + ResidentKeyRequirement::kDiscouraged, + UserVerificationRequirement::kPreferred)); task_environment_.FastForwardUntilNoTasksRemain(); EXPECT_TRUE(callback().was_called()); @@ -677,10 +646,9 @@ TEST_F(FidoMakeCredentialHandlerTest, CtapDeviceResponseCode::kCtap2ErrOperationDenied); auto request_handler = - CreateMakeCredentialHandlerWithAuthenticatorSelectionCriteria( - AuthenticatorSelectionCriteria( - AuthenticatorAttachment::kAny, /*require_resident_key=*/false, - UserVerificationRequirement::kPreferred)); + CreateMakeCredentialHandler(AuthenticatorSelectionCriteria( + AuthenticatorAttachment::kAny, ResidentKeyRequirement::kDiscouraged, + UserVerificationRequirement::kPreferred)); discovery()->WaitForCallToStartAndSimulateSuccess(); discovery()->AddDevice(std::move(device)); @@ -707,10 +675,9 @@ TEST_F(FidoMakeCredentialHandlerTest, IsUvRequest(true)); auto request_handler = - CreateMakeCredentialHandlerWithAuthenticatorSelectionCriteria( - AuthenticatorSelectionCriteria( - AuthenticatorAttachment::kAny, /*require_resident_key=*/false, - UserVerificationRequirement::kDiscouraged)); + CreateMakeCredentialHandler(AuthenticatorSelectionCriteria( + AuthenticatorAttachment::kAny, ResidentKeyRequirement::kDiscouraged, + UserVerificationRequirement::kDiscouraged)); discovery()->AddDevice(std::move(device)); discovery()->WaitForCallToStartAndSimulateSuccess(); @@ -727,10 +694,9 @@ TEST_F(FidoMakeCredentialHandlerTest, TestRequestWithPinAuthInvalid) { CtapDeviceResponseCode::kCtap2ErrPinAuthInvalid); auto request_handler = - CreateMakeCredentialHandlerWithAuthenticatorSelectionCriteria( - AuthenticatorSelectionCriteria( - AuthenticatorAttachment::kAny, /*require_resident_key=*/false, - UserVerificationRequirement::kPreferred)); + CreateMakeCredentialHandler(AuthenticatorSelectionCriteria( + AuthenticatorAttachment::kAny, ResidentKeyRequirement::kDiscouraged, + UserVerificationRequirement::kPreferred)); discovery()->WaitForCallToStartAndSimulateSuccess(); discovery()->AddDevice(std::move(device)); diff --git a/chromium/device/fido/make_credential_request_handler.cc b/chromium/device/fido/make_credential_request_handler.cc index 30c96104071..19720a51ac2 100644 --- a/chromium/device/fido/make_credential_request_handler.cc +++ b/chromium/device/fido/make_credential_request_handler.cc @@ -79,7 +79,7 @@ base::Optional<MakeCredentialStatus> ConvertDeviceResponseCode( // should even blink for a request. bool IsCandidateAuthenticatorPreTouch( FidoAuthenticator* authenticator, - const AuthenticatorSelectionCriteria& authenticator_selection_criteria) { + AuthenticatorAttachment requested_attachment) { const auto& opt_options = authenticator->Options(); if (!opt_options) { // This authenticator doesn't know its capabilities yet, so we need @@ -88,11 +88,9 @@ bool IsCandidateAuthenticatorPreTouch( return true; } - if ((authenticator_selection_criteria.authenticator_attachment() == - AuthenticatorAttachment::kPlatform && + if ((requested_attachment == AuthenticatorAttachment::kPlatform && !opt_options->is_platform_device) || - (authenticator_selection_criteria.authenticator_attachment() == - AuthenticatorAttachment::kCrossPlatform && + (requested_attachment == AuthenticatorAttachment::kCrossPlatform && opt_options->is_platform_device)) { return false; } @@ -106,14 +104,14 @@ MakeCredentialStatus IsCandidateAuthenticatorPostTouch( const CtapMakeCredentialRequest& request, FidoAuthenticator* authenticator, const MakeCredentialRequestHandler::Options& options, - const AuthenticatorSelectionCriteria& authenticator_selection_criteria, const FidoRequestHandlerBase::Observer* observer) { if (options.cred_protect_request && options.cred_protect_request->second && !authenticator->SupportsCredProtectExtension()) { return MakeCredentialStatus::kAuthenticatorMissingResidentKeys; } - const auto& auth_options = authenticator->Options(); + const base::Optional<AuthenticatorSupportedOptions>& auth_options = + authenticator->Options(); if (!auth_options) { // This authenticator doesn't know its capabilities yet, so we need // to assume it can handle the request. This is the case for Windows, @@ -121,7 +119,7 @@ MakeCredentialStatus IsCandidateAuthenticatorPostTouch( return MakeCredentialStatus::kSuccess; } - if (authenticator_selection_criteria.require_resident_key() && + if (options.resident_key == ResidentKeyRequirement::kRequired && !auth_options->supports_resident_key) { return MakeCredentialStatus::kAuthenticatorMissingResidentKeys; } @@ -160,10 +158,8 @@ MakeCredentialStatus IsCandidateAuthenticatorPostTouch( } base::flat_set<FidoTransportProtocol> GetTransportsAllowedByRP( - const AuthenticatorSelectionCriteria& authenticator_selection_criteria) { - const auto attachment_type = - authenticator_selection_criteria.authenticator_attachment(); - switch (attachment_type) { + AuthenticatorAttachment authenticator_attachment) { + switch (authenticator_attachment) { case AuthenticatorAttachment::kPlatform: return {FidoTransportProtocol::kInternal}; case AuthenticatorAttachment::kCrossPlatform: @@ -317,6 +313,11 @@ bool ResponseValid(const FidoAuthenticator& authenticator, return false; } + if (request.large_blob_key && !response.large_blob_key()) { + FIDO_LOG(ERROR) << "Large blob key requested but not returned"; + return false; + } + return true; } } // namespace @@ -324,26 +325,37 @@ bool ResponseValid(const FidoAuthenticator& authenticator, MakeCredentialRequestHandler::Options::Options() = default; MakeCredentialRequestHandler::Options::~Options() = default; MakeCredentialRequestHandler::Options::Options(const Options&) = default; +MakeCredentialRequestHandler::Options::Options( + const AuthenticatorSelectionCriteria& authenticator_selection_criteria) + : authenticator_attachment( + authenticator_selection_criteria.authenticator_attachment()), + resident_key(authenticator_selection_criteria.resident_key()), + user_verification( + authenticator_selection_criteria.user_verification_requirement()) {} +MakeCredentialRequestHandler::Options::Options(Options&&) = default; +MakeCredentialRequestHandler::Options& +MakeCredentialRequestHandler::Options::operator=(const Options&) = default; +MakeCredentialRequestHandler::Options& +MakeCredentialRequestHandler::Options::operator=(Options&&) = default; MakeCredentialRequestHandler::MakeCredentialRequestHandler( FidoDiscoveryFactory* fido_discovery_factory, const base::flat_set<FidoTransportProtocol>& supported_transports, CtapMakeCredentialRequest request, - AuthenticatorSelectionCriteria authenticator_selection_criteria, const Options& options, CompletionCallback completion_callback) : FidoRequestHandlerBase( fido_discovery_factory, base::STLSetIntersection<base::flat_set<FidoTransportProtocol>>( supported_transports, - GetTransportsAllowedByRP(authenticator_selection_criteria))), + GetTransportsAllowedByRP(options.authenticator_attachment))), completion_callback_(std::move(completion_callback)), request_(std::move(request)), - authenticator_selection_criteria_( - std::move(authenticator_selection_criteria)), options_(options) { // These parts of the request should be filled in by // |SpecializeRequestForAuthenticator|. + DCHECK_EQ(request_.authenticator_attachment, AuthenticatorAttachment::kAny); + DCHECK(!request_.resident_key_required); DCHECK(!request_.cred_protect); DCHECK(!request_.android_client_data_ext); DCHECK(!request_.cred_protect_enforce); @@ -351,21 +363,6 @@ MakeCredentialRequestHandler::MakeCredentialRequestHandler( transport_availability_info().request_type = FidoRequestHandlerBase::RequestType::kMakeCredential; - // Set the rk, uv and attachment fields, which were only initialized to - // default values up to here. TODO(martinkr): Initialize these fields earlier - // (in AuthenticatorImpl) and get rid of the separate - // AuthenticatorSelectionCriteriaParameter. - if (authenticator_selection_criteria_.require_resident_key()) { - request_.resident_key_required = true; - request_.user_verification = UserVerificationRequirement::kRequired; - } else { - request_.resident_key_required = false; - request_.user_verification = - authenticator_selection_criteria_.user_verification_requirement(); - } - request_.authenticator_attachment = - authenticator_selection_criteria_.authenticator_attachment(); - Start(); } @@ -377,7 +374,7 @@ void MakeCredentialRequestHandler::DispatchRequest( if (state_ != State::kWaitingForTouch || !IsCandidateAuthenticatorPreTouch(authenticator, - authenticator_selection_criteria_)) { + options_.authenticator_attachment)) { return; } @@ -386,7 +383,6 @@ void MakeCredentialRequestHandler::DispatchRequest( SpecializeRequestForAuthenticator(request.get(), authenticator); if (IsCandidateAuthenticatorPostTouch(*request.get(), authenticator, options_, - authenticator_selection_criteria_, observer()) != MakeCredentialStatus::kSuccess) { #if defined(OS_WIN) @@ -457,12 +453,9 @@ void MakeCredentialRequestHandler::DispatchRequest( if (!request->is_u2f_only && request->user_verification != UserVerificationRequirement::kDiscouraged && authenticator->CanGetUvToken()) { - base::Optional<std::string> rp_id(request->rp.id); - authenticator->GetUvToken( - std::move(rp_id), - base::BindOnce(&MakeCredentialRequestHandler::OnHaveUvToken, - weak_factory_.GetWeakPtr(), authenticator, - std::move(request))); + authenticator->GetUvRetries(base::BindOnce( + &MakeCredentialRequestHandler::OnStartUvTokenOrFallback, + weak_factory_.GetWeakPtr(), authenticator, std::move(request))); return; } @@ -561,6 +554,24 @@ void MakeCredentialRequestHandler::HandleResponse( return; } + if (options_.resident_key == ResidentKeyRequirement::kPreferred && + request->resident_key_required && + status == CtapDeviceResponseCode::kCtap2ErrKeyStoreFull) { + // TODO(crbug/1117630): This probably requires a second touch and we should + // add UI for that. PR #962 aims to change CTAP2.1 to return this error + // before UP, so we might need to gate this on the supported CTAP version. + FIDO_LOG(DEBUG) << "Downgrading rk=preferred to non-resident credential " + "because key storage is full"; + request->resident_key_required = false; + CtapMakeCredentialRequest request_copy(*request); + authenticator->MakeCredential( + std::move(request_copy), + base::BindOnce(&MakeCredentialRequestHandler::HandleResponse, + weak_factory_.GetWeakPtr(), authenticator, + std::move(request), base::ElapsedTimer())); + return; + } + const base::Optional<MakeCredentialStatus> maybe_result = ConvertDeviceResponseCode(status); if (!maybe_result) { @@ -666,7 +677,6 @@ void MakeCredentialRequestHandler::HandleInapplicableAuthenticator( CancelActiveAuthenticators(authenticator->GetId()); const MakeCredentialStatus capability_error = IsCandidateAuthenticatorPostTouch(*request.get(), authenticator, options_, - authenticator_selection_criteria_, observer()); DCHECK_NE(capability_error, MakeCredentialStatus::kSuccess); std::move(completion_callback_).Run(capability_error, base::nullopt, nullptr); @@ -851,6 +861,42 @@ void MakeCredentialRequestHandler::OnEnrollmentComplete( DispatchRequestWithToken(std::move(request), std::move(token)); } +void MakeCredentialRequestHandler::OnStartUvTokenOrFallback( + FidoAuthenticator* authenticator, + std::unique_ptr<CtapMakeCredentialRequest> request, + CtapDeviceResponseCode status, + base::Optional<pin::RetriesResponse> response) { + size_t retries; + if (status != CtapDeviceResponseCode::kSuccess) { + FIDO_LOG(ERROR) << "OnStartUvTokenOrFallback() failed for " + << authenticator_->GetDisplayName() + << ", assuming authenticator locked."; + retries = 0; + } else { + retries = response->retries; + } + + if (retries == 0) { + if (authenticator->WillNeedPINToMakeCredential(*request, observer()) == + MakeCredentialPINDisposition::kUsePINForFallback) { + authenticator->GetTouch(base::BindOnce( + &MakeCredentialRequestHandler::StartPINFallbackForInternalUv, + weak_factory_.GetWeakPtr(), authenticator, std::move(request))); + return; + } + authenticator->GetTouch( + base::BindOnce(&MakeCredentialRequestHandler::HandleInternalUvLocked, + weak_factory_.GetWeakPtr(), authenticator)); + } + + base::Optional<std::string> rp_id(request->rp.id); + authenticator->GetUvToken( + std::move(rp_id), + base::BindOnce(&MakeCredentialRequestHandler::OnHaveUvToken, + weak_factory_.GetWeakPtr(), authenticator, + std::move(request))); +} + void MakeCredentialRequestHandler::OnUvRetriesResponse( std::unique_ptr<CtapMakeCredentialRequest> request, CtapDeviceResponseCode status, @@ -894,28 +940,19 @@ void MakeCredentialRequestHandler::OnHaveUvToken( return; } - if (status == CtapDeviceResponseCode::kCtap2ErrPinInvalid || + if (status == CtapDeviceResponseCode::kCtap2ErrUvInvalid || status == CtapDeviceResponseCode::kCtap2ErrOperationDenied || status == CtapDeviceResponseCode::kCtap2ErrUvBlocked) { if (status == CtapDeviceResponseCode::kCtap2ErrUvBlocked) { - // This error is returned immediately without user interaction. Ask for a - // touch and fall back to PIN. - FIDO_LOG(DEBUG) << "Internal UV blocked for " - << authenticator->GetDisplayName() - << ", falling back to PIN."; if (authenticator->WillNeedPINToMakeCredential(*request, observer()) == MakeCredentialPINDisposition::kUsePINForFallback) { - authenticator->GetTouch(base::BindOnce( - &MakeCredentialRequestHandler::StartPINFallbackForInternalUv, - weak_factory_.GetWeakPtr(), authenticator, std::move(request))); + StartPINFallbackForInternalUv(authenticator, std::move(request)); return; } - authenticator->GetTouch( - base::BindOnce(&MakeCredentialRequestHandler::HandleInternalUvLocked, - weak_factory_.GetWeakPtr(), authenticator)); + HandleInternalUvLocked(authenticator); return; } - DCHECK(status == CtapDeviceResponseCode::kCtap2ErrPinInvalid || + DCHECK(status == CtapDeviceResponseCode::kCtap2ErrUvInvalid || status == CtapDeviceResponseCode::kCtap2ErrOperationDenied); CancelActiveAuthenticators(authenticator->GetId()); authenticator_ = authenticator; @@ -958,6 +995,40 @@ void MakeCredentialRequestHandler::DispatchRequestWithToken( void MakeCredentialRequestHandler::SpecializeRequestForAuthenticator( CtapMakeCredentialRequest* request, const FidoAuthenticator* authenticator) { + // Only Windows cares about |authenticator_attachment| on the request. + request->authenticator_attachment = options_.authenticator_attachment; + + const base::Optional<AuthenticatorSupportedOptions>& auth_options = + authenticator->Options(); + switch (options_.resident_key) { + case ResidentKeyRequirement::kRequired: + request->resident_key_required = true; + request->user_verification = UserVerificationRequirement::kRequired; + break; + case ResidentKeyRequirement::kPreferred: { + // Create a resident key if the authenticator supports it and the UI is + // capable of prompting for PIN/UV. + request->resident_key_required = +#if defined(OS_WIN) + // Windows does not yet support rk=preferred. + !authenticator->IsWinNativeApiAuthenticator() && +#endif + auth_options && auth_options->supports_resident_key && + (observer()->SupportsPIN() || + auth_options->user_verification_availability == + AuthenticatorSupportedOptions::UserVerificationAvailability:: + kSupportedAndConfigured); + break; + } + case ResidentKeyRequirement::kDiscouraged: + request->resident_key_required = false; + break; + } + + request->user_verification = request->resident_key_required + ? UserVerificationRequirement::kRequired + : options_.user_verification; + if (options_.cred_protect_request && authenticator->SupportsCredProtectExtension()) { request->cred_protect = CredProtectForAuthenticator( @@ -965,8 +1036,8 @@ void MakeCredentialRequestHandler::SpecializeRequestForAuthenticator( request->cred_protect_enforce = options_.cred_protect_request->second; } - if (options_.android_client_data_ext && authenticator->Options() && - authenticator->Options()->supports_android_client_data_ext) { + if (options_.android_client_data_ext && auth_options && + auth_options->supports_android_client_data_ext) { request->android_client_data_ext = *options_.android_client_data_ext; } @@ -974,6 +1045,11 @@ void MakeCredentialRequestHandler::SpecializeRequestForAuthenticator( request->hmac_secret = false; } + if (request->large_blob_key && + !authenticator->Options()->supports_large_blobs) { + request->large_blob_key = false; + } + if (!authenticator->SupportsEnterpriseAttestation()) { switch (request->attestation_preference) { case AttestationConveyancePreference::kEnterpriseApprovedByBrowser: diff --git a/chromium/device/fido/make_credential_request_handler.h b/chromium/device/fido/make_credential_request_handler.h index 6f42a9c56b0..2d3682bd224 100644 --- a/chromium/device/fido/make_credential_request_handler.h +++ b/chromium/device/fido/make_credential_request_handler.h @@ -76,24 +76,48 @@ class COMPONENT_EXPORT(DEVICE_FIDO) MakeCredentialRequestHandler // |CtapMakeCredentialRequest|. struct COMPONENT_EXPORT(DEVICE_FIDO) Options { Options(); + explicit Options( + const AuthenticatorSelectionCriteria& authenticator_selection_criteria); ~Options(); Options(const Options&); + Options(Options&&); + Options& operator=(const Options&); + Options& operator=(Options&&); - bool allow_skipping_pin_touch = false; - base::Optional<AndroidClientDataExtensionInput> android_client_data_ext; + // authenticator_attachment is a constraint on the type of authenticator + // that a credential should be created on. + AuthenticatorAttachment authenticator_attachment = + AuthenticatorAttachment::kAny; + + // resident_key indicates whether the request should result in the creation + // of a client-side discoverable credential (aka resident key). + ResidentKeyRequirement resident_key = ResidentKeyRequirement::kDiscouraged; + + // user_verification indicates whether the authenticator should (or must) + // perform user verficiation before creating the credential. + UserVerificationRequirement user_verification = + UserVerificationRequirement::kPreferred; // cred_protect_request extends |CredProtect| to include information that // applies at request-routing time. The second element is true if the // indicated protection level must be provided by the target authenticator // for the MakeCredential request to be sent. base::Optional<std::pair<CredProtectRequest, bool>> cred_protect_request; + + // allow_skipping_pin_touch causes the handler to forego the first + // "touch-only" step to collect a PIN if exactly one authenticator is + // discovered. + bool allow_skipping_pin_touch = false; + + // android_client_data_ext is a compatibility hack to support the Clank + // caBLEv2 authenticator. + base::Optional<AndroidClientDataExtensionInput> android_client_data_ext; }; MakeCredentialRequestHandler( FidoDiscoveryFactory* fido_discovery_factory, const base::flat_set<FidoTransportProtocol>& supported_transports, CtapMakeCredentialRequest request_parameter, - AuthenticatorSelectionCriteria authenticator_criteria, const Options& options, CompletionCallback completion_callback); ~MakeCredentialRequestHandler() override; @@ -157,6 +181,11 @@ class COMPONENT_EXPORT(DEVICE_FIDO) MakeCredentialRequestHandler base::Optional<pin::TokenResponse> response); void OnEnrollmentComplete(std::unique_ptr<CtapMakeCredentialRequest> request); void OnEnrollmentDismissed(); + void OnStartUvTokenOrFallback( + FidoAuthenticator* authenticator, + std::unique_ptr<CtapMakeCredentialRequest> request, + CtapDeviceResponseCode status, + base::Optional<pin::RetriesResponse> response); void OnUvRetriesResponse(std::unique_ptr<CtapMakeCredentialRequest> request, CtapDeviceResponseCode status, base::Optional<pin::RetriesResponse> response); @@ -176,7 +205,6 @@ class COMPONENT_EXPORT(DEVICE_FIDO) MakeCredentialRequestHandler State state_ = State::kWaitingForTouch; CtapMakeCredentialRequest request_; base::Optional<base::RepeatingClosure> bio_enrollment_complete_barrier_; - AuthenticatorSelectionCriteria authenticator_selection_criteria_; const Options options_; // authenticator_ points to the authenticator that will be used for this diff --git a/chromium/device/fido/make_credential_task.cc b/chromium/device/fido/make_credential_task.cc index 304f0018a18..ca6976033ce 100644 --- a/chromium/device/fido/make_credential_task.cc +++ b/chromium/device/fido/make_credential_task.cc @@ -51,6 +51,47 @@ bool CtapDeviceShouldUseU2fBecauseClientPinIsSet( return client_pin_set && supports_u2f; } +// ConvertCTAPResponse returns the AuthenticatorMakeCredentialResponse for a +// given CTAP response message in |cbor|. It wraps +// ReadCTAPMakeCredentialResponse() and in addition fills in |is_resident_key|, +// which requires looking at the request and device. +base::Optional<AuthenticatorMakeCredentialResponse> ConvertCTAPResponse( + FidoDevice* device, + bool resident_key_required, + const base::Optional<cbor::Value>& cbor) { + DCHECK_EQ(device->supported_protocol(), ProtocolVersion::kCtap2); + DCHECK(device->device_info()); + + base::Optional<AuthenticatorMakeCredentialResponse> response = + ReadCTAPMakeCredentialResponse(device->DeviceTransport(), cbor); + if (!response) { + return base::nullopt; + } + + // Fill in whether the created credential is client-side discoverable + // (resident). CTAP 2.0 authenticators may decide to treat all credentials as + // discoverable, so we need to omit the value unless a resident key was + // required. + DCHECK(!response->is_resident_key.has_value()); + if (resident_key_required) { + response->is_resident_key = true; + } else { + const bool resident_key_supported = + device->device_info()->options.supports_resident_key; + const base::flat_set<Ctap2Version>& ctap2_versions = + device->device_info()->ctap2_versions; + DCHECK(!ctap2_versions.empty()); + const bool is_at_least_ctap2_1 = + std::any_of(ctap2_versions.begin(), ctap2_versions.end(), + [](Ctap2Version v) { return v > Ctap2Version::kCtap2_0; }); + if (!resident_key_supported || is_at_least_ctap2_1) { + response->is_resident_key = false; + } + } + + return response; +} + } // namespace MakeCredentialTask::MakeCredentialTask(FidoDevice* device, @@ -169,8 +210,8 @@ void MakeCredentialTask::MakeCredential() { register_operation_ = std::make_unique<Ctap2DeviceOperation< CtapMakeCredentialRequest, AuthenticatorMakeCredentialResponse>>( device(), std::move(request), std::move(callback_), - base::BindOnce(&ReadCTAPMakeCredentialResponse, - device()->DeviceTransport()), + base::BindOnce(&ConvertCTAPResponse, device(), + request_.resident_key_required), /*string_fixup_predicate=*/nullptr); register_operation_->Start(); return; @@ -210,8 +251,8 @@ void MakeCredentialTask::HandleResponseToSilentSignRequest( register_operation_ = std::make_unique<Ctap2DeviceOperation< CtapMakeCredentialRequest, AuthenticatorMakeCredentialResponse>>( device(), std::move(request), std::move(callback_), - base::BindOnce(&ReadCTAPMakeCredentialResponse, - device()->DeviceTransport()), + base::BindOnce(&ConvertCTAPResponse, device(), + request_.resident_key_required), /*string_fixup_predicate=*/nullptr); register_operation_->Start(); return; @@ -228,8 +269,8 @@ void MakeCredentialTask::HandleResponseToSilentSignRequest( device(), GetTouchRequest(device()), base::BindOnce(&MakeCredentialTask::HandleResponseToDummyTouch, weak_factory_.GetWeakPtr()), - base::BindOnce(&ReadCTAPMakeCredentialResponse, - device()->DeviceTransport()), + base::BindOnce(&ConvertCTAPResponse, device(), + /*resident_key_required=*/false), /*string_fixup_predicate=*/nullptr); register_operation_->Start(); return; @@ -267,8 +308,8 @@ void MakeCredentialTask::HandleResponseToSilentSignRequest( register_operation_ = std::make_unique<Ctap2DeviceOperation< CtapMakeCredentialRequest, AuthenticatorMakeCredentialResponse>>( device(), std::move(request), std::move(callback_), - base::BindOnce(&ReadCTAPMakeCredentialResponse, - device()->DeviceTransport()), + base::BindOnce(&ConvertCTAPResponse, device(), + request_.resident_key_required), /*string_fixup_predicate=*/nullptr); register_operation_->Start(); } @@ -306,6 +347,8 @@ void MakeCredentialTask::MaybeRevertU2fFallback( device()->set_supported_protocol(ProtocolVersion::kCtap2); } + DCHECK(!response || *response->is_resident_key == false); + std::move(callback_).Run(status, std::move(response)); } diff --git a/chromium/device/fido/pin.h b/chromium/device/fido/pin.h index ef91cb736d1..2732ef9de61 100644 --- a/chromium/device/fido/pin.h +++ b/chromium/device/fido/pin.h @@ -31,13 +31,20 @@ enum class Permissions : uint8_t { kGetAssertion = 0x02, kCredentialManagement = 0x04, kBioEnrollment = 0x08, - kPlatformConfiguration = 0x10, + kLargeBlobWrite = 0x10, }; // kProtocolVersion is the version of the PIN protocol that this code // implements. constexpr int kProtocolVersion = 1; +// Some commands that validate PinUvAuthTokens include this padding to ensure a +// PinUvAuthParam cannot be reused across different commands. +constexpr std::array<uint8_t, 32> kPinUvAuthTokenSafetyPadding = { + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}; + // IsValid returns true if |pin|, which must be UTF-8, is a syntactically valid // PIN. COMPONENT_EXPORT(DEVICE_FIDO) bool IsValid(const std::string& pin); @@ -246,7 +253,7 @@ class HMACSecretRequest { // decrypt a response, the shared key from the request is needed. Once a pin- // token has been decrypted, it can be used to calculate the pinAuth parameters // needed to show user-verification in future operations. -class TokenResponse { +class COMPONENT_EXPORT(DEVICE_FIDO) TokenResponse { public: ~TokenResponse(); TokenResponse(const TokenResponse&); diff --git a/chromium/device/fido/virtual_ctap2_device.cc b/chromium/device/fido/virtual_ctap2_device.cc index f30470032b5..ca36525fc7a 100644 --- a/chromium/device/fido/virtual_ctap2_device.cc +++ b/chromium/device/fido/virtual_ctap2_device.cc @@ -24,12 +24,14 @@ #include "crypto/ec_private_key.h" #include "device/fido/authenticator_get_assertion_response.h" #include "device/fido/authenticator_make_credential_response.h" +#include "device/fido/authenticator_supported_options.h" #include "device/fido/bio/enrollment.h" #include "device/fido/credential_management.h" #include "device/fido/ctap_get_assertion_request.h" #include "device/fido/ctap_make_credential_request.h" #include "device/fido/fido_constants.h" #include "device/fido/fido_parsing_utils.h" +#include "device/fido/large_blob.h" #include "device/fido/opaque_attestation_statement.h" #include "device/fido/p256_public_key.h" #include "device/fido/pin.h" @@ -59,7 +61,8 @@ constexpr uint8_t kSupportedPermissionsMask = static_cast<uint8_t>(pin::Permissions::kMakeCredential) | static_cast<uint8_t>(pin::Permissions::kGetAssertion) | static_cast<uint8_t>(pin::Permissions::kCredentialManagement) | - static_cast<uint8_t>(pin::Permissions::kBioEnrollment); + static_cast<uint8_t>(pin::Permissions::kBioEnrollment) | + static_cast<uint8_t>(pin::Permissions::kLargeBlobWrite); struct PinUvAuthTokenPermissions { uint8_t permissions; @@ -167,7 +170,8 @@ std::vector<uint8_t> ConstructMakeCredentialResponse( base::span<const uint8_t> signature, AuthenticatorData authenticator_data, base::Optional<std::vector<uint8_t>> android_client_data_ext, - bool enterprise_attestation_requested) { + bool enterprise_attestation_requested, + base::Optional<std::array<uint8_t, kLargeBlobKeyLength>> large_blob_key) { cbor::Value::MapValue attestation_map; attestation_map.emplace("alg", -7); attestation_map.emplace("sig", fido_parsing_utils::Materialize(signature)); @@ -190,6 +194,9 @@ std::vector<uint8_t> ConstructMakeCredentialResponse( } make_credential_response.enterprise_attestation_returned = enterprise_attestation_requested; + if (large_blob_key) { + make_credential_response.set_large_blob_key(*large_blob_key); + } return AsCTAPStyleCBORBytes(make_credential_response); } @@ -417,6 +424,9 @@ std::vector<uint8_t> EncodeGetAssertionResponse( response_map.emplace(0xf0, cbor::Value(*response.android_client_data_ext())); } + if (response.large_blob_key()) { + response_map.emplace(0x0b, cbor::Value(*response.large_blob_key())); + } return WriteCBOR(cbor::Value(std::move(response_map)), allow_invalid_utf8); } @@ -439,12 +449,16 @@ VirtualCtap2Device::Config& VirtualCtap2Device::Config::operator=( VirtualCtap2Device::Config::~Config() = default; VirtualCtap2Device::VirtualCtap2Device() { + RegenerateKeyAgreementKey(); Init({ProtocolVersion::kCtap2}); } VirtualCtap2Device::VirtualCtap2Device(scoped_refptr<State> state, const Config& config) : VirtualFidoDevice(std::move(state)), config_(config) { + RegenerateKeyAgreementKey(); + + Init({ProtocolVersion::kCtap2}); std::vector<ProtocolVersion> versions = {ProtocolVersion::kCtap2}; if (config.u2f_support) { versions.emplace_back(ProtocolVersion::kU2f); @@ -536,6 +550,12 @@ VirtualCtap2Device::VirtualCtap2Device(scoped_refptr<State> state, options.enterprise_attestation = true; } + if (config.large_blob_support) { + DCHECK(config.resident_key_support); + options_updated = true; + options.supports_large_blobs = true; + } + if (options_updated) { device_info_->options = std::move(options); } @@ -554,6 +574,10 @@ VirtualCtap2Device::VirtualCtap2Device(scoped_refptr<State> state, extensions.emplace_back(device::kExtensionAndroidClientData); } + if (config.large_blob_support) { + extensions.emplace_back(device::kExtensionLargeBlobKey); + } + if (!extensions.empty()) { device_info_->extensions.emplace(std::move(extensions)); } @@ -575,6 +599,16 @@ VirtualCtap2Device::VirtualCtap2Device(scoped_refptr<State> state, VirtualCtap2Device::~VirtualCtap2Device() = default; +void VirtualCtap2Device::SetPin(std::string pin) { + DCHECK_NE( + device_info_->options.client_pin_availability, + AuthenticatorSupportedOptions::ClientPinAvailability::kNotSupported); + mutable_state()->pin = std::move(pin); + mutable_state()->pin_retries = device::kMaxPinRetries; + device_info_->options.client_pin_availability = + AuthenticatorSupportedOptions::ClientPinAvailability::kSupportedAndPinSet; +} + // As all operations for VirtualCtap2Device are synchronous and we do not wait // for user touch, Cancel command is no-op. void VirtualCtap2Device::Cancel(CancelToken) {} @@ -647,6 +681,9 @@ FidoDevice::CancelToken VirtualCtap2Device::DeviceTransact( case CtapRequestCommand::kAuthenticatorBioEnrollmentPreview: response_code = OnBioEnrollment(request_bytes, &response_data); break; + case CtapRequestCommand::kAuthenticatorLargeBlobs: + response_code = OnLargeBlobs(request_bytes, &response_data); + break; default: break; } @@ -914,7 +951,7 @@ base::Optional<CtapDeviceResponseCode> VirtualCtap2Device::OnMakeCredential( // extensions but Chromium should not make this mistake. DLOG(ERROR) << "Rejecting makeCredential due to unexpected hmac_secret extension"; - return base::nullopt; + return CtapDeviceResponseCode::kCtap2ErrUnsupportedExtension; } extensions_map.emplace(cbor::Value(kExtensionHmacSecret), cbor::Value(true)); @@ -934,6 +971,19 @@ base::Optional<CtapDeviceResponseCode> VirtualCtap2Device::OnMakeCredential( cbor::Value(static_cast<int64_t>(cred_protect))); } + if (request.large_blob_key) { + if (!config_.large_blob_support) { + DLOG(ERROR) << "Rejecting makeCredential due to unexpected largeBlobKey " + "extension"; + return CtapDeviceResponseCode::kCtap2ErrUnsupportedExtension; + } + if (!request.resident_key_required) { + DLOG(ERROR) + << "largeBlobKey is not supported for non resident credentials"; + return CtapDeviceResponseCode::kCtap2ErrInvalidOption; + } + } + if (config_.add_extra_extension) { extensions_map.emplace(cbor::Value("unsolicited"), cbor::Value(42)); } @@ -1025,9 +1075,16 @@ base::Optional<CtapDeviceResponseCode> VirtualCtap2Device::OnMakeCredential( client_data_json.size())); } + base::Optional<std::array<uint8_t, kLargeBlobKeyLength>> large_blob_key; + if (request.large_blob_key) { + large_blob_key.emplace(); + RAND_bytes(large_blob_key->data(), large_blob_key->size()); + } + *response = ConstructMakeCredentialResponse( std::move(attestation_cert), sig, std::move(authenticator_data), - std::move(opt_android_client_data_ext), enterprise_attestation_requested); + std::move(opt_android_client_data_ext), enterprise_attestation_requested, + large_blob_key); RegistrationData registration(std::move(private_key), rp_id_hash, 1 /* signature counter */); @@ -1068,6 +1125,8 @@ base::Optional<CtapDeviceResponseCode> VirtualCtap2Device::OnMakeCredential( registration.hmac_key->second.size()); } + registration.large_blob_key = std::move(large_blob_key); + StoreNewKey(key_handle, std::move(registration)); return CtapDeviceResponseCode::kSuccess; } @@ -1350,6 +1409,15 @@ base::Optional<CtapDeviceResponseCode> VirtualCtap2Device::OnGetAssertion( assertion.SetUserEntity(registration.second->user.value()); } + if (request.large_blob_key) { + if (!config_.large_blob_support) { + return CtapDeviceResponseCode::kCtap2ErrUnsupportedExtension; + } + if (registration.second->large_blob_key) { + assertion.set_large_blob_key(*registration.second->large_blob_key); + } + } + if (opt_android_client_data_json) { std::vector<uint8_t> android_client_data_ext; fido_parsing_utils::Append( @@ -1464,19 +1532,16 @@ base::Optional<CtapDeviceResponseCode> VirtualCtap2Device::OnPINCommand( break; case static_cast<int>(device::pin::Subcommand::kGetKeyAgreement): { - bssl::UniquePtr<EC_KEY> key( - EC_KEY_new_by_curve_name(NID_X9_62_prime256v1)); - CHECK(EC_KEY_generate_key(key.get())); std::array<uint8_t, kP256X962Length> x962; CHECK_EQ(x962.size(), - EC_POINT_point2oct(EC_KEY_get0_group(key.get()), - EC_KEY_get0_public_key(key.get()), - POINT_CONVERSION_UNCOMPRESSED, x962.data(), - x962.size(), nullptr /* BN_CTX */)); + EC_POINT_point2oct( + EC_KEY_get0_group(mutable_state()->ecdh_key.get()), + EC_KEY_get0_public_key(mutable_state()->ecdh_key.get()), + POINT_CONVERSION_UNCOMPRESSED, x962.data(), x962.size(), + nullptr /* BN_CTX */)); response_map.emplace(static_cast<int>(pin::ResponseKey::kKeyAgreement), pin::EncodeCOSEPublicKey(x962)); - mutable_state()->ecdh_key = std::move(key); break; } @@ -1538,17 +1603,13 @@ base::Optional<CtapDeviceResponseCode> VirtualCtap2Device::OnPINCommand( } uint8_t shared_key[SHA256_DIGEST_LENGTH]; - if (!mutable_state()->ecdh_key) { - // kGetKeyAgreement should have been called first. - NOTREACHED(); - return CtapDeviceResponseCode::kCtap2ErrPinTokenExpired; - } pin::CalculateSharedKey(mutable_state()->ecdh_key.get(), peer_key->get(), shared_key); CtapDeviceResponseCode err = ConfirmPresentedPIN(mutable_state(), shared_key, *encrypted_pin_hash); if (err != CtapDeviceResponseCode::kSuccess) { + RegenerateKeyAgreementKey(); return err; } @@ -1616,6 +1677,7 @@ base::Optional<CtapDeviceResponseCode> VirtualCtap2Device::OnPINCommand( CtapDeviceResponseCode err = ConfirmPresentedPIN(mutable_state(), shared_key, *encrypted_pin_hash); if (err != CtapDeviceResponseCode::kSuccess) { + RegenerateKeyAgreementKey(); return err; } @@ -1670,7 +1732,7 @@ base::Optional<CtapDeviceResponseCode> VirtualCtap2Device::OnPINCommand( return base::nullopt; } if (!config_.user_verification_succeeds) { - return CtapDeviceResponseCode::kCtap2ErrPinInvalid; + return CtapDeviceResponseCode::kCtap2ErrUvInvalid; } mutable_state()->pin_retries = kMaxPinRetries; @@ -2114,6 +2176,161 @@ CtapDeviceResponseCode VirtualCtap2Device::OnBioEnrollment( return CtapDeviceResponseCode::kSuccess; } +CtapDeviceResponseCode VirtualCtap2Device::OnLargeBlobs( + base::span<const uint8_t> request_bytes, + std::vector<uint8_t>* response) { + if (!config_.large_blob_support) { + DLOG(ERROR) << "Large blob not supported"; + return CtapDeviceResponseCode::kCtap2ErrUnsupportedExtension; + } + + // Read request bytes into |cbor::Value::MapValue|. + const auto& cbor_request = cbor::Reader::Read(request_bytes); + if (!cbor_request || !cbor_request->is_map()) { + return CtapDeviceResponseCode::kCtap2ErrCBORUnexpectedType; + } + const auto& request_map = cbor_request->GetMap(); + + const auto offset_it = request_map.find( + cbor::Value(static_cast<uint8_t>(LargeBlobsRequestKey::kOffset))); + if (offset_it == request_map.end() || !offset_it->second.is_unsigned()) { + return CtapDeviceResponseCode::kCtap1ErrInvalidParameter; + } + const uint64_t offset = offset_it->second.GetUnsigned(); + + const auto get_it = request_map.find( + cbor::Value(static_cast<uint8_t>(LargeBlobsRequestKey::kGet))); + const auto set_it = request_map.find( + cbor::Value(static_cast<uint8_t>(LargeBlobsRequestKey::kSet))); + if ((get_it == request_map.end() && set_it == request_map.end()) || + (get_it != request_map.end() && set_it != request_map.end())) { + return CtapDeviceResponseCode::kCtap1ErrInvalidParameter; + } + if ((get_it != request_map.end() && !get_it->second.is_unsigned()) || + (set_it != request_map.end() && !set_it->second.is_bytestring())) { + return CtapDeviceResponseCode::kCtap1ErrInvalidParameter; + } + const auto length_it = request_map.find( + cbor::Value(static_cast<uint8_t>(LargeBlobsRequestKey::kLength))); + const size_t max_fragment_length = kLargeBlobDefaultMaxFragmentLength; + + if (get_it != request_map.end()) { + if (length_it != request_map.end()) { + return CtapDeviceResponseCode::kCtap1ErrInvalidParameter; + } + const uint64_t get = get_it->second.GetUnsigned(); + if (get > max_fragment_length) { + return CtapDeviceResponseCode::kCtap1ErrInvalidLength; + } + if (offset > mutable_state()->large_blob.size()) { + return CtapDeviceResponseCode::kCtap1ErrInvalidParameter; + } + cbor::Value::MapValue response_map; + response_map.emplace( + static_cast<uint8_t>(LargeBlobsResponseKey::kConfig), + base::make_span( + mutable_state()->large_blob.data() + offset, + std::min(get, mutable_state()->large_blob.size() - offset))); + *response = + cbor::Writer::Write(cbor::Value(std::move(response_map))).value(); + } else { + DCHECK(set_it != request_map.end()); + const std::vector<uint8_t>& set = set_it->second.GetBytestring(); + if (set.size() > max_fragment_length) { + return CtapDeviceResponseCode::kCtap1ErrInvalidLength; + } + if (offset == 0) { + if (length_it == request_map.end() || !length_it->second.is_unsigned()) { + return CtapDeviceResponseCode::kCtap1ErrInvalidParameter; + } + const uint64_t length = length_it->second.GetUnsigned(); + if (length > config_.available_large_blob_storage) { + return CtapDeviceResponseCode::kCtap2ErrLargeBlobStorageFull; + } + constexpr size_t kMinBlobLength = 17; + if (length < kMinBlobLength) { + return CtapDeviceResponseCode::kCtap1ErrInvalidParameter; + } + mutable_state()->large_blob_expected_length = length; + mutable_state()->large_blob_expected_next_offset = 0; + } else { + if (length_it != request_map.end()) { + return CtapDeviceResponseCode::kCtap1ErrInvalidParameter; + } + } + + if (offset != mutable_state()->large_blob_expected_next_offset) { + return CtapDeviceResponseCode::kCtap1ErrInvalidSeq; + } + + if (device_info_->options.client_pin_availability == + AuthenticatorSupportedOptions::ClientPinAvailability:: + kSupportedAndPinSet || + device_info_->options.user_verification_availability == + AuthenticatorSupportedOptions::UserVerificationAvailability:: + kSupportedAndConfigured) { + // If the device is protected by some sort of user verification: + const auto pin_uv_auth_param_it = request_map.find(cbor::Value( + static_cast<uint8_t>(LargeBlobsRequestKey::kPinUvAuthParam))); + const auto pin_uv_auth_protocol_it = request_map.find(cbor::Value( + static_cast<uint8_t>(LargeBlobsRequestKey::kPinUvAuthProtocol))); + if (pin_uv_auth_param_it == request_map.end() || + !pin_uv_auth_param_it->second.is_bytestring() || + pin_uv_auth_protocol_it == request_map.end() || + !pin_uv_auth_protocol_it->second.is_unsigned()) { + return CtapDeviceResponseCode::kCtap2ErrOperationDenied; + } + if (pin_uv_auth_protocol_it->second.GetUnsigned() != + pin::kProtocolVersion) { + return CtapDeviceResponseCode::kCtap2ErrPinAuthInvalid; + } + if (!(mutable_state()->pin_uv_token_permissions & + static_cast<uint8_t>(pin::Permissions::kLargeBlobWrite))) { + return CtapDeviceResponseCode::kCtap2ErrPinAuthInvalid; + } + + // verify(pinUvAuthToken, + // 32×0xff || h’0c00' || uint32LittleEndian(offset) || + // contents of set byte string, i.e. not including an outer CBOR + // tag with major type two, + // pinUvAuthParam) + std::vector<uint8_t> pinauth_bytes; + pinauth_bytes.insert(pinauth_bytes.begin(), + pin::kPinUvAuthTokenSafetyPadding.begin(), + pin::kPinUvAuthTokenSafetyPadding.end()); + pinauth_bytes.insert(pinauth_bytes.end(), kLargeBlobPinPrefix.begin(), + kLargeBlobPinPrefix.end()); + auto offset_vec = fido_parsing_utils::Uint32LittleEndian(offset); + pinauth_bytes.insert(pinauth_bytes.end(), offset_vec.begin(), + offset_vec.end()); + pinauth_bytes.insert(pinauth_bytes.end(), set.begin(), set.end()); + if (!CheckPINToken(mutable_state()->pin_token, + pin_uv_auth_param_it->second.GetBytestring(), + pinauth_bytes)) { + return CtapDeviceResponseCode::kCtap2ErrPinAuthInvalid; + } + } + if (offset + set.size() > mutable_state()->large_blob_expected_length) { + return CtapDeviceResponseCode::kCtap1ErrInvalidParameter; + } + if (offset == 0) { + mutable_state()->large_blob_buffer.clear(); + } + mutable_state()->large_blob_buffer.insert( + mutable_state()->large_blob_buffer.end(), set.begin(), set.end()); + mutable_state()->large_blob_expected_next_offset = + mutable_state()->large_blob_buffer.size(); + if (mutable_state()->large_blob_buffer.size() == + mutable_state()->large_blob_expected_length) { + if (!VerifyLargeBlobArrayIntegrity(mutable_state()->large_blob_buffer)) { + return CtapDeviceResponseCode::kCtap2ErrIntegrityFailure; + } + mutable_state()->large_blob = mutable_state()->large_blob_buffer; + } + } + return CtapDeviceResponseCode::kSuccess; +} + void VirtualCtap2Device::InitPendingRPs() { mutable_state()->pending_rps.clear(); std::set<std::string> rp_ids; @@ -2162,6 +2379,12 @@ void VirtualCtap2Device::InitPendingRegistrations( } } +void VirtualCtap2Device::RegenerateKeyAgreementKey() { + bssl::UniquePtr<EC_KEY> key(EC_KEY_new_by_curve_name(NID_X9_62_prime256v1)); + CHECK(EC_KEY_generate_key(key.get())); + mutable_state()->ecdh_key = std::move(key); +} + void VirtualCtap2Device::GetNextRP(cbor::Value::MapValue* response_map) { DCHECK(!mutable_state()->pending_rps.empty()); response_map->emplace( diff --git a/chromium/device/fido/virtual_ctap2_device.h b/chromium/device/fido/virtual_ctap2_device.h index 93535bddc36..6753add3f07 100644 --- a/chromium/device/fido/virtual_ctap2_device.h +++ b/chromium/device/fido/virtual_ctap2_device.h @@ -52,7 +52,8 @@ class COMPONENT_EXPORT(DEVICE_FIDO) VirtualCtap2Device Config& operator=(const Config&); ~Config(); - base::flat_set<Ctap2Version> ctap2_versions = {Ctap2Version::kCtap2_0}; + base::flat_set<Ctap2Version> ctap2_versions = { + std::begin(kCtap2Versions2_0), std::end(kCtap2Versions2_0)}; // u2f_support, if true, makes this device a dual-protocol (i.e. CTAP2 and // U2F) device. bool u2f_support = false; @@ -68,6 +69,12 @@ class COMPONENT_EXPORT(DEVICE_FIDO) VirtualCtap2Device uint8_t bio_enrollment_samples_required = 4; bool cred_protect_support = false; bool hmac_secret_support = false; + bool large_blob_support = false; + // The space available to store a large blob. In real authenticators this + // may change depending on the number of resident credentials. We treat this + // as a fixed size area for the large blob. + size_t available_large_blob_storage = 1024; + IncludeCredential include_credential_in_assertion_response = IncludeCredential::ONLY_IF_NEEDED; @@ -179,6 +186,9 @@ class COMPONENT_EXPORT(DEVICE_FIDO) VirtualCtap2Device VirtualCtap2Device(scoped_refptr<State> state, const Config& config); ~VirtualCtap2Device() override; + // Configures and sets a PIN on the authenticator. + void SetPin(std::string pin); + // FidoDevice: void Cancel(CancelToken) override; CancelToken DeviceTransact(std::vector<uint8_t> command, @@ -217,12 +227,15 @@ class COMPONENT_EXPORT(DEVICE_FIDO) VirtualCtap2Device std::vector<uint8_t>* response); CtapDeviceResponseCode OnBioEnrollment(base::span<const uint8_t> request, std::vector<uint8_t>* response); + CtapDeviceResponseCode OnLargeBlobs(base::span<const uint8_t> request, + std::vector<uint8_t>* response); CtapDeviceResponseCode OnAuthenticatorGetInfo( std::vector<uint8_t>* response) const; void InitPendingRPs(); void GetNextRP(cbor::Value::MapValue* response_map); void InitPendingRegistrations(base::span<const uint8_t> rp_id_hash); + void RegenerateKeyAgreementKey(); AttestedCredentialData ConstructAttestedCredentialData( base::span<const uint8_t> key_handle, diff --git a/chromium/device/fido/virtual_fido_device.cc b/chromium/device/fido/virtual_fido_device.cc index 418e1f5f27a..97ddd611e7c 100644 --- a/chromium/device/fido/virtual_fido_device.cc +++ b/chromium/device/fido/virtual_fido_device.cc @@ -93,7 +93,11 @@ class EVPBackedPrivateKey : public VirtualFidoDevice::PrivateKey { ret.resize(EVP_PKEY_size(pkey_.get())); size_t sig_len = ret.size(); - CHECK(EVP_DigestSignInit(md_ctx.get(), /*pctx=*/nullptr, EVP_sha256(), + // Ed25519 does not separate out the hash function as an independent + // variable so it must be nullptr in that case. + const EVP_MD* digest = + EVP_PKEY_id(pkey_.get()) == EVP_PKEY_ED25519 ? nullptr : EVP_sha256(); + CHECK(EVP_DigestSignInit(md_ctx.get(), /*pctx=*/nullptr, digest, /*engine=*/nullptr, pkey_.get()) && EVP_DigestSign(md_ctx.get(), ret.data(), &sig_len, msg.data(), msg.size()) && @@ -427,7 +431,6 @@ bool VirtualFidoDevice::State::InjectResidentKey( VirtualFidoDevice::VirtualFidoDevice() = default; - VirtualFidoDevice::VirtualFidoDevice(scoped_refptr<State> state) : state_(std::move(state)) {} diff --git a/chromium/device/fido/virtual_fido_device.h b/chromium/device/fido/virtual_fido_device.h index a9bb4b39523..d3e8e95d58f 100644 --- a/chromium/device/fido/virtual_fido_device.h +++ b/chromium/device/fido/virtual_fido_device.h @@ -114,6 +114,8 @@ class COMPONENT_EXPORT(DEVICE_FIDO) VirtualFidoDevice : public FidoDevice { base::Optional<std::pair<std::array<uint8_t, 32>, std::array<uint8_t, 32>>> hmac_key; + base::Optional<std::array<uint8_t, 32>> large_blob_key; + DISALLOW_COPY_AND_ASSIGN(RegistrationData); }; @@ -224,6 +226,17 @@ class COMPONENT_EXPORT(DEVICE_FIDO) VirtualFidoDevice : public FidoDevice { // expected sequence of requests was sent. std::vector<size_t> allow_list_sizes; + // The large-blob array. This is initialized to an empty CBOR array (0x80) + // followed by LEFT(SHA-256(h'80'), 16). + std::vector<uint8_t> large_blob = {0x80, 0x76, 0xbe, 0x8b, 0x52, 0x8d, + 0x00, 0x75, 0xf7, 0xaa, 0xe9, 0x8d, + 0x6f, 0xa5, 0x7a, 0x6d, 0x3c}; + // Buffer that gets progressively filled with large blob fragments until + // committed. + std::vector<uint8_t> large_blob_buffer; + uint64_t large_blob_expected_next_offset = 0; + uint64_t large_blob_expected_length = 0; + FidoTransportProtocol transport = FidoTransportProtocol::kUsbHumanInterfaceDevice; diff --git a/chromium/device/fido/virtual_fido_device_factory.cc b/chromium/device/fido/virtual_fido_device_factory.cc index aafc7858154..ffdf386c4b8 100644 --- a/chromium/device/fido/virtual_fido_device_factory.cc +++ b/chromium/device/fido/virtual_fido_device_factory.cc @@ -80,13 +80,13 @@ VirtualFidoDevice::State* VirtualFidoDeviceFactory::mutable_state() { return state_.get(); } -std::unique_ptr<FidoDiscoveryBase> VirtualFidoDeviceFactory::Create( - FidoTransportProtocol transport) { +std::vector<std::unique_ptr<FidoDiscoveryBase>> +VirtualFidoDeviceFactory::Create(FidoTransportProtocol transport) { if (transport != transport_) { - return nullptr; + return {}; } - return std::make_unique<VirtualFidoDeviceDiscovery>( - transport_, state_, supported_protocol_, ctap2_config_); + return SingleDiscovery(std::make_unique<VirtualFidoDeviceDiscovery>( + transport_, state_, supported_protocol_, ctap2_config_)); } bool VirtualFidoDeviceFactory::IsTestOverride() { diff --git a/chromium/device/fido/virtual_fido_device_factory.h b/chromium/device/fido/virtual_fido_device_factory.h index 92b9bf0a6e9..82958de2439 100644 --- a/chromium/device/fido/virtual_fido_device_factory.h +++ b/chromium/device/fido/virtual_fido_device_factory.h @@ -39,7 +39,7 @@ class VirtualFidoDeviceFactory : public device::FidoDiscoveryFactory { protected: // device::FidoDiscoveryFactory: - std::unique_ptr<FidoDiscoveryBase> Create( + std::vector<std::unique_ptr<FidoDiscoveryBase>> Create( FidoTransportProtocol transport) override; bool IsTestOverride() override; diff --git a/chromium/device/fido/win/logging.cc b/chromium/device/fido/win/logging.cc index 8490cd0e286..f38110f6cca 100644 --- a/chromium/device/fido/win/logging.cc +++ b/chromium/device/fido/win/logging.cc @@ -32,6 +32,10 @@ std::wstring Quoted(base::WStringPiece in) { return L"\"" + result + L"\""; } +std::wstring Quoted(const wchar_t* in) { + return Quoted(base::WStringPiece(in ? in : L"")); +} + } // namespace std::ostream& operator<<(std::ostream& out, diff --git a/chromium/device/fido/win/webauthn_api.cc b/chromium/device/fido/win/webauthn_api.cc index c5ad6e58c06..b1483525ddd 100644 --- a/chromium/device/fido/win/webauthn_api.cc +++ b/chromium/device/fido/win/webauthn_api.cc @@ -14,6 +14,7 @@ #include "base/strings/string16.h" #include "base/strings/string_piece_forward.h" #include "base/strings/utf_string_conversions.h" +#include "base/threading/scoped_thread_priority.h" #include "components/device_event_log/device_event_log.h" #include "device/fido/win/logging.h" #include "device/fido/win/type_conversions.h" @@ -34,8 +35,13 @@ constexpr uint32_t kWinWebAuthnTimeoutMilliseconds = 1000 * 60 * 5; class WinWebAuthnApiImpl : public WinWebAuthnApi { public: WinWebAuthnApiImpl() : WinWebAuthnApi(), is_bound_(false) { - webauthn_dll_ = - LoadLibraryExA("webauthn.dll", NULL, LOAD_LIBRARY_SEARCH_SYSTEM32); + { + // Mitigate the issues caused by loading DLLs on a background thread + // (http://crbug/973868). + SCOPED_MAY_LOAD_LIBRARY_AT_BACKGROUND_PRIORITY(); + webauthn_dll_ = + LoadLibraryExA("webauthn.dll", NULL, LOAD_LIBRARY_SEARCH_SYSTEM32); + } if (!webauthn_dll_) { return; } @@ -89,6 +95,9 @@ class WinWebAuthnApiImpl : public WinWebAuthnApi { HRESULT IsUserVerifyingPlatformAuthenticatorAvailable( BOOL* available) override { DCHECK(is_bound_); + // Mitigate the issues caused by loading DLLs on a background thread + // (http://crbug/973868). + SCOPED_MAY_LOAD_LIBRARY_AT_BACKGROUND_PRIORITY(); return is_user_verifying_platform_authenticator_available_(available); } diff --git a/chromium/device/gamepad/BUILD.gn b/chromium/device/gamepad/BUILD.gn index b0af36dfb64..352256fd712 100644 --- a/chromium/device/gamepad/BUILD.gn +++ b/chromium/device/gamepad/BUILD.gn @@ -9,6 +9,13 @@ if (is_android) { import("//build/config/android/rules.gni") # For generate_jni(). } +# This file depends on the legacy global sources assignment filter. It should +# be converted to check target platform before assigning source files to the +# sources variable. Remove this import and set_sources_assignment_filter call +# when the file has been converted. See https://crbug.com/1018739 for details. +import("//build/config/deprecated_default_sources_assignment_filter.gni") +set_sources_assignment_filter(deprecated_default_sources_assignment_filter) + component("gamepad") { output_name = "device_gamepad" diff --git a/chromium/device/gamepad/android/java/src/org/chromium/device/gamepad/GamepadDevice.java b/chromium/device/gamepad/android/java/src/org/chromium/device/gamepad/GamepadDevice.java deleted file mode 100644 index 267bd9601de..00000000000 --- a/chromium/device/gamepad/android/java/src/org/chromium/device/gamepad/GamepadDevice.java +++ /dev/null @@ -1,212 +0,0 @@ -// Copyright 2014 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package org.chromium.device.gamepad; - -import android.os.SystemClock; -import android.view.InputDevice; -import android.view.InputDevice.MotionRange; -import android.view.KeyEvent; -import android.view.MotionEvent; - -import androidx.annotation.VisibleForTesting; - -import java.util.Arrays; -import java.util.BitSet; -import java.util.List; - -/** - * Manages information related to each connected gamepad device. - */ -class GamepadDevice { - // Axis ids are used as indices which are empirically always smaller than 256 so this allows - // us to create cheap associative arrays. - @VisibleForTesting - static final int MAX_RAW_AXIS_VALUES = 256; - - // Keycodes are used as indices which are empirically always smaller than 256 so this allows - // us to create cheap associative arrays. - @VisibleForTesting - static final int MAX_RAW_BUTTON_VALUES = 256; - - /** Keycodes which might be mapped by {@link GamepadMappings}. */ - private static final int RELEVANT_KEYCODES[] = {KeyEvent.KEYCODE_BUTTON_A, - KeyEvent.KEYCODE_BUTTON_B, KeyEvent.KEYCODE_BUTTON_C, KeyEvent.KEYCODE_BUTTON_X, - KeyEvent.KEYCODE_BUTTON_Y, KeyEvent.KEYCODE_BUTTON_Z, KeyEvent.KEYCODE_BUTTON_L1, - KeyEvent.KEYCODE_BUTTON_R1, KeyEvent.KEYCODE_BUTTON_L2, KeyEvent.KEYCODE_BUTTON_R2, - KeyEvent.KEYCODE_BUTTON_SELECT, KeyEvent.KEYCODE_BUTTON_START, - KeyEvent.KEYCODE_BUTTON_THUMBL, KeyEvent.KEYCODE_BUTTON_THUMBR, - KeyEvent.KEYCODE_DPAD_UP, KeyEvent.KEYCODE_DPAD_DOWN, KeyEvent.KEYCODE_DPAD_LEFT, - KeyEvent.KEYCODE_DPAD_RIGHT, KeyEvent.KEYCODE_BUTTON_MODE}; - - // An id for the gamepad. - private int mDeviceId; - // The index of the gamepad in the Navigator. - private int mDeviceIndex; - // Last time the data for this gamepad was updated. - private long mTimestamp; - - // Array of values for all axes of the gamepad. - // All axis values must be linearly normalized to the range [-1.0 .. 1.0]. - // As appropriate, -1.0 should correspond to "up" or "left", and 1.0 - // should correspond to "down" or "right". - private final float[] mAxisValues = new float[CanonicalAxisIndex.COUNT]; - - private final float[] mButtonsValues = new float[CanonicalButtonIndex.COUNT]; - - // When the user agent recognizes the attached inputDevice, it is recommended - // that it be remapped to a canonical ordering when possible. Devices that are - // not recognized should still be exposed in their raw form. Therefore we must - // pass the raw Button and raw Axis values. - private final float[] mRawButtons = new float[MAX_RAW_BUTTON_VALUES]; - private final float[] mRawAxes = new float[MAX_RAW_AXIS_VALUES]; - - // An identification string for the gamepad. - private String mDeviceName; - - // Array of axes ids. - private int[] mAxes; - - // Mappings to canonical gamepad - private GamepadMappings mMappings; - - GamepadDevice(int index, InputDevice inputDevice) { - mDeviceIndex = index; - mDeviceId = inputDevice.getId(); - mDeviceName = inputDevice.getName(); - mTimestamp = SystemClock.uptimeMillis(); - // Get axis ids and initialize axes values. - final List<MotionRange> ranges = inputDevice.getMotionRanges(); - mAxes = new int[ranges.size()]; - int i = 0; - for (MotionRange range : ranges) { - if ((range.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) { - int axis = range.getAxis(); - assert axis < MAX_RAW_AXIS_VALUES; - mAxes[i++] = axis; - } - } - - // Get the set of relevant buttons which exist on the gamepad. - BitSet buttons = new BitSet(KeyEvent.KEYCODE_BUTTON_MODE); - boolean[] presentKeys = inputDevice.hasKeys(RELEVANT_KEYCODES); - for (int j = 0; j < RELEVANT_KEYCODES.length; ++j) { - if (presentKeys[j]) { - buttons.set(RELEVANT_KEYCODES[j]); - } - } - - mMappings = GamepadMappings.getMappings(inputDevice, mAxes, buttons); - } - - /** - * Updates the axes and buttons maping of a gamepad device to a standard gamepad format. - */ - public void updateButtonsAndAxesMapping() { - mMappings.mapToStandardGamepad(mAxisValues, mButtonsValues, mRawAxes, mRawButtons); - } - - /** - * @return Device Id of the gamepad device. - */ - public int getId() { - return mDeviceId; - } - - /** - * @return Mapping status of the gamepad device. - */ - public boolean isStandardGamepad() { - return mMappings.isStandard(); - } - - /** - * @return Device name of the gamepad device. - */ - public String getName() { - return mDeviceName; - } - - /** - * @return Device index of the gamepad device. - */ - public int getIndex() { - return mDeviceIndex; - } - - /** - * @return The timestamp when the gamepad device was last interacted. - */ - public long getTimestamp() { - return mTimestamp; - } - - /** - * @return The axes state of the gamepad device. - */ - public float[] getAxes() { - return mAxisValues; - } - - /** - * @return The buttons state of the gamepad device. - */ - public float[] getButtons() { - return mButtonsValues; - } - - /** - * @return The number of mapped buttons. - */ - public int getButtonsLength() { - return mMappings.getButtonsLength(); - } - - /** - * Reset the axes and buttons data of the gamepad device every time gamepad data access is - * paused. - */ - public void clearData() { - Arrays.fill(mAxisValues, 0); - Arrays.fill(mRawAxes, 0); - Arrays.fill(mButtonsValues, 0); - Arrays.fill(mRawButtons, 0); - } - - /** - * Handles key event from the gamepad device. - * @return True if the key event from the gamepad device has been consumed. - */ - public boolean handleKeyEvent(KeyEvent event) { - // Ignore event if it is not for standard gamepad key. - if (!GamepadList.isGamepadEvent(event)) return false; - int keyCode = event.getKeyCode(); - assert keyCode < MAX_RAW_BUTTON_VALUES; - // Button value 0.0 must mean fully unpressed, and 1.0 must mean fully pressed. - if (event.getAction() == KeyEvent.ACTION_DOWN) { - mRawButtons[keyCode] = 1.0f; - } else if (event.getAction() == KeyEvent.ACTION_UP) { - mRawButtons[keyCode] = 0.0f; - } - mTimestamp = event.getEventTime(); - - return true; - } - - /** - * Handles motion event from the gamepad device. - * @return True if the motion event from the gamepad device has been consumed. - */ - public boolean handleMotionEvent(MotionEvent event) { - // Ignore event if it is not a standard gamepad motion event. - if (!GamepadList.isGamepadEvent(event)) return false; - // Update axes values. - for (int i = 0; i < mAxes.length; i++) { - int axis = mAxes[i]; - mRawAxes[axis] = event.getAxisValue(axis); - } - mTimestamp = event.getEventTime(); - return true; - } -} diff --git a/chromium/device/gamepad/android/java/src/org/chromium/device/gamepad/GamepadList.java b/chromium/device/gamepad/android/java/src/org/chromium/device/gamepad/GamepadList.java deleted file mode 100644 index 140c50adf2f..00000000000 --- a/chromium/device/gamepad/android/java/src/org/chromium/device/gamepad/GamepadList.java +++ /dev/null @@ -1,332 +0,0 @@ -// Copyright 2014 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package org.chromium.device.gamepad; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.hardware.input.InputManager; -import android.hardware.input.InputManager.InputDeviceListener; -import android.view.InputDevice; -import android.view.InputEvent; -import android.view.KeyEvent; -import android.view.MotionEvent; - -import org.chromium.base.ThreadUtils; -import org.chromium.base.annotations.CalledByNative; -import org.chromium.base.annotations.JNINamespace; -import org.chromium.base.annotations.NativeMethods; - -/** - * Class to manage connected gamepad devices list. - * - * It is a Java counterpart of GamepadPlatformDataFetcherAndroid and feeds Gamepad API with input - * data. - */ -@JNINamespace("device") -public class GamepadList { - private static final int MAX_GAMEPADS = 4; - - private final Object mLock = new Object(); - - private final GamepadDevice[] mGamepadDevices = new GamepadDevice[MAX_GAMEPADS]; - private InputManager mInputManager; - private int mAttachedToWindowCounter; - private boolean mIsGamepadAPIActive; - private InputDeviceListener mInputDeviceListener; - - private GamepadList() { - mInputDeviceListener = new InputDeviceListener() { - // Override InputDeviceListener methods - @Override - public void onInputDeviceChanged(int deviceId) { - onInputDeviceChangedImpl(deviceId); - } - - @Override - public void onInputDeviceRemoved(int deviceId) { - onInputDeviceRemovedImpl(deviceId); - } - - @Override - public void onInputDeviceAdded(int deviceId) { - onInputDeviceAddedImpl(deviceId); - } - }; - } - - private void initializeDevices() { - // Get list of all the attached input devices. - int[] deviceIds = mInputManager.getInputDeviceIds(); - for (int i = 0; i < deviceIds.length; i++) { - InputDevice inputDevice = InputDevice.getDevice(deviceIds[i]); - // Check for gamepad device - if (isGamepadDevice(inputDevice)) { - // Register a new gamepad device. - registerGamepad(inputDevice); - } - } - } - - /** - * Notifies the GamepadList that a {@link ContentView} is attached to a window and it should - * prepare itself for gamepad input. It must be called before {@link onGenericMotionEvent} and - * {@link dispatchKeyEvent}. - */ - public static void onAttachedToWindow(Context context) { - assert ThreadUtils.runningOnUiThread(); - getInstance().attachedToWindow(context); - } - - private void attachedToWindow(Context context) { - if (mAttachedToWindowCounter++ == 0) { - mInputManager = (InputManager) context.getSystemService(Context.INPUT_SERVICE); - synchronized (mLock) { - initializeDevices(); - } - // Register an input device listener. - mInputManager.registerInputDeviceListener(mInputDeviceListener, null); - } - } - - /** - * Notifies the GamepadList that a {@link ContentView} is detached from it's window. - */ - @SuppressLint("MissingSuperCall") - public static void onDetachedFromWindow() { - assert ThreadUtils.runningOnUiThread(); - getInstance().detachedFromWindow(); - } - - private void detachedFromWindow() { - if (--mAttachedToWindowCounter == 0) { - synchronized (mLock) { - for (int i = 0; i < MAX_GAMEPADS; ++i) { - mGamepadDevices[i] = null; - } - } - mInputManager.unregisterInputDeviceListener(mInputDeviceListener); - mInputManager = null; - } - } - - // ------------------------------------------------------------ - - private void onInputDeviceChangedImpl(int deviceId) {} - - private void onInputDeviceRemovedImpl(int deviceId) { - synchronized (mLock) { - unregisterGamepad(deviceId); - } - } - - private void onInputDeviceAddedImpl(int deviceId) { - InputDevice inputDevice = InputDevice.getDevice(deviceId); - if (!isGamepadDevice(inputDevice)) return; - synchronized (mLock) { - registerGamepad(inputDevice); - } - } - - // ------------------------------------------------------------ - - private static GamepadList getInstance() { - return LazyHolder.INSTANCE; - } - - private int getDeviceCount() { - int count = 0; - for (int i = 0; i < MAX_GAMEPADS; i++) { - if (getDevice(i) != null) { - count++; - } - } - return count; - } - - private boolean isDeviceConnected(int index) { - if (index < MAX_GAMEPADS && getDevice(index) != null) { - return true; - } - return false; - } - - private GamepadDevice getDeviceById(int deviceId) { - for (int i = 0; i < MAX_GAMEPADS; i++) { - GamepadDevice gamepad = mGamepadDevices[i]; - if (gamepad != null && gamepad.getId() == deviceId) { - return gamepad; - } - } - return null; - } - - private GamepadDevice getDevice(int index) { - // Maximum 4 Gamepads can be connected at a time starting at index zero. - assert index >= 0 && index < MAX_GAMEPADS; - return mGamepadDevices[index]; - } - - /** - * Handles key events from the gamepad devices. - * @return True if the event has been consumed. - */ - public static boolean dispatchKeyEvent(KeyEvent event) { - if (!isGamepadEvent(event)) return false; - return getInstance().handleKeyEvent(event); - } - - private boolean handleKeyEvent(KeyEvent event) { - synchronized (mLock) { - if (!mIsGamepadAPIActive) return false; - GamepadDevice gamepad = getGamepadForEvent(event); - if (gamepad == null) return false; - return gamepad.handleKeyEvent(event); - } - } - - /** - * Handles motion events from the gamepad devices. - * @return True if the event has been consumed. - */ - public static boolean onGenericMotionEvent(MotionEvent event) { - if (!isGamepadEvent(event)) return false; - return getInstance().handleMotionEvent(event); - } - - private boolean handleMotionEvent(MotionEvent event) { - synchronized (mLock) { - if (!mIsGamepadAPIActive) return false; - GamepadDevice gamepad = getGamepadForEvent(event); - if (gamepad == null) return false; - return gamepad.handleMotionEvent(event); - } - } - - private int getNextAvailableIndex() { - // When multiple gamepads are connected to a user agent, indices must be assigned on a - // first-come first-serve basis, starting at zero. If a gamepad is disconnected, previously - // assigned indices must not be reassigned to gamepads that continue to be connected. - // However, if a gamepad is disconnected, and subsequently the same or a different - // gamepad is then connected, index entries must be reused. - - for (int i = 0; i < MAX_GAMEPADS; ++i) { - if (getDevice(i) == null) { - return i; - } - } - // Reached maximum gamepads limit. - return -1; - } - - private boolean registerGamepad(InputDevice inputDevice) { - int index = getNextAvailableIndex(); - if (index == -1) return false; // invalid index - - GamepadDevice gamepad = new GamepadDevice(index, inputDevice); - mGamepadDevices[index] = gamepad; - return true; - } - - private void unregisterGamepad(int deviceId) { - GamepadDevice gamepadDevice = getDeviceById(deviceId); - if (gamepadDevice == null) return; // Not a registered device. - int index = gamepadDevice.getIndex(); - mGamepadDevices[index] = null; - } - - private static boolean isGamepadDevice(InputDevice inputDevice) { - if (inputDevice == null) return false; - return ((inputDevice.getSources() & InputDevice.SOURCE_JOYSTICK) - == InputDevice.SOURCE_JOYSTICK); - } - - private GamepadDevice getGamepadForEvent(InputEvent event) { - return getDeviceById(event.getDeviceId()); - } - - /** - * @return True if HTML5 gamepad API is active. - */ - public static boolean isGamepadAPIActive() { - return getInstance().mIsGamepadAPIActive; - } - - /** - * @return True if the motion event corresponds to a gamepad event. - */ - public static boolean isGamepadEvent(MotionEvent event) { - return ((event.getSource() & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK); - } - - /** - * @return True if event's keycode corresponds to a gamepad key. - */ - public static boolean isGamepadEvent(KeyEvent event) { - int keyCode = event.getKeyCode(); - switch (keyCode) { - // Specific handling for dpad keys is required because - // KeyEvent.isGamepadButton doesn't consider dpad keys. - case KeyEvent.KEYCODE_DPAD_UP: - case KeyEvent.KEYCODE_DPAD_DOWN: - case KeyEvent.KEYCODE_DPAD_LEFT: - case KeyEvent.KEYCODE_DPAD_RIGHT: - return true; - default: - return KeyEvent.isGamepadButton(keyCode); - } - } - - @CalledByNative - static void updateGamepadData(long webGamepadsPtr) { - getInstance().grabGamepadData(webGamepadsPtr); - } - - private void grabGamepadData(long webGamepadsPtr) { - synchronized (mLock) { - for (int i = 0; i < MAX_GAMEPADS; i++) { - final GamepadDevice device = getDevice(i); - if (device != null) { - device.updateButtonsAndAxesMapping(); - GamepadListJni.get().setGamepadData(GamepadList.this, webGamepadsPtr, i, - device.isStandardGamepad(), true, device.getName(), - device.getTimestamp(), device.getAxes(), device.getButtons(), - device.getButtonsLength()); - } else { - GamepadListJni.get().setGamepadData(GamepadList.this, webGamepadsPtr, i, false, - false, null, 0, null, null, 0); - } - } - } - } - - @CalledByNative - static void setGamepadAPIActive(boolean isActive) { - getInstance().setIsGamepadActive(isActive); - } - - private void setIsGamepadActive(boolean isGamepadActive) { - synchronized (mLock) { - mIsGamepadAPIActive = isGamepadActive; - if (isGamepadActive) { - for (int i = 0; i < MAX_GAMEPADS; i++) { - GamepadDevice gamepadDevice = getDevice(i); - if (gamepadDevice == null) continue; - gamepadDevice.clearData(); - } - } - } - } - - private static class LazyHolder { - private static final GamepadList INSTANCE = new GamepadList(); - } - - @NativeMethods - interface Natives { - void setGamepadData(GamepadList caller, long webGamepadsPtr, int index, boolean mapping, - boolean connected, String devicename, long timestamp, float[] axes, float[] buttons, - int buttonsLength); - } -} diff --git a/chromium/device/gamepad/android/java/src/org/chromium/device/gamepad/GamepadMappings.java b/chromium/device/gamepad/android/java/src/org/chromium/device/gamepad/GamepadMappings.java deleted file mode 100644 index 291484598e8..00000000000 --- a/chromium/device/gamepad/android/java/src/org/chromium/device/gamepad/GamepadMappings.java +++ /dev/null @@ -1,642 +0,0 @@ -// Copyright 2015 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package org.chromium.device.gamepad; - -import android.os.Build; -import android.view.InputDevice; -import android.view.KeyEvent; -import android.view.MotionEvent; - -import androidx.annotation.VisibleForTesting; - -import org.chromium.base.annotations.JNINamespace; - -import java.util.BitSet; - -/** - * Class to manage mapping information related to each supported gamepad controller device. - */ -@JNINamespace("content") -abstract class GamepadMappings { - @VisibleForTesting - static final String NVIDIA_SHIELD_DEVICE_NAME_PREFIX = "NVIDIA Corporation NVIDIA Controller"; - @VisibleForTesting - static final String MICROSOFT_XBOX_PAD_DEVICE_NAME = "Microsoft X-Box 360 pad"; - @VisibleForTesting - static final String PS_DUALSHOCK_3_SIXAXIS_DEVICE_NAME = "Sony PLAYSTATION(R)3 Controller"; - @VisibleForTesting - static final String SAMSUNG_EI_GP20_DEVICE_NAME = "Samsung Game Pad EI-GP20"; - @VisibleForTesting - static final String AMAZON_FIRE_DEVICE_NAME = "Amazon Fire Game Controller"; - - @VisibleForTesting - static final int PS_DUALSHOCK_4_VENDOR_ID = 1356; - @VisibleForTesting - static final int PS_DUALSHOCK_4_PRODUCT_ID = 1476; - @VisibleForTesting - static final int PS_DUALSHOCK_4_SLIM_PRODUCT_ID = 2508; - @VisibleForTesting - static final int PS_DUALSHOCK_4_USB_RECEIVER_PRODUCT_ID = 2976; - - @VisibleForTesting - static final int XBOX_ONE_S_2016_FIRMWARE_VENDOR_ID = 0x045e; - @VisibleForTesting - static final int XBOX_ONE_S_2016_FIRMWARE_PRODUCT_ID = 0x02e0; - - @VisibleForTesting - static final int BROADCOM_VENDOR_ID = 0x0a5c; - @VisibleForTesting - static final int SNAKEBYTE_IDROIDCON_PRODUCT_ID = 0x8502; - - private static final float BUTTON_AXIS_DEADZONE = 0.01f; - - public static GamepadMappings getMappings(InputDevice device, int[] axes, BitSet buttons) { - GamepadMappings mappings = getMappings(device.getProductId(), device.getVendorId(), axes); - if (mappings == null) { - mappings = getMappings(device.getName()); - } - if (mappings == null) { - mappings = new UnknownGamepadMappings(axes, buttons); - } - return mappings; - } - - @VisibleForTesting - static GamepadMappings getMappings(int productId, int vendorId, int[] axes) { - // Device name of a DualShock 4 gamepad is "Wireless Controller". This is not reliably - // unique so we better go by the product and vendor ids. - if (vendorId == PS_DUALSHOCK_4_VENDOR_ID - && (productId == PS_DUALSHOCK_4_PRODUCT_ID - || productId == PS_DUALSHOCK_4_SLIM_PRODUCT_ID - || productId == PS_DUALSHOCK_4_USB_RECEIVER_PRODUCT_ID)) { - // Android 9 included improvements for Sony PlayStation gamepads that changed the - // KeyEvent and MotionEvent codes for some buttons and axes. Use an alternate mapping - // for versions of Android that include these improvements. - if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - return new XboxCompatibleGamepadMappings(); - } - return new Dualshock4GamepadMappingsPreP(); - } - // Microsoft released a firmware update for the Xbox One S gamepad that modified the button - // and axis assignments. With the new firmware, these gamepads work correctly in Android - // using the default mapping, but a custom mapping is still required for the old firmware. - // Both gamepads return the same device name, so we must compare hardware IDs to distinguish - // them. - if (vendorId == XBOX_ONE_S_2016_FIRMWARE_VENDOR_ID - && productId == XBOX_ONE_S_2016_FIRMWARE_PRODUCT_ID) { - return new XboxOneS2016FirmwareMappings(); - } - if (vendorId == BROADCOM_VENDOR_ID && productId == SNAKEBYTE_IDROIDCON_PRODUCT_ID) { - return new SnakebyteIDroidConMappings(axes); - } - return null; - } - - @VisibleForTesting - static GamepadMappings getMappings(String deviceName) { - if (deviceName.startsWith(NVIDIA_SHIELD_DEVICE_NAME_PREFIX) - || deviceName.equals(MICROSOFT_XBOX_PAD_DEVICE_NAME)) { - return new XboxCompatibleGamepadMappings(); - } else if (deviceName.equals(PS_DUALSHOCK_3_SIXAXIS_DEVICE_NAME)) { - // Android 9 included improvements for Sony PlayStation gamepads that changed the - // KeyEvent and MotionEvent codes for some buttons and axes. Use an alternate mapping - // for versions of Android that include these improvements. - if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - return new Dualshock3SixAxisGamepadMappings(); - } - return new Dualshock3SixAxisGamepadMappingsPreP(); - } else if (deviceName.equals(SAMSUNG_EI_GP20_DEVICE_NAME)) { - return new SamsungEIGP20GamepadMappings(); - } else if (deviceName.equals(AMAZON_FIRE_DEVICE_NAME)) { - return new AmazonFireGamepadMappings(); - } - return null; - } - - @VisibleForTesting - static GamepadMappings getUnknownGamepadMappings(int[] axes, BitSet buttons) { - return new UnknownGamepadMappings(axes, buttons); - } - - /** - * Method that specifies whether the mappings are standard or not. - * It should be overridden in subclasses that don't provide standard - * mappings. - */ - public boolean isStandard() { - return true; - } - - /** - * Returns the number of mapped buttons. Subclasses which support more or fewer buttons (e.g. no - * meta button) should override this. - */ - public int getButtonsLength() { - return CanonicalButtonIndex.COUNT; - } - - /** - * Method implemented by subclasses to perform mapping from raw axes and buttons - * to canonical axes and buttons. - */ - public abstract void mapToStandardGamepad(float[] mappedAxes, float[] mappedButtons, - float[] rawAxes, float[] rawButtons); - - private static void mapCommonXYABButtons(float[] mappedButtons, float[] rawButtons) { - float a = rawButtons[KeyEvent.KEYCODE_BUTTON_A]; - float b = rawButtons[KeyEvent.KEYCODE_BUTTON_B]; - float x = rawButtons[KeyEvent.KEYCODE_BUTTON_X]; - float y = rawButtons[KeyEvent.KEYCODE_BUTTON_Y]; - mappedButtons[CanonicalButtonIndex.PRIMARY] = a; - mappedButtons[CanonicalButtonIndex.SECONDARY] = b; - mappedButtons[CanonicalButtonIndex.TERTIARY] = x; - mappedButtons[CanonicalButtonIndex.QUATERNARY] = y; - } - - private static void mapCommonStartSelectMetaButtons(float[] mappedButtons, float[] rawButtons) { - float start = rawButtons[KeyEvent.KEYCODE_BUTTON_START]; - float select = rawButtons[KeyEvent.KEYCODE_BUTTON_SELECT]; - float mode = rawButtons[KeyEvent.KEYCODE_BUTTON_MODE]; - mappedButtons[CanonicalButtonIndex.START] = start; - mappedButtons[CanonicalButtonIndex.BACK_SELECT] = select; - mappedButtons[CanonicalButtonIndex.META] = mode; - } - - private static void mapCommonThumbstickButtons(float[] mappedButtons, float[] rawButtons) { - float thumbL = rawButtons[KeyEvent.KEYCODE_BUTTON_THUMBL]; - float thumbR = rawButtons[KeyEvent.KEYCODE_BUTTON_THUMBR]; - mappedButtons[CanonicalButtonIndex.LEFT_THUMBSTICK] = thumbL; - mappedButtons[CanonicalButtonIndex.RIGHT_THUMBSTICK] = thumbR; - } - - /** - * Method for mapping the L1/R1 buttons to lower shoulder buttons, rather than - * upper shoulder as the user would normally expect. Please think twice before - * using this, as it can easily confuse the user. It is only really useful if - * the controller completely lacks a second set of shoulder buttons. - */ - private static void mapUpperTriggerButtonsToBottomShoulder(float[] mappedButtons, - float[] rawButtons) { - float l1 = rawButtons[KeyEvent.KEYCODE_BUTTON_L1]; - float r1 = rawButtons[KeyEvent.KEYCODE_BUTTON_R1]; - mappedButtons[CanonicalButtonIndex.LEFT_TRIGGER] = l1; - mappedButtons[CanonicalButtonIndex.RIGHT_TRIGGER] = r1; - } - - private static void mapTriggerButtonsToTopShoulder(float[] mappedButtons, float[] rawButtons) { - float l1 = rawButtons[KeyEvent.KEYCODE_BUTTON_L1]; - float r1 = rawButtons[KeyEvent.KEYCODE_BUTTON_R1]; - mappedButtons[CanonicalButtonIndex.LEFT_SHOULDER] = l1; - mappedButtons[CanonicalButtonIndex.RIGHT_SHOULDER] = r1; - } - - private static void mapCommonDpadButtons(float[] mappedButtons, float[] rawButtons) { - float dpadDown = rawButtons[KeyEvent.KEYCODE_DPAD_DOWN]; - float dpadUp = rawButtons[KeyEvent.KEYCODE_DPAD_UP]; - float dpadLeft = rawButtons[KeyEvent.KEYCODE_DPAD_LEFT]; - float dpadRight = rawButtons[KeyEvent.KEYCODE_DPAD_RIGHT]; - mappedButtons[CanonicalButtonIndex.DPAD_DOWN] = dpadDown; - mappedButtons[CanonicalButtonIndex.DPAD_UP] = dpadUp; - mappedButtons[CanonicalButtonIndex.DPAD_LEFT] = dpadLeft; - mappedButtons[CanonicalButtonIndex.DPAD_RIGHT] = dpadRight; - } - - private static void mapXYAxes(float[] mappedAxes, float[] rawAxes) { - mappedAxes[CanonicalAxisIndex.LEFT_STICK_X] = rawAxes[MotionEvent.AXIS_X]; - mappedAxes[CanonicalAxisIndex.LEFT_STICK_Y] = rawAxes[MotionEvent.AXIS_Y]; - } - - private static void mapRXAndRYAxesToRightStick(float[] mappedAxes, float[] rawAxes) { - mappedAxes[CanonicalAxisIndex.RIGHT_STICK_X] = rawAxes[MotionEvent.AXIS_RX]; - mappedAxes[CanonicalAxisIndex.RIGHT_STICK_Y] = rawAxes[MotionEvent.AXIS_RY]; - } - - private static void mapZAndRZAxesToRightStick(float[] mappedAxes, float[] rawAxes) { - mappedAxes[CanonicalAxisIndex.RIGHT_STICK_X] = rawAxes[MotionEvent.AXIS_Z]; - mappedAxes[CanonicalAxisIndex.RIGHT_STICK_Y] = rawAxes[MotionEvent.AXIS_RZ]; - } - - private static void mapPedalAxesToBottomShoulder(float[] mappedButtons, float[] rawAxes) { - float lTrigger = rawAxes[MotionEvent.AXIS_BRAKE]; - float rTrigger = rawAxes[MotionEvent.AXIS_GAS]; - mappedButtons[CanonicalButtonIndex.LEFT_TRIGGER] = lTrigger; - mappedButtons[CanonicalButtonIndex.RIGHT_TRIGGER] = rTrigger; - } - - private static void mapTriggerAxesToBottomShoulder(float[] mappedButtons, float[] rawAxes) { - float lTrigger = rawAxes[MotionEvent.AXIS_LTRIGGER]; - float rTrigger = rawAxes[MotionEvent.AXIS_RTRIGGER]; - mappedButtons[CanonicalButtonIndex.LEFT_TRIGGER] = lTrigger; - mappedButtons[CanonicalButtonIndex.RIGHT_TRIGGER] = rTrigger; - } - - private static void mapZAxisToBottomShoulder(float[] mappedButtons, float[] rawAxes) { - float z = rawAxes[MotionEvent.AXIS_Z]; - mappedButtons[CanonicalButtonIndex.LEFT_TRIGGER] = z > BUTTON_AXIS_DEADZONE ? z : 0.0f; - mappedButtons[CanonicalButtonIndex.RIGHT_TRIGGER] = -z > BUTTON_AXIS_DEADZONE ? -z : 0.0f; - } - - private static void mapLowerTriggerButtonsToBottomShoulder(float[] mappedButtons, - float[] rawButtons) { - float l2 = rawButtons[KeyEvent.KEYCODE_BUTTON_L2]; - float r2 = rawButtons[KeyEvent.KEYCODE_BUTTON_R2]; - mappedButtons[CanonicalButtonIndex.LEFT_TRIGGER] = l2; - mappedButtons[CanonicalButtonIndex.RIGHT_TRIGGER] = r2; - } - - @VisibleForTesting - static float negativeAxisValueAsButton(float input) { - return (input < -0.5f) ? 1.f : 0.f; - } - - @VisibleForTesting - static float positiveAxisValueAsButton(float input) { - return (input > 0.5f) ? 1.f : 0.f; - } - - private static void mapHatAxisToDpadButtons(float[] mappedButtons, float[] rawAxes) { - float hatX = rawAxes[MotionEvent.AXIS_HAT_X]; - float hatY = rawAxes[MotionEvent.AXIS_HAT_Y]; - mappedButtons[CanonicalButtonIndex.DPAD_LEFT] = negativeAxisValueAsButton(hatX); - mappedButtons[CanonicalButtonIndex.DPAD_RIGHT] = positiveAxisValueAsButton(hatX); - mappedButtons[CanonicalButtonIndex.DPAD_UP] = negativeAxisValueAsButton(hatY); - mappedButtons[CanonicalButtonIndex.DPAD_DOWN] = positiveAxisValueAsButton(hatY); - } - - private static class AmazonFireGamepadMappings extends GamepadMappings { - - /** - * Method for mapping Amazon Fire gamepad axis and button values - * to standard gamepad button and axes values. - */ - @Override - public void mapToStandardGamepad(float[] mappedAxes, float[] mappedButtons, - float[] rawAxes, float[] rawButtons) { - mapCommonXYABButtons(mappedButtons, rawButtons); - mapTriggerButtonsToTopShoulder(mappedButtons, rawButtons); - mapCommonThumbstickButtons(mappedButtons, rawButtons); - mapCommonStartSelectMetaButtons(mappedButtons, rawButtons); - mapPedalAxesToBottomShoulder(mappedButtons, rawAxes); - mapHatAxisToDpadButtons(mappedButtons, rawAxes); - - mapXYAxes(mappedAxes, rawAxes); - mapZAndRZAxesToRightStick(mappedAxes, rawAxes); - } - } - - private static class XboxCompatibleGamepadMappings extends GamepadMappings { - - /** - * Method for mapping Xbox 360-compatible gamepad axis and button values - * to standard gamepad button and axes values. - */ - @Override - public void mapToStandardGamepad(float[] mappedAxes, float[] mappedButtons, - float[] rawAxes, float[] rawButtons) { - mapCommonXYABButtons(mappedButtons, rawButtons); - mapTriggerButtonsToTopShoulder(mappedButtons, rawButtons); - mapCommonThumbstickButtons(mappedButtons, rawButtons); - mapCommonStartSelectMetaButtons(mappedButtons, rawButtons); - mapTriggerAxesToBottomShoulder(mappedButtons, rawAxes); - mapHatAxisToDpadButtons(mappedButtons, rawAxes); - - mapXYAxes(mappedAxes, rawAxes); - mapZAndRZAxesToRightStick(mappedAxes, rawAxes); - } - } - - private static class SnakebyteIDroidConMappings extends GamepadMappings { - private final boolean mAnalogMode; - - public SnakebyteIDroidConMappings(int[] axes) { - // Digital mode has X, Y, Z, RZ, HAT_X, HAT_Y - // Analog mode has X, Y, Z, RX, RY, HAT_X, HAT_Y - mAnalogMode = arrayContains(axes, MotionEvent.AXIS_RX); - } - - private static boolean arrayContains(int[] array, int element) { - for (int e : array) { - if (e == element) { - return true; - } - } - return false; - } - - @Override - public int getButtonsLength() { - // No meta button. - return CanonicalButtonIndex.COUNT - 1; - } - - @Override - public void mapToStandardGamepad( - float[] mappedAxes, float[] mappedButtons, float[] rawAxes, float[] rawButtons) { - mapCommonXYABButtons(mappedButtons, rawButtons); - mapTriggerButtonsToTopShoulder(mappedButtons, rawButtons); - mapCommonStartSelectMetaButtons(mappedButtons, rawButtons); - mapXYAxes(mappedAxes, rawAxes); - mapHatAxisToDpadButtons(mappedButtons, rawAxes); - - // On older versions of Android the thumbstick buttons are incorrectly mapped to C and - // Z. Support either. - float thumbL = rawButtons[KeyEvent.KEYCODE_BUTTON_THUMBL]; - float thumbR = rawButtons[KeyEvent.KEYCODE_BUTTON_THUMBR]; - float c = rawButtons[KeyEvent.KEYCODE_BUTTON_C]; - float z = rawButtons[KeyEvent.KEYCODE_BUTTON_Z]; - mappedButtons[CanonicalButtonIndex.LEFT_THUMBSTICK] = Math.max(thumbL, c); - mappedButtons[CanonicalButtonIndex.RIGHT_THUMBSTICK] = Math.max(thumbR, z); - - if (mAnalogMode) { - mapZAxisToBottomShoulder(mappedButtons, rawAxes); - mapRXAndRYAxesToRightStick(mappedAxes, rawAxes); - } else { - mapLowerTriggerButtonsToBottomShoulder(mappedButtons, rawButtons); - mapZAndRZAxesToRightStick(mappedAxes, rawAxes); - } - } - } - - private static class XboxOneS2016FirmwareMappings extends GamepadMappings { - private boolean mLeftTriggerActivated; - private boolean mRightTriggerActivated; - - /** - * Method for mapping Xbox One S controller (in Bluetooth mode) to - * standard gamepad button and axes values. - */ - @Override - public void mapToStandardGamepad( - float[] mappedAxes, float[] mappedButtons, float[] rawAxes, float[] rawButtons) { - mappedButtons[CanonicalButtonIndex.PRIMARY] = rawButtons[KeyEvent.KEYCODE_BUTTON_A]; - mappedButtons[CanonicalButtonIndex.SECONDARY] = rawButtons[KeyEvent.KEYCODE_BUTTON_B]; - mappedButtons[CanonicalButtonIndex.TERTIARY] = rawButtons[KeyEvent.KEYCODE_BUTTON_C]; - mappedButtons[CanonicalButtonIndex.QUATERNARY] = rawButtons[KeyEvent.KEYCODE_BUTTON_X]; - - mappedButtons[CanonicalButtonIndex.LEFT_SHOULDER] = - rawButtons[KeyEvent.KEYCODE_BUTTON_Y]; - mappedButtons[CanonicalButtonIndex.RIGHT_SHOULDER] = - rawButtons[KeyEvent.KEYCODE_BUTTON_Z]; - - mappedButtons[CanonicalButtonIndex.BACK_SELECT] = - rawButtons[KeyEvent.KEYCODE_BUTTON_L1]; - mappedButtons[CanonicalButtonIndex.START] = rawButtons[KeyEvent.KEYCODE_BUTTON_R1]; - - mappedButtons[CanonicalButtonIndex.LEFT_THUMBSTICK] = - rawButtons[KeyEvent.KEYCODE_BUTTON_L2]; - mappedButtons[CanonicalButtonIndex.RIGHT_THUMBSTICK] = - rawButtons[KeyEvent.KEYCODE_BUTTON_R2]; - - // The left and right triggers on the Xbox One S controller - // are exposed as AXIS_Z and AXIS_RZ respectively. However, - // these nominally idle at -1 rather than 0, like other triggers. - // Unfortunately, the -1 value is only reported upon the first - // activation of each trigger axis. In order to prevent idling at - // 0.5 before trigger activation, we only expose trigger values - // when we've seen them report a non-zero value at least once. - if (rawAxes[MotionEvent.AXIS_Z] != 0) { - mLeftTriggerActivated = true; - } - if (rawAxes[MotionEvent.AXIS_RZ] != 0) { - mRightTriggerActivated = true; - } - if (mLeftTriggerActivated) { - mappedButtons[CanonicalButtonIndex.LEFT_TRIGGER] = - (rawAxes[MotionEvent.AXIS_Z] + 1) / 2; - } else { - mappedButtons[CanonicalButtonIndex.LEFT_TRIGGER] = 0.f; - } - if (mRightTriggerActivated) { - mappedButtons[CanonicalButtonIndex.RIGHT_TRIGGER] = - (rawAxes[MotionEvent.AXIS_RZ] + 1) / 2; - } else { - mappedButtons[CanonicalButtonIndex.RIGHT_TRIGGER] = 0.f; - } - - mapHatAxisToDpadButtons(mappedButtons, rawAxes); - mapXYAxes(mappedAxes, rawAxes); - mapRXAndRYAxesToRightStick(mappedAxes, rawAxes); - } - - @Override - public int getButtonsLength() { - // No meta button. - return CanonicalButtonIndex.COUNT - 1; - } - } - - private static class Dualshock3SixAxisGamepadMappingsPreP extends GamepadMappings { - /** - * Method for mapping DualShock 3 and SIXAXIS gamepad inputs to standard gamepad button and - * axis values. This mapping function should only be used on Android 8 and earlier. - */ - @Override - public void mapToStandardGamepad(float[] mappedAxes, float[] mappedButtons, - float[] rawAxes, float[] rawButtons) { - // On DualShock 3 and SIXAXIS, X/Y has higher priority. - float a = rawButtons[KeyEvent.KEYCODE_BUTTON_A]; - float b = rawButtons[KeyEvent.KEYCODE_BUTTON_B]; - float x = rawButtons[KeyEvent.KEYCODE_BUTTON_X]; - float y = rawButtons[KeyEvent.KEYCODE_BUTTON_Y]; - mappedButtons[CanonicalButtonIndex.PRIMARY] = x; - mappedButtons[CanonicalButtonIndex.SECONDARY] = y; - mappedButtons[CanonicalButtonIndex.TERTIARY] = a; - mappedButtons[CanonicalButtonIndex.QUATERNARY] = b; - - mapTriggerButtonsToTopShoulder(mappedButtons, rawButtons); - mapCommonThumbstickButtons(mappedButtons, rawButtons); - mapCommonDpadButtons(mappedButtons, rawButtons); - mapCommonStartSelectMetaButtons(mappedButtons, rawButtons); - mapTriggerAxesToBottomShoulder(mappedButtons, rawAxes); - - mapXYAxes(mappedAxes, rawAxes); - mapZAndRZAxesToRightStick(mappedAxes, rawAxes); - } - } - - private static class Dualshock3SixAxisGamepadMappings extends GamepadMappings { - /** - * Method for mapping DualShock 3 and SIXAXIS gamepad inputs to standard gamepad button and - * axis values. This mapping function should only be used on Android 10+. - */ - @Override - public void mapToStandardGamepad( - float[] mappedAxes, float[] mappedButtons, float[] rawAxes, float[] rawButtons) { - mapCommonXYABButtons(mappedButtons, rawButtons); - mapTriggerButtonsToTopShoulder(mappedButtons, rawButtons); - mapCommonThumbstickButtons(mappedButtons, rawButtons); - mapCommonStartSelectMetaButtons(mappedButtons, rawButtons); - mapCommonDpadButtons(mappedButtons, rawButtons); - mapXYAxes(mappedAxes, rawAxes); - mapZAndRZAxesToRightStick(mappedAxes, rawAxes); - mapTriggerAxesToBottomShoulder(mappedButtons, rawAxes); - } - } - - static class Dualshock4GamepadMappingsPreP extends GamepadMappings { - // Scale input from [-1, 1] to [0, 1] uniformly. - private static float scaleRxRy(float input) { - return 1.f - ((1.f - input) / 2.f); - } - - /** - * Method for mapping DualShock 4 gamepad inputs to standard gamepad button and axis values. - * This mapping function should only be used on Android 9 and earlier. - */ - @Override - public void mapToStandardGamepad( - float[] mappedAxes, float[] mappedButtons, float[] rawAxes, float[] rawButtons) { - float a = rawButtons[KeyEvent.KEYCODE_BUTTON_A]; - float b = rawButtons[KeyEvent.KEYCODE_BUTTON_B]; - float c = rawButtons[KeyEvent.KEYCODE_BUTTON_C]; - float x = rawButtons[KeyEvent.KEYCODE_BUTTON_X]; - mappedButtons[CanonicalButtonIndex.PRIMARY] = b; - mappedButtons[CanonicalButtonIndex.SECONDARY] = c; - mappedButtons[CanonicalButtonIndex.TERTIARY] = a; - mappedButtons[CanonicalButtonIndex.QUATERNARY] = x; - - float y = rawButtons[KeyEvent.KEYCODE_BUTTON_Y]; - float z = rawButtons[KeyEvent.KEYCODE_BUTTON_Z]; - mappedButtons[CanonicalButtonIndex.LEFT_SHOULDER] = y; - mappedButtons[CanonicalButtonIndex.RIGHT_SHOULDER] = z; - - float rx = rawAxes[MotionEvent.AXIS_RX]; - float ry = rawAxes[MotionEvent.AXIS_RY]; - mappedButtons[CanonicalButtonIndex.LEFT_TRIGGER] = scaleRxRy(rx); - mappedButtons[CanonicalButtonIndex.RIGHT_TRIGGER] = scaleRxRy(ry); - - float share = rawButtons[KeyEvent.KEYCODE_BUTTON_L2]; - float options = rawButtons[KeyEvent.KEYCODE_BUTTON_R2]; - mappedButtons[CanonicalButtonIndex.BACK_SELECT] = share; - mappedButtons[CanonicalButtonIndex.START] = options; - - float thumbL = rawButtons[KeyEvent.KEYCODE_BUTTON_SELECT]; - float thumbR = rawButtons[KeyEvent.KEYCODE_BUTTON_START]; - mappedButtons[CanonicalButtonIndex.LEFT_THUMBSTICK] = thumbL; - mappedButtons[CanonicalButtonIndex.RIGHT_THUMBSTICK] = thumbR; - - float mode = rawButtons[KeyEvent.KEYCODE_BUTTON_MODE]; - mappedButtons[CanonicalButtonIndex.META] = mode; - - mapHatAxisToDpadButtons(mappedButtons, rawAxes); - mapXYAxes(mappedAxes, rawAxes); - mapZAndRZAxesToRightStick(mappedAxes, rawAxes); - } - } - - private static class SamsungEIGP20GamepadMappings extends GamepadMappings { - /** - * Method for mapping Samsung GamePad EI-GP20 axis and button values - * to standard gamepad button and axes values. - */ - @Override - public void mapToStandardGamepad(float[] mappedAxes, float[] mappedButtons, - float[] rawAxes, float[] rawButtons) { - mapCommonXYABButtons(mappedButtons, rawButtons); - mapUpperTriggerButtonsToBottomShoulder(mappedButtons, rawButtons); - mapCommonThumbstickButtons(mappedButtons, rawButtons); - mapCommonStartSelectMetaButtons(mappedButtons, rawButtons); - mapHatAxisToDpadButtons(mappedButtons, rawAxes); - - mapXYAxes(mappedAxes, rawAxes); - mapRXAndRYAxesToRightStick(mappedAxes, rawAxes); - } - } - - private static class UnknownGamepadMappings extends GamepadMappings { - private int mLeftTriggerAxis = -1; - private int mRightTriggerAxis = -1; - private int mRightStickXAxis = -1; - private int mRightStickYAxis = -1; - private boolean mUseHatAxes; - private final boolean mHasMetaButton; - - UnknownGamepadMappings(int[] axes, BitSet buttons) { - mHasMetaButton = buttons.get(KeyEvent.KEYCODE_BUTTON_MODE); - - int hatAxesFound = 0; - - for (int axis : axes) { - switch (axis) { - case MotionEvent.AXIS_LTRIGGER: - case MotionEvent.AXIS_BRAKE: - mLeftTriggerAxis = axis; - break; - case MotionEvent.AXIS_RTRIGGER: - case MotionEvent.AXIS_GAS: - case MotionEvent.AXIS_THROTTLE: - mRightTriggerAxis = axis; - break; - case MotionEvent.AXIS_RX: - case MotionEvent.AXIS_Z: - mRightStickXAxis = axis; - break; - case MotionEvent.AXIS_RY: - case MotionEvent.AXIS_RZ: - mRightStickYAxis = axis; - break; - case MotionEvent.AXIS_HAT_X: - hatAxesFound++; - break; - case MotionEvent.AXIS_HAT_Y: - hatAxesFound++; - break; - default: - break; - } - } - - if (hatAxesFound == 2) { - mUseHatAxes = true; - } - } - - @Override - public boolean isStandard() { - // These mappings should not be considered standard - return false; - } - - @Override - public int getButtonsLength() { - return mHasMetaButton ? CanonicalButtonIndex.COUNT : CanonicalButtonIndex.COUNT - 1; - } - - @Override - public void mapToStandardGamepad(float[] mappedAxes, float[] mappedButtons, - float[] rawAxes, float[] rawButtons) { - // These are shared among all gamepads intended for use with Android - // that we tested so far. - mapCommonXYABButtons(mappedButtons, rawButtons); - mapTriggerButtonsToTopShoulder(mappedButtons, rawButtons); - mapCommonThumbstickButtons(mappedButtons, rawButtons); - mapCommonStartSelectMetaButtons(mappedButtons, rawButtons); - mapXYAxes(mappedAxes, rawAxes); - - if (mLeftTriggerAxis != -1 && mRightTriggerAxis != -1) { - float lTrigger = rawAxes[mLeftTriggerAxis]; - float rTrigger = rawAxes[mRightTriggerAxis]; - mappedButtons[CanonicalButtonIndex.LEFT_TRIGGER] = lTrigger; - mappedButtons[CanonicalButtonIndex.RIGHT_TRIGGER] = rTrigger; - } else { - // Devices without analog triggers use digital buttons - mapLowerTriggerButtonsToBottomShoulder(mappedButtons, rawButtons); - } - - if (mRightStickXAxis != -1 && mRightStickYAxis != -1) { - float rX = rawAxes[mRightStickXAxis]; - float rY = rawAxes[mRightStickYAxis]; - mappedAxes[CanonicalAxisIndex.RIGHT_STICK_X] = rX; - mappedAxes[CanonicalAxisIndex.RIGHT_STICK_Y] = rY; - } - - if (mUseHatAxes) { - mapHatAxisToDpadButtons(mappedButtons, rawAxes); - } else { - mapCommonDpadButtons(mappedButtons, rawButtons); - } - } - } -} diff --git a/chromium/device/gamepad/gamepad_provider.cc b/chromium/device/gamepad/gamepad_provider.cc index 89439c77e92..ecc58373ba9 100644 --- a/chromium/device/gamepad/gamepad_provider.cc +++ b/chromium/device/gamepad/gamepad_provider.cc @@ -29,6 +29,8 @@ namespace device { +constexpr int64_t kPollingIntervalMilliseconds = 4; // ~250 Hz + GamepadProvider::GamepadProvider( GamepadConnectionChangeClient* connection_change_client) : gamepad_shared_buffer_(std::make_unique<GamepadSharedBuffer>()), @@ -139,7 +141,7 @@ void GamepadProvider::OnDevicesChanged(base::SystemMonitor::DeviceType type) { void GamepadProvider::Initialize(std::unique_ptr<GamepadDataFetcher> fetcher) { sampling_interval_delta_ = - base::TimeDelta::FromMilliseconds(features::GetGamepadPollingInterval()); + base::TimeDelta::FromMilliseconds(kPollingIntervalMilliseconds); base::SystemMonitor* monitor = base::SystemMonitor::Get(); if (monitor) diff --git a/chromium/device/gamepad/public/cpp/gamepad_features.cc b/chromium/device/gamepad/public/cpp/gamepad_features.cc index c74ddbb2582..d942501706e 100644 --- a/chromium/device/gamepad/public/cpp/gamepad_features.cc +++ b/chromium/device/gamepad/public/cpp/gamepad_features.cc @@ -15,24 +15,6 @@ namespace features { -namespace { - -const size_t kPollingIntervalMillisecondsMin = 4; // ~250 Hz -const size_t kPollingIntervalMillisecondsMax = 16; // ~62.5 Hz - -size_t OverrideIntervalIfValid(base::StringPiece param_value, - size_t default_interval) { - size_t interval; - if (param_value.empty() || !base::StringToSizeT(param_value, &interval)) - return default_interval; - // Clamp interval duration to valid range. - interval = std::max(interval, kPollingIntervalMillisecondsMin); - interval = std::min(interval, kPollingIntervalMillisecondsMax); - return interval; -} - -} // namespace - // Enables gamepadbuttondown, gamepadbuttonup, gamepadbuttonchange, // gamepadaxismove non-standard gamepad events. const base::Feature kEnableGamepadButtonAxisEvents{ @@ -42,15 +24,9 @@ const base::Feature kEnableGamepadButtonAxisEvents{ const base::Feature kEnableWindowsGamingInputDataFetcher{ "EnableWindowsGamingInputDataFetcher", base::FEATURE_DISABLED_BY_DEFAULT}; -// Overrides the gamepad polling interval. -const base::Feature kGamepadPollingInterval{"GamepadPollingInterval", - base::FEATURE_DISABLED_BY_DEFAULT}; - const base::Feature kRestrictGamepadAccess{"RestrictGamepadAccess", base::FEATURE_DISABLED_BY_DEFAULT}; -const char kGamepadPollingIntervalParamKey[] = "interval-ms"; - bool AreGamepadButtonAxisEventsEnabled() { // Check if button and axis events are enabled by a field trial. if (base::FeatureList::IsEnabled(kEnableGamepadButtonAxisEvents)) @@ -66,27 +42,4 @@ bool AreGamepadButtonAxisEventsEnabled() { return false; } -size_t GetGamepadPollingInterval() { - // Default to the minimum polling interval. - size_t polling_interval = kPollingIntervalMillisecondsMin; - - // Check if the polling interval is overridden by a field trial. - if (base::FeatureList::IsEnabled(kGamepadPollingInterval)) { - std::string param_value = base::GetFieldTrialParamValueByFeature( - kGamepadPollingInterval, kGamepadPollingIntervalParamKey); - polling_interval = OverrideIntervalIfValid(param_value, polling_interval); - } - - // Check if the polling interval is overridden by a command-line flag. - base::CommandLine* command_line = base::CommandLine::ForCurrentProcess(); - if (command_line && - command_line->HasSwitch(switches::kGamepadPollingInterval)) { - std::string switch_value = - command_line->GetSwitchValueASCII(switches::kGamepadPollingInterval); - polling_interval = OverrideIntervalIfValid(switch_value, polling_interval); - } - - return polling_interval; -} - } // namespace features diff --git a/chromium/device/gamepad/public/cpp/gamepad_features.h b/chromium/device/gamepad/public/cpp/gamepad_features.h index d5fc29c7fc8..12e857660f6 100644 --- a/chromium/device/gamepad/public/cpp/gamepad_features.h +++ b/chromium/device/gamepad/public/cpp/gamepad_features.h @@ -14,12 +14,9 @@ GAMEPAD_FEATURES_EXPORT extern const base::Feature kEnableGamepadButtonAxisEvents; GAMEPAD_FEATURES_EXPORT extern const base::Feature kEnableWindowsGamingInputDataFetcher; -GAMEPAD_FEATURES_EXPORT extern const base::Feature kGamepadPollingInterval; GAMEPAD_FEATURES_EXPORT extern const base::Feature kRestrictGamepadAccess; -GAMEPAD_FEATURES_EXPORT extern const char kGamepadPollingIntervalParamKey[]; GAMEPAD_FEATURES_EXPORT bool AreGamepadButtonAxisEventsEnabled(); -GAMEPAD_FEATURES_EXPORT size_t GetGamepadPollingInterval(); } // namespace features diff --git a/chromium/device/gamepad/udev_gamepad_linux.cc b/chromium/device/gamepad/udev_gamepad_linux.cc index 8845f575b22..e03e0da0344 100644 --- a/chromium/device/gamepad/udev_gamepad_linux.cc +++ b/chromium/device/gamepad/udev_gamepad_linux.cc @@ -26,6 +26,11 @@ bool DeviceIndexFromDevicePath(base::StringPiece path, return base::StringToInt(index_str, index); } +// Small helper to avoid constructing a `StringPiece` from nullptr. +base::StringPiece ToStringPiece(const char* str) { + return str ? base::StringPiece(str) : base::StringPiece(); +} + } // namespace const char UdevGamepadLinux::kInputSubsystem[] = "input"; @@ -49,11 +54,11 @@ std::unique_ptr<UdevGamepadLinux> UdevGamepadLinux::Create(udev_device* dev) { if (!dev) return nullptr; - const base::StringPiece node_path = device::udev_device_get_devnode(dev); + const auto node_path = ToStringPiece(device::udev_device_get_devnode(dev)); if (node_path.empty()) return nullptr; - const base::StringPiece node_syspath = device::udev_device_get_syspath(dev); + const auto node_syspath = ToStringPiece(device::udev_device_get_syspath(dev)); if (node_syspath.empty()) return nullptr; @@ -62,7 +67,7 @@ std::unique_ptr<UdevGamepadLinux> UdevGamepadLinux::Create(udev_device* dev) { device::udev_device_get_parent_with_subsystem_devtype( dev, kInputSubsystem, nullptr); if (parent_dev) - parent_syspath = device::udev_device_get_syspath(parent_dev); + parent_syspath = ToStringPiece(device::udev_device_get_syspath(parent_dev)); for (const auto& entry : device_roots) { const Type node_type = entry.first; diff --git a/chromium/device/vr/BUILD.gn b/chromium/device/vr/BUILD.gn index 5a2f428a561..8f0b6e6d21a 100644 --- a/chromium/device/vr/BUILD.gn +++ b/chromium/device/vr/BUILD.gn @@ -8,12 +8,33 @@ if (is_android) { import("//build/config/android/rules.gni") # For generate_jni(). } +config("vr_gl_mode") { + if (use_command_buffer) { + defines = [ + "VR_USE_COMMAND_BUFFER", + "GL_GLEXT_PROTOTYPES", + ] + } else { + defines = [ "VR_USE_NATIVE_GL" ] + } +} + +source_set("vr_gl_bindings") { + sources = [ "gl_bindings.h" ] + public_configs = [ ":vr_gl_mode" ] + if (use_command_buffer) { + public_deps = [ "//gpu/command_buffer/client:gles2_c_lib" ] + } else { + public_deps = [ "//ui/gl" ] + } +} + if (enable_vr) { # TODO(https://crbug.com/1073113): Flesh out and cleanup this target. component("vr_base") { visibility = [ # TODO(https://crbug.com/843374): Move arcore_device - "//chrome/browser/android/vr/*", + "//chrome/browser/*", "//content/services/isolated_xr_device/*", "//device/vr/*", ] @@ -25,9 +46,13 @@ if (enable_vr) { "vr_device.h", "vr_device_base.cc", "vr_device_base.h", + "vr_gl_util.cc", + "vr_gl_util.h", ] + public_configs = [ ":vr_gl_mode" ] public_deps = [ + ":vr_gl_bindings", "//device/vr/public/cpp", "//device/vr/public/mojom", ] @@ -100,37 +125,15 @@ if (enable_vr) { configs += [ "//third_party/gvr-android-sdk:libgvr_config" ] } - if (enable_openvr) { - if (!is_component_build) { - defines += [ "OPENVR_BUILD_STATIC" ] - } - - deps += [ "//third_party/openvr:openvr" ] + if (enable_oculus_vr || enable_windows_mr || enable_openxr) { sources += [ - "openvr/openvr_api_wrapper.cc", - "openvr/openvr_api_wrapper.h", - "openvr/openvr_device.cc", - "openvr/openvr_device.h", - "openvr/openvr_gamepad_helper.cc", - "openvr/openvr_gamepad_helper.h", - "openvr/openvr_render_loop.cc", - "openvr/openvr_render_loop.h", - "openvr/openvr_type_converters.cc", - "openvr/openvr_type_converters.h", "test/test_hook.h", - ] - } - - if (enable_openvr || enable_oculus_vr || enable_windows_mr || - enable_openxr) { - sources += [ "windows/compositor_base.cc", "windows/compositor_base.h", ] } - if (is_win && (enable_openvr || enable_oculus_vr || enable_windows_mr || - enable_openxr)) { + if (is_win && (enable_oculus_vr || enable_windows_mr || enable_openxr)) { libs = [ "d3d11.lib", "DXGI.lib", @@ -313,35 +316,6 @@ if (enable_vr) { } } -if (enable_openvr) { - shared_library("openvr_mock") { - testonly = true - output_name = "mock_vr_clients/bin/vrclient" - if (target_cpu == "x64" && is_win) { - output_name = "mock_vr_clients/bin/vrclient_x64" - } - - sources = [ - "openvr/test/fake_openvr_impl_api.cc", - "openvr/test/test_helper.cc", - "openvr/test/test_helper.h", - "test/test_hook.h", - ] - - libs = [ - "d3d11.lib", - "DXGI.lib", - ] - - deps = [ - ":directx_helpers", - "//base", - "//device/vr/public/mojom:test_mojom", - "//third_party/openvr:openvr_headers", - ] - } -} - if (enable_openxr) { # The OpenXR Loader by default looks for the path to the OpenXR Runtime from a # registry key, which typically points to the OpenXR runtime installed on the diff --git a/chromium/device/vr/DEPS b/chromium/device/vr/DEPS index 7f0638354ca..66aa93959bb 100644 --- a/chromium/device/vr/DEPS +++ b/chromium/device/vr/DEPS @@ -8,7 +8,8 @@ include_rules = [ "+services/metrics/public/cpp/ukm_builders.h", "+third_party/gvr-android-sdk/src", "+third_party/libovr/src", - "+third_party/openvr/src/headers/openvr.h", + "+third_party/skia/include/core/SkColor.h", "+ui/display", "+ui/gfx", + "+ui/gl/gl_bindings.h", ] diff --git a/chromium/device/vr/README.md b/chromium/device/vr/README.md index 42fa1a65895..7734f602fcb 100644 --- a/chromium/device/vr/README.md +++ b/chromium/device/vr/README.md @@ -17,13 +17,12 @@ towards OpenXR being the only API used on desktops. | API | OS | Supports | Enabled by Default | |-----------------------|:--------:|:--------:|:------------------:| | OpenXR | Windows* | VR* | Yes** | -| Windows Mixed Reality | Windows | VR | Yes | | AR Core | Android | AR | Yes | | Google VR | Android | VR | Yes | +| Windows Mixed Reality | Windows | VR | No | | Oculus | Windows | VR | No | -| OpenVR | Windows | VR | No | - - * OpenXR may support multiple OSes and AR use cases as well. Currently we + - \* OpenXR may support multiple OSes and AR use cases as well. Currently we only use it for VR on Windows since that's what the majority of existing runtimes support. - ** OpenXR runtimes are only enabled by default if they implement the diff --git a/chromium/device/vr/android/BUILD.gn b/chromium/device/vr/android/BUILD.gn new file mode 100644 index 00000000000..37f58947431 --- /dev/null +++ b/chromium/device/vr/android/BUILD.gn @@ -0,0 +1,17 @@ +# Copyright 2020 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +static_library("vr_android") { + defines = [] + sources = [ + "mailbox_to_surface_bridge.h", + "web_xr_presentation_state.cc", + "web_xr_presentation_state.h", + ] + + deps = [ + "//gpu/ipc/common:common", + "//ui/gl:gl", + ] +} diff --git a/chromium/device/vr/android/DEPS b/chromium/device/vr/android/DEPS new file mode 100644 index 00000000000..fefc58b8a5b --- /dev/null +++ b/chromium/device/vr/android/DEPS @@ -0,0 +1,5 @@ +include_rules = [ + "+gpu/ipc/common", + "+gpu/command_buffer/common/shared_image_usage.h", + "+ui/gl", +] diff --git a/chromium/device/vr/android/arcore/BUILD.gn b/chromium/device/vr/android/arcore/BUILD.gn index 7676a20c359..e733c1cc580 100644 --- a/chromium/device/vr/android/arcore/BUILD.gn +++ b/chromium/device/vr/android/arcore/BUILD.gn @@ -11,16 +11,30 @@ component("arcore") { defines = [ "IS_VR_ARCORE_IMPL" ] sources = [ "address_to_id_map.h", + "ar_image_transport.cc", + "ar_image_transport.h", + "ar_renderer.cc", + "ar_renderer.h", + "arcore.cc", "arcore.h", "arcore_anchor_manager.cc", "arcore_anchor_manager.h", + "arcore_device.cc", + "arcore_device.h", "arcore_device_provider_factory.cc", "arcore_device_provider_factory.h", + "arcore_gl.cc", + "arcore_gl.h", + "arcore_gl_thread.cc", + "arcore_gl_thread.h", "arcore_impl.cc", "arcore_impl.h", + "arcore_math_utils.cc", + "arcore_math_utils.h", "arcore_plane_manager.cc", "arcore_plane_manager.h", "arcore_sdk.h", + "arcore_session_utils.h", "arcore_shim.cc", "arcore_shim.h", "scoped_arcore_objects.h", @@ -32,9 +46,12 @@ component("arcore") { deps = [ "//base", + "//device/vr:vr", "//device/vr:vr_base", + "//device/vr/android:vr_android", "//mojo/public/cpp/bindings", "//ui/gfx", + "//ui/gl/init", ] configs += [ "//third_party/arcore-android-sdk:libarcore_config" ] diff --git a/chromium/device/vr/android/arcore/DEPS b/chromium/device/vr/android/arcore/DEPS index efd700cc2a1..5e9ba59297a 100644 --- a/chromium/device/vr/android/arcore/DEPS +++ b/chromium/device/vr/android/arcore/DEPS @@ -1 +1,3 @@ -include_rules = [ "+third_party/arcore-android-sdk/src", ]
\ No newline at end of file +include_rules = [ + "+third_party/arcore-android-sdk/src", + ]
\ No newline at end of file diff --git a/chromium/device/vr/android/arcore/ar_image_transport.cc b/chromium/device/vr/android/arcore/ar_image_transport.cc new file mode 100644 index 00000000000..6d063b6a439 --- /dev/null +++ b/chromium/device/vr/android/arcore/ar_image_transport.cc @@ -0,0 +1,408 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "device/vr/android/arcore/ar_image_transport.h" + +#include "base/android/android_hardware_buffer_compat.h" +#include "base/android/scoped_hardware_buffer_handle.h" +#include "base/containers/queue.h" +#include "base/trace_event/trace_event.h" +#include "base/trace_event/traced_value.h" +#include "device/vr/android/mailbox_to_surface_bridge.h" +#include "device/vr/android/web_xr_presentation_state.h" +#include "gpu/command_buffer/common/shared_image_usage.h" +#include "gpu/ipc/common/gpu_memory_buffer_impl_android_hardware_buffer.h" +#include "ui/gfx/gpu_fence.h" +#include "ui/gl/android/scoped_java_surface.h" +#include "ui/gl/android/surface_texture.h" +#include "ui/gl/gl_bindings.h" +#include "ui/gl/gl_context.h" +#include "ui/gl/gl_fence_egl.h" +#include "ui/gl/gl_image_ahardwarebuffer.h" +#include "ui/gl/gl_surface.h" +#include "ui/gl/init/gl_factory.h" + +namespace device { + +ArImageTransport::ArImageTransport( + std::unique_ptr<MailboxToSurfaceBridge> mailbox_bridge) + : gl_thread_task_runner_(base::ThreadTaskRunnerHandle::Get()), + mailbox_bridge_(std::move(mailbox_bridge)) { + DVLOG(2) << __func__; +} + +ArImageTransport::~ArImageTransport() = default; + +void ArImageTransport::DestroySharedBuffers(vr::WebXrPresentationState* webxr) { + DVLOG(2) << __func__; + DCHECK(IsOnGlThread()); + + if (!webxr || !UseSharedBuffer()) + return; + + std::vector<std::unique_ptr<vr::WebXrSharedBuffer>> buffers = + webxr->TakeSharedBuffers(); + for (auto& buffer : buffers) { + if (!buffer->mailbox_holder.mailbox.IsZero()) { + DCHECK(mailbox_bridge_); + DVLOG(2) << ": DestroySharedImage, mailbox=" + << buffer->mailbox_holder.mailbox.ToDebugString(); + // Note: the sync token in mailbox_holder may not be accurate. See + // comment in TransferFrame below. + mailbox_bridge_->DestroySharedImage(buffer->mailbox_holder); + } + } +} + +void ArImageTransport::Initialize(vr::WebXrPresentationState* webxr, + base::OnceClosure callback) { + DCHECK(IsOnGlThread()); + DVLOG(2) << __func__; + + glDisable(GL_DEPTH_TEST); + glDepthMask(GL_FALSE); + ar_renderer_ = std::make_unique<ArRenderer>(); + glGenTextures(1, &camera_texture_id_arcore_); + + glGenFramebuffersEXT(1, &camera_fbo_); + + // When available (Android O and up), use AHardwareBuffer-based shared + // images for frame transport. + shared_buffer_draw_ = base::AndroidHardwareBufferCompat::IsSupportAvailable(); + + if (shared_buffer_draw_) { + DVLOG(2) << __func__ << ": UseSharedBuffer()=true"; + } else { + DVLOG(2) << __func__ << ": UseSharedBuffer()=false, setting up surface"; + glGenTextures(1, &transport_texture_id_); + transport_surface_texture_ = + gl::SurfaceTexture::Create(transport_texture_id_); + surface_size_ = {0, 0}; + mailbox_bridge_->CreateSurface(transport_surface_texture_.get()); + transport_surface_texture_->SetFrameAvailableCallback(base::BindRepeating( + &ArImageTransport::OnFrameAvailable, weak_ptr_factory_.GetWeakPtr())); + } + + mailbox_bridge_->CreateAndBindContextProvider( + base::BindOnce(&ArImageTransport::OnMailboxBridgeReady, + weak_ptr_factory_.GetWeakPtr(), std::move(callback))); +} + +void ArImageTransport::OnMailboxBridgeReady(base::OnceClosure callback) { + DVLOG(2) << __func__; + DCHECK(IsOnGlThread()); + + DCHECK(mailbox_bridge_->IsConnected()); + + std::move(callback).Run(); +} + +void ArImageTransport::SetFrameAvailableCallback( + XrFrameCallback on_transport_frame_available) { + DVLOG(2) << __func__; + on_transport_frame_available_ = std::move(on_transport_frame_available); +} + +void ArImageTransport::OnFrameAvailable() { + DVLOG(2) << __func__; + DCHECK(on_transport_frame_available_); + + // This function assumes that there's only at most one frame in "processing" + // state at any given time, the webxr_ state handling ensures that. Drawing + // and swapping twice without an intervening UpdateTexImage call would lose + // an image, and that would lead to images and poses getting out of sync. + // + // It also assumes that the ArImageTransport and Surface only exist for the + // duration of a single session, and a new session will use fresh objects. For + // comparison, see GvrSchedulerDelegate::OnWebXrFrameAvailable which has more + // complex logic to support a lifetime across multiple sessions, including + // handling a possibly-unconsumed frame left over from a previous session. + + transport_surface_texture_->UpdateTexImage(); + + // The SurfaceTexture needs to be drawn using the corresponding + // UV transform, that's usually a Y flip. + transport_surface_texture_->GetTransformMatrix( + &transport_surface_texture_uv_matrix_[0]); + transport_surface_texture_uv_transform_.matrix().setColMajorf( + transport_surface_texture_uv_matrix_); + + on_transport_frame_available_.Run(transport_surface_texture_uv_transform_); +} + +GLuint ArImageTransport::GetCameraTextureId() { + return camera_texture_id_arcore_; +} + +bool ArImageTransport::ResizeSharedBuffer(vr::WebXrPresentationState* webxr, + const gfx::Size& size, + vr::WebXrSharedBuffer* buffer) { + DCHECK(IsOnGlThread()); + + if (buffer->size == size) + return false; + + TRACE_EVENT0("gpu", __FUNCTION__); + // Unbind previous image (if any). + if (!buffer->mailbox_holder.mailbox.IsZero()) { + DVLOG(2) << ": DestroySharedImage, mailbox=" + << buffer->mailbox_holder.mailbox.ToDebugString(); + // Note: the sync token in mailbox_holder may not be accurate. See comment + // in TransferFrame below. + mailbox_bridge_->DestroySharedImage(buffer->mailbox_holder); + } + + DVLOG(2) << __FUNCTION__ << ": width=" << size.width() + << " height=" << size.height(); + // Remove reference to previous image (if any). + buffer->local_glimage = nullptr; + + static constexpr gfx::BufferFormat format = gfx::BufferFormat::RGBA_8888; + static constexpr gfx::BufferUsage usage = gfx::BufferUsage::SCANOUT; + + gfx::GpuMemoryBufferId kBufferId(webxr->next_memory_buffer_id++); + buffer->gmb = gpu::GpuMemoryBufferImplAndroidHardwareBuffer::Create( + kBufferId, size, format, usage, + gpu::GpuMemoryBufferImpl::DestructionCallback()); + + uint32_t shared_image_usage = gpu::SHARED_IMAGE_USAGE_SCANOUT | + gpu::SHARED_IMAGE_USAGE_DISPLAY | + gpu::SHARED_IMAGE_USAGE_GLES2; + buffer->mailbox_holder = mailbox_bridge_->CreateSharedImage( + buffer->gmb.get(), gfx::ColorSpace(), shared_image_usage); + DVLOG(2) << ": CreateSharedImage, mailbox=" + << buffer->mailbox_holder.mailbox.ToDebugString() << ", SyncToken=" + << buffer->mailbox_holder.sync_token.ToDebugString(); + + auto img = base::MakeRefCounted<gl::GLImageAHardwareBuffer>(size); + + base::android::ScopedHardwareBufferHandle ahb = + buffer->gmb->CloneHandle().android_hardware_buffer; + bool ret = img->Initialize(ahb.get(), false /* preserved */); + if (!ret) { + DLOG(WARNING) << __FUNCTION__ << ": ERROR: failed to initialize image!"; + return false; + } + glBindTexture(GL_TEXTURE_EXTERNAL_OES, buffer->local_texture); + img->BindTexImage(GL_TEXTURE_EXTERNAL_OES); + buffer->local_glimage = std::move(img); + + // Save size to avoid resize next time. + DVLOG(1) << __FUNCTION__ << ": resized to " << size.width() << "x" + << size.height(); + buffer->size = size; + return true; +} + +std::unique_ptr<vr::WebXrSharedBuffer> ArImageTransport::CreateBuffer() { + std::unique_ptr<vr::WebXrSharedBuffer> buffer = + std::make_unique<vr::WebXrSharedBuffer>(); + // Local resources + glGenTextures(1, &buffer->local_texture); + return buffer; +} + +gpu::MailboxHolder ArImageTransport::TransferFrame( + vr::WebXrPresentationState* webxr, + const gfx::Size& frame_size, + const gfx::Transform& uv_transform) { + DCHECK(IsOnGlThread()); + DCHECK(UseSharedBuffer()); + + if (!webxr->GetAnimatingFrame()->shared_buffer) { + webxr->GetAnimatingFrame()->shared_buffer = CreateBuffer(); + } + + vr::WebXrSharedBuffer* shared_buffer = + webxr->GetAnimatingFrame()->shared_buffer.get(); + ResizeSharedBuffer(webxr, frame_size, shared_buffer); + // Sanity check that the lazily created/resized buffer looks valid. + DCHECK(!shared_buffer->mailbox_holder.mailbox.IsZero()); + DCHECK(shared_buffer->local_glimage); + DCHECK_EQ(shared_buffer->local_glimage->GetSize(), frame_size); + + // We don't need to create a sync token here. ResizeSharedBuffer has created + // one on reallocation, including initial buffer creation, and we can use + // that. The shared image interface internally uses its own command buffer ID + // and separate sync token release count namespace, and we must not overwrite + // that. We don't need a new sync token when reusing a correctly-sized buffer, + // it's only eligible for reuse after all reads from it are complete, meaning + // that it's transitioned through "processing" and "rendering" states back + // to "animating". + DCHECK(shared_buffer->mailbox_holder.sync_token.HasData()); + DVLOG(2) << ": SyncToken=" + << shared_buffer->mailbox_holder.sync_token.ToDebugString(); + + return shared_buffer->mailbox_holder; +} + +gpu::MailboxHolder ArImageTransport::TransferCameraImageFrame( + vr::WebXrPresentationState* webxr, + const gfx::Size& frame_size, + const gfx::Transform& uv_transform) { + DCHECK(IsOnGlThread()); + DCHECK(UseSharedBuffer()); + + if (!webxr->GetAnimatingFrame()->camera_image_shared_buffer) { + webxr->GetAnimatingFrame()->camera_image_shared_buffer = CreateBuffer(); + } + + vr::WebXrSharedBuffer* camera_image_shared_buffer = + webxr->GetAnimatingFrame()->camera_image_shared_buffer.get(); + bool was_resized = + ResizeSharedBuffer(webxr, frame_size, camera_image_shared_buffer); + if (was_resized) { + // Ensure that the following GPU command buffer actions are sequenced after + // the shared buffer operations. The shared image interface uses a separate + // command buffer stream. + DCHECK(camera_image_shared_buffer->mailbox_holder.sync_token.HasData()); + WaitSyncToken(camera_image_shared_buffer->mailbox_holder.sync_token); + DVLOG(3) << __func__ + << ": " + "camera_image_shared_buffer->mailbox_holder.sync_" + "token=" + << camera_image_shared_buffer->mailbox_holder.sync_token + .ToDebugString(); + } + // Sanity checks for the camera image buffer. + DCHECK(!camera_image_shared_buffer->mailbox_holder.mailbox.IsZero()); + DCHECK(camera_image_shared_buffer->local_glimage); + DCHECK_EQ(camera_image_shared_buffer->local_glimage->GetSize(), frame_size); + + // Temporarily change drawing buffer to the camera image buffer. + if (!camera_image_fbo_) { + glGenFramebuffersEXT(1, &camera_image_fbo_); + } + glBindFramebufferEXT(GL_DRAW_FRAMEBUFFER, camera_image_fbo_); + glFramebufferTexture2DEXT(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, + GL_TEXTURE_EXTERNAL_OES, + camera_image_shared_buffer->local_texture, 0); + + CopyCameraImageToFramebuffer(frame_size, uv_transform); + +#if DCHECK_IS_ON() + if (!framebuffer_complete_checked_for_camera_buffer_) { + auto status = glCheckFramebufferStatusEXT(GL_DRAW_FRAMEBUFFER); + DVLOG(1) << __func__ << ": framebuffer status=" << std::hex << status; + DCHECK(status == GL_FRAMEBUFFER_COMPLETE); + framebuffer_complete_checked_for_camera_buffer_ = true; + } +#endif + + // Restore default drawing buffer. + glBindFramebufferEXT(GL_DRAW_FRAMEBUFFER, 0); + + std::unique_ptr<gl::GLFence> gl_fence = gl::GLFence::CreateForGpuFence(); + std::unique_ptr<gfx::GpuFence> gpu_fence = gl_fence->GetGpuFence(); + mailbox_bridge_->WaitForClientGpuFence(gpu_fence.release()); + + mailbox_bridge_->GenSyncToken( + &camera_image_shared_buffer->mailbox_holder.sync_token); + DVLOG(3) + << __func__ << ": camera_image_shared_buffer->mailbox_holder.sync_token=" + << camera_image_shared_buffer->mailbox_holder.sync_token.ToDebugString(); + return camera_image_shared_buffer->mailbox_holder; +} + +void ArImageTransport::CreateGpuFenceForSyncToken( + const gpu::SyncToken& sync_token, + base::OnceCallback<void(std::unique_ptr<gfx::GpuFence>)> callback) { + DVLOG(2) << __func__; + mailbox_bridge_->CreateGpuFence(sync_token, std::move(callback)); +} + +void ArImageTransport::WaitSyncToken(const gpu::SyncToken& sync_token) { + mailbox_bridge_->WaitSyncToken(sync_token); +} + +void ArImageTransport::CopyCameraImageToFramebuffer( + const gfx::Size& frame_size, + const gfx::Transform& uv_transform) { + glDisable(GL_BLEND); + CopyTextureToFramebuffer(camera_texture_id_arcore_, frame_size, uv_transform); +} + +void ArImageTransport::ServerWaitForGpuFence( + std::unique_ptr<gfx::GpuFence> gpu_fence) { + std::unique_ptr<gl::GLFence> local_fence = + gl::GLFence::CreateFromGpuFence(*gpu_fence); + local_fence->ServerWait(); +} + +void ArImageTransport::CopyDrawnImageToFramebuffer( + vr::WebXrPresentationState* webxr, + const gfx::Size& frame_size, + const gfx::Transform& uv_transform) { + DVLOG(2) << __func__; + + GLuint source_texture; + if (UseSharedBuffer()) { + vr::WebXrSharedBuffer* shared_buffer = + webxr->GetRenderingFrame()->shared_buffer.get(); + source_texture = shared_buffer->local_texture; + } else { + source_texture = transport_texture_id_; + } + + // Set the blend mode for combining the drawn image (source) with the camera + // image (destination). WebXR assumes that the canvas has premultiplied alpha, + // so the source blend function is GL_ONE. The destination blend function is + // (1 - src_alpha) as usual. (Setting that to GL_ONE would simulate an + // additive AR headset that can't draw opaque black.) + glEnable(GL_BLEND); + glBlendFunc(GL_ONE, GL_ONE_MINUS_SRC_ALPHA); + glBindFramebufferEXT(GL_DRAW_FRAMEBUFFER, 0); + CopyTextureToFramebuffer(source_texture, frame_size, uv_transform); +} + +void ArImageTransport::CopyTextureToFramebuffer( + GLuint texture, + const gfx::Size& frame_size, + const gfx::Transform& uv_transform) { + DVLOG(2) << __func__; + // Don't need face culling, depth testing, blending, etc. Turn it all off. + // TODO(klausw): see if we can do this one time on initialization. That would + // be a tiny bit more efficient, but is only safe if ARCore and ArRenderer + // don't modify these states. + glDisable(GL_CULL_FACE); + glDisable(GL_SCISSOR_TEST); + glDisable(GL_POLYGON_OFFSET_FILL); + glViewport(0, 0, frame_size.width(), frame_size.height()); + + // Draw the ARCore texture! + float uv_transform_floats[16]; + uv_transform.matrix().asColMajorf(uv_transform_floats); + ar_renderer_->Draw(texture, uv_transform_floats); +} + +void ArImageTransport::CopyMailboxToSurfaceAndSwap( + const gfx::Size& frame_size, + const gpu::MailboxHolder& mailbox) { + DVLOG(2) << __func__; + if (frame_size != surface_size_) { + DVLOG(2) << __func__ << " resize from " << surface_size_.ToString() + << " to " << frame_size.ToString(); + transport_surface_texture_->SetDefaultBufferSize(frame_size.width(), + frame_size.height()); + mailbox_bridge_->ResizeSurface(frame_size.width(), frame_size.height()); + surface_size_ = frame_size; + } + + // Draw the image to the surface in the GPU process's command buffer context. + // This will trigger an OnFrameAvailable event once the corresponding + // SurfaceTexture in the local GL context is ready for updating. + bool swapped = mailbox_bridge_->CopyMailboxToSurfaceAndSwap(mailbox); + DCHECK(swapped); +} + +bool ArImageTransport::IsOnGlThread() const { + return gl_thread_task_runner_->BelongsToCurrentThread(); +} + +std::unique_ptr<ArImageTransport> ArImageTransportFactory::Create( + std::unique_ptr<MailboxToSurfaceBridge> mailbox_bridge) { + return std::make_unique<ArImageTransport>(std::move(mailbox_bridge)); +} + +} // namespace device diff --git a/chromium/device/vr/android/arcore/ar_image_transport.h b/chromium/device/vr/android/arcore/ar_image_transport.h new file mode 100644 index 00000000000..7796b94baab --- /dev/null +++ b/chromium/device/vr/android/arcore/ar_image_transport.h @@ -0,0 +1,147 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef DEVICE_VR_ANDROID_ARCORE_AR_IMAGE_TRANSPORT_H_ +#define DEVICE_VR_ANDROID_ARCORE_AR_IMAGE_TRANSPORT_H_ + +#include "base/macros.h" +#include "base/memory/ref_counted.h" +#include "base/memory/weak_ptr.h" +#include "base/single_thread_task_runner.h" +#include "device/vr/android/arcore/ar_renderer.h" +#include "device/vr/public/mojom/vr_service.mojom.h" +#include "ui/gfx/geometry/size_f.h" + +namespace gl { +class SurfaceTexture; +} // namespace gl + +namespace gfx { +class GpuFence; +} // namespace gfx + +namespace gpu { +struct MailboxHolder; +struct SyncToken; +} // namespace gpu + +namespace vr { +class WebXrPresentationState; +struct WebXrSharedBuffer; +} // namespace vr + +namespace device { + +class MailboxToSurfaceBridge; + +using XrFrameCallback = base::RepeatingCallback<void(const gfx::Transform&)>; + +// This class handles transporting WebGL rendered output from the GPU process's +// command buffer GL context to the local GL context, and compositing WebGL +// output onto the camera image using the local GL context. +class COMPONENT_EXPORT(VR_ARCORE) ArImageTransport { + public: + explicit ArImageTransport( + std::unique_ptr<MailboxToSurfaceBridge> mailbox_bridge); + virtual ~ArImageTransport(); + + virtual void DestroySharedBuffers(vr::WebXrPresentationState* webxr); + + // All methods must be called on a valid GL thread. Initialization + // must happen after the local GL context is ready for use. That + // starts the asynchronous setup for the GPU process command buffer + // GL context via MailboxToSurfaceBridge, and the callback is called + // once that's complete. + virtual void Initialize(vr::WebXrPresentationState* webxr, + base::OnceClosure callback); + + virtual GLuint GetCameraTextureId(); + + // This creates a shared buffer if one doesn't already exist, and populates it + // with the current animating frame's buffer data. It returns a + // gpu::Mailboxholder with this shared buffer data. + virtual gpu::MailboxHolder TransferFrame(vr::WebXrPresentationState* webxr, + const gfx::Size& frame_size, + const gfx::Transform& uv_transform); + + // This transfers whatever the contents of the texture specified + // by GetCameraTextureId() is at the time it is called and returns + // a gpu::MailboxHolder with that texture copied to a shared buffer. + virtual gpu::MailboxHolder TransferCameraImageFrame( + vr::WebXrPresentationState* webxr, + const gfx::Size& frame_size, + const gfx::Transform& uv_transform); + + virtual void CreateGpuFenceForSyncToken( + const gpu::SyncToken& sync_token, + base::OnceCallback<void(std::unique_ptr<gfx::GpuFence>)>); + virtual void CopyCameraImageToFramebuffer(const gfx::Size& frame_size, + const gfx::Transform& uv_transform); + virtual void CopyDrawnImageToFramebuffer(vr::WebXrPresentationState* webxr, + const gfx::Size& frame_size, + const gfx::Transform& uv_transform); + virtual void CopyTextureToFramebuffer(GLuint texture, + const gfx::Size& frame_size, + const gfx::Transform& uv_transform); + virtual void WaitSyncToken(const gpu::SyncToken& sync_token); + virtual void CopyMailboxToSurfaceAndSwap(const gfx::Size& frame_size, + const gpu::MailboxHolder& mailbox); + + bool UseSharedBuffer() { return shared_buffer_draw_; } + void SetFrameAvailableCallback(XrFrameCallback on_frame_available); + void ServerWaitForGpuFence(std::unique_ptr<gfx::GpuFence> gpu_fence); + + private: + std::unique_ptr<vr::WebXrSharedBuffer> CreateBuffer(); + // Returns true if the buffer was resized and its sync token updated. + bool ResizeSharedBuffer(vr::WebXrPresentationState* webxr, + const gfx::Size& size, + vr::WebXrSharedBuffer* buffer); + void ResizeSurface(const gfx::Size& size); + bool IsOnGlThread() const; + void OnMailboxBridgeReady(base::OnceClosure callback); + void OnFrameAvailable(); + std::unique_ptr<ArRenderer> ar_renderer_; + // samplerExternalOES texture for the camera image. + GLuint camera_texture_id_arcore_ = 0; + GLuint camera_fbo_ = 0; + GLuint camera_image_fbo_ = 0; + + scoped_refptr<base::SingleThreadTaskRunner> gl_thread_task_runner_; + + std::unique_ptr<MailboxToSurfaceBridge> mailbox_bridge_; + + // If true, use shared buffer transport aka DRAW_INTO_TEXTURE_MAILBOX. + // If false, use Surface transport aka SUBMIT_AS_MAILBOX_HOLDER. + bool shared_buffer_draw_ = false; + + // Used to limit framebuffer complete check to occurring once, due to it being + // expensive. + bool framebuffer_complete_checked_for_camera_buffer_ = false; + + // Used for Surface transport (Android N) + // + // samplerExternalOES texture data for WebXR content image. + GLuint transport_texture_id_ = 0; + gfx::Size surface_size_; + scoped_refptr<gl::SurfaceTexture> transport_surface_texture_; + gfx::Transform transport_surface_texture_uv_transform_; + float transport_surface_texture_uv_matrix_[16]; + XrFrameCallback on_transport_frame_available_; + + // Must be last. + base::WeakPtrFactory<ArImageTransport> weak_ptr_factory_{this}; + DISALLOW_COPY_AND_ASSIGN(ArImageTransport); +}; + +class COMPONENT_EXPORT(VR_ARCORE) ArImageTransportFactory { + public: + virtual ~ArImageTransportFactory() = default; + virtual std::unique_ptr<ArImageTransport> Create( + std::unique_ptr<MailboxToSurfaceBridge> mailbox_bridge); +}; + +} // namespace device + +#endif // DEVICE_VR_ANDROID_ARCORE_AR_IMAGE_TRANSPORT_H_ diff --git a/chromium/device/vr/android/arcore/ar_renderer.cc b/chromium/device/vr/android/arcore/ar_renderer.cc new file mode 100644 index 00000000000..1291a089bfb --- /dev/null +++ b/chromium/device/vr/android/arcore/ar_renderer.cc @@ -0,0 +1,125 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "device/vr/android/arcore/ar_renderer.h" + +#include "base/stl_util.h" +#include "device/vr/vr_gl_util.h" + +namespace device { + +namespace { + +static constexpr float kQuadVertices[8] = { + -0.5f, 0.5f, -0.5f, -0.5f, 0.5f, 0.5f, 0.5f, -0.5f, +}; +static constexpr GLushort kQuadIndices[6] = {0, 1, 2, 1, 3, 2}; + +// clang-format off +static constexpr char const* kVertexShader = SHADER( + precision mediump float; + uniform mat4 u_UvTransform; + attribute vec4 a_Position; + varying vec2 v_TexCoordinate; + + void main() { + // The quad vertex coordinate range is [-0.5, 0.5]. Transform to [0, 1], + // then apply the supplied affine transform matrix to get the final UV. + float xposition = a_Position[0] + 0.5; + float yposition = a_Position[1] + 0.5; + vec4 uv_in = vec4(xposition, yposition, 0.0, 1.0); + vec4 uv_out = u_UvTransform * uv_in; + v_TexCoordinate = vec2(uv_out.x, uv_out.y); + gl_Position = vec4(a_Position.xyz * 2.0, 1.0); + } +); + +static constexpr char const* kFragmentShader = OEIE_SHADER( + precision highp float; + uniform samplerExternalOES u_Texture; + varying vec2 v_TexCoordinate; + + void main() { + gl_FragColor = texture2D(u_Texture, v_TexCoordinate); + } +); +// clang-format on + +} // namespace + +ArRenderer::ArRenderer() { + std::string error; + GLuint vertex_shader_handle = + vr::CompileShader(GL_VERTEX_SHADER, kVertexShader, error); + // TODO(crbug.com/866593): fail gracefully if shaders don't compile. + CHECK(vertex_shader_handle) << error << "\nvertex_src\n" << kVertexShader; + + GLuint fragment_shader_handle = + vr::CompileShader(GL_FRAGMENT_SHADER, kFragmentShader, error); + CHECK(fragment_shader_handle) << error << "\nfragment_src\n" + << kFragmentShader; + + program_handle_ = vr::CreateAndLinkProgram(vertex_shader_handle, + fragment_shader_handle, error); + CHECK(program_handle_) << error; + + // Once the program is linked the shader objects are no longer needed + glDeleteShader(vertex_shader_handle); + glDeleteShader(fragment_shader_handle); + + position_handle_ = glGetAttribLocation(program_handle_, "a_Position"); + clip_rect_handle_ = glGetUniformLocation(program_handle_, "u_ClipRect"); + texture_handle_ = glGetUniformLocation(program_handle_, "u_Texture"); + uv_transform_ = glGetUniformLocation(program_handle_, "u_UvTransform"); +} + +void ArRenderer::Draw(int texture_handle, const float (&uv_transform)[16]) { + if (!vertex_buffer_ || !index_buffer_) { + GLuint buffers[2]; + glGenBuffersARB(2, buffers); + vertex_buffer_ = buffers[0]; + index_buffer_ = buffers[1]; + + glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer_); + glBufferData(GL_ARRAY_BUFFER, base::size(kQuadVertices) * sizeof(float), + kQuadVertices, GL_STATIC_DRAW); + + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, index_buffer_); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, + base::size(kQuadIndices) * sizeof(GLushort), kQuadIndices, + GL_STATIC_DRAW); + } + + glUseProgram(program_handle_); + + // Bind vertex attributes + glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer_); + + // Set up position attribute. + glVertexAttribPointer(position_handle_, 2, GL_FLOAT, false, 0, 0); + glEnableVertexAttribArray(position_handle_); + + // Bind texture. This is a 1:1 pixel copy since the source surface + // and renderbuffer destination size are resized to match, so use + // GL_NEAREST. + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_EXTERNAL_OES, texture_handle); + vr::SetTexParameters(GL_TEXTURE_EXTERNAL_OES); + glUniform1i(texture_handle_, 0); + + glUniformMatrix4fv(uv_transform_, 1, GL_FALSE, &uv_transform[0]); + + // Blit texture to buffer + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, index_buffer_); + glDrawElements(GL_TRIANGLES, base::size(kQuadIndices), GL_UNSIGNED_SHORT, 0); + + glDisableVertexAttribArray(position_handle_); +} + +// Note that we don't explicitly delete gl objects here, they're deleted +// automatically when we call ShutdownGL, and deleting them here leads to +// segfaults. +ArRenderer::~ArRenderer() = default; + +} // namespace device diff --git a/chromium/device/vr/android/arcore/ar_renderer.h b/chromium/device/vr/android/arcore/ar_renderer.h new file mode 100644 index 00000000000..b1365bcf780 --- /dev/null +++ b/chromium/device/vr/android/arcore/ar_renderer.h @@ -0,0 +1,36 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef DEVICE_VR_ANDROID_ARCORE_AR_RENDERER_H_ +#define DEVICE_VR_ANDROID_ARCORE_AR_RENDERER_H_ + +#include "base/macros.h" +#include "ui/gl/gl_bindings.h" + +namespace device { + +// Issues GL for rendering a texture for AR. +// TODO(crbug.com/838013): Share code with WebVrRenderer. +class ArRenderer { + public: + ArRenderer(); + ~ArRenderer(); + + void Draw(int texture_handle, const float (&uv_transform)[16]); + + private: + GLuint program_handle_ = 0; + GLuint position_handle_ = 0; + GLuint clip_rect_handle_ = 0; + GLuint texture_handle_ = 0; + GLuint uv_transform_ = 0; + GLuint vertex_buffer_ = 0; + GLuint index_buffer_ = 0; + + DISALLOW_COPY_AND_ASSIGN(ArRenderer); +}; + +} // namespace device + +#endif // DEVICE_VR_ANDROID_ARCORE_AR_RENDERER_H_ diff --git a/chromium/device/vr/android/arcore/arcore.cc b/chromium/device/vr/android/arcore/arcore.cc new file mode 100644 index 00000000000..714b3b4db29 --- /dev/null +++ b/chromium/device/vr/android/arcore/arcore.cc @@ -0,0 +1,28 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "device/vr/android/arcore/arcore.h" + +#include "device/vr/android/arcore/arcore_math_utils.h" + +namespace device { + +gfx::Transform ArCore::GetCameraUvFromScreenUvTransform() const { + // + // Observe how kInputCoordinatesForTransform are transformed by ArCore, + // compute a matrix based on that and post-multiply with a matrix that + // performs a Y-flip. + // + // We need to add a Y flip because ArCore's + // AR_COORDINATES_2D_TEXTURE_NORMALIZED coordinates have the origin at the top + // left to match 2D Android APIs, so it needs a Y flip to get an origin at + // bottom left as used for textures. + // The post-multiplied matrix is performing a mapping: (x, y) -> (x, 1 - y). + // + return MatrixFromTransformedPoints( + TransformDisplayUvCoords(kInputCoordinatesForTransform)) * + gfx::Transform(1, 0, 0, -1, 0, 1); +} + +} // namespace device diff --git a/chromium/device/vr/android/arcore/arcore.h b/chromium/device/vr/android/arcore/arcore.h index 787b6f18520..629ef1b8437 100644 --- a/chromium/device/vr/android/arcore/arcore.h +++ b/chromium/device/vr/android/arcore/arcore.h @@ -9,6 +9,7 @@ #include <vector> #include "base/android/scoped_java_ref.h" +#include "base/component_export.h" #include "base/macros.h" #include "base/optional.h" #include "base/time/time.h" @@ -20,22 +21,24 @@ namespace device { // This allows a real or fake implementation of ArCore to // be used as appropriate (i.e. for testing). -class ArCore { +class COMPONENT_EXPORT(VR_ARCORE) ArCore { public: virtual ~ArCore() = default; // Initializes the runtime and returns whether it was successful. // If successful, the runtime must be paused when this method returns. virtual bool Initialize( - base::android::ScopedJavaLocalRef<jobject> application_context) = 0; + base::android::ScopedJavaLocalRef<jobject> application_context, + const std::unordered_set<device::mojom::XRSessionFeature>& + enabled_features) = 0; virtual void SetDisplayGeometry( const gfx::Size& frame_size, display::Display::Rotation display_rotation) = 0; virtual void SetCameraTexture(uint32_t camera_texture_id) = 0; - // Transform the given UV coordinates by the current display rotation. - virtual std::vector<float> TransformDisplayUvCoords( - const base::span<const float> uvs) = 0; + + gfx::Transform GetCameraUvFromScreenUvTransform() const; + virtual gfx::Transform GetProjectionMatrix(float near, float far) = 0; // Update ArCore state. This call blocks for up to 1/30s while waiting for a @@ -61,6 +64,8 @@ class ArCore { // Returns information about lighting estimation. virtual mojom::XRLightEstimationDataPtr GetLightEstimationData() = 0; + virtual mojom::XRDepthDataPtr GetDepthData() = 0; + virtual bool RequestHitTest( const mojom::XRRayPtr& ray, std::vector<mojom::XRHitResultPtr>* hit_results) = 0; @@ -136,6 +141,10 @@ class ArCore { virtual void Pause() = 0; virtual void Resume() = 0; + + protected: + virtual std::vector<float> TransformDisplayUvCoords( + const base::span<const float> uvs) const = 0; }; class ArCoreFactory { diff --git a/chromium/device/vr/android/arcore/arcore_device.cc b/chromium/device/vr/android/arcore/arcore_device.cc new file mode 100644 index 00000000000..6d8f9879bc1 --- /dev/null +++ b/chromium/device/vr/android/arcore/arcore_device.cc @@ -0,0 +1,343 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "device/vr/android/arcore/arcore_device.h" + +#include <algorithm> + +#include "base/bind.h" +#include "base/numerics/math_constants.h" +#include "base/optional.h" +#include "base/task/post_task.h" +#include "base/trace_event/trace_event.h" +#include "device/vr/android/arcore/ar_image_transport.h" +#include "device/vr/android/arcore/arcore_gl.h" +#include "device/vr/android/arcore/arcore_gl_thread.h" +#include "device/vr/android/arcore/arcore_impl.h" +#include "device/vr/android/arcore/arcore_session_utils.h" +#include "device/vr/android/mailbox_to_surface_bridge.h" +#include "ui/display/display.h" + +using base::android::JavaRef; + +namespace { +constexpr float kDegreesPerRadian = 180.0f / base::kPiFloat; +} // namespace + +namespace device { + +namespace { + +mojom::VRDisplayInfoPtr CreateVRDisplayInfo(const gfx::Size& frame_size) { + mojom::VRDisplayInfoPtr device = mojom::VRDisplayInfo::New(); + device->left_eye = mojom::VREyeParameters::New(); + device->right_eye = nullptr; + mojom::VREyeParametersPtr& left_eye = device->left_eye; + left_eye->field_of_view = mojom::VRFieldOfView::New(); + // TODO(lincolnfrog): get these values for real (see gvr device). + double fov_x = 1437.387; + double fov_y = 1438.074; + // TODO(lincolnfrog): get real camera intrinsics. + int width = frame_size.width(); + int height = frame_size.height(); + float horizontal_degrees = atan(width / (2.0 * fov_x)) * kDegreesPerRadian; + float vertical_degrees = atan(height / (2.0 * fov_y)) * kDegreesPerRadian; + left_eye->field_of_view->left_degrees = horizontal_degrees; + left_eye->field_of_view->right_degrees = horizontal_degrees; + left_eye->field_of_view->up_degrees = vertical_degrees; + left_eye->field_of_view->down_degrees = vertical_degrees; + left_eye->render_width = width; + left_eye->render_height = height; + return device; +} + +} // namespace + +ArCoreDevice::SessionState::SessionState() = default; +ArCoreDevice::SessionState::~SessionState() = default; + +ArCoreDevice::ArCoreDevice( + std::unique_ptr<ArCoreFactory> arcore_factory, + std::unique_ptr<ArImageTransportFactory> ar_image_transport_factory, + std::unique_ptr<MailboxToSurfaceBridgeFactory> + mailbox_to_surface_bridge_factory, + std::unique_ptr<vr::ArCoreSessionUtils> arcore_session_utils) + : VRDeviceBase(mojom::XRDeviceId::ARCORE_DEVICE_ID), + main_thread_task_runner_(base::ThreadTaskRunnerHandle::Get()), + arcore_factory_(std::move(arcore_factory)), + ar_image_transport_factory_(std::move(ar_image_transport_factory)), + mailbox_bridge_factory_(std::move(mailbox_to_surface_bridge_factory)), + arcore_session_utils_(std::move(arcore_session_utils)), + mailbox_bridge_(mailbox_bridge_factory_->Create()), + session_state_(std::make_unique<ArCoreDevice::SessionState>()) { + // Ensure display_info_ is set to avoid crash in CallDeferredSessionCallback + // if initialization fails. Use an arbitrary but really low resolution to make + // it obvious if we're using this data instead of the actual values we get + // from the output drawing surface. + SetVRDisplayInfo(CreateVRDisplayInfo({16, 16})); +} + +ArCoreDevice::~ArCoreDevice() { + // If there's still a pending session request, reject it. + CallDeferredRequestSessionCallback(/*success=*/false); + + // Ensure that any active sessions are terminated. Terminating the GL thread + // would normally do so via its session_shutdown_callback_, but that happens + // asynchronously via CreateMainThreadCallback, and it doesn't seem safe to + // depend on all posted tasks being handled before the thread is shut down. + // Repeated EndSession calls are a no-op, so it's OK to do this redundantly. + OnSessionEnded(); + + // The GL thread must be terminated since it uses our members. For example, + // there might still be a posted Initialize() call in flight that uses + // arcore_session_utils_ and arcore_factory_. Ensure that the thread is + // stopped before other members get destructed. Don't call Stop() here, + // destruction calls Stop() and doing so twice is illegal (null pointer + // dereference). + session_state_->arcore_gl_thread_ = nullptr; +} + +void ArCoreDevice::RequestSession( + mojom::XRRuntimeSessionOptionsPtr options, + mojom::XRRuntime::RequestSessionCallback callback) { + DVLOG(1) << __func__; + DCHECK(IsOnMainThread()); + + if (HasExclusiveSession()) { + DVLOG(1) << __func__ << ": Rejecting additional session request"; + std::move(callback).Run(nullptr, mojo::NullRemote()); + return; + } + + // Set HasExclusiveSession status to true. This lasts until OnSessionEnded. + OnStartPresenting(); + + DCHECK(!session_state_->pending_request_session_callback_); + session_state_->pending_request_session_callback_ = std::move(callback); + session_state_->enabled_features_ = options->enabled_features; + + bool use_dom_overlay = base::Contains( + options->enabled_features, device::mojom::XRSessionFeature::DOM_OVERLAY); + + // mailbox_bridge_ is either supplied from the constructor, or recreated in + // OnSessionEnded(). + DCHECK(mailbox_bridge_); + + session_state_->arcore_gl_thread_ = std::make_unique<ArCoreGlThread>( + std::move(ar_image_transport_factory_), std::move(mailbox_bridge_), + CreateMainThreadCallback( + base::BindOnce(&ArCoreDevice::OnGlThreadReady, GetWeakPtr(), + options->render_process_id, options->render_frame_id, + use_dom_overlay))); + session_state_->arcore_gl_thread_->Start(); +} + +void ArCoreDevice::OnGlThreadReady(int render_process_id, + int render_frame_id, + bool use_overlay) { + auto ready_callback = + base::BindRepeating(&ArCoreDevice::OnDrawingSurfaceReady, GetWeakPtr()); + auto touch_callback = + base::BindRepeating(&ArCoreDevice::OnDrawingSurfaceTouch, GetWeakPtr()); + auto destroyed_callback = + base::BindOnce(&ArCoreDevice::OnDrawingSurfaceDestroyed, GetWeakPtr()); + + arcore_session_utils_->RequestArSession( + render_process_id, render_frame_id, use_overlay, + std::move(ready_callback), std::move(touch_callback), + std::move(destroyed_callback)); +} + +void ArCoreDevice::OnDrawingSurfaceReady(gfx::AcceleratedWidget window, + display::Display::Rotation rotation, + const gfx::Size& frame_size) { + DVLOG(1) << __func__ << ": size=" << frame_size.width() << "x" + << frame_size.height() << " rotation=" << static_cast<int>(rotation); + DCHECK(!session_state_->is_arcore_gl_initialized_); + + auto display_info = CreateVRDisplayInfo(frame_size); + SetVRDisplayInfo(std::move(display_info)); + + RequestArCoreGlInitialization(window, rotation, frame_size); +} + +void ArCoreDevice::OnDrawingSurfaceTouch(bool is_primary, + bool touching, + int32_t pointer_id, + const gfx::PointF& location) { + DVLOG(2) << __func__ << ": pointer_id=" << pointer_id + << " is_primary=" << is_primary << " touching=" << touching; + + if (!session_state_->is_arcore_gl_initialized_ || + !session_state_->arcore_gl_thread_) + return; + + PostTaskToGlThread(base::BindOnce( + &ArCoreGl::OnScreenTouch, + session_state_->arcore_gl_thread_->GetArCoreGl()->GetWeakPtr(), + is_primary, touching, pointer_id, location)); +} + +void ArCoreDevice::OnDrawingSurfaceDestroyed() { + DVLOG(1) << __func__; + + CallDeferredRequestSessionCallback(/*success=*/false); + + OnSessionEnded(); +} + +void ArCoreDevice::ShutdownSession( + mojom::XRRuntime::ShutdownSessionCallback on_completed) { + DVLOG(2) << __func__; + OnDrawingSurfaceDestroyed(); + std::move(on_completed).Run(); +} + +void ArCoreDevice::OnSessionEnded() { + DVLOG(1) << __func__; + + if (!HasExclusiveSession()) + return; + + // This may be a no-op in case session end was initiated from the Java side. + arcore_session_utils_->EndSession(); + + // The GL thread had initialized its context with a drawing_widget based on + // the ArImmersiveOverlay's Surface, and the one it has is no longer valid. + // For now, just destroy the GL thread so that it is recreated for the next + // session with fresh associated resources. Also go through these steps in + // case the GL thread hadn't completed, or had initialized partially, to + // ensure consistent state. + + // TODO(https://crbug.com/849568): Instead of splitting the initialization + // of this class between construction and RequestSession, perform all the + // initialization at once on the first successful RequestSession call. + + // Reset per-session members to initial values. + session_state_ = std::make_unique<ArCoreDevice::SessionState>(); + + // The image transport factory should be reusable, but we've std::moved it + // to the GL thread. Make a new one for next time. (This is cheap, it's + // just a factory.) + ar_image_transport_factory_ = std::make_unique<ArImageTransportFactory>(); + + // Create a new mailbox bridge for use in the next session. (This is cheap, + // the constructor doesn't establish a GL context.) + mailbox_bridge_ = mailbox_bridge_factory_->Create(); + + // This sets HasExclusiveSession status to false. + OnExitPresent(); +} + +void ArCoreDevice::CallDeferredRequestSessionCallback(bool success) { + DVLOG(1) << __func__ << " success=" << success; + DCHECK(IsOnMainThread()); + + // We might not have any pending session requests, i.e. if destroyed + // immediately after construction. + if (!session_state_->pending_request_session_callback_) + return; + + mojom::XRRuntime::RequestSessionCallback deferred_callback = + std::move(session_state_->pending_request_session_callback_); + + if (!success) { + std::move(deferred_callback).Run(nullptr, mojo::NullRemote()); + return; + } + + // Success case should only happen after GL thread is ready. + auto create_callback = + base::BindOnce(&ArCoreDevice::OnCreateSessionCallback, GetWeakPtr(), + std::move(deferred_callback)); + + auto shutdown_callback = + base::BindOnce(&ArCoreDevice::OnSessionEnded, GetWeakPtr()); + + PostTaskToGlThread(base::BindOnce( + &ArCoreGl::CreateSession, + session_state_->arcore_gl_thread_->GetArCoreGl()->GetWeakPtr(), + display_info_->Clone(), + CreateMainThreadCallback(std::move(create_callback)), + CreateMainThreadCallback(std::move(shutdown_callback)))); +} + +void ArCoreDevice::OnCreateSessionCallback( + mojom::XRRuntime::RequestSessionCallback deferred_callback, + mojo::PendingRemote<mojom::XRFrameDataProvider> frame_data_provider, + mojom::VRDisplayInfoPtr display_info, + mojo::PendingRemote<mojom::XRSessionController> session_controller, + mojom::XRPresentationConnectionPtr presentation_connection) { + DVLOG(2) << __func__; + DCHECK(IsOnMainThread()); + + mojom::XRSessionPtr session = mojom::XRSession::New(); + session->data_provider = std::move(frame_data_provider); + session->display_info = std::move(display_info); + session->submit_frame_sink = std::move(presentation_connection); + + std::move(deferred_callback) + .Run(std::move(session), std::move(session_controller)); +} + +void ArCoreDevice::PostTaskToGlThread(base::OnceClosure task) { + DCHECK(IsOnMainThread()); + session_state_->arcore_gl_thread_->GetArCoreGl() + ->GetGlThreadTaskRunner() + ->PostTask(FROM_HERE, std::move(task)); +} + +bool ArCoreDevice::IsOnMainThread() { + return main_thread_task_runner_->BelongsToCurrentThread(); +} + +void ArCoreDevice::RequestArCoreGlInitialization( + gfx::AcceleratedWidget drawing_widget, + int drawing_rotation, + const gfx::Size& frame_size) { + DVLOG(1) << __func__; + DCHECK(IsOnMainThread()); + + if (!arcore_session_utils_->EnsureLoaded()) { + DLOG(ERROR) << "ARCore was not loaded properly."; + OnArCoreGlInitializationComplete(false); + return; + } + + if (!session_state_->is_arcore_gl_initialized_) { + // We will only try to initialize ArCoreGl once, at the end of the + // permission sequence, and will resolve pending requests that have queued + // up once that initialization completes. We set is_arcore_gl_initialized_ + // in the callback to block operations that require it to be ready. + auto rotation = static_cast<display::Display::Rotation>(drawing_rotation); + PostTaskToGlThread(base::BindOnce( + &ArCoreGl::Initialize, + session_state_->arcore_gl_thread_->GetArCoreGl()->GetWeakPtr(), + arcore_session_utils_.get(), arcore_factory_.get(), drawing_widget, + frame_size, rotation, session_state_->enabled_features_, + CreateMainThreadCallback(base::BindOnce( + &ArCoreDevice::OnArCoreGlInitializationComplete, GetWeakPtr())))); + return; + } + + OnArCoreGlInitializationComplete(true); +} + +void ArCoreDevice::OnArCoreGlInitializationComplete(bool success) { + DVLOG(1) << __func__; + DCHECK(IsOnMainThread()); + + if (!success) { + CallDeferredRequestSessionCallback(/*success=*/false); + return; + } + + session_state_->is_arcore_gl_initialized_ = true; + + // We only start GL initialization after the user has granted consent, so we + // can now start the session. + CallDeferredRequestSessionCallback(/*success=*/true); +} + +} // namespace device diff --git a/chromium/device/vr/android/arcore/arcore_device.h b/chromium/device/vr/android/arcore/arcore_device.h new file mode 100644 index 00000000000..1e9858e16ef --- /dev/null +++ b/chromium/device/vr/android/arcore/arcore_device.h @@ -0,0 +1,155 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef DEVICE_VR_ANDROID_ARCORE_ARCORE_DEVICE_H_ +#define DEVICE_VR_ANDROID_ARCORE_ARCORE_DEVICE_H_ + +#include <jni.h> +#include <memory> +#include <utility> +#include <vector> + +#include "base/android/jni_android.h" +#include "base/bind.h" +#include "base/callback.h" +#include "base/macros.h" +#include "base/optional.h" +#include "device/vr/vr_device.h" +#include "device/vr/vr_device_base.h" +#include "mojo/public/cpp/bindings/pending_remote.h" +#include "ui/gfx/geometry/size_f.h" +#include "ui/gfx/native_widget_types.h" + +namespace vr { +class ArCoreSessionUtils; +} // namespace vr + +namespace device { + +class ArImageTransportFactory; +class ArCoreFactory; +class ArCoreGlThread; +class MailboxToSurfaceBridge; +class MailboxToSurfaceBridgeFactory; + +class COMPONENT_EXPORT(VR_ARCORE) ArCoreDevice : public VRDeviceBase { + public: + ArCoreDevice( + std::unique_ptr<ArCoreFactory> arcore_factory, + std::unique_ptr<ArImageTransportFactory> ar_image_transport_factory, + std::unique_ptr<MailboxToSurfaceBridgeFactory> + mailbox_to_surface_bridge_factory, + std::unique_ptr<vr::ArCoreSessionUtils> arcore_session_utils); + ~ArCoreDevice() override; + + // VRDeviceBase implementation. + void RequestSession( + mojom::XRRuntimeSessionOptionsPtr options, + mojom::XRRuntime::RequestSessionCallback callback) override; + + void ShutdownSession(mojom::XRRuntime::ShutdownSessionCallback) override; + + base::WeakPtr<ArCoreDevice> GetWeakPtr() { + return weak_ptr_factory_.GetWeakPtr(); + } + + private: + void OnDrawingSurfaceReady(gfx::AcceleratedWidget window, + display::Display::Rotation rotation, + const gfx::Size& frame_size); + void OnDrawingSurfaceTouch(bool is_primary, + bool touching, + int32_t pointer_id, + const gfx::PointF& location); + void OnDrawingSurfaceDestroyed(); + void OnSessionEnded(); + + template <typename... Args> + static void RunCallbackOnTaskRunner( + const scoped_refptr<base::TaskRunner>& task_runner, + base::OnceCallback<void(Args...)> callback, + Args... args) { + task_runner->PostTask( + FROM_HERE, + base::BindOnce(std::move(callback), std::forward<Args>(args)...)); + } + template <typename... Args> + base::OnceCallback<void(Args...)> CreateMainThreadCallback( + base::OnceCallback<void(Args...)> callback) { + return base::BindOnce(&ArCoreDevice::RunCallbackOnTaskRunner<Args...>, + main_thread_task_runner_, std::move(callback)); + } + + void PostTaskToGlThread(base::OnceClosure task); + + bool IsOnMainThread(); + + // Called once the GL thread is started. At this point, it doesn't + // have a valid GL context yet. + void OnGlThreadReady(int render_process_id, + int render_frame_id, + bool use_overlay); + + // Replies to the pending mojo RequestSession request. + void CallDeferredRequestSessionCallback(bool success); + + // Tells the GL thread to initialize a GL context and other resources, + // using the supplied window as a drawing surface. + void RequestArCoreGlInitialization(gfx::AcceleratedWidget window, + int rotation, + const gfx::Size& size); + + // Called when the GL thread's GL context initialization completes. + void OnArCoreGlInitializationComplete(bool success); + + void OnCreateSessionCallback( + mojom::XRRuntime::RequestSessionCallback deferred_callback, + mojo::PendingRemote<mojom::XRFrameDataProvider> frame_data_provider, + mojom::VRDisplayInfoPtr display_info, + mojo::PendingRemote<mojom::XRSessionController> session_controller, + mojom::XRPresentationConnectionPtr presentation_connection); + + scoped_refptr<base::SingleThreadTaskRunner> main_thread_task_runner_; + std::unique_ptr<ArCoreFactory> arcore_factory_; + std::unique_ptr<ArImageTransportFactory> ar_image_transport_factory_; + std::unique_ptr<MailboxToSurfaceBridgeFactory> mailbox_bridge_factory_; + std::unique_ptr<vr::ArCoreSessionUtils> arcore_session_utils_; + + std::unique_ptr<MailboxToSurfaceBridge> mailbox_bridge_; + + // Encapsulates data with session lifetime. + struct SessionState { + SessionState(); + ~SessionState(); + + std::unique_ptr<ArCoreGlThread> arcore_gl_thread_; + bool is_arcore_gl_initialized_ = false; + + base::OnceClosure start_immersive_activity_callback_; + + // The initial requestSession triggers the initialization sequence, store + // the callback for replying once that initialization completes. Only one + // concurrent session is supported, other requests are rejected. + mojom::XRRuntime::RequestSessionCallback pending_request_session_callback_; + + // List of features that are enabled on the session. + std::vector<device::mojom::XRSessionFeature> enabled_features_; + }; + + // This object is reset to initial values when ending a session. This helps + // ensure that each session has consistent per-session state. + std::unique_ptr<SessionState> session_state_; + + base::OnceCallback<void(bool)> + on_request_arcore_install_or_update_result_callback_; + base::OnceCallback<void(bool)> on_request_ar_module_result_callback_; + + // Must be last. + base::WeakPtrFactory<ArCoreDevice> weak_ptr_factory_{this}; + DISALLOW_COPY_AND_ASSIGN(ArCoreDevice); +}; + +} // namespace device + +#endif // DEVICE_VR_ANDROID_ARCORE_ARCORE_DEVICE_H_ diff --git a/chromium/device/vr/android/arcore/arcore_gl.cc b/chromium/device/vr/android/arcore/arcore_gl.cc new file mode 100644 index 00000000000..81beaef2471 --- /dev/null +++ b/chromium/device/vr/android/arcore/arcore_gl.cc @@ -0,0 +1,1110 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "device/vr/android/arcore/arcore_gl.h" + +#include <algorithm> +#include <iomanip> +#include <limits> +#include <utility> +#include "base/android/android_hardware_buffer_compat.h" +#include "base/android/jni_android.h" +#include "base/bind.h" +#include "base/bind_helpers.h" +#include "base/containers/queue.h" +#include "base/memory/ptr_util.h" +#include "base/metrics/histogram_macros.h" +#include "base/task/post_task.h" +#include "base/threading/thread_task_runner_handle.h" +#include "base/time/time.h" +#include "base/trace_event/trace_event.h" +#include "base/trace_event/traced_value.h" +#include "device/vr/android/arcore/ar_image_transport.h" +#include "device/vr/android/arcore/arcore.h" +#include "device/vr/android/arcore/arcore_math_utils.h" +#include "device/vr/android/arcore/arcore_session_utils.h" +#include "device/vr/android/arcore/type_converters.h" +#include "device/vr/android/web_xr_presentation_state.h" +#include "device/vr/public/mojom/pose.h" +#include "device/vr/public/mojom/vr_service.mojom.h" +#include "gpu/ipc/common/gpu_memory_buffer_impl_android_hardware_buffer.h" +#include "ui/display/display.h" +#include "ui/display/screen.h" +#include "ui/gfx/geometry/angle_conversions.h" +#include "ui/gfx/gpu_fence.h" +#include "ui/gl/android/scoped_java_surface.h" +#include "ui/gl/android/surface_texture.h" +#include "ui/gl/gl_bindings.h" +#include "ui/gl/gl_context.h" +#include "ui/gl/gl_fence_egl.h" +#include "ui/gl/gl_image_ahardwarebuffer.h" +#include "ui/gl/gl_surface.h" +#include "ui/gl/init/gl_factory.h" + +namespace { +// When scheduling the next ARCore update task, aim to have that run this much +// time ahead of when the next camera image is expected to be ready. In case +// the overall system is running slower than ideal, i.e. if the device switches +// from 30fps to 60fps, it'll catch up by this amount every frame until it +// reaches a new steady state. +constexpr base::TimeDelta kUpdateTargetDelta = + base::TimeDelta::FromMilliseconds(2); + +// Maximum delay for scheduling the next ARCore update. This helps ensure +// that there isn't an unreasonable delay due to a bogus estimate if the device +// is paused or unresponsive. +constexpr base::TimeDelta kUpdateMaxDelay = + base::TimeDelta::FromMilliseconds(30); + +const char kInputSourceProfileName[] = "generic-touchscreen"; + +const gfx::Size kDefaultFrameSize = {1, 1}; +const display::Display::Rotation kDefaultRotation = display::Display::ROTATE_0; + +} // namespace + +namespace device { + +ArCoreGl::ArCoreGl(std::unique_ptr<ArImageTransport> ar_image_transport) + : gl_thread_task_runner_(base::ThreadTaskRunnerHandle::Get()), + ar_image_transport_(std::move(ar_image_transport)), + webxr_(std::make_unique<vr::WebXrPresentationState>()) { + DVLOG(1) << __func__; +} + +ArCoreGl::~ArCoreGl() { + DVLOG(1) << __func__; + DCHECK(IsOnGlThread()); + ar_image_transport_->DestroySharedBuffers(webxr_.get()); + ar_image_transport_.reset(); + + // Make sure mojo bindings are closed before proceeding with member + // destruction. Specifically, destroying pending_getframedata_ + // must happen after closing bindings, see RunNextGetFrameData() + // comments. + CloseBindingsIfOpen(); +} + +void ArCoreGl::Initialize( + vr::ArCoreSessionUtils* session_utils, + ArCoreFactory* arcore_factory, + gfx::AcceleratedWidget drawing_widget, + const gfx::Size& frame_size, + display::Display::Rotation display_rotation, + const std::vector<device::mojom::XRSessionFeature>& enabled_features, + base::OnceCallback<void(bool)> callback) { + DVLOG(3) << __func__; + + DCHECK(IsOnGlThread()); + DCHECK(!is_initialized_); + + transfer_size_ = frame_size; + camera_image_size_ = frame_size; + display_rotation_ = display_rotation; + // TODO(https://crbug.com/953503): start using the list to control the + // behavior of local and unbounded spaces & send appropriate data back in + // GetFrameData(). + enabled_features_.insert(enabled_features.begin(), enabled_features.end()); + should_update_display_geometry_ = true; + + if (!InitializeGl(drawing_widget)) { + std::move(callback).Run(false); + return; + } + + // Get the activity context. + base::android::ScopedJavaLocalRef<jobject> application_context = + session_utils->GetApplicationContext(); + if (!application_context.obj()) { + DLOG(ERROR) << "Unable to retrieve the Java context/activity!"; + std::move(callback).Run(false); + return; + } + + arcore_ = arcore_factory->Create(); + if (!arcore_->Initialize(application_context, enabled_features_)) { + DLOG(ERROR) << "ARCore failed to initialize"; + std::move(callback).Run(false); + return; + } + + DVLOG(3) << "ar_image_transport_->Initialize()..."; + ar_image_transport_->Initialize( + webxr_.get(), + base::BindOnce(&ArCoreGl::OnArImageTransportReady, + weak_ptr_factory_.GetWeakPtr(), std::move(callback))); + + // Set the texture on ArCore to render the camera. Must be after + // ar_image_transport_->Initialize(). + arcore_->SetCameraTexture(ar_image_transport_->GetCameraTextureId()); + // Set the Geometry to ensure consistent behaviour. + arcore_->SetDisplayGeometry(kDefaultFrameSize, kDefaultRotation); +} + +void ArCoreGl::OnArImageTransportReady( + base::OnceCallback<void(bool)> callback) { + DVLOG(3) << __func__; + is_initialized_ = true; + webxr_->NotifyMailboxBridgeReady(); + std::move(callback).Run(true); +} + +void ArCoreGl::CreateSession(mojom::VRDisplayInfoPtr display_info, + ArCoreGlCreateSessionCallback create_callback, + base::OnceClosure shutdown_callback) { + DVLOG(3) << __func__; + + DCHECK(IsOnGlThread()); + DCHECK(is_initialized_); + + session_shutdown_callback_ = std::move(shutdown_callback); + + CloseBindingsIfOpen(); + + device::mojom::XRPresentationTransportOptionsPtr transport_options = + device::mojom::XRPresentationTransportOptions::New(); + transport_options->wait_for_gpu_fence = true; + + if (ar_image_transport_->UseSharedBuffer()) { + DVLOG(2) << __func__ + << ": UseSharedBuffer()=true, DRAW_INTO_TEXTURE_MAILBOX"; + transport_options->transport_method = + device::mojom::XRPresentationTransportMethod::DRAW_INTO_TEXTURE_MAILBOX; + } else { + DVLOG(2) << __func__ + << ": UseSharedBuffer()=false, SUBMIT_AS_MAILBOX_HOLDER"; + transport_options->transport_method = + device::mojom::XRPresentationTransportMethod::SUBMIT_AS_MAILBOX_HOLDER; + transport_options->wait_for_transfer_notification = true; + ar_image_transport_->SetFrameAvailableCallback(base::BindRepeating( + &ArCoreGl::OnTransportFrameAvailable, weak_ptr_factory_.GetWeakPtr())); + } + + auto submit_frame_sink = device::mojom::XRPresentationConnection::New(); + submit_frame_sink->client_receiver = + submit_client_.BindNewPipeAndPassReceiver(); + submit_frame_sink->provider = + presentation_receiver_.BindNewPipeAndPassRemote(); + submit_frame_sink->transport_options = std::move(transport_options); + + display_info_ = std::move(display_info); + + std::move(create_callback) + .Run(frame_data_receiver_.BindNewPipeAndPassRemote(), + display_info_->Clone(), + session_controller_receiver_.BindNewPipeAndPassRemote(), + std::move(submit_frame_sink)); + + frame_data_receiver_.set_disconnect_handler(base::BindOnce( + &ArCoreGl::OnBindingDisconnect, weak_ptr_factory_.GetWeakPtr())); + session_controller_receiver_.set_disconnect_handler(base::BindOnce( + &ArCoreGl::OnBindingDisconnect, weak_ptr_factory_.GetWeakPtr())); +} + +bool ArCoreGl::InitializeGl(gfx::AcceleratedWidget drawing_widget) { + DVLOG(3) << __func__; + + DCHECK(IsOnGlThread()); + DCHECK(!is_initialized_); + + if (gl::GetGLImplementation() == gl::kGLImplementationNone && + !gl::init::InitializeGLOneOff()) { + DLOG(ERROR) << "gl::init::InitializeGLOneOff failed"; + return false; + } + + scoped_refptr<gl::GLSurface> surface = + gl::init::CreateViewGLSurface(drawing_widget); + DVLOG(3) << "surface=" << surface.get(); + if (!surface.get()) { + DLOG(ERROR) << "gl::init::CreateViewGLSurface failed"; + return false; + } + + gl::GLContextAttribs context_attribs; + // When using augmented images or certain other ARCore features that involve a + // frame delay, ARCore's shared EGL context needs to be compatible with ours. + // Any mismatches result in a EGL_BAD_MATCH error, including different reset + // notification behavior according to + // https://www.khronos.org/registry/EGL/specs/eglspec.1.5.pdf page 56. + // Chromium defaults to lose context on reset when the robustness extension is + // present, even if robustness features are not requested specifically. + context_attribs.client_major_es_version = 3; + context_attribs.client_minor_es_version = 0; + context_attribs.lose_context_on_reset = false; + + scoped_refptr<gl::GLContext> context = + gl::init::CreateGLContext(nullptr, surface.get(), context_attribs); + if (!context.get()) { + DLOG(ERROR) << "gl::init::CreateGLContext failed"; + return false; + } + if (!context->MakeCurrent(surface.get())) { + DLOG(ERROR) << "gl::GLContext::MakeCurrent() failed"; + return false; + } + + // Assign the surface and context members now that initialization has + // succeeded. + surface_ = std::move(surface); + context_ = std::move(context); + + DVLOG(3) << "done"; + return true; +} + +void ArCoreGl::GetFrameData( + mojom::XRFrameDataRequestOptionsPtr options, + mojom::XRFrameDataProvider::GetFrameDataCallback callback) { + TRACE_EVENT0("gpu", __func__); + + if (webxr_->HaveAnimatingFrame()) { + DVLOG(3) << __func__ << ": deferring, HaveAnimatingFrame"; + pending_getframedata_ = + base::BindOnce(&ArCoreGl::GetFrameData, GetWeakPtr(), + std::move(options), std::move(callback)); + return; + } + + DVLOG(3) << __func__ << ": should_update_display_geometry_=" + << should_update_display_geometry_ + << ", transfer_size_=" << transfer_size_.ToString() + << ", display_rotation_=" << display_rotation_; + + DCHECK(IsOnGlThread()); + DCHECK(is_initialized_); + + if (restrict_frame_data_) { + std::move(callback).Run(nullptr); + return; + } + + if (is_paused_) { + DVLOG(2) << __func__ << ": paused but frame data not restricted. Resuming."; + Resume(); + } + + // Check if the frame_size and display_rotation updated last frame. If yes, + // apply the update for this frame. In the current implementation, this should + // only happen once per session since we don't support mid-session rotation or + // resize. + if (should_recalculate_uvs_) { + // Get the UV transform matrix from ArCore's UV transform. + uv_transform_ = arcore_->GetCameraUvFromScreenUvTransform(); + + DVLOG(3) << __func__ << ": uv_transform_=" << uv_transform_.ToString(); + + // We need near/far distances to make a projection matrix. The actual + // values don't matter, the Renderer will recalculate dependent values + // based on the application's near/far settngs. + constexpr float depth_near = 0.1f; + constexpr float depth_far = 1000.f; + projection_ = arcore_->GetProjectionMatrix(depth_near, depth_far); + auto m = projection_.matrix(); + float left = depth_near * (m.get(2, 0) - 1.f) / m.get(0, 0); + float right = depth_near * (m.get(2, 0) + 1.f) / m.get(0, 0); + float bottom = depth_near * (m.get(2, 1) - 1.f) / m.get(1, 1); + float top = depth_near * (m.get(2, 1) + 1.f) / m.get(1, 1); + + // Also calculate the inverse projection which is needed for converting + // screen touches to world rays. + bool has_inverse = projection_.GetInverse(&inverse_projection_); + DCHECK(has_inverse); + + // VRFieldOfView wants positive angles. + mojom::VRFieldOfViewPtr field_of_view = mojom::VRFieldOfView::New(); + field_of_view->left_degrees = gfx::RadToDeg(atanf(-left / depth_near)); + field_of_view->right_degrees = gfx::RadToDeg(atanf(right / depth_near)); + field_of_view->down_degrees = gfx::RadToDeg(atanf(-bottom / depth_near)); + field_of_view->up_degrees = gfx::RadToDeg(atanf(top / depth_near)); + DVLOG(3) << " fov degrees up=" << field_of_view->up_degrees + << " down=" << field_of_view->down_degrees + << " left=" << field_of_view->left_degrees + << " right=" << field_of_view->right_degrees; + + display_info_->left_eye->field_of_view = std::move(field_of_view); + display_info_changed_ = true; + + should_recalculate_uvs_ = false; + } + + // Now check if the frame_size or display_rotation needs to be updated + // for the next frame. This must happen after the should_recalculate_uvs_ + // check above to ensure it executes with the needed one-frame delay. + // The delay is needed due to the fact that ArCoreImpl already got a frame + // and we don't want to calculate uvs for stale frame with new geometry. + if (should_update_display_geometry_) { + // Set display geometry before calling Update. It's a pending request that + // applies to the next frame. + arcore_->SetDisplayGeometry(camera_image_size_, display_rotation_); + + // Tell the uvs to recalculate on the next animation frame, by which time + // SetDisplayGeometry will have set the new values in arcore_. + should_recalculate_uvs_ = true; + should_update_display_geometry_ = false; + } + + bool camera_updated = false; + base::TimeTicks arcore_update_started = base::TimeTicks::Now(); + mojom::VRPosePtr pose = arcore_->Update(&camera_updated); + base::TimeTicks now = base::TimeTicks::Now(); + base::TimeDelta frame_timestamp = arcore_->GetFrameTimestamp(); + + DVLOG(3) << __func__ << ": frame_timestamp=" << frame_timestamp; + + if (!arcore_last_frame_timestamp_.is_zero()) { + arcore_frame_interval_ = frame_timestamp - arcore_last_frame_timestamp_; + arcore_update_next_expected_ = now + arcore_frame_interval_; + } + arcore_last_frame_timestamp_ = frame_timestamp; + base::TimeDelta arcore_update_elapsed = now - arcore_update_started; + TRACE_COUNTER1("gpu", "ARCore update elapsed (ms)", + arcore_update_elapsed.InMilliseconds()); + + if (!camera_updated) { + DVLOG(1) << "arcore_->Update() failed"; + std::move(callback).Run(nullptr); + have_camera_image_ = false; + return; + } + + // First frame will be requested without a prior call to SetDisplayGeometry - + // handle this case. + if (transfer_size_.IsEmpty()) { + DLOG(ERROR) << "No valid AR frame size provided!"; + std::move(callback).Run(nullptr); + have_camera_image_ = false; + return; + } + + have_camera_image_ = true; + mojom::XRFrameDataPtr frame_data = mojom::XRFrameData::New(); + + // Check if floor height estimate has changed. + float new_floor_height_estimate = arcore_->GetEstimatedFloorHeight(); + if (!floor_height_estimate_ || + *floor_height_estimate_ != new_floor_height_estimate) { + floor_height_estimate_ = new_floor_height_estimate; + + frame_data->stage_parameters_updated = true; + frame_data->stage_parameters = mojom::VRStageParameters::New(); + frame_data->stage_parameters->mojo_from_floor = gfx::Transform(); + frame_data->stage_parameters->mojo_from_floor.Translate3d( + 0, (-1 * *floor_height_estimate_), 0); + } + + frame_data->frame_id = webxr_->StartFrameAnimating(); + DVLOG(2) << __func__ << " frame=" << frame_data->frame_id; + TRACE_EVENT1("gpu", __func__, "frame", frame_data->frame_id); + + vr::WebXrFrame* xrframe = webxr_->GetAnimatingFrame(); + xrframe->time_pose = now; + + if (display_info_changed_) { + frame_data->left_eye = display_info_->left_eye.Clone(); + display_info_changed_ = false; + } + + if (ar_image_transport_->UseSharedBuffer()) { + // Set up a shared buffer for the renderer to draw into, it'll be sent + // alongside the frame pose. + gpu::MailboxHolder buffer_holder = ar_image_transport_->TransferFrame( + webxr_.get(), transfer_size_, uv_transform_); + frame_data->buffer_holder = buffer_holder; + + if (IsFeatureEnabled(device::mojom::XRSessionFeature::CAMERA_ACCESS)) { + gpu::MailboxHolder camera_image_buffer_holder = + ar_image_transport_->TransferCameraImageFrame( + webxr_.get(), transfer_size_, uv_transform_); + frame_data->camera_image_buffer_holder = camera_image_buffer_holder; + } + } + + // Create the frame data to return to the renderer. + if (!pose) { + DVLOG(1) << __func__ << ": pose unavailable!"; + } + + frame_data->pose = std::move(pose); + frame_data->time_delta = now - base::TimeTicks(); + + fps_meter_.AddFrame(now); + TRACE_COUNTER1("gpu", "WebXR FPS", fps_meter_.GetFPS()); + + // Post a task to finish processing the frame to give a chance for + // OnScreenTouch() tasks to run and added anchors to be registered. + gl_thread_task_runner_->PostTask( + FROM_HERE, + base::BindOnce(&ArCoreGl::ProcessFrame, weak_ptr_factory_.GetWeakPtr(), + std::move(options), std::move(frame_data), + std::move(callback))); +} + +bool ArCoreGl::IsSubmitFrameExpected(int16_t frame_index) { + // submit_client_ could be null when we exit presentation, if there were + // pending SubmitFrame messages queued. XRSessionClient::OnExitPresent + // will clean up state in blink, so it doesn't wait for + // OnSubmitFrameTransferred or OnSubmitFrameRendered. Similarly, + // the animating frame state is cleared when exiting presentation, + // and we should ignore a leftover queued SubmitFrame. + if (!submit_client_.get() || !webxr_->HaveAnimatingFrame()) + return false; + + vr::WebXrFrame* animating_frame = webxr_->GetAnimatingFrame(); + animating_frame->time_js_submit = base::TimeTicks::Now(); + + if (animating_frame->index != frame_index) { + DVLOG(1) << __func__ << ": wrong frame index, got " << frame_index + << ", expected " << animating_frame->index; + mojo::ReportBadMessage("SubmitFrame called with wrong frame index"); + CloseBindingsIfOpen(); + return false; + } + + // Frame looks valid. + return true; +} + +void ArCoreGl::CopyCameraImageToFramebuffer() { + DVLOG(2) << __func__; + + // Draw the current camera texture to the output default framebuffer now, if + // available. + if (have_camera_image_) { + glBindFramebufferEXT(GL_DRAW_FRAMEBUFFER, 0); + ar_image_transport_->CopyCameraImageToFramebuffer(camera_image_size_, + uv_transform_); + have_camera_image_ = false; + } + + // We're done with the camera image for this frame, post a task to start the + // next ARCore update if we had deferred it. This will get the next frame's + // camera image and pose in parallel while we're waiting for this frame's + // rendered image. + if (pending_getframedata_) { + base::TimeDelta delay = base::TimeDelta(); + if (!arcore_update_next_expected_.is_null()) { + // Try to schedule the next ARCore update to happen a short time before + // the camera image is expected to be ready.. + delay = arcore_update_next_expected_ - base::TimeTicks::Now() - + kUpdateTargetDelta; + if (delay < base::TimeDelta()) { + // Negative sleep means we're behind schedule, run immediately. + delay = base::TimeDelta(); + } else { + if (delay > kUpdateMaxDelay) { + DVLOG(1) << __func__ << ": delay " << delay << " too long, clamp to " + << kUpdateMaxDelay; + delay = kUpdateMaxDelay; + } + } + } + TRACE_COUNTER1("gpu", "ARCore update schedule (ms)", + delay.InMilliseconds()); + // RunNextGetFrameData is needed since we must retain ownership of the mojo + // callback inside the pending_getframedata_ closure. + gl_thread_task_runner_->PostDelayedTask( + FROM_HERE, base::BindOnce(&ArCoreGl::RunNextGetFrameData, GetWeakPtr()), + delay); + } +} + +void ArCoreGl::RunNextGetFrameData() { + DVLOG(3) << __func__; + DCHECK(pending_getframedata_); + std::move(pending_getframedata_).Run(); +} + +void ArCoreGl::FinishFrame(int16_t frame_index) { + TRACE_EVENT1("gpu", __func__, "frame", frame_index); + DVLOG(3) << __func__; + surface_->SwapBuffers(base::DoNothing()); + + // If we have a rendering frame (we don't if the app didn't submit one), + // update statistics. + if (!webxr_->HaveRenderingFrame()) + return; + vr::WebXrFrame* frame = webxr_->GetRenderingFrame(); + base::TimeDelta pose_to_submit = frame->time_js_submit - frame->time_pose; + base::TimeDelta submit_to_swap = + base::TimeTicks::Now() - frame->time_js_submit; + TRACE_COUNTER2("gpu", "WebXR frame time (ms)", "javascript", + pose_to_submit.InMilliseconds(), "processing", + submit_to_swap.InMilliseconds()); +} + +void ArCoreGl::SubmitFrameMissing(int16_t frame_index, + const gpu::SyncToken& sync_token) { + TRACE_EVENT1("gpu", __func__, "frame", frame_index); + DVLOG(2) << __func__; + + if (!IsSubmitFrameExpected(frame_index)) + return; + + webxr_->RecycleUnusedAnimatingFrame(); + ar_image_transport_->WaitSyncToken(sync_token); + + CopyCameraImageToFramebuffer(); + + FinishFrame(frame_index); + DVLOG(3) << __func__ << ": frame=" << frame_index << " SwapBuffers"; +} + +void ArCoreGl::SubmitFrame(int16_t frame_index, + const gpu::MailboxHolder& mailbox, + base::TimeDelta time_waited) { + TRACE_EVENT1("gpu", __func__, "frame", frame_index); + DVLOG(2) << __func__ << ": frame=" << frame_index; + DCHECK(!ar_image_transport_->UseSharedBuffer()); + + if (!IsSubmitFrameExpected(frame_index)) + return; + + webxr_->ProcessOrDefer(base::BindOnce(&ArCoreGl::ProcessFrameFromMailbox, + weak_ptr_factory_.GetWeakPtr(), + frame_index, mailbox)); +} + +void ArCoreGl::ProcessFrameFromMailbox(int16_t frame_index, + const gpu::MailboxHolder& mailbox) { + TRACE_EVENT1("gpu", __func__, "frame", frame_index); + DVLOG(2) << __func__ << ": frame=" << frame_index; + DCHECK(webxr_->HaveProcessingFrame()); + DCHECK(!ar_image_transport_->UseSharedBuffer()); + + ar_image_transport_->CopyMailboxToSurfaceAndSwap(transfer_size_, mailbox); + // Notify the client that we're done with the mailbox so that the underlying + // image is eligible for destruction. + submit_client_->OnSubmitFrameTransferred(true); + + CopyCameraImageToFramebuffer(); + + // Now wait for ar_image_transport_ to call OnTransportFrameAvailable + // indicating that the image drawn onto the Surface is ready for consumption + // from the SurfaceTexture. +} + +void ArCoreGl::OnTransportFrameAvailable(const gfx::Transform& uv_transform) { + DVLOG(2) << __func__; + DCHECK(!ar_image_transport_->UseSharedBuffer()); + DCHECK(webxr_->HaveProcessingFrame()); + int16_t frame_index = webxr_->GetProcessingFrame()->index; + TRACE_EVENT1("gpu", __func__, "frame", frame_index); + webxr_->GetProcessingFrame()->time_copied = base::TimeTicks::Now(); + webxr_->TransitionFrameProcessingToRendering(); + + glBindFramebufferEXT(GL_DRAW_FRAMEBUFFER, 0); + ar_image_transport_->CopyDrawnImageToFramebuffer( + webxr_.get(), camera_image_size_, uv_transform); + + FinishFrame(frame_index); + + webxr_->EndFrameRendering(); + + if (submit_client_) { + // Create a local GpuFence and pass it to the Renderer via IPC. + std::unique_ptr<gl::GLFence> gl_fence = gl::GLFence::CreateForGpuFence(); + std::unique_ptr<gfx::GpuFence> gpu_fence2 = gl_fence->GetGpuFence(); + submit_client_->OnSubmitFrameGpuFence( + gpu_fence2->GetGpuFenceHandle().Clone()); + } + // We finished processing a frame, unblock a potentially waiting next frame. + webxr_->TryDeferredProcessing(); +} + +void ArCoreGl::SubmitFrameWithTextureHandle( + int16_t frame_index, + mojo::PlatformHandle texture_handle) { + NOTIMPLEMENTED(); +} + +void ArCoreGl::SubmitFrameDrawnIntoTexture(int16_t frame_index, + const gpu::SyncToken& sync_token, + base::TimeDelta time_waited) { + TRACE_EVENT1("gpu", __func__, "frame", frame_index); + DVLOG(2) << __func__ << ": frame=" << frame_index; + DCHECK(ar_image_transport_->UseSharedBuffer()); + + if (!IsSubmitFrameExpected(frame_index)) + return; + + // Start processing the frame now if possible. If there's already a current + // processing frame, defer it until that frame calls TryDeferredProcessing. + webxr_->ProcessOrDefer(base::BindOnce(&ArCoreGl::ProcessFrameDrawnIntoTexture, + weak_ptr_factory_.GetWeakPtr(), + frame_index, sync_token)); +} + +void ArCoreGl::ProcessFrameDrawnIntoTexture(int16_t frame_index, + const gpu::SyncToken& sync_token) { + TRACE_EVENT1("gpu", __func__, "frame", frame_index); + + DCHECK(webxr_->HaveProcessingFrame()); + DCHECK(ar_image_transport_->UseSharedBuffer()); + CopyCameraImageToFramebuffer(); + + ar_image_transport_->CreateGpuFenceForSyncToken( + sync_token, base::BindOnce(&ArCoreGl::OnWebXrTokenSignaled, GetWeakPtr(), + frame_index)); +} + +void ArCoreGl::OnWebXrTokenSignaled(int16_t frame_index, + std::unique_ptr<gfx::GpuFence> gpu_fence) { + TRACE_EVENT1("gpu", __func__, "frame", frame_index); + DVLOG(3) << __func__ << ": frame=" << frame_index; + + ar_image_transport_->ServerWaitForGpuFence(std::move(gpu_fence)); + + DCHECK(webxr_->HaveProcessingFrame()); + DCHECK(ar_image_transport_->UseSharedBuffer()); + webxr_->GetProcessingFrame()->time_copied = base::TimeTicks::Now(); + webxr_->TransitionFrameProcessingToRendering(); + + glBindFramebufferEXT(GL_DRAW_FRAMEBUFFER, 0); + ar_image_transport_->CopyDrawnImageToFramebuffer( + webxr_.get(), camera_image_size_, shared_buffer_transform_); + + FinishFrame(frame_index); + + webxr_->EndFrameRendering(); + + if (submit_client_) { + // Create a local GpuFence and pass it to the Renderer via IPC. + std::unique_ptr<gl::GLFence> gl_fence = gl::GLFence::CreateForGpuFence(); + std::unique_ptr<gfx::GpuFence> gpu_fence2 = gl_fence->GetGpuFence(); + submit_client_->OnSubmitFrameGpuFence( + gpu_fence2->GetGpuFenceHandle().Clone()); + } + // We finished processing a frame, unblock a potentially waiting next frame. + webxr_->TryDeferredProcessing(); +} + +void ArCoreGl::UpdateLayerBounds(int16_t frame_index, + const gfx::RectF& left_bounds, + const gfx::RectF& right_bounds, + const gfx::Size& source_size) { + DVLOG(2) << __func__ << " source_size=" << source_size.ToString(); + + transfer_size_ = source_size; +} + +void ArCoreGl::GetEnvironmentIntegrationProvider( + mojo::PendingAssociatedReceiver< + device::mojom::XREnvironmentIntegrationProvider> environment_provider) { + DVLOG(3) << __func__; + + DCHECK(IsOnGlThread()); + DCHECK(is_initialized_); + + environment_receiver_.reset(); + environment_receiver_.Bind(std::move(environment_provider)); + environment_receiver_.set_disconnect_handler(base::BindOnce( + &ArCoreGl::OnBindingDisconnect, weak_ptr_factory_.GetWeakPtr())); +} + +void ArCoreGl::SetInputSourceButtonListener( + mojo::PendingAssociatedRemote<device::mojom::XRInputSourceButtonListener>) { + // Input eventing is not supported. This call should not + // be made on this device. + mojo::ReportBadMessage("Input eventing is not supported."); +} + +void ArCoreGl::SubscribeToHitTest( + mojom::XRNativeOriginInformationPtr native_origin_information, + const std::vector<mojom::EntityTypeForHitTest>& entity_types, + mojom::XRRayPtr ray, + mojom::XREnvironmentIntegrationProvider::SubscribeToHitTestCallback + callback) { + DVLOG(2) << __func__ << ": ray origin=" << ray->origin.ToString() + << ", ray direction=" << ray->direction.ToString(); + + // Input source state information is known to ArCoreGl and not to ArCore - + // check if we recognize the input source id. + + if (native_origin_information->is_input_source_id()) { + DVLOG(1) << __func__ + << ": ARCore device supports only transient input sources for " + "now. Rejecting subscription request."; + std::move(callback).Run( + device::mojom::SubscribeToHitTestResult::FAILURE_GENERIC, 0); + return; + } + + base::Optional<uint64_t> maybe_subscription_id = arcore_->SubscribeToHitTest( + std::move(native_origin_information), entity_types, std::move(ray)); + + if (maybe_subscription_id) { + DVLOG(2) << __func__ << ": subscription_id=" << *maybe_subscription_id; + std::move(callback).Run(device::mojom::SubscribeToHitTestResult::SUCCESS, + *maybe_subscription_id); + } else { + DVLOG(1) << __func__ << ": subscription failed"; + std::move(callback).Run( + device::mojom::SubscribeToHitTestResult::FAILURE_GENERIC, 0); + } +} + +void ArCoreGl::SubscribeToHitTestForTransientInput( + const std::string& profile_name, + const std::vector<mojom::EntityTypeForHitTest>& entity_types, + mojom::XRRayPtr ray, + mojom::XREnvironmentIntegrationProvider:: + SubscribeToHitTestForTransientInputCallback callback) { + DVLOG(2) << __func__ << ": ray origin=" << ray->origin.ToString() + << ", ray direction=" << ray->direction.ToString(); + + base::Optional<uint64_t> maybe_subscription_id = + arcore_->SubscribeToHitTestForTransientInput(profile_name, entity_types, + std::move(ray)); + + if (maybe_subscription_id) { + DVLOG(2) << __func__ << ": subscription_id=" << *maybe_subscription_id; + std::move(callback).Run(device::mojom::SubscribeToHitTestResult::SUCCESS, + *maybe_subscription_id); + } else { + DVLOG(1) << __func__ << ": subscription failed"; + std::move(callback).Run( + device::mojom::SubscribeToHitTestResult::FAILURE_GENERIC, 0); + } +} + +void ArCoreGl::UnsubscribeFromHitTest(uint64_t subscription_id) { + DVLOG(2) << __func__; + + arcore_->UnsubscribeFromHitTest(subscription_id); +} + +void ArCoreGl::CreateAnchor( + mojom::XRNativeOriginInformationPtr native_origin_information, + const device::Pose& native_origin_from_anchor, + CreateAnchorCallback callback) { + DVLOG(2) << __func__; + + DCHECK(native_origin_information); + + arcore_->CreateAnchor(*native_origin_information, native_origin_from_anchor, + std::move(callback)); +} + +void ArCoreGl::CreatePlaneAnchor( + mojom::XRNativeOriginInformationPtr native_origin_information, + const device::Pose& native_origin_from_anchor, + uint64_t plane_id, + CreatePlaneAnchorCallback callback) { + DVLOG(2) << __func__ << ": plane_id=" << plane_id; + + DCHECK(native_origin_information); + DCHECK(plane_id); + + arcore_->CreatePlaneAttachedAnchor(*native_origin_information, + native_origin_from_anchor, plane_id, + std::move(callback)); +} + +void ArCoreGl::DetachAnchor(uint64_t anchor_id) { + DVLOG(2) << __func__; + + arcore_->DetachAnchor(anchor_id); +} + +void ArCoreGl::SetFrameDataRestricted(bool frame_data_restricted) { + DCHECK(IsOnGlThread()); + DCHECK(is_initialized_); + + DVLOG(3) << __func__ << ": frame_data_restricted=" << frame_data_restricted; + restrict_frame_data_ = frame_data_restricted; + if (restrict_frame_data_) { + Pause(); + } else { + Resume(); + } +} + +void ArCoreGl::ProcessFrame( + mojom::XRFrameDataRequestOptionsPtr options, + mojom::XRFrameDataPtr frame_data, + mojom::XRFrameDataProvider::GetFrameDataCallback callback) { + DVLOG(3) << __func__ << " frame=" << frame_data->frame_id << ", pose valid? " + << (frame_data->pose ? true : false); + + DCHECK(IsOnGlThread()); + DCHECK(is_initialized_); + + if (frame_data->pose) { + DCHECK(frame_data->pose->position); + DCHECK(frame_data->pose->orientation); + + frame_data->input_state = GetInputSourceStates(); + + device::Pose mojo_from_viewer(*frame_data->pose->position, + *frame_data->pose->orientation); + + // Get results for hit test subscriptions. + frame_data->hit_test_subscription_results = + arcore_->GetHitTestSubscriptionResults(mojo_from_viewer.ToTransform(), + *frame_data->input_state); + + arcore_->ProcessAnchorCreationRequests( + mojo_from_viewer.ToTransform(), *frame_data->input_state, + frame_data->time_delta + base::TimeTicks()); + } + + // Get anchors data, including anchors created this frame. + frame_data->anchors_data = arcore_->GetAnchorsData(); + + // Get planes data if it was requested. + if (IsFeatureEnabled(device::mojom::XRSessionFeature::PLANE_DETECTION)) { + frame_data->detected_planes_data = arcore_->GetDetectedPlanesData(); + } + + // Get lighting estimation data if it was requested. + if (options && options->include_lighting_estimation_data) { + frame_data->light_estimation_data = arcore_->GetLightEstimationData(); + } + + if (IsFeatureEnabled(device::mojom::XRSessionFeature::DEPTH)) { + frame_data->depth_data = arcore_->GetDepthData(); + } + + // Running this callback after resolving all the hit-test requests ensures + // that we satisfy the guarantee of the WebXR hit-test spec - that the + // hit-test promise resolves immediately prior to the frame for which it is + // valid. + std::move(callback).Run(std::move(frame_data)); +} + +void ArCoreGl::OnScreenTouch(bool is_primary, + bool touching, + int32_t pointer_id, + const gfx::PointF& touch_point) { + DVLOG(2) << __func__ << ": is_primary=" << is_primary + << ", pointer_id=" << pointer_id << ", touching=" << touching + << ", touch_point=" << touch_point.ToString(); + + if (!base::Contains(pointer_id_to_input_source_id_, pointer_id)) { + // assign ID + DCHECK(next_input_source_id_ != 0) << "ID equal to 0 cannot be used!"; + pointer_id_to_input_source_id_[pointer_id] = next_input_source_id_; + + DVLOG(3) + << __func__ + << " : pointer id not previously recognized, assigned input source id=" + << next_input_source_id_; + + // Overflow is defined behavior for unsigned integers, just make sure that + // we never send out ID = 0. + next_input_source_id_++; + if (next_input_source_id_ == 0) { + next_input_source_id_ = 1; + } + } + + uint32_t inputSourceId = pointer_id_to_input_source_id_[pointer_id]; + ScreenTouchEvent& screen_touch_event = screen_touch_events_[inputSourceId]; + + screen_touch_event.pointer_id = pointer_id; + screen_touch_event.is_primary = is_primary; + screen_touch_event.screen_last_touch = touch_point; + screen_touch_event.screen_touch_active = touching; + if (touching) { + screen_touch_event.screen_touch_pending = true; + } +} + +std::vector<mojom::XRInputSourceStatePtr> ArCoreGl::GetInputSourceStates() { + DVLOG(3) << __func__; + + std::vector<mojom::XRInputSourceStatePtr> result; + + for (auto& id_and_touch_event : screen_touch_events_) { + bool is_primary = id_and_touch_event.second.is_primary; + bool screen_touch_pending = id_and_touch_event.second.screen_touch_pending; + bool screen_touch_active = id_and_touch_event.second.screen_touch_active; + gfx::PointF screen_last_touch = id_and_touch_event.second.screen_last_touch; + + DVLOG(3) << __func__ + << " : pointer for input source id=" << id_and_touch_event.first + << ", pointer_id=" << id_and_touch_event.second.pointer_id + << ", active=" << screen_touch_active + << ", pending=" << screen_touch_pending; + + // If there's no active screen touch, and no unreported past click + // event, don't report a device. + if (!screen_touch_pending && !screen_touch_active) { + continue; + } + + device::mojom::XRInputSourceStatePtr state = + device::mojom::XRInputSourceState::New(); + + state->source_id = id_and_touch_event.first; + + state->is_auxiliary = !is_primary; + + state->primary_input_pressed = screen_touch_active; + + // If the touch is not active but pending, it means that it was clicked + // within a single frame. + if (!screen_touch_active && screen_touch_pending) { + state->primary_input_clicked = true; + + // Clear screen_touch_pending for this input source - we have consumed it. + id_and_touch_event.second.screen_touch_pending = false; + } + + // Save the touch point for use in Blink's XR input event deduplication. + state->overlay_pointer_position = screen_last_touch; + + state->description = device::mojom::XRInputSourceDescription::New(); + + state->description->handedness = device::mojom::XRHandedness::NONE; + + state->description->target_ray_mode = + device::mojom::XRTargetRayMode::TAPPING; + + state->description->profiles.push_back(kInputSourceProfileName); + + // Controller doesn't have a measured position. + state->emulated_position = true; + + // The Renderer code ignores state->grip for TAPPING (screen-based) target + // ray mode, so we don't bother filling it in here. If this does get used at + // some point in the future, this should be set to the inverse of the + // pose rigid transform. + + // Get a viewer-space ray from screen-space coordinates by applying the + // inverse of the projection matrix. Z coordinate of -1 means the point will + // be projected onto the projection matrix near plane. See also + // third_party/blink/renderer/modules/xr/xr_view.cc's UnprojectPointer. + const float x_normalized = + screen_last_touch.x() / camera_image_size_.width() * 2.f - 1.f; + const float y_normalized = + (1.f - screen_last_touch.y() / camera_image_size_.height()) * 2.f - 1.f; + gfx::Point3F touch_point(x_normalized, y_normalized, -1.f); + DVLOG(3) << __func__ << ": touch_point=" << touch_point.ToString(); + inverse_projection_.TransformPoint(&touch_point); + DVLOG(3) << __func__ << ": unprojected=" << touch_point.ToString(); + + // Ray points along -Z in ray space, so we need to flip it to get + // the +Z axis unit vector. + gfx::Vector3dF ray_backwards(-touch_point.x(), -touch_point.y(), + -touch_point.z()); + gfx::Vector3dF new_z; + bool can_normalize = ray_backwards.GetNormalized(&new_z); + DCHECK(can_normalize); + + // Complete the ray-space basis by adding X and Y unit + // vectors based on cross products. + const gfx::Vector3dF kUp(0.f, 1.f, 0.f); + gfx::Vector3dF new_x(kUp); + new_x.Cross(new_z); + new_x.GetNormalized(&new_x); + gfx::Vector3dF new_y(new_z); + new_y.Cross(new_x); + new_y.GetNormalized(&new_y); + + // Fill in the transform matrix in row-major order. The first three columns + // contain the basis vectors, the fourth column the position offset. + gfx::Transform viewer_from_pointer( + new_x.x(), new_y.x(), new_z.x(), touch_point.x(), // row 1 + new_x.y(), new_y.y(), new_z.y(), touch_point.y(), // row 2 + new_x.z(), new_y.z(), new_z.z(), touch_point.z(), // row 3 + 0, 0, 0, 1); + DVLOG(3) << __func__ << ": viewer_from_pointer=\n" + << viewer_from_pointer.ToString(); + + state->description->input_from_pointer = viewer_from_pointer; + + // Create the gamepad object and modify necessary fields. + state->gamepad = device::Gamepad{}; + state->gamepad->connected = true; + state->gamepad->id[0] = '\0'; + state->gamepad->timestamp = + base::TimeTicks::Now().since_origin().InMicroseconds(); + + state->gamepad->axes_length = 2; + state->gamepad->axes[0] = x_normalized; + state->gamepad->axes[1] = + -y_normalized; // Gamepad's Y axis is actually + // inverted (1.0 means "backward"). + + state->gamepad->buttons_length = 3; // 2 placeholders + the real one + // Default-constructed buttons are already valid placeholders. + state->gamepad->buttons[2].touched = true; + state->gamepad->buttons[2].value = 1.0; + state->gamepad->mapping = device::GamepadMapping::kNone; + state->gamepad->hand = device::GamepadHand::kNone; + + result.push_back(std::move(state)); + } + + // All the input source IDs that are no longer touching need to remain unused + // for at least one frame. For now, we always assign new ID for input source + // so there's no need to remember the IDs that have to be put on hold. Just + // clean up all the no longer touching pointers: + std::unordered_map<uint32_t, ScreenTouchEvent> still_touching_events; + for (const auto& screen_touch_event : screen_touch_events_) { + if (!screen_touch_event.second.screen_touch_active) { + // This pointer is no longer touching - remove it from the mapping, do not + // consider it as still touching: + pointer_id_to_input_source_id_.erase( + screen_touch_event.second.pointer_id); + } else { + still_touching_events.insert(screen_touch_event); + } + } + + screen_touch_events_.swap(still_touching_events); + + return result; +} + +bool ArCoreGl::IsFeatureEnabled(mojom::XRSessionFeature feature) { + return base::Contains(enabled_features_, feature); +} + +void ArCoreGl::Pause() { + DCHECK(IsOnGlThread()); + DCHECK(is_initialized_); + DVLOG(1) << __func__; + + arcore_->Pause(); + is_paused_ = true; +} + +void ArCoreGl::Resume() { + DCHECK(IsOnGlThread()); + DCHECK(is_initialized_); + DVLOG(1) << __func__; + + arcore_->Resume(); + is_paused_ = false; +} + +void ArCoreGl::OnBindingDisconnect() { + DVLOG(3) << __func__; + + CloseBindingsIfOpen(); + + std::move(session_shutdown_callback_).Run(); +} + +void ArCoreGl::CloseBindingsIfOpen() { + DVLOG(3) << __func__; + + environment_receiver_.reset(); + frame_data_receiver_.reset(); + session_controller_receiver_.reset(); + presentation_receiver_.reset(); +} + +bool ArCoreGl::IsOnGlThread() const { + return gl_thread_task_runner_->BelongsToCurrentThread(); +} + +base::WeakPtr<ArCoreGl> ArCoreGl::GetWeakPtr() { + return weak_ptr_factory_.GetWeakPtr(); +} + +} // namespace device diff --git a/chromium/device/vr/android/arcore/arcore_gl.h b/chromium/device/vr/android/arcore/arcore_gl.h new file mode 100644 index 00000000000..8d790cc3f8c --- /dev/null +++ b/chromium/device/vr/android/arcore/arcore_gl.h @@ -0,0 +1,327 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef DEVICE_VR_ANDROID_ARCORE_ARCORE_GL_H_ +#define DEVICE_VR_ANDROID_ARCORE_ARCORE_GL_H_ + +#include <memory> +#include <unordered_set> +#include <utility> +#include <vector> + +#include "base/cancelable_callback.h" +#include "base/containers/queue.h" +#include "base/macros.h" +#include "base/memory/ref_counted.h" +#include "base/memory/weak_ptr.h" +#include "base/single_thread_task_runner.h" +#include "device/vr/public/mojom/isolated_xr_service.mojom.h" +#include "device/vr/public/mojom/vr_service.mojom.h" +#include "device/vr/util/fps_meter.h" +#include "mojo/public/cpp/bindings/associated_receiver.h" +#include "mojo/public/cpp/bindings/pending_associated_receiver.h" +#include "mojo/public/cpp/bindings/pending_associated_remote.h" +#include "mojo/public/cpp/bindings/pending_remote.h" +#include "mojo/public/cpp/bindings/receiver.h" +#include "mojo/public/cpp/bindings/remote.h" +#include "ui/display/display.h" +#include "ui/gfx/geometry/point_f.h" +#include "ui/gfx/geometry/quaternion.h" +#include "ui/gfx/geometry/rect_f.h" +#include "ui/gfx/geometry/size_f.h" +#include "ui/gfx/native_widget_types.h" + +namespace gfx { +class GpuFence; +} // namespace gfx + +namespace gl { +class GLContext; +class GLSurface; +} // namespace gl + +namespace vr { +class ArCoreSessionUtils; +class WebXrPresentationState; +} // namespace vr + +namespace device { + +class ArCore; +class ArCoreFactory; +class ArImageTransport; + +using ArCoreGlCreateSessionCallback = base::OnceCallback<void( + mojo::PendingRemote<mojom::XRFrameDataProvider> frame_data_provider, + mojom::VRDisplayInfoPtr display_info, + mojo::PendingRemote<mojom::XRSessionController> session_controller, + mojom::XRPresentationConnectionPtr presentation_connection)>; + +// All of this class's methods must be called on the same valid GL thread with +// the exception of GetGlThreadTaskRunner() and GetWeakPtr(). +class ArCoreGl : public mojom::XRFrameDataProvider, + public mojom::XRPresentationProvider, + public mojom::XREnvironmentIntegrationProvider, + public mojom::XRSessionController { + public: + explicit ArCoreGl(std::unique_ptr<ArImageTransport> ar_image_transport); + ~ArCoreGl() override; + + void Initialize( + vr::ArCoreSessionUtils* session_utils, + ArCoreFactory* arcore_factory, + gfx::AcceleratedWidget drawing_widget, + const gfx::Size& frame_size, + display::Display::Rotation display_rotation, + const std::vector<device::mojom::XRSessionFeature>& enabled_features, + base::OnceCallback<void(bool)> callback); + + void CreateSession(mojom::VRDisplayInfoPtr display_info, + ArCoreGlCreateSessionCallback create_callback, + base::OnceClosure shutdown_callback); + + const scoped_refptr<base::SingleThreadTaskRunner>& GetGlThreadTaskRunner() { + return gl_thread_task_runner_; + } + + // mojom::XRFrameDataProvider + void GetFrameData(mojom::XRFrameDataRequestOptionsPtr options, + GetFrameDataCallback callback) override; + + void GetEnvironmentIntegrationProvider( + mojo::PendingAssociatedReceiver<mojom::XREnvironmentIntegrationProvider> + environment_provider) override; + void SetInputSourceButtonListener( + mojo::PendingAssociatedRemote<device::mojom::XRInputSourceButtonListener>) + override; + + // XRPresentationProvider + void SubmitFrameMissing(int16_t frame_index, const gpu::SyncToken&) override; + void SubmitFrame(int16_t frame_index, + const gpu::MailboxHolder& mailbox, + base::TimeDelta time_waited) override; + void SubmitFrameWithTextureHandle( + int16_t frame_index, + mojo::PlatformHandle texture_handle) override; + void SubmitFrameDrawnIntoTexture(int16_t frame_index, + const gpu::SyncToken&, + base::TimeDelta time_waited) override; + void UpdateLayerBounds(int16_t frame_index, + const gfx::RectF& left_bounds, + const gfx::RectF& right_bounds, + const gfx::Size& source_size) override; + + // XREnvironmentIntegrationProvider + void SubscribeToHitTest( + mojom::XRNativeOriginInformationPtr native_origin_information, + const std::vector<mojom::EntityTypeForHitTest>& entity_types, + mojom::XRRayPtr ray, + mojom::XREnvironmentIntegrationProvider::SubscribeToHitTestCallback + callback) override; + void SubscribeToHitTestForTransientInput( + const std::string& profile_name, + const std::vector<mojom::EntityTypeForHitTest>& entity_types, + mojom::XRRayPtr ray, + mojom::XREnvironmentIntegrationProvider:: + SubscribeToHitTestForTransientInputCallback callback) override; + + void UnsubscribeFromHitTest(uint64_t subscription_id) override; + + void CreateAnchor( + mojom::XRNativeOriginInformationPtr native_origin_information, + const device::Pose& native_origin_from_anchor, + CreateAnchorCallback callback) override; + void CreatePlaneAnchor( + mojom::XRNativeOriginInformationPtr native_origin_information, + const device::Pose& native_origin_from_anchor, + uint64_t plane_id, + CreatePlaneAnchorCallback callback) override; + + void DetachAnchor(uint64_t anchor_id) override; + + // mojom::XRSessionController + void SetFrameDataRestricted(bool restricted) override; + + void ProcessFrameFromMailbox(int16_t frame_index, + const gpu::MailboxHolder& mailbox); + void ProcessFrameDrawnIntoTexture(int16_t frame_index, + const gpu::SyncToken& sync_token); + void OnWebXrTokenSignaled(int16_t frame_index, + std::unique_ptr<gfx::GpuFence> gpu_fence); + + // Notifies that the screen was touched at |touch_point| using a pointer. + // |touching| will be set to true if the screen is still touched. |is_primary| + // signifies that the used pointer is considered primary. + void OnScreenTouch(bool is_primary, + bool touching, + int32_t pointer_id, + const gfx::PointF& touch_point); + std::vector<mojom::XRInputSourceStatePtr> GetInputSourceStates(); + + base::WeakPtr<ArCoreGl> GetWeakPtr(); + + private: + void Pause(); + void Resume(); + + void FinishFrame(int16_t frame_index); + bool IsSubmitFrameExpected(int16_t frame_index); + void ProcessFrame(mojom::XRFrameDataRequestOptionsPtr options, + mojom::XRFrameDataPtr frame_data, + mojom::XRFrameDataProvider::GetFrameDataCallback callback); + + bool InitializeGl(gfx::AcceleratedWidget drawing_widget); + void OnArImageTransportReady(base::OnceCallback<void(bool)> callback); + bool IsOnGlThread() const; + void CopyCameraImageToFramebuffer(); + void OnTransportFrameAvailable(const gfx::Transform& uv_transform); + + // Use a helper method to avoid storing the mojo getframedata callback + // in a closure owned by the task runner, that would lead to inconsistent + // state on session shutdown. See https://crbug.com/1065572. + void RunNextGetFrameData(); + + bool IsFeatureEnabled(mojom::XRSessionFeature feature); + + // Set of features enabled on this session. Required to correctly configure + // the session and only send out necessary data related to reference spaces to + // blink. Valid after the call to |Initialize()| method. + std::unordered_set<device::mojom::XRSessionFeature> enabled_features_; + + base::OnceClosure session_shutdown_callback_; + + scoped_refptr<gl::GLSurface> surface_; + scoped_refptr<gl::GLContext> context_; + scoped_refptr<base::SingleThreadTaskRunner> gl_thread_task_runner_; + + // Created on GL thread and should only be accessed on that thread. + std::unique_ptr<ArCore> arcore_; + std::unique_ptr<ArImageTransport> ar_image_transport_; + + // This class uses the same overall presentation state logic + // as GvrGraphicsDelegate, with some difference due to drawing + // camera images even on frames with no pose and therefore + // no blink-generated rendered image. + // + // Rough sequence is: + // + // SubmitFrame N N animating->processing + // draw camera N + // waitForToken + // GetFrameData N+1 N+1 start animating + // update ARCore N to N+1 + // OnToken N N processing->rendering + // draw rendered N + // swap N rendering done + // SubmitFrame N+1 N+1 animating->processing + // draw camera N+1 + // waitForToken + std::unique_ptr<vr::WebXrPresentationState> webxr_; + + // Default dummy values to ensure consistent behaviour. + + // Transfer size is the size of the WebGL framebuffer, this may be + // smaller than the camera image if framebufferScaleFactor is < 1.0. + gfx::Size transfer_size_ = gfx::Size(0, 0); + + // The camera image size stays locked to the screen size even if + // framebufferScaleFactor changes. + gfx::Size camera_image_size_ = gfx::Size(0, 0); + display::Display::Rotation display_rotation_ = display::Display::ROTATE_0; + bool should_update_display_geometry_ = true; + + // UV transform for drawing the camera texture, this is supplied by ARCore + // and can include 90 degree rotations or other nontrivial transforms. + gfx::Transform uv_transform_; + + // UV transform for drawing received WebGL content from a shared buffer's + // texture, this is simply an identity. + gfx::Transform shared_buffer_transform_; + + gfx::Transform projection_; + gfx::Transform inverse_projection_; + // The first run of ProduceFrame should set uv_transform_ and projection_ + // using the default settings in ArCore. + bool should_recalculate_uvs_ = true; + bool have_camera_image_ = false; + + bool is_initialized_ = false; + bool is_paused_ = true; + + bool restrict_frame_data_ = false; + + base::TimeTicks arcore_update_next_expected_; + base::TimeDelta arcore_last_frame_timestamp_; + base::TimeDelta arcore_frame_interval_; + FPSMeter fps_meter_; + + mojo::Receiver<mojom::XRFrameDataProvider> frame_data_receiver_{this}; + mojo::Receiver<mojom::XRSessionController> session_controller_receiver_{this}; + mojo::AssociatedReceiver<mojom::XREnvironmentIntegrationProvider> + environment_receiver_{this}; + + void OnBindingDisconnect(); + void CloseBindingsIfOpen(); + + mojo::Receiver<device::mojom::XRPresentationProvider> presentation_receiver_{ + this}; + mojo::Remote<device::mojom::XRPresentationClient> submit_client_; + + // This closure saves arguments for the next GetFrameData call, including a + // mojo callback. Must remain owned by ArCoreGl, don't pass it off to the task + // runner directly. See RunNextGetFrameData() comments. + base::OnceClosure pending_getframedata_; + + mojom::VRDisplayInfoPtr display_info_; + bool display_info_changed_ = false; + + // Currently estimated floor height. + base::Optional<float> floor_height_estimate_; + + // Touch-related data. + // Android will report touch events via MotionEvent - see ArImmersiveOverlay + // for details. + struct ScreenTouchEvent { + gfx::PointF screen_last_touch; + + // Screen touch start/end events get reported asynchronously. We want to + // report at least one "clicked" event even if start and end happen within a + // single frame. The "active" state corresponds to the current state and is + // updated asynchronously. The "pending" state is set to true whenever the + // screen is touched, but only gets cleared by the input source handler. + // + // active pending event + // 0 0 + // 1 1 + // 1 1 pressed=true (selectstart) + // 1 1 pressed=true + // 0 1->0 pressed=false clicked=true (selectend, click) + // + // 0 0 + // 1 1 + // 0 1 + // 0 1->0 pressed=false clicked=true (selectend, click) + float screen_touch_pending = false; + float screen_touch_active = false; + + // ID of the pointer that raised this event. + int32_t pointer_id; + bool is_primary; + }; + + // Map from input source ID to its latest information. + std::unordered_map<uint32_t, ScreenTouchEvent> screen_touch_events_; + // Map from pointer ID to input source ID currently assigned to that pointer. + std::unordered_map<int32_t, uint32_t> pointer_id_to_input_source_id_; + + uint32_t next_input_source_id_ = 1; + + // Must be last. + base::WeakPtrFactory<ArCoreGl> weak_ptr_factory_{this}; + DISALLOW_COPY_AND_ASSIGN(ArCoreGl); +}; + +} // namespace device + +#endif // DEVICE_VR_ANDROID_ARCORE_ARCORE_GL_H_ diff --git a/chromium/device/vr/android/arcore/arcore_gl_thread.cc b/chromium/device/vr/android/arcore/arcore_gl_thread.cc new file mode 100644 index 00000000000..cd04fcb65de --- /dev/null +++ b/chromium/device/vr/android/arcore/arcore_gl_thread.cc @@ -0,0 +1,46 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "device/vr/android/arcore/arcore_gl_thread.h" + +#include <utility> +#include "base/version.h" +#include "device/vr/android/arcore/ar_image_transport.h" +#include "device/vr/android/arcore/arcore_gl.h" + +namespace device { + +ArCoreGlThread::ArCoreGlThread( + std::unique_ptr<ArImageTransportFactory> ar_image_transport_factory, + std::unique_ptr<MailboxToSurfaceBridge> mailbox_bridge, + base::OnceCallback<void()> initialized_callback) + : base::android::JavaHandlerThread("ArCoreGL"), + ar_image_transport_factory_(std::move(ar_image_transport_factory)), + mailbox_bridge_(std::move(mailbox_bridge)), + initialized_callback_(std::move(initialized_callback)) { + DVLOG(3) << __func__; +} + +ArCoreGlThread::~ArCoreGlThread() { + DVLOG(3) << __func__; + Stop(); +} + +ArCoreGl* ArCoreGlThread::GetArCoreGl() { + return arcore_gl_.get(); +} + +void ArCoreGlThread::Init() { + DCHECK(!arcore_gl_); + + arcore_gl_ = std::make_unique<ArCoreGl>( + ar_image_transport_factory_->Create(std::move(mailbox_bridge_))); + std::move(initialized_callback_).Run(); +} + +void ArCoreGlThread::CleanUp() { + arcore_gl_.reset(); +} + +} // namespace device diff --git a/chromium/device/vr/android/arcore/arcore_gl_thread.h b/chromium/device/vr/android/arcore/arcore_gl_thread.h new file mode 100644 index 00000000000..a8a857836cf --- /dev/null +++ b/chromium/device/vr/android/arcore/arcore_gl_thread.h @@ -0,0 +1,47 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef DEVICE_VR_ANDROID_ARCORE_ARCORE_GL_THREAD_H_ +#define DEVICE_VR_ANDROID_ARCORE_ARCORE_GL_THREAD_H_ + +#include <memory> +#include "base/android/java_handler_thread.h" +#include "base/macros.h" +#include "base/memory/weak_ptr.h" +#include "base/single_thread_task_runner.h" +#include "device/vr/android/mailbox_to_surface_bridge.h" + +namespace device { + +class ArCoreGl; +class ArImageTransportFactory; +class MailboxToSurfaceBridge; + +class ArCoreGlThread : public base::android::JavaHandlerThread { + public: + ArCoreGlThread( + std::unique_ptr<ArImageTransportFactory> ar_image_transport_factory, + std::unique_ptr<MailboxToSurfaceBridge> mailbox_bridge, + base::OnceCallback<void()> initialized_callback); + ~ArCoreGlThread() override; + ArCoreGl* GetArCoreGl(); + + protected: + void Init() override; + void CleanUp() override; + + private: + std::unique_ptr<ArImageTransportFactory> ar_image_transport_factory_; + std::unique_ptr<MailboxToSurfaceBridge> mailbox_bridge_; + base::OnceCallback<void()> initialized_callback_; + + // Created on GL thread. + std::unique_ptr<ArCoreGl> arcore_gl_; + + DISALLOW_COPY_AND_ASSIGN(ArCoreGlThread); +}; + +} // namespace device + +#endif // DEVICE_VR_ANDROID_ARCORE_ARCORE_GL_THREAD_H_ diff --git a/chromium/device/vr/android/arcore/arcore_impl.cc b/chromium/device/vr/android/arcore/arcore_impl.cc index 88262a2c3ef..7b1d8fb1c6f 100644 --- a/chromium/device/vr/android/arcore/arcore_impl.cc +++ b/chromium/device/vr/android/arcore/arcore_impl.cc @@ -6,10 +6,13 @@ #include "base/android/jni_android.h" #include "base/bind.h" +#include "base/containers/span.h" +#include "base/numerics/checked_math.h" #include "base/numerics/math_constants.h" #include "base/optional.h" #include "base/trace_event/trace_event.h" #include "base/util/type_safety/pass_key.h" +#include "device/vr/android/arcore/arcore_math_utils.h" #include "device/vr/android/arcore/arcore_plane_manager.h" #include "device/vr/android/arcore/type_converters.h" #include "device/vr/public/mojom/pose.h" @@ -92,50 +95,59 @@ void ReleaseArCoreCubemap(ArImageCubemap* cube_map) { memset(cube_map, 0, sizeof(*cube_map)); } -void CopyArCoreImage_RGBA16F(const ArSession* session, - const ArImage* image, - int32_t plane_index, - std::vector<device::RgbaTupleF16>* out_pixels, - uint32_t* out_width, - uint32_t* out_height) { - // Get source image information - int32_t width = 0, height = 0, src_row_stride = 0, src_pixel_stride = 0; - ArImage_getWidth(session, image, &width); - ArImage_getHeight(session, image, &height); +// Helper, copies ARCore image to the passed in buffer, assuming that the caller +// allocated the buffer to fit all the data. +template <typename T> +void CopyArCoreImage(const ArSession* session, + const ArImage* image, + int32_t plane_index, + base::span<T> out_pixels, + uint32_t width, + uint32_t height) { + DVLOG(3) << __func__ << ": width=" << width << ", height=" << height + << ", out_pixels.size()=" << out_pixels.size(); + + DCHECK_GE(out_pixels.size(), width * height); + + int32_t src_row_stride = 0, src_pixel_stride = 0; ArImage_getPlaneRowStride(session, image, plane_index, &src_row_stride); ArImage_getPlanePixelStride(session, image, plane_index, &src_pixel_stride); + // Naked pointer since ArImage_getPlaneData does not transfer ownership to us. uint8_t const* src_buffer = nullptr; int32_t src_buffer_length = 0; ArImage_getPlaneData(session, image, plane_index, &src_buffer, &src_buffer_length); - // Create destination - *out_width = width; - *out_height = height; - out_pixels->resize(width * height); - // Fast path: Source and destination have the same layout - bool const fast_path = static_cast<size_t>(src_row_stride) == - width * sizeof(device::RgbaTupleF16); - TRACE_EVENT1("xr", "CopyArCoreImage_RGBA16F: memcpy", "fastPath", fast_path); + bool const fast_path = + static_cast<size_t>(src_row_stride) == width * sizeof(T); + TRACE_EVENT1("xr", "CopyArCoreImage: memcpy", "fastPath", fast_path); + + DVLOG(3) << __func__ << ": plane_index=" << plane_index + << ", src_buffer_length=" << src_buffer_length + << ", src_row_stride=" << src_row_stride + << ", src_pixel_stride=" << src_pixel_stride + << ", fast_path=" << fast_path << ", sizeof(T)=" << sizeof(T); // If they have the same layout, we can copy the entire buffer at once if (fast_path) { - DCHECK_EQ(out_pixels->size() * sizeof(device::RgbaTupleF16), - static_cast<size_t>(src_buffer_length)); - memcpy(out_pixels->data(), src_buffer, src_buffer_length); + CHECK_EQ(out_pixels.size() * sizeof(T), + static_cast<size_t>(src_buffer_length)); + memcpy(out_pixels.data(), src_buffer, src_buffer_length); return; } + CHECK_EQ(sizeof(T), static_cast<size_t>(src_pixel_stride)); + // Slow path: copy pixel by pixel, row by row - for (int32_t row = 0; row < height; ++row) { + for (uint32_t row = 0; row < height; ++row) { auto* src = src_buffer + src_row_stride * row; - auto* dest = out_pixels->data() + width * row; + auto* dest = out_pixels.data() + width * row; // For each pixel - for (int32_t x = 0; x < width; ++x) { - memcpy(dest, src, sizeof(device::RgbaTupleF16)); + for (uint32_t x = 0; x < width; ++x) { + memcpy(dest, src, sizeof(T)); src += src_pixel_stride; dest += 1; @@ -143,6 +155,30 @@ void CopyArCoreImage_RGBA16F(const ArSession* session, } } +// Helper, copies ARCore image to the passed in vector, discovering the buffer +// size and resizing the vector first. +template <typename T> +void CopyArCoreImage(const ArSession* session, + const ArImage* image, + int32_t plane_index, + std::vector<T>* out_pixels, + uint32_t* out_width, + uint32_t* out_height) { + // Get source image information + int32_t width = 0, height = 0; + ArImage_getWidth(session, image, &width); + ArImage_getHeight(session, image, &height); + + *out_width = width; + *out_height = height; + + // Allocate memory for the output. + out_pixels->resize(width * height); + + CopyArCoreImage(session, image, plane_index, base::span<T>(*out_pixels), + width, height); +} + device::mojom::XRLightProbePtr GetLightProbe( ArSession* arcore_session, ArLightEstimate* arcore_light_estimate) { @@ -230,8 +266,8 @@ device::mojom::XRReflectionProbePtr GetReflectionProbe( // Copy the cubemap uint32_t face_width = 0, face_height = 0; - CopyArCoreImage_RGBA16F(arcore_session, arcore_cube_map_face, 0, - cube_map_face, &face_width, &face_height); + CopyArCoreImage(arcore_session, arcore_cube_map_face, 0, cube_map_face, + &face_width, &face_height); // Make sure the cube map is square if (face_width != face_height) { @@ -303,7 +339,9 @@ ArCoreImpl::~ArCoreImpl() { } bool ArCoreImpl::Initialize( - base::android::ScopedJavaLocalRef<jobject> context) { + base::android::ScopedJavaLocalRef<jobject> context, + const std::unordered_set<device::mojom::XRSessionFeature>& + enabled_features) { DCHECK(IsOnGlThread()); DCHECK(!arcore_session_.is_valid()); @@ -346,6 +384,12 @@ bool ArCoreImpl::Initialize( ArConfig_setLightEstimationMode(session.get(), arcore_config.get(), AR_LIGHT_ESTIMATION_MODE_ENVIRONMENTAL_HDR); + if (base::Contains(enabled_features, + device::mojom::XRSessionFeature::DEPTH)) { + ArConfig_setDepthMode(session.get(), arcore_config.get(), + AR_DEPTH_MODE_AUTOMATIC); + } + status = ArSession_configure(session.get(), arcore_config.get()); if (status != AR_SUCCESS) { DLOG(ERROR) << "ArSession_configure failed: " << status; @@ -604,19 +648,25 @@ void ArCoreImpl::SetDisplayGeometry( } std::vector<float> ArCoreImpl::TransformDisplayUvCoords( - const base::span<const float> uvs) { + const base::span<const float> uvs) const { DCHECK(IsOnGlThread()); DCHECK(arcore_session_.is_valid()); DCHECK(arcore_frame_.is_valid()); size_t num_elements = uvs.size(); DCHECK(num_elements % 2 == 0); - std::vector<float> uvs_out(num_elements); + DCHECK_GE(num_elements, 6u); + std::vector<float> uvs_out(num_elements); ArFrame_transformCoordinates2d( arcore_session_.get(), arcore_frame_.get(), AR_COORDINATES_2D_VIEW_NORMALIZED, num_elements / 2, &uvs[0], AR_COORDINATES_2D_TEXTURE_NORMALIZED, &uvs_out[0]); + + DVLOG(3) << __func__ << ": transformed uvs=[ " << uvs_out[0] << " , " + << uvs_out[1] << " , " << uvs_out[2] << " , " << uvs_out[3] << " , " + << uvs_out[4] << " , " << uvs_out[5] << " ]"; + return uvs_out; } @@ -1412,7 +1462,96 @@ void ArCoreImpl::DetachAnchor(uint64_t anchor_id) { anchor_manager_->DetachAnchor(AnchorId(anchor_id)); } -bool ArCoreImpl::IsOnGlThread() { +mojom::XRDepthDataPtr ArCoreImpl::GetDepthData() { + DVLOG(3) << __func__; + + internal::ScopedArCoreObject<ArImage*> ar_image; + ArStatus status = ArFrame_acquireDepthImage( + arcore_session_.get(), arcore_frame_.get(), + internal::ScopedArCoreObject<ArImage*>::Receiver(ar_image).get()); + + if (status != AR_SUCCESS) { + DVLOG(2) << __func__ + << ": ArFrame_acquireDepthImage failed, status=" << status; + return nullptr; + } + + int64_t timestamp_ns; + ArImage_getTimestamp(arcore_session_.get(), ar_image.get(), ×tamp_ns); + base::TimeDelta time_delta = base::TimeDelta::FromNanoseconds(timestamp_ns); + DVLOG(3) << __func__ << ": depth image time_delta=" << time_delta; + + // The image returned from ArFrame_acquireDepthImage() is documented to have + // a single 16-bit plane at index 0. The ArImage format is documented to be + // AR_IMAGE_FORMAT_DEPTH16 (equivalent to ImageFormat.DEPTH16). There should + // be no need to validate this in non-debug builds. + // https://developers.google.com/ar/reference/c/group/ar-frame#arframe_acquiredepthimage + // https://developer.android.com/reference/android/graphics/ImageFormat#DEPTH16 + + ArImageFormat image_format; + ArImage_getFormat(arcore_session_.get(), ar_image.get(), &image_format); + + CHECK_EQ(image_format, AR_IMAGE_FORMAT_DEPTH16) + << "Depth image format must be AR_IMAGE_FORMAT_DEPTH16, found: " + << image_format; + + int32_t num_planes; + ArImage_getNumberOfPlanes(arcore_session_.get(), ar_image.get(), &num_planes); + + CHECK_EQ(num_planes, 1) << "Depth image must have 1 plane, found: " + << num_planes; + + if (time_delta > previous_depth_data_time_) { + mojom::XRDepthDataUpdatedPtr result = mojom::XRDepthDataUpdated::New(); + + result->time_delta = time_delta; + + int32_t width = 0, height = 0; + ArImage_getWidth(arcore_session_.get(), ar_image.get(), &width); + ArImage_getHeight(arcore_session_.get(), ar_image.get(), &height); + + DVLOG(3) << __func__ << ": depth image dimensions=" << width << "x" + << height; + + // Depth image is defined as a width by height array of 2-byte elements: + auto checked_buffer_size = base::CheckMul<size_t>(2, width, height); + + size_t buffer_size; + if (!checked_buffer_size.AssignIfValid(&buffer_size)) { + DVLOG(2) << __func__ + << ": overflow in 2 * width * height expression, returning null " + "depth data"; + return nullptr; + } + + mojo_base::BigBuffer pixels(buffer_size); + + // Interpret BigBuffer's data as a width by height array of uint16_t's and + // copy image data into it: + CopyArCoreImage( + arcore_session_.get(), ar_image.get(), 0, + base::span<uint16_t>(reinterpret_cast<uint16_t*>(pixels.data()), + pixels.size() / 2), + width, height); + + result->pixel_data = std::move(pixels); + // Transform needed to consume the data: + result->norm_texture_from_norm_view = GetCameraUvFromScreenUvTransform(); + result->size = gfx::Size(width, height); + + DVLOG(3) << __func__ << ": norm_texture_from_norm_view=\n" + << result->norm_texture_from_norm_view.ToString(); + + previous_depth_data_time_ = time_delta; + + return mojom::XRDepthData::NewUpdatedDepthData(std::move(result)); + } + + return mojom::XRDepthData::NewDataStillValid( + mojom::XRDepthDataStillValid::New()); +} + +bool ArCoreImpl::IsOnGlThread() const { return gl_thread_task_runner_->BelongsToCurrentThread(); } diff --git a/chromium/device/vr/android/arcore/arcore_impl.h b/chromium/device/vr/android/arcore/arcore_impl.h index c6e51ff7bde..bc6f50dd8ea 100644 --- a/chromium/device/vr/android/arcore/arcore_impl.h +++ b/chromium/device/vr/android/arcore/arcore_impl.h @@ -107,12 +107,12 @@ class ArCoreImpl : public ArCore { ~ArCoreImpl() override; bool Initialize( - base::android::ScopedJavaLocalRef<jobject> application_context) override; + base::android::ScopedJavaLocalRef<jobject> application_context, + const std::unordered_set<device::mojom::XRSessionFeature>& + enabled_features) override; void SetDisplayGeometry(const gfx::Size& frame_size, display::Display::Rotation display_rotation) override; void SetCameraTexture(uint32_t camera_texture_id) override; - std::vector<float> TransformDisplayUvCoords( - const base::span<const float> uvs) override; gfx::Transform GetProjectionMatrix(float near, float far) override; mojom::VRPosePtr Update(bool* camera_updated) override; base::TimeDelta GetFrameTimestamp() override; @@ -170,8 +170,14 @@ class ArCoreImpl : public ArCore { void DetachAnchor(uint64_t anchor_id) override; + mojom::XRDepthDataPtr GetDepthData() override; + + protected: + std::vector<float> TransformDisplayUvCoords( + const base::span<const float> uvs) const override; + private: - bool IsOnGlThread(); + bool IsOnGlThread() const; base::WeakPtr<ArCoreImpl> GetWeakPtr() { return weak_ptr_factory_.GetWeakPtr(); } @@ -203,6 +209,11 @@ class ArCoreImpl : public ArCore { std::vector<CreatePlaneAttachedAnchorRequest> create_plane_attached_anchor_requests_; + // The time delta (relative to ARCore's depth data time base) of the last + // retrieved depth API data. Used to ensure that we do not return same data to + // the renderer if there were no changes. + base::TimeDelta previous_depth_data_time_; + HitTestSubscriptionId CreateHitTestSubscriptionId(); // Returns hit test subscription results for a single subscription given diff --git a/chromium/device/vr/android/arcore/arcore_math_utils.cc b/chromium/device/vr/android/arcore/arcore_math_utils.cc new file mode 100644 index 00000000000..5fc3b8744d9 --- /dev/null +++ b/chromium/device/vr/android/arcore/arcore_math_utils.cc @@ -0,0 +1,63 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "device/vr/android/arcore/arcore_math_utils.h" + +#include "base/check_op.h" +#include "base/logging.h" + +namespace device { + +gfx::Transform MatrixFromTransformedPoints(const base::span<const float> uvs) { + DCHECK_GE(uvs.size(), 6u); + + // + // In order to compute the matrix, we need to solve the following 3 equations + // for 6 unknowns: + // + // | a b c | | u | | u' | + // | d e f | * | v | = | v' | + // | 0 0 1 | | 1 | | 1 | + // + // where 3 (u', v') pairs are passed in as an input to the method, and (u,v) + // pairs are assumed to come from kInputCoordinatesForTransform. + // + // 1. From substituting point (0, 0) for (u,v), we get: + // + // c = uvs[0] + // f = uvs[1] + // + // 2. From substituting point (1, 0) for (u,v), we get: + // + // a + c = uvs[2] -> a = uvs[2] - uvs[0] + // d + f = uvs[3] -> d = uvs[3] - uvs[1] + // + // 3. From substituting point (0, 1) for (u,v), we get: + // + // b + c = uvs[4] -> b = uvs[4] - uvs[0] + // e + f = uvs[5] -> e = uvs[5] - uvs[1] + // + + DVLOG(3) << __func__ << ": uvs=[ " << uvs[0] << " , " << uvs[1] << " , " + << uvs[2] << " , " << uvs[3] << " , " << uvs[4] << " , " << uvs[5] + << " ]"; + + // Assumes that |uvs| is the result of transforming the display coordinates + // from kInputCoordinatesForTransform - size must match. + DCHECK_EQ(uvs.size(), kInputCoordinatesForTransform.size()); + + // Transform initializes to the identity matrix and then is modified by uvs. + gfx::Transform result; + result.matrix().set(0, 0, uvs[2] - uvs[0]); + result.matrix().set(0, 1, uvs[4] - uvs[0]); + result.matrix().set(0, 3, uvs[0]); + + result.matrix().set(1, 0, uvs[3] - uvs[1]); + result.matrix().set(1, 1, uvs[5] - uvs[1]); + result.matrix().set(1, 3, uvs[1]); + + return result; +} + +} // namespace device diff --git a/chromium/device/vr/android/arcore/arcore_math_utils.h b/chromium/device/vr/android/arcore/arcore_math_utils.h new file mode 100644 index 00000000000..ca6701dae90 --- /dev/null +++ b/chromium/device/vr/android/arcore/arcore_math_utils.h @@ -0,0 +1,31 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef DEVICE_VR_ANDROID_ARCORE_ARCORE_MATH_UTILS_H_ +#define DEVICE_VR_ANDROID_ARCORE_ARCORE_MATH_UTILS_H_ + +#include <array> +#include <vector> + +#include "base/containers/span.h" +#include "ui/gfx/transform.h" + +namespace device { + +// Creates a matrix that transforms UV coordinates based on how a well known +// input was transformed. ArCore doesn't provide a way to get a matrix directly. +// There's a function to transform UV vectors individually, which can't be used +// from a shader, so we run that on selected well-known vectors +// (kDisplayCoordinatesForTransform) and recreate the matrix from the result. +gfx::Transform MatrixFromTransformedPoints(const base::span<const float> uvs); + +// Input coordinates used when computing UV transform. +// |MatrixFromTransformedPoints(uvs)| function above assumes that the |uvs| are +// the result of transforming kInputCoordinatesForTransform by some matrix. +constexpr std::array<float, 6> kInputCoordinatesForTransform = {0.f, 0.f, 1.f, + 0.f, 0.f, 1.f}; + +} // namespace device + +#endif // DEVICE_VR_ANDROID_ARCORE_ARCORE_MATH_UTILS_H_ diff --git a/chromium/device/vr/android/arcore/arcore_session_utils.h b/chromium/device/vr/android/arcore/arcore_session_utils.h new file mode 100644 index 00000000000..d3c3f4d3d03 --- /dev/null +++ b/chromium/device/vr/android/arcore/arcore_session_utils.h @@ -0,0 +1,54 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef DEVICE_VR_ANDROID_ARCORE_ARCORE_SESSION_UTILS_H_ +#define DEVICE_VR_ANDROID_ARCORE_ARCORE_SESSION_UTILS_H_ + +#include "base/android/scoped_java_ref.h" +#include "base/memory/weak_ptr.h" +#include "ui/display/display.h" +#include "ui/gfx/geometry/point_f.h" +#include "ui/gfx/geometry/size.h" +#include "ui/gfx/native_widget_types.h" + +namespace vr { + +// Immersive AR sessions use callbacks in the following sequence: +// +// RequestArSession +// SurfaceReadyCallback +// SurfaceTouchCallback (repeated for each touch) +// [exit session via "back" button, or via JS session exit] +// DestroyedCallback +// +using SurfaceReadyCallback = + base::RepeatingCallback<void(gfx::AcceleratedWidget window, + display::Display::Rotation rotation, + const gfx::Size& size)>; +using SurfaceTouchCallback = + base::RepeatingCallback<void(bool is_primary, + bool touching, + int32_t pointer_id, + const gfx::PointF& location)>; +using SurfaceDestroyedCallback = base::OnceClosure; + +class ArCoreSessionUtils { + public: + virtual ~ArCoreSessionUtils() = default; + virtual bool EnsureLoaded() = 0; + virtual base::android::ScopedJavaLocalRef<jobject> + GetApplicationContext() = 0; + virtual void RequestArSession( + int render_process_id, + int render_frame_id, + bool use_overlay, + SurfaceReadyCallback ready_callback, + SurfaceTouchCallback touch_callback, + SurfaceDestroyedCallback destroyed_callback) = 0; + virtual void EndSession() = 0; +}; + +} // namespace vr + +#endif // DEVICE_VR_ANDROID_ARCORE_ARCORE_SESSION_UTILS_H_ diff --git a/chromium/device/vr/android/arcore/scoped_arcore_objects.h b/chromium/device/vr/android/arcore/scoped_arcore_objects.h index 0d47da47890..6c8a08b7d46 100644 --- a/chromium/device/vr/android/arcore/scoped_arcore_objects.h +++ b/chromium/device/vr/android/arcore/scoped_arcore_objects.h @@ -69,6 +69,11 @@ void inline ScopedGenericArObject<ArPlane*>::Free(ArPlane* ar_plane) { } template <> +void inline ScopedGenericArObject<ArImage*>::Free(ArImage* ar_image) { + ArImage_release(ar_image); +} + +template <> void inline ScopedGenericArObject<ArAnchor*>::Free(ArAnchor* ar_anchor) { ArAnchor_release(ar_anchor); } diff --git a/chromium/device/vr/android/gvr/gvr_device.cc b/chromium/device/vr/android/gvr/gvr_device.cc index 1dff51894ef..a4e25c155f3 100644 --- a/chromium/device/vr/android/gvr/gvr_device.cc +++ b/chromium/device/vr/android/gvr/gvr_device.cc @@ -32,17 +32,6 @@ namespace device { namespace { -// Default downscale factor for computing the recommended WebXR -// render_width/render_height from the 1:1 pixel mapped size. Using a rather -// aggressive downscale due to the high overhead of copying pixels -// twice before handing off to GVR. For comparison, the polyfill -// uses approximately 0.55 on a Pixel XL. -static constexpr float kWebXrRecommendedResolutionScale = 0.7; - -// The scale factor for WebXR on devices that don't have shared buffer -// support. (Android N and earlier.) -static constexpr float kWebXrNoSharedBufferResolutionScale = 0.5; - gfx::Size GetMaximumWebVrSize(gvr::GvrApi* gvr_api) { // Get the default, unscaled size for the WebVR transfer surface // based on the optimal 1:1 render resolution. A scalar will be applied to @@ -98,14 +87,11 @@ mojom::VREyeParametersPtr CreateEyeParamater( return eye_params; } -mojom::VRDisplayInfoPtr CreateVRDisplayInfo(gvr::GvrApi* gvr_api, - mojom::XRDeviceId device_id) { +mojom::VRDisplayInfoPtr CreateVRDisplayInfo(gvr::GvrApi* gvr_api) { TRACE_EVENT0("input", "GvrDelegate::CreateVRDisplayInfo"); mojom::VRDisplayInfoPtr device = mojom::VRDisplayInfo::New(); - device->id = device_id; - gvr::BufferViewportList gvr_buffer_viewports = gvr_api->CreateEmptyBufferViewportList(); gvr_buffer_viewports.SetToRecommendedBufferViewports(); @@ -116,16 +102,6 @@ mojom::VRDisplayInfoPtr CreateVRDisplayInfo(gvr::GvrApi* gvr_api, device->right_eye = CreateEyeParamater(gvr_api, GVR_RIGHT_EYE, gvr_buffer_viewports, maximum_size); - // This scalar will be applied in the renderer to the recommended render - // target sizes. For WebVR it will always be applied, for WebXR it can be - // overridden. - if (base::AndroidHardwareBufferCompat::IsSupportAvailable()) { - device->webxr_default_framebuffer_scale = kWebXrRecommendedResolutionScale; - } else { - device->webxr_default_framebuffer_scale = - kWebXrNoSharedBufferResolutionScale; - } - return device; } @@ -270,7 +246,7 @@ GvrDelegateProvider* GvrDevice::GetGvrDelegateProvider() { void GvrDevice::OnDisplayConfigurationChanged(JNIEnv* env, const JavaRef<jobject>& obj) { DCHECK(gvr_api_); - SetVRDisplayInfo(CreateVRDisplayInfo(gvr_api_.get(), GetId())); + SetVRDisplayInfo(CreateVRDisplayInfo(gvr_api_.get())); } void GvrDevice::Init(base::OnceCallback<void(bool)> on_finished) { @@ -294,7 +270,7 @@ void GvrDevice::CreateNonPresentingContext() { jlong context = Java_NonPresentingGvrContext_getNativeGvrContext( env, non_presenting_context_); gvr_api_ = gvr::GvrApi::WrapNonOwned(reinterpret_cast<gvr_context*>(context)); - SetVRDisplayInfo(CreateVRDisplayInfo(gvr_api_.get(), GetId())); + SetVRDisplayInfo(CreateVRDisplayInfo(gvr_api_.get())); if (paused_) { PauseTracking(); diff --git a/chromium/device/vr/android/java/DEPS b/chromium/device/vr/android/java/DEPS deleted file mode 100644 index 05f395ef21d..00000000000 --- a/chromium/device/vr/android/java/DEPS +++ /dev/null @@ -1,3 +0,0 @@ -include_rules = [ - "+ui/android/java", -] diff --git a/chromium/device/vr/android/java/src/org/chromium/device/vr/NonPresentingGvrContext.java b/chromium/device/vr/android/java/src/org/chromium/device/vr/NonPresentingGvrContext.java deleted file mode 100644 index bb31aecfe74..00000000000 --- a/chromium/device/vr/android/java/src/org/chromium/device/vr/NonPresentingGvrContext.java +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package org.chromium.device.vr; - -import android.content.Context; -import android.view.Display; - -import com.google.vr.cardboard.DisplaySynchronizer; -import com.google.vr.ndk.base.GvrApi; - -import org.chromium.base.ContextUtils; -import org.chromium.base.StrictModeContext; -import org.chromium.base.annotations.CalledByNative; -import org.chromium.base.annotations.JNINamespace; -import org.chromium.base.annotations.NativeMethods; -import org.chromium.ui.display.DisplayAndroidManager; - -/** - * Creates an active GvrContext from a GvrApi created from the Application Context. This GvrContext - * cannot be used for VR rendering, and should only be used to query pose information and device - * parameters. - */ -@JNINamespace("device") -public class NonPresentingGvrContext { - private GvrApi mGvrApi; - private DisplaySynchronizer mDisplaySynchronizer; - private boolean mResumed; - - private long mNativeGvrDevice; - - private NonPresentingGvrContext(long nativeGvrDevice) { - mNativeGvrDevice = nativeGvrDevice; - Context context = ContextUtils.getApplicationContext(); - Display display = DisplayAndroidManager.getDefaultDisplayForContext(context); - - try (StrictModeContext ignored = StrictModeContext.allowDiskReads()) { - mDisplaySynchronizer = new DisplaySynchronizer(context, display) { - @Override - public void onConfigurationChanged() { - super.onConfigurationChanged(); - onDisplayConfigurationChanged(); - } - }; - } - - // Creating the GvrApi can sometimes create the Daydream config file. - try (StrictModeContext ignored = StrictModeContext.allowDiskWrites()) { - mGvrApi = new GvrApi(context, mDisplaySynchronizer); - } - resume(); - } - - @CalledByNative - private static NonPresentingGvrContext create(long nativeNonPresentingGvrContext) { - try { - return new NonPresentingGvrContext(nativeNonPresentingGvrContext); - } catch (IllegalStateException | UnsatisfiedLinkError e) { - return null; - } - } - - @CalledByNative - private long getNativeGvrContext() { - return mGvrApi.getNativeGvrContext(); - } - - @CalledByNative - private void pause() { - if (!mResumed) return; - mResumed = false; - mDisplaySynchronizer.onPause(); - } - - @CalledByNative - private void resume() { - if (mResumed) return; - mResumed = true; - mDisplaySynchronizer.onResume(); - } - - @CalledByNative - private void shutdown() { - mDisplaySynchronizer.shutdown(); - mGvrApi.shutdown(); - mNativeGvrDevice = 0; - } - - public void onDisplayConfigurationChanged() { - mGvrApi.refreshDisplayMetrics(); - if (mNativeGvrDevice != 0) { - NonPresentingGvrContextJni.get().onDisplayConfigurationChanged( - mNativeGvrDevice, NonPresentingGvrContext.this); - } - } - - @NativeMethods - interface Natives { - void onDisplayConfigurationChanged(long nativeGvrDevice, NonPresentingGvrContext caller); - } -} diff --git a/chromium/device/vr/android/mailbox_to_surface_bridge.h b/chromium/device/vr/android/mailbox_to_surface_bridge.h new file mode 100644 index 00000000000..493d696d124 --- /dev/null +++ b/chromium/device/vr/android/mailbox_to_surface_bridge.h @@ -0,0 +1,97 @@ +// Copyright 2020 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef DEVICE_VR_ANDROID_MAILBOX_TO_SURFACE_BRIDGE_H_ +#define DEVICE_VR_ANDROID_MAILBOX_TO_SURFACE_BRIDGE_H_ + +namespace gfx { +class ColorSpace; +class GpuFence; +} // namespace gfx + +namespace gl { +class SurfaceTexture; +} // namespace gl + +namespace gpu { +class GpuMemoryBufferImplAndroidHardwareBuffer; +struct MailboxHolder; +struct SyncToken; +} // namespace gpu + +namespace device { +class MailboxToSurfaceBridge { + public: + virtual ~MailboxToSurfaceBridge() {} + + // Returns true if the GPU process connection is established and ready to use. + // Equivalent to waiting for on_initialized to be called. + virtual bool IsConnected() = 0; + + // Checks if a workaround from "gpu/config/gpu_driver_bug_workaround_type.h" + // is active. Requires initialization to be complete. + virtual bool IsGpuWorkaroundEnabled(int32_t workaround) = 0; + + // This call is needed for Surface transport, in that case it must be called + // on the GL thread with a valid local native GL context. If it's not used, + // only the SharedBuffer transport methods are available. + virtual void CreateSurface(gl::SurfaceTexture*) = 0; + + // Asynchronously create the context using the surface provided by an earlier + // CreateSurface call, or an offscreen context if that wasn't called. Also + // binds the context provider to the current thread (making it the GL thread), + // and calls the callback on the GL thread. + virtual void CreateAndBindContextProvider(base::OnceClosure callback) = 0; + + // All other public methods below must be called on the GL thread + // (except when marked otherwise). + + virtual void ResizeSurface(int width, int height) = 0; + + // Returns true if swapped successfully. This can fail if the GL + // context isn't ready for use yet, in that case the caller + // won't get a new frame on the SurfaceTexture. + virtual bool CopyMailboxToSurfaceAndSwap( + const gpu::MailboxHolder& mailbox) = 0; + + virtual void GenSyncToken(gpu::SyncToken* out_sync_token) = 0; + + virtual void WaitSyncToken(const gpu::SyncToken& sync_token) = 0; + + // Copies a GpuFence from the local context to the GPU process, + // and issues a server wait for it. + virtual void WaitForClientGpuFence(gfx::GpuFence*) = 0; + + // Creates a GpuFence in the GPU process after the supplied sync_token + // completes, and copies it for use in the local context. This is + // asynchronous, the callback receives the GpuFence once it's available. + virtual void CreateGpuFence( + const gpu::SyncToken& sync_token, + base::OnceCallback<void(std::unique_ptr<gfx::GpuFence>)> callback) = 0; + + // Creates a shared image bound to |buffer|. Returns a mailbox holder that + // references the shared image with a sync token representing a point after + // the creation. Caller must call DestroySharedImage to free the shared image. + // Does not take ownership of |buffer| or retain any references to it. + virtual gpu::MailboxHolder CreateSharedImage( + gpu::GpuMemoryBufferImplAndroidHardwareBuffer* buffer, + const gfx::ColorSpace& color_space, + uint32_t usage) = 0; + + // Destroys a shared image created by CreateSharedImage. The mailbox_holder's + // sync_token must have been updated to a sync token after the last use of the + // shared image. + virtual void DestroySharedImage(const gpu::MailboxHolder& mailbox_holder) = 0; +}; + +class MailboxToSurfaceBridgeFactory { + public: + virtual ~MailboxToSurfaceBridgeFactory() {} + + virtual std::unique_ptr<device::MailboxToSurfaceBridge> Create() const = 0; +}; + +} // namespace device + +#endif // DEVICE_VR_ANDROID_MAILBOX_TO_SURFACE_BRIDGE_H_ diff --git a/chromium/device/vr/android/web_xr_presentation_state.cc b/chromium/device/vr/android/web_xr_presentation_state.cc new file mode 100644 index 00000000000..7c42a6d8571 --- /dev/null +++ b/chromium/device/vr/android/web_xr_presentation_state.cc @@ -0,0 +1,184 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "device/vr/android/web_xr_presentation_state.h" + +#include "base/logging.h" +#include "base/trace_event/trace_event.h" +#include "gpu/ipc/common/gpu_memory_buffer_impl_android_hardware_buffer.h" +#include "ui/gl/gl_fence.h" +#include "ui/gl/gl_image_egl.h" + +namespace vr { + +WebXrSharedBuffer::WebXrSharedBuffer() = default; +WebXrSharedBuffer::~WebXrSharedBuffer() = default; + +WebXrFrame::WebXrFrame() = default; + +WebXrFrame::~WebXrFrame() = default; + +bool WebXrFrame::IsValid() { + return index >= 0; +} + +void WebXrFrame::Recycle() { + DCHECK(!state_locked); + index = -1; + deferred_start_processing.Reset(); + recycle_once_unlocked = false; + gvr_handoff_fence.reset(); +} + +WebXrPresentationState::WebXrPresentationState() { + for (auto& frame : frames_storage_) { + // Create frames in "idle" state. + frame = std::make_unique<WebXrFrame>(); + idle_frames_.push(frame.get()); + } +} + +WebXrPresentationState::~WebXrPresentationState() {} + +WebXrFrame* WebXrPresentationState::GetAnimatingFrame() { + DCHECK(HaveAnimatingFrame()); + DCHECK(animating_frame_->IsValid()); + return animating_frame_; +} + +WebXrFrame* WebXrPresentationState::GetProcessingFrame() { + DCHECK(HaveProcessingFrame()); + DCHECK(processing_frame_->IsValid()); + return processing_frame_; +} + +WebXrFrame* WebXrPresentationState::GetRenderingFrame() { + DCHECK(HaveRenderingFrame()); + DCHECK(rendering_frame_->IsValid()); + return rendering_frame_; +} + +WebXrPresentationState::FrameIndexType +WebXrPresentationState::StartFrameAnimating() { + DCHECK(!HaveAnimatingFrame()); + DCHECK(!idle_frames_.empty()); + animating_frame_ = idle_frames_.front(); + idle_frames_.pop(); + animating_frame_->index = next_frame_index_++; + return animating_frame_->index; +} + +void WebXrPresentationState::TransitionFrameAnimatingToProcessing() { + DCHECK(HaveAnimatingFrame()); + DCHECK(animating_frame_->IsValid()); + DCHECK(!animating_frame_->state_locked); + DCHECK(!HaveProcessingFrame()); + processing_frame_ = animating_frame_; + animating_frame_ = nullptr; +} + +void WebXrPresentationState::RecycleUnusedAnimatingFrame() { + DCHECK(HaveAnimatingFrame()); + animating_frame_->Recycle(); + idle_frames_.push(animating_frame_); + animating_frame_ = nullptr; +} + +void WebXrPresentationState::TransitionFrameProcessingToRendering() { + DCHECK(HaveProcessingFrame()); + DCHECK(processing_frame_->IsValid()); + DCHECK(!processing_frame_->state_locked); + DCHECK(!HaveRenderingFrame()); + rendering_frame_ = processing_frame_; + processing_frame_ = nullptr; +} + +void WebXrPresentationState::EndFrameRendering() { + DCHECK(HaveRenderingFrame()); + DCHECK(rendering_frame_->IsValid()); + rendering_frame_->Recycle(); + idle_frames_.push(rendering_frame_); + rendering_frame_ = nullptr; +} + +bool WebXrPresentationState::RecycleProcessingFrameIfPossible() { + DCHECK(HaveProcessingFrame()); + bool can_cancel = !processing_frame_->state_locked; + if (can_cancel) { + processing_frame_->Recycle(); + idle_frames_.push(processing_frame_); + processing_frame_ = nullptr; + } else { + processing_frame_->recycle_once_unlocked = true; + } + return can_cancel; +} + +std::vector<std::unique_ptr<WebXrSharedBuffer>> +WebXrPresentationState::TakeSharedBuffers() { + std::vector<std::unique_ptr<WebXrSharedBuffer>> shared_buffers; + for (auto& frame : frames_storage_) { + if (frame->shared_buffer) + shared_buffers.emplace_back(std::move(frame->shared_buffer)); + if (frame->camera_image_shared_buffer) + shared_buffers.emplace_back(std::move(frame->camera_image_shared_buffer)); + } + return shared_buffers; +} + +void WebXrPresentationState::EndPresentation() { + TRACE_EVENT0("gpu", __FUNCTION__); + + if (HaveRenderingFrame()) { + rendering_frame_->Recycle(); + idle_frames_.push(rendering_frame_); + rendering_frame_ = nullptr; + } + if (HaveProcessingFrame()) { + RecycleProcessingFrameIfPossible(); + } + if (HaveAnimatingFrame()) { + RecycleUnusedAnimatingFrame(); + } + + last_ui_allows_sending_vsync = false; +} + +bool WebXrPresentationState::CanProcessFrame() const { + if (!mailbox_bridge_ready_) { + DVLOG(2) << __FUNCTION__ << ": waiting for mailbox bridge"; + return false; + } + if (processing_frame_) { + DVLOG(2) << __FUNCTION__ << ": waiting for previous processing frame"; + return false; + } + + return true; +} + +void WebXrPresentationState::ProcessOrDefer(base::OnceClosure callback) { + DCHECK(animating_frame_ && !animating_frame_->deferred_start_processing); + if (CanProcessFrame()) { + TransitionFrameAnimatingToProcessing(); + std::move(callback).Run(); + } else { + DVLOG(2) << "Deferring processing frame, not ready"; + animating_frame_->deferred_start_processing = std::move(callback); + } +} + +void WebXrPresentationState::TryDeferredProcessing() { + if (!animating_frame_ || !animating_frame_->deferred_start_processing || + !CanProcessFrame()) { + return; + } + DVLOG(2) << "Running deferred SubmitFrame"; + // Run synchronously, not via PostTask, to ensure we don't + // get a new SendVSync scheduling in between. + TransitionFrameAnimatingToProcessing(); + std::move(animating_frame_->deferred_start_processing).Run(); +} + +} // namespace vr diff --git a/chromium/device/vr/android/web_xr_presentation_state.h b/chromium/device/vr/android/web_xr_presentation_state.h new file mode 100644 index 00000000000..a13e1a1a725 --- /dev/null +++ b/chromium/device/vr/android/web_xr_presentation_state.h @@ -0,0 +1,217 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef DEVICE_VR_ANDROID_WEB_XR_PRESENTATION_STATE_H_ +#define DEVICE_VR_ANDROID_WEB_XR_PRESENTATION_STATE_H_ + +#include <memory> +#include <utility> + +#include "base/callback.h" +#include "base/containers/queue.h" +#include "base/macros.h" +#include "base/memory/ref_counted.h" +#include "gpu/command_buffer/common/mailbox_holder.h" +#include "ui/gfx/geometry/size.h" +#include "ui/gfx/transform.h" + +namespace gl { +class GLFence; +class GLImageEGL; +} // namespace gl + +namespace gpu { +class GpuMemoryBufferImplAndroidHardwareBuffer; +} // namespace gpu + +namespace vr { +// WebVR/WebXR frames go through a three-stage pipeline: Animating, Processing, +// and Rendering. There's also an Idle state used as the starting state before +// Animating and ending state after Rendering. +// +// The stages can overlap, but we enforce that there isn't more than one +// frame in a given non-Idle state at any one time. +// +// <- GetFrameData +// Idle +// SendVSync +// Animating +// <- UpdateLayerBounds (optional) +// <- GetFrameData +// <- SubmitFrame +// ProcessWebVrFrame +// Processing +// <- OnWebVrFrameAvailable +// DrawFrame +// DrawFrameSubmitWhenReady +// <= poll prev_frame_completion_fence_ +// DrawFrameSubmitNow +// Rendering +// <= prev_frame_completion_fence_ signals +// DrawFrameSubmitNow (of next frame) +// Idle +// +// Note that the frame is considered to still be in "Animating" state until +// ProcessWebVrFrame is called. If the current processing frame isn't done yet +// at the time the incoming SubmitFrame arrives, we defer ProcessWebVrFrame +// until that finishes. +// +// The renderer may call SubmitFrameMissing instead of SubmitFrame. In that +// case, the frame transitions from Animating back to Idle. +// +// <- GetFrameData +// Idle +// SendVSync +// Animating +// <- UpdateLayerBounds (optional) +// <- GetFrameData +// <- SubmitFrameMissing +// Idle + +struct WebXrSharedBuffer { + WebXrSharedBuffer(); + ~WebXrSharedBuffer(); + + gfx::Size size = {0, 0}; + + // Shared GpuMemoryBuffer + std::unique_ptr<gpu::GpuMemoryBufferImplAndroidHardwareBuffer> gmb; + + // Resources in the remote GPU process command buffer context + gpu::MailboxHolder mailbox_holder; + + // Resources in the local GL context + uint32_t local_texture = 0; + // This refptr keeps the image alive while processing a frame. That's + // required because it owns underlying resources, and must still be + // alive when the mailbox texture backed by this image is used. + scoped_refptr<gl::GLImageEGL> local_glimage; +}; + +struct WebXrFrame { + WebXrFrame(); + ~WebXrFrame(); + + bool IsValid(); + void Recycle(); + + // If true, this frame cannot change state until unlocked. Used to mark + // processing frames for the critical stage from drawing to Surface until + // they arrive in OnWebVRFrameAvailable. See also recycle_once_unlocked. + bool state_locked = false; + + // Start of elements that need to be reset on Recycle + + int16_t index = -1; + + // Set on an animating frame if it is waiting for being able to transition + // to processing state. + base::OnceClosure deferred_start_processing; + + // Set if a frame recycle failed due to being locked. The client should check + // this after unlocking it and retry recycling it at that time. + bool recycle_once_unlocked = false; + + std::unique_ptr<gl::GLFence> gvr_handoff_fence; + + // End of elements that need to be reset on Recycle + + base::TimeTicks time_pose; + base::TimeTicks time_js_submit; + base::TimeTicks time_copied; + gfx::Transform head_pose; + + // In SharedBuffer mode, keep a swap chain. + std::unique_ptr<WebXrSharedBuffer> shared_buffer; + + std::unique_ptr<WebXrSharedBuffer> camera_image_shared_buffer; + + DISALLOW_COPY_AND_ASSIGN(WebXrFrame); +}; + +class WebXrPresentationState { + public: + // WebXR frames use an arbitrary sequential ID to help catch logic errors + // involving out-of-order frames. We use an 8-bit unsigned counter, wrapping + // from 255 back to 0. Elsewhere we use -1 to indicate a non-WebXR frame, so + // most internal APIs use int16_t to ensure that they can store a full + // -1..255 value range. + using FrameIndexType = uint8_t; + + // We have at most one frame animating, one frame being processed, + // and one frame tracked after submission to GVR. + static constexpr int kWebXrFrameCount = 3; + + WebXrPresentationState(); + ~WebXrPresentationState(); + + // State transitions for normal flow + FrameIndexType StartFrameAnimating(); + void TransitionFrameAnimatingToProcessing(); + void TransitionFrameProcessingToRendering(); + void EndFrameRendering(); + + // Shuts down a presentation session. This will recycle any + // animating or rendering frame. A processing frame cannot be + // recycled if its state is locked, it will be recycled later + // once the state unlocks. + void EndPresentation(); + + // Variant transitions, if Renderer didn't call SubmitFrame, + // or if we want to discard an unwanted incoming frame. + void RecycleUnusedAnimatingFrame(); + bool RecycleProcessingFrameIfPossible(); + + void ProcessOrDefer(base::OnceClosure callback); + // Call this after state changes that could result in CanProcessFrame + // becoming true. + void TryDeferredProcessing(); + + bool HaveAnimatingFrame() const { return animating_frame_; } + WebXrFrame* GetAnimatingFrame(); + bool HaveProcessingFrame() const { return processing_frame_; } + WebXrFrame* GetProcessingFrame(); + bool HaveRenderingFrame() const { return rendering_frame_; } + WebXrFrame* GetRenderingFrame(); + + bool mailbox_bridge_ready() { return mailbox_bridge_ready_; } + void NotifyMailboxBridgeReady() { mailbox_bridge_ready_ = true; } + + // Extracts the shared buffers from all frames, resetting said frames to an + // invalid state. + // This is intended for resource cleanup, after EndPresentation was called. + std::vector<std::unique_ptr<WebXrSharedBuffer>> TakeSharedBuffers(); + + // Used by WebVrCanAnimateFrame() to detect when ui_->CanSendWebVrVSync() + // transitions from false to true, as part of starting the incoming frame + // timeout. + bool last_ui_allows_sending_vsync = false; + + // GpuMemoryBuffer creation needs a buffer ID. We don't really care about + // this, but try to keep it unique to avoid confusion. + int next_memory_buffer_id = 0; + + private: + // Checks if we're in a valid state for processing the current animating + // frame. Invalid states include mailbox_bridge_ready_ being false, or an + // already existing processing frame that's not done yet. + bool CanProcessFrame() const; + std::unique_ptr<WebXrFrame> frames_storage_[kWebXrFrameCount]; + + // Index of the next animating WebXR frame. + FrameIndexType next_frame_index_ = 0; + + WebXrFrame* animating_frame_ = nullptr; + WebXrFrame* processing_frame_ = nullptr; + WebXrFrame* rendering_frame_ = nullptr; + base::queue<WebXrFrame*> idle_frames_; + + bool mailbox_bridge_ready_ = false; + + DISALLOW_COPY_AND_ASSIGN(WebXrPresentationState); +}; + +} // namespace vr + +#endif // DEVICE_VR_ANDROID_WEB_XR_PRESENTATION_STATE_H_ diff --git a/chromium/device/vr/buildflags/BUILD.gn b/chromium/device/vr/buildflags/BUILD.gn index 91b93e56834..7fcb22389c7 100644 --- a/chromium/device/vr/buildflags/BUILD.gn +++ b/chromium/device/vr/buildflags/BUILD.gn @@ -11,7 +11,6 @@ buildflag_header("buildflags") { flags = [ "ENABLE_ARCORE=$enable_arcore", "ENABLE_OCULUS_VR=$enable_oculus_vr", - "ENABLE_OPENVR=$enable_openvr", "ENABLE_VR=$enable_vr", "ENABLE_WINDOWS_MR=$enable_windows_mr", "ENABLE_OPENXR=$enable_openxr", diff --git a/chromium/device/vr/buildflags/buildflags.gni b/chromium/device/vr/buildflags/buildflags.gni index d85ca83da71..46792daad3a 100644 --- a/chromium/device/vr/buildflags/buildflags.gni +++ b/chromium/device/vr/buildflags/buildflags.gni @@ -10,10 +10,10 @@ declare_args() { enable_gvr_services = is_android && !is_chromecast && (current_cpu == "arm" || current_cpu == "arm64") - enable_openvr = is_win - enable_windows_mr = is_win + use_command_buffer = is_win + # To build with OpenXR support, the OpenXR Loader needs to be pulled to # third_party/openxr. enable_openxr = checkout_openxr && is_win @@ -28,8 +28,8 @@ declare_args() { # Enable VR device support whenever VR device SDK(s) are supported. # We enable VR on Linux even though VR features aren't usable because # the binary size impact is small and allows many VR tests to run on Linux - enable_vr = enable_gvr_services || enable_openvr || enable_oculus_vr || - enable_windows_mr || enable_openxr || + enable_vr = enable_gvr_services || enable_oculus_vr || enable_windows_mr || + enable_openxr || (is_desktop_linux && (current_cpu == "x64" || current_cpu == "x86") && !is_chromecast) diff --git a/chromium/device/vr/gl_bindings.h b/chromium/device/vr/gl_bindings.h new file mode 100644 index 00000000000..b727fdd4149 --- /dev/null +++ b/chromium/device/vr/gl_bindings.h @@ -0,0 +1,26 @@ +// Copyright 2018 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef DEVICE_VR_GL_BINDINGS_H_ +#define DEVICE_VR_GL_BINDINGS_H_ + +#if defined(VR_USE_COMMAND_BUFFER) + +#include <GLES2/gl2.h> +#include <GLES2/gl2ext.h> + +#elif defined(VR_USE_NATIVE_GL) + +#include "ui/gl/gl_bindings.h" // nogncheck + +// The above header still uses the ARB prefix for the following GL API call. +#define glGenBuffers glGenBuffersARB + +#else + +#error "Missing configuration for GL mode." + +#endif // defined(VR_USE_COMMAND_BUFFER) + +#endif // DEVICE_VR_GL_BINDINGS_H_ diff --git a/chromium/device/vr/oculus/oculus_device.cc b/chromium/device/vr/oculus/oculus_device.cc index 1aa88489ece..252c26ace02 100644 --- a/chromium/device/vr/oculus/oculus_device.cc +++ b/chromium/device/vr/oculus/oculus_device.cc @@ -59,10 +59,8 @@ mojom::VREyeParametersPtr GetEyeDetails(ovrSession session, return eye_parameters; } -mojom::VRDisplayInfoPtr CreateVRDisplayInfo(mojom::XRDeviceId id, - ovrSession session) { +mojom::VRDisplayInfoPtr CreateVRDisplayInfo(ovrSession session) { mojom::VRDisplayInfoPtr display_info = mojom::VRDisplayInfo::New(); - display_info->id = id; ovrHmdDesc hmdDesc = ovr_GetHmdDesc(session); display_info->left_eye = GetEyeDetails(session, hmdDesc, ovrEye_Left); @@ -174,7 +172,7 @@ bool OculusDevice::EnsureValidDisplayInfo() { return false; } - SetVRDisplayInfo(CreateVRDisplayInfo(GetId(), session_)); + SetVRDisplayInfo(CreateVRDisplayInfo(session_)); have_real_display_info_ = true; } return have_real_display_info_; diff --git a/chromium/device/vr/openvr/OWNERS b/chromium/device/vr/openvr/OWNERS deleted file mode 100644 index 5b1397a961f..00000000000 --- a/chromium/device/vr/openvr/OWNERS +++ /dev/null @@ -1,5 +0,0 @@ -per-file *_type_converter*.*=set noparent -per-file *_type_converter*.*=file://ipc/SECURITY_OWNERS - -# TEAM: xr-dev@chromium.org -# COMPONENT: Internals>XR>VR diff --git a/chromium/device/vr/openvr/openvr_api_wrapper.cc b/chromium/device/vr/openvr/openvr_api_wrapper.cc deleted file mode 100644 index 9a48eeb3225..00000000000 --- a/chromium/device/vr/openvr/openvr_api_wrapper.cc +++ /dev/null @@ -1,116 +0,0 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#include "device/vr/openvr/openvr_api_wrapper.h" - -#include "base/single_thread_task_runner.h" -#include "base/threading/thread_task_runner_handle.h" -#include "device/vr/test/test_hook.h" - -namespace device { - -OpenVRWrapper::OpenVRWrapper(bool for_rendering) { - initialized_ = Initialize(for_rendering); -} - -OpenVRWrapper::~OpenVRWrapper() { - if (initialized_) - Uninitialize(); -} - -vr::IVRCompositor* OpenVRWrapper::GetCompositor() { - DCHECK(current_task_runner_->BelongsToCurrentThread()); - return compositor_; -} - -vr::IVRSystem* OpenVRWrapper::GetSystem() { - DCHECK(current_task_runner_->BelongsToCurrentThread()); - return system_; -} - -void OpenVRWrapper::SetTestHook(VRTestHook* hook) { - // This may be called from any thread - tests are responsible for - // maintaining thread safety, typically by not changing the test hook - // while presenting. - test_hook_ = hook; - if (service_test_hook_) { - service_test_hook_->SetTestHook(test_hook_); - } -} - -bool OpenVRWrapper::Initialize(bool for_rendering) { - DCHECK(!any_initialized_); - any_initialized_ = true; - - // device can only be used on this thread once initailized - vr::EVRInitError init_error = vr::VRInitError_None; - system_ = - vr::VR_Init(&init_error, vr::EVRApplicationType::VRApplication_Scene); - - if (init_error != vr::VRInitError_None) { - LOG(ERROR) << vr::VR_GetVRInitErrorAsEnglishDescription(init_error); - any_initialized_ = false; - return false; - } - - current_task_runner_ = base::ThreadTaskRunnerHandle::Get(); - - if (for_rendering) { - compositor_ = vr::VRCompositor(); - } - - if (test_hook_) { - // Allow our mock implementation of OpenVR to be controlled by tests. - // Note that SetTestHook must be called before CreateDevice, or - // service_test_hook_s will remain null. This is a good pattern for - // tests anyway, since the alternative is we start mocking part-way through - // using the device, and end up with race conditions for when we started - // controlling things. - vr::EVRInitError eError; - service_test_hook_ = static_cast<ServiceTestHook*>( - vr::VR_GetGenericInterface(kChromeOpenVRTestHookAPI, &eError)); - if (service_test_hook_) { - service_test_hook_->SetTestHook(test_hook_); - test_hook_->AttachCurrentThread(); - } - } - - return true; -} - -void OpenVRWrapper::Uninitialize() { - DCHECK(initialized_); - initialized_ = false; - system_ = nullptr; - compositor_ = nullptr; - service_test_hook_ = nullptr; - current_task_runner_ = nullptr; - if (test_hook_) - test_hook_->DetachCurrentThread(); - vr::VR_Shutdown(); - - any_initialized_ = false; -} - -VRTestHook* OpenVRWrapper::test_hook_ = nullptr; -bool OpenVRWrapper::any_initialized_ = false; -ServiceTestHook* OpenVRWrapper::service_test_hook_ = nullptr; - -std::string GetOpenVRString(vr::IVRSystem* vr_system, - vr::TrackedDeviceProperty prop, - uint32_t device_index) { - std::string out; - - vr::TrackedPropertyError error = vr::TrackedProp_Success; - char openvr_string[vr::k_unMaxPropertyStringSize]; - vr_system->GetStringTrackedDeviceProperty( - device_index, prop, openvr_string, vr::k_unMaxPropertyStringSize, &error); - - if (error == vr::TrackedProp_Success) - out = openvr_string; - - return out; -} - -} // namespace device diff --git a/chromium/device/vr/openvr/openvr_api_wrapper.h b/chromium/device/vr/openvr/openvr_api_wrapper.h deleted file mode 100644 index 8fb60ec0b39..00000000000 --- a/chromium/device/vr/openvr/openvr_api_wrapper.h +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#ifndef DEVICE_VR_OPENVR_OPENVR_API_WRAPPER_H_ -#define DEVICE_VR_OPENVR_OPENVR_API_WRAPPER_H_ - -#include "base/memory/scoped_refptr.h" -#include "device/vr/vr_export.h" -#include "third_party/openvr/src/headers/openvr.h" - -namespace base { -class SingleThreadTaskRunner; -} - -namespace device { -class VRTestHook; -class ServiceTestHook; - -class OpenVRWrapper { - public: - OpenVRWrapper(bool for_rendering); - ~OpenVRWrapper(); - - bool IsInitialized() { return initialized_; } - - // Gets the OpenVR API objects. - // Ensures that they are used in a single thread at a time. - vr::IVRCompositor* GetCompositor(); - vr::IVRSystem* GetSystem(); - - static void DEVICE_VR_EXPORT SetTestHook(VRTestHook* hook); - - private: - bool Initialize(bool for_rendering); - void Uninitialize(); - - vr::IVRSystem* system_ = nullptr; - vr::IVRCompositor* compositor_ = nullptr; - scoped_refptr<base::SingleThreadTaskRunner> current_task_runner_; - bool initialized_ = false; - - static ServiceTestHook* service_test_hook_; - static VRTestHook* test_hook_; - static bool any_initialized_; -}; - -std::string GetOpenVRString( - vr::IVRSystem* vr_system, - vr::TrackedDeviceProperty prop, - uint32_t device_index = vr::k_unTrackedDeviceIndex_Hmd); - -} // namespace device - -#endif // DEVICE_VR_OPENVR_OPENVR_API_WRAPPER_H_ diff --git a/chromium/device/vr/openvr/openvr_device.cc b/chromium/device/vr/openvr/openvr_device.cc deleted file mode 100644 index 71fd647f2b1..00000000000 --- a/chromium/device/vr/openvr/openvr_device.cc +++ /dev/null @@ -1,310 +0,0 @@ -// Copyright (c) 2017 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#include "device/vr/openvr/openvr_device.h" - -#include <math.h> - -#include <utility> - -#include "base/bind.h" -#include "base/bind_helpers.h" -#include "base/macros.h" -#include "base/memory/ptr_util.h" -#include "base/numerics/math_constants.h" -#include "build/build_config.h" -#include "device/vr/openvr/openvr_render_loop.h" -#include "device/vr/openvr/openvr_type_converters.h" -#include "device/vr/util/stage_utils.h" -#include "mojo/public/cpp/bindings/pending_remote.h" -#include "third_party/openvr/src/headers/openvr.h" -#include "ui/gfx/geometry/angle_conversions.h" - -namespace device { - -namespace { - -constexpr base::TimeDelta kPollingInterval = - base::TimeDelta::FromSecondsD(0.25); - -mojom::VRFieldOfViewPtr OpenVRFovToWebVRFov(vr::IVRSystem* vr_system, - vr::Hmd_Eye eye) { - auto out = mojom::VRFieldOfView::New(); - float up_tan, down_tan, left_tan, right_tan; - vr_system->GetProjectionRaw(eye, &left_tan, &right_tan, &up_tan, &down_tan); - - // TODO(billorr): Plumb the expected projection matrix over mojo instead of - // using angles. Up and down are intentionally swapped to account for - // differences in expected projection matrix format for GVR and OpenVR. - out->up_degrees = gfx::RadToDeg(atanf(down_tan)); - out->down_degrees = -gfx::RadToDeg(atanf(up_tan)); - out->left_degrees = -gfx::RadToDeg(atanf(left_tan)); - out->right_degrees = gfx::RadToDeg(atanf(right_tan)); - return out; -} - -gfx::Transform HmdMatrix34ToTransform(const vr::HmdMatrix34_t& mat) { - // Disable formatting so that the 4x4 matrix is more readable - // clang-format off - return gfx::Transform( - mat.m[0][0], mat.m[0][1], mat.m[0][2], mat.m[0][3], - mat.m[1][0], mat.m[1][1], mat.m[1][2], mat.m[1][3], - mat.m[2][0], mat.m[2][1], mat.m[2][2], mat.m[2][3], - 0.0f, 0.0f, 0.0f, 1.0f); - // clang-format on -} - -// OpenVR uses A_to_B convention for naming transformation matrices, but we pass -// matrices through mojo using the B_from_A naming convention since that what -// blink uses. -gfx::Transform HeadFromEyeTransform(vr::IVRSystem* vr_system, vr::Hmd_Eye eye) { - return HmdMatrix34ToTransform(vr_system->GetEyeToHeadTransform(eye)); -} - -mojom::VRDisplayInfoPtr CreateVRDisplayInfo(vr::IVRSystem* vr_system, - device::mojom::XRDeviceId id) { - mojom::VRDisplayInfoPtr display_info = mojom::VRDisplayInfo::New(); - display_info->id = id; - - display_info->left_eye = mojom::VREyeParameters::New(); - display_info->right_eye = mojom::VREyeParameters::New(); - mojom::VREyeParametersPtr& left_eye = display_info->left_eye; - mojom::VREyeParametersPtr& right_eye = display_info->right_eye; - - left_eye->field_of_view = OpenVRFovToWebVRFov(vr_system, vr::Eye_Left); - right_eye->field_of_view = OpenVRFovToWebVRFov(vr_system, vr::Eye_Right); - - left_eye->head_from_eye = HeadFromEyeTransform(vr_system, vr::Eye_Left); - right_eye->head_from_eye = HeadFromEyeTransform(vr_system, vr::Eye_Right); - - uint32_t width, height; - vr_system->GetRecommendedRenderTargetSize(&width, &height); - left_eye->render_width = width; - left_eye->render_height = height; - right_eye->render_width = left_eye->render_width; - right_eye->render_height = left_eye->render_height; - - display_info->stage_parameters = mojom::VRStageParameters::New(); - vr::HmdMatrix34_t mat = - vr_system->GetSeatedZeroPoseToStandingAbsoluteTrackingPose(); - gfx::Transform floor_from_mojo = HmdMatrix34ToTransform(mat); - display_info->stage_parameters->mojo_from_floor = gfx::Transform(); - bool succeeded = floor_from_mojo.GetInverse( - &display_info->stage_parameters->mojo_from_floor); - DCHECK(succeeded); - - vr::IVRChaperone* chaperone = vr::VRChaperone(); - if (chaperone) { - float size_x = 0; - float size_z = 0; - chaperone->GetPlayAreaSize(&size_x, &size_z); - display_info->stage_parameters->bounds = - vr_utils::GetStageBoundsFromSize(size_x, size_z); - } - return display_info; -} - - -} // namespace - -OpenVRDevice::OpenVRDevice() - : VRDeviceBase(device::mojom::XRDeviceId::OPENVR_DEVICE_ID), - main_thread_task_runner_(base::ThreadTaskRunnerHandle::Get()) { - render_loop_ = std::make_unique<OpenVRRenderLoop>(); - - OnPollingEvents(); -} - -bool OpenVRDevice::IsHwAvailable() { - return vr::VR_IsHmdPresent(); -} - -bool OpenVRDevice::IsApiAvailable() { - return vr::VR_IsRuntimeInstalled(); -} - -mojo::PendingRemote<mojom::XRCompositorHost> -OpenVRDevice::BindCompositorHost() { - return compositor_host_receiver_.BindNewPipeAndPassRemote(); -} - -OpenVRDevice::~OpenVRDevice() { - Shutdown(); -} - -void OpenVRDevice::Shutdown() { - // Wait for the render loop to stop before completing destruction. This will - // ensure that the IVRSystem doesn't get shutdown until the render loop is no - // longer referencing it. - if (render_loop_ && render_loop_->IsRunning()) - render_loop_->Stop(); -} - -void OpenVRDevice::RequestSession( - mojom::XRRuntimeSessionOptionsPtr options, - mojom::XRRuntime::RequestSessionCallback callback) { - if (!EnsureValidDisplayInfo()) { - std::move(callback).Run(nullptr, mojo::NullRemote()); - return; - } - - DCHECK_EQ(options->mode, mojom::XRSessionMode::kImmersiveVr); - - if (!render_loop_->IsRunning()) { - render_loop_->Start(); - - if (!render_loop_->IsRunning()) { - std::move(callback).Run(nullptr, mojo::NullRemote()); - return; - } - - if (overlay_receiver_) { - render_loop_->task_runner()->PostTask( - FROM_HERE, base::BindOnce(&XRCompositorCommon::RequestOverlay, - base::Unretained(render_loop_.get()), - std::move(overlay_receiver_))); - } - } - - // We are done using OpenVR until the presentation session ends. - openvr_ = nullptr; - - auto my_callback = - base::BindOnce(&OpenVRDevice::OnRequestSessionResult, - weak_ptr_factory_.GetWeakPtr(), std::move(callback)); - - auto on_presentation_ended = base::BindOnce( - &OpenVRDevice::OnPresentationEnded, weak_ptr_factory_.GetWeakPtr()); - - render_loop_->task_runner()->PostTask( - FROM_HERE, - base::BindOnce(&XRCompositorCommon::RequestSession, - base::Unretained(render_loop_.get()), - std::move(on_presentation_ended), - base::DoNothing::Repeatedly<mojom::XRVisibilityState>(), - std::move(options), std::move(my_callback))); - outstanding_session_requests_count_++; -} - -bool OpenVRDevice::EnsureValidDisplayInfo() { - // Ensure we have had a valid display_info set at least once. - if (!have_real_display_info_) { - DCHECK(!openvr_); - // Initialize OpenVR. - openvr_ = std::make_unique<OpenVRWrapper>(false /* presenting */); - if (!openvr_->IsInitialized()) { - openvr_ = nullptr; - return false; - } - - SetVRDisplayInfo(CreateVRDisplayInfo(openvr_->GetSystem(), GetId())); - have_real_display_info_ = true; - } - return have_real_display_info_; -} - -void OpenVRDevice::OnPresentationEnded() { - if (!openvr_ && outstanding_session_requests_count_ == 0) { - openvr_ = std::make_unique<OpenVRWrapper>(false /* presenting */); - if (!openvr_->IsInitialized()) { - openvr_ = nullptr; - return; - } - } -} - -void OpenVRDevice::OnRequestSessionResult( - mojom::XRRuntime::RequestSessionCallback callback, - bool result, - mojom::XRSessionPtr session) { - outstanding_session_requests_count_--; - if (!result) { - OnPresentationEnded(); - std::move(callback).Run(nullptr, mojo::NullRemote()); - return; - } - - OnStartPresenting(); - - session->display_info = display_info_.Clone(); - - std::move(callback).Run( - std::move(session), - exclusive_controller_receiver_.BindNewPipeAndPassRemote()); - - // Use of Unretained is safe because the callback will only occur if the - // binding is not destroyed. - exclusive_controller_receiver_.set_disconnect_handler( - base::BindOnce(&OpenVRDevice::OnPresentingControllerMojoConnectionError, - base::Unretained(this))); -} - -bool OpenVRDevice::IsAvailable() { - return vr::VR_IsRuntimeInstalled() && vr::VR_IsHmdPresent(); -} - -void OpenVRDevice::CreateImmersiveOverlay( - mojo::PendingReceiver<mojom::ImmersiveOverlay> overlay_receiver) { - if (render_loop_->IsRunning()) { - render_loop_->task_runner()->PostTask( - FROM_HERE, base::BindOnce(&XRCompositorCommon::RequestOverlay, - base::Unretained(render_loop_.get()), - std::move(overlay_receiver))); - } else { - overlay_receiver_ = std::move(overlay_receiver); - } -} - -// XRSessionController -void OpenVRDevice::SetFrameDataRestricted(bool restricted) { - // Presentation sessions can not currently be restricted. - DCHECK(false); -} - -void OpenVRDevice::OnPresentingControllerMojoConnectionError() { - render_loop_->task_runner()->PostTask( - FROM_HERE, base::BindOnce(&XRCompositorCommon::ExitPresent, - base::Unretained(render_loop_.get()))); - OnExitPresent(); - exclusive_controller_receiver_.reset(); -} - -// Only deal with events that will cause displayInfo changes for now. -void OpenVRDevice::OnPollingEvents() { - main_thread_task_runner_->PostDelayedTask( - FROM_HERE, - base::BindOnce(&OpenVRDevice::OnPollingEvents, - weak_ptr_factory_.GetWeakPtr()), - kPollingInterval); - - if (!openvr_) - return; - - vr::VREvent_t event; - bool is_changed = false; - while (openvr_->GetSystem()->PollNextEvent(&event, sizeof(event))) { - if (event.trackedDeviceIndex != vr::k_unTrackedDeviceIndex_Hmd && - event.trackedDeviceIndex != vr::k_unTrackedDeviceIndexInvalid) { - continue; - } - - switch (event.eventType) { - case vr::VREvent_TrackedDeviceUpdated: - case vr::VREvent_IpdChanged: - case vr::VREvent_ChaperoneDataHasChanged: - case vr::VREvent_ChaperoneSettingsHaveChanged: - case vr::VREvent_ChaperoneUniverseHasChanged: - is_changed = true; - break; - - default: - break; - } - } - - if (is_changed) - SetVRDisplayInfo(CreateVRDisplayInfo(openvr_->GetSystem(), GetId())); -} - -} // namespace device diff --git a/chromium/device/vr/openvr/openvr_device.h b/chromium/device/vr/openvr/openvr_device.h deleted file mode 100644 index 919e0dc4ff5..00000000000 --- a/chromium/device/vr/openvr/openvr_device.h +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#ifndef DEVICE_VR_OPENVR_OPENVR_DEVICE_H_ -#define DEVICE_VR_OPENVR_OPENVR_DEVICE_H_ - -#include <memory> - -#include "base/macros.h" -#include "base/single_thread_task_runner.h" -#include "device/vr/openvr/openvr_api_wrapper.h" -#include "device/vr/public/mojom/vr_service.mojom.h" -#include "device/vr/vr_device_base.h" -#include "mojo/public/cpp/bindings/pending_receiver.h" -#include "mojo/public/cpp/bindings/pending_remote.h" -#include "mojo/public/cpp/bindings/receiver.h" - -namespace device { - -class XRCompositorCommon; - -class DEVICE_VR_EXPORT OpenVRDevice - : public VRDeviceBase, - public mojom::XRSessionController, - public mojom::XRCompositorHost { - public: - OpenVRDevice(); - ~OpenVRDevice() override; - - static bool IsHwAvailable(); - static bool IsApiAvailable(); - - void Shutdown(); - - // VRDeviceBase - void RequestSession( - mojom::XRRuntimeSessionOptionsPtr options, - mojom::XRRuntime::RequestSessionCallback callback) override; - - void OnPollingEvents(); - - void OnRequestSessionResult(mojom::XRRuntime::RequestSessionCallback callback, - bool result, - mojom::XRSessionPtr session); - - bool IsAvailable(); - - mojo::PendingRemote<mojom::XRCompositorHost> BindCompositorHost(); - - private: - // XRSessionController - void SetFrameDataRestricted(bool restricted) override; - - // XRCompositorHost - void CreateImmersiveOverlay( - mojo::PendingReceiver<mojom::ImmersiveOverlay> overlay_receiver) override; - - void OnPresentingControllerMojoConnectionError(); - void OnPresentationEnded(); - bool EnsureValidDisplayInfo(); - - int outstanding_session_requests_count_ = 0; - bool have_real_display_info_ = false; - std::unique_ptr<XRCompositorCommon> render_loop_; - std::unique_ptr<OpenVRWrapper> openvr_; - scoped_refptr<base::SingleThreadTaskRunner> main_thread_task_runner_; - - mojo::Receiver<mojom::XRSessionController> exclusive_controller_receiver_{ - this}; - - mojo::Receiver<mojom::XRCompositorHost> compositor_host_receiver_{this}; - mojo::PendingReceiver<mojom::ImmersiveOverlay> overlay_receiver_; - - base::WeakPtrFactory<OpenVRDevice> weak_ptr_factory_{this}; - - DISALLOW_COPY_AND_ASSIGN(OpenVRDevice); -}; - -} // namespace device - -#endif // DEVICE_VR_OPENVR_OPENVR_DEVICE_H_ diff --git a/chromium/device/vr/openvr/openvr_gamepad_helper.cc b/chromium/device/vr/openvr/openvr_gamepad_helper.cc deleted file mode 100644 index f1875bedff4..00000000000 --- a/chromium/device/vr/openvr/openvr_gamepad_helper.cc +++ /dev/null @@ -1,333 +0,0 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#include "device/vr/openvr/openvr_gamepad_helper.h" -#include "device/vr/openvr/openvr_api_wrapper.h" - -#include <memory> -#include <unordered_set> - -#include "base/compiler_specific.h" -#include "base/strings/string_split.h" -#include "base/strings/string_util.h" -#include "base/strings/utf_string_conversions.h" -#include "device/gamepad/public/cpp/gamepads.h" -#include "device/vr/util/gamepad_builder.h" -#include "device/vr/util/xr_standard_gamepad_builder.h" -#include "device/vr/vr_device.h" -#include "third_party/openvr/src/headers/openvr.h" -#include "ui/gfx/transform.h" -#include "ui/gfx/transform_util.h" - -namespace device { - -namespace { - -constexpr double kJoystickDeadzone = 0.16; - -bool TryGetGamepadButton(const vr::VRControllerState_t& controller_state, - uint64_t supported_buttons, - vr::EVRButtonId button_id, - GamepadButton* button) { - uint64_t button_mask = vr::ButtonMaskFromId(button_id); - if ((supported_buttons & button_mask) != 0) { - bool button_pressed = (controller_state.ulButtonPressed & button_mask) != 0; - bool button_touched = (controller_state.ulButtonTouched & button_mask) != 0; - button->touched = button_touched || button_pressed; - button->pressed = button_pressed; - button->value = button_pressed ? 1.0 : 0.0; - return true; - } - - return false; -} - -vr::EVRButtonId GetAxisId(uint32_t axis_offset) { - return static_cast<vr::EVRButtonId>(vr::k_EButton_Axis0 + axis_offset); -} - -std::map<vr::EVRButtonId, GamepadBuilder::ButtonData> GetAxesButtons( - vr::IVRSystem* vr_system, - const vr::VRControllerState_t& controller_state, - uint64_t supported_buttons, - uint32_t controller_id) { - std::map<vr::EVRButtonId, GamepadBuilder::ButtonData> button_data_map; - - for (uint32_t j = 0; j < vr::k_unControllerStateAxisCount; ++j) { - int32_t axis_type = vr_system->GetInt32TrackedDeviceProperty( - controller_id, - static_cast<vr::TrackedDeviceProperty>(vr::Prop_Axis0Type_Int32 + j)); - - GamepadBuilder::ButtonData button_data; - - // Invert the y axis because -1 is up in the Gamepad API but down in OpenVR. - double x_axis = controller_state.rAxis[j].x; - double y_axis = -controller_state.rAxis[j].y; - - if (axis_type == vr::k_eControllerAxis_Joystick) { - button_data.type = GamepadBuilder::ButtonData::Type::kThumbstick; - - // We only want to apply the deadzone to joysticks, since various - // runtimes may not have already done that, but touchpads should - // be fine. - x_axis = std::fabs(x_axis) < kJoystickDeadzone ? 0 : x_axis; - y_axis = std::fabs(y_axis) < kJoystickDeadzone ? 0 : y_axis; - } else if (axis_type == vr::k_eControllerAxis_TrackPad) { - button_data.type = GamepadBuilder::ButtonData::Type::kTouchpad; - } - - switch (axis_type) { - case vr::k_eControllerAxis_Joystick: - case vr::k_eControllerAxis_TrackPad: { - button_data.x_axis = x_axis; - button_data.y_axis = y_axis; - vr::EVRButtonId button_id = GetAxisId(j); - - // Even if the button associated with the axis isn't supported, if we - // have valid axis data, we should still send that up. Since the spec - // expects buttons with axes, then we will add a dummy button to match - // the axes. - GamepadButton button; - if (TryGetGamepadButton(controller_state, supported_buttons, button_id, - &button)) { - button_data.touched = button.touched; - button_data.pressed = button.pressed; - button_data.value = button.value; - } else { - button_data.pressed = false; - button_data.value = 0.0; - button_data.touched = - (std::fabs(x_axis) > 0 || std::fabs(y_axis) > 0); - } - - button_data_map[button_id] = button_data; - } break; - case vr::k_eControllerAxis_Trigger: { - GamepadButton button; - GamepadBuilder::ButtonData button_data; - vr::EVRButtonId button_id = GetAxisId(j); - if (TryGetGamepadButton(controller_state, supported_buttons, button_id, - &button)) { - button_data.touched = button.touched; - button_data.pressed = button.pressed; - button_data.value = x_axis; - button_data_map[button_id] = button_data; - } - } break; - } - } - - return button_data_map; -} - -constexpr std::array<vr::EVRButtonId, 5> kWebXRButtonOrder = { - vr::k_EButton_A, vr::k_EButton_DPad_Left, vr::k_EButton_DPad_Up, - vr::k_EButton_DPad_Right, vr::k_EButton_DPad_Down, -}; - -// To make sure this string fits the requirements of the WebXR spec, separate -// words/tokens are separated by "-" instead of whitespace and convert it to -// lowercase. -std::string FixupProfileString(const std::string& name) { - std::vector<std::string> tokens = - base::SplitString(name, base::kWhitespaceASCII, base::KEEP_WHITESPACE, - base::SPLIT_WANT_NONEMPTY); - std::string result = base::JoinString(tokens, "-"); - return base::ToLowerASCII(result); -} - -} // namespace - -// Helper classes and WebXR Getters -class OpenVRGamepadBuilder : public XRStandardGamepadBuilder { - public: - enum class AxesRequirement { - kOptional = 0, - kRequireBoth = 1, - }; - - OpenVRGamepadBuilder(vr::IVRSystem* vr_system, - uint32_t controller_id, - vr::VRControllerState_t controller_state, - mojom::XRHandedness handedness) - : XRStandardGamepadBuilder(handedness), - controller_state_(controller_state) { - supported_buttons_ = vr_system->GetUint64TrackedDeviceProperty( - controller_id, vr::Prop_SupportedButtons_Uint64); - - axes_data_ = GetAxesButtons(vr_system, controller_state_, - supported_buttons_, controller_id); - - base::Optional<GamepadBuilder::ButtonData> primary_button = - TryGetAxesOrTriggerButton(vr::k_EButton_SteamVR_Trigger); - - if (!primary_button) { - return; - } - - SetPrimaryButton(primary_button.value()); - - base::Optional<GamepadButton> secondary_button = - TryGetButton(vr::k_EButton_Grip); - if (secondary_button) { - SetSecondaryButton(secondary_button.value()); - } - - base::Optional<GamepadBuilder::ButtonData> touchpad_data = - TryGetNextUnusedButtonOfType( - GamepadBuilder::ButtonData::Type::kTouchpad); - if (touchpad_data) { - SetTouchpadData(touchpad_data.value()); - } - - base::Optional<GamepadBuilder::ButtonData> thumbstick_data = - TryGetNextUnusedButtonOfType( - GamepadBuilder::ButtonData::Type::kThumbstick); - if (thumbstick_data) { - SetThumbstickData(thumbstick_data.value()); - } - - // Now that all of the xr-standard reserved buttons have been filled in, we - // add the rest of the buttons in order of decreasing importance. - // First add regular buttons. - for (const auto& id : kWebXRButtonOrder) { - base::Optional<GamepadButton> button = TryGetButton(id); - if (button) { - AddOptionalButtonData(button.value()); - } - } - - // Finally, add any remaining axis buttons (triggers/josysticks/touchpads) - AddRemainingTriggersAndAxes(); - - // Find out the model and manufacturer names in case the caller wants this - // information for the input profiles array. - std::string model = - GetOpenVRString(vr_system, vr::Prop_ModelNumber_String, controller_id); - std::string manufacturer = GetOpenVRString( - vr_system, vr::Prop_ManufacturerName_String, controller_id); - - UpdateProfiles(manufacturer, model); - } - - ~OpenVRGamepadBuilder() override = default; - - std::vector<std::string> GetProfiles() const { return profiles_; } - - private: - void UpdateProfiles(const std::string& manufacturer, - const std::string& model) { - // Per the WebXR spec, the first entry in the profiles array should be the - // most specific one. - std::string name = - FixupProfileString(manufacturer) + "-" + FixupProfileString(model); - profiles_.push_back(name); - - // Also record information about what this controller actually does in a - // more general sense. The controller is guaranteed to at least have a - // trigger if we get here. - std::string capabilities = "generic-trigger"; - if (HasSecondaryButton()) { - capabilities += "-squeeze"; - } - if (HasTouchpad()) { - capabilities += "-touchpad"; - } - if (HasThumbstick()) { - capabilities += "-thumbstick"; - } - profiles_.push_back(capabilities); - } - - base::Optional<GamepadBuilder::ButtonData> TryGetAxesOrTriggerButton( - vr::EVRButtonId button_id, - AxesRequirement requirement = AxesRequirement::kOptional) { - if (!IsInAxesData(button_id)) - return base::nullopt; - - bool require_axes = (requirement == AxesRequirement::kRequireBoth); - if (require_axes && - axes_data_[button_id].type == GamepadBuilder::ButtonData::Type::kButton) - return base::nullopt; - - used_axes_.insert(button_id); - return axes_data_[button_id]; - } - - base::Optional<GamepadBuilder::ButtonData> TryGetNextUnusedButtonOfType( - GamepadBuilder::ButtonData::Type type) { - for (const auto& axes_data_pair : axes_data_) { - vr::EVRButtonId button_id = axes_data_pair.first; - if (IsUsed(button_id)) - continue; - - if (axes_data_pair.second.type != type) - continue; - - return TryGetAxesOrTriggerButton(button_id, - AxesRequirement::kRequireBoth); - } - - return base::nullopt; - } - - base::Optional<GamepadButton> TryGetButton(vr::EVRButtonId button_id) { - GamepadButton button; - if (TryGetGamepadButton(controller_state_, supported_buttons_, button_id, - &button)) { - return button; - } - - return base::nullopt; - } - - // This will add any remaining unused values from axes_data to the gamepad. - // Returns a bool indicating whether any additional axes were added. - void AddRemainingTriggersAndAxes() { - for (const auto& axes_data_pair : axes_data_) { - if (!IsUsed(axes_data_pair.first)) { - AddOptionalButtonData(axes_data_pair.second); - } - } - } - - bool IsUsed(vr::EVRButtonId button_id) { - auto it = used_axes_.find(button_id); - return it != used_axes_.end(); - } - - bool IsInAxesData(vr::EVRButtonId button_id) { - auto it = axes_data_.find(button_id); - return it != axes_data_.end(); - } - - const vr::VRControllerState_t controller_state_; - uint64_t supported_buttons_; - std::map<vr::EVRButtonId, GamepadBuilder::ButtonData> axes_data_; - std::unordered_set<vr::EVRButtonId> used_axes_; - std::vector<std::string> profiles_; - - DISALLOW_COPY_AND_ASSIGN(OpenVRGamepadBuilder); -}; - -OpenVRInputSourceData::OpenVRInputSourceData() = default; -OpenVRInputSourceData::~OpenVRInputSourceData() = default; -OpenVRInputSourceData::OpenVRInputSourceData( - const OpenVRInputSourceData& other) = default; - -OpenVRInputSourceData OpenVRGamepadHelper::GetXRInputSourceData( - vr::IVRSystem* vr_system, - uint32_t controller_id, - vr::VRControllerState_t controller_state, - mojom::XRHandedness handedness) { - OpenVRGamepadBuilder builder(vr_system, controller_id, controller_state, - handedness); - - OpenVRInputSourceData data; - data.gamepad = builder.GetGamepad(); - data.profiles = builder.GetProfiles(); - return data; -} - -} // namespace device diff --git a/chromium/device/vr/openvr/openvr_gamepad_helper.h b/chromium/device/vr/openvr/openvr_gamepad_helper.h deleted file mode 100644 index 591d8581d66..00000000000 --- a/chromium/device/vr/openvr/openvr_gamepad_helper.h +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#ifndef DEVICE_VR_OPENVR_OPENVR_GAMEPAD_HELPER_H_ -#define DEVICE_VR_OPENVR_OPENVR_GAMEPAD_HELPER_H_ - -#include <string> -#include <utility> -#include <vector> - -#include "base/optional.h" -#include "device/gamepad/public/cpp/gamepads.h" -#include "device/vr/public/mojom/vr_service.mojom.h" -#include "third_party/openvr/src/headers/openvr.h" - -namespace device { - -struct OpenVRInputSourceData { - OpenVRInputSourceData(); - ~OpenVRInputSourceData(); - OpenVRInputSourceData(const OpenVRInputSourceData& other); - base::Optional<Gamepad> gamepad; - std::vector<std::string> profiles; -}; - -class OpenVRGamepadHelper { - public: - static OpenVRInputSourceData GetXRInputSourceData( - vr::IVRSystem* system, - uint32_t controller_id, - vr::VRControllerState_t controller_state, - mojom::XRHandedness handedness); -}; - -} // namespace device -#endif // DEVICE_VR_OPENVR_OPENVR_GAMEPAD_HELPER_H_ diff --git a/chromium/device/vr/openvr/openvr_render_loop.cc b/chromium/device/vr/openvr/openvr_render_loop.cc deleted file mode 100644 index 97abc700f6b..00000000000 --- a/chromium/device/vr/openvr/openvr_render_loop.cc +++ /dev/null @@ -1,324 +0,0 @@ -// Copyright (c) 2017 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#include "device/vr/openvr/openvr_render_loop.h" - -#include "base/trace_event/trace_event.h" -#include "device/vr/openvr/openvr_api_wrapper.h" -#include "device/vr/openvr/openvr_gamepad_helper.h" -#include "device/vr/openvr/openvr_type_converters.h" -#include "ui/gfx/geometry/angle_conversions.h" -#include "ui/gfx/transform.h" - -#if defined(OS_WIN) -#include "device/vr/windows/d3d11_texture_helper.h" -#endif - -namespace device { - -namespace { - -// OpenVR reports the controllers pose of the controller's tip, while WebXR -// needs to report the pose of the controller's grip (centered on the user's -// palm.) This experimentally determined value is how far back along the Z axis -// in meters OpenVR's pose needs to be translated to align with WebXR's -// coordinate system. -const float kGripOffsetZMeters = 0.08f; - -// WebXR reports a pointer pose separate from the grip pose, which represents a -// pointer ray emerging from the tip of the controller. OpenVR does not report -// anything like that, and most pointers are assumed to come straight from the -// controller's tip. For consistency with other WebXR backends we'll synthesize -// a pointer ray that's angled down slightly from the controller's handle, -// defined by this angle. Experimentally determined, should roughly point in the -// same direction as a user's outstretched index finger while holding a -// controller. -const float kPointerErgoAngleDegrees = -40.0f; - -gfx::Transform HmdMatrix34ToTransform(const vr::HmdMatrix34_t& mat) { - return gfx::Transform(mat.m[0][0], mat.m[0][1], mat.m[0][2], mat.m[0][3], - mat.m[1][0], mat.m[1][1], mat.m[1][2], mat.m[1][3], - mat.m[2][0], mat.m[2][1], mat.m[2][2], mat.m[2][3], 0, - 0, 0, 1); -} - -device::mojom::XRHandedness ConvertToMojoHandedness( - vr::ETrackedControllerRole controller_role) { - switch (controller_role) { - case vr::TrackedControllerRole_LeftHand: - return device::mojom::XRHandedness::LEFT; - case vr::TrackedControllerRole_RightHand: - return device::mojom::XRHandedness::RIGHT; - case vr::TrackedControllerRole_Invalid: - case vr::TrackedControllerRole_OptOut: - case vr::TrackedControllerRole_Treadmill: - case vr::TrackedControllerRole_Max: - return device::mojom::XRHandedness::NONE; - } - - NOTREACHED(); -} - -} // namespace - -void OpenVRRenderLoop::InputActiveState::MarkAsInactive() { - active = false; - primary_input_pressed = false; - device_class = vr::TrackedDeviceClass_Invalid; - controller_role = vr::TrackedControllerRole_Invalid; -} - -OpenVRRenderLoop::OpenVRRenderLoop() : XRCompositorCommon() {} - -OpenVRRenderLoop::~OpenVRRenderLoop() { - Stop(); -} - -bool OpenVRRenderLoop::PreComposite() { - texture_helper_.AllocateBackBuffer(); - return true; -} - -bool OpenVRRenderLoop::SubmitCompositedFrame() { - DCHECK(openvr_); - vr::IVRCompositor* vr_compositor = openvr_->GetCompositor(); - DCHECK(vr_compositor); - if (!vr_compositor) - return false; - - vr::Texture_t texture; - texture.handle = texture_helper_.GetBackbuffer().Get(); - texture.eType = vr::TextureType_DirectX; - texture.eColorSpace = vr::ColorSpace_Auto; - - gfx::RectF left_bounds = texture_helper_.BackBufferLeft(); - gfx::RectF right_bounds = texture_helper_.BackBufferRight(); - - vr::VRTextureBounds_t bounds[2]; - bounds[0] = {left_bounds.x(), left_bounds.y(), - left_bounds.width() + left_bounds.x(), - left_bounds.height() + left_bounds.y()}; - bounds[1] = {right_bounds.x(), right_bounds.y(), - right_bounds.width() + right_bounds.x(), - right_bounds.height() + right_bounds.y()}; - - vr::EVRCompositorError error = - vr_compositor->Submit(vr::EVREye::Eye_Left, &texture, &bounds[0]); - if (error != vr::VRCompositorError_None) { - return false; - } - error = vr_compositor->Submit(vr::EVREye::Eye_Right, &texture, &bounds[1]); - if (error != vr::VRCompositorError_None) { - return false; - } - vr_compositor->PostPresentHandoff(); - return true; -} - -bool OpenVRRenderLoop::StartRuntime() { - if (!openvr_) { - openvr_ = std::make_unique<OpenVRWrapper>(true); - if (!openvr_->IsInitialized()) { - openvr_ = nullptr; - return false; - } - - openvr_->GetCompositor()->SuspendRendering(true); - openvr_->GetCompositor()->SetTrackingSpace( - vr::ETrackingUniverseOrigin::TrackingUniverseSeated); - } - -#if defined(OS_WIN) - int32_t adapter_index; - openvr_->GetSystem()->GetDXGIOutputInfo(&adapter_index); - if (!texture_helper_.SetAdapterIndex(adapter_index) || - !texture_helper_.EnsureInitialized()) { - openvr_ = nullptr; - return false; - } -#endif - - uint32_t width, height; - openvr_->GetSystem()->GetRecommendedRenderTargetSize(&width, &height); - texture_helper_.SetDefaultSize(gfx::Size(width, height)); - - return true; -} - -void OpenVRRenderLoop::StopRuntime() { - if (openvr_) - openvr_->GetCompositor()->SuspendRendering(true); - openvr_ = nullptr; -} - -void OpenVRRenderLoop::OnSessionStart() { - // Reset the active states for all the controllers. - for (uint32_t i = 0; i < vr::k_unMaxTrackedDeviceCount; ++i) { - InputActiveState& input_active_state = input_active_states_[i]; - input_active_state.active = false; - input_active_state.primary_input_pressed = false; - input_active_state.device_class = vr::TrackedDeviceClass_Invalid; - input_active_state.controller_role = vr::TrackedControllerRole_Invalid; - } - - openvr_->GetCompositor()->SuspendRendering(false); - - // Measure the VrViewerType we are presenting with. - std::string model = - GetOpenVRString(openvr_->GetSystem(), vr::Prop_ModelNumber_String); - VrViewerType type = VrViewerType::OPENVR_UNKNOWN; - if (model == "Oculus Rift CV1") - type = VrViewerType::OPENVR_RIFT_CV1; - else if (model == "Vive MV") - type = VrViewerType::OPENVR_VIVE; - - LogViewerType(type); -} - -mojom::XRFrameDataPtr OpenVRRenderLoop::GetNextFrameData() { - mojom::XRFrameDataPtr frame_data = mojom::XRFrameData::New(); - frame_data->frame_id = next_frame_id_; - - if (openvr_) { - vr::TrackedDevicePose_t rendering_poses[vr::k_unMaxTrackedDeviceCount]; - - TRACE_EVENT0("gpu", "WaitGetPoses"); - openvr_->GetCompositor()->WaitGetPoses( - rendering_poses, vr::k_unMaxTrackedDeviceCount, nullptr, 0); - - frame_data->pose = mojo::ConvertTo<mojom::VRPosePtr>( - rendering_poses[vr::k_unTrackedDeviceIndex_Hmd]); - - // Update WebXR input sources. - frame_data->input_state = - GetInputState(rendering_poses, vr::k_unMaxTrackedDeviceCount); - - vr::Compositor_FrameTiming timing; - timing.m_nSize = sizeof(vr::Compositor_FrameTiming); - bool valid_time = openvr_->GetCompositor()->GetFrameTiming(&timing); - if (valid_time) { - frame_data->time_delta = - base::TimeDelta::FromSecondsD(timing.m_flSystemTimeInSeconds); - } - } - - return frame_data; -} - -std::vector<mojom::XRInputSourceStatePtr> OpenVRRenderLoop::GetInputState( - vr::TrackedDevicePose_t* poses, - uint32_t count) { - std::vector<mojom::XRInputSourceStatePtr> input_states; - - if (!openvr_) - return input_states; - - // Loop through every device pose and determine which are controllers - for (uint32_t i = vr::k_unTrackedDeviceIndex_Hmd + 1; i < count; ++i) { - const vr::TrackedDevicePose_t& pose = poses[i]; - InputActiveState& input_active_state = input_active_states_[i]; - - if (!pose.bDeviceIsConnected) { - // If this was an active controller on the last frame report it as - // disconnected. - if (input_active_state.active) - input_active_state.MarkAsInactive(); - continue; - } - - // Is this a newly connected controller? - bool newly_active = false; - if (!input_active_state.active) { - input_active_state.active = true; - input_active_state.device_class = - openvr_->GetSystem()->GetTrackedDeviceClass(i); - newly_active = true; - } - - // Skip over any tracked devices that aren't controllers. - if (input_active_state.device_class != vr::TrackedDeviceClass_Controller) { - continue; - } - - device::mojom::XRInputSourceStatePtr state = - device::mojom::XRInputSourceState::New(); - - vr::VRControllerState_t controller_state; - bool have_state = openvr_->GetSystem()->GetControllerState( - i, &controller_state, sizeof(vr::VRControllerState_t)); - if (!have_state) { - input_active_state.MarkAsInactive(); - continue; - } - - bool pressed = controller_state.ulButtonPressed & - vr::ButtonMaskFromId(vr::k_EButton_SteamVR_Trigger); - - state->source_id = i; - state->primary_input_pressed = pressed; - state->primary_input_clicked = - (!pressed && input_active_state.primary_input_pressed); - - input_active_state.primary_input_pressed = pressed; - - if (pose.bPoseIsValid) { - state->mojo_from_input = - HmdMatrix34ToTransform(pose.mDeviceToAbsoluteTracking); - // Scoot the grip matrix back a bit so that it actually lines up with the - // user's palm. - state->mojo_from_input->Translate3d(0, 0, kGripOffsetZMeters); - } - - // Poll controller roll per-frame, since OpenVR controllers can swap hands. - vr::ETrackedControllerRole controller_role = - openvr_->GetSystem()->GetControllerRoleForTrackedDeviceIndex(i); - - device::mojom::XRHandedness handedness = - ConvertToMojoHandedness(controller_role); - - OpenVRInputSourceData input_source_data = - OpenVRGamepadHelper::GetXRInputSourceData(openvr_->GetSystem(), i, - controller_state, handedness); - state->gamepad = input_source_data.gamepad; - - // OpenVR controller are fully 6DoF. - state->emulated_position = false; - - // Re-send the controller's description if it's newly active or if the - // handedness or profile strings have changed. - if (newly_active || - (controller_role != input_active_state.controller_role) || - (input_source_data.profiles != input_active_state.profiles)) { - device::mojom::XRInputSourceDescriptionPtr desc = - device::mojom::XRInputSourceDescription::New(); - - // It's a handheld pointing device. - desc->target_ray_mode = device::mojom::XRTargetRayMode::POINTING; - - desc->handedness = handedness; - input_active_state.controller_role = controller_role; - - // Tweak the pointer transform so that it's angled down from the - // grip. This should be a bit more ergonomic. - desc->input_from_pointer = gfx::Transform(); - desc->input_from_pointer->RotateAboutXAxis(kPointerErgoAngleDegrees); - - desc->profiles = input_source_data.profiles; - - state->description = std::move(desc); - - // Keep track of the current profiles so we know if it changes next frame. - input_active_state.profiles = input_source_data.profiles; - } - - input_states.push_back(std::move(state)); - } - - return input_states; -} - -OpenVRRenderLoop::InputActiveState::InputActiveState() = default; -OpenVRRenderLoop::InputActiveState::~InputActiveState() = default; - -} // namespace device diff --git a/chromium/device/vr/openvr/openvr_render_loop.h b/chromium/device/vr/openvr/openvr_render_loop.h deleted file mode 100644 index 327532db79c..00000000000 --- a/chromium/device/vr/openvr/openvr_render_loop.h +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright (c) 2017 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#ifndef DEVICE_VR_OPENVR_RENDER_LOOP_H -#define DEVICE_VR_OPENVR_RENDER_LOOP_H - -#include <string> -#include <vector> - -#include "base/memory/scoped_refptr.h" -#include "base/threading/thread.h" -#include "base/time/time.h" -#include "build/build_config.h" -#include "device/vr/public/mojom/vr_service.mojom.h" -#include "device/vr/vr_device.h" -#include "device/vr/windows/compositor_base.h" -#include "mojo/public/cpp/system/platform_handle.h" -#include "third_party/openvr/src/headers/openvr.h" -#include "ui/gfx/geometry/rect_f.h" - -#if defined(OS_WIN) -#include "device/vr/windows/d3d11_texture_helper.h" -#endif - -namespace device { - -class OpenVRWrapper; - -class OpenVRRenderLoop : public XRCompositorCommon { - public: - OpenVRRenderLoop(); - ~OpenVRRenderLoop() override; - - private: - // XRDeviceAbstraction: - mojom::XRFrameDataPtr GetNextFrameData() override; - bool StartRuntime() override; - void StopRuntime() override; - void OnSessionStart() override; - bool PreComposite() override; - bool SubmitCompositedFrame() override; - - // Helpers to implement XRDeviceAbstraction. - std::vector<mojom::XRInputSourceStatePtr> GetInputState( - vr::TrackedDevicePose_t* poses, - uint32_t count); - - struct InputActiveState { - bool active; - bool primary_input_pressed; - vr::ETrackedDeviceClass device_class; - vr::ETrackedControllerRole controller_role; - - std::vector<std::string> profiles; - - InputActiveState(); - ~InputActiveState(); - void MarkAsInactive(); - - DISALLOW_COPY_AND_ASSIGN(InputActiveState); - }; - - InputActiveState input_active_states_[vr::k_unMaxTrackedDeviceCount]; - std::unique_ptr<OpenVRWrapper> openvr_; - - DISALLOW_COPY_AND_ASSIGN(OpenVRRenderLoop); -}; - -} // namespace device - -#endif // DEVICE_VR_OPENVR_RENDER_LOOP_H diff --git a/chromium/device/vr/openvr/openvr_type_converters.cc b/chromium/device/vr/openvr/openvr_type_converters.cc deleted file mode 100644 index ce8fb793aa8..00000000000 --- a/chromium/device/vr/openvr/openvr_type_converters.cc +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) 2017 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#include "device/vr/openvr/openvr_type_converters.h" - -#include <math.h> -#include <iterator> -#include <vector> - -#include "device/vr/public/mojom/vr_service.mojom.h" -#include "third_party/openvr/src/headers/openvr.h" -#include "ui/gfx/transform_util.h" - -namespace mojo { - -device::mojom::VRPosePtr -TypeConverter<device::mojom::VRPosePtr, vr::TrackedDevicePose_t>::Convert( - const vr::TrackedDevicePose_t& hmd_pose) { - device::mojom::VRPosePtr pose = device::mojom::VRPose::New(); - pose->orientation = gfx::Quaternion(); - pose->position = gfx::Point3F(); - - if (hmd_pose.bPoseIsValid && hmd_pose.bDeviceIsConnected) { - const float(&m)[3][4] = hmd_pose.mDeviceToAbsoluteTracking.m; - - gfx::Transform transform = gfx::Transform( - m[0][0], m[0][1], m[0][2], 0.0f, m[1][0], m[1][1], m[1][2], 0.0f, - m[2][0], m[2][1], m[2][2], 0.0f, 0.0f, 0.0f, 0.0f, 1.0f); - gfx::DecomposedTransform decomposed; - if (gfx::DecomposeTransform(&decomposed, transform)) { - pose->orientation = decomposed.quaternion; - } - - pose->position->SetPoint(m[0][3], m[1][3], m[2][3]); - } - - return pose; -} - -} // namespace mojo diff --git a/chromium/device/vr/openvr/openvr_type_converters.h b/chromium/device/vr/openvr/openvr_type_converters.h deleted file mode 100644 index 55d2b5e8fac..00000000000 --- a/chromium/device/vr/openvr/openvr_type_converters.h +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) 2017 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. -#ifndef DEVICE_VR_OPENVR_TYPE_CONVERTERS_H -#define DEVICE_VR_OPENVR_TYPE_CONVERTERS_H - -#include "device/vr/public/mojom/vr_service.mojom.h" -#include "third_party/openvr/src/headers/openvr.h" - -namespace mojo { - -template <> -struct TypeConverter<device::mojom::VRPosePtr, vr::TrackedDevicePose_t> { - static device::mojom::VRPosePtr Convert( - const vr::TrackedDevicePose_t& hmd_pose); -}; - -} // namespace mojo - -#endif // DEVICE_VR_OPENVR_TYPE_CONVERTERS_H diff --git a/chromium/device/vr/openxr/openxr_controller.cc b/chromium/device/vr/openxr/openxr_controller.cc index 6c1c2df4be7..65c920cac54 100644 --- a/chromium/device/vr/openxr/openxr_controller.cc +++ b/chromium/device/vr/openxr/openxr_controller.cc @@ -68,6 +68,7 @@ XrResult OpenXrController::Initialize( XrInstance instance, XrSession session, const OpenXRPathHelper* path_helper, + const OpenXrExtensionHelper& extension_helper, std::map<XrPath, std::vector<XrActionSuggestedBinding>>* bindings) { DCHECK(bindings); type_ = type; @@ -95,7 +96,7 @@ XrResult OpenXrController::Initialize( RETURN_IF_XR_FAILED(InitializeControllerActions()); - SuggestBindings(bindings); + SuggestBindings(extension_helper, bindings); RETURN_IF_XR_FAILED(InitializeControllerSpaces()); return XR_SUCCESS; @@ -132,10 +133,23 @@ XrResult OpenXrController::InitializeControllerActions() { } XrResult OpenXrController::SuggestBindings( + const OpenXrExtensionHelper& extension_helper, std::map<XrPath, std::vector<XrActionSuggestedBinding>>* bindings) const { const std::string binding_prefix = GetTopLevelUserPath(type_); for (auto interaction_profile : kOpenXrControllerInteractionProfiles) { + // If the interaction profile is defined by an extension, check it here, + // otherwise continue + const bool extension_required = + interaction_profile.required_extension != nullptr; + if (extension_required) { + const bool extension_enabled = extension_helper.ExtensionSupported( + interaction_profile.required_extension); + if (!extension_enabled) { + continue; + } + } + XrPath interaction_profile_path = path_helper_->GetInteractionProfileXrPath(interaction_profile.type); RETURN_IF_XR_FAILED(SuggestActionBinding( diff --git a/chromium/device/vr/openxr/openxr_controller.h b/chromium/device/vr/openxr/openxr_controller.h index 3997d09d975..3b6e7786444 100644 --- a/chromium/device/vr/openxr/openxr_controller.h +++ b/chromium/device/vr/openxr/openxr_controller.h @@ -34,6 +34,7 @@ class OpenXrController { XrInstance instance, XrSession session, const OpenXRPathHelper* path_helper, + const OpenXrExtensionHelper& extension_helper, std::map<XrPath, std::vector<XrActionSuggestedBinding>>* bindings); XrActionSet action_set() const { return action_set_; } @@ -61,6 +62,7 @@ class OpenXrController { XrResult InitializeControllerSpaces(); XrResult SuggestBindings( + const OpenXrExtensionHelper& extension_helper, std::map<XrPath, std::vector<XrActionSuggestedBinding>>* bindings) const; XrResult CreateActionsForButton(OpenXrButtonType button_type); diff --git a/chromium/device/vr/openxr/openxr_defs.h b/chromium/device/vr/openxr/openxr_defs.h index 0ca8d86a720..54ddb8b4add 100644 --- a/chromium/device/vr/openxr/openxr_defs.h +++ b/chromium/device/vr/openxr/openxr_defs.h @@ -9,6 +9,11 @@ namespace device { constexpr char kWin32AppcontainerCompatibleExtensionName[] = "XR_EXT_win32_appcontainer_compatible"; +constexpr char kExtSamsungOdysseyControllerExtensionName[] = + "XR_EXT_samsung_odyssey_controller"; +constexpr char kExtHPMixedRealityControllerExtensionName[] = + "XR_EXT_hp_mixed_reality_controller"; + } // namespace device #endif // DEVICE_VR_OPENXR_OPENXR_DEFS_H_ diff --git a/chromium/device/vr/openxr/openxr_device.cc b/chromium/device/vr/openxr/openxr_device.cc index dd2542e7860..bab86430a87 100644 --- a/chromium/device/vr/openxr/openxr_device.cc +++ b/chromium/device/vr/openxr/openxr_device.cc @@ -27,11 +27,9 @@ constexpr unsigned int kRenderHeight = 1024; // However our mojo interface expects display info right away to support WebVR. // We create a fake display info to use, then notify the client that the display // info changed when we get real data. -mojom::VRDisplayInfoPtr CreateFakeVRDisplayInfo(device::mojom::XRDeviceId id) { +mojom::VRDisplayInfoPtr CreateFakeVRDisplayInfo() { mojom::VRDisplayInfoPtr display_info = mojom::VRDisplayInfo::New(); - display_info->id = id; - display_info->left_eye = mojom::VREyeParameters::New(); display_info->right_eye = mojom::VREyeParameters::New(); @@ -61,7 +59,7 @@ OpenXrDevice::OpenXrDevice(OpenXrStatics* openxr_statics) : VRDeviceBase(device::mojom::XRDeviceId::OPENXR_DEVICE_ID), instance_(openxr_statics->GetXrInstance()), weak_ptr_factory_(this) { - mojom::VRDisplayInfoPtr display_info = CreateFakeVRDisplayInfo(GetId()); + mojom::VRDisplayInfoPtr display_info = CreateFakeVRDisplayInfo(); SetVRDisplayInfo(std::move(display_info)); #if defined(OS_WIN) diff --git a/chromium/device/vr/openxr/openxr_input_helper.cc b/chromium/device/vr/openxr/openxr_input_helper.cc index 8f910588ec9..668faadcfe2 100644 --- a/chromium/device/vr/openxr/openxr_input_helper.cc +++ b/chromium/device/vr/openxr/openxr_input_helper.cc @@ -122,10 +122,11 @@ XrResult OpenXRInputHelper::Initialize(XrInstance instance) { // on availability. std::map<XrPath, std::vector<XrActionSuggestedBinding>> bindings; + OpenXrExtensionHelper extension_helper; for (size_t i = 0; i < controller_states_.size(); i++) { RETURN_IF_XR_FAILED(controller_states_[i].controller.Initialize( static_cast<OpenXrHandednessType>(i), instance, session_, - path_helper_.get(), &bindings)); + path_helper_.get(), extension_helper, &bindings)); controller_states_[i].primary_button_pressed = false; } diff --git a/chromium/device/vr/openxr/openxr_interaction_profiles.h b/chromium/device/vr/openxr/openxr_interaction_profiles.h index 0cd071f37ab..9c32aa9fca9 100644 --- a/chromium/device/vr/openxr/openxr_interaction_profiles.h +++ b/chromium/device/vr/openxr/openxr_interaction_profiles.h @@ -7,6 +7,7 @@ #include "base/stl_util.h" #include "device/gamepad/public/cpp/gamepad.h" +#include "device/vr/openxr/openxr_defs.h" #include "third_party/openxr/src/include/openxr/openxr.h" namespace device { @@ -25,7 +26,9 @@ enum class OpenXrInteractionProfileType { kOculusTouch = 2, kValveIndex = 3, kHTCVive = 4, - kCount = 5, + kSamsungOdyssey = 5, + kHPReverbG2 = 6, + kCount = 7, }; enum class OpenXrButtonType { @@ -71,6 +74,7 @@ struct OpenXrAxisPathMap { struct OpenXrControllerInteractionProfile { OpenXrInteractionProfileType type; const char* const path; + const char* const required_extension; GamepadMapping mapping; const char* const* const input_profiles; const size_t profile_size; @@ -82,13 +86,14 @@ struct OpenXrControllerInteractionProfile { size_t axis_map_size; }; -// TODO(crbug.com/1017513) // Currently Supports: // Microsoft motion controller. +// Samsung Odyssey controller // Khronos simple controller. // Oculus touch controller. // Valve index controller. // HTC vive controller +// HP Reverb G2 controller // Declare OpenXR input profile bindings for other runtimes when they become // available. constexpr const char* kMicrosoftMotionInputProfiles[] = { @@ -105,6 +110,13 @@ constexpr const char* kValveIndexInputProfiles[] = { constexpr const char* kHTCViveInputProfiles[] = { "htc-vive", "generic-trigger-squeeze-touchpad"}; +constexpr const char* kSamsungOdysseyInputProfiles[] = { + "samsung-odyssey", "windows-mixed-reality", + "generic-trigger-squeeze-touchpad-thumbstick"}; + +constexpr const char* kHPReverbG2InputProfiles[] = { + "hp-mixed-reality", "oculus-touch", "generic-trigger-squeeze"}; + constexpr OpenXrButtonPathMap kMicrosoftMotionControllerButtonPathMaps[] = { {OpenXrButtonType::kTrigger, { @@ -205,7 +217,7 @@ constexpr OpenXrButtonPathMap kValveIndexControllerButtonPathMaps[] = { {{OpenXrButtonActionType::kPress, "/input/a/click"}, {OpenXrButtonActionType::kTouch, "/input/a/touch"}}, 2}, -}; // namespace device +}; constexpr OpenXrButtonPathMap kHTCViveControllerButtonPathMaps[] = { {OpenXrButtonType::kTrigger, @@ -222,6 +234,46 @@ constexpr OpenXrButtonPathMap kHTCViveControllerButtonPathMaps[] = { {OpenXrButtonActionType::kTouch, "/input/trackpad/touch"}}, 2}}; +constexpr OpenXrButtonPathMap kHPReverbG2LeftControllerButtonPathMaps[] = { + {OpenXrButtonType::kTrigger, + {{OpenXrButtonActionType::kPress, "/input/trigger/value"}, + {OpenXrButtonActionType::kValue, "/input/trigger/value"}}, + 2}, + {OpenXrButtonType::kSqueeze, + {{OpenXrButtonActionType::kPress, "/input/squeeze/value"}, + {OpenXrButtonActionType::kValue, "/input/squeeze/value"}}, + 2}, + {OpenXrButtonType::kThumbstick, + {{OpenXrButtonActionType::kPress, "/input/thumbstick/click"}}, + 1}, + {OpenXrButtonType::kButton1, + {{OpenXrButtonActionType::kPress, "/input/x/click"}}, + 1}, + {OpenXrButtonType::kButton2, + {{OpenXrButtonActionType::kPress, "/input/y/click"}}, + 1}, +}; + +constexpr OpenXrButtonPathMap kHPReverbG2RightControllerButtonPathMaps[] = { + {OpenXrButtonType::kTrigger, + {{OpenXrButtonActionType::kPress, "/input/trigger/value"}, + {OpenXrButtonActionType::kValue, "/input/trigger/value"}}, + 2}, + {OpenXrButtonType::kSqueeze, + {{OpenXrButtonActionType::kPress, "/input/squeeze/value"}, + {OpenXrButtonActionType::kValue, "/input/squeeze/value"}}, + 2}, + {OpenXrButtonType::kThumbstick, + {{OpenXrButtonActionType::kPress, "/input/thumbstick/click"}}, + 1}, + {OpenXrButtonType::kButton1, + {{OpenXrButtonActionType::kPress, "/input/a/click"}}, + 1}, + {OpenXrButtonType::kButton2, + {{OpenXrButtonActionType::kPress, "/input/b/click"}}, + 1}, +}; + constexpr OpenXrAxisPathMap kMicrosoftMotionControllerAxisPathMaps[] = { {OpenXrAxisType::kTrackpad, "/input/trackpad"}, {OpenXrAxisType::kThumbstick, "/input/thumbstick"}, @@ -240,10 +292,15 @@ constexpr OpenXrAxisPathMap kHTCViveControllerAxisPathMaps[] = { {OpenXrAxisType::kTrackpad, "/input/trackpad"}, }; +constexpr OpenXrAxisPathMap kHPReverbG2ControllerAxisPathMaps[] = { + {OpenXrAxisType::kThumbstick, "/input/thumbstick"}, +}; + constexpr OpenXrControllerInteractionProfile kMicrosoftMotionInteractionProfile = { OpenXrInteractionProfileType::kMicrosoftMotion, "/interaction_profiles/microsoft/motion_controller", + nullptr, GamepadMapping::kXrStandard, kMicrosoftMotionInputProfiles, base::size(kMicrosoftMotionInputProfiles), @@ -257,6 +314,7 @@ constexpr OpenXrControllerInteractionProfile constexpr OpenXrControllerInteractionProfile kKHRSimpleInteractionProfile = { OpenXrInteractionProfileType::kKHRSimple, "/interaction_profiles/khr/simple_controller", + nullptr, GamepadMapping::kNone, kGenericButtonInputProfiles, base::size(kGenericButtonInputProfiles), @@ -270,6 +328,7 @@ constexpr OpenXrControllerInteractionProfile kKHRSimpleInteractionProfile = { constexpr OpenXrControllerInteractionProfile kOculusTouchInteractionProfile = { OpenXrInteractionProfileType::kOculusTouch, "/interaction_profiles/oculus/touch_controller", + nullptr, GamepadMapping::kXrStandard, kOculusTouchInputProfiles, base::size(kOculusTouchInputProfiles), @@ -283,6 +342,7 @@ constexpr OpenXrControllerInteractionProfile kOculusTouchInteractionProfile = { constexpr OpenXrControllerInteractionProfile kValveIndexInteractionProfile = { OpenXrInteractionProfileType::kValveIndex, "/interaction_profiles/valve/index_controller", + nullptr, GamepadMapping::kXrStandard, kValveIndexInputProfiles, base::size(kValveIndexInputProfiles), @@ -296,6 +356,7 @@ constexpr OpenXrControllerInteractionProfile kValveIndexInteractionProfile = { constexpr OpenXrControllerInteractionProfile kHTCViveInteractionProfile = { OpenXrInteractionProfileType::kHTCVive, "/interaction_profiles/htc/vive_controller", + nullptr, GamepadMapping::kXrStandard, kHTCViveInputProfiles, base::size(kHTCViveInputProfiles), @@ -306,11 +367,40 @@ constexpr OpenXrControllerInteractionProfile kHTCViveInteractionProfile = { kHTCViveControllerAxisPathMaps, base::size(kHTCViveControllerAxisPathMaps)}; +constexpr OpenXrControllerInteractionProfile kSamsungOdysseyInteractionProfile = + {OpenXrInteractionProfileType::kSamsungOdyssey, + "/interaction_profiles/samsung/odyssey_controller", + kExtSamsungOdysseyControllerExtensionName, + GamepadMapping::kXrStandard, + kSamsungOdysseyInputProfiles, + base::size(kSamsungOdysseyInputProfiles), + kMicrosoftMotionControllerButtonPathMaps, + base::size(kMicrosoftMotionControllerButtonPathMaps), + kMicrosoftMotionControllerButtonPathMaps, + base::size(kMicrosoftMotionControllerButtonPathMaps), + kMicrosoftMotionControllerAxisPathMaps, + base::size(kMicrosoftMotionControllerAxisPathMaps)}; + +constexpr OpenXrControllerInteractionProfile kHPReverbG2InteractionProfile = { + OpenXrInteractionProfileType::kHPReverbG2, + "/interaction_profiles/hp/mixed_reality_controller", + kExtHPMixedRealityControllerExtensionName, + GamepadMapping::kXrStandard, + kHPReverbG2InputProfiles, + base::size(kHPReverbG2InputProfiles), + kHPReverbG2LeftControllerButtonPathMaps, + base::size(kHPReverbG2LeftControllerButtonPathMaps), + kHPReverbG2RightControllerButtonPathMaps, + base::size(kHPReverbG2RightControllerButtonPathMaps), + kHPReverbG2ControllerAxisPathMaps, + base::size(kHPReverbG2ControllerAxisPathMaps)}; + constexpr OpenXrControllerInteractionProfile kOpenXrControllerInteractionProfiles[] = { kMicrosoftMotionInteractionProfile, kKHRSimpleInteractionProfile, - kOculusTouchInteractionProfile, kValveIndexInteractionProfile, - kHTCViveInteractionProfile}; + kOculusTouchInteractionProfile, kValveIndexInteractionProfile, + kHTCViveInteractionProfile, kSamsungOdysseyInteractionProfile, + kHPReverbG2InteractionProfile}; } // namespace device diff --git a/chromium/device/vr/openxr/openxr_render_loop.cc b/chromium/device/vr/openxr/openxr_render_loop.cc index 6d37f401340..de8f53b5e1d 100644 --- a/chromium/device/vr/openxr/openxr_render_loop.cc +++ b/chromium/device/vr/openxr/openxr_render_loop.cc @@ -168,8 +168,6 @@ void OpenXrRenderLoop::InitializeDisplayInfo() { current_display_info_->left_eye = mojom::VREyeParameters::New(); } - current_display_info_->id = device::mojom::XRDeviceId::OPENXR_DEVICE_ID; - gfx::Size view_size = openxr_->GetViewSize(); current_display_info_->left_eye->render_width = view_size.width(); current_display_info_->right_eye->render_width = view_size.width(); diff --git a/chromium/device/vr/openxr/openxr_util.cc b/chromium/device/vr/openxr/openxr_util.cc index 89b5a2273eb..82d38060c79 100644 --- a/chromium/device/vr/openxr/openxr_util.cc +++ b/chromium/device/vr/openxr/openxr_util.cc @@ -70,7 +70,8 @@ OpenXrExtensionHelper::OpenXrExtensionHelper() { OpenXrExtensionHelper::~OpenXrExtensionHelper() = default; -bool OpenXrExtensionHelper::ExtensionSupported(const char* extension_name) { +bool OpenXrExtensionHelper::ExtensionSupported( + const char* extension_name) const { return std::find_if( extension_properties_.begin(), extension_properties_.end(), [&extension_name](const XrExtensionProperties& properties) { @@ -134,6 +135,22 @@ XrResult CreateInstance(XrInstance* instance) { extensions.push_back(XR_MSFT_UNBOUNDED_REFERENCE_SPACE_EXTENSION_NAME); } + // Input extensions. These enable interaction profiles not defined in the core + // spec + const bool samsungInteractionProfileExtensionSupported = + extension_helper.ExtensionSupported( + kExtSamsungOdysseyControllerExtensionName); + if (samsungInteractionProfileExtensionSupported) { + extensions.push_back(kExtSamsungOdysseyControllerExtensionName); + } + + const bool hpControllerExtensionSupported = + extension_helper.ExtensionSupported( + kExtHPMixedRealityControllerExtensionName); + if (hpControllerExtensionSupported) { + extensions.push_back(kExtHPMixedRealityControllerExtensionName); + } + instance_create_info.enabledExtensionCount = static_cast<uint32_t>(extensions.size()); instance_create_info.enabledExtensionNames = extensions.data(); diff --git a/chromium/device/vr/openxr/openxr_util.h b/chromium/device/vr/openxr/openxr_util.h index 9ba08da712c..31d55162b32 100644 --- a/chromium/device/vr/openxr/openxr_util.h +++ b/chromium/device/vr/openxr/openxr_util.h @@ -16,7 +16,7 @@ class OpenXrExtensionHelper { OpenXrExtensionHelper(); ~OpenXrExtensionHelper(); - bool ExtensionSupported(const char* extension_name); + bool ExtensionSupported(const char* extension_name) const; private: std::vector<XrExtensionProperties> extension_properties_; diff --git a/chromium/device/vr/orientation/orientation_device.cc b/chromium/device/vr/orientation/orientation_device.cc index a69e9278ecf..0c1ca109e98 100644 --- a/chromium/device/vr/orientation/orientation_device.cc +++ b/chromium/device/vr/orientation/orientation_device.cc @@ -27,12 +27,6 @@ using gfx::Vector3dF; namespace { static constexpr int kDefaultPumpFrequencyHz = 60; -mojom::VRDisplayInfoPtr CreateVRDisplayInfo(mojom::XRDeviceId id) { - mojom::VRDisplayInfoPtr display_info = mojom::VRDisplayInfo::New(); - display_info->id = id; - return display_info; -} - display::Display::Rotation GetRotation() { display::Screen* screen = display::Screen::GetScreen(); if (!screen) { @@ -54,7 +48,7 @@ VROrientationDevice::VROrientationDevice(mojom::SensorProvider* sensor_provider, base::BindOnce(&VROrientationDevice::SensorReady, base::Unretained(this))); - SetVRDisplayInfo(CreateVRDisplayInfo(GetId())); + SetVRDisplayInfo(mojom::VRDisplayInfo::New()); } VROrientationDevice::~VROrientationDevice() { @@ -170,10 +164,6 @@ void VROrientationDevice::GetInlineFrameData( mojom::XRFrameDataProvider::GetFrameDataCallback callback) { // Orientation sessions should never be exclusive or presenting. DCHECK(!HasExclusiveSession()); - if (!inline_poses_enabled_) { - std::move(callback).Run(nullptr); - return; - } mojom::VRPosePtr pose = mojom::VRPose::New(); diff --git a/chromium/device/vr/orientation/orientation_device_unittest.cc b/chromium/device/vr/orientation/orientation_device_unittest.cc index 3667a9f3c74..7bd19b9f26e 100644 --- a/chromium/device/vr/orientation/orientation_device_unittest.cc +++ b/chromium/device/vr/orientation/orientation_device_unittest.cc @@ -164,10 +164,6 @@ class VROrientationDeviceTest : public testing::Test { } } - void SetInlinePosesEnabled(bool enabled) { - device_->SetInlinePosesEnabled(enabled); - } - std::unique_ptr<VROrientationSession> MakeDisplay() { mojo::PendingRemote<mojom::XRFrameDataProvider> data_provider; mojo::PendingRemote<mojom::XRSessionController> controller; @@ -383,14 +379,6 @@ TEST_F(VROrientationDeviceTest, OrientationLandscape270Test) { })); } -TEST_F(VROrientationDeviceTest, NoMagicWindowPosesWhileBrowsing) { - InitializeDevice(FakeInitParams()); - - AssertInlineFrameDataAvailable(true); - SetInlinePosesEnabled(false); - AssertInlineFrameDataAvailable(false); -} - TEST_F(VROrientationDeviceTest, GetFrameDataHelper) { InitializeDevice(FakeInitParams()); diff --git a/chromium/device/vr/public/mojom/BUILD.gn b/chromium/device/vr/public/mojom/BUILD.gn index 56752b7b348..3f887e4eab8 100644 --- a/chromium/device/vr/public/mojom/BUILD.gn +++ b/chromium/device/vr/public/mojom/BUILD.gn @@ -28,9 +28,6 @@ mojom_component("mojom") { if (enable_oculus_vr) { enabled_features += [ "enable_oculus_vr" ] } - if (enable_openvr) { - enabled_features += [ "enable_openvr" ] - } if (enable_windows_mr) { enabled_features += [ "enable_windows_mr" ] } diff --git a/chromium/device/vr/public/mojom/README.md b/chromium/device/vr/public/mojom/README.md index 3b621bb8d71..f78552e3f1c 100644 --- a/chromium/device/vr/public/mojom/README.md +++ b/chromium/device/vr/public/mojom/README.md @@ -85,5 +85,5 @@ on the platform. # Test interfaces (defined in browser_test_interfaces.mojom) XRTestHook allows a test to control the behavior of a fake implementation of -OpenVR, and potentially other runtimes. This allows testing the entire stack +OpenXR, and potentially other runtimes. This allows testing the entire stack of Chromium WebXR code end-to-end. diff --git a/chromium/device/vr/public/mojom/isolated_xr_service.mojom b/chromium/device/vr/public/mojom/isolated_xr_service.mojom index 12235c2a141..842ad9865a2 100644 --- a/chromium/device/vr/public/mojom/isolated_xr_service.mojom +++ b/chromium/device/vr/public/mojom/isolated_xr_service.mojom @@ -74,8 +74,21 @@ interface XRRuntime { ListenToDeviceChanges( pending_associated_remote<XRRuntimeEventListener> listener) => (VRDisplayInfo? display_info); +}; - SetInlinePosesEnabled(bool enable); +// Information required for rendering, used by ImmersiveOverlay. +// TODO(https://crbug.com/1126608): This struct currently contains subset of +// data contained by XRFrameData, move it to vr_service.mojom and refactor +// XRFrameData so that it embeds this struct. +struct XRRenderInfo +{ + // The frame_id maps frame data to a frame arriving from the compositor. IDs + // will be reused after the frame arrives from the compositor. Negative IDs + // imply no mapping. + int16 frame_id; + + // The pose may be null if the device lost tracking. + VRPose? pose; }; // Represents an overlay that the browser may show on top of or instead of WebXR @@ -84,7 +97,7 @@ interface XRRuntime { interface ImmersiveOverlay { // Request a pose. If there is WebXR and an overlay visible at the same time, // the same pose will be given to both. - RequestNextOverlayPose() => (device.mojom.XRFrameData pose); + RequestNextOverlayPose() => (XRRenderInfo render_info); // Submit a frame to show in the headset. Only can be called when an overlay // is visible. The frame will be composited on top of WebXR content. diff --git a/chromium/device/vr/public/mojom/vr_service.mojom b/chromium/device/vr/public/mojom/vr_service.mojom index 0d8c7ac22ac..32ce3b521ee 100644 --- a/chromium/device/vr/public/mojom/vr_service.mojom +++ b/chromium/device/vr/public/mojom/vr_service.mojom @@ -5,6 +5,7 @@ module device.mojom; import "device/gamepad/public/mojom/gamepad.mojom"; +import "mojo/public/mojom/base/big_buffer.mojom"; import "mojo/public/mojom/base/time.mojom"; import "gpu/ipc/common/mailbox_holder.mojom"; import "gpu/ipc/common/sync_token.mojom"; @@ -22,13 +23,13 @@ import "ui/gfx/mojom/transform.mojom"; // TODO(https://crbug.com/966099): Use EnableIf to only define values on // platforms that have implementations. -// XRDeviceId is used in metrics, so don't reorder. +// XRDeviceId is used in metrics, so don't reorder or reuse. enum XRDeviceId { WEB_TEST_DEVICE_ID = 0, // Fake device used by web_tests. FAKE_DEVICE_ID = 1, // Fake device used in unit tests. ORIENTATION_DEVICE_ID = 2, GVR_DEVICE_ID = 3, - [EnableIf=enable_openvr] OPENVR_DEVICE_ID = 4, + // OPENVR_DEVICE_ID = 4, [EnableIf=enable_oculus_vr] OCULUS_DEVICE_ID = 5, [EnableIf=enable_windows_mr] WINDOWS_MIXED_REALITY_ID = 6, ARCORE_DEVICE_ID = 7, @@ -62,6 +63,7 @@ enum XRSessionFeature { ANCHORS = 9, CAMERA_ACCESS = 10, // Experimental feature. PLANE_DETECTION = 11, // Experimental feature. + DEPTH = 12, // Experimental feature. }; // These values are persisted to logs. Entries should not be renumbered and @@ -127,6 +129,10 @@ struct XRSession { // Indicates whether the device backing this session sends input events solely // via eventing (as opposed to polling). bool uses_input_eventing; + + // The default scale that should be applied to the native framebuffer size + // unless overridden by the developer. + float default_framebuffer_scale = 1.0; }; // This structure contains the infomation and interfaces needed to create a two @@ -296,13 +302,11 @@ struct VRStageParameters { }; struct VRDisplayInfo { - XRDeviceId id; VRStageParameters? stage_parameters; // Parameters required to distort a scene for viewing in a VR headset. Only // required for devices which have the can_present capability. VREyeParameters? left_eye; VREyeParameters? right_eye; - float webxr_default_framebuffer_scale = 1.0; }; // Frame transport method from the Renderer's point of view. @@ -535,6 +539,36 @@ struct XRLightEstimationData { XRReflectionProbe? reflection_probe; }; +// Structure that signifies that the depth data is still valid, no additional +// information is provided. See |XRDepthData| for more details. +struct XRDepthDataStillValid {}; + +// Structure that signifies that the Depth data was updated. Provides +// information about current depth data. See |XRDepthData| for more details. +struct XRDepthDataUpdated { + // Timestamp of the returned data, in unspecified base. + mojo_base.mojom.TimeDelta time_delta; + // Array of 16-bit numbers representing depth in millimeters from the camera + // plane. + mojo_base.mojom.BigBuffer pixel_data; + // Transform that needs to be applied when indexing into pixel_data when using + // normalized view coordinates (with origin at bottom left corner of the + // screen). + gfx.mojom.Transform norm_texture_from_norm_view; + // Size of the pixel array. Valid iff pixel_data is not null. + gfx.mojom.Size size; +}; + +// Depth data may be the same as the one returned in the previous frame - it is +// represented as a union of (empty) XRDepthDataStillValid structure that +// conveys no additional information except that the previously returned depth +// data is still valid, and the XRDepthDataUpdated structure that signifies that +// the depth data was updated & carries all information about it. +union XRDepthData { + XRDepthDataStillValid data_still_valid; + XRDepthDataUpdated updated_depth_data; +}; + // The data needed for each animation frame of an XRSession. struct XRFrameData { // General XRSession value @@ -547,10 +581,18 @@ struct XRFrameData { // The buffer_holder is used for sending imagery data back and forth across // the process boundary. gpu.mojom.MailboxHolder? buffer_holder; + // The camera_image_buffer_holder is used to send a copy of the camera image - // across the process boundary for immersive-ar sessions. + // across the process boundary for immersive-ar sessions. This assumes that + // there exists a single camera. gpu.mojom.MailboxHolder? camera_image_buffer_holder; + // Depth data (if the device supports it and the environment integration is + // enabled). This assumes that depth information is provided from a single + // sensor. If for any reason the latest depth data could not be obtained, it + // will be set to null. + XRDepthData? depth_data; + // Indicates that there has been a significant discontinuity in the mojo space // coordinate system, and that poses from this point forward with the same // coordinates as those received previously may not actually be in the same @@ -660,10 +702,11 @@ union RequestSessionResult { // process is compatible with the active VR headset. enum XrCompatibleResult { // Compatibility results where the GPU process was not restarted. - kAlreadyCompatible, - kNotCompatible, + kAlreadyCompatible, // GPU process is already on a compatible GPU. + kNoDeviceAvailable, // No WebXR device is available. + kWebXrFeaturePolicyBlocked, // WebXR is blocked by feature policy. - // Compatibility results where the GPU was restarted. Context lost and + // Compatibility results where the GPU process was restarted. Context lost and // restored for existing WebGL contexts must be handled before using for XR. kCompatibleAfterRestart, // XR compatible, GPU process was restarted. kNotCompatibleAfterRestart, // Not XR compatible, GPU process was restarted. diff --git a/chromium/device/vr/test/test_hook.h b/chromium/device/vr/test/test_hook.h index 6c2db8e236b..0d51c4a9211 100644 --- a/chromium/device/vr/test/test_hook.h +++ b/chromium/device/vr/test/test_hook.h @@ -14,7 +14,6 @@ namespace device { // Update this string whenever either interface changes. -constexpr char kChromeOpenVRTestHookAPI[] = "ChromeTestHook_3"; constexpr unsigned int kMaxTrackedDevices = 64; constexpr unsigned int kMaxNumAxes = 5; diff --git a/chromium/device/vr/vr_device.h b/chromium/device/vr/vr_device.h index 5193d1b212f..99bf6a5480b 100644 --- a/chromium/device/vr/vr_device.h +++ b/chromium/device/vr/vr_device.h @@ -21,9 +21,9 @@ enum class VrViewerType { GVR_DAYDREAM = 2, ORIENTATION_SENSOR_DEVICE = 10, FAKE_DEVICE = 11, - OPENVR_UNKNOWN = 20, - OPENVR_VIVE = 21, - OPENVR_RIFT_CV1 = 22, + // OPENVR_UNKNOWN = 20, + // OPENVR_VIVE = 21, + // OPENVR_RIFT_CV1 = 22, OCULUS_UNKNOWN = 40, // Going through Oculus APIs WINDOWS_MIXED_REALITY_UNKNOWN = 60, // Going through WMR APIs OPENXR_UNKNOWN = 70, // Going through OpenXR APIs diff --git a/chromium/device/vr/vr_device_base.cc b/chromium/device/vr/vr_device_base.cc index 9d113bb653d..d72cb0980f4 100644 --- a/chromium/device/vr/vr_device_base.cc +++ b/chromium/device/vr/vr_device_base.cc @@ -66,7 +66,6 @@ void VRDeviceBase::ListenToDeviceChanges( void VRDeviceBase::SetVRDisplayInfo(mojom::VRDisplayInfoPtr display_info) { DCHECK(display_info); - DCHECK(display_info->id == id_); display_info_ = std::move(display_info); if (listener_) @@ -93,10 +92,6 @@ mojo::PendingRemote<mojom::XRRuntime> VRDeviceBase::BindXRRuntime() { return runtime_receiver_.BindNewPipeAndPassRemote(); } -void VRDeviceBase::SetInlinePosesEnabled(bool enable) { - inline_poses_enabled_ = enable; -} - void LogViewerType(VrViewerType type) { base::UmaHistogramSparse("VRViewerType", static_cast<int>(type)); } diff --git a/chromium/device/vr/vr_device_base.h b/chromium/device/vr/vr_device_base.h index c16a63ca192..b5e9ef8b8e6 100644 --- a/chromium/device/vr/vr_device_base.h +++ b/chromium/device/vr/vr_device_base.h @@ -32,7 +32,6 @@ class COMPONENT_EXPORT(DEVICE_VR_BASE) VRDeviceBase : public mojom::XRRuntime { void ListenToDeviceChanges( mojo::PendingAssociatedRemote<mojom::XRRuntimeEventListener> listener, mojom::XRRuntime::ListenToDeviceChangesCallback callback) final; - void SetInlinePosesEnabled(bool enable) override; void ShutdownSession(mojom::XRRuntime::ShutdownSessionCallback) override; device::mojom::XRDeviceId GetId() const; @@ -69,8 +68,6 @@ class COMPONENT_EXPORT(DEVICE_VR_BASE) VRDeviceBase : public mojom::XRRuntime { mojom::VRDisplayInfoPtr display_info_; - bool inline_poses_enabled_ = true; - private: mojo::AssociatedRemote<mojom::XRRuntimeEventListener> listener_; diff --git a/chromium/device/vr/vr_device_base_unittest.cc b/chromium/device/vr/vr_device_base_unittest.cc index 7a911a2fa4e..c72e8dd4b49 100644 --- a/chromium/device/vr/vr_device_base_unittest.cc +++ b/chromium/device/vr/vr_device_base_unittest.cc @@ -72,16 +72,10 @@ class VRDeviceTest : public testing::Test { std::unique_ptr<VRDeviceBaseForTesting> MakeVRDevice() { std::unique_ptr<VRDeviceBaseForTesting> device = std::make_unique<VRDeviceBaseForTesting>(); - device->SetVRDisplayInfoForTest(MakeVRDisplayInfo(device->GetId())); + device->SetVRDisplayInfoForTest(mojom::VRDisplayInfo::New()); return device; } - mojom::VRDisplayInfoPtr MakeVRDisplayInfo(mojom::XRDeviceId device_id) { - mojom::VRDisplayInfoPtr display_info = mojom::VRDisplayInfo::New(); - display_info->id = device_id; - return display_info; - } - base::test::SingleThreadTaskEnvironment task_environment_; DISALLOW_COPY_AND_ASSIGN(VRDeviceTest); @@ -99,7 +93,7 @@ TEST_F(VRDeviceTest, DeviceChangedDispatched) { base::DoNothing()); // TODO: consider getting initial info base::RunLoop().RunUntilIdle(); EXPECT_CALL(listener, DoOnChanged(testing::_)).Times(1); - device->SetVRDisplayInfoForTest(MakeVRDisplayInfo(device->GetId())); + device->SetVRDisplayInfoForTest(mojom::VRDisplayInfo::New()); base::RunLoop().RunUntilIdle(); } diff --git a/chromium/device/vr/vr_gl_util.cc b/chromium/device/vr/vr_gl_util.cc new file mode 100644 index 00000000000..c289b690dcd --- /dev/null +++ b/chromium/device/vr/vr_gl_util.cc @@ -0,0 +1,105 @@ +// Copyright 2016 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "device/vr/vr_gl_util.h" + +#include "ui/gfx/transform.h" + +namespace vr { + +// This code is adapted from the GVR Treasure Hunt demo source. +std::array<float, 16> MatrixToGLArray(const gfx::Transform& transform) { + std::array<float, 16> result; + transform.matrix().asColMajorf(result.data()); + return result; +} + +GLuint CompileShader(GLenum shader_type, + const std::string& shader_source, + std::string& error) { + GLuint shader_handle = glCreateShader(shader_type); + if (shader_handle != 0) { + // Pass in the shader source. No need to pass in a length for + // null-terminated input. + const char* source = shader_source.c_str(); + glShaderSource(shader_handle, 1, &source, nullptr); + // Compile the shader. + glCompileShader(shader_handle); + // Get the compilation status. + GLint status = GL_FALSE; + glGetShaderiv(shader_handle, GL_COMPILE_STATUS, &status); + if (status == GL_FALSE) { + GLint info_log_length = 0; + glGetShaderiv(shader_handle, GL_INFO_LOG_LENGTH, &info_log_length); + GLchar* str_info_log = new GLchar[info_log_length + 1]; + glGetShaderInfoLog(shader_handle, info_log_length, nullptr, str_info_log); + error = "Error compiling shader: "; + error += str_info_log; + delete[] str_info_log; + glDeleteShader(shader_handle); + shader_handle = 0; + } + } else { + error = "Could not create a shader handle (did not attempt compilation)."; + } + + return shader_handle; +} + +GLuint CreateAndLinkProgram(GLuint vertext_shader_handle, + GLuint fragment_shader_handle, + std::string& error) { + GLuint program_handle = glCreateProgram(); + + if (program_handle != 0) { + // Bind the vertex shader to the program. + glAttachShader(program_handle, vertext_shader_handle); + + // Bind the fragment shader to the program. + glAttachShader(program_handle, fragment_shader_handle); + + // Link the two shaders together into a program. + glLinkProgram(program_handle); + + // Get the link status. + GLint link_status = GL_FALSE; + glGetProgramiv(program_handle, GL_LINK_STATUS, &link_status); + + // If the link failed, delete the program. + if (link_status == GL_FALSE) { + GLint info_log_length = 0; + glGetProgramiv(program_handle, GL_INFO_LOG_LENGTH, &info_log_length); + + GLchar* str_info_log = new GLchar[info_log_length + 1]; + glGetProgramInfoLog(program_handle, info_log_length, nullptr, + str_info_log); + error = "Error compiling program: "; + error += str_info_log; + delete[] str_info_log; + glDeleteProgram(program_handle); + program_handle = 0; + } + } + + return program_handle; +} + +void SetTexParameters(GLenum texture_type) { + glTexParameteri(texture_type, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(texture_type, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexParameteri(texture_type, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(texture_type, GL_TEXTURE_MAG_FILTER, GL_NEAREST); +} + +void SetColorUniform(GLuint handle, SkColor c) { + glUniform4f(handle, SkColorGetR(c) / 255.0, SkColorGetG(c) / 255.0, + SkColorGetB(c) / 255.0, SkColorGetA(c) / 255.0); +} + +void SetOpaqueColorUniform(GLuint handle, SkColor c) { + glUniform3f(handle, SkColorGetR(c) / 255.0, SkColorGetG(c) / 255.0, + SkColorGetB(c) / 255.0); +} + +} // namespace vr diff --git a/chromium/device/vr/vr_gl_util.h b/chromium/device/vr/vr_gl_util.h new file mode 100644 index 00000000000..e84543df4d1 --- /dev/null +++ b/chromium/device/vr/vr_gl_util.h @@ -0,0 +1,56 @@ +// Copyright 2016 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef DEVICE_VR_VR_GL_UTIL_H_ +#define DEVICE_VR_VR_GL_UTIL_H_ + +#include <array> +#include <string> + +#include "base/component_export.h" +#include "device/vr/gl_bindings.h" +#include "third_party/skia/include/core/SkColor.h" + +#define SHADER(Src) "#version 100\n" #Src +#define OEIE_SHADER(Src) \ + "#version 100\n#extension GL_OES_EGL_image_external : require\n" #Src +#define VOID_OFFSET(x) reinterpret_cast<void*>(x) + +namespace gfx { +class Transform; +} // namespace gfx + +namespace vr { + +COMPONENT_EXPORT(DEVICE_VR_BASE) +std::array<float, 16> MatrixToGLArray(const gfx::Transform& matrix); + +// Compile a shader. This is intended for browser-internal shaders only, +// don't use this for user-supplied arbitrary shaders since data is handed +// directly to the GL driver without further sanity checks. +COMPONENT_EXPORT(DEVICE_VR_BASE) +GLuint CompileShader(GLenum shader_type, + const std::string& shader_source, + std::string& error); + +// Compile and link a program. +COMPONENT_EXPORT(DEVICE_VR_BASE) +GLuint CreateAndLinkProgram(GLuint vertex_shader_handle, + GLuint fragment_shader_handle, + std::string& error); + +// Sets default texture parameters given a texture type. +COMPONENT_EXPORT(DEVICE_VR_BASE) void SetTexParameters(GLenum texture_type); + +// Sets color uniforms given an SkColor. +COMPONENT_EXPORT(DEVICE_VR_BASE) void SetColorUniform(GLuint handle, SkColor c); + +// Sets color uniforms (but not alpha) given an SkColor. The alpha is assumed to +// be 1.0 in this case. +COMPONENT_EXPORT(DEVICE_VR_BASE) +void SetOpaqueColorUniform(GLuint handle, SkColor c); + +} // namespace vr + +#endif // DEVICE_VR_VR_GL_UTIL_H_ diff --git a/chromium/device/vr/windows/compositor_base.cc b/chromium/device/vr/windows/compositor_base.cc index 2f9004f45a9..87ea395ac59 100644 --- a/chromium/device/vr/windows/compositor_base.cc +++ b/chromium/device/vr/windows/compositor_base.cc @@ -17,6 +17,17 @@ namespace { // Number of frames to use for sliding averages for pose timings, // as used for estimating prediction times. constexpr unsigned kSlidingAverageSize = 5; + +device::mojom::XRRenderInfoPtr GetRenderInfo( + const device::mojom::XRFrameData& frame_data) { + device::mojom::XRRenderInfoPtr result = device::mojom::XRRenderInfo::New(); + + result->frame_id = frame_data.frame_id; + result->pose = frame_data.pose.Clone(); + + return result; +} + } // namespace namespace device { @@ -100,7 +111,8 @@ void XRCompositorCommon::SubmitFrameWithTextureHandle( if (on_webxr_submitted_) std::move(on_webxr_submitted_).Run(); - if (!pending_frame_ || pending_frame_->frame_data_->frame_id != frame_index) { + if (!pending_frame_ || + pending_frame_->render_info_->frame_id != frame_index) { // We weren't expecting a submitted frame. This can happen if WebXR was // hidden by an overlay for some time. if (submit_client_) { @@ -283,8 +295,9 @@ void XRCompositorCommon::StartPendingFrame() { pending_frame_->waiting_for_webxr_ = webxr_visible_; pending_frame_->waiting_for_overlay_ = overlay_visible_; pending_frame_->frame_data_ = GetNextFrameData(); - // pending_frame_->frame_data_ should never be null + // GetNextFrameData() should never return null: DCHECK(pending_frame_->frame_data_); + pending_frame_->render_info_ = GetRenderInfo(*pending_frame_->frame_data_); } } @@ -327,9 +340,9 @@ void XRCompositorCommon::GetFrameData( // specifically the next gamepad callback request that's likely to // have been sent during WaitGetPoses. task_runner()->PostTask( - FROM_HERE, - base::BindOnce(&XRCompositorCommon::SendFrameData, base::Unretained(this), - std::move(callback), pending_frame_->frame_data_.Clone())); + FROM_HERE, base::BindOnce(&XRCompositorCommon::SendFrameData, + base::Unretained(this), std::move(callback), + std::move(pending_frame_->frame_data_))); next_frame_id_ += 1; if (next_frame_id_ < 0) { @@ -413,7 +426,7 @@ void XRCompositorCommon::RequestNextOverlayPose( // Ensure we have a pending frame. StartPendingFrame(); pending_frame_->overlay_has_pose_ = true; - std::move(callback).Run(pending_frame_->frame_data_.Clone()); + std::move(callback).Run(pending_frame_->render_info_->Clone()); } void XRCompositorCommon::SetOverlayAndWebXRVisibility(bool overlay_visible, diff --git a/chromium/device/vr/windows/compositor_base.h b/chromium/device/vr/windows/compositor_base.h index a5f378d383b..375fc124843 100644 --- a/chromium/device/vr/windows/compositor_base.h +++ b/chromium/device/vr/windows/compositor_base.h @@ -149,7 +149,9 @@ class XRCompositorCommon : public base::Thread, bool overlay_submitted_ = false; bool waiting_for_webxr_ = false; bool waiting_for_overlay_ = false; + mojom::XRFrameDataPtr frame_data_; + mojom::XRRenderInfoPtr render_info_; base::TimeTicks sent_frame_data_time_; base::TimeTicks submit_frame_time_; diff --git a/chromium/device/vr/windows_mixed_reality/mixed_reality_device.cc b/chromium/device/vr/windows_mixed_reality/mixed_reality_device.cc index 24fe7d01be0..4a1dde0d4de 100644 --- a/chromium/device/vr/windows_mixed_reality/mixed_reality_device.cc +++ b/chromium/device/vr/windows_mixed_reality/mixed_reality_device.cc @@ -27,9 +27,8 @@ namespace { // However our mojo interface expects display info right away to support WebVR. // We create a fake display info to use, then notify the client that the display // info changed when we get real data. -mojom::VRDisplayInfoPtr CreateFakeVRDisplayInfo(device::mojom::XRDeviceId id) { +mojom::VRDisplayInfoPtr CreateFakeVRDisplayInfo() { mojom::VRDisplayInfoPtr display_info = mojom::VRDisplayInfo::New(); - display_info->id = id; display_info->left_eye = mojom::VREyeParameters::New(); display_info->right_eye = mojom::VREyeParameters::New(); @@ -56,7 +55,7 @@ mojom::VRDisplayInfoPtr CreateFakeVRDisplayInfo(device::mojom::XRDeviceId id) { MixedRealityDevice::MixedRealityDevice() : VRDeviceBase(device::mojom::XRDeviceId::WINDOWS_MIXED_REALITY_ID) { - SetVRDisplayInfo(CreateFakeVRDisplayInfo(GetId())); + SetVRDisplayInfo(CreateFakeVRDisplayInfo()); } MixedRealityDevice::~MixedRealityDevice() { diff --git a/chromium/device/vr/windows_mixed_reality/mixed_reality_renderloop.cc b/chromium/device/vr/windows_mixed_reality/mixed_reality_renderloop.cc index 7524e7b08af..77d83ab1917 100644 --- a/chromium/device/vr/windows_mixed_reality/mixed_reality_renderloop.cc +++ b/chromium/device/vr/windows_mixed_reality/mixed_reality_renderloop.cc @@ -671,8 +671,6 @@ bool MixedRealityRenderLoop::UpdateDisplayInfo() { if (!current_display_info_) { current_display_info_ = mojom::VRDisplayInfo::New(); - current_display_info_->id = - device::mojom::XRDeviceId::WINDOWS_MIXED_REALITY_ID; changed = true; } |