blob: e77e8630a9a9e50a21647ad69ea6d8a49f66e141 [file] [log] [blame]
// 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.
import 'chrome://resources/cr_elements/cr_action_menu/cr_action_menu.m.js';
import 'chrome://resources/cr_elements/cr_button/cr_button.m.js';
import '../settings_shared_css.js';
import 'chrome://resources/cr_elements/icons.m.js';
import 'chrome://resources/cr_elements/shared_style_css.m.js';
import 'chrome://resources/polymer/v3_0/iron-flex-layout/iron-flex-layout-classes.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/paper-spinner/paper-spinner-lite.js';
import '../route.js';
import '../prefs/prefs.js';
import './password_check_edit_dialog.js';
import './password_check_edit_disclaimer_dialog.js';
import './password_check_list_item.js';
import './password_remove_confirmation_dialog.js';
// <if expr="chromeos">
import '../controls/password_prompt_dialog.js';
// </if>
import {assert, assertNotReached} from 'chrome://resources/js/assert.m.js';
import {focusWithoutInk} from 'chrome://resources/js/cr/ui/focus_without_ink.m.js';
import {I18nBehavior} from 'chrome://resources/js/i18n_behavior.m.js';
// <if expr="chromeos">
import {getDeepActiveElement} from 'chrome://resources/js/util.m.js';
// </if>
import {WebUIListenerBehavior} from 'chrome://resources/js/web_ui_listener_behavior.m.js';
import {html, Polymer} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {loadTimeData} from '../i18n_setup.js';
import {SyncBrowserProxyImpl, SyncPrefs, SyncStatus} from '../people_page/sync_browser_proxy.js';
import {PrefsBehavior} from '../prefs/prefs_behavior.js';
import {routes} from '../route.js';
import {Route, RouteObserverBehavior, Router} from '../router.js';
// <if expr="chromeos">
import {BlockingRequestManager} from './blocking_request_manager.js';
// </if>
import {PasswordCheckBehavior} from './password_check_behavior.js';
import {PasswordManagerImpl, PasswordManagerProxy} from './password_manager_proxy.js';
const CheckState = chrome.passwordsPrivate.PasswordCheckState;
Polymer({
is: 'settings-password-check',
_template: html`{__html_template__}`,
behaviors: [
I18nBehavior,
PasswordCheckBehavior,
PrefsBehavior,
RouteObserverBehavior,
WebUIListenerBehavior,
],
properties: {
// <if expr="not chromeos">
/** @private */
storedAccounts_: Array,
// </if>
/** @private */
title_: {
type: String,
computed: 'computeTitle_(status, canUsePasswordCheckup_)',
},
/** @private */
isSignedOut_: {
type: Boolean,
computed: 'computeIsSignedOut_(syncStatus_, storedAccounts_)',
},
/** @private */
isSyncingPasswords_: {
type: Boolean,
computed: 'computeIsSyncingPasswords_(syncPrefs_, syncStatus_)',
},
canUsePasswordCheckup_: {
type: Boolean,
computed: 'computeCanUsePasswordCheckup_(syncPrefs_, syncStatus_)',
},
/** @private */
isButtonHidden_: {
type: Boolean,
computed: 'computeIsButtonHidden_(status, isSignedOut_, isInitialStatus)',
},
/** @private {SyncPrefs} */
syncPrefs_: Object,
/** @private {SyncStatus} */
syncStatus_: Object,
/** @private */
showPasswordEditDialog_: Boolean,
/** @private */
showPasswordRemoveDialog_: Boolean,
/** @private */
showPasswordEditDisclaimer_: Boolean,
/**
* The password that the user is interacting with now.
* @private {?PasswordManagerProxy.InsecureCredential}
*/
activePassword_: Object,
/** @private */
showCompromisedCredentialsBody_: {
type: Boolean,
computed: 'computeShowCompromisedCredentialsBody_(' +
'isSignedOut_, leakedPasswords)',
},
/** @private */
showNoCompromisedPasswordsLabel_: {
type: Boolean,
computed: 'computeShowNoCompromisedPasswordsLabel_(' +
'syncStatus_, prefs.*, status, leakedPasswords)',
},
/** @private */
showHideMenuTitle_: {
type: String,
computed: 'computeShowHideMenuTitle(activePassword_)',
},
/** @private */
iconHaloClass_: {
type: String,
computed: 'computeIconHaloClass_(' +
'status, isSignedOut_, leakedPasswords, weakPasswords)',
},
/**
* The ids of insecure credentials for which user clicked "Change Password"
* button
* @private
*/
clickedChangePasswordIds_: {
type: Object,
value: new Set(),
},
// <if expr="chromeos">
/** @private */
showPasswordPromptDialog_: Boolean,
/** @private {BlockingRequestManager} */
tokenRequestManager_: Object,
// </if>
},
/**
* A stack of the elements that triggered dialog to open and should therefore
* receive focus when that dialog is closed. The bottom of the stack is the
* element that triggered the earliest open dialog and top of the stack is the
* element that triggered the most recent (i.e. active) dialog. If no dialog
* is open, the stack is empty.
* @private {?Array<!HTMLElement>}
*/
activeDialogAnchorStack_: null,
/**
* The password_check_list_item that the user is interacting with now.
* @private {?EventTarget}
*/
activeListItem_: null,
/** @private {boolean} */
startCheckAutomaticallySucceeded: false,
/**
* Observer for saved passwords to update startCheckAutomaticallySucceeded
* once they are changed. It's needed to run password check on navigation
* again once passwords changed.
* @private {?function(!Array<PasswordManagerProxy.PasswordUiEntry>):void}
*/
setSavedPasswordsListener_: null,
/** @override */
attached() {
// <if expr="chromeos">
// If the user's account supports the password check, an auth token will be
// required in order for them to view or export passwords. Otherwise there
// is no additional security so |tokenRequestManager_| will immediately
// resolve requests.
this.tokenRequestManager_ =
loadTimeData.getBoolean('userCannotManuallyEnterPassword') ?
new BlockingRequestManager() :
new BlockingRequestManager(this.openPasswordPromptDialog_.bind(this));
// </if>
this.activeDialogAnchorStack_ = [];
const setSavedPasswordsListener = list => {
this.startCheckAutomaticallySucceeded = false;
};
this.setSavedPasswordsListener_ = setSavedPasswordsListener;
this.passwordManager.addSavedPasswordListChangedListener(
setSavedPasswordsListener);
// Set the manager. These can be overridden by tests.
const syncBrowserProxy = SyncBrowserProxyImpl.getInstance();
const syncStatusChanged = syncStatus => this.syncStatus_ = syncStatus;
const syncPrefsChanged = syncPrefs => this.syncPrefs_ = syncPrefs;
// Listen for changes.
this.addWebUIListener('sync-status-changed', syncStatusChanged);
this.addWebUIListener('sync-prefs-changed', syncPrefsChanged);
// Request initial data.
syncBrowserProxy.getSyncStatus().then(syncStatusChanged);
syncBrowserProxy.sendSyncPrefsChanged();
// For non-ChromeOS, also check whether accounts are available.
// <if expr="not chromeos">
const storedAccountsChanged = accounts => this.storedAccounts_ = accounts;
syncBrowserProxy.getStoredAccounts().then(storedAccountsChanged);
this.addWebUIListener('stored-accounts-updated', storedAccountsChanged);
// </if>
},
/** @override */
detached() {
this.passwordManager.removeSavedPasswordListChangedListener(
assert(this.setSavedPasswordsListener_));
},
/**
* Tries to start bulk password check on page open if instructed to do so and
* didn't start successfully before
* @private
*/
currentRouteChanged(currentRoute) {
const router = Router.getInstance();
if (currentRoute.path === routes.CHECK_PASSWORDS.path &&
!this.startCheckAutomaticallySucceeded &&
router.getQueryParameters().get('start') === 'true') {
this.passwordManager.recordPasswordCheckInteraction(
PasswordManagerProxy.PasswordCheckInteraction
.START_CHECK_AUTOMATICALLY);
this.passwordManager.startBulkPasswordCheck().then(
() => {
this.startCheckAutomaticallySucceeded = true;
},
error => {
// Catching error
});
}
// Requesting status on navigation to update elapsedTimeSinceLastCheck
this.passwordManager.getPasswordCheckStatus().then(
status => this.status = status);
},
/**
* Start/Stop bulk password check.
* @private
*/
onPasswordCheckButtonClick_() {
switch (this.status.state) {
case CheckState.RUNNING:
this.passwordManager.recordPasswordCheckInteraction(
PasswordManagerProxy.PasswordCheckInteraction.STOP_CHECK);
this.passwordManager.stopBulkPasswordCheck();
return;
case CheckState.IDLE:
case CheckState.CANCELED:
case CheckState.OFFLINE:
case CheckState.OTHER_ERROR:
this.passwordManager.recordPasswordCheckInteraction(
PasswordManagerProxy.PasswordCheckInteraction.START_CHECK_MANUALLY);
this.passwordManager.startBulkPasswordCheck();
return;
case CheckState.SIGNED_OUT:
// Runs the startBulkPasswordCheck to check passwords for weakness that
// works for both sign in and sign out users.
this.passwordManager.recordPasswordCheckInteraction(
PasswordManagerProxy.PasswordCheckInteraction.START_CHECK_MANUALLY);
this.passwordManager.startBulkPasswordCheck().then(
() => {},
error => {
// Catching error
});
return;
case CheckState.NO_PASSWORDS:
case CheckState.QUOTA_LIMIT:
}
assertNotReached(
'Can\'t trigger an action for state: ' + this.status.state);
},
/**
* Returns true if there are any compromised credentials.
* @return {boolean}
* @private
*/
hasLeakedCredentials_() {
return !!this.leakedPasswords.length;
},
/**
* Returns true if there are any weak credentials.
* @return {boolean}
* @private
*/
hasWeakCredentials_() {
return !!this.weakPasswords.length;
},
/**
* Returns true if there are any insecure credentials.
* @return {boolean}
* @private
*/
hasInsecureCredentials_() {
return !!this.leakedPasswords.length || this.hasWeakCredentials_();
},
/**
* Returns a relevant help text for weak passwords. Contains a link that
* depends on whether the user is syncing passwords or not.
* @return {string}
* @private
*/
getWeakPasswordsHelpText_() {
return this.i18nAdvanced(
this.isSyncingPasswords_ ? 'weakPasswordsDescriptionGeneration' :
'weakPasswordsDescription');
},
/**
* @param {!CustomEvent<{moreActionsButton: !HTMLElement}>} event
* @private
*/
onMoreActionsClick_(event) {
const target = event.detail.moreActionsButton;
this.$.moreActionsMenu.showAt(target);
this.activeDialogAnchorStack_.push(target);
this.activeListItem_ = event.target;
this.activePassword_ = this.activeListItem_.item;
},
/** @private */
onMenuShowPasswordClick_() {
this.activePassword_.password ? this.activeListItem_.hidePassword() :
this.activeListItem_.showPassword();
this.$.moreActionsMenu.close();
this.activePassword_ = null;
this.activeDialogAnchorStack_.pop();
},
/** @private */
onEditPasswordClick_() {
this.passwordManager
.getPlaintextInsecurePassword(
assert(this.activePassword_),
chrome.passwordsPrivate.PlaintextReason.EDIT)
.then(
insecureCredential => {
this.activePassword_ = insecureCredential;
this.showPasswordEditDialog_ = true;
},
error => {
// <if expr="chromeos">
// If no password was found, refresh auth token and retry.
this.tokenRequestManager_.request(
this.onEditPasswordClick_.bind(this));
// </if>
// <if expr="not chromeos">
this.activePassword_ = null;
this.onPasswordEditDialogClosed_();
// </if>
});
this.$.moreActionsMenu.close();
},
/** @private */
onMenuRemovePasswordClick_() {
this.$.moreActionsMenu.close();
this.showPasswordRemoveDialog_ = true;
},
/** @private */
onPasswordRemoveDialogClosed_() {
this.showPasswordRemoveDialog_ = false;
focusWithoutInk(assert(this.activeDialogAnchorStack_.pop()));
},
/** @private */
onPasswordEditDialogClosed_() {
this.showPasswordEditDialog_ = false;
focusWithoutInk(assert(this.activeDialogAnchorStack_.pop()));
},
/**
* @param {!CustomEvent<!HTMLElement>} event
* @private
*/
onAlreadyChangedClick_(event) {
const target = event.detail;
// Setting required properties for Password Check Edit dialog
this.activeDialogAnchorStack_.push(target);
this.activeListItem_ = event.target;
this.activePassword_ = event.target.item;
this.showPasswordEditDisclaimer_ = true;
},
/** @private */
onEditDisclaimerClosed_() {
this.showPasswordEditDisclaimer_ = false;
focusWithoutInk(assert(this.activeDialogAnchorStack_.pop()));
},
/**
* @return {string}
* @private
*/
computeShowHideMenuTitle() {
return this.i18n(
this.activeListItem_.isPasswordVisible_ ? 'hideCompromisedPassword' :
'showCompromisedPassword');
},
/**
* @return {string}
* @private
*/
computeIconHaloClass_() {
return !this.isCheckInProgress_() && this.hasLeakedCredentials_() ?
'warning-halo' :
'';
},
/**
* Returns the icon (warning, info or error) indicating the check status.
* @return {string}
* @private
*/
getStatusIcon_() {
if (!this.hasInsecureCredentialsOrErrors_()) {
return 'settings:check-circle';
}
if (this.hasLeakedCredentials_()) {
return 'cr:warning';
}
return 'cr:info';
},
/**
* Returns the CSS class used to style the icon (warning, info or error).
* @return {string}
* @private
*/
getStatusIconClass_() {
if (!this.hasInsecureCredentialsOrErrors_()) {
return this.waitsForFirstCheck_() ? 'hidden' : 'no-security-issues';
}
if (this.hasLeakedCredentials_()) {
return 'has-security-issues';
}
return '';
},
/**
* Returns the title message indicating the state of the last/ongoing check.
* @return {string}
* @private
*/
computeTitle_() {
switch (this.status.state) {
case CheckState.IDLE:
return this.waitsForFirstCheck_() ?
this.i18n('checkPasswordsDescription') :
this.i18n('checkedPasswords');
case CheckState.CANCELED:
return this.i18n('checkPasswordsCanceled');
case CheckState.RUNNING:
// Returns the progress of a running check. Ensures that both numbers
// are at least 1.
return this.i18n(
'checkPasswordsProgress', (this.status.alreadyProcessed || 0) + 1,
Math.max(
this.status.remainingInQueue + this.status.alreadyProcessed,
1));
case CheckState.OFFLINE:
return this.i18n('checkPasswordsErrorOffline');
case CheckState.SIGNED_OUT:
// When user is signed out we run the password weakness check. Since it
// works very fast, we always shows "Checked passwords" in this case.
return this.i18n('checkedPasswords');
case CheckState.NO_PASSWORDS:
return this.i18n('checkPasswordsErrorNoPasswords');
case CheckState.QUOTA_LIMIT:
// Note: For the checkup case we embed the link as HTML, thus we need to
// use i18nAdvanced() here as well as the `inner-h-t-m-l` attribute in
// the DOM.
return this.canUsePasswordCheckup_ ?
this.i18nAdvanced('checkPasswordsErrorQuotaGoogleAccount') :
this.i18n('checkPasswordsErrorQuota');
case CheckState.OTHER_ERROR:
return this.i18n('checkPasswordsErrorGeneric');
}
assertNotReached('Can\'t find a title for state: ' + this.status.state);
},
/**
* Returns true iff a check is running right according to the given |status|.
* @return {boolean}
* @private
*/
isCheckInProgress_() {
return this.status.state === CheckState.RUNNING;
},
/**
* Returns true to show the timestamp when a check was completed successfully.
* @return {boolean}
* @private
*/
showsTimestamp_() {
return !!this.status.elapsedTimeSinceLastCheck &&
(this.status.state === CheckState.IDLE ||
this.status.state === CheckState.SIGNED_OUT);
},
/**
* Returns the button caption indicating it's current functionality.
* @return {string}
* @private
*/
getButtonText_() {
switch (this.status.state) {
case CheckState.IDLE:
return this.waitsForFirstCheck_() ? this.i18n('checkPasswords') :
this.i18n('checkPasswordsAgain');
case CheckState.CANCELED:
return this.i18n('checkPasswordsAgain');
case CheckState.RUNNING:
return this.i18n('checkPasswordsStop');
case CheckState.OFFLINE:
case CheckState.NO_PASSWORDS:
case CheckState.OTHER_ERROR:
return this.i18n('checkPasswordsAgainAfterError');
case CheckState.SIGNED_OUT:
// We should allow signed out users to click the "Check again" button to
// run the passwords weakness check.
return this.i18n('checkPasswordsAgain');
case CheckState.QUOTA_LIMIT:
return ''; // Undefined behavior. Don't show any misleading text.
}
assertNotReached(
'Can\'t find a button text for state: ' + this.status.state);
},
/**
* Returns 'action-button' only for the very first check.
* @return {string}
* @private
*/
getButtonTypeClass_() {
return this.waitsForFirstCheck_() ? 'action-button' : ' ';
},
/**
* Returns true iff the check/stop button should be visible for a given state.
* @return {!boolean}
* @private
*/
computeIsButtonHidden_() {
switch (this.status.state) {
case CheckState.IDLE:
return this.isInitialStatus; // Only a native IDLE state allows checks.
case CheckState.CANCELED:
case CheckState.RUNNING:
case CheckState.OFFLINE:
case CheckState.OTHER_ERROR:
case CheckState.SIGNED_OUT:
return false;
case CheckState.NO_PASSWORDS:
case CheckState.QUOTA_LIMIT:
return true;
}
assertNotReached(
'Can\'t determine button visibility for state: ' + this.status.state);
},
/**
* Returns the chrome:// address where the banner image is located.
* @param {boolean} isDarkMode
* @return {string}
* @private
*/
bannerImageSrc_(isDarkMode) {
const type =
(this.status.state === CheckState.IDLE && !this.waitsForFirstCheck_()) ?
'positive' :
'neutral';
const suffix = isDarkMode ? '_dark' : '';
return `chrome://settings/images/password_check_${type}${suffix}.svg`;
},
/**
* Returns true iff the banner should be shown.
* @return {boolean}
* @private
*/
shouldShowBanner_() {
if (this.hasInsecureCredentials_()) {
return false;
}
return this.status.state === CheckState.CANCELED ||
!this.hasInsecureCredentialsOrErrors_();
},
/**
* Returns true if there are insecure credentials or the status is unexpected
* for a regular password check.
* @return {boolean}
* @private
*/
hasInsecureCredentialsOrErrors_() {
if (this.hasInsecureCredentials_()) {
return true;
}
switch (this.status.state) {
case CheckState.IDLE:
case CheckState.RUNNING:
case CheckState.SIGNED_OUT:
return false;
case CheckState.CANCELED:
case CheckState.OFFLINE:
case CheckState.NO_PASSWORDS:
case CheckState.QUOTA_LIMIT:
case CheckState.OTHER_ERROR:
return true;
}
assertNotReached(
'Not specified whether to state is an error: ' + this.status.state);
},
/**
* Returns true if there are insecure credentials or the status is unexpected
* for a regular password check.
* @return {boolean}
* @private
*/
showsPasswordsCount_() {
if (this.hasInsecureCredentials_()) {
return true;
}
switch (this.status.state) {
case CheckState.IDLE:
return !this.waitsForFirstCheck_();
case CheckState.CANCELED:
case CheckState.RUNNING:
case CheckState.OFFLINE:
case CheckState.NO_PASSWORDS:
case CheckState.QUOTA_LIMIT:
case CheckState.OTHER_ERROR:
return false;
case CheckState.SIGNED_OUT:
// Shows "No security issues found" if user is signed out and doesn't
// have insecure credentials.
return true;
}
assertNotReached(
'Not specified whether to show passwords for state: ' +
this.status.state);
},
/**
* Returns a localized and pluralized string of the passwords count, depending
* on whether the user is signed in and whether other compromised passwords
* exist.
* @return {string}
* @private
*/
getPasswordsCount_() {
return this.isSignedOut_ && this.leakedPasswords.length === 0 ?
this.weakPasswordsCount :
this.insecurePasswordsCount;
},
/**
* Returns the label that should be shown in the compromised password section
* if a user is signed out. This label depends on whether the user already had
* compromised credentials that were found in the past.
* @return {string}
* @private
*/
getSignedOutUserLabel_() {
// This label contains the link, thus we need to use i18nAdvanced() here as
// well as the `inner-h-t-m-l` attribute in the DOM.
return this.i18nAdvanced(
this.hasLeakedCredentials_() ?
'signedOutUserHasCompromisedCredentialsLabel' :
'signedOutUserLabel');
},
/**
* Returns true iff the leak or weak check was performed at least once before.
* @return {boolean}
* @private
*/
waitsForFirstCheck_() {
return !this.status.elapsedTimeSinceLastCheck;
},
/**
* Returns true iff the user is signed out.
* @return {boolean}
* @private
*/
computeIsSignedOut_() {
if (!this.syncStatus_ || !this.syncStatus_.signedIn) {
return !this.storedAccounts_ || this.storedAccounts_.length === 0;
}
return !!this.syncStatus_.hasError;
},
/**
* Returns true iff the user is syncing passwords.
* @return {boolean}
* @private
*/
computeIsSyncingPasswords_() {
return !!this.syncStatus_ && !!this.syncStatus_.signedIn &&
!this.syncStatus_.hasError && !!this.syncPrefs_ &&
this.syncPrefs_.passwordsSynced;
},
/**
* Returns whether the user can use the online Password Checkup.
* @return {boolean}
* @private
*/
computeCanUsePasswordCheckup_() {
return !!this.syncStatus_ && !!this.syncStatus_.signedIn &&
(!this.syncPrefs_ || !this.syncPrefs_.encryptAllData);
},
/**
* @return {boolean}
* @private
*/
computeShowCompromisedCredentialsBody_() {
// Always shows compromised credetnials section if user is signed out.
return this.isSignedOut_ || this.hasLeakedCredentials_();
},
/**
* @return {boolean}
* @private
*/
computeShowNoCompromisedPasswordsLabel_() {
// Check if user isn't signed in.
if (!this.syncStatus_ || !this.syncStatus_.signedIn) {
return false;
}
// Check if breach detection is turned off in settings.
if (!this.prefs ||
!this.getPref('profile.password_manager_leak_detection').value) {
return false;
}
// Return true if there was a successful check and no compromised passwords
// were found.
return !this.hasLeakedCredentials_() && this.showsTimestamp_();
},
/**
* @param {!CustomEvent<{id: number}>} event
* @private
*/
onChangePasswordClick_(event) {
this.clickedChangePasswordIds_.add(event.detail.id);
this.notifyPath('clickedChangePasswordIds_.size');
},
/**
* @param {!PasswordManagerProxy.InsecureCredential} item
* @return {boolean}
* @private
*/
clickedChangePassword_(item) {
return this.clickedChangePasswordIds_.has(item.id);
},
// <if expr="chromeos">
/**
* Copied from passwords_section.js.
* TODO(crbug.com/1074228): Extract to a separate behavior
*
* @param {!CustomEvent<!chrome.quickUnlockPrivate.TokenInfo>} e - Contains
* newly created auth token. Note that its precise value is not relevant
* here, only the facts that it's created.
* @private
*/
onTokenObtained_(e) {
assert(e.detail);
this.tokenRequestManager_.resolve();
},
/** @private */
onPasswordPromptClosed_() {
this.showPasswordPromptDialog_ = false;
focusWithoutInk(assert(this.activeDialogAnchorStack_.pop()));
},
/** @private */
openPasswordPromptDialog_() {
this.activeDialogAnchorStack_.push(/** @type {!HTMLElement} */ (getDeepActiveElement()));
this.showPasswordPromptDialog_ = true;
},
// </if>
});