| // 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. |
| * See https://w3c.github.io/webauthn/#signature-counter. |
| */ |
| signCount: 'Signature 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(); |
| } |
| } |