[WebAuthn] Add large blob toggle
Add a toggle that enables / disables large blob support for WebAuthn
virtual authenticators. This feature has been long supported by the
devtools protocol but we hadn't surfaced it on the front-end.
Large blobs are only available for resident-key enabled authenticators,
so we disable the checkbox if that option is not selected. Also, large
blobs require CTAP 2.1.
Fixed: 1321803
Change-Id: Ic1654276ba5a39ddd8943cd8689978117546e70e
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/3625237
Reviewed-by: Simon Zünd <szuend@chromium.org>
Commit-Queue: Nina Satragno <nsatragno@chromium.org>
Auto-Submit: Nina Satragno <nsatragno@chromium.org>
diff --git a/front_end/core/i18n/locales/en-US.json b/front_end/core/i18n/locales/en-US.json
index 835aa3b..f0f02c9 100644
--- a/front_end/core/i18n/locales/en-US.json
+++ b/front_end/core/i18n/locales/en-US.json
@@ -11531,6 +11531,9 @@
"panels/webauthn/WebauthnPane.ts | signCount": {
"message": "Signature Count"
},
+ "panels/webauthn/WebauthnPane.ts | supportsLargeBlob": {
+ "message": "Supports large blob"
+ },
"panels/webauthn/WebauthnPane.ts | supportsResidentKeys": {
"message": "Supports resident keys"
},
diff --git a/front_end/core/i18n/locales/en-XL.json b/front_end/core/i18n/locales/en-XL.json
index d49568a..64b0777 100644
--- a/front_end/core/i18n/locales/en-XL.json
+++ b/front_end/core/i18n/locales/en-XL.json
@@ -11531,6 +11531,9 @@
"panels/webauthn/WebauthnPane.ts | signCount": {
"message": "Ŝíĝńât́ûŕê Ćôún̂t́"
},
+ "panels/webauthn/WebauthnPane.ts | supportsLargeBlob": {
+ "message": "Ŝúp̂ṕôŕt̂ś l̂ár̂ǵê b́l̂ób̂"
+ },
"panels/webauthn/WebauthnPane.ts | supportsResidentKeys": {
"message": "Ŝúp̂ṕôŕt̂ś r̂éŝíd̂én̂t́ k̂éŷś"
},
diff --git a/front_end/panels/webauthn/BUILD.gn b/front_end/panels/webauthn/BUILD.gn
index c1150e4..34adb89 100644
--- a/front_end/panels/webauthn/BUILD.gn
+++ b/front_end/panels/webauthn/BUILD.gn
@@ -36,6 +36,7 @@
visibility = [
":*",
"../../../test/unittests/front_end/entrypoints/missing_entrypoints/*",
+ "../../../test/unittests/front_end/panels/webauthn/*",
"../../entrypoints/*",
]
diff --git a/front_end/panels/webauthn/WebauthnPane.ts b/front_end/panels/webauthn/WebauthnPane.ts
index 7756269..4689bf1 100644
--- a/front_end/panels/webauthn/WebauthnPane.ts
+++ b/front_end/panels/webauthn/WebauthnPane.ts
@@ -84,6 +84,12 @@
*/
supportsResidentKeys: 'Supports resident keys',
/**
+ *@description Label for checkbox that toggles large blob support on virtual authenticators. Large blobs are opaque data associated
+ * with a WebAuthn credential that a website can store, like an SSH certificate or a symmetric encryption key.
+ * See https://w3c.github.io/webauthn/#sctn-large-blob-extension
+ */
+ supportsLargeBlob: 'Supports large blob',
+ /**
*@description Text to add something
*/
add: 'Add',
@@ -244,10 +250,12 @@
#protocolSelect: HTMLSelectElement|undefined;
#transportSelect: HTMLSelectElement|undefined;
#residentKeyCheckboxLabel: UI.UIUtils.CheckboxLabel|undefined;
- #residentKeyCheckbox: HTMLInputElement|undefined;
+ residentKeyCheckbox: HTMLInputElement|undefined;
#userVerificationCheckboxLabel: UI.UIUtils.CheckboxLabel|undefined;
#userVerificationCheckbox: HTMLInputElement|undefined;
- #addAuthenticatorButton: HTMLButtonElement|undefined;
+ #largeBlobCheckboxLabel: UI.UIUtils.CheckboxLabel|undefined;
+ largeBlobCheckbox: HTMLInputElement|undefined;
+ addAuthenticatorButton: HTMLButtonElement|undefined;
#isEnabling?: Promise<void>;
constructor() {
@@ -475,13 +483,18 @@
}
#updateNewAuthenticatorSectionOptions(): void {
- if (!this.#protocolSelect || !this.#residentKeyCheckbox || !this.#userVerificationCheckbox) {
+ if (!this.#protocolSelect || !this.residentKeyCheckbox || !this.#userVerificationCheckbox ||
+ !this.largeBlobCheckbox) {
return;
}
if (this.#protocolSelect.value === Protocol.WebAuthn.AuthenticatorProtocol.Ctap2) {
- this.#residentKeyCheckbox.disabled = false;
+ this.residentKeyCheckbox.disabled = false;
this.#userVerificationCheckbox.disabled = false;
+ this.largeBlobCheckbox.disabled = !this.residentKeyCheckbox.checked;
+ if (this.largeBlobCheckbox.disabled) {
+ this.largeBlobCheckbox.checked = false;
+ }
this.#updateEnabledTransportOptions([
Protocol.WebAuthn.AuthenticatorTransport.Usb,
Protocol.WebAuthn.AuthenticatorTransport.Ble,
@@ -491,10 +504,12 @@
Protocol.WebAuthn.AuthenticatorTransport.Internal,
]);
} else {
- this.#residentKeyCheckbox.checked = false;
- this.#residentKeyCheckbox.disabled = true;
+ this.residentKeyCheckbox.checked = false;
+ this.residentKeyCheckbox.disabled = true;
this.#userVerificationCheckbox.checked = false;
this.#userVerificationCheckbox.disabled = true;
+ this.largeBlobCheckbox.checked = false;
+ this.largeBlobCheckbox.disabled = true;
this.#updateEnabledTransportOptions([
Protocol.WebAuthn.AuthenticatorTransport.Usb,
Protocol.WebAuthn.AuthenticatorTransport.Ble,
@@ -524,6 +539,7 @@
const transportGroup = this.#newAuthenticatorForm.createChild('div', 'authenticator-option');
const residentKeyGroup = this.#newAuthenticatorForm.createChild('div', 'authenticator-option');
const userVerificationGroup = this.#newAuthenticatorForm.createChild('div', 'authenticator-option');
+ const largeBlobGroup = this.#newAuthenticatorForm.createChild('div', 'authenticator-option');
const addButtonGroup = this.#newAuthenticatorForm.createChild('div', 'authenticator-option');
const protocolSelectTitle = UI.UIUtils.createLabel(i18nString(UIStrings.protocol), 'authenticator-option-label');
@@ -551,9 +567,9 @@
this.#residentKeyCheckboxLabel = UI.UIUtils.CheckboxLabel.create(i18nString(UIStrings.supportsResidentKeys), false);
this.#residentKeyCheckboxLabel.textElement.classList.add('authenticator-option-label');
residentKeyGroup.appendChild(this.#residentKeyCheckboxLabel.textElement);
- this.#residentKeyCheckbox = this.#residentKeyCheckboxLabel.checkboxElement;
- this.#residentKeyCheckbox.checked = false;
- this.#residentKeyCheckbox.classList.add('authenticator-option-checkbox');
+ this.residentKeyCheckbox = this.#residentKeyCheckboxLabel.checkboxElement;
+ this.residentKeyCheckbox.checked = false;
+ this.residentKeyCheckbox.classList.add('authenticator-option-checkbox');
residentKeyGroup.appendChild(this.#residentKeyCheckboxLabel);
this.#userVerificationCheckboxLabel = UI.UIUtils.CheckboxLabel.create('Supports user verification', false);
@@ -564,17 +580,29 @@
this.#userVerificationCheckbox.classList.add('authenticator-option-checkbox');
userVerificationGroup.appendChild(this.#userVerificationCheckboxLabel);
- this.#addAuthenticatorButton =
+ this.#largeBlobCheckboxLabel = UI.UIUtils.CheckboxLabel.create(i18nString(UIStrings.supportsLargeBlob), false);
+ this.#largeBlobCheckboxLabel.textElement.classList.add('authenticator-option-label');
+ largeBlobGroup.appendChild(this.#largeBlobCheckboxLabel.textElement);
+ this.largeBlobCheckbox = this.#largeBlobCheckboxLabel.checkboxElement;
+ this.largeBlobCheckbox.checked = false;
+ this.largeBlobCheckbox.classList.add('authenticator-option-checkbox');
+ this.largeBlobCheckbox.name = 'large-blob-checkbox';
+ largeBlobGroup.appendChild(this.#largeBlobCheckboxLabel);
+
+ this.addAuthenticatorButton =
UI.UIUtils.createTextButton(i18nString(UIStrings.add), this.#handleAddAuthenticatorButton.bind(this), '');
addButtonGroup.createChild('div', 'authenticator-option-label');
- addButtonGroup.appendChild(this.#addAuthenticatorButton);
+ addButtonGroup.appendChild(this.addAuthenticatorButton);
const addAuthenticatorTitle = UI.UIUtils.createLabel(i18nString(UIStrings.addAuthenticator), '');
- UI.ARIAUtils.bindLabelToControl(addAuthenticatorTitle, this.#addAuthenticatorButton);
+ UI.ARIAUtils.bindLabelToControl(addAuthenticatorTitle, this.addAuthenticatorButton);
this.#updateNewAuthenticatorSectionOptions();
if (this.#protocolSelect) {
this.#protocolSelect.addEventListener('change', this.#updateNewAuthenticatorSectionOptions.bind(this));
}
+ if (this.residentKeyCheckbox) {
+ this.residentKeyCheckbox.addEventListener('change', this.#updateNewAuthenticatorSectionOptions.bind(this));
+ }
}
async #handleAddAuthenticatorButton(): Promise<void> {
@@ -702,6 +730,7 @@
const protocolField = sectionFields.createChild('div', 'authenticator-field');
const transportField = sectionFields.createChild('div', 'authenticator-field');
const srkField = sectionFields.createChild('div', 'authenticator-field');
+ const slbField = sectionFields.createChild('div', 'authenticator-field');
const suvField = sectionFields.createChild('div', 'authenticator-field');
uuidField.appendChild(UI.UIUtils.createLabel(i18nString(UIStrings.uuid), 'authenticator-option-label'));
@@ -709,6 +738,7 @@
transportField.appendChild(UI.UIUtils.createLabel(i18nString(UIStrings.transport), 'authenticator-option-label'));
srkField.appendChild(
UI.UIUtils.createLabel(i18nString(UIStrings.supportsResidentKeys), 'authenticator-option-label'));
+ slbField.appendChild(UI.UIUtils.createLabel(i18nString(UIStrings.supportsLargeBlob), 'authenticator-option-label'));
suvField.appendChild(
UI.UIUtils.createLabel(i18nString(UIStrings.supportsUserVerification), 'authenticator-option-label'));
@@ -717,6 +747,8 @@
transportField.createChild('div', 'authenticator-field-value').textContent = options.transport;
srkField.createChild('div', 'authenticator-field-value').textContent =
options.hasResidentKey ? i18nString(UIStrings.yes) : i18nString(UIStrings.no);
+ slbField.createChild('div', 'authenticator-field-value').textContent =
+ options.hasLargeBlob ? i18nString(UIStrings.yes) : i18nString(UIStrings.no);
suvField.createChild('div', 'authenticator-field-value').textContent =
options.hasUserVerification ? i18nString(UIStrings.yes) : i18nString(UIStrings.no);
}
@@ -779,18 +811,20 @@
#createOptionsFromCurrentInputs(): Protocol.WebAuthn.VirtualAuthenticatorOptions {
// TODO(crbug.com/1034663): Add optionality for isUserVerified param.
- if (!this.#protocolSelect || !this.#transportSelect || !this.#residentKeyCheckbox ||
- !this.#userVerificationCheckbox) {
+ if (!this.#protocolSelect || !this.#transportSelect || !this.residentKeyCheckbox ||
+ !this.#userVerificationCheckbox || !this.largeBlobCheckbox) {
throw new Error('Unable to create options from current inputs');
}
return {
protocol: this.#protocolSelect.options[this.#protocolSelect.selectedIndex].value as
Protocol.WebAuthn.AuthenticatorProtocol,
+ ctap2Version: Protocol.WebAuthn.Ctap2Version.Ctap2_1,
transport: this.#transportSelect.options[this.#transportSelect.selectedIndex].value as
Protocol.WebAuthn.AuthenticatorTransport,
- hasResidentKey: this.#residentKeyCheckbox.checked,
+ hasResidentKey: this.residentKeyCheckbox.checked,
hasUserVerification: this.#userVerificationCheckbox.checked,
+ hasLargeBlob: this.largeBlobCheckbox.checked,
automaticPresenceSimulation: true,
isUserVerified: true,
};
diff --git a/test/unittests/front_end/BUILD.gn b/test/unittests/front_end/BUILD.gn
index 0c6aed0..a99235c 100644
--- a/test/unittests/front_end/BUILD.gn
+++ b/test/unittests/front_end/BUILD.gn
@@ -45,6 +45,7 @@
"panels/timeline",
"panels/timeline/components",
"panels/utils",
+ "panels/webauthn",
"test_setup",
"third_party/i18n",
"ui",
diff --git a/test/unittests/front_end/panels/webauthn/BUILD.gn b/test/unittests/front_end/panels/webauthn/BUILD.gn
new file mode 100644
index 0000000..1eec7c3
--- /dev/null
+++ b/test/unittests/front_end/panels/webauthn/BUILD.gn
@@ -0,0 +1,17 @@
+# Copyright 2022 The Chromium Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import("../../../../../third_party/typescript/typescript.gni")
+
+ts_library("webauthn") {
+ testonly = true
+ sources = [ "WebauthnPane_test.ts" ]
+
+ deps = [
+ "../../../../../front_end/core/platform:bundle",
+ "../../../../../front_end/core/sdk:bundle",
+ "../../../../../front_end/panels/webauthn:bundle",
+ "../../helpers",
+ ]
+}
diff --git a/test/unittests/front_end/panels/webauthn/WebauthnPane_test.ts b/test/unittests/front_end/panels/webauthn/WebauthnPane_test.ts
new file mode 100644
index 0000000..126fc6d
--- /dev/null
+++ b/test/unittests/front_end/panels/webauthn/WebauthnPane_test.ts
@@ -0,0 +1,110 @@
+// Copyright (c) 2022 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import {createTarget} from '../../helpers/EnvironmentHelpers.js';
+import {
+ describeWithMockConnection,
+ setMockConnectionResponseHandler,
+} from '../../helpers/MockConnection.js';
+
+import type * as WebauthnModule from '../../../../../front_end/panels/webauthn/webauthn.js';
+import * as SDK from '../../../../../front_end/core/sdk/sdk.js';
+
+const {assert} = chai;
+
+describeWithMockConnection('WebAuthn pane', () => {
+ let Webauthn: typeof WebauthnModule;
+
+ before(async () => {
+ Webauthn = await import('../../../../../front_end/panels/webauthn/webauthn.js');
+ });
+
+ it('disables the large blob checkbox if resident key is disabled', () => {
+ const panel = Webauthn.WebauthnPane.WebauthnPaneImpl.instance();
+ const largeBlob = panel.largeBlobCheckbox;
+ const residentKeys = panel.residentKeyCheckbox;
+
+ if (!largeBlob || !residentKeys) {
+ assert.fail('Required checkbox not found');
+ return;
+ }
+
+ // Make sure resident keys is disabled. Large blob should be disabled and
+ // unchecked.
+ residentKeys.checked = false;
+ residentKeys.dispatchEvent(new Event('change'));
+ assert.isTrue(largeBlob.disabled);
+ assert.isFalse(largeBlob.checked);
+
+ // Enable resident keys. Large blob should be enabled but still not
+ // checked.
+ residentKeys.checked = true;
+ residentKeys.dispatchEvent(new Event('change'));
+ assert.isFalse(largeBlob.disabled);
+ assert.isFalse(largeBlob.checked);
+
+ // Manually check large blob.
+ largeBlob.checked = true;
+ assert.isTrue(largeBlob.checked);
+
+ // Disabling resident keys should reset large blob to disabled and
+ // unchecked.
+ residentKeys.checked = false;
+ residentKeys.dispatchEvent(new Event('change'));
+ assert.isTrue(largeBlob.disabled);
+ assert.isFalse(largeBlob.checked);
+ });
+
+ it('adds an authenticator with large blob option', done => {
+ const target = createTarget();
+ const panel = Webauthn.WebauthnPane.WebauthnPaneImpl.instance();
+ panel.modelAdded(new SDK.WebAuthnModel.WebAuthnModel(target));
+
+ const largeBlob = panel.largeBlobCheckbox;
+ const residentKeys = panel.residentKeyCheckbox;
+
+ if (!largeBlob || !residentKeys) {
+ assert.fail('Required checkbox not found');
+ return;
+ }
+ residentKeys.checked = true;
+ largeBlob.checked = true;
+
+ setMockConnectionResponseHandler('WebAuthn.addVirtualAuthenticator', params => {
+ assert.isTrue(params.options.hasLargeBlob);
+ assert.isTrue(params.options.hasResidentKey);
+ done();
+ return {
+ authenticatorId: 'test',
+ };
+ });
+ panel.addAuthenticatorButton?.click();
+ });
+
+ it('adds an authenticator without the large blob option', done => {
+ const target = createTarget();
+ const panel = Webauthn.WebauthnPane.WebauthnPaneImpl.instance();
+ panel.modelAdded(new SDK.WebAuthnModel.WebAuthnModel(target));
+
+ const largeBlob = panel.largeBlobCheckbox;
+ const residentKeys = panel.residentKeyCheckbox;
+
+ if (!largeBlob || !residentKeys) {
+ assert.fail('Required checkbox not found');
+ return;
+ }
+ residentKeys.checked = true;
+ largeBlob.checked = false;
+
+ setMockConnectionResponseHandler('WebAuthn.addVirtualAuthenticator', params => {
+ assert.isFalse(params.options.hasLargeBlob);
+ assert.isTrue(params.options.hasResidentKey);
+ done();
+ return {
+ authenticatorId: 'test',
+ };
+ });
+ panel.addAuthenticatorButton?.click();
+ });
+});