Skip to content

Commit

Permalink
Limit enumerateDevices() fingerprinting vector ahead of (and after) g…
Browse files Browse the repository at this point in the history
…etUserMedia success, to spec.

Updates enumerateDevices() to limit exposure of privacy sensitive
information ahead of actual camera or microphone use.

It also implements the "creating a device info object" algorithm correctly
after getUserMedia success, which only exposes information on cameras or
microphones (but not both) if only one or the other kind has successfully
been used.

Includes the latest privacy improvements to the spec:
- w3c/mediacapture-main#632
- w3c/mediacapture-main#641
- w3c/mediacapture-main#773

This also fixes media.navigator.permission.disabled leaking labels.

Differential Revision: https://phabricator.services.mozilla.com/D100378

bugzilla-url: https://bugzilla.mozilla.org/show_bug.cgi?id=1528042
gecko-commit: 55c6b85b37c1fb8d6d0b6a273e237b4921672743
gecko-reviewers: karlt
  • Loading branch information
jan-ivar authored and moz-wptsync-bot committed May 27, 2023
1 parent 3119ef4 commit 5f76d19
Showing 1 changed file with 57 additions and 22 deletions.
79 changes: 57 additions & 22 deletions mediacapture-streams/MediaDevices-enumerateDevices.https.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,36 +23,71 @@ <h1 class="instructions">Description</h1>

promise_test(async () => {
assert_not_equals(navigator.mediaDevices.enumerateDevices, undefined, "navigator.mediaDevices.enumerateDevices exists");
const deviceList = await navigator.mediaDevices.enumerateDevices();
for (const mediaInfo of deviceList) {
assert_not_equals(mediaInfo.kind, undefined, "mediaInfo's kind should exist.");
assert_equals(mediaInfo.deviceId, "", "mediaInfo's deviceId should exist and be empty if getUserMedia was never called successfully.");
assert_equals(mediaInfo.label, "", "mediaInfo's label should exist and be empty if getUserMedia was never called successfully.");
assert_equals(mediaInfo.groupId, "", "mediaInfo's groupId should exist and be empty if getUserMedia was never called successfully.");
assert_in_array(mediaInfo.kind, ["videoinput", "audioinput", "audiooutput"]);
const devices = await navigator.mediaDevices.enumerateDevices();
for (const {kind, deviceId, label, groupId} of devices) {
assert_in_array(kind, ["videoinput", "audioinput", "audiooutput"]);
assert_equals(deviceId, "", "deviceId should be empty string if getUserMedia was never called successfully.");
assert_equals(label, "", "label should be empty string if getUserMedia was never called successfully.");
assert_equals(groupId, "", "groupId should be empty string if getUserMedia was never called successfully.");
}
assert_less_than_equal(deviceList.filter((item) => { return item.kind == "audioinput"; }).length, 1, "there should be zero or one audio input device ");
assert_less_than_equal(deviceList.filter((item) => { return item.kind == "videoinput"; }).length, 1, "there should be zero or one video input device ");

assert_less_than_equal(devices.filter(({kind}) => kind == "audioinput").length,
1, "there should be zero or one audio input device.");
assert_less_than_equal(devices.filter(({kind}) => kind == "videoinput").length,
1, "there should be zero or one video input device.");
}, "mediaDevices.enumerateDevices() is present and working - before capture");

promise_test(async () => {
await setMediaPermission("granted", ["microphone"]);
await navigator.mediaDevices.getUserMedia({ audio : true });
const deviceList = await navigator.mediaDevices.enumerateDevices();
for (const mediaInfo of deviceList) {
assert_not_equals(mediaInfo.kind, undefined, "mediaInfo's kind should exist.");
assert_not_equals(mediaInfo.deviceId, "", "mediaInfo's deviceId should exist and not be empty.");
assert_in_array(mediaInfo.kind, ["videoinput", "audioinput", "audiooutput"]);
promise_test(async t => {
await setMediaPermission("granted");
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
stream.getTracks()[0].stop();

const devices = await navigator.mediaDevices.enumerateDevices();
const kinds = ["audioinput", "videoinput"];
for (const {kind, deviceId} of devices) {
assert_in_array(kind, kinds, "camera doesn't expose audiooutput");
assert_equals(typeof deviceId, "string", "deviceId is a string.");
switch (kind) {
case "videoinput":
assert_greater_than(deviceId.length, 0, "video deviceId should not be empty.");
break;
case "audioinput":
assert_equals(deviceId.length, 0, "audio deviceId should be empty.");
break;
}
}
}, "mediaDevices.enumerateDevices() is working - after video capture");

// This test is designed to come after its video counterpart directly above
promise_test(async t => {
const devices1 = await navigator.mediaDevices.enumerateDevices();
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
stream.getTracks()[0].stop();
const devices = await navigator.mediaDevices.enumerateDevices();
assert_equals(devices.filter(({kind}) => kind != "audiooutput").length,
devices1.filter(({kind}) => kind != "audiooutput").length,
"same number of input devices");
const kinds = ["audioinput", "videoinput", "audiooutput"];
for (const {kind, deviceId} of devices) {
assert_in_array(kind, kinds, "expected kind");
assert_equals(typeof deviceId, "string", "deviceId is a string.");
switch (kind) {
case "videoinput":
assert_greater_than(deviceId.length, 0, "video deviceId should not be empty.");
break;
case "audioinput":
assert_greater_than(deviceId.length, 0, "audio deviceId should not be empty.");
break;
}
}
}, "mediaDevices.enumerateDevices() is present and working - after capture");
}, "mediaDevices.enumerateDevices() is working - after video then audio capture");

promise_test(async () => {
const deviceList = await navigator.mediaDevices.enumerateDevices();
for (const mediaInfo of deviceList) {
const devices = await navigator.mediaDevices.enumerateDevices();
for (const mediaInfo of devices) {
if (mediaInfo.kind == "audioinput" || mediaInfo.kind == "videoinput") {
assert_true("InputDeviceInfo" in window, "InputDeviceInfo exists");
assert_true(mediaInfo instanceof InputDeviceInfo);
} else if ( mediaInfo.kind == "audiooutput" ) {
} else if (mediaInfo.kind == "audiooutput") {
assert_true(mediaInfo instanceof MediaDeviceInfo);
} else {
assert_unreached("mediaInfo.kind should be one of 'audioinput', 'videoinput', or 'audiooutput'.")
Expand Down

0 comments on commit 5f76d19

Please sign in to comment.