Commit 5e3e7faf authored by Yiming Zhou's avatar Yiming Zhou Committed by Commit Bot

Record a user's Chrome autofill profile and Passwords in the Recorder.

Chrome extensions do not have direct access to a user's Chrome profile information, information such as saved passwords and autofill profiles.
However, the Action Recorder Extension may derive a user's saved passwords and autofill profile by observing the password fields, shipping info fields and payment fields that Chrome autofills.

To land this change, I also had to change how the extension detects the Chrome Autofill action. Prior to this change, the extension detects Chrome Autofill using a combination of keydown and onchange event listeners. This strategy can distinguish user typing actions from Chrome autofill actions, but this stategy does not distinguish Chrome autofill actions from Page JavaScript changing input values. In this change, I switched the extension to using the method outlined in https://medium.com/@brunn/detecting-autofilled-fields-in-javascript-aed598d25da7 to detect Chrome autofill.

Bug: 855284
Change-Id: I22283d5684237084ca2a8b95d9f4723be46b7746
Reviewed-on: https://chromium-review.googlesource.com/1208446
Commit-Queue: Yiming Zhou <uwyiming@google.com>
Reviewed-by: default avatarJared Saul <jsaul@google.com>
Cr-Commit-Position: refs/heads/master@{#589653}
parent 85683b62
......@@ -57,6 +57,10 @@
{ keyPath: 'action_index', autoIncrement: true });
db.createObjectStore(Indexed_DB_Vars.SAVED_ACTION_PARAMS,
{ autoIncrement: true });
db.createObjectStore(Indexed_DB_Vars.AUTOFILL_PROFILE,
{ keyPath: 'type' });
db.createObjectStore(Indexed_DB_Vars.PASSWORD_MANAGER_PROFILE,
{ keyPath: ['website', 'username'] });
event.target.transaction.oncomplete = (event) => {
resolve(db);
};
......@@ -85,10 +89,13 @@
async function performTransactionOnRecipeIndexedDB(transactionToPerform) {
const db = await openRecipeIndexedDB();
return await new Promise((resolve, reject) => {
const transaction = db.transaction([Indexed_DB_Vars.ATTRIBUTES,
Indexed_DB_Vars.ACTIONS,
Indexed_DB_Vars.SAVED_ACTION_PARAMS],
'readwrite');
const transaction = db.transaction([
Indexed_DB_Vars.ATTRIBUTES,
Indexed_DB_Vars.ACTIONS,
Indexed_DB_Vars.SAVED_ACTION_PARAMS,
Indexed_DB_Vars.AUTOFILL_PROFILE,
Indexed_DB_Vars.PASSWORD_MANAGER_PROFILE],
'readwrite');
transaction.oncomplete = (event) => {
resolve(event);
};
......@@ -115,7 +122,7 @@
});
}
async function addActionToRecipe(action, tabId) {
async function addActionToRecipe(action, tabId, skipUpdatingUi) {
const db = await openRecipeIndexedDB();
const key = await new Promise((resolve, reject) => {
const transaction = db.transaction([Indexed_DB_Vars.ACTIONS],
......@@ -145,13 +152,15 @@
db.close();
});
// Update the recording UI with the new action.
const frameId = await getRecorderUiFrameId();
action.action_index = key;
await sendMessageToTab(
tabId,
{ type: RecorderUiMsgEnum.ADD_ACTION, action: action},
{ frameId: frameId });
if (!skipUpdatingUi) {
// Update the recording UI with the new action.
const frameId = await getRecorderUiFrameId();
action.action_index = key;
await sendMessageToTab(
tabId,
{ type: RecorderUiMsgEnum.ADD_ACTION, action: action},
{ frameId: frameId });
}
}
function removeActionFromRecipe(index) {
......@@ -160,13 +169,32 @@
});
}
function insertChromeAutofillProfileEntry(entry) {
return performTransactionOnRecipeIndexedDB((transaction) => {
const autofillProfileStore =
transaction.objectStore(Indexed_DB_Vars.AUTOFILL_PROFILE);
autofillProfileStore.add(entry);
});
}
function insertChromePasswordManagerProfileEntry(entry) {
return performTransactionOnRecipeIndexedDB((transaction) => {
const passwordManagerProfileStore =
transaction.objectStore(Indexed_DB_Vars.PASSWORD_MANAGER_PROFILE);
passwordManagerProfileStore.add(entry);
});
}
async function getRecipe() {
const db = await openRecipeIndexedDB();
let recipe = {};
return await new Promise((resolve, reject) => {
const transaction = db.transaction([Indexed_DB_Vars.ATTRIBUTES,
Indexed_DB_Vars.ACTIONS],
'readonly');
const transaction = db.transaction([
Indexed_DB_Vars.ATTRIBUTES,
Indexed_DB_Vars.ACTIONS,
Indexed_DB_Vars.AUTOFILL_PROFILE,
Indexed_DB_Vars.PASSWORD_MANAGER_PROFILE],
'readonly');
transaction.oncomplete = (event) => {
resolve(recipe);
};
......@@ -182,6 +210,30 @@
recipe.startingURL = urlReq.result;
};
const autofillProfileStore =
transaction.objectStore(Indexed_DB_Vars.AUTOFILL_PROFILE);
const autofillProfileReq = autofillProfileStore.getAll();
autofillProfileReq.onsuccess = (event) => {
recipe.autofillProfile =
autofillProfileReq.result ? autofillProfileReq.result : [];
};
const passwordManagerProfileStore =
transaction.objectStore(Indexed_DB_Vars.PASSWORD_MANAGER_PROFILE);
const passwordManagerProfileReq = passwordManagerProfileStore.getAll();
passwordManagerProfileReq.onsuccess = (event) => {
recipe.passwordManagerProfiles = [];
// Filter out passwords submitted by the user during the course of
// recording.
if (passwordManagerProfileReq.result) {
for (const entry of passwordManagerProfileReq.result) {
if (entry.submittedByUser === undefined) {
recipe.passwordManagerProfiles.push(entry);
}
}
}
};
const actionsStore = transaction.objectStore(Indexed_DB_Vars.ACTIONS);
const actionsReq = actionsStore.getAll();
actionsReq.onsuccess = (event) => {
......@@ -223,13 +275,14 @@
});
}
function savePasswordEventParams(userName, password) {
function savePasswordEventParams(username, password, website) {
return performTransactionOnRecipeIndexedDB((transaction) => {
const attributeStore =
transaction.objectStore(Indexed_DB_Vars.SAVED_ACTION_PARAMS);
attributeStore.put({
userName: userName,
password: password
username: username,
password: password,
website: website
}, Indexed_DB_Vars.PASSWORD_MANAGER_PARAMS);
});
}
......@@ -538,23 +591,47 @@
}
async function setPasswordEventParams(params, mainFrameIsReady) {
await savePasswordEventParams(params.userName, params.password);
// Add an empty entry to the Password Manager Profile Table.
//
// The Password Manager Profile Table stores the saved passwords a user
// has at the start of recording.
//
// An empty entry is an entry consisting of an origin and a user name,
// but not a password.
//
// The background script adds Password Manager Profile table entries by
// insertion - in other words, the background script cannot override an
// existing entry. The background script creates an empty entry to denote
// that at the start of recording, Chrome did not have a saved password for
// the specified username on the specified origin. If a user saves a new
// password during the course of recording, the empty entry prevents the
// background from erroneously recording the new entry as present at the
// start of recording.
await insertChromePasswordManagerProfileEntry({
submittedByUser: true,
username: params.username,
website: params.website
});
await savePasswordEventParams(params.username, params.password,
params.website);
if (!mainFrameIsReady) {
return true;
}
return await sendPasswordEventParamsToUi(params.userName, params.password);
return await sendPasswordEventParamsToUi(params.username, params.password);
}
async function sendPasswordEventParamsToUi(userName, password) {
async function sendPasswordEventParamsToUi(username, password, website) {
const tabId = await getRecordingTabId();
const frameId = await getRecorderUiFrameId();
const response = await sendMessageToTab(
tabId,
{ type: RecorderUiMsgEnum.SET_PASSWORD_MANAGER_ACTION_PARAMS,
userName: userName,
password: password },
username: username,
password: password,
website: website },
{ frameId: frameId });
return response;
}
......@@ -669,7 +746,7 @@
url: details.url,
context: { 'isIframe': false },
type: ActionTypeEnum.LOAD_PAGE
});
}, tabId, true);
const state = await getRecordingState();
if (state === RecorderStateEnum.HIDDEN) {
......@@ -759,6 +836,14 @@
// message originates from the main frame.
sender.frameId != 0);
return false;
case RecorderMsgEnum.SET_AUTOFILL_PROFILE_ENTRY:
insertChromeAutofillProfileEntry(request.entry);
sendResponse(true);
return false;
case RecorderMsgEnum.SET_PASSWORD_MANAGER_PROFILE_ENTRY:
insertChromePasswordManagerProfileEntry(request.entry);
sendResponse(true);
return false;
default:
}
return false;
......
......@@ -53,7 +53,9 @@ const RecorderMsgEnum = {
CANCEL: 'cancel-recording',
GET_IFRAME_NAME: 'get-iframe-name',
ADD_ACTION: 'record-action',
SET_PASSWORD_MANAGER_ACTION_PARAMS: 'set-password-manager-action-params'
SET_PASSWORD_MANAGER_ACTION_PARAMS: 'set-password-manager-action-params',
SET_AUTOFILL_PROFILE_ENTRY: 'set-autofill-profile-entry',
SET_PASSWORD_MANAGER_PROFILE_ENTRY: 'set-password-manager-profile-entry'
};
const Local_Storage_Vars = {
......@@ -74,4 +76,8 @@ const Indexed_DB_Vars = {
// actions.
SAVED_ACTION_PARAMS: 'Saved_Action_Params',
PASSWORD_MANAGER_PARAMS: 'password_manager_params',
// The 'Profile' tables stores the user's Chrome autofill profile and Chrome
// password manager profile.
AUTOFILL_PROFILE: 'autofill_profile',
PASSWORD_MANAGER_PROFILE: 'password_manager_profile'
};
......@@ -127,9 +127,14 @@ document.addEventListener('DOMContentLoaded', () => {
break;
case ActionTypeEnum.VALIDATE_FIELD:
actionLabel.textContent = 'validate field';
actionDetailLabel.textContent = `check that field
(${action.expectedAutofillType}) has the value
'${action.expectedValue}'`;
if (action.expectedAutofillType) {
actionDetailLabel.textContent = `check that field
(${action.expectedAutofillType}) has the value
'${action.expectedValue}'`;
} else {
actionDetailLabel.textContent = `check that field
has the value '${action.expectedValue}'`;
}
break;
case ActionTypeEnum.SAVE_PASSWORD:
actionLabel.textContent = 'save password';
......
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