From 18b2a4e95732c4b53f56941db0ac0eb261d15875 Mon Sep 17 00:00:00 2001 From: John Kleinschmidt Date: Thu, 1 Jun 2023 10:12:48 -0400 Subject: [PATCH] feat: add USB protected classes handler (#38498) feat: add USB protected classes handler (#38263) * feat: add USB protected classes handler * chore: apply review suggestions Co-authored-by: Charles Kerr * chore: update docs * chore: apply review suggestions * update doc per suggestion --------- Co-authored-by: Charles Kerr (cherry picked from commit b4ec363b3ddfccc094e9ff2016b66a87294e0e84) --- docs/api/session.md | 49 ++++++++++++++ docs/fiddles/features/web-usb/main.js | 7 ++ docs/tutorial/devices.md | 2 + shell/browser/api/electron_api_session.cc | 15 +++++ shell/browser/api/electron_api_session.h | 2 + shell/browser/electron_permission_manager.cc | 21 ++++++ shell/browser/electron_permission_manager.h | 11 ++++ shell/browser/usb/electron_usb_delegate.cc | 34 +--------- .../usb_protected_classes_converter.h | 66 +++++++++++++++++++ 9 files changed, 176 insertions(+), 31 deletions(-) create mode 100644 shell/common/gin_converters/usb_protected_classes_converter.h diff --git a/docs/api/session.md b/docs/api/session.md index 5b092be26fef0..1da49dae68571 100644 --- a/docs/api/session.md +++ b/docs/api/session.md @@ -1022,6 +1022,55 @@ app.whenReady().then(() => { }) ``` +#### `ses.setUSBProtectedClassesHandler(handler)` + +* `handler` Function\ | null + * `details` Object + * `protectedClasses` string[] - The current list of protected USB classes. Possible class values are: + * `audio` + * `audio-video` + * `hid` + * `mass-storage` + * `smart-card` + * `video` + * `wireless` + +Sets the handler which can be used to override which [USB classes are protected](https://wicg.github.io/webusb/#usbinterface-interface). +The return value for the handler is a string array of USB classes which should be considered protected (eg not available in the renderer). Valid values for the array are: + +* `audio` +* `audio-video` +* `hid` +* `mass-storage` +* `smart-card` +* `video` +* `wireless` + +Returning an empty string array from the handler will allow all USB classes; returning the passed in array will maintain the default list of protected USB classes (this is also the default behavior if a handler is not defined). +To clear the handler, call `setUSBProtectedClassesHandler(null)`. + +```javascript +const { app, BrowserWindow } = require('electron') + +let win = null + +app.whenReady().then(() => { + win = new BrowserWindow() + + win.webContents.session.setUSBProtectedClassesHandler((details) => { + // Allow all classes: + // return [] + // Keep the current set of protected classes: + // return details.protectedClasses + // Selectively remove classes: + return details.protectedClasses.filter((usbClass) => { + // Exclude classes except for audio classes + return usbClass.indexOf('audio') === -1 + }) + }) +}) +``` + #### `ses.setBluetoothPairingHandler(handler)` _Windows_ _Linux_ * `handler` Function | null diff --git a/docs/fiddles/features/web-usb/main.js b/docs/fiddles/features/web-usb/main.js index fda4b3c734083..4ebe41e36dc28 100644 --- a/docs/fiddles/features/web-usb/main.js +++ b/docs/fiddles/features/web-usb/main.js @@ -51,6 +51,13 @@ function createWindow () { } }) + mainWindow.webContents.session.setUSBProtectedClassesHandler((details) => { + return details.protectedClasses.filter((usbClass) => { + // Exclude classes except for audio classes + return usbClass.indexOf('audio') === -1 + }) + }) + mainWindow.loadFile('index.html') } diff --git a/docs/tutorial/devices.md b/docs/tutorial/devices.md index be153d51da4b3..64f360e22f76c 100644 --- a/docs/tutorial/devices.md +++ b/docs/tutorial/devices.md @@ -142,6 +142,8 @@ Electron provides several APIs for working with the WebUSB API: `setDevicePermissionHandler`. * [`ses.setPermissionCheckHandler(handler)`](../api/session.md#sessetpermissioncheckhandlerhandler) can be used to disable USB access for specific origins. +* [`ses.setUSBProtectedClassesHandler](../api/session.md#sessetusbprotectedclasseshandlerhandler) + can be used to allow usage of [protected USB classes](https://wicg.github.io/webusb/#usbinterface-interface) that are not available by default. ### Example diff --git a/shell/browser/api/electron_api_session.cc b/shell/browser/api/electron_api_session.cc index 73ae99144b5ac..a641412d97009 100644 --- a/shell/browser/api/electron_api_session.cc +++ b/shell/browser/api/electron_api_session.cc @@ -71,6 +71,7 @@ #include "shell/common/gin_converters/gurl_converter.h" #include "shell/common/gin_converters/media_converter.h" #include "shell/common/gin_converters/net_converter.h" +#include "shell/common/gin_converters/usb_protected_classes_converter.h" #include "shell/common/gin_converters/value_converter.h" #include "shell/common/gin_helper/dictionary.h" #include "shell/common/gin_helper/object_template_builder.h" @@ -697,6 +698,18 @@ void Session::SetDevicePermissionHandler(v8::Local val, permission_manager->SetDevicePermissionHandler(handler); } +void Session::SetUSBProtectedClassesHandler(v8::Local val, + gin::Arguments* args) { + ElectronPermissionManager::ProtectedUSBHandler handler; + if (!(val->IsNull() || gin::ConvertFromV8(args->isolate(), val, &handler))) { + args->ThrowTypeError("Must pass null or function"); + return; + } + auto* permission_manager = static_cast( + browser_context()->GetPermissionControllerDelegate()); + permission_manager->SetProtectedUSBHandler(handler); +} + void Session::SetBluetoothPairingHandler(v8::Local val, gin::Arguments* args) { ElectronPermissionManager::BluetoothPairingHandler handler; @@ -1262,6 +1275,8 @@ gin::ObjectTemplateBuilder Session::GetObjectTemplateBuilder( &Session::SetDisplayMediaRequestHandler) .SetMethod("setDevicePermissionHandler", &Session::SetDevicePermissionHandler) + .SetMethod("setUSBProtectedClassesHandler", + &Session::SetUSBProtectedClassesHandler) .SetMethod("setBluetoothPairingHandler", &Session::SetBluetoothPairingHandler) .SetMethod("clearHostResolverCache", &Session::ClearHostResolverCache) diff --git a/shell/browser/api/electron_api_session.h b/shell/browser/api/electron_api_session.h index 3b40d3fc0c1a9..a8b86667f8113 100644 --- a/shell/browser/api/electron_api_session.h +++ b/shell/browser/api/electron_api_session.h @@ -109,6 +109,8 @@ class Session : public gin::Wrappable, gin::Arguments* args); void SetDevicePermissionHandler(v8::Local val, gin::Arguments* args); + void SetUSBProtectedClassesHandler(v8::Local val, + gin::Arguments* args); void SetBluetoothPairingHandler(v8::Local val, gin::Arguments* args); v8::Local ClearHostResolverCache(gin::Arguments* args); diff --git a/shell/browser/electron_permission_manager.cc b/shell/browser/electron_permission_manager.cc index 995a704bb6503..a509b973b7d57 100644 --- a/shell/browser/electron_permission_manager.cc +++ b/shell/browser/electron_permission_manager.cc @@ -25,6 +25,7 @@ #include "shell/browser/web_contents_preferences.h" #include "shell/common/gin_converters/content_converter.h" #include "shell/common/gin_converters/frame_converter.h" +#include "shell/common/gin_converters/usb_protected_classes_converter.h" #include "shell/common/gin_converters/value_converter.h" #include "shell/common/gin_helper/event_emitter_caller.h" #include "third_party/blink/public/common/permissions/permission_utils.h" @@ -130,6 +131,11 @@ void ElectronPermissionManager::SetDevicePermissionHandler( device_permission_handler_ = handler; } +void ElectronPermissionManager::SetProtectedUSBHandler( + const ProtectedUSBHandler& handler) { + protected_usb_handler_ = handler; +} + void ElectronPermissionManager::SetBluetoothPairingHandler( const BluetoothPairingHandler& handler) { bluetooth_pairing_handler_ = handler; @@ -362,6 +368,21 @@ void ElectronPermissionManager::RevokeDevicePermission( browser_context->RevokeDevicePermission(origin, device, permission); } +ElectronPermissionManager::USBProtectedClasses +ElectronPermissionManager::CheckProtectedUSBClasses( + const USBProtectedClasses& classes) const { + if (protected_usb_handler_.is_null()) { + return classes; + } else { + v8::Isolate* isolate = JavascriptEnvironment::GetIsolate(); + v8::HandleScope scope(isolate); + v8::Local details = gin::DataObjectBuilder(isolate) + .Set("protectedClasses", classes) + .Build(); + return protected_usb_handler_.Run(details); + } +} + blink::mojom::PermissionStatus ElectronPermissionManager::GetPermissionStatusForCurrentDocument( blink::PermissionType permission, diff --git a/shell/browser/electron_permission_manager.h b/shell/browser/electron_permission_manager.h index 761976d5c2492..b664f765e3072 100644 --- a/shell/browser/electron_permission_manager.h +++ b/shell/browser/electron_permission_manager.h @@ -35,6 +35,8 @@ class ElectronPermissionManager : public content::PermissionControllerDelegate { ElectronPermissionManager& operator=(const ElectronPermissionManager&) = delete; + using USBProtectedClasses = std::vector; + using StatusCallback = base::OnceCallback; using StatusesCallback = base::OnceCallback&)>; + + using ProtectedUSBHandler = base::RepeatingCallback&)>; + using BluetoothPairingHandler = base::RepeatingCallback; @@ -59,6 +65,7 @@ class ElectronPermissionManager : public content::PermissionControllerDelegate { void SetPermissionRequestHandler(const RequestHandler& handler); void SetPermissionCheckHandler(const CheckHandler& handler); void SetDevicePermissionHandler(const DeviceCheckHandler& handler); + void SetProtectedUSBHandler(const ProtectedUSBHandler& handler); void SetBluetoothPairingHandler(const BluetoothPairingHandler& handler); // content::PermissionControllerDelegate: @@ -109,6 +116,9 @@ class ElectronPermissionManager : public content::PermissionControllerDelegate { const base::Value& object, ElectronBrowserContext* browser_context) const; + USBProtectedClasses CheckProtectedUSBClasses( + const USBProtectedClasses& classes) const; + protected: void OnPermissionResponse(int request_id, int permission_id, @@ -155,6 +165,7 @@ class ElectronPermissionManager : public content::PermissionControllerDelegate { RequestHandler request_handler_; CheckHandler check_handler_; DeviceCheckHandler device_permission_handler_; + ProtectedUSBHandler protected_usb_handler_; BluetoothPairingHandler bluetooth_pairing_handler_; PendingRequestsMap pending_requests_; diff --git a/shell/browser/usb/electron_usb_delegate.cc b/shell/browser/usb/electron_usb_delegate.cc index a5bb4bc0e3750..9baf621c660d6 100644 --- a/shell/browser/usb/electron_usb_delegate.cc +++ b/shell/browser/usb/electron_usb_delegate.cc @@ -152,37 +152,9 @@ void ElectronUsbDelegate::AdjustProtectedInterfaceClasses( const url::Origin& origin, content::RenderFrameHost* frame, std::vector& classes) { - // Isolated Apps have unrestricted access to any USB interface class. - if (frame && frame->GetWebExposedIsolationLevel() >= - content::RenderFrameHost::WebExposedIsolationLevel:: - kMaybeIsolatedApplication) { - // TODO(https://crbug.com/1236706): Should the list of interface classes the - // app expects to claim be encoded in the Web App Manifest? - classes.clear(); - return; - } - -#if BUILDFLAG(ENABLE_EXTENSIONS) - // Don't enforce protected interface classes for Chrome Apps since the - // chrome.usb API has no such restriction. - if (origin.scheme() == extensions::kExtensionScheme) { - auto* extension_registry = - extensions::ExtensionRegistry::Get(browser_context); - if (extension_registry) { - const extensions::Extension* extension = - extension_registry->enabled_extensions().GetByID(origin.host()); - if (extension && extension->is_platform_app()) { - classes.clear(); - return; - } - } - } - - if (origin.scheme() == extensions::kExtensionScheme && - base::Contains(kSmartCardPrivilegedExtensionIds, origin.host())) { - base::Erase(classes, device::mojom::kUsbSmartCardClass); - } -#endif // BUILDFLAG(ENABLE_EXTENSIONS) + auto* permission_manager = static_cast( + browser_context->GetPermissionControllerDelegate()); + classes = permission_manager->CheckProtectedUSBClasses(classes); } std::unique_ptr ElectronUsbDelegate::RunChooser( diff --git a/shell/common/gin_converters/usb_protected_classes_converter.h b/shell/common/gin_converters/usb_protected_classes_converter.h new file mode 100644 index 0000000000000..d4db86b5971a6 --- /dev/null +++ b/shell/common/gin_converters/usb_protected_classes_converter.h @@ -0,0 +1,66 @@ +// Copyright (c) 2022 Microsoft, Inc. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#ifndef ELECTRON_SHELL_COMMON_GIN_CONVERTERS_USB_PROTECTED_CLASSES_CONVERTER_H_ +#define ELECTRON_SHELL_COMMON_GIN_CONVERTERS_USB_PROTECTED_CLASSES_CONVERTER_H_ + +#include +#include +#include +#include + +#include "gin/converter.h" +#include "services/device/public/mojom/usb_device.mojom-forward.h" +#include "shell/browser/electron_permission_manager.h" + +namespace gin { + +static auto constexpr ClassMapping = + std::array, 7>{ + {{device::mojom::kUsbAudioClass, "audio"}, + {device::mojom::kUsbHidClass, "hid"}, + {device::mojom::kUsbMassStorageClass, "mass-storage"}, + {device::mojom::kUsbSmartCardClass, "smart-card"}, + {device::mojom::kUsbVideoClass, "video"}, + {device::mojom::kUsbAudioVideoClass, "audio-video"}, + {device::mojom::kUsbWirelessClass, "wireless"}}}; + +template <> +struct Converter { + static v8::Local ToV8( + v8::Isolate* isolate, + const electron::ElectronPermissionManager::USBProtectedClasses& classes) { + std::vector class_strings; + class_strings.reserve(std::size(classes)); + for (const auto& itr : classes) { + for (const auto& [usb_class, name] : ClassMapping) { + if (usb_class == itr) + class_strings.emplace_back(name); + } + } + return gin::ConvertToV8(isolate, class_strings); + } + + static bool FromV8( + v8::Isolate* isolate, + v8::Local val, + electron::ElectronPermissionManager::USBProtectedClasses* out) { + std::vector class_strings; + if (ConvertFromV8(isolate, val, &class_strings)) { + out->reserve(std::size(class_strings)); + for (const auto& itr : class_strings) { + for (const auto& [usb_class, name] : ClassMapping) { + if (name == itr) + out->emplace_back(usb_class); + } + } + return true; + } + return false; + } +}; + +} // namespace gin + +#endif // ELECTRON_SHELL_COMMON_GIN_CONVERTERS_USB_PROTECTED_CLASSES_CONVERTER_H_