Commit 5645e0da authored by Viktor Semeniuk's avatar Viktor Semeniuk Committed by Commit Bot

[Passwords] List of compromised credentials

This change adds UI for visualizing compromised credentials list in
password check page. Also, it uses passwordsPrivate extension API to
obtain required data.

Bug: 1047726
Change-Id: Ia80ffd5d52447722d175aa5d66f10c2eb7dd3500
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2078257
Commit-Queue: Viktor Semeniuk <vsemeniuk@google.com>
Reviewed-by: default avatardpapad <dpapad@chromium.org>
Reviewed-by: default avatarJan Wilken Dörrie <jdoerrie@chromium.org>
Cr-Commit-Position: refs/heads/master@{#747221}
parent 4eed5b17
......@@ -332,6 +332,21 @@
<message name="IDS_SETTINGS_CHECK_PASSWORDS_AGAIN" desc="Button to start bulk password check manually in passwords check section.">
Check again
</message>
<message name="IDS_SETTINGS_COMPROMISED_PASSWORDS" desc="Title for list of compromised credentials after passwords bulk check.">
Compromised passwords
</message>
<message name="IDS_SETTINGS_COMPROMISED_PASSWORDS_ADVICE" desc="Description of what user should do after compromised passwords are found.">
Change these passwords immediately to keep your account safe:
</message>
<message name="IDS_SETTINGS_CHANGE_PASSWORD_BUTTON" desc="Button inside password check section which opens url for changing leaked password.">
Change password
</message>
<message name="IDS_SETTINGS_COMPROMISED_PASSWORD_REASON_LEAKED" desc="Password compromise reason shown when a password was found in a data breach.">
Found in data breach
</message>
<message name="IDS_SETTINGS_COMPROMISED_PASSWORD_REASON_PHISHED" desc="Password compromise reason shown when a password was reused on a phishing site.">
Entered on deceptive site
</message>
<message name="IDS_SETTINGS_PASSWORDS_SAVE_PASSWORDS_TOGGLE_LABEL" desc="Label for a toggle that allows users to be prompted if they want to save their passwords when logging into webpages.">
Offer to save passwords
</message>
......
bcaa799cb01b95769c46fa0c61368e01a32d0e1c
\ No newline at end of file
bcaa799cb01b95769c46fa0c61368e01a32d0e1c
\ No newline at end of file
a0ab79acfdbeed71eaed7dd45b5d18c2ed0520eb
\ No newline at end of file
bcaa799cb01b95769c46fa0c61368e01a32d0e1c
\ No newline at end of file
bcaa799cb01b95769c46fa0c61368e01a32d0e1c
\ No newline at end of file
......@@ -13,6 +13,7 @@ js_type_check("closure_compile") {
":credit_card_edit_dialog",
":credit_card_list_entry",
":password_check",
":password_check_list_item",
":password_edit_dialog",
":password_list_item",
":password_manager_proxy",
......@@ -89,6 +90,17 @@ js_library("credit_card_list_entry") {
}
js_library("password_check") {
deps = [
":password_manager_proxy",
"//ui/webui/resources/js:i18n_behavior",
]
}
js_library("password_check_list_item") {
deps = [
":password_manager_proxy",
"//ui/webui/resources/js:i18n_behavior",
]
}
js_library("password_list_item") {
......@@ -305,6 +317,7 @@ group("polymer3_elements") {
":credit_card_edit_dialog_module",
":credit_card_list_entry_module",
":modulize",
":password_check_list_item_module",
":password_check_module",
":password_edit_dialog_module",
":password_list_item_module",
......@@ -353,6 +366,12 @@ polymer_modulizer("password_check") {
html_type = "dom-module"
}
polymer_modulizer("password_check_list_item") {
js_file = "password_check_list_item.js"
html_file = "password_check_list_item.html"
html_type = "dom-module"
}
polymer_modulizer("password_edit_dialog") {
js_file = "password_edit_dialog.js"
html_file = "password_edit_dialog.html"
......
......@@ -6,6 +6,7 @@
<link rel="import" href="chrome://resources/cr_elements/icons.html">
<link rel="import" href="chrome://resources/polymer/v1_0/iron-icon/iron-icon.html">
<link rel="import" href="chrome://resources/html/i18n_behavior.html">
<link rel="import" href="password_check_list_item.html">
<dom-module id="settings-password-check">
<template>
......@@ -22,8 +23,9 @@
<div class="start settings-box-text">
<div>
$i18n{checkedPasswords}
<span class="secondary inline" id="lastCompletedCheck">
&bull; [[getLastCompletedCheck_(passwordLeakCount_)]]
<span class="secondary inline" id="lastCompletedCheck"
hidden$="[[!lastCompletedCheck_]]">
&bull; [[lastCompletedCheck_]]
</span>
</div>
<div class="secondary" id="passwordLeakCount"
......@@ -36,7 +38,25 @@
$i18n{checkPasswordsAgain}
</cr-button>
</div>
<div class="settings-box"></div>
<div id="passwordCheckBody"
hidden$="[[!hasLeakedCredentials_(leakedPasswords)]]">
<div class="settings-box">
<h2 class="start">$i18n{compromisedPasswords}</h2>
</div>
<div class="list-frame vertical-list">
<div class="list-item secondary">
$i18n{compromisedPasswordsDescription}
</div>
</div>
<div class="list-frame first">
<iron-list id="leakedPasswordList" items="[[leakedPasswords]]">
<template>
<password-check-list-item item="[[item]]">
</password-check-list-item>
</template>
</iron-list>
</div>
</div>
</template>
<script src="password_check.js"></script>
</dom-module>
......@@ -15,9 +15,24 @@ Polymer({
},
/** @private */
lastCompletedCheck_: Date,
lastCompletedCheck_: String,
/**
* An array of leaked passwords to display.
* @type {!Array<!PasswordManagerProxy.CompromisedCredential>}
*/
leakedPasswords: {
type: Array,
value: () => [],
},
},
/**
* @type {?function(!PasswordManagerProxy.CompromisedCredentialsInfo):void}
* @private
*/
leakedCredentialsListener_: null,
/**
* @private {PasswordManagerProxy}
*/
......@@ -25,11 +40,31 @@ Polymer({
/** @override */
attached() {
// It's just a placeholder at the moment.
this.passwordLeakCount_ = 5;
// Set the manager. These can be overridden by tests.
this.passwordManager_ = PasswordManagerImpl.getInstance();
const setLeakedCredentialsListener = info => {
this.leakedPasswords = info.compromisedCredentials;
this.passwordLeakCount_ = info.compromisedCredentials.length;
this.lastCompletedCheck_ = info.elapsedTimeSinceLastCheck;
};
this.leakedCredentialsListener_ = setLeakedCredentialsListener;
// Request initial data.
this.passwordManager_.getCompromisedCredentialsInfo().then(
this.leakedCredentialsListener_);
// Listen for changes.
this.passwordManager_.addCompromisedCredentialsListener(
this.leakedCredentialsListener_);
},
/** @override */
detached() {
this.passwordManager_.removeCompromisedCredentialsListener(
assert(this.leakedCredentialsListener_));
this.leakedCredentialsListener_ = null;
},
/**
......@@ -51,12 +86,12 @@ Polymer({
},
/**
* @return {string}
* Returns true if there are any compromised credentials.
* @param {!Array<!PasswordManagerProxy.CompromisedCredential>} list
* @return {boolean}
* @private
*/
getLastCompletedCheck_() {
// TODO(https://crbug.com/1047726): use lastCompletedCheck_ to return proper
// passed time from the last password check.
return '5 min ago';
hasLeakedCredentials_(list) {
return list && !!list.length;
},
});
<link rel="import" href="chrome://resources/html/polymer.html">
<link rel="import" href="chrome://resources/cr_elements/cr_icon_button/cr_icon_button.html">
<link rel="import" href="chrome://resources/cr_elements/cr_icons_css.html">
<link rel="import" href="../settings_shared_css.html">
<link rel="import" href="../site_favicon.html">
<link rel="import" href="passwords_shared_css.html">
<link rel="import" href="show_password_behavior.html">
<link rel="import" href="password_manager_proxy.html">
<dom-module id="password-check-list-item">
<template>
<style include="settings-shared passwords-shared">
.icon-column {
align-items: center;
height: 16px;
margin-inline-end: 16px;
}
#change-password-link-icon {
height: 16px;
margin-inline-start: 10px;
width: 16px;
--iron-icon-fill-color: var(--text-color-action);
}
#icon-more-vert {
height: 32px;
width: 32px;
}
#leakedPassword {
background-color: transparent;
border: none;
color: inherit;
}
#leaked-item {
margin-bottom: 12px;
margin-top: 12px;
}
#leaked-info {
flex: 1;
}
</style>
<div class="list-item" id="leaked-item">
<div class="icon-column">
<site-favicon url="[[item.changePasswordUrl]]"></site-favicon>
</div>
<div class="info-column two-line" id="leaked-info">
<div class="start text">
<div id="leakOrigin">[[item.formattedOrigin]]</div>
<div>
<span class="secondary" id="leakUsername">[[item.username]]</span>
<input id="leakedPassword" type="password" value="[[password_]]"
readonly disabled>
</div>
<div class="secondary"
id="leakType">[[getCompromiseType_(item)]]
</div>
<div class="secondary"
id="elapsedTime">[[item.elapsedTimeSinceCompromise]]
</div>
</div>
</div>
<div class="button-container">
<!-- TODO:(https://crbug.com/1047726) add 'Already changed this password?' link -->
<cr-button class="action-button" on-click="onChangePasswordClick_">
$i18n{changePasswordButton}
<iron-icon icon="cr:open-in-new" id="change-password-link-icon">
</iron-icon>
</cr-button>
</div>
<cr-icon-button class="icon-more-vert"
title="$i18n{moreActions}"></cr-icon-button>
</div>
</template>
<script src="password_check_list_item.js"></script>
</dom-module>
// 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.
/**
* @fileoverview PasswordCheckListItem represents one leaked credential in the
* list of compromised passwords.
*/
Polymer({
is: 'password-check-list-item',
behaviors: [I18nBehavior],
properties: {
password_: {
type: String,
value: ' '.repeat(10),
},
/**
* The password that is being displayed.
* @type {!PasswordManagerProxy.CompromisedCredential}
*/
item: Object,
},
/**
* @param {!PasswordManagerProxy.CompromisedCredential} item
* @return {string}
* @private
*/
getCompromiseType_(item) {
return loadTimeData.getString(
item.compromiseType == chrome.passwordsPrivate.CompromiseType.LEAKED ?
'leakedPassword' :
'phishedPassword');
},
/**
* @private
*/
onChangePasswordClick_() {
const url = this.item.changePasswordUrl;
if(url) {
settings.OpenWindowProxyImpl.getInstance().openURL(url);
}
},
});
......@@ -153,6 +153,26 @@ class PasswordManagerProxy {
* Requests the start of the bulk password check.
*/
startBulkPasswordCheck() {}
/**
* Requests the latest information about compromised credentials.
* @return {!Promise<(PasswordManagerProxy.CompromisedCredentialsInfo)>}
*/
getCompromisedCredentialsInfo() {}
/**
* Add an observer to the compromised passwords change.
* @param {function(!PasswordManagerProxy.CompromisedCredentialsInfo):void}
* listener
*/
addCompromisedCredentialsListener(listener) {}
/**
* Remove an observer to the compromised passwords change.
* @param {function(!PasswordManagerProxy.CompromisedCredentialsInfo):void}
* listener
*/
removeCompromisedCredentialsListener(listener) {}
}
/** @typedef {chrome.passwordsPrivate.PasswordUiEntry} */
......@@ -172,6 +192,12 @@ PasswordManagerProxy.PasswordExportProgress;
/** @typedef {chrome.passwordsPrivate.ExportProgressStatus} */
PasswordManagerProxy.ExportProgressStatus;
/** @typedef {chrome.passwordsPrivate.CompromisedCredential} */
PasswordManagerProxy.CompromisedCredential;
/** @typedef {chrome.passwordsPrivate.CompromisedCredentialsInfo} */
PasswordManagerProxy.CompromisedCredentialsInfo;
/**
* Implementation that accesses the private API.
* @implements {PasswordManagerProxy}
......@@ -297,6 +323,25 @@ class PasswordManagerImpl {
/** @override */
startBulkPasswordCheck() {}
/** @override */
getCompromisedCredentialsInfo() {
return new Promise(resolve => {
chrome.passwordsPrivate.getCompromisedCredentialsInfo(resolve);
});
}
/** @override */
addCompromisedCredentialsListener(listener) {
chrome.passwordsPrivate.onCompromisedCredentialsInfoChanged.addListener(
listener);
}
/** @override */
removeCompromisedCredentialsListener(listener) {
chrome.passwordsPrivate.onCompromisedCredentialsInfoChanged.removeListener(
listener);
}
}
cr.addSingletonGetter(PasswordManagerImpl);
......@@ -636,6 +636,12 @@
<structure name="IDR_SETTINGS_PASSWORD_CHECK_JS"
file="autofill_page/password_check.js"
type="chrome_html" />
<structure name="IDR_SETTINGS_PASSWORD_CHECK_LIST_ITEM_HTML"
file="autofill_page/password_check_list_item.html"
type="chrome_html" />
<structure name="IDR_SETTINGS_PASSWORD_CHECK_LIST_ITEM_JS"
file="autofill_page/password_check_list_item.js"
type="chrome_html" />
<structure name="IDR_SETTINGS_PASSWORD_LIST_ITEM_HTML"
file="autofill_page/password_list_item.html"
type="chrome_html"
......
......@@ -733,6 +733,12 @@ void AddAutofillStrings(content::WebUIDataSource* html_source,
{"checkPasswordsDescription", IDS_SETTINGS_CHECK_PASSWORDS_DESCRIPTION},
{"checkPasswordLeakCount", IDS_SETTINGS_LEAKED_PASSWORDS_COUNT},
{"checkPasswordsAgain", IDS_SETTINGS_CHECK_PASSWORDS_AGAIN},
{"compromisedPasswords", IDS_SETTINGS_COMPROMISED_PASSWORDS},
{"compromisedPasswordsDescription",
IDS_SETTINGS_COMPROMISED_PASSWORDS_ADVICE},
{"changePasswordButton", IDS_SETTINGS_CHANGE_PASSWORD_BUTTON},
{"leakedPassword", IDS_SETTINGS_COMPROMISED_PASSWORD_REASON_LEAKED},
{"phishedPassword", IDS_SETTINGS_COMPROMISED_PASSWORD_REASON_PHISHED},
{"creditCards", IDS_AUTOFILL_PAYMENT_METHODS},
{"noPaymentMethodsFound", IDS_SETTINGS_PAYMENT_METHODS_NONE},
{"googlePayments", IDS_SETTINGS_GOOGLE_PAYMENTS},
......
......@@ -14,6 +14,33 @@ cr.define('settings_passwords_check', function() {
return passwordsSection;
}
/**
* Helper method that validates a that elements in the compromised credentials
* list match the expected data.
* @param {!Element} listElements The iron-list element that will be checked.
* @param {!Array<!chrome.passwordsPrivate.CompromisedCredential>}
* passwordList The expected data.
* @private
*/
function validateLeakedPasswordsList(listElements, compromisedCredentials) {
assertEquals(listElements.items.length, compromisedCredentials.length);
for (let index = 0; index < compromisedCredentials.length; ++index) {
// The first child is a template, skip and get the real 'first child'.
const node = Polymer.dom(listElements).children[index + 1];
assert(node);
assertEquals(
node.$.elapsedTime.textContent.trim(),
compromisedCredentials[index].elapsedTimeSinceCompromise);
assertEquals(
node.$.leakUsername.textContent.trim(),
compromisedCredentials[index].username);
assertEquals(
node.$.leakOrigin.textContent.trim(),
compromisedCredentials[index].formattedOrigin);
}
}
suite('PasswordsCheckSection', function() {
/** @type {TestPasswordManagerProxy} */
let passwordManager = null;
......@@ -32,9 +59,38 @@ cr.define('settings_passwords_check', function() {
// Test verifies that clicking 'Check again' make proper function call to
// password manager
test('testCheckAgainButton', function() {
const checkSection = createCheckPasswordSection();
checkSection.$.controlPasswordCheckButton.click();
const checkPasswordSection = createCheckPasswordSection();
checkPasswordSection.$.controlPasswordCheckButton.click();
return passwordManager.whenCalled('startBulkPasswordCheck');
});
// Test verifies that if no compromised credentials found than list is not
// shown TODO(https://crbug.com/1047726): add additional checks after
// UI is implemented
test('testNoCompromisedCredentials', function() {
const checkPasswordSection = createCheckPasswordSection();
assertTrue(checkPasswordSection.$.passwordCheckBody.hidden);
validateLeakedPasswordsList(
checkPasswordSection.$.leakedPasswordList, []);
});
// Test verifies that compromised credentials are displayed in a proper way
test('testSomeCompromisedCredentials', function() {
const leakedPasswords = [
FakeDataMaker.makeCompromisedCredentials('one.com', 'test4', 'LEAKED'),
FakeDataMaker.makeCompromisedCredentials('two.com', 'test3', 'PHISHED'),
];
const leakedPasswordsInfo = FakeDataMaker.makeCompromisedCredentialsInfo(
leakedPasswords, '5 min ago');
passwordManager.data.leakedCredentials = leakedPasswordsInfo;
const checkPasswordSection = createCheckPasswordSection();
return passwordManager.whenCalled('getCompromisedCredentialsInfo')
.then(() => {
Polymer.dom.flush();
assertFalse(checkPasswordSection.$.passwordCheckBody.hidden);
validateLeakedPasswordsList(
checkPasswordSection.$.leakedPasswordList, leakedPasswords);
});
});
});
});
......@@ -140,6 +140,38 @@ FakeDataMaker.patternMaker_ = function(pattern, base) {
});
};
/**
* Creates a new compromised credential.
* @param {string=} url
* @param {string=} username
* @param {string=} type
* @return {chrome.passwordsPrivate.CompromisedCredential}
* @private
*/
FakeDataMaker.makeCompromisedCredentials = function(url, username, type) {
return {
formattedOrigin: url,
changePasswordUrl: 'http://${url}/',
username: username,
elapsedTimeSinceCompromise:
(Math.floor(Math.random() * 60)).toString() + ' min ago',
compromiseType: type,
};
};
/**
* Creates a new compromised credential info.
* @param {!Array<!chrome.passwordsPrivate.CompromisedCredential>} list
* @param {string=} lastCheck
* @return {chrome.passwordsPrivate.CompromisedCredentialsInfo}
* @private
*/
FakeDataMaker.makeCompromisedCredentialsInfo = function(list, lastCheck) {
return {
compromisedCredentials: list,
elapsedTimeSinceLastCheck: lastCheck,
};
};
/**
* Helper class for creating password-section sub-element from fake data and
......
......@@ -11,7 +11,11 @@
*/
class TestPasswordManagerProxy extends TestBrowserProxy {
constructor() {
super(['requestPlaintextPassword', 'startBulkPasswordCheck']);
super([
'requestPlaintextPassword',
'startBulkPasswordCheck',
'getCompromisedCredentialsInfo',
]);
this.actual_ = new PasswordManagerExpectations();
......@@ -19,6 +23,7 @@ class TestPasswordManagerProxy extends TestBrowserProxy {
this.data = {
passwords: [],
exceptions: [],
leakedCredentials: FakeDataMaker.makeCompromisedCredentialsInfo([], ''),
};
// Holds the last callbacks so they can be called when needed/
......@@ -143,4 +148,16 @@ class TestPasswordManagerProxy extends TestBrowserProxy {
startBulkPasswordCheck() {
this.methodCalled('startBulkPasswordCheck');
}
/** @override */
getCompromisedCredentialsInfo() {
this.methodCalled('getCompromisedCredentialsInfo');
return Promise.resolve(this.data.leakedCredentials);
}
/** @override */
addCompromisedCredentialsListener(listener) {}
/** @override */
removeCompromisedCredentialsListener(listener) {}
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment