WebHID API

Editor’s Draft,

This version:
https://github.com/nondebug/webhid
Issue Tracking:
GitHub
Editor:
(Google LLC)

Abstract

This document describes an API for providing access to devices that support the Human Interface Device (HID) protocol.

Status of this document

This specification was published by the Web Platform Incubator Community Group. It is not a W3C Standard nor is it on the W3C Standards Track. Please note that under the W3C Community Contributor License Agreement (CLA) there is a limited opt-out and other conditions apply. Learn more about W3C Community and Business Groups.

1. Introduction

This section is non-normative.

A HID (Human Interface Device) is a type of device that takes input from or provides output to humans. It also refers to the HID protocol, a standard for bi-directional communication between a host and a device that is designed to simplify the installation procedure. The HID protocol was originally developed for USB devices but has since been implemented over many other protocols, including Bluetooth.

The web platform already supports input from many HID devices. Keyboards, pointing devices, and gamepads are all typically implemented using the HID protocol. However, this support relies on the operating system’s HID drivers that transform the HID input into high-level input APIs. Devices that are not well supported by the host’s HID driver are often inaccessible to web pages. Similarly, the outputs on most devices that are not supported by the host’s HID driver are inaccessible.

2. Motivating Applications

This section is non-normative.

2.1. Niche devices

The most common classes of HID input devices are already well-supported on the web platform through existing high-level input APIs. For instance, mouse input is accessible through the PointerEvent API and keyboard input is accessible through the Keyboard API. Input from these devices is handled using the host’s native HID driver and typically does not require device-specific drivers or configuration to work correctly. WebHID is not intended for devices like these that are already well-supported through high-level input APIs.

For some classes of HID devices, the web platform supports some features of the device but limits access to other features. For instance, the Gamepad API supports the input capabilities of most game controllers but does not support less common capabilities like LED indicators or audio. These features are often not well-supported by host APIs and adding support within the user agent can lead to significant complexity. WebHID would give applications an alternative when the functionality provided by the high-level API is incomplete.

Many HID devices are not supported through any web platform API. The HID specification describes a wide array of devices that could be supported through HID, including virtual reality controls, flight simulators, medical equipment, and more. WebHID would allow these devices to be used without requiring additional drivers or modification to the user agent.

2.2. Prototyping, hobbyists, and educational devices

HID is attractive for prototyping and hobbyist applications because it allows devices to use the host’s generic HID driver instead of requiring a driver for each host where the device will be used. The simplified installation procedure also makes it easier for educators to deploy a device to a classroom of students without modifying the system configuration on each host. Providing access to HID devices through the web platform would further reduce installation requirements, particularly for devices that are currently only supported through host-specific applications.

3. Security and Privacy Considerations

This section is non-normative.

3.1. Abusing Access to a Device

HID peripherals may expose powerful functionality that should not be made accessible to the page without explicit consent from the user. For instance, a HID peripheral may have sensors that allow it to collect information about its surroundings. A device may store private information that should not be revealed or overwritten. Many devices expose functionality that allow the device firmware to be upgraded. Operating systems typically do not restrict access to HID devices from applications, and this access can occasionally be abused to damage the device or corrupt the data stored on it.

In some cases, a device may expose functionality that should not be accessible at all from a web page. Specific devices may be blocked by recognizing the device by its vendor and product ID and hiding such devices during enumeration. The HID report descriptor allows a device to describe its own capabilities, and this information can be used to block classes of devices even when the vendor and product ID cannot be known in advance. This can be accomplished by inspecting the usage values assigned to collections within the report descriptor; for instance, access to keyboard-like devices can be denied by denying access to devices that include a top-level collection with the Keyboard usage ID.

To mitigate abuse, a device will not be made available to the page until the user has granted explicit access. Access must first be requested by the page and will be granted once the user has selected the device from a chooser displaying all available devices. Once a page has been granted access, an indicator will be displayed to indicate the device is in use.

3.2. Attacking a Device

The HID protocol is extremely versatile, and a wide variety of devices have been designed that take advantage of this versatility. As a result, there are many possibilities for how access to a device could allow a malicious page to damage the device itself. It is relatively common for HID peripherals to allow firmware updates to be sent using custom HID reports. Device manufacturers must take care to design the firmware upgrade mechanism to verify that the firmware binary is authentic, and should also protect against downgrades that could reduce the security of a device. Untrusted upgrades could be used to add new capabilities to a device or cause it to masquerade as an entirely different type of device.

Some devices may be damaged when inputs are provided outside of the expected range. For instance, the rumble feature on a gamepad could become damaged if a page sends an invalid rumble command.

In general, these types of attacks are device-specific and cannot be mitigated at the API level.

3.3. Attacking the Host

If a malicious page gains access to a device, in some cases this access can be used to attack the host. A major concern is whether the device can be used to generate trusted input events. These events serve as a proxy for user intent and can be used to access more powerful web platform features.

Mice and keyboards are typically implemented as HID peripherals, and are also capable of generating trusted input. A HID keyboard or mouse may contain advanced features like programmable macros, which store a sequence of inputs and allow the sequence to be played back at a later time. Device manufacturers must take care to design such features in a way that would prevent a malicious app from reprogramming the device with an unexpected input sequence, and must also protect against triggering macro playback without the user’s explicit consent.

4. Device Enumeration

dictionary HIDDeviceFilter {
    unsigned long vendorId;
    unsigned short productId;
    unsigned short usagePage;
    unsigned short usage;
};

dictionary HIDDeviceRequestOptions {
    required sequence<HIDDeviceFilter> filters;
};

[SecureContext]
interface HID : EventTarget {
    attribute EventHandler onconnect;
    attribute EventHandler ondisconnect;
    Promise<sequence<HIDDevice>> getDevices();
    Promise<sequence<HIDDevice>> requestDevice(
        HIDDeviceRequestOptions options);
};

[SecureContext] partial interface Navigator {
    [SameObject] readonly attribute HID hid;
};

Retrieve devices and log the device names to the console.
document.addEventListener('DOMContentLoaded', async () => {
  let devices = await navigator.hid.getDevices();
  devices.forEach(device => {
    console.log('HID: ${device.productName}');
  });
});

Register event listeners for connection and disconnection of HID devices.

navigator.hid.addEventListener('connect', async () => {
  console.log('HID connected: ${device.productName}');
});

navigator.hid.addEventListener('disconnect', async () => {
  console.log('HID disconnected: ${device.productName}');
});

Devices are not accessible through getDevices() and will not generate connection events until permission has been granted to access the device. The page may request permission using requestDevice(). In this example, the page requests access to a device with vendor ID 0xABCD, product ID 0x1234. The device must also have a collection with usage page Consumer (0x0C) and usage ID Consumer Control (0x01).

let requestButton = document.getElementById('request-hid-device');
requestButton.addEventListener('click', async () => {
  let device;
  try {
    device = await navigator.hid.requestDevice({ filters: [{
        vendorId: 0xABCD,
        productId: 0x1234,
        usagePage: 0x0C,
        usage: 0x01
    }]});
  } catch (error) {
    console.log('No device was selected.');
  }

  if (device !== undefined) {
    console.log('HID: ${device.productName}');
  }
});

A HID device device matches a device filter filter if the following steps return match:

  1. If filter.vendorId is present and device.vendorId does not equal filter.vendorId, return mismatch.

  2. If filter.productId is present and device.productId does not equal filter.productId, return mismatch.

  3. If filter.usagePage is present, iterate over the HIDCollectionInfo collections in device.collections. If no collection matches the device filter, return mismatch.

  4. Return match.

A HIDCollectionInfo collection matches a device filter filter if the following steps return match:

  1. If filter.usagePage is present and device.usagePage does not equal filter.usagePage, return mismatch.

  2. If filter.usage is present and device.usage does not equal filter.usage, return mismatch.

  3. Return match.

A HIDDeviceFilter filter is valid if the following steps return valid:

  1. If filter.productId is present and filter.vendorId is not present, return invalid.

  2. If filter.usage is present and filter.usagePage is not present, return invalid.

  3. Return valid.

The UA MUST be able to enumerate all devices attached to the system. It is not required to perform this work each time an algorithm requests an enumeration. The UA MAY cache the result of the first enumeration it performs and then begin monitoring for device connection and disconnection events, adding connected devices to its cached enumeration and removing disconnected devices. This mode of operation is preferred as it reduces the number of operating system calls.

4.1. Events

dictionary HIDConnectionEventInit : EventInit {
    required HIDDevice device;
};

[
    Constructor(DOMString type, HIDConnectionEventInit eventInitDict),
    SecureContext
] interface HIDConnectionEvent : Event {
    [SameObject] readonly attribute HIDDevice device;
};

dictionary HIDInputReportEventInit : EventInit {
    required HIDDevice device;
    required octet reportId;
    required DataView data;
};

[
    Constructor(DOMString type, HIDInputReportEventInit eventInitDict),
    SecureContext
] interface HIDInputReportEvent : Event {
    [SameObject] readonly attribute HIDDevice device;
    readonly attribute octet reportId;
    readonly attribute DataView data;
};

4.2. HID Collection and Report Information

enum HIDUnitSystem {
    "none", "si-linear", "si-rotation", "english-linear",
    "english-rotation", "vendor-defined", "reserved"
};

[SecureContext] interface HIDReportItem {
    readonly attribute boolean isAbsolute;
    readonly attribute boolean isArray;
    readonly attribute boolean isRange;
    readonly attribute boolean hasNull;
    readonly attribute FrozenArray<unsigned long> usages;
    readonly attribute unsigned long usageMinimum;
    readonly attribute unsigned long usageMaximum;
    readonly attribute unsigned short reportSize;
    readonly attribute unsigned short reportCount;
    readonly attribute unsigned long unitExponent;
    readonly attribute HIDUnitSystem unitSystem;
    readonly attribute byte unitFactorLengthExponent;
    readonly attribute byte unitFactorMassExponent;
    readonly attribute byte unitFactorTimeExponent;
    readonly attribute byte unitFactorTemperatureExponent;
    readonly attribute byte unitFactorCurrentExponent;
    readonly attribute byte unitFactorLuminousIntensityExponent;
    readonly attribute long logicalMinimum;
    readonly attribute long logicalMaximum;
    readonly attribute long physicalMinimum;
    readonly attribute long physicalMaximum;
    readonly attribute FrozenArray<DOMString> strings;
};

[SecureContext] interface HIDReportInfo {
    readonly attribute octet reportId;
    readonly attribute FrozenArray<HIDReportItem> items;
};

[SecureContext] interface HIDCollectionInfo {
    readonly attribute unsigned short usagePage;
    readonly attribute unsigned short usage;
    readonly attribute FrozenArray<HIDCollectionInfo> children;
    readonly attribute FrozenArray<HIDReportInfo> inputReports;
    readonly attribute FrozenArray<HIDReportInfo> outputReports;
    readonly attribute FrozenArray<HIDReportInfo> featureReports;
};

5. Device Usage

[SecureContext] interface HIDDevice : EventTarget {
    attribute EventHandler oninputreport;
    readonly attribute boolean opened;
    readonly attribute unsigned short vendorId;
    readonly attribute unsigned short productId;
    readonly attribute DOMString productName;
    readonly attribute FrozenArray<HIDCollectionInfo> collections;
    Promise<void> open();
    Promise<void> close();
    Promise<void> sendReport(octet reportId, BufferSource data);
    Promise<void> sendFeatureReport(octet reportId, BufferSource data);
    Promise<DataView> receiveFeatureReport(octet reportId);
};

6. Integrations

6.1. Feature Policy

This specification defines a feature that controls whether the hid attribute is exposed on the Navigator object.

The feature name for this feature is "hid".

The default allowlist for this feature is ["self"].

Conformance

Conformance requirements are expressed with a combination of descriptive assertions and RFC 2119 terminology. The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in the normative parts of this document are to be interpreted as described in RFC 2119. However, for readability, these words do not appear in all uppercase letters in this specification.

All of the text of this specification is normative except sections explicitly marked as non-normative, examples, and notes. [RFC2119]

Examples in this specification are introduced with the words “for example” or are set apart from the normative text with class="example", like this:

This is an example of an informative example.

Informative notes begin with the word “Note” and are set apart from the normative text with class="note", like this:

Note, this is an informative note.

Index

Terms defined by this specification

Terms defined by reference

References

Normative References

[DOM]
Anne van Kesteren. DOM Standard. Living Standard. URL: https://dom.spec.whatwg.org/
[FEATURE-POLICY-1]
Ian Clelland. Feature Policy. 16 April 2019. WD. URL: https://www.w3.org/TR/feature-policy-1/
[HTML]
Anne van Kesteren; et al. HTML Standard. Living Standard. URL: https://html.spec.whatwg.org/multipage/
[RFC2119]
S. Bradner. Key words for use in RFCs to Indicate Requirement Levels. March 1997. Best Current Practice. URL: https://tools.ietf.org/html/rfc2119
[WebIDL]
Boris Zbarsky. Web IDL. 15 December 2016. ED. URL: https://heycam.github.io/webidl/

IDL Index

dictionary HIDDeviceFilter {
    unsigned long vendorId;
    unsigned short productId;
    unsigned short usagePage;
    unsigned short usage;
};

dictionary HIDDeviceRequestOptions {
    required sequence<HIDDeviceFilter> filters;
};

[SecureContext]
interface HID : EventTarget {
    attribute EventHandler onconnect;
    attribute EventHandler ondisconnect;
    Promise<sequence<HIDDevice>> getDevices();
    Promise<sequence<HIDDevice>> requestDevice(
        HIDDeviceRequestOptions options);
};

[SecureContext] partial interface Navigator {
    [SameObject] readonly attribute HID hid;
};


dictionary HIDConnectionEventInit : EventInit {
    required HIDDevice device;
};

[
    Constructor(DOMString type, HIDConnectionEventInit eventInitDict),
    SecureContext
] interface HIDConnectionEvent : Event {
    [SameObject] readonly attribute HIDDevice device;
};

dictionary HIDInputReportEventInit : EventInit {
    required HIDDevice device;
    required octet reportId;
    required DataView data;
};

[
    Constructor(DOMString type, HIDInputReportEventInit eventInitDict),
    SecureContext
] interface HIDInputReportEvent : Event {
    [SameObject] readonly attribute HIDDevice device;
    readonly attribute octet reportId;
    readonly attribute DataView data;
};


enum HIDUnitSystem {
    "none", "si-linear", "si-rotation", "english-linear",
    "english-rotation", "vendor-defined", "reserved"
};

[SecureContext] interface HIDReportItem {
    readonly attribute boolean isAbsolute;
    readonly attribute boolean isArray;
    readonly attribute boolean isRange;
    readonly attribute boolean hasNull;
    readonly attribute FrozenArray<unsigned long> usages;
    readonly attribute unsigned long usageMinimum;
    readonly attribute unsigned long usageMaximum;
    readonly attribute unsigned short reportSize;
    readonly attribute unsigned short reportCount;
    readonly attribute unsigned long unitExponent;
    readonly attribute HIDUnitSystem unitSystem;
    readonly attribute byte unitFactorLengthExponent;
    readonly attribute byte unitFactorMassExponent;
    readonly attribute byte unitFactorTimeExponent;
    readonly attribute byte unitFactorTemperatureExponent;
    readonly attribute byte unitFactorCurrentExponent;
    readonly attribute byte unitFactorLuminousIntensityExponent;
    readonly attribute long logicalMinimum;
    readonly attribute long logicalMaximum;
    readonly attribute long physicalMinimum;
    readonly attribute long physicalMaximum;
    readonly attribute FrozenArray<DOMString> strings;
};

[SecureContext] interface HIDReportInfo {
    readonly attribute octet reportId;
    readonly attribute FrozenArray<HIDReportItem> items;
};

[SecureContext] interface HIDCollectionInfo {
    readonly attribute unsigned short usagePage;
    readonly attribute unsigned short usage;
    readonly attribute FrozenArray<HIDCollectionInfo> children;
    readonly attribute FrozenArray<HIDReportInfo> inputReports;
    readonly attribute FrozenArray<HIDReportInfo> outputReports;
    readonly attribute FrozenArray<HIDReportInfo> featureReports;
};


[SecureContext] interface HIDDevice : EventTarget {
    attribute EventHandler oninputreport;
    readonly attribute boolean opened;
    readonly attribute unsigned short vendorId;
    readonly attribute unsigned short productId;
    readonly attribute DOMString productName;
    readonly attribute FrozenArray<HIDCollectionInfo> collections;
    Promise<void> open();
    Promise<void> close();
    Promise<void> sendReport(octet reportId, BufferSource data);
    Promise<void> sendFeatureReport(octet reportId, BufferSource data);
    Promise<DataView> receiveFeatureReport(octet reportId);
};