Skip to content

Commit

Permalink
feat: add USB protected classes handler (#38498)
Browse files Browse the repository at this point in the history
feat: add USB protected classes handler (#38263)

* feat: add USB protected classes handler

* chore: apply review suggestions

Co-authored-by: Charles Kerr <charles@charleskerr.com>

* chore: update docs

* chore: apply review suggestions

* update doc per suggestion

---------

Co-authored-by: Charles Kerr <charles@charleskerr.com>
(cherry picked from commit b4ec363)
  • Loading branch information
jkleinsc committed Jun 1, 2023
1 parent a21fa57 commit 18b2a4e
Show file tree
Hide file tree
Showing 9 changed files with 176 additions and 31 deletions.
49 changes: 49 additions & 0 deletions docs/api/session.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions docs/fiddles/features/web-usb/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
}

Expand Down
2 changes: 2 additions & 0 deletions docs/tutorial/devices.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
15 changes: 15 additions & 0 deletions shell/browser/api/electron_api_session.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions shell/browser/api/electron_api_session.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
21 changes: 21 additions & 0 deletions shell/browser/electron_permission_manager.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down
11 changes: 11 additions & 0 deletions shell/browser/electron_permission_manager.h
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -52,13 +54,18 @@ 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)>;

// Handler to dispatch permission requests in JS.
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:
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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_;
Expand Down
34 changes: 3 additions & 31 deletions shell/browser/usb/electron_usb_delegate.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
66 changes: 66 additions & 0 deletions shell/common/gin_converters/usb_protected_classes_converter.h
Original file line number Diff line number Diff line change
@@ -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_

0 comments on commit 18b2a4e

Please sign in to comment.