diff --git a/js/autofillSetup.js b/js/autofillSetup.js index ea80a30fc..21ce4b26f 100644 --- a/js/autofillSetup.js +++ b/js/autofillSetup.js @@ -2,29 +2,30 @@ const setupDialog = require('passwordManager/managerSetup.js') const settings = require('util/settings/settings.js') const PasswordManagers = require('passwordManager/passwordManager.js') -const AutofillSetup = { - checkSettings: function () { - const manager = PasswordManagers.getActivePasswordManager() +async function checkSettings () { + const manager = PasswordManagers.getActivePasswordManager() + + if (!manager) { + return + } + + try { + const configured = await manager.checkIfConfigured() + if (!configured) { + setupDialog.show(manager) + } + } catch (e) { + console.error(e) + } +} + +function initialize () { + settings.listen('passwordManager', manager => { if (!manager) { return } - - manager.checkIfConfigured().then((configured) => { - if (!configured) { - setupDialog.show(manager) - } - }).catch((err) => { - console.error(err) - }) - }, - initialize: function () { - settings.listen('passwordManager', function (manager) { - if (manager) { - // Trigger the check on browser launch and after manager is enabled - AutofillSetup.checkSettings() - } - }) - } + checkSettings() + }) } -module.exports = AutofillSetup +module.exports = { initialize } diff --git a/js/passwordManager/bitwarden.js b/js/passwordManager/bitwarden.js index 78b6994d5..bf387be4d 100644 --- a/js/passwordManager/bitwarden.js +++ b/js/passwordManager/bitwarden.js @@ -1,7 +1,9 @@ -const ProcessSpawner = require('util/process.js') -const path = require('path') + +const { join } = require('path') const fs = require('fs') -var { ipcRenderer } = require('electron') +const { ipcRenderer } = require('electron') + +const ProcessSpawner = require('util/process.js') // Bitwarden password manager. Requires session key to unlock the vault. class Bitwarden { @@ -12,18 +14,17 @@ class Bitwarden { } getDownloadLink () { - switch (window.platformType) { - case 'mac': - return 'https://vault.bitwarden.com/download/?app=cli&platform=macos' - case 'windows': - return 'https://vault.bitwarden.com/download/?app=cli&platform=windows' - case 'linux': - return 'https://vault.bitwarden.com/download/?app=cli&platform=linux' + if (window.platformType === 'mac') { + return 'https://vault.bitwarden.com/download/?app=cli&platform=macos' + } + if (window.platformType === 'windows') { + return 'https://vault.bitwarden.com/download/?app=cli&platform=windows' } + return 'https://vault.bitwarden.com/download/?app=cli&platform=linux' } getLocalPath () { - return path.join(window.globalArgs['user-data-path'], 'tools', (platformType === 'windows' ? 'bw.exe' : 'bw')) + return join(window.globalArgs['user-data-path'], 'tools', (platformType === 'windows' ? 'bw.exe' : 'bw')) } getSetupMode () { @@ -41,7 +42,7 @@ class Bitwarden { try { await fs.promises.access(localPath, fs.constants.X_OK) local = true - } catch (e) { } + } catch { } if (local) { return localPath } @@ -71,7 +72,7 @@ class Bitwarden { // Tries to get a list of credential suggestions for a given domain name. async getSuggestions (domain) { - if (this.lastCallList[domain] != null) { + if (this.lastCallList[domain]) { return this.lastCallList[domain] } @@ -84,32 +85,24 @@ class Bitwarden { throw new Error() } - this.lastCallList[domain] = this.loadSuggestions(command, domain).then(suggestions => { - this.lastCallList[domain] = null - return suggestions - }).catch(ex => { - this.lastCallList[domain] = null - }) - + this.lastCallList[domain] = await this.loadSuggestions(command, domain) return this.lastCallList[domain] } // Loads credential suggestions for given domain name. async loadSuggestions (command, domain) { try { - const process = new ProcessSpawner(command, ['list', 'items', '--url', this.sanitize(domain), '--session', this.sessionKey]) - const data = await process.execute() - - const matches = JSON.parse(data) - const credentials = matches.map(match => { - const { login: { username, password } } = match - return { username, password, manager: 'Bitwarden' } - }) - - return credentials - } catch (ex) { - const { error, data } = ex - console.error('Error accessing Bitwarden CLI. STDOUT: ' + data + '. STDERR: ' + error) + const process = new ProcessSpawner( + command, + ['list', 'items', '--url', domain.replace(/[^a-zA-Z0-9.-]/g, ''), '--session', this.sessionKey] + ) + const matches = JSON.parse(await process.execute()) + return matches.map( + ({ login: { username, password } }) => + ({ username, password, manager: 'Bitwarden' }) + ) + } catch ({ error, data }) { + console.error(`Error accessing Bitwarden CLI. STDOUT: ${data}. STDERR: ${error}`) return [] } } @@ -118,9 +111,8 @@ class Bitwarden { try { const process = new ProcessSpawner(command, ['sync', '--session', this.sessionKey]) await process.execute() - } catch (ex) { - const { error, data } = ex - console.error('Error accessing Bitwarden CLI. STDOUT: ' + data + '. STDERR: ' + error) + } catch ({ error, data }) { + console.error(`Error accessing Bitwarden CLI. STDOUT: ${data}. STDERR: ${error}`) } } @@ -138,17 +130,17 @@ class Bitwarden { await this.forceSync(this.path) return true - } catch (ex) { - const { error, data } = ex + } catch (err) { + const { error, data } = err - console.error('Error accessing Bitwarden CLI. STDOUT: ' + data + '. STDERR: ' + error) + console.error(`Error accessing Bitwarden CLI. STDOUT: ${data}. STDERR: ${error}`) if (error.includes('not logged in')) { await this.signInAndSave() return await this.unlockStore(password) } - throw ex + throw err } } @@ -161,41 +153,43 @@ class Bitwarden { console.warn(e) } - // show credentials dialog - - var signInFields = [ + // show ask-for-credential dialog + const signInFields = [ + { placeholder: 'Server URL (Leave blank for the default Bitwarden server)', id: 'url', type: 'text' }, { placeholder: 'Client ID', id: 'clientID', type: 'password' }, { placeholder: 'Client Secret', id: 'clientSecret', type: 'password' } ] - const credentials = ipcRenderer.sendSync('prompt', { - text: l('passwordManagerBitwardenSignIn'), - values: signInFields, - ok: l('dialogConfirmButton'), - cancel: l('dialogSkipButton'), - width: 500, - height: 260 - }) - - for (const key in credentials) { - if (credentials[key] === '') { - throw new Error('no credentials entered') + const credentials = ipcRenderer.sendSync( + 'prompt', + { + text: l('passwordManagerBitwardenSignIn'), + values: signInFields, + ok: l('dialogConfirmButton'), + cancel: l('dialogSkipButton'), + width: 500, + height: 260 } + ) + + if (credentials.clientID === '' || credentials.clientSecret === '') { + throw new Error('no credentials entered') } - const process = new ProcessSpawner(path, ['login', '--apikey'], { - BW_CLIENTID: credentials.clientID.trim(), - BW_CLIENTSECRET: credentials.clientSecret.trim() - }) + credentials.url = credentials.url || 'bitwarden.com' - await process.execute() + const process1 = new ProcessSpawner(path, ['config', 'server', credentials.url.trim()]) + await process1.execute() - return true - } - - // Basic domain name cleanup. Removes any non-ASCII symbols. - sanitize (domain) { - return domain.replace(/[^a-zA-Z0-9.-]/g, '') + const process2 = new ProcessSpawner( + path, + ['login', '--apikey'], + { + BW_CLIENTID: credentials.clientID.trim(), + BW_CLIENTSECRET: credentials.clientSecret.trim() + } + ) + await process2.execute() } } diff --git a/js/passwordManager/passwordCapture.js b/js/passwordManager/passwordCapture.js index 06af18cc8..5062ff532 100644 --- a/js/passwordManager/passwordCapture.js +++ b/js/passwordManager/passwordCapture.js @@ -13,7 +13,8 @@ const passwordCapture = { closeButton: document.getElementById('password-capture-ignore'), currentDomain: null, barHeight: 0, - showCaptureBar: function (username, password) { + + showCaptureBar (username, password) { passwordCapture.description.textContent = l('passwordCaptureSavePassword').replace('%s', passwordCapture.currentDomain) passwordCapture.bar.hidden = false @@ -27,7 +28,8 @@ const passwordCapture = { passwordCapture.barHeight = passwordCapture.bar.getBoundingClientRect().height webviews.adjustMargin([passwordCapture.barHeight, 0, 0, 0]) }, - hideCaptureBar: function () { + + hideCaptureBar () { webviews.adjustMargin([passwordCapture.barHeight * -1, 0, 0, 0]) passwordCapture.bar.hidden = true @@ -35,7 +37,8 @@ const passwordCapture = { passwordCapture.passwordInput.value = '' passwordCapture.currentDomain = null }, - togglePasswordVisibility: function () { + + togglePasswordVisibility () { if (passwordCapture.passwordInput.type === 'password') { passwordCapture.passwordInput.type = 'text' passwordCapture.revealButton.classList.remove('carbon:view') @@ -46,8 +49,9 @@ const passwordCapture = { passwordCapture.revealButton.classList.remove('carbon:view-off') } }, - handleRecieveCredentials: function (tab, args, frameId) { - var domain = args[0][0] + + async handleReceivedCredentials (tab, args, frameId) { + let domain = args[0][0] if (domain.startsWith('www.')) { domain = domain.slice(4) } @@ -56,34 +60,37 @@ const passwordCapture = { return } - var username = args[0][1] || '' - var password = args[0][2] || '' + try { + const username = args[0][1] || '' + const password = args[0][2] || '' - PasswordManagers.getConfiguredPasswordManager().then(function (manager) { + const manager = await PasswordManagers.getConfiguredPasswordManager() if (!manager || !manager.saveCredential) { // the password can't be saved return } // check if this username/password combo is already saved - manager.getSuggestions(domain).then(function (credentials) { - var alreadyExists = credentials.some(cred => cred.username === username && cred.password === password) - if (!alreadyExists) { - if (!passwordCapture.bar.hidden) { - passwordCapture.hideCaptureBar() - } - - passwordCapture.currentDomain = domain - passwordCapture.showCaptureBar(username, password) + const credentials = await manager.getSuggestions(domain) + const alreadyExists = credentials.some(cred => cred.username === username && cred.password === password) + if (!alreadyExists) { + if (!passwordCapture.bar.hidden) { + passwordCapture.hideCaptureBar() } - }) - }) + + passwordCapture.currentDomain = domain + passwordCapture.showCaptureBar(username, password) + } + } catch (e) { + console.error(`Failed to get password suggestions: ${e.message}`) + } }, - initialize: function () { + + initialize () { passwordCapture.usernameInput.placeholder = l('username') passwordCapture.passwordInput.placeholder = l('password') - webviews.bindIPC('password-form-filled', passwordCapture.handleRecieveCredentials) + webviews.bindIPC('password-form-filled', passwordCapture.handleReceivedCredentials) passwordCapture.saveButton.addEventListener('click', function () { if (passwordCapture.usernameInput.checkValidity() && passwordCapture.passwordInput.checkValidity()) { @@ -106,7 +113,7 @@ const passwordCapture = { // the bar can change height when the window is resized, so the webview needs to be resized in response window.addEventListener('resize', function () { if (!passwordCapture.bar.hidden) { - var oldHeight = passwordCapture.barHeight + const oldHeight = passwordCapture.barHeight passwordCapture.barHeight = passwordCapture.bar.getBoundingClientRect().height webviews.adjustMargin([passwordCapture.barHeight - oldHeight, 0, 0, 0]) } diff --git a/js/passwordManager/passwordManager.js b/js/passwordManager/passwordManager.js index d09ec8499..e3253dd36 100644 --- a/js/passwordManager/passwordManager.js +++ b/js/passwordManager/passwordManager.js @@ -9,126 +9,122 @@ const Bitwarden = require('js/passwordManager/bitwarden.js') const OnePassword = require('js/passwordManager/onePassword.js') const Keychain = require('js/passwordManager/keychain.js') -const PasswordManagers = { - // List of supported password managers. Each password manager is expected to - // have getSuggestions(domain) method that returns a Promise with credentials - // suggestions matching given domain name. - managers: [ - new Bitwarden(), - new OnePassword(), - new Keychain() - ], - // Returns an active password manager, which is the one that is selected in app's - // settings. - getActivePasswordManager: function () { - if (PasswordManagers.managers.length === 0) { - return null +const managers = [ + new Bitwarden(), + new OnePassword(), + new Keychain() +] + +function getActivePasswordManager () { + if (managers.length === 0) { + return null + } + + const managerSetting = settings.get('passwordManager') + if (managerSetting === null) { + return managers.find(({ name }) => name === 'Built-in password manager') + } + return managers.find(({ name }) => name === managerSetting.name) +} + +async function getConfiguredPasswordManager () { + const manager = getActivePasswordManager() + if (!manager) { + return null + } + + const configured = await manager.checkIfConfigured() + if (!configured) { + return null + } + + return manager +} + +// Shows a prompt dialog for password store's master password. +async function promptForMasterPassword (manager) { + return new Promise((resolve, reject) => { + const { password } = ipcRenderer.sendSync('prompt', { + text: l('passwordManagerUnlock').replace('%p', manager.name), + values: [{ placeholder: l('password'), id: 'password', type: 'password' }], + ok: l('dialogConfirmButton'), + cancel: l('dialogSkipButton'), + height: 175 + }) + if (password === null || password === '') { + reject(new Error('No password provided')) + } else { + resolve(password) } + }) +} - const managerSetting = settings.get('passwordManager') - if (managerSetting == null) { - return PasswordManagers.managers.find(mgr => mgr.name === 'Built-in password manager') +async function unlock (manager) { + let success = false + while (!success) { + let password + try { + password = await promptForMasterPassword(manager) + } catch (e) { + // dialog was canceled + break + } + try { + success = await manager.unlockStore(password) + } catch (e) { + // incorrect password, prompt again } + } + return success +} - return PasswordManagers.managers.find(mgr => mgr.name === managerSetting.name) - }, - getConfiguredPasswordManager: async function () { - const manager = PasswordManagers.getActivePasswordManager() +// Binds IPC events. +function initialize () { + // Called when page preload script detects a form with username and password. + webviews.bindIPC('password-autofill', async (tab, args, frameId, frameURL) => { + const manager = await getConfiguredPasswordManager() if (!manager) { - return null + return } - const configured = await manager.checkIfConfigured() - if (!configured) { - return null + // it's important to use frameURL here and not the tab URL, because the domain of the + // requesting iframe may not match the domain of the top-level page + let hostname = new URL(frameURL).hostname + + if (!manager.isUnlocked()) { + await unlock(manager) } - return manager - }, - // Shows a prompt dialog for password store's master password. - promptForMasterPassword: async function (manager) { - return new Promise((resolve, reject) => { - const { password } = ipcRenderer.sendSync('prompt', { - text: l('passwordManagerUnlock').replace('%p', manager.name), - values: [{ placeholder: l('password'), id: 'password', type: 'password' }], - ok: l('dialogConfirmButton'), - cancel: l('dialogSkipButton'), - height: 175 - }) - if (password === null || password === '') { - reject(new Error('No password provided')) - } else { - resolve(password) - } - }) - }, - unlock: async function (manager) { - let success = false - while (!success) { - let password - try { - password = await PasswordManagers.promptForMasterPassword(manager) - } catch (e) { - // dialog was canceled - break - } - try { - success = await manager.unlockStore(password) - } catch (e) { - // incorrect password, prompt again - } + if (hostname.startsWith('www.')) { + hostname = hostname.slice(4) } - return success - }, - // Binds IPC events. - initialize: function () { - // Called when page preload script detects a form with username and password. - webviews.bindIPC('password-autofill', function (tab, args, frameId, frameURL) { - // it's important to use frameURL here and not the tab URL, because the domain of the - // requesting iframe may not match the domain of the top-level page - const hostname = new URL(frameURL).hostname - - PasswordManagers.getConfiguredPasswordManager().then(async (manager) => { - if (!manager) { - return - } - - if (!manager.isUnlocked()) { - await PasswordManagers.unlock(manager) - } - - var formattedHostname = hostname - if (formattedHostname.startsWith('www.')) { - formattedHostname = formattedHostname.slice(4) - } - - manager.getSuggestions(formattedHostname).then(credentials => { - if (credentials != null) { - webviews.callAsync(tab, 'sendToFrame', [frameId, 'password-autofill-match', { - credentials, - hostname - }]) - } - }).catch(e => { - console.error('Failed to get password suggestions: ' + e.message) - }) - }) - }) - webviews.bindIPC('password-autofill-check', function (tab, args, frameId) { - if (PasswordManagers.getActivePasswordManager()) { - webviews.callAsync(tab, 'sendToFrame', [frameId, 'password-autofill-enabled']) + try { + const credentials = await manager.getSuggestions(hostname) + if (credentials) { + webviews.callAsync(tab, 'sendToFrame', [frameId, 'password-autofill-match', { + credentials, + hostname + }]) } - }) + } catch (e) { + console.error(`Failed to get password suggestions: ${e.message}`) + } + }) - keybindings.defineShortcut('fillPassword', function () { - webviews.callAsync(tabs.getSelected(), 'send', ['password-autofill-shortcut']) - }) + webviews.bindIPC('password-autofill-check', function (tab, args, frameId) { + if (getActivePasswordManager()) { + webviews.callAsync(tab, 'sendToFrame', [frameId, 'password-autofill-enabled']) + } + }) - statistics.registerGetter('passwordManager', function () { - return PasswordManagers.getActivePasswordManager().name - }) - } + keybindings.defineShortcut('fillPassword', function () { + webviews.callAsync(tabs.getSelected(), 'send', ['password-autofill-shortcut']) + }) + + statistics.registerGetter('passwordManager', function () { + return getActivePasswordManager().name + }) } -module.exports = PasswordManagers +module.exports = { getActivePasswordManager, getConfiguredPasswordManager, initialize } diff --git a/main/prompt.js b/main/prompt.js index f85ffba11..019621864 100644 --- a/main/prompt.js +++ b/main/prompt.js @@ -1,44 +1,47 @@ /* Simple input prompt. */ -var promptAnswer -var promptOptions +let promptAnswer +let promptOptions -function createPrompt (options, callback) { +function createPrompt (options) { promptOptions = options const { parent, width = 360, height = 140 } = options - var promptWindow = new BrowserWindow({ - width: width, - height: height, - parent: parent != null ? parent : windows.getCurrent(), - show: false, - modal: true, - alwaysOnTop: true, - title: options.title, - autoHideMenuBar: true, - frame: false, - webPreferences: { - nodeIntegration: true, - sandbox: false, - contextIsolation: false - } + return new Promise(resolve => { + let promptWindow = new BrowserWindow({ + width: width, + height: height, + parent: parent != null ? parent : windows.getCurrent(), + show: false, + modal: true, + alwaysOnTop: true, + title: options.title, + autoHideMenuBar: true, + frame: false, + webPreferences: { + nodeIntegration: true, + sandbox: false, + contextIsolation: false + } + }) + + promptWindow.on('closed', () => { + promptWindow = null + resolve(promptAnswer) + }) + + // Load the HTML dialog box + promptWindow.loadURL('file://' + __dirname + '/pages/prompt/index.html') + promptWindow.once('ready-to-show', () => { promptWindow.show() }) }) - - promptWindow.on('closed', () => { - promptWindow = null - callback(promptAnswer) - }) - - // Load the HTML dialog box - promptWindow.loadURL('file://' + __dirname + '/pages/prompt/index.html') - promptWindow.once('ready-to-show', () => { promptWindow.show() }) } -ipc.on('show-prompt', function (options, callback) { - createPrompt(options, callback) +ipc.on('show-prompt', async (options, callback) => { + const result = await createPrompt(options) + callback(result) }) -ipc.on('open-prompt', function (event) { +ipc.on('open-prompt', event => { event.returnValue = JSON.stringify({ label: promptOptions.text, ok: promptOptions.ok, @@ -48,12 +51,10 @@ ipc.on('open-prompt', function (event) { }) }) -ipc.on('close-prompt', function (event, data) { +ipc.on('close-prompt', (event, data) => { promptAnswer = data }) -ipc.on('prompt', function (event, data) { - createPrompt(data, function (result) { - event.returnValue = result - }) +ipc.on('prompt', async (event, data) => { + event.returnValue = await createPrompt(data) }) diff --git a/main/viewManager.js b/main/viewManager.js index 9a77b66a5..37dbe2fa4 100644 --- a/main/viewManager.js +++ b/main/viewManager.js @@ -1,9 +1,9 @@ const BrowserView = electron.BrowserView -var viewMap = {} // id: view -var viewStateMap = {} // id: view state +const viewMap = {} // id: view +const viewStateMap = {} // id: view state -var temporaryPopupViews = {} // id: view +const temporaryPopupViews = {} // id: view const defaultViewWebPreferences = { nodeIntegration: false, @@ -134,25 +134,25 @@ function createView (existingViewId, id, webPreferencesString, boundsString, eve }) // Open a login prompt when site asks for http authentication - view.webContents.on('login', (event, authenticationResponseDetails, authInfo, callback) => { - if (authInfo.scheme !== 'basic') { // Only for basic auth - return + view.webContents.on( + 'login', + async (event, authenticationResponseDetails, authInfo, callback) => { + if (authInfo.scheme !== 'basic') { // Only for basic auth + return + } + event.preventDefault() + const { username, password } = await createPrompt({ + text: l('loginPromptTitle').replace('%h', authInfo.host).replace('%r', authInfo.realm), + values: [{ placeholder: l('username'), id: 'username', type: 'text' }, + { placeholder: l('password'), id: 'password', type: 'password' }], + ok: l('dialogConfirmButton'), + cancel: l('dialogSkipButton'), + width: 400, + height: 200 + }) + callback(username, password) } - event.preventDefault() - var title = l('loginPromptTitle').replace('%h', authInfo.host).replace('%r', authInfo.realm) - createPrompt({ - text: title, - values: [{ placeholder: l('username'), id: 'username', type: 'text' }, - { placeholder: l('password'), id: 'password', type: 'password' }], - ok: l('dialogConfirmButton'), - cancel: l('dialogSkipButton'), - width: 400, - height: 200 - }, function (result) { - // resend request with auth credentials - callback(result.username, result.password) - }) - }) + ) // show an "open in app" prompt for external protocols