Browse Source

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 <[email protected]>

* chore: update docs

* chore: apply review suggestions

* update doc per suggestion

---------

Co-authored-by: Charles Kerr <[email protected]>
(cherry picked from commit b4ec363b3ddfccc094e9ff2016b66a87294e0e84)
John Kleinschmidt 1 year ago
parent
commit
18b2a4e957

+ 49 - 0
docs/api/session.md

@@ -1022,6 +1022,55 @@ app.whenReady().then(() => {
 })
 ```
 
+#### `ses.setUSBProtectedClassesHandler(handler)`
+
+* `handler` Function\<string[]> | 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

+ 7 - 0
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')
 }
 

+ 2 - 0
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
 

+ 15 - 0
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<v8::Value> val,
   permission_manager->SetDevicePermissionHandler(handler);
 }
 
+void Session::SetUSBProtectedClassesHandler(v8::Local<v8::Value> 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<ElectronPermissionManager*>(
+      browser_context()->GetPermissionControllerDelegate());
+  permission_manager->SetProtectedUSBHandler(handler);
+}
+
 void Session::SetBluetoothPairingHandler(v8::Local<v8::Value> 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)

+ 2 - 0
shell/browser/api/electron_api_session.h

@@ -109,6 +109,8 @@ class Session : public gin::Wrappable<Session>,
                                  gin::Arguments* args);
   void SetDevicePermissionHandler(v8::Local<v8::Value> val,
                                   gin::Arguments* args);
+  void SetUSBProtectedClassesHandler(v8::Local<v8::Value> val,
+                                     gin::Arguments* args);
   void SetBluetoothPairingHandler(v8::Local<v8::Value> val,
                                   gin::Arguments* args);
   v8::Local<v8::Promise> ClearHostResolverCache(gin::Arguments* args);

+ 21 - 0
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<v8::Object> details = gin::DataObjectBuilder(isolate)
+                                        .Set("protectedClasses", classes)
+                                        .Build();
+    return protected_usb_handler_.Run(details);
+  }
+}
+
 blink::mojom::PermissionStatus
 ElectronPermissionManager::GetPermissionStatusForCurrentDocument(
     blink::PermissionType permission,

+ 11 - 0
shell/browser/electron_permission_manager.h

@@ -35,6 +35,8 @@ class ElectronPermissionManager : public content::PermissionControllerDelegate {
   ElectronPermissionManager& operator=(const ElectronPermissionManager&) =
       delete;
 
+  using USBProtectedClasses = std::vector<uint8_t>;
+
   using StatusCallback =
       base::OnceCallback<void(blink::mojom::PermissionStatus)>;
   using StatusesCallback = base::OnceCallback<void(
@@ -52,6 +54,10 @@ class ElectronPermissionManager : public content::PermissionControllerDelegate {
 
   using DeviceCheckHandler =
       base::RepeatingCallback<bool(const v8::Local<v8::Object>&)>;
+
+  using ProtectedUSBHandler = base::RepeatingCallback<USBProtectedClasses(
+      const v8::Local<v8::Object>&)>;
+
   using BluetoothPairingHandler =
       base::RepeatingCallback<void(gin_helper::Dictionary, PairCallback)>;
 
@@ -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_;

+ 3 - 31
shell/browser/usb/electron_usb_delegate.cc

@@ -152,37 +152,9 @@ void ElectronUsbDelegate::AdjustProtectedInterfaceClasses(
     const url::Origin& origin,
     content::RenderFrameHost* frame,
     std::vector<uint8_t>& 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<ElectronPermissionManager*>(
+      browser_context->GetPermissionControllerDelegate());
+  classes = permission_manager->CheckProtectedUSBClasses(classes);
 }
 
 std::unique_ptr<UsbChooser> ElectronUsbDelegate::RunChooser(

+ 66 - 0
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 <string>
+#include <string_view>
+#include <utility>
+#include <vector>
+
+#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<std::pair<uint8_t, std::string_view>, 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<electron::ElectronPermissionManager::USBProtectedClasses> {
+  static v8::Local<v8::Value> ToV8(
+      v8::Isolate* isolate,
+      const electron::ElectronPermissionManager::USBProtectedClasses& classes) {
+    std::vector<std::string> 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<v8::Value> val,
+      electron::ElectronPermissionManager::USBProtectedClasses* out) {
+    std::vector<std::string> 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_