diff --git a/app/autofill.js b/app/autofill.js index c38f6c2a1ef..0d54cbf967e 100644 --- a/app/autofill.js +++ b/app/autofill.js @@ -52,3 +52,19 @@ module.exports.clearAutocompleteData = () => { module.exports.clearAutofillData = () => { session.defaultSession.autofill.clearAutofillData() } + +module.exports.addLogin = (form) => { + session.defaultSession.autofill.addLogin(form) +} + +module.exports.updateLogin = (form) => { + session.defaultSession.autofill.updateLogin(form) +} + +module.exports.removeLogin = (form) => { + session.defaultSession.autofill.removeLogin(form) +} + +module.exports.clearLogins = (form) => { + session.defaultSession.autofill.clearLogins(form) +} diff --git a/app/browser/reducers/passwordManagerReducer.js b/app/browser/reducers/passwordManagerReducer.js index eebebad9ddd..cbc189570e2 100644 --- a/app/browser/reducers/passwordManagerReducer.js +++ b/app/browser/reducers/passwordManagerReducer.js @@ -7,13 +7,14 @@ const keytar = require('keytar') const appConstants = require('../../../js/constants/appConstants') const appActions = require('../../../js/actions/appActions') -const AppStore = require('../../../js/stores/appStore') const CryptoUtil = require('../../../js/lib/cryptoUtil') const locale = require('../../locale') const messages = require('../../../js/constants/messages') -const siteSettings = require('../../../js/state/siteSettings') +const {getWebContents} = require('../webContentsCache') const {makeImmutable} = require('../../common/state/immutableUtil') -const {BrowserWindow, ipcMain} = require('electron') +const Immutable = require('immutable') +const {ipcMain} = require('electron') +const autofill = require('../../autofill') const unsafeTestMasterKey = 'c66af15fc6555ebecf7cee3a5b82c108fd3cb4b587ab0b299d28e39c79ecc708' @@ -86,171 +87,134 @@ const init = () => { passwordCallbacks[message](buttonIndex) } }) +} - ipcMain.on(messages.DECRYPT_PASSWORD, (e, encrypted, authTag, iv, id) => { - masterKey = masterKey || getMasterKey() - if (!masterKey) { - console.log('Could not access master password; aborting') - return - } - let decrypted = CryptoUtil.decryptVerify(encrypted, authTag, masterKey, iv) - e.sender.send(messages.DECRYPTED_PASSWORD, { - id, - decrypted +const savePassword = (username, origin, tabId) => { + if (!origin) { + return + } + + var message = username + ? locale.translation('notificationPasswordWithUserName').replace(/{{\s*username\s*}}/, username).replace(/{{\s*origin\s*}}/, origin) + : locale.translation('notificationPassword').replace(/{{\s*origin\s*}}/, origin) + + if (!(message in passwordCallbacks)) { + // Notification not shown already + appActions.showNotification({ + buttons: [ + {text: locale.translation('yes')}, + {text: locale.translation('no')}, + {text: locale.translation('neverForThisSite')} + ], + options: { + persist: false, + advancedText: locale.translation('notificationPasswordSettings'), + advancedLink: 'about:passwords' + }, + message }) - }) + } - ipcMain.on(messages.GET_PASSWORDS, (e, origin, action) => { - const passwords = AppStore.getState().get('passwords') - if (!passwords || passwords.size === 0) { - return - } + const webContents = getWebContents(tabId) - let results = passwords.filter((password) => { - return password.get('origin') === origin && password.get('action') === action - }) + passwordCallbacks[message] = (buttonIndex) => { + delete passwordCallbacks[message] + appActions.hideNotification(message) - if (results.size === 0) { + if (buttonIndex === 1) { + // don't save return } - - masterKey = masterKey || getMasterKey() - if (!masterKey) { - console.log('Could not access master password; aborting') + if (buttonIndex === 2) { + // never save + webContents.neverSavePassword() return } - let isUnique = results.size === 1 - results.forEach((result) => { - let password = CryptoUtil.decryptVerify(result.get('encryptedPassword'), - result.get('authTag'), - masterKey, - result.get('iv')) - e.sender.send(messages.GOT_PASSWORD, result.get('username'), - password, origin, action, isUnique) - }) - }) + // save password + webContents.savePassword() + } +} - ipcMain.on(messages.SHOW_USERNAME_LIST, (e, origin, action, boundingRect, value) => { - const passwords = AppStore.getState().get('passwords') - if (!passwords || passwords.size === 0) { - return - } +const updatePassword = (username, origin, tabId) => { + if (!origin) { + return + } - let usernames = {} - let results = passwords.filter((password) => { - return password.get('username') && - password.get('username').startsWith(value) && - password.get('origin') === origin && - password.get('action') === action + var message = username + ? locale.translation('notificationUpdatePasswordWithUserName').replace(/{{\s*username\s*}}/, username).replace(/{{\s*origin\s*}}/, origin) + : locale.translation('notificationUpdatePassword').replace(/{{\s*origin\s*}}/, origin) + + if (!(message in passwordCallbacks)) { + // Notification not shown already + appActions.showNotification({ + buttons: [ + {text: locale.translation('yes')}, + {text: locale.translation('no')} + ], + options: { + persist: false, + advancedText: locale.translation('notificationPasswordSettings'), + advancedLink: 'about:passwords' + }, + message }) + } - if (results.size === 0) { - if (BrowserWindow.getFocusedWindow()) { - BrowserWindow.getFocusedWindow().webContents.send(messages.HIDE_CONTEXT_MENU) - } - return - } - - masterKey = masterKey || getMasterKey() - if (!masterKey) { - console.log('Could not access master password; aborting') - return - } + const webContents = getWebContents(tabId) - results.forEach((result) => { - usernames[result.get('username')] = CryptoUtil.decryptVerify(result.get('encryptedPassword'), - result.get('authTag'), - masterKey, - result.get('iv')) || '' - }) - let win = BrowserWindow.getFocusedWindow() - if (!win) { - return - } - if (Object.keys(usernames).length > 0) { - win.webContents.send(messages.SHOW_USERNAME_LIST, - usernames, origin, action, - boundingRect) - } else { - win.webContents.send(messages.HIDE_CONTEXT_MENU) - } - }) + passwordCallbacks[message] = (buttonIndex) => { + delete passwordCallbacks[message] + appActions.hideNotification(message) - ipcMain.on(messages.SAVE_PASSWORD, (e, username, password, origin, action) => { - if (!password || !origin || !action) { - return - } - const originSettings = siteSettings.getSiteSettingsForURL(AppStore.getState().get('siteSettings'), origin) - if (originSettings && originSettings.get('savePasswords') === false) { + if (buttonIndex === 0) { + webContents.updatePassword() return } + // never save + webContents.noUpdatePassword() + } +} +const migrate = (state) => { + const passwords = state.get('passwords') + if (passwords.size) { masterKey = masterKey || getMasterKey() if (!masterKey) { console.log('Could not access master password; aborting') return } - - const passwords = AppStore.getState().get('passwords') - - // If the same password already exists, don't offer to save it - let result = passwords.findLast((pw) => { - return pw.get('origin') === origin && pw.get('action') === action && (username ? pw.get('username') === username : !pw.get('username')) - }) - if (result && password === CryptoUtil.decryptVerify(result.get('encryptedPassword'), - result.get('authTag'), - masterKey, - result.get('iv'))) { - return - } - - var message = username - ? locale.translation('notificationPasswordWithUserName').replace(/{{\s*username\s*}}/, username).replace(/{{\s*origin\s*}}/, origin) - : locale.translation('notificationPassword').replace(/{{\s*origin\s*}}/, origin) - - if (!(message in passwordCallbacks)) { - // Notification not shown already - appActions.showNotification({ - buttons: [ - {text: locale.translation('yes')}, - {text: locale.translation('no')}, - {text: locale.translation('neverForThisSite')} - ], - options: { - persist: false, - advancedText: locale.translation('notificationPasswordSettings'), - advancedLink: 'about:passwords' - }, - message - }) - } - - passwordCallbacks[message] = (buttonIndex) => { - delete passwordCallbacks[message] - appActions.hideNotification(message) - - if (buttonIndex === 1) { - return - } - if (buttonIndex === 2) { - // Never save the password on this site - appActions.changeSiteSetting(origin, 'savePasswords', false) - return + passwords.forEach((password) => { + let decrypted = CryptoUtil.decryptVerify(password.get('encryptedPassword'), + password.get('authTag'), + masterKey, + password.get('iv')) + if (decrypted) { + let form = {} + form['origin'] = password.get('origin') + form['signon_realm'] = password.get('origin') + '/' + form['action'] = password.get('action') + form['username'] = password.get('username') + form['password'] = decrypted + autofill.addLogin(form) } - - // Save the password - const encrypted = CryptoUtil.encryptAuthenticate(password, masterKey) - appActions.savePassword({ - origin, - action, - username: username || '', - encryptedPassword: encrypted.content, - authTag: encrypted.tag, - iv: encrypted.iv - }) - } - }) + }) + state = state.set('legacyPasswords', state.get('passwords')) + state = state.set('passwords', new Immutable.List()) + } + const allSiteSettings = state.get('siteSettings') + const blackedList = allSiteSettings.filter((setting) => setting.get('savePasswords') === false) + if (blackedList.size) { + blackedList.forEach((entry, index) => { + let form = {} + form['origin'] = index + form['signon_realm'] = index + '/' + form['blacklisted_by_user'] = true + autofill.addLogin(form) + appActions.deletePasswordSite(index) + }) + } + return state } const passwordManagerReducer = (state, action, immutableAction) => { @@ -258,6 +222,13 @@ const passwordManagerReducer = (state, action, immutableAction) => { switch (action.get('actionType')) { case appConstants.APP_SET_STATE: init() + state = migrate(state) + break + case appConstants.APP_SAVE_PASSWORD: + savePassword(action.get('username'), action.get('origin'), action.get('tabId')) + break + case appConstants.APP_UPDATE_PASSWORD: + updatePassword(action.get('username'), action.get('origin'), action.get('tabId')) break } return state diff --git a/app/browser/tabs.js b/app/browser/tabs.js index ce1a100c102..ae9bfabf312 100644 --- a/app/browser/tabs.js +++ b/app/browser/tabs.js @@ -185,9 +185,13 @@ const updateAboutDetails = (tab, tabValue) => { downloads: downloads.toJS() }) } else if (location === 'about:passwords' && passwords) { - tab.send(messages.PASSWORD_DETAILS_UPDATED, passwords.toJS()) - tab.send(messages.PASSWORD_SITE_DETAILS_UPDATED, - allSiteSettings.filter((setting) => setting.get('savePasswords') === false).toJS()) + const defaultSession = session.defaultSession + defaultSession.autofill.getAutofillableLogins((result) => { + tab.send(messages.PASSWORD_DETAILS_UPDATED, result) + }) + defaultSession.autofill.getBlackedlistLogins((result) => { + tab.send(messages.PASSWORD_SITE_DETAILS_UPDATED, result) + }) } else if (location === 'about:flash') { tab.send(messages.BRAVERY_DEFAULTS_UPDATED, braveryDefaults.toJS()) } else if (location === 'about:newtab') { @@ -353,7 +357,16 @@ const api = { console.log('responsive') }) + tab.on('save-password', (e, username, origin) => { + appActions.savePassword(username, origin, tabId) + }) + + tab.on('update-password', (e, username, origin) => { + appActions.updatePassword(username, origin, tabId) + }) + updateWebContents(tabId, tab) + let tabValue = getTabValue(tabId) if (tabValue) { appActions.tabCreated(tabValue) diff --git a/app/extensions.js b/app/extensions.js index b6f086b48a6..9b660c62ed4 100644 --- a/app/extensions.js +++ b/app/extensions.js @@ -94,7 +94,6 @@ let generateBraveManifest = () => { ], js: [ 'content/scripts/adInsertion.js', - 'content/scripts/passwordManager.js', 'content/scripts/pageInformation.js', 'content/scripts/flashListener.js' ] diff --git a/app/extensions/brave/content/scripts/passwordManager.js b/app/extensions/brave/content/scripts/passwordManager.js deleted file mode 100644 index a33d6db8142..00000000000 --- a/app/extensions/brave/content/scripts/passwordManager.js +++ /dev/null @@ -1,291 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. */ - -if (chrome.contentSettings.passwordManager == 'allow') { - let credentials = {} - function savePassword (username/*: ?string*/, pw/*: string*/, origin/*: string*/, action/*: string*/) { - chrome.ipcRenderer.send('save-password', username, pw, origin, action) - } - - let submittedForms = [] - function onFormSubmit (form/*: HTMLFormElement*/, formOrigin/*: string*/) { - if (submittedForms.includes(form)) { - return - } - var fields = getFormFields(form, true) - var passwordElem = fields[1] - if (!passwordElem || !passwordElem.value) { - return - } - // Re-get action in case it has changed - var action = form.action || document.location.href - var usernameElem = fields[0] || {} - savePassword(usernameElem.value, passwordElem.value, formOrigin, normalizeURL(action)) - submittedForms.push(form) - } - - /** - * Try to autofill a form with credentials, using roughly the heuristic from - * http://mxr.mozilla.org/firefox/source/toolkit/components/passwordmgr/src/nsLoginManager.js - * @param {string} formOrigin - origin of the form - * @param {Element} form - the form node - */ - function tryAutofillForm (formOrigin, form) { - var fields = getFormFields(form, false) - var action = form.action || document.location.href - action = normalizeURL(action) - var usernameElem = fields[0] - var passwordElem = fields[1] - - if (!passwordElem) { - return - } - - if (Array.isArray(credentials[action])) { - let isDuplicate = false - credentials[action].forEach((elems) => { - if (isDuplicate === true) { - return - } else if (elems[0] === passwordElem && elems[1] === usernameElem) { - isDuplicate = true - } - }) - if (isDuplicate === false) { - credentials[action].push([passwordElem, usernameElem]) - } else { - return - } - } else { - credentials[action] = [[passwordElem, usernameElem]] - } - - // Fill the password immediately if there's only one or if the username - // is already autofilled - chrome.ipcRenderer.send('get-passwords', formOrigin, action) - - if (usernameElem) { - usernameElem.addEventListener('keyup', (e) => { - if (!usernameElem || !(e instanceof KeyboardEvent)) { - return - } - switch (e.keyCode) { - case KeyEvent.DOM_VK_ESCAPE: - e.preventDefault() - e.stopPropagation() - chrome.ipcRenderer.sendToHost('hide-context-menu') - break - default: - let rect = usernameElem.getBoundingClientRect() - chrome.ipcRenderer.send('show-username-list', formOrigin, action, { - bottom: rect.bottom, - left: rect.left, - width: rect.width - }, usernameElem.value || '') - } - }) - } - - // Whenever a form is submitted, offer to save it in the password manager - // if the credentials have changed. - form.addEventListener('submit', (e) => { - if (usernameElem) { - usernameElem.blur() - chrome.ipcRenderer.sendToHost('hide-context-menu') - } - onFormSubmit(form, formOrigin) - }) - Array.from(form.querySelectorAll('button')).forEach((button) => { - button.addEventListener('click', (e) => { - onFormSubmit(form, formOrigin) - }) - }) - } - - /** - * Gets protocol + host + path from a URL. - * @return {string} - */ - function normalizeURL (url) { - if (typeof url !== 'string') { - return '' - } - var a = document.createElement('a') - a.href = url - return [a.protocol, a.host].join('//') + a.pathname - } - - /** - * @return {boolean} - */ - function autofillPasswordListener () { - // Don't autofill on non-HTTP(S) sites for now - if (document.location.protocol !== 'http:' && document.location.protocol !== 'https:') { - return - } - - if (document.querySelectorAll('input[type=password]').length === 0) { - // No password fields; - return - } - - // Map of action origin to [[password element, username element]] - var formOrigin = [document.location.protocol, document.location.host].join('//') - var formNodes = document.querySelectorAll('form') - - Array.from(formNodes).forEach((form) => { - tryAutofillForm(formOrigin, form) - }) - - chrome.ipcRenderer.on('got-password', (e, username, password, origin, action, isUnique) => { - var elems = credentials[action] - if (formOrigin === origin && elems) { - elems.forEach((elem) => { - if (isUnique) { - // Autofill password if there is only one available - elem[0].value = password - if (username && elem[1]) { - // Autofill the username if needed - elem[1].value = username - } - } else if (elem[1] && username && username === elem[1].value) { - // If the username is already autofilled by something else, fill - // in the corresponding password - elem[0].value = password - } - }) - } - }) - } - - /** - * Gets form fields. - * @param {Element} form - The form to inspect - * @param {boolean} isSubmission - Whether the form is being submitted - * @return {Array.} - */ - function getFormFields (form/*: HTMLFormElement */, isSubmission/*: boolean*/)/*: Array*/ { - var passwords = getPasswordFields(form, isSubmission) - - // We have no idea what is going on with a form that has 0 or >3 password fields - if (passwords.length === 0 || passwords.length > 3) { - return [null, null, null] - } - - // look for any form field that has username-ish attributes - var username = form.querySelector('input[type=email i]') || - form.querySelector('input[autocomplete=email i]') || - form.querySelector('input[autocomplete=username i]') || - form.querySelector('input[name=email i]') || - form.querySelector('input[name=username i]') || - form.querySelector('input[name=user i]') || - form.querySelector('input[name="session[username_or_email]"]') - - if (!username) { - // Search backwards from first password field to find the username field - let previousSibling = passwords[0].previousSibling - while (previousSibling) { - if ((previousSibling instanceof HTMLElement)) { - if (previousSibling.getAttribute('type') === 'text') { - username = previousSibling - break - } - } - previousSibling = previousSibling.previousSibling - } - } - - // Last resort: find the first text input in the form - username = username || form.querySelector('input[type=text i]') - - // If the username turns out to be a password field, just ignore it so - // we don't show the password in plaintext. - if (username) { - let autocomplete = username.getAttribute('autocomplete') - if (username.getAttribute('type') === 'password' || - (autocomplete && autocomplete.includes('password'))) { - username = null - } - } - - // If not a submission, autofill the first password field and ignore the rest - if (!isSubmission || passwords.length === 1) { - return [username instanceof HTMLInputElement ? username : null, passwords[0], null] - } - - // Otherwise, this is probably a password change form and we need to figure out - // what username/password combo to save. - var oldPassword = null - var newPassword = null - var value1 = passwords[0] ? passwords[0].value : '' - var value2 = passwords[1] ? passwords[1].value : '' - var value3 = passwords[2] ? passwords[2].value : '' - - if (passwords.length === 2) { - if (value1 === value2) { - // Treat as if there were 1 pw field - newPassword = passwords[0] - } else { - oldPassword = passwords[0] - newPassword = passwords[1] - } - } else { - // There is probably a "confirm your password" field for the new - // password, so the new password is the one that is repeated. - if (value1 === value2 && value2 === value3) { - // Treat as if there were 1 pw field - newPassword = passwords[0] - } else if (value1 === value2) { - newPassword = passwords[0] - oldPassword = passwords[2] - } else if (value2 === value3) { - newPassword = passwords[2] - oldPassword = passwords[0] - } else if (value1 === value3) { - // Weird - newPassword = passwords[0] - oldPassword = passwords[1] - } - } - return [username instanceof HTMLInputElement ? username : null, newPassword, oldPassword] - } - - /** - * Gets password fields in a form. - * @param {Element} form - The form to inspect - * @param {boolean} isSubmission - Whether the form is being submitted - * @return {Array.|null} - */ - function getPasswordFields (form, isSubmission) { - var currentPassword = form.querySelector('input[autocomplete=current-password i]') - var newPassword = form.querySelector('input[autocomplete=new-password i]') - if (currentPassword instanceof HTMLInputElement) { - if (!newPassword) { - // This probably isn't a password change form; ex: twitter login - return [currentPassword] - } else if (newPassword instanceof HTMLInputElement){ - return [currentPassword, newPassword] - } - } - var passwordNodes = Array.from(form.querySelectorAll('input[type=password]')) - // Skip nodes that are invisible - passwordNodes = passwordNodes.filter((e) => { - return (e instanceof HTMLInputElement && e.clientHeight > 0 && e.clientWidth > 0) - }) - if (isSubmission) { - // Skip empty fields - passwordNodes = passwordNodes.filter((e) => { return (e instanceof HTMLInputElement && e.value) }) - } - return passwordNodes - } - - autofillPasswordListener() - let interval = setInterval(autofillPasswordListener, 3000) - document.addEventListener('visibilitychange', () => { - clearInterval(interval) - if (document.visibilityState !== 'hidden') { - interval = setInterval(autofillPasswordListener, 3000) - } - }) -} - diff --git a/app/extensions/brave/locales/en-US/app.properties b/app/extensions/brave/locales/en-US/app.properties index 37df438e513..d900ddc0cae 100644 --- a/app/extensions/brave/locales/en-US/app.properties +++ b/app/extensions/brave/locales/en-US/app.properties @@ -192,6 +192,8 @@ dismiss=Dismiss addFunds=Add funds notificationPasswordWithUserName=Would you like Brave to remember the password for {{username}} on {{origin}}? notificationPassword=Would you like Brave to remember your password on {{origin}}? +notificationUpdatePasswordWithUserName=Would you like Brave to update the password for {{username}} on {{origin}}? +notificationUpdatePassword=Would you like Brave to update your password on {{origin}}? notificationPasswordSettings=[Password settings] notificationPaymentDone=Your contribution of {{amount}} {{currency}} has been processed. Thanks for supporting your favorite websites! notificationTryPayments=Are you ready to support the sites you use most? diff --git a/app/locale.js b/app/locale.js index eaa5766bd18..72b31e45c22 100644 --- a/app/locale.js +++ b/app/locale.js @@ -215,6 +215,7 @@ var rendererIdentifiers = function () { 'updateLater', 'updateHello', 'notificationPasswordWithUserName', + 'notificationUpdatePasswordWithUserName', 'notificationPassword', 'notificationPasswordSettings', 'notificationPaymentDone', diff --git a/docs/appActions.md b/docs/appActions.md index 5385513e333..78854700c5c 100644 --- a/docs/appActions.md +++ b/docs/appActions.md @@ -328,32 +328,6 @@ Sets the update status -### savePassword(passwordDetail) - -Saves login credentials - -**Parameters** - -**passwordDetail**: `Object`, login details - - - -### deletePassword(passwordDetail) - -Deletes login credentials - -**Parameters** - -**passwordDetail**: `Object`, login details - - - -### clearPasswords() - -Deletes all saved login credentials - - - ### changeSetting(key, value) Changes an application level setting @@ -1048,6 +1022,28 @@ Notifies autoplay has been blocked +### deletePassword(passwordDetail) + +Deletes login credentials + +**Parameters** + +**passwordDetail**: `Object`, login details + + + +### clearPasswords() + +Deletes all saved login credentials + + + +### deletePasswordSite() + +Delete legacy "never saved password" list + + + * * * diff --git a/docs/state.md b/docs/state.md index 9a69bacb0cb..af2557101af 100644 --- a/docs/state.md +++ b/docs/state.md @@ -145,6 +145,16 @@ AppStore enabled: boolean // enable noscript }, passwords: [{ + // not being used anymore + action: string, // URL of the form action + authTag: string, // AES-GCM authentication data, binary-encoded + encryptedPassword: string, // encrypted by master password, binary-encoded + iv: string, // AES-GCM initialization vector, binary-encoded + origin: string, // origin of the form + username: string + }], + legacyPasswords: [{ + // backup of passwords migration action: string, // URL of the form action authTag: string, // AES-GCM authentication data, binary-encoded encryptedPassword: string, // encrypted by master password, binary-encoded diff --git a/js/about/aboutActions.js b/js/about/aboutActions.js index b4f6aca9e64..ac6d18549d4 100644 --- a/js/about/aboutActions.js +++ b/js/about/aboutActions.js @@ -205,10 +205,6 @@ const aboutActions = { }) }, - decryptPassword: function (encryptedPassword, authTag, iv, id) { - ipc.send(messages.DECRYPT_PASSWORD, encryptedPassword, authTag, iv, id) - }, - setClipboard: function (text) { ipc.send(messages.SET_CLIPBOARD, text) }, @@ -228,11 +224,10 @@ const aboutActions = { }) }, - deletePasswordSite: function (origin) { + deletePasswordSite: function (password) { aboutActions.dispatchAction({ - actionType: appConstants.APP_CHANGE_SITE_SETTING, - hostPattern: origin, - key: 'savePasswords' + actionType: appConstants.APP_REMOVE_PASSWORD_SITE, + passwordDetail: password }) }, diff --git a/js/about/passwords.js b/js/about/passwords.js index 019624645fe..aa7048d5b7c 100644 --- a/js/about/passwords.js +++ b/js/about/passwords.js @@ -63,7 +63,7 @@ class SiteItem extends React.Component { } onDelete () { - aboutActions.deletePasswordSite(this.props.site) + aboutActions.deletePassword(this.props.site.toJS()) } render () { @@ -77,7 +77,7 @@ class SiteItem extends React.Component { style={{backgroundColor: 'inherit'}} /> - {this.props.site} + {this.props.site.get('signon_realm')} } } @@ -89,20 +89,8 @@ SiteItem.propTypes = { class PasswordItem extends React.Component { constructor () { super() - this.state = { - decrypted: null - } this.onDelete = this.onDelete.bind(this) this.onCopy = this.onCopy.bind(this) - this.onDecrypt = this.onDecrypt.bind(this) - } - - decrypt () { - // Ask the main process to decrypt the password - const password = this.props.password - aboutActions.decryptPassword(password.get('encryptedPassword'), - password.get('authTag'), password.get('iv'), - this.props.id) } onDelete () { @@ -110,25 +98,7 @@ class PasswordItem extends React.Component { } onCopy () { - if (this.state.decrypted !== null) { - aboutActions.setClipboard(this.state.decrypted) - } else { - this.decrypt(false) - } - } - - onDecrypt (e, details) { - if (details.id !== this.props.id) { - return - } - aboutActions.setClipboard(details.decrypted) - this.setState({ - decrypted: details.decrypted - }) - } - - componentDidMount () { - ipc.on('decrypted-password', this.onDecrypt) + aboutActions.setClipboard(this.props.password.get('password')) } render () { @@ -143,10 +113,10 @@ class PasswordItem extends React.Component { style={{backgroundColor: 'inherit'}} /> - {password.get('origin')} + {password.get('signon_realm')} {password.get('username')} - {'*'.repeat(password.get('encryptedPassword').length)} + {'*'.repeat(password.get('password').length)} @@ -171,7 +141,7 @@ class AboutPasswords extends React.Component { super() this.state = { passwordDetails: new Immutable.List(), - disabledSiteDetails: new Immutable.Map() + disabledSiteDetails: new Immutable.List() } this.onClear = this.onClear.bind(this) ipc.on(messages.PASSWORD_DETAILS_UPDATED, (e, detail) => { @@ -226,7 +196,7 @@ class AboutPasswords extends React.Component { { this.state.passwordDetails.sort((a, b) => { - return a.get('origin') > b.get('origin') ? 1 : -1 + return a.get('signon_realm') > b.get('signon_realm') ? 1 : -1 }).map((item) => ) } @@ -249,8 +219,8 @@ class AboutPasswords extends React.Component { { - this.state.disabledSiteDetails.map((item, site) => - ) + this.state.disabledSiteDetails.map((item) => + ) }
diff --git a/js/actions/appActions.js b/js/actions/appActions.js index b2f054d9c8e..1dc4f2647bf 100644 --- a/js/actions/appActions.js +++ b/js/actions/appActions.js @@ -408,37 +408,6 @@ const appActions = { }) }, - /** - * Saves login credentials - * @param {Object} passwordDetail - login details - */ - savePassword: function (passwordDetail) { - AppDispatcher.dispatch({ - actionType: appConstants.APP_ADD_PASSWORD, - passwordDetail - }) - }, - - /** - * Deletes login credentials - * @param {Object} passwordDetail - login details - */ - deletePassword: function (passwordDetail) { - AppDispatcher.dispatch({ - actionType: appConstants.APP_REMOVE_PASSWORD, - passwordDetail - }) - }, - - /** - * Deletes all saved login credentials - */ - clearPasswords: function () { - AppDispatcher.dispatch({ - actionType: appConstants.APP_CLEAR_PASSWORDS - }) - }, - /** * Changes an application level setting * @param {string} key - The key name for the setting @@ -1305,6 +1274,61 @@ const appActions = { location, tabId }) + }, + + /* + * Handle 'save-password' event from muon + */ + savePassword: function (username, origin, tabId) { + AppDispatcher.dispatch({ + actionType: appConstants.APP_SAVE_PASSWORD, + username, + origin, + tabId + }) + }, + + /* + * Handle 'update-password' event from muon + */ + updatePassword: function (username, origin, tabId) { + AppDispatcher.dispatch({ + actionType: appConstants.APP_UPDATE_PASSWORD, + username, + origin, + tabId + }) + }, + + /** + * Deletes login credentials + * @param {Object} passwordDetail - login details + */ + deletePassword: function (passwordDetail) { + AppDispatcher.dispatch({ + actionType: appConstants.APP_REMOVE_PASSWORD, + passwordDetail + }) + }, + + /** + * Deletes all saved login credentials + */ + clearPasswords: function () { + AppDispatcher.dispatch({ + actionType: appConstants.APP_CLEAR_PASSWORDS + }) + }, + + /** + * Delete legacy "never saved password" list + */ + deletePasswordSite: function (origin) { + AppDispatcher.dispatch({ + actionType: appConstants.APP_CHANGE_SITE_SETTING, + hostPattern: origin, + key: 'savePasswords' + }) } } diff --git a/js/components/frame.js b/js/components/frame.js index b13a80e4677..798ac262580 100644 --- a/js/components/frame.js +++ b/js/components/frame.js @@ -400,18 +400,6 @@ class Frame extends React.Component { case 'show-findbar': windowActions.setFindbarShown(this.props.frameKey, true) break - case 'fill-password': - let currentUrl = urlParse(this.props.tabUrl) - if (currentUrl && - [currentUrl.protocol, currentUrl.host].join('//') === this.props.shortcutDetailsOrigin) { - this.webview.send(messages.GOT_PASSWORD, - this.props.shortcutDetailsUsername, - this.props.shortcutDetailsPassword, - this.props.shortcutDetailsOrigin, - this.props.shortcutDetailsAction, - true) - } - break case 'focus-webview': setImmediate(() => this.webview.focus()) break diff --git a/js/components/main.js b/js/components/main.js index fc1b4da9421..531481654bf 100644 --- a/js/components/main.js +++ b/js/components/main.js @@ -445,11 +445,6 @@ class Main extends ImmutableComponent { securityState) }) - ipc.on(messages.SHOW_USERNAME_LIST, (e, usernames, origin, action, boundingRect) => { - const topOffset = this.tabContainer.getBoundingClientRect().top - contextMenus.onShowUsernameMenu(usernames, origin, action, boundingRect, topOffset) - }) - ipc.on(messages.HIDE_CONTEXT_MENU, () => { windowActions.setContextMenuDetail() }) diff --git a/js/constants/appConstants.js b/js/constants/appConstants.js index f7ea8959105..73fce1c7654 100644 --- a/js/constants/appConstants.js +++ b/js/constants/appConstants.js @@ -22,9 +22,6 @@ const appConstants = { APP_APPLY_SITE_RECORDS: _, APP_MERGE_DOWNLOAD_DETAIL: _, // Sets an individual download detail APP_CLEAR_COMPLETED_DOWNLOADS: _, // Removes all completed downloads - APP_ADD_PASSWORD: _, /** @param {Object} passwordDetail */ - APP_REMOVE_PASSWORD: _, /** @param {Object} passwordDetail */ - APP_CLEAR_PASSWORDS: _, APP_DEFAULT_WINDOW_PARAMS_CHANGED: _, APP_SET_DATA_FILE_ETAG: _, APP_SET_DATA_FILE_LAST_CHECK: _, @@ -124,7 +121,13 @@ const appConstants = { APP_ON_GO_TO_INDEX: _, APP_ON_GO_BACK_LONG: _, APP_ON_GO_FORWARD_LONG: _, - APP_AUTOPLAY_BLOCKED: _ + APP_AUTOPLAY_BLOCKED: _, + APP_SAVE_PASSWORD: _, + APP_UPDATE_PASSWORD: _, + APP_ADD_PASSWORD: _, /** @param {Object} passwordDetail */ + APP_REMOVE_PASSWORD: _, /** @param {Object} passwordDetail */ + APP_REMOVE_PASSWORD_SITE: _, /** @param {Object} passwordDetail */ + APP_CLEAR_PASSWORDS: _ } module.exports = mapValuesByKeys(appConstants) diff --git a/js/constants/messages.js b/js/constants/messages.js index 9332e214b2d..e8eb61d9984 100644 --- a/js/constants/messages.js +++ b/js/constants/messages.js @@ -71,18 +71,10 @@ const messages = { GO_FORWARD: _, RELOAD: _, DETACH: _, - // Password manager - GET_PASSWORDS: _, /** @arg {string} formOrigin, @arg {string} action */ - GOT_PASSWORD: _, /** @arg {string} username, @arg {string} password, @arg {string} origin, @arg {string} action, @arg {boolean} isUnique */ - SAVE_PASSWORD: _, /** @arg {string} username, @arg {string} password, @arg {string} formOrigin, @arg {string} action */ IS_MISSPELLED: _, /** @arg {string} word, the word to check */ GET_MISSPELLING_INFO: _, /** @arg {string} word, the word to lookup */ - SHOW_USERNAME_LIST: _, /** @arg {string} formOrigin, @arg {string} action, @arg {Object} boundingRect, @arg {string} usernameValue */ - FILL_PASSWORD: _, /** @arg {string} username, @arg {string} password, @arg {string} origin, @arg {string} action */ PASSWORD_DETAILS_UPDATED: _, /** @arg {Object} passwords app state */ PASSWORD_SITE_DETAILS_UPDATED: _, /** @arg {Object} passwords app state */ - DECRYPT_PASSWORD: _, /** @arg {string} encrypted pw, @arg {string} iv, @arg {string} authTag, @arg {number} id */ - DECRYPTED_PASSWORD: _, /** @arg {number} decrypted pw, @arg {number} id */ // Init INITIALIZE_WINDOW: _, // Session restore diff --git a/js/contextMenus.js b/js/contextMenus.js index eeb5698b1d8..e20837f48b6 100644 --- a/js/contextMenus.js +++ b/js/contextMenus.js @@ -443,26 +443,6 @@ function moreBookmarksTemplateInit (allBookmarkItems, bookmarks, activeFrame) { return menuUtil.sanitizeTemplateItems(template) } -function usernameTemplateInit (usernames, origin, action) { - let template = [] - for (let username in usernames) { - let password = usernames[username] - template.push({ - label: username, - click: (item) => { - windowActions.frameShortcutChanged(null, messages.FILL_PASSWORD, { - username, - password, - origin, - action - }) - windowActions.setContextMenuDetail() - } - }) - } - return menuUtil.sanitizeTemplateItems(template) -} - function autofillTemplateInit (suggestions, frame) { const template = [] for (let i = 0; i < suggestions.length; ++i) { @@ -472,12 +452,21 @@ function autofillTemplateInit (suggestions, frame) { value = suggestions[i].value } else if (frontendId === -1) { // POPUP_ITEM_ID_WARNING_MESSAGE value = 'Disabled due to unsecure connection.' + } else if (frontendId === -2) { // POPUP_ITEM_ID_PASSWORD_ENTRY + value = suggestions[i].value } else if (frontendId === -4) { // POPUP_ITEM_ID_CLEAR_FORM value = 'Clear Form' } else if (frontendId === -5) { // POPUP_ITEM_ID_AUTOFILL_OPTIONS value = 'Autofill Settings' } else if (frontendId === -6) { // POPUP_ITEM_ID_DATALIST_ENTRY value = suggestions[i].value + } else if (frontendId === -11) { // POPUP_ITEM_ID_USERNAME_ENTRY + value = suggestions[i].value + } + if (frontendId === -2) { + template.push({ + label: 'Use password for:' + }) } if (frontendId === -3) { // POPUP_ITEM_ID_SEPARATOR template.push(CommonMenu.separatorMenuItem) @@ -1458,24 +1447,6 @@ function onShowBookmarkFolderMenu (bookmarks, bookmark, activeFrame, e) { })) } -/** - * @param {Object} usernames - map of username to plaintext password - * @param {string} origin - origin of the form - * @param {string} action - action of the form - * @param {Object} boundingRect - bounding rectangle of username input field - * @param {number} topOffset - distance from webview to the top of window - */ -function onShowUsernameMenu (usernames, origin, action, boundingRect, - topOffset) { - const downloadsBarOffset = windowStore.getState().getIn(['ui', 'downloadsToolbar', 'isVisible']) ? getDownloadsBarHeight() : 0 - const menuTemplate = usernameTemplateInit(usernames, origin, action) - windowActions.setContextMenuDetail(Immutable.fromJS({ - left: boundingRect.left, - top: boundingRect.bottom + topOffset - downloadsBarOffset, - template: menuTemplate - })) -} - function onShowAutofillMenu (suggestions, boundingRect, frame) { const menuTemplate = autofillTemplateInit(suggestions, frame) const downloadsBarOffset = windowStore.getState().getIn(['ui', 'downloadsToolbar', 'isVisible']) && @@ -1531,7 +1502,6 @@ module.exports = { onFindBarContextMenu, onSiteDetailContextMenu, onShowBookmarkFolderMenu, - onShowUsernameMenu, onShowAutofillMenu, onMoreBookmarksMenu, onReloadContextMenu diff --git a/js/state/privacy.js b/js/state/privacy.js index be3596499b7..c499825f84d 100644 --- a/js/state/privacy.js +++ b/js/state/privacy.js @@ -5,12 +5,18 @@ const AppDispatcher = require('../dispatcher/appDispatcher') const AppStore = require('../stores/appStore') const appConstants = require('../constants/appConstants') +const {passwordManagers} = require('../constants/passwordManagers') const settings = require('../constants/settings') const {registerUserPrefs} = require('./userPrefs') const getSetting = require('../settings').getSetting const getPrivacySettings = () => { - return { 'autofill.enabled': getSetting(settings.AUTOFILL_ENABLED) } + const passwordManagerEnabled = getSetting(settings.ACTIVE_PASSWORD_MANAGER) === passwordManagers.BUILT_IN + return { 'autofill.enabled': getSetting(settings.AUTOFILL_ENABLED), + 'profile.password_manager_enabled': passwordManagerEnabled, + 'credentials_enable_service': passwordManagerEnabled, + 'credentials_enable_autosignin': false + } } let updateTrigger diff --git a/js/stores/appStore.js b/js/stores/appStore.js index 74658672a3a..e1b7240f236 100644 --- a/js/stores/appStore.js +++ b/js/stores/appStore.js @@ -422,23 +422,16 @@ const handleAppAction = (action) => { case appConstants.APP_NEW_WINDOW: createWindow(action) break - case appConstants.APP_ADD_PASSWORD: - // If there is already an entry for this exact origin, action, and - // username if it exists, update the password instead of creating a new entry - let passwords = appState.get('passwords').filterNot((pw) => { - return pw.get('origin') === action.passwordDetail.origin && - pw.get('action') === action.passwordDetail.action && - (!pw.get('username') || pw.get('username') === action.passwordDetail.username) - }) - appState = appState.set('passwords', passwords.push(Immutable.fromJS(action.passwordDetail))) - break case appConstants.APP_REMOVE_PASSWORD: - appState = appState.set('passwords', appState.get('passwords').filterNot((pw) => { - return Immutable.is(pw, Immutable.fromJS(action.passwordDetail)) - })) + autofill.removeLogin(action.passwordDetail.toJS()) + break + case appConstants.APP_REMOVE_PASSWORD_SITE: + let newPasswordDetail = action.passwordDetail.toJS() + delete newPasswordDetail['blacklisted_by_user'] + autofill.updateLogin(newPasswordDetail) break case appConstants.APP_CLEAR_PASSWORDS: - appState = appState.set('passwords', new Immutable.List()) + autofill.clearLogins() break case appConstants.APP_CHANGE_NEW_TAB_DETAIL: appState = aboutNewTabState.mergeDetails(appState, action)