diff --git a/js/default.js b/js/default.js index 9f2d4584f..b1932b091 100644 --- a/js/default.js +++ b/js/default.js @@ -159,6 +159,7 @@ require('autofillSetup.js').initialize() require('passwordManager/passwordManager.js').initialize() require('passwordManager/passwordCapture.js').initialize() require('passwordManager/passwordViewer.js').initialize() +require('passwordManager/passwordMigrator.js').initialize() require('util/theme.js').initialize() require('userscripts.js').initialize() require('statistics.js').initialize() diff --git a/js/passwordManager/bitwarden.js b/js/passwordManager/bitwarden.js index 78b6994d5..a5d112253 100644 --- a/js/passwordManager/bitwarden.js +++ b/js/passwordManager/bitwarden.js @@ -97,7 +97,8 @@ class Bitwarden { // 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 urlObj = new URL(domain) + const process = new ProcessSpawner(command, ['list', 'items', '--url', `${urlObj.protocol}//${this.sanitize(urlObj.hostname)}`, '--session', this.sessionKey]) const data = await process.execute() const matches = JSON.parse(data) diff --git a/js/passwordManager/keychain.js b/js/passwordManager/keychain.js index 19bf96765..cb4a2b89b 100644 --- a/js/passwordManager/keychain.js +++ b/js/passwordManager/keychain.js @@ -64,7 +64,7 @@ class Keychain { const domainWithProtocol = includesProtocol ? credential.url : `https://${credential.url}` return { - domain: new URL(domainWithProtocol).hostname.replace(/^www\./g, ''), + domain: new URL(domainWithProtocol).origin, username: credential.username, password: credential.password } diff --git a/js/passwordManager/onePassword.js b/js/passwordManager/onePassword.js index 60f4facdb..40e7cb305 100644 --- a/js/passwordManager/onePassword.js +++ b/js/passwordManager/onePassword.js @@ -148,11 +148,8 @@ class OnePassword { const credentials = matches.filter((match) => { try { - var matchHost = new URL(match.urls.find(url => url.primary).href).hostname - if (matchHost.startsWith('www.')) { - matchHost = matchHost.slice(4) - } - return matchHost === domain + var matchHost = new URL(match.urls.find(url => url.primary).href).origin + return matchHost.replace(/^www\./, '') === domain || matchHost === domain } catch (e) { return false } diff --git a/js/passwordManager/passwordCapture.js b/js/passwordManager/passwordCapture.js index 06af18cc8..425f04b61 100644 --- a/js/passwordManager/passwordCapture.js +++ b/js/passwordManager/passwordCapture.js @@ -48,11 +48,11 @@ const passwordCapture = { }, handleRecieveCredentials: function (tab, args, frameId) { var domain = args[0][0] - if (domain.startsWith('www.')) { - domain = domain.slice(4) - } - if (settings.get('passwordsNeverSaveDomains') && settings.get('passwordsNeverSaveDomains').includes(domain)) { + if (settings.get('passwordsNeverSaveDomains') && ( + settings.get('passwordsNeverSaveDomains').includes(domain.replace(/^www\./, '')) || + settings.get('passwordsNeverSaveDomains').includes(domain) + )) { return } diff --git a/js/passwordManager/passwordManager.js b/js/passwordManager/passwordManager.js index d09ec8499..c463f947d 100644 --- a/js/passwordManager/passwordManager.js +++ b/js/passwordManager/passwordManager.js @@ -86,7 +86,7 @@ const PasswordManagers = { 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 + const origin = new URL(frameURL).origin PasswordManagers.getConfiguredPasswordManager().then(async (manager) => { if (!manager) { @@ -97,16 +97,11 @@ const PasswordManagers = { await PasswordManagers.unlock(manager) } - var formattedHostname = hostname - if (formattedHostname.startsWith('www.')) { - formattedHostname = formattedHostname.slice(4) - } - - manager.getSuggestions(formattedHostname).then(credentials => { + manager.getSuggestions(origin).then(credentials => { if (credentials != null) { webviews.callAsync(tab, 'sendToFrame', [frameId, 'password-autofill-match', { credentials, - hostname + origin }]) } }).catch(e => { diff --git a/js/passwordManager/passwordMigrator.js b/js/passwordManager/passwordMigrator.js new file mode 100644 index 000000000..5967645c4 --- /dev/null +++ b/js/passwordManager/passwordMigrator.js @@ -0,0 +1,94 @@ +const { ipcRenderer } = require('electron') +const PasswordManagers = require('passwordManager/passwordManager.js') +const places = require('places/places.js') +const settings = require('util/settings/settings.js') + +class PasswordMigrator { + #currentVersion = 2; + + constructor() { + this.startMigration() + } + + async _isOutdated(version) { + return version < this.#currentVersion + } + + async _getInUseCredentialVersion() { + const version = await ipcRenderer.invoke('credentialStoreGetVersion') + return version + } + + async startMigration() { + const inUseVersion = await this._getInUseCredentialVersion() + const isOutdated = await this._isOutdated(inUseVersion) + if (!isOutdated) return + + try { + if (inUseVersion === 1 && this.#currentVersion === 2) { + await this.migrateVersion1to2() + console.log('[PasswordMigrator]: Migration complete.') + return + } + } catch (error) { + console.error('Error during password migration:', error) + } + } + + async migrateVersion1to2() { + console.log('[PasswordMigrator]: Migrating keychain data to version', this.#currentVersion) + + const passwordManager = await PasswordManagers.getConfiguredPasswordManager() + if (!passwordManager || !passwordManager.getAllCredentials) { + throw new Error('Incompatible password manager') + } + + const historyData = await places.getAllItems() + const currentCredentials = await passwordManager.getAllCredentials() + console.log('[PasswordMigrator]: Found', historyData.length, 'history entries', historyData) + console.log('[PasswordMigrator]: Found', currentCredentials.length, 'credentials in the current password manager', currentCredentials) + + function createNewCredential(credential) { + // 1) check if the saved url has been visited, if so use that url + const historyEntry = historyData.find(entry => new URL(entry.url).host.replace(/^(https?:\/\/)?(www\.)?/, '') === credential.domain.replace(/^(https?:\/\/)?(www\.)?/, '')) + if (historyEntry) { + return { + username: credential.username, + password: credential.password, + url: new URL(historyEntry.url).origin + } + } + + // 2) check if domain has protocol, if not, add 'https://' + if (!newUrl.startsWith('http://') && !newUrl.startsWith('https://')) { + newUrl = `https://${newUrl}` + } + + return { + username: credential.username, + password: credential.password, + url: newUrl + }; + } + + const migratedCredentials = currentCredentials.map(createNewCredential) + console.log('[PasswordMigrator]: Migrated', migratedCredentials.length, 'credentials', migratedCredentials); + + const neverSavedCredentials = settings.get('passwordsNeverSaveDomains') || [] + console.log('[PasswordMigrator]: Found', neverSavedCredentials.length, 'never-saved credentials', neverSavedCredentials) + const migratedNeverSavedCredentials = neverSavedCredentials.map(createNewCredential) + settings.set('passwordsNeverSaveDomains', migratedNeverSavedCredentials) + console.log('[PasswordMigrator]: Migrated', migratedNeverSavedCredentials.length, 'never-saved credentials', migratedNeverSavedCredentials) + + await ipcRenderer.invoke('credentialStoreSetPasswordBulk', migratedCredentials) + + // finally upate the version + await ipcRenderer.invoke('credentialStoreSetVersion', this.#currentVersion) + } +} + +function initialize() { + new PasswordMigrator() +} + +module.exports = { initialize } diff --git a/js/preload/passwordFill.js b/js/preload/passwordFill.js index 56b988aeb..67481f46d 100644 --- a/js/preload/passwordFill.js +++ b/js/preload/passwordFill.js @@ -296,7 +296,7 @@ function handleBlur (event) { // Handle credentials fetched from the backend. Credentials are expected to be // an array of { username, password, manager } objects. ipc.on('password-autofill-match', (event, data) => { - if (data.hostname !== window.location.hostname) { + if (data.origin.replace(/^www\./, '') !== window.location.origin || data.origin !== window.location.origin) { throw new Error('password origin must match current page origin') } @@ -344,7 +344,7 @@ function handleFormSubmit () { var passwordValue = getBestPasswordField()?.value if ((usernameValue && usernameValue.length > 0) && (passwordValue && passwordValue.length > 0)) { - ipc.send('password-form-filled', [window.location.hostname, usernameValue, passwordValue]) + ipc.send('password-form-filled', [window.location.origin, usernameValue, passwordValue]) } } @@ -428,7 +428,7 @@ ipc.on('generate-password', function (location) { setTimeout(function () { if (input.value === generatedPassword) { var usernameValue = getBestUsernameField()?.value - ipc.send('password-form-filled', [window.location.hostname, usernameValue, generatedPassword]) + ipc.send('password-form-filled', [window.location.origin, usernameValue, generatedPassword]) } }, 0) } diff --git a/main/keychainService.js b/main/keychainService.js index c0fac17bd..361039f8a 100644 --- a/main/keychainService.js +++ b/main/keychainService.js @@ -88,3 +88,13 @@ ipc.handle('credentialStoreDeletePassword', async function (event, account) { ipc.handle('credentialStoreGetCredentials', async function () { return readSavedPasswordFile().credentials }) + +ipc.handle('credentialStoreGetVersion', async function () { + return readSavedPasswordFile().version +}) + +ipc.handle('credentialStoreSetVersion', async function (event, version) { + const fileContent = readSavedPasswordFile() + fileContent.version = version + return writeSavedPasswordFile(fileContent) +})