UNPKG

chrome-devtools-frontend

Version:
810 lines (706 loc) • 32.6 kB
// Copyright 2020 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. /* eslint-disable rulesdir/no_underscored_properties */ import * as Common from '../common/common.js'; import * as DataGrid from '../data_grid/data_grid.js'; import * as Host from '../host/host.js'; import * as i18n from '../i18n/i18n.js'; import * as SDK from '../sdk/sdk.js'; import * as UI from '../ui/ui.js'; export const UIStrings = { /** *@description Label for button that allows user to download the private key related to a credential. */ export: 'Export', /** *@description Label for an item to remove something */ remove: 'Remove', /** *@description Label for empty credentials table. *@example {navigator.credentials.create()} PH1 */ noCredentialsTryCallingSFromYour: 'No credentials. Try calling {PH1} from your website.', /** *@description Label for checkbox to toggle the virtual authenticator environment allowing user to interact with software-based virtual authenticators. */ enableVirtualAuthenticator: 'Enable virtual authenticator environment', /** *@description Label for ID field for credentials. */ id: 'ID', /** *@description Label for field that describes whether a credential is a resident credential. */ isResident: 'Is Resident', /** *@description Label for credential field that represents the Relying Party ID that the credential is scoped to. */ rpId: 'RP ID', /** *@description Label for credential field that represents the user a credential is mapped to */ userHandle: 'User Handle', /** *@description Label for signature counter field for credentials which represents the number of successful assertions. */ signCount: 'Sign Count', /** *@description Label for column with actions for credentials. */ actions: 'Actions', /** *@description Title for the table that holds the credentials that a authenticator has registered. */ credentials: 'Credentials', /** *@description Label for the learn more link that is shown before the virtual environment is enabled. */ useWebauthnForPhishingresistant: 'Use WebAuthn for phishing-resistant authentication', /** *@description Text that is usually a hyperlink to more documentation */ learnMore: 'Learn more', /** *@description Title for section of interface that allows user to add a new virtual authenticator. */ newAuthenticator: 'New authenticator', /** *@description Text for security or network protocol */ protocol: 'Protocol', /** *@description Label for input to select which transport option to use on virtual authenticators, e.g. USB or Bluetooth. */ transport: 'Transport', /** *@description Label for checkbox that toggles resident key support on virtual authenticators. */ supportsResidentKeys: 'Supports resident keys', /** *@description Text to add something */ add: 'Add', /** *@description Label for button to add a new virtual authenticator. */ addAuthenticator: 'Add authenticator', /** *@description Label for radio button that toggles whether an authenticator is active. */ active: 'Active', /** *@description Title for button that enables user to customize name of authenticator. */ editName: 'Edit name', /** *@description Title for button that enables user to save name of authenticator after editing it. */ saveName: 'Save name', /** *@description Title for a user-added virtual authenticator which is uniquely identified with its AUTHENTICATORID. *@example {8c7873be-0b13-4996-a794-1521331bbd96} PH1 */ authenticatorS: 'Authenticator {PH1}', /** *@description Name for generated file which user can download. A private key is a secret code which enables encoding and decoding of a credential. .pem is the file extension. */ privateKeypem: 'Private key.pem', /** *@description Label for field that holds an authenticator's universally unique identifier (UUID). */ uuid: 'UUID', /** *@description Label for checkbox that toggles user verification support on virtual authenticators. */ supportsUserVerification: 'Supports user verification', /** *@description Text in Timeline indicating that input has happened recently */ yes: 'Yes', /** *@description Text in Timeline indicating that input has not happened recently */ no: 'No', /** *@description Title of radio button that sets an authenticator as active. *@example {Authenticator ABCDEF} PH1 */ setSAsTheActiveAuthenticator: 'Set {PH1} as the active authenticator', }; const str_ = i18n.i18n.registerUIStrings('webauthn/WebauthnPane.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); const TIMEOUT = 1000; const enum Events { ExportCredential = 'ExportCredential', RemoveCredential = 'RemoveCredential', } class DataGridNode extends DataGrid.DataGrid.DataGridNode<DataGridNode> { constructor(credential: Protocol.WebAuthn.Credential) { super(credential); } nodeSelfHeight(): number { return 24; } createCell(columnId: string): HTMLElement { const cell = super.createCell(columnId); UI.Tooltip.Tooltip.install(cell, cell.textContent || ''); if (columnId !== 'actions') { return cell; } const exportButton = UI.UIUtils.createTextButton(i18nString(UIStrings.export), (): void => { if (this.dataGrid) { this.dataGrid.dispatchEventToListeners(Events.ExportCredential, this.data); } }); cell.appendChild(exportButton); const removeButton = UI.UIUtils.createTextButton(i18nString(UIStrings.remove), (): void => { if (this.dataGrid) { this.dataGrid.dispatchEventToListeners(Events.RemoveCredential, this.data); } }); cell.appendChild(removeButton); return cell; } } class EmptyDataGridNode extends DataGrid.DataGrid.DataGridNode<DataGridNode> { createCells(element: Element): void { element.removeChildren(); const td = (this.createTDWithClass(DataGrid.DataGrid.Align.Center) as HTMLTableCellElement); if (this.dataGrid) { td.colSpan = this.dataGrid.visibleColumnsArray.length; } const code = document.createElement('span', {is: 'source-code'}); code.textContent = 'navigator.credentials.create()'; code.classList.add('code'); const message = i18n.i18n.getFormatLocalizedString(str_, UIStrings.noCredentialsTryCallingSFromYour, {PH1: code}); td.appendChild(message); element.appendChild(td); } } type AvailableAuthenticatorOptions = Protocol.WebAuthn.VirtualAuthenticatorOptions&{ active: boolean, authenticatorId: Protocol.WebAuthn.AuthenticatorId, }; let webauthnPaneImplInstance: WebauthnPaneImpl; export class WebauthnPaneImpl extends UI.Widget.VBox { _enabled: boolean; _activeAuthId: string|null; _hasBeenEnabled: boolean; _dataGrids: Map<string, DataGrid.DataGrid.DataGridImpl<DataGridNode>>; // @ts-ignore _enableCheckbox: UI.Toolbar.ToolbarCheckbox; _availableAuthenticatorSetting: Common.Settings.Setting<AvailableAuthenticatorOptions[]>; _model: SDK.WebAuthnModel.WebAuthnModel|null|undefined; _authenticatorsView: HTMLElement; _topToolbarContainer: HTMLElement|undefined; _topToolbar: UI.Toolbar.Toolbar|undefined; _learnMoreView: HTMLElement|undefined; _newAuthenticatorSection: HTMLElement|undefined; _newAuthenticatorForm: HTMLElement|undefined; _protocolSelect: HTMLSelectElement|undefined; _transportSelect: HTMLSelectElement|undefined; _residentKeyCheckboxLabel: UI.UIUtils.CheckboxLabel|undefined; _residentKeyCheckbox: HTMLInputElement|undefined; _userVerificationCheckboxLabel: UI.UIUtils.CheckboxLabel|undefined; _userVerificationCheckbox: HTMLInputElement|undefined; _addAuthenticatorButton: HTMLButtonElement|undefined; _isEnabling?: Promise<void>; constructor() { super(true); this.registerRequiredCSS('webauthn/webauthnPane.css', {enableLegacyPatching: true}); this.contentElement.classList.add('webauthn-pane'); this._enabled = false; this._activeAuthId = null; this._hasBeenEnabled = false; this._dataGrids = new Map(); this._availableAuthenticatorSetting = (Common.Settings.Settings.instance().createSetting('webauthnAuthenticators', []) as Common.Settings.Setting<AvailableAuthenticatorOptions[]>); const mainTarget = SDK.SDKModel.TargetManager.instance().mainTarget(); if (mainTarget) { this._model = mainTarget.model(SDK.WebAuthnModel.WebAuthnModel); } this._createToolbar(); this._authenticatorsView = this.contentElement.createChild('div', 'authenticators-view'); this._createNewAuthenticatorSection(); this._updateVisibility(false); } static instance(opts = {forceNew: null}): WebauthnPaneImpl { const {forceNew} = opts; if (!webauthnPaneImplInstance || forceNew) { webauthnPaneImplInstance = new WebauthnPaneImpl(); } return webauthnPaneImplInstance; } async _loadInitialAuthenticators(): Promise<void> { let activeAuthenticatorId: string|null = null; const availableAuthenticators = this._availableAuthenticatorSetting.get(); for (const options of availableAuthenticators) { if (!this._model) { continue; } const authenticatorId = await this._model.addAuthenticator(options); this._addAuthenticatorSection(authenticatorId, options); // Update the authenticatorIds in the options. options.authenticatorId = authenticatorId; if (options.active) { activeAuthenticatorId = authenticatorId; } } // Update the settings to reflect the new authenticatorIds. this._availableAuthenticatorSetting.set(availableAuthenticators); if (activeAuthenticatorId) { this._setActiveAuthenticator(activeAuthenticatorId); } } async ownerViewDisposed(): Promise<void> { if (this._enableCheckbox) { this._enableCheckbox.setChecked(false); } await this._setVirtualAuthEnvEnabled(false); } _createToolbar(): void { this._topToolbarContainer = this.contentElement.createChild('div', 'webauthn-toolbar-container'); this._topToolbar = new UI.Toolbar.Toolbar('webauthn-toolbar', this._topToolbarContainer); const enableCheckboxTitle = i18nString(UIStrings.enableVirtualAuthenticator); this._enableCheckbox = new UI.Toolbar.ToolbarCheckbox(enableCheckboxTitle, enableCheckboxTitle, this._handleCheckboxToggle.bind(this)); this._topToolbar.appendToolbarItem(this._enableCheckbox); } _createCredentialsDataGrid(authenticatorId: string): DataGrid.DataGrid.DataGridImpl<DataGridNode> { const columns = ([ { id: 'credentialId', title: i18nString(UIStrings.id), longText: true, weight: 24, }, { id: 'isResidentCredential', title: i18nString(UIStrings.isResident), dataType: DataGrid.DataGrid.DataType.Boolean, weight: 10, }, { id: 'rpId', title: i18nString(UIStrings.rpId), }, { id: 'userHandle', title: i18nString(UIStrings.userHandle), }, { id: 'signCount', title: i18nString(UIStrings.signCount), }, {id: 'actions', title: i18nString(UIStrings.actions)}, ] as DataGrid.DataGrid.ColumnDescriptor[]); const dataGridConfig = { displayName: i18nString(UIStrings.credentials), columns, editCallback: undefined, deleteCallback: undefined, refreshCallback: undefined, }; const dataGrid = new DataGrid.DataGrid.DataGridImpl(dataGridConfig); dataGrid.renderInline(); dataGrid.setStriped(true); dataGrid.addEventListener(Events.ExportCredential, this._handleExportCredential, this); dataGrid.addEventListener(Events.RemoveCredential, this._handleRemoveCredential.bind(this, authenticatorId)); this._dataGrids.set(authenticatorId, dataGrid); return dataGrid; } _handleExportCredential(e: {data: Protocol.WebAuthn.Credential}): void { this._exportCredential(e.data); } _handleRemoveCredential(authenticatorId: string, e: {data: Protocol.WebAuthn.Credential}): void { this._removeCredential(authenticatorId, e.data.credentialId); } async _updateCredentials(authenticatorId: string): Promise<void> { const dataGrid = this._dataGrids.get(authenticatorId); if (!dataGrid) { return; } if (this._model) { const credentials = await this._model.getCredentials(authenticatorId); dataGrid.rootNode().removeChildren(); for (const credential of credentials) { const node = new DataGridNode(credential); dataGrid.rootNode().appendChild(node); } this._maybeAddEmptyNode(dataGrid); } // TODO(crbug.com/1112528): Add back-end events for credential creation and removal to avoid polling. setTimeout(this._updateCredentials.bind(this, authenticatorId), TIMEOUT); } _maybeAddEmptyNode(dataGrid: DataGrid.DataGrid.DataGridImpl<DataGridNode>): void { if (dataGrid.rootNode().children.length) { return; } const node = new EmptyDataGridNode(); dataGrid.rootNode().appendChild(node); } async _setVirtualAuthEnvEnabled(enable: boolean): Promise<void> { await this._isEnabling; this._isEnabling = new Promise<void>(async (resolve: (value: void) => void) => { if (enable && !this._hasBeenEnabled) { // Ensures metric is only tracked once per session. Host.userMetrics.actionTaken(Host.UserMetrics.Action.VirtualAuthenticatorEnvironmentEnabled); this._hasBeenEnabled = true; } this._enabled = enable; if (this._model) { await this._model.setVirtualAuthEnvEnabled(enable); } if (enable) { await this._loadInitialAuthenticators(); } else { this._removeAuthenticatorSections(); } this._updateVisibility(enable); this._isEnabling = undefined; resolve(); }); } _updateVisibility(enabled: boolean): void { this.contentElement.classList.toggle('enabled', enabled); } _removeAuthenticatorSections(): void { this._authenticatorsView.innerHTML = ''; this._dataGrids.clear(); } _handleCheckboxToggle(e: MouseEvent): void { this._setVirtualAuthEnvEnabled((e.target as HTMLInputElement).checked); } _updateEnabledTransportOptions(enabledOptions: Protocol.WebAuthn.AuthenticatorTransport[]): void { if (!this._transportSelect) { return; } const prevValue = this._transportSelect.value; this._transportSelect.removeChildren(); for (const option of enabledOptions) { this._transportSelect.appendChild(new Option(option, option)); } // Make sure the currently selected value stays the same. this._transportSelect.value = prevValue; // If the new set does not include the previous value. if (!this._transportSelect.value) { // Select the first available value. this._transportSelect.selectedIndex = 0; } } _updateNewAuthenticatorSectionOptions(): void { if (!this._protocolSelect || !this._residentKeyCheckbox || !this._userVerificationCheckbox) { return; } if (this._protocolSelect.value === Protocol.WebAuthn.AuthenticatorProtocol.Ctap2) { this._residentKeyCheckbox.disabled = false; this._userVerificationCheckbox.disabled = false; this._updateEnabledTransportOptions([ Protocol.WebAuthn.AuthenticatorTransport.Usb, Protocol.WebAuthn.AuthenticatorTransport.Ble, Protocol.WebAuthn.AuthenticatorTransport.Nfc, // TODO (crbug.com/1034663): Toggle cable as option depending on if cablev2 flag is on. // Protocol.WebAuthn.AuthenticatorTransport.Cable, Protocol.WebAuthn.AuthenticatorTransport.Internal, ]); } else { this._residentKeyCheckbox.checked = false; this._residentKeyCheckbox.disabled = true; this._userVerificationCheckbox.checked = false; this._userVerificationCheckbox.disabled = true; this._updateEnabledTransportOptions([ Protocol.WebAuthn.AuthenticatorTransport.Usb, Protocol.WebAuthn.AuthenticatorTransport.Ble, Protocol.WebAuthn.AuthenticatorTransport.Nfc, ]); } } _createNewAuthenticatorSection(): void { this._learnMoreView = this.contentElement.createChild('div', 'learn-more'); this._learnMoreView.appendChild(UI.Fragment.html` <div> ${i18nString(UIStrings.useWebauthnForPhishingresistant)}<br /><br /> ${ UI.XLink.XLink.create( 'https://developers.google.com/web/updates/2018/05/webauthn', i18nString(UIStrings.learnMore))} </div> `); this._newAuthenticatorSection = this.contentElement.createChild('div', 'new-authenticator-container'); const newAuthenticatorTitle = UI.UIUtils.createLabel(i18nString(UIStrings.newAuthenticator), 'new-authenticator-title'); this._newAuthenticatorSection.appendChild(newAuthenticatorTitle); this._newAuthenticatorForm = this._newAuthenticatorSection.createChild('div', 'new-authenticator-form'); const protocolGroup = this._newAuthenticatorForm.createChild('div', 'authenticator-option'); 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 addButtonGroup = this._newAuthenticatorForm.createChild('div', 'authenticator-option'); const protocolSelectTitle = UI.UIUtils.createLabel(i18nString(UIStrings.protocol), 'authenticator-option-label'); protocolGroup.appendChild(protocolSelectTitle); this._protocolSelect = (protocolGroup.createChild('select', 'chrome-select') as HTMLSelectElement); UI.ARIAUtils.bindLabelToControl(protocolSelectTitle, (this._protocolSelect as Element)); Object.values(Protocol.WebAuthn.AuthenticatorProtocol) .sort() .forEach((option: Protocol.WebAuthn.AuthenticatorProtocol): void => { if (this._protocolSelect) { this._protocolSelect.appendChild(new Option(option, option)); } }); if (this._protocolSelect) { this._protocolSelect.value = Protocol.WebAuthn.AuthenticatorProtocol.Ctap2; } const transportSelectTitle = UI.UIUtils.createLabel(i18nString(UIStrings.transport), 'authenticator-option-label'); transportGroup.appendChild(transportSelectTitle); this._transportSelect = (transportGroup.createChild('select', 'chrome-select') as HTMLSelectElement); UI.ARIAUtils.bindLabelToControl(transportSelectTitle, (this._transportSelect as Element)); // transportSelect will be populated in _updateNewAuthenticatorSectionOptions. 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'); residentKeyGroup.appendChild(this._residentKeyCheckboxLabel); this._userVerificationCheckboxLabel = UI.UIUtils.CheckboxLabel.create('Supports user verification', false); this._userVerificationCheckboxLabel.textElement.classList.add('authenticator-option-label'); userVerificationGroup.appendChild(this._userVerificationCheckboxLabel.textElement); this._userVerificationCheckbox = this._userVerificationCheckboxLabel.checkboxElement; this._userVerificationCheckbox.checked = false; this._userVerificationCheckbox.classList.add('authenticator-option-checkbox'); userVerificationGroup.appendChild(this._userVerificationCheckboxLabel); this._addAuthenticatorButton = UI.UIUtils.createTextButton(i18nString(UIStrings.add), this._handleAddAuthenticatorButton.bind(this), ''); addButtonGroup.createChild('div', 'authenticator-option-label'); addButtonGroup.appendChild(this._addAuthenticatorButton); const addAuthenticatorTitle = UI.UIUtils.createLabel(i18nString(UIStrings.addAuthenticator), ''); UI.ARIAUtils.bindLabelToControl(addAuthenticatorTitle, this._addAuthenticatorButton); this._updateNewAuthenticatorSectionOptions(); if (this._protocolSelect) { this._protocolSelect.addEventListener('change', this._updateNewAuthenticatorSectionOptions.bind(this)); } } async _handleAddAuthenticatorButton(): Promise<void> { const options = this._createOptionsFromCurrentInputs(); if (this._model) { const authenticatorId = await this._model.addAuthenticator(options); const availableAuthenticators = this._availableAuthenticatorSetting.get(); availableAuthenticators.push({authenticatorId, active: true, ...options}); this._availableAuthenticatorSetting.set( availableAuthenticators.map(a => ({...a, active: a.authenticatorId === authenticatorId}))); const section = await this._addAuthenticatorSection(authenticatorId, options); const mediaQueryList = window.matchMedia('(prefers-reduced-motion: reduce)'); const prefersReducedMotion = mediaQueryList.matches; section.scrollIntoView({block: 'start', behavior: prefersReducedMotion ? 'auto' : 'smooth'}); } } async _addAuthenticatorSection(authenticatorId: string, options: Protocol.WebAuthn.VirtualAuthenticatorOptions): Promise<HTMLDivElement> { const section = document.createElement('div'); section.classList.add('authenticator-section'); section.setAttribute('data-authenticator-id', authenticatorId); this._authenticatorsView.appendChild(section); const headerElement = section.createChild('div', 'authenticator-section-header'); const titleElement = headerElement.createChild('div', 'authenticator-section-title'); UI.ARIAUtils.markAsHeading(titleElement, 2); await this._clearActiveAuthenticator(); const activeButtonContainer = headerElement.createChild('div', 'active-button-container'); const activeLabel = UI.UIUtils.createRadioLabel(`active-authenticator-${authenticatorId}`, i18nString(UIStrings.active)); activeLabel.radioElement.addEventListener('click', this._setActiveAuthenticator.bind(this, authenticatorId)); activeButtonContainer.appendChild(activeLabel); /** @type {!HTMLInputElement} */ (activeLabel.radioElement as HTMLInputElement).checked = true; this._activeAuthId = authenticatorId; // Newly added authenticator is automatically set as active. const removeButton = headerElement.createChild('button', 'text-button'); removeButton.textContent = i18nString(UIStrings.remove); removeButton.addEventListener('click', this._removeAuthenticator.bind(this, authenticatorId)); const toolbar = new UI.Toolbar.Toolbar('edit-name-toolbar', titleElement); const editName = new UI.Toolbar.ToolbarButton(i18nString(UIStrings.editName), 'largeicon-edit'); const saveName = new UI.Toolbar.ToolbarButton(i18nString(UIStrings.saveName), 'largeicon-checkmark'); saveName.setVisible(false); const nameField = (titleElement.createChild('input', 'authenticator-name-field') as HTMLInputElement); nameField.disabled = true; const userFriendlyName = authenticatorId.slice(-5); // User friendly name defaults to last 5 chars of UUID. nameField.value = i18nString(UIStrings.authenticatorS, {PH1: userFriendlyName}); this._updateActiveLabelTitle(activeLabel, nameField.value); editName.addEventListener( UI.Toolbar.ToolbarButton.Events.Click, (): void => this._handleEditNameButton(titleElement, nameField, editName, saveName)); saveName.addEventListener( UI.Toolbar.ToolbarButton.Events.Click, (): void => this._handleSaveNameButton(titleElement, nameField, editName, saveName, activeLabel)); nameField.addEventListener( 'focusout', (): void => this._handleSaveNameButton(titleElement, nameField, editName, saveName, activeLabel)); nameField.addEventListener('keydown', (event: KeyboardEvent): void => { if (event.key === 'Enter') { this._handleSaveNameButton(titleElement, nameField, editName, saveName, activeLabel); } }); toolbar.appendToolbarItem(editName); toolbar.appendToolbarItem(saveName); this._createAuthenticatorFields(section, authenticatorId, options); const label = document.createElementWithClass('div', 'credentials-title'); label.textContent = i18nString(UIStrings.credentials); section.appendChild(label); const dataGrid = this._createCredentialsDataGrid(authenticatorId); dataGrid.asWidget().show(section); this._updateCredentials(authenticatorId); return section; } _exportCredential(credential: Protocol.WebAuthn.Credential): void { let pem = '-----BEGIN PRIVATE KEY-----\n'; for (let i = 0; i < credential.privateKey.length; i += 64) { pem += credential.privateKey.substring(i, i + 64) + '\n'; } pem += '-----END PRIVATE KEY-----'; const link = document.createElement('a'); link.download = i18nString(UIStrings.privateKeypem); link.href = 'data:application/x-pem-file,' + encodeURIComponent(pem); link.click(); } async _removeCredential(authenticatorId: string, credentialId: string): Promise<void> { const dataGrid = this._dataGrids.get(authenticatorId); if (!dataGrid) { return; } // @ts-ignore dataGrid node type is indeterminate. dataGrid.rootNode() .children .find((n: DataGrid.DataGrid.DataGridNode<DataGridNode>): boolean => n.data.credentialId === credentialId) .remove(); this._maybeAddEmptyNode(dataGrid); if (this._model) { await this._model.removeCredential(authenticatorId, credentialId); } } /** * Creates the fields describing the authenticator in the front end. */ _createAuthenticatorFields( section: Element, authenticatorId: string, options: Protocol.WebAuthn.VirtualAuthenticatorOptions): void { const sectionFields = section.createChild('div', 'authenticator-fields'); const uuidField = sectionFields.createChild('div', 'authenticator-field'); const protocolField = sectionFields.createChild('div', 'authenticator-field'); const transportField = sectionFields.createChild('div', 'authenticator-field'); const srkField = sectionFields.createChild('div', 'authenticator-field'); const suvField = sectionFields.createChild('div', 'authenticator-field'); uuidField.appendChild(UI.UIUtils.createLabel(i18nString(UIStrings.uuid), 'authenticator-option-label')); protocolField.appendChild(UI.UIUtils.createLabel(i18nString(UIStrings.protocol), 'authenticator-option-label')); transportField.appendChild(UI.UIUtils.createLabel(i18nString(UIStrings.transport), 'authenticator-option-label')); srkField.appendChild( UI.UIUtils.createLabel(i18nString(UIStrings.supportsResidentKeys), 'authenticator-option-label')); suvField.appendChild( UI.UIUtils.createLabel(i18nString(UIStrings.supportsUserVerification), 'authenticator-option-label')); uuidField.createChild('div', 'authenticator-field-value').textContent = authenticatorId; protocolField.createChild('div', 'authenticator-field-value').textContent = options.protocol; transportField.createChild('div', 'authenticator-field-value').textContent = options.transport; srkField.createChild('div', 'authenticator-field-value').textContent = options.hasResidentKey ? i18nString(UIStrings.yes) : i18nString(UIStrings.no); suvField.createChild('div', 'authenticator-field-value').textContent = options.hasUserVerification ? i18nString(UIStrings.yes) : i18nString(UIStrings.no); } _handleEditNameButton( titleElement: Element, nameField: HTMLInputElement, editName: UI.Toolbar.ToolbarButton, saveName: UI.Toolbar.ToolbarButton): void { nameField.disabled = false; titleElement.classList.add('editing-name'); nameField.focus(); saveName.setVisible(true); editName.setVisible(false); } _handleSaveNameButton( titleElement: Element, nameField: HTMLInputElement, editName: UI.Toolbar.ToolbarItem, saveName: UI.Toolbar.ToolbarItem, activeLabel: UI.UIUtils.DevToolsRadioButton): void { nameField.disabled = true; titleElement.classList.remove('editing-name'); editName.setVisible(true); saveName.setVisible(false); this._updateActiveLabelTitle(activeLabel, nameField.value); } _updateActiveLabelTitle(activeLabel: UI.UIUtils.DevToolsRadioButton, authenticatorName: string): void { UI.Tooltip.Tooltip.install( activeLabel.radioElement, i18nString(UIStrings.setSAsTheActiveAuthenticator, {PH1: authenticatorName})); } /** * Removes both the authenticator and its respective UI element. */ _removeAuthenticator(authenticatorId: string): void { if (this._authenticatorsView) { const child = this._authenticatorsView.querySelector(`[data-authenticator-id=${CSS.escape(authenticatorId)}]`); if (child) { child.remove(); } } this._dataGrids.delete(authenticatorId); if (this._model) { this._model.removeAuthenticator(authenticatorId); } // Update available authenticator setting. const prevAvailableAuthenticators = this._availableAuthenticatorSetting.get(); const newAvailableAuthenticators = prevAvailableAuthenticators.filter(a => a.authenticatorId !== authenticatorId); this._availableAuthenticatorSetting.set(newAvailableAuthenticators); if (this._activeAuthId === authenticatorId) { const availableAuthenticatorIds = Array.from(this._dataGrids.keys()); if (availableAuthenticatorIds.length) { this._setActiveAuthenticator(availableAuthenticatorIds[0]); } else { this._activeAuthId = null; } } } _createOptionsFromCurrentInputs(): Protocol.WebAuthn.VirtualAuthenticatorOptions { // TODO(crbug.com/1034663): Add optionality for isUserVerified param. if (!this._protocolSelect || !this._transportSelect || !this._residentKeyCheckbox || !this._userVerificationCheckbox) { throw new Error('Unable to create options from current inputs'); } /** * @type {!Protocol.WebAuthn.VirtualAuthenticatorOptions} */ const options = ({ protocol: this._protocolSelect.options[this._protocolSelect.selectedIndex].value, transport: this._transportSelect.options[this._transportSelect.selectedIndex].value, hasResidentKey: this._residentKeyCheckbox.checked, hasUserVerification: this._userVerificationCheckbox.checked, automaticPresenceSimulation: true, isUserVerified: true, } as Protocol.WebAuthn.VirtualAuthenticatorOptions); return options; } /** * Sets the given authenticator as active. * Note that a newly added authenticator will automatically be set as active. */ async _setActiveAuthenticator(authenticatorId: string): Promise<void> { await this._clearActiveAuthenticator(); if (this._model) { await this._model.setAutomaticPresenceSimulation(authenticatorId, true); } this._activeAuthId = authenticatorId; const prevAvailableAuthenticators = this._availableAuthenticatorSetting.get(); const newAvailableAuthenticators = prevAvailableAuthenticators.map(a => ({...a, active: a.authenticatorId === authenticatorId})); this._availableAuthenticatorSetting.set(newAvailableAuthenticators); this._updateActiveButtons(); } _updateActiveButtons(): void { const authenticators = this._authenticatorsView.getElementsByClassName('authenticator-section'); Array.from(authenticators).forEach((authenticator: Element): void => { const button = (authenticator.querySelector('input.dt-radio-button') as HTMLInputElement); if (!button) { return; } button.checked = /** @type {!HTMLElement} */ (authenticator as HTMLElement).dataset.authenticatorId === this._activeAuthId; }); } async _clearActiveAuthenticator(): Promise<void> { if (this._activeAuthId && this._model) { await this._model.setAutomaticPresenceSimulation(this._activeAuthId, false); } this._activeAuthId = null; this._updateActiveButtons(); } }