Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add USB protected classes handler #38498

Merged
merged 1 commit into from
Jun 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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_