blob: ba411166b3d7988c72c304a6d43c8ccbae47d2ff [file] [log] [blame]
// Copyright 2019 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.
/**
* @fileoverview 'settings-security-keys-bio-enroll-dialog' is a dialog for
* listing, adding, renaming, and deleting biometric enrollments stored on a
* security key.
*/
import 'chrome://resources/cr_elements/cr_button/cr_button.m.js';
import 'chrome://resources/cr_elements/cr_dialog/cr_dialog.m.js';
import 'chrome://resources/cr_elements/cr_fingerprint/cr_fingerprint_progress_arc.m.js';
import 'chrome://resources/cr_elements/cr_icon_button/cr_icon_button.m.js';
import 'chrome://resources/polymer/v3_0/iron-icon/iron-icon.js';
import 'chrome://resources/polymer/v3_0/iron-list/iron-list.js';
import 'chrome://resources/polymer/v3_0/iron-pages/iron-pages.js';
import 'chrome://resources/polymer/v3_0/paper-spinner/paper-spinner-lite.js';
import '../settings_shared_css.js';
import '../site_favicon.js';
import {assert, assertNotReached} from 'chrome://resources/js/assert.m.js';
import {I18nBehavior, I18nBehaviorInterface} from 'chrome://resources/js/i18n_behavior.m.js';
import {WebUIListenerBehavior, WebUIListenerBehaviorInterface} from 'chrome://resources/js/web_ui_listener_behavior.m.js';
import {IronA11yAnnouncer} from 'chrome://resources/polymer/v3_0/iron-a11y-announcer/iron-a11y-announcer.js';
import {afterNextRender, html, mixinBehaviors, PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {loadTimeData} from '../i18n_setup.js';
import {Ctap2Status, Enrollment, EnrollmentResponse, SampleResponse, SampleStatus, SecurityKeysBioEnrollProxy, SecurityKeysBioEnrollProxyImpl,} from './security_keys_browser_proxy.js';
import {SettingsSecurityKeysPinFieldElement} from './security_keys_pin_field.js';
/** @enum {string} */
export const BioEnrollDialogPage = {
INITIAL: 'initial',
PIN_PROMPT: 'pinPrompt',
ENROLLMENTS: 'enrollments',
ENROLL: 'enroll',
CHOOSE_NAME: 'chooseName',
ERROR: 'error',
};
/**
* @constructor
* @extends {PolymerElement}
* @implements {I18nBehaviorInterface}
* @implements {WebUIListenerBehaviorInterface}
*/
const SettingsSecurityKeysBioEnrollDialogElementBase =
mixinBehaviors([I18nBehavior, WebUIListenerBehavior], PolymerElement);
/** @polymer */
class SettingsSecurityKeysBioEnrollDialogElement extends
SettingsSecurityKeysBioEnrollDialogElementBase {
static get is() {
return 'settings-security-keys-bio-enroll-dialog';
}
static get template() {
return html`{__html_template__}`;
}
static get properties() {
return {
/** @private */
cancelButtonDisabled_: Boolean,
/** @private */
cancelButtonVisible_: Boolean,
/** @private */
confirmButtonDisabled_: Boolean,
/** @private */
confirmButtonVisible_: Boolean,
/** @private */
confirmButtonLabel_: String,
/** @private */
deleteInProgress_: Boolean,
/**
* The ID of the element currently shown in the dialog.
* @private {!BioEnrollDialogPage}
*/
dialogPage_: {
type: String,
value: BioEnrollDialogPage.INITIAL,
observer: 'dialogPageChanged_',
},
/** @private */
doneButtonVisible_: Boolean,
/**
* The list of enrollments displayed.
* @private {!Array<!Enrollment>}
*/
enrollments_: Array,
/** @private */
minPinLength_: Number,
/** @private */
progressArcLabel_: String,
/** @private */
recentEnrollmentName_: String,
/** @private {?string} */
enrollmentNameError_: String,
/** @private */
enrollmentNameMaxUtf8Length_: Number,
/** @private */
errorMsg_: String,
};
}
constructor() {
super();
/** @private {!SecurityKeysBioEnrollProxy} */
this.browserProxy_ = SecurityKeysBioEnrollProxyImpl.getInstance();
/** @private {number} */
this.maxSamples_ = -1;
/** @private {string} */
this.recentEnrollmentId_ = '';
/** @private {boolean} */
this.showSetPINButton_ = false;
}
/** @override */
connectedCallback() {
super.connectedCallback();
afterNextRender(this, function() {
IronA11yAnnouncer.requestAvailability();
});
this.$.dialog.showModal();
this.addWebUIListener(
'security-keys-bio-enroll-error', this.onError_.bind(this));
this.addWebUIListener(
'security-keys-bio-enroll-status', this.onEnrollmentSample_.bind(this));
this.browserProxy_.startBioEnroll().then(([minPinLength]) => {
this.minPinLength_ = minPinLength;
this.dialogPage_ = BioEnrollDialogPage.PIN_PROMPT;
});
}
/**
* @param {string} eventName
* @param {*=} detail
* @private
*/
fire_(eventName, detail) {
this.dispatchEvent(
new CustomEvent(eventName, {bubbles: true, composed: true, detail}));
}
/**
* @private
* @param {string} error
* @param {boolean=} requiresPINChange
*/
onError_(error, requiresPINChange = false) {
this.errorMsg_ = error;
this.showSetPINButton_ = requiresPINChange;
this.dialogPage_ = BioEnrollDialogPage.ERROR;
}
/** @private */
submitPIN_() {
// Disable the confirm button to prevent concurrent submissions.
this.confirmButtonDisabled_ = true;
/** @type {!SettingsSecurityKeysPinFieldElement} */ (this.$.pin)
.trySubmit(pin => this.browserProxy_.providePIN(pin))
.then(
() => {
this.browserProxy_.getSensorInfo().then(sensorInfo => {
this.enrollmentNameMaxUtf8Length_ =
sensorInfo.maxTemplateFriendlyName;
// Leave confirm button disabled while enumerating fingerprints.
// It will be re-enabled by dialogPageChanged_() where
// appropriate.
this.showEnrollmentsPage_();
});
},
() => {
// Wrong PIN.
this.confirmButtonDisabled_ = false;
});
}
/**
* @private
* @param {!Array<!Enrollment>} enrollments
*/
onEnrollments_(enrollments) {
this.enrollments_ =
enrollments.slice().sort((a, b) => a.name.localeCompare(b.name));
this.$.enrollmentList.fire('iron-resize');
this.dialogPage_ = BioEnrollDialogPage.ENROLLMENTS;
}
/** @private */
dialogPageChanged_() {
switch (this.dialogPage_) {
case BioEnrollDialogPage.INITIAL:
this.cancelButtonVisible_ = true;
this.cancelButtonDisabled_ = false;
this.confirmButtonVisible_ = false;
this.doneButtonVisible_ = false;
break;
case BioEnrollDialogPage.PIN_PROMPT:
this.cancelButtonVisible_ = true;
this.cancelButtonDisabled_ = false;
this.confirmButtonVisible_ = true;
this.confirmButtonLabel_ = this.i18n('continue');
this.confirmButtonDisabled_ = false;
this.doneButtonVisible_ = false;
this.$.pin.focus();
break;
case BioEnrollDialogPage.ENROLLMENTS:
this.cancelButtonVisible_ = false;
this.confirmButtonVisible_ = false;
this.doneButtonVisible_ = true;
break;
case BioEnrollDialogPage.ENROLL:
this.cancelButtonVisible_ = true;
this.cancelButtonDisabled_ = false;
this.confirmButtonVisible_ = false;
this.doneButtonVisible_ = false;
break;
case BioEnrollDialogPage.CHOOSE_NAME:
this.cancelButtonVisible_ = false;
this.confirmButtonVisible_ = true;
this.confirmButtonLabel_ = this.i18n('continue');
this.confirmButtonDisabled_ = !this.recentEnrollmentName_.length;
this.doneButtonVisible_ = false;
this.$.enrollmentName.focus();
break;
case BioEnrollDialogPage.ERROR:
this.cancelButtonVisible_ = true;
this.confirmButtonVisible_ = this.showSetPINButton_;
this.confirmButtonLabel_ = this.i18n('securityKeysSetPinButton');
this.doneButtonVisible_ = false;
break;
default:
assertNotReached();
}
this.fire_('bio-enroll-dialog-ready-for-testing');
}
/** @private */
addButtonClick_() {
assert(this.dialogPage_ === BioEnrollDialogPage.ENROLLMENTS);
this.maxSamples_ = -1; // Reset maxSamples_ before enrolling starts.
/** @type {!CrFingerprintProgressArcElement} */ (this.$.arc).reset();
this.progressArcLabel_ =
this.i18n('securityKeysBioEnrollmentEnrollingLabel');
this.recentEnrollmentId_ = '';
this.recentEnrollmentName_ = '';
this.dialogPage_ = BioEnrollDialogPage.ENROLL;
this.browserProxy_.startEnrolling().then(response => {
this.onEnrollmentComplete_(response);
});
}
/**
* @private
* @param {!SampleResponse} response
*/
onEnrollmentSample_(response) {
if (response.status !== SampleStatus.OK) {
this.progressArcLabel_ =
this.i18n('securityKeysBioEnrollmentTryAgainLabel');
this.fire_('iron-announce', {text: this.progressArcLabel_});
return;
}
this.progressArcLabel_ =
this.i18n('securityKeysBioEnrollmentEnrollingLabel');
assert(response.remaining >= 0);
if (this.maxSamples_ === -1) {
this.maxSamples_ = response.remaining + 1;
}
/** @type {!CrFingerprintProgressArcElement} */ (this.$.arc)
.setProgress(
100 * (this.maxSamples_ - response.remaining - 1) /
this.maxSamples_,
100 * (this.maxSamples_ - response.remaining) / this.maxSamples_,
false);
}
/**
* @private
* @param {!EnrollmentResponse} response
*/
onEnrollmentComplete_(response) {
switch (response.code) {
case Ctap2Status.OK:
break;
case Ctap2Status.ERR_KEEPALIVE_CANCEL:
this.showEnrollmentsPage_();
return;
case Ctap2Status.ERR_FP_DATABASE_FULL:
this.onError_(this.i18n('securityKeysBioEnrollmentStorageFullLabel'));
return;
default:
this.onError_(
this.i18n('securityKeysBioEnrollmentEnrollingFailedLabel'));
return;
}
this.maxSamples_ = Math.max(this.maxSamples_, 1);
/** @type {!CrFingerprintProgressArcElement} */ (this.$.arc)
.setProgress(
100 * (this.maxSamples_ - 1) / this.maxSamples_, 100, true);
assert(response.enrollment);
this.recentEnrollmentId_ = response.enrollment.id;
this.recentEnrollmentName_ = response.enrollment.name;
this.cancelButtonVisible_ = false;
this.confirmButtonVisible_ = true;
this.confirmButtonDisabled_ = false;
this.progressArcLabel_ =
this.i18n('securityKeysBioEnrollmentEnrollingCompleteLabel');
this.$.confirmButton.focus();
// Make screen-readers announce enrollment completion.
this.fire_('iron-announce', {text: this.progressArcLabel_});
this.fire_('bio-enroll-dialog-ready-for-testing');
}
/** @private */
confirmButtonClick_() {
switch (this.dialogPage_) {
case BioEnrollDialogPage.PIN_PROMPT:
this.submitPIN_();
break;
case BioEnrollDialogPage.ENROLL:
assert(!!this.recentEnrollmentId_.length);
this.dialogPage_ = BioEnrollDialogPage.CHOOSE_NAME;
break;
case BioEnrollDialogPage.CHOOSE_NAME:
this.renameNewEnrollment_();
break;
case BioEnrollDialogPage.ERROR:
this.$.dialog.close();
this.fire_('bio-enroll-set-pin');
break;
default:
assertNotReached();
}
}
/** @private */
renameNewEnrollment_() {
assert(this.dialogPage_ === BioEnrollDialogPage.CHOOSE_NAME);
// Check that the user-provided name doesn't exceed the maximum permissible
// length reported by the security key when encoded as UTF-8. (Note that
// JavaScript String length counts code units, but string length maximums in
// CTAP 2.1 are generally on UTF-8 bytes.)
if (new TextEncoder().encode(this.recentEnrollmentName_).length >
this.enrollmentNameMaxUtf8Length_) {
this.enrollmentNameError_ =
this.i18n('securityKeysBioEnrollmentNameLabelTooLong');
return;
}
this.enrollmentNameError_ = null;
// Disable the confirm button to prevent concurrent submissions. It will
// be re-enabled by dialogPageChanged_() where appropriate.
this.confirmButtonDisabled_ = true;
this.browserProxy_
.renameEnrollment(this.recentEnrollmentId_, this.recentEnrollmentName_)
.then(enrollments => {
this.onEnrollments_(enrollments);
});
}
/** @private */
showEnrollmentsPage_() {
this.browserProxy_.enumerateEnrollments().then(enrollments => {
this.onEnrollments_(enrollments);
});
}
/** @private */
cancel_() {
if (this.dialogPage_ === BioEnrollDialogPage.ENROLL) {
// Cancel an ongoing enrollment. Will cause the pending
// enumerateEnrollments() promise to be resolved and proceed to the
// enrollments page.
this.cancelButtonDisabled_ = true;
this.browserProxy_.cancelEnrollment();
} else {
// On any other screen, simply close the dialog.
this.done_();
}
}
/** @private */
done_() {
this.$.dialog.close();
}
/** @private */
onDialogClosed_() {
this.browserProxy_.close();
}
/**
* @private
* @param {!Event} e
*/
onIronSelect_(e) {
// Prevent this event from bubbling since it is unnecessarily triggering
// the listener within settings-animated-pages.
e.stopPropagation();
}
/**
* @private
* @param {!DomRepeatEvent} event
*/
deleteEnrollment_(event) {
if (this.deleteInProgress_) {
return;
}
this.deleteInProgress_ = true;
const enrollment = this.enrollments_[event.model.index];
this.browserProxy_.deleteEnrollment(enrollment.id).then(enrollments => {
this.deleteInProgress_ = false;
this.onEnrollments_(enrollments);
});
}
/** @private */
onEnrollmentNameInput_() {
this.confirmButtonDisabled_ = !this.recentEnrollmentName_.length;
}
/**
* @private
* @param {!BioEnrollDialogPage} dialogPage
* @return {string} The title string for the current dialog page.
*/
dialogTitle_(dialogPage) {
if (dialogPage === BioEnrollDialogPage.ENROLL ||
dialogPage === BioEnrollDialogPage.CHOOSE_NAME) {
return this.i18n('securityKeysBioEnrollmentAddTitle');
}
return this.i18n('securityKeysBioEnrollmentDialogTitle');
}
/**
* @private
* @param {?Array} enrollments
* @return {string} The header label for the enrollments page.
*/
enrollmentsHeader_(enrollments) {
return this.i18n(
enrollments && enrollments.length ?
'securityKeysBioEnrollmentEnrollmentsLabel' :
'securityKeysBioEnrollmentNoEnrollmentsLabel');
}
/**
* @private
* @param {string} string
* @return {boolean}
*/
isNullOrEmpty_(string) {
return string === '' || !string;
}
}
customElements.define(
SettingsSecurityKeysBioEnrollDialogElement.is,
SettingsSecurityKeysBioEnrollDialogElement);