blob: e981a07a51d9b6197a07feaef6aa0e2a6ed92181 [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-pin-field' is a component for entering
* a security key PIN.
*/
import 'chrome://resources/cr_elements/cr_input/cr_input.m.js';
import 'chrome://resources/cr_elements/cr_icon_button/cr_icon_button.js';
import '../settings_shared.css.js';
import '../i18n_setup.js';
import {getInstance as getAnnouncerInstance} from 'chrome://resources/cr_elements/cr_a11y_announcer/cr_a11y_announcer.js';
import {CrInputElement} from 'chrome://resources/cr_elements/cr_input/cr_input.m.js';
import {I18nMixin} from 'chrome://resources/js/i18n_mixin.js';
import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {getTemplate} from './security_keys_pin_field.html.js';
/**
* A function that submits a PIN to a security key. It returns a Promise which
* resolves with null if the PIN was correct, or with the number of retries
* remaining otherwise.
*/
type PINFieldSubmitFunc = (pin: string) => Promise<number|null>;
export interface SettingsSecurityKeysPinFieldElement {
$: {
pin: CrInputElement,
};
}
const SettingsSecurityKeysPinFieldElementBase = I18nMixin(PolymerElement);
export class SettingsSecurityKeysPinFieldElement extends
SettingsSecurityKeysPinFieldElementBase {
static get is() {
return 'settings-security-keys-pin-field';
}
static get template() {
return getTemplate();
}
static get properties() {
return {
minPinLength: {
value: 4,
type: Number,
},
error_: {
type: String,
observer: 'errorChanged_',
},
value_: String,
inputVisible_: {
type: Boolean,
value: false,
},
};
}
minPinLength: number;
private error_: string;
private value_: string;
private inputVisible_: boolean;
/** Focuses the PIN input field. */
override focus() {
this.$.pin.focus();
}
/**
* Validates the PIN and sets the validation error if it is not valid.
* @return True iff the PIN is valid.
*/
private validate_(): boolean {
const error = this.isValidPIN_(this.value_);
if (error !== '') {
this.error_ = error;
return false;
}
return true;
}
/**
* Attempts submission of the PIN by invoking |submitFunc|. Updates the UI
* to show an error if the PIN was incorrect.
* @return A Promise that resolves if the PIN was correct, else rejects.
*/
trySubmit(submitFunc: PINFieldSubmitFunc): Promise<void> {
if (!this.validate_()) {
this.focus();
return Promise.reject();
}
return submitFunc(this.value_).then(retries => {
if (retries !== null) {
this.showIncorrectPINError_(retries);
this.focus();
return Promise.reject();
}
return;
});
}
/**
* Sets the validation error to indicate the PIN was incorrect.
* @param retries The number of retries remaining.
*/
private showIncorrectPINError_(retries: number) {
// Warn the user if the number of retries is getting low.
let error;
if (1 < retries && retries <= 3) {
error =
this.i18n('securityKeysPINIncorrectRetriesPl', retries.toString());
} else if (retries === 1) {
error = this.i18n('securityKeysPINIncorrectRetriesSin');
} else {
error = this.i18n('securityKeysPINIncorrect');
}
this.error_ = error;
}
private onPINInput_() {
// Typing in the PIN box after an error makes the error message
// disappear.
this.error_ = '';
}
/**
* Polymer helper function to detect when an error string is empty.
* @return True iff |s| is non-empty.
*/
private isNonEmpty_(s: string): boolean {
return s !== '';
}
/**
* @return The PIN-input element type.
*/
private inputType_(): string {
return this.inputVisible_ ? 'text' : 'password';
}
/**
* @return The class (and thus icon) to be displayed.
*/
private showButtonClass_(): string {
return 'icon-visibility' + (this.inputVisible_ ? '-off' : '');
}
/**
* @return The tooltip for the icon.
*/
private showButtonTitle_(): string {
return this.i18n(
this.inputVisible_ ? 'securityKeysHidePINs' : 'securityKeysShowPINs');
}
/**
* onClick handler for the show/hide icon.
*/
private showButtonClick_() {
this.inputVisible_ = !this.inputVisible_;
}
/**
* @param pin A candidate PIN.
* @return An error string or else '' to indicate validity.
*/
private isValidPIN_(pin: string): string {
// The UTF-8 encoding of the PIN must be between minPinLength
// and 63 bytes, and the final byte cannot be zero.
const utf8Encoded = new TextEncoder().encode(pin);
if (utf8Encoded.length < this.minPinLength) {
return this.i18n('securityKeysPINTooShort');
}
if (utf8Encoded.length > 63 ||
// If the PIN somehow has a NUL at the end then it's invalid, but this
// is so obscure that we don't try to message it. Rather we just say
// that it's too long because trimming the final character is the best
// response by the user.
utf8Encoded[utf8Encoded.length - 1] === 0) {
return this.i18n('securityKeysPINTooLong');
}
// A PIN must contain at least minPinLength code-points. Javascript strings
// are UCS-2 and the |length| property counts UCS-2 elements, not
// code-points. (For example, '\u{1f6b4}'.length === 2, but it's a single
// code-point.) Therefore, iterate over the string (which does yield
// codepoints) and check that four or more were seen.
let length = 0;
for (const _codepoint of pin) {
length++;
}
if (length < this.minPinLength) {
return this.i18n('securityKeysPINTooShort');
}
return '';
}
private errorChanged_() {
// Make screen readers announce changes to the PIN validation error
// label.
getAnnouncerInstance().announce(this.error_);
}
}
customElements.define(
SettingsSecurityKeysPinFieldElement.is,
SettingsSecurityKeysPinFieldElement);