From c1c8d9aeb9442b762c7bbee8e2198931ea90d77e Mon Sep 17 00:00:00 2001 From: RubenSmn Date: Tue, 4 Feb 2025 17:29:38 +0100 Subject: [PATCH 1/6] chore: credential migration --- js/default.js | 1 + js/passwordManager/onePassword.js | 5 +- js/passwordManager/passwordCapture.js | 21 ++++-- js/passwordManager/passwordManager.js | 11 +--- js/passwordManager/passwordMigrator.js | 90 ++++++++++++++++++++++++++ js/preload/passwordFill.js | 6 +- main/keychainService.js | 4 ++ 7 files changed, 117 insertions(+), 21 deletions(-) create mode 100644 js/passwordManager/passwordMigrator.js 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/onePassword.js b/js/passwordManager/onePassword.js index 60f4facdb..75ca8a153 100644 --- a/js/passwordManager/onePassword.js +++ b/js/passwordManager/onePassword.js @@ -148,10 +148,7 @@ 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) - } + var matchHost = new URL(match.urls.find(url => url.primary).href).origin return matchHost === domain } catch (e) { return false diff --git a/js/passwordManager/passwordCapture.js b/js/passwordManager/passwordCapture.js index 06af18cc8..3cf290876 100644 --- a/js/passwordManager/passwordCapture.js +++ b/js/passwordManager/passwordCapture.js @@ -48,12 +48,21 @@ 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)) { - return + console.log('domain', domain) + + // get the old domain version + const oldDomainVersion = new URL(domain).host.replace('www.', '') + const passwordsNeverSaveDomains = settings.get('passwordsNeverSaveDomains') + if (passwordsNeverSaveDomains) { + // check if the old domain version should not be saved + if (passwordsNeverSaveDomains.includes(oldDomainVersion)) { + // update to the new domain version + settings.set('passwordsNeverSaveDomains', passwordsNeverSaveDomains.map(d => d === oldDomainVersion ? domain : d)) + return + } + if (passwordsNeverSaveDomains.includes(domain)) { + return + } } var username = args[0][1] || '' 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..f24fa4bd0 --- /dev/null +++ b/js/passwordManager/passwordMigrator.js @@ -0,0 +1,90 @@ +const { ipcRenderer } = require('electron'); +const PasswordManagers = require('passwordManager/passwordManager.js'); +const places = require('places/places.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) + + const migratedCredentials = currentCredentials.map(credential => { + // 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: historyEntry.url + } + } + let newUrl = credential.domain; + // 1) check if domain has subdomain, if so, use it, otherwise add 'www.' + const domainParts = newUrl.split('.') + if (domainParts.length > 2) { + newUrl = domainParts.join('.') + } else { + newUrl = `www.${newUrl}` + } + + // 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 + }; + }); + + await ipcRenderer.invoke('credentialStoreSetPasswordBulk', migratedCredentials) + console.log('[PasswordMigrator]: Migrated', migratedCredentials.length, 'credentials', migratedCredentials); + } +} + +function initialize() { + new PasswordMigrator() +} + +module.exports = { initialize } diff --git a/js/preload/passwordFill.js b/js/preload/passwordFill.js index 56b988aeb..e47a3f771 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 !== 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..bc3325abd 100644 --- a/main/keychainService.js +++ b/main/keychainService.js @@ -88,3 +88,7 @@ ipc.handle('credentialStoreDeletePassword', async function (event, account) { ipc.handle('credentialStoreGetCredentials', async function () { return readSavedPasswordFile().credentials }) + +ipc.handle('credentialStoreGetVersion', async function () { + return readSavedPasswordFile().version +}) From 546a81b51ebd7ea2644a0ec320911de28c635b06 Mon Sep 17 00:00:00 2001 From: RubenSmn Date: Sat, 8 Feb 2025 19:37:14 +0100 Subject: [PATCH 2/6] chore: credential migration --- js/passwordManager/keychain.js | 2 +- js/passwordManager/onePassword.js | 2 +- js/passwordManager/passwordCapture.js | 21 +++++------------ js/passwordManager/passwordMigrator.js | 31 +++++++++++++------------- js/preload/passwordFill.js | 2 +- 5 files changed, 25 insertions(+), 33 deletions(-) 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 75ca8a153..6d9e1e7fd 100644 --- a/js/passwordManager/onePassword.js +++ b/js/passwordManager/onePassword.js @@ -149,7 +149,7 @@ class OnePassword { const credentials = matches.filter((match) => { try { var matchHost = new URL(match.urls.find(url => url.primary).href).origin - return matchHost === domain + return matchHost.replace('www.', '') === domain || matchHost === domain } catch (e) { return false } diff --git a/js/passwordManager/passwordCapture.js b/js/passwordManager/passwordCapture.js index 3cf290876..2fcaa26b7 100644 --- a/js/passwordManager/passwordCapture.js +++ b/js/passwordManager/passwordCapture.js @@ -48,21 +48,12 @@ const passwordCapture = { }, handleRecieveCredentials: function (tab, args, frameId) { var domain = args[0][0] - console.log('domain', domain) - - // get the old domain version - const oldDomainVersion = new URL(domain).host.replace('www.', '') - const passwordsNeverSaveDomains = settings.get('passwordsNeverSaveDomains') - if (passwordsNeverSaveDomains) { - // check if the old domain version should not be saved - if (passwordsNeverSaveDomains.includes(oldDomainVersion)) { - // update to the new domain version - settings.set('passwordsNeverSaveDomains', passwordsNeverSaveDomains.map(d => d === oldDomainVersion ? domain : d)) - return - } - if (passwordsNeverSaveDomains.includes(domain)) { - return - } + + if (settings.get('passwordsNeverSaveDomains') && ( + settings.get('passwordsNeverSaveDomains').includes(domain.replace('www.', '')) || + settings.get('passwordsNeverSaveDomains').includes(domain) + )) { + return } var username = args[0][1] || '' diff --git a/js/passwordManager/passwordMigrator.js b/js/passwordManager/passwordMigrator.js index f24fa4bd0..15bef47f6 100644 --- a/js/passwordManager/passwordMigrator.js +++ b/js/passwordManager/passwordMigrator.js @@ -1,6 +1,7 @@ -const { ipcRenderer } = require('electron'); -const PasswordManagers = require('passwordManager/passwordManager.js'); -const places = require('places/places.js'); +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; @@ -47,8 +48,8 @@ class PasswordMigrator { console.log('[PasswordMigrator]: Found', historyData.length, 'history entries', historyData) console.log('[PasswordMigrator]: Found', currentCredentials.length, 'credentials in the current password manager', currentCredentials) - const migratedCredentials = currentCredentials.map(credential => { - // check if the saved url has been visited, if so use that url + 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 { @@ -57,14 +58,6 @@ class PasswordMigrator { url: historyEntry.url } } - let newUrl = credential.domain; - // 1) check if domain has subdomain, if so, use it, otherwise add 'www.' - const domainParts = newUrl.split('.') - if (domainParts.length > 2) { - newUrl = domainParts.join('.') - } else { - newUrl = `www.${newUrl}` - } // 2) check if domain has protocol, if not, add 'https://' if (!newUrl.startsWith('http://') && !newUrl.startsWith('https://')) { @@ -76,10 +69,18 @@ class PasswordMigrator { password: credential.password, url: newUrl }; - }); + } - await ipcRenderer.invoke('credentialStoreSetPasswordBulk', migratedCredentials) + 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) } } diff --git a/js/preload/passwordFill.js b/js/preload/passwordFill.js index e47a3f771..a021568ef 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.origin !== window.location.origin) { + if (data.origin.replace('www.', '') !== window.location.origin || data.origin !== window.location.origin) { throw new Error('password origin must match current page origin') } From 6f39f086f80942927c43dd4b8ca2f39bcd52f6aa Mon Sep 17 00:00:00 2001 From: RubenSmn Date: Sat, 8 Feb 2025 19:45:13 +0100 Subject: [PATCH 3/6] fix: credential migration does not update version --- js/passwordManager/passwordMigrator.js | 3 +++ main/keychainService.js | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/js/passwordManager/passwordMigrator.js b/js/passwordManager/passwordMigrator.js index 15bef47f6..3da2d5608 100644 --- a/js/passwordManager/passwordMigrator.js +++ b/js/passwordManager/passwordMigrator.js @@ -81,6 +81,9 @@ class PasswordMigrator { 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) } } diff --git a/main/keychainService.js b/main/keychainService.js index bc3325abd..0a2c87500 100644 --- a/main/keychainService.js +++ b/main/keychainService.js @@ -92,3 +92,9 @@ ipc.handle('credentialStoreGetCredentials', async function () { ipc.handle('credentialStoreGetVersion', async function () { return readSavedPasswordFile().version }) + +ipc.handl('credentialStoreSetVersion', async function (event, version) { + const fileContent = readSavedPasswordFile() + fileContent.version = version + return writeSavedPasswordFile(fileContent) +}) From 5c96f22db63e0aad5511af744af135b134e5f25c Mon Sep 17 00:00:00 2001 From: RubenSmn Date: Wed, 12 Feb 2025 20:30:55 +0100 Subject: [PATCH 4/6] fix: credential migration unsafe www replace --- js/passwordManager/onePassword.js | 2 +- js/passwordManager/passwordCapture.js | 2 +- js/passwordManager/passwordMigrator.js | 2 +- js/preload/passwordFill.js | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/js/passwordManager/onePassword.js b/js/passwordManager/onePassword.js index 6d9e1e7fd..40e7cb305 100644 --- a/js/passwordManager/onePassword.js +++ b/js/passwordManager/onePassword.js @@ -149,7 +149,7 @@ class OnePassword { const credentials = matches.filter((match) => { try { var matchHost = new URL(match.urls.find(url => url.primary).href).origin - return matchHost.replace('www.', '') === domain || matchHost === domain + return matchHost.replace(/^www\./, '') === domain || matchHost === domain } catch (e) { return false } diff --git a/js/passwordManager/passwordCapture.js b/js/passwordManager/passwordCapture.js index 2fcaa26b7..425f04b61 100644 --- a/js/passwordManager/passwordCapture.js +++ b/js/passwordManager/passwordCapture.js @@ -50,7 +50,7 @@ const passwordCapture = { var domain = args[0][0] if (settings.get('passwordsNeverSaveDomains') && ( - settings.get('passwordsNeverSaveDomains').includes(domain.replace('www.', '')) || + settings.get('passwordsNeverSaveDomains').includes(domain.replace(/^www\./, '')) || settings.get('passwordsNeverSaveDomains').includes(domain) )) { return diff --git a/js/passwordManager/passwordMigrator.js b/js/passwordManager/passwordMigrator.js index 3da2d5608..5967645c4 100644 --- a/js/passwordManager/passwordMigrator.js +++ b/js/passwordManager/passwordMigrator.js @@ -55,7 +55,7 @@ class PasswordMigrator { return { username: credential.username, password: credential.password, - url: historyEntry.url + url: new URL(historyEntry.url).origin } } diff --git a/js/preload/passwordFill.js b/js/preload/passwordFill.js index a021568ef..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.origin.replace('www.', '') !== window.location.origin || data.origin !== window.location.origin) { + if (data.origin.replace(/^www\./, '') !== window.location.origin || data.origin !== window.location.origin) { throw new Error('password origin must match current page origin') } From 567997e59d35fe89df2d4fe33268e195682dd092 Mon Sep 17 00:00:00 2001 From: RubenSmn Date: Wed, 12 Feb 2025 20:36:58 +0100 Subject: [PATCH 5/6] fix: credential migration bitwarden --- js/passwordManager/bitwarden.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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) From 97c59c78b92bea9fabf7277dce4ed37b28cf28f7 Mon Sep 17 00:00:00 2001 From: RubenSmn Date: Sun, 23 Feb 2025 14:02:54 +0100 Subject: [PATCH 6/6] fix: typo --- main/keychainService.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/keychainService.js b/main/keychainService.js index 0a2c87500..361039f8a 100644 --- a/main/keychainService.js +++ b/main/keychainService.js @@ -93,7 +93,7 @@ ipc.handle('credentialStoreGetVersion', async function () { return readSavedPasswordFile().version }) -ipc.handl('credentialStoreSetVersion', async function (event, version) { +ipc.handle('credentialStoreSetVersion', async function (event, version) { const fileContent = readSavedPasswordFile() fileContent.version = version return writeSavedPasswordFile(fileContent)