├── .editorconfig ├── .eslintrc ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ └── feature_request.yml └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .tx └── config ├── .vscode └── settings.json ├── CHANGELOG ├── LICENSE ├── README.md ├── build.js ├── dev-resources ├── readme.md └── toolbar_icon_source.svg ├── dist ├── manifest_chromium.json └── manifest_firefox.json ├── keepassxc-browser ├── _locales │ ├── bg │ │ └── messages.json │ ├── cs │ │ └── messages.json │ ├── da │ │ └── messages.json │ ├── de │ │ └── messages.json │ ├── el │ │ └── messages.json │ ├── en │ │ └── messages.json │ ├── en_GB │ │ └── messages.json │ ├── es │ │ └── messages.json │ ├── et │ │ └── messages.json │ ├── fi │ │ └── messages.json │ ├── fr │ │ └── messages.json │ ├── he │ │ └── messages.json │ ├── hu │ │ └── messages.json │ ├── id │ │ └── messages.json │ ├── it │ │ └── messages.json │ ├── ja │ │ └── messages.json │ ├── ko │ │ └── messages.json │ ├── la │ │ └── messages.json │ ├── lt │ │ └── messages.json │ ├── my │ │ └── messages.json │ ├── nb │ │ └── messages.json │ ├── nl │ │ └── messages.json │ ├── pl │ │ └── messages.json │ ├── pt │ │ └── messages.json │ ├── pt_BR │ │ └── messages.json │ ├── pt_PT │ │ └── messages.json │ ├── ro │ │ └── messages.json │ ├── ru │ │ └── messages.json │ ├── sl │ │ └── messages.json │ ├── sv │ │ └── messages.json │ ├── tr │ │ └── messages.json │ ├── uk │ │ └── messages.json │ ├── zh_CN │ │ └── messages.json │ └── zh_TW │ │ └── messages.json ├── background │ ├── background_service.js │ ├── browserAction.js │ ├── client.js │ ├── event.js │ ├── httpauth.js │ ├── init.js │ ├── keepass.js │ ├── nacl-util.min.js │ ├── nacl.min.js │ ├── offscreen.js │ └── page.js ├── bootstrap │ ├── bootstrap.min.css │ └── bootstrap.min.js ├── common │ ├── browser-polyfill.min.js │ ├── global.js │ ├── global_ui.js │ ├── sites.js │ └── translate.js ├── content │ ├── autocomplete.js │ ├── banner.js │ ├── credential-autocomplete.js │ ├── custom-fields-banner.js │ ├── fields.js │ ├── fill.js │ ├── form.js │ ├── icons.js │ ├── keepassxc-browser.js │ ├── observer-helper.js │ ├── passkeys-inject.js │ ├── passkeys-utils.js │ ├── passkeys.js │ ├── pwgen.js │ ├── totp-autocomplete.js │ ├── totp-field.js │ ├── ui.js │ └── username-field.js ├── css │ ├── autocomplete.css │ ├── banner.css │ ├── button.css │ ├── colors.css │ ├── define.css │ ├── notification.css │ ├── pwgen.css │ ├── totp.css │ └── username.css ├── fonts │ ├── fork-awesome.min.css │ └── forkawesome-webfont.woff2 ├── icons │ ├── custom_login_fields.svg │ ├── disconnected.svg │ ├── help.svg │ ├── keepassxc-dark_128x128.png │ ├── keepassxc-dark_16x16.png │ ├── keepassxc-dark_18x18.png │ ├── keepassxc-dark_19x19.png │ ├── keepassxc-dark_32x32.png │ ├── keepassxc-dark_36x36.png │ ├── keepassxc-dark_38x38.png │ ├── keepassxc-dark_48x48.png │ ├── keepassxc-dark_64x64.png │ ├── keepassxc.svg │ ├── keepassxc_128x128.png │ ├── keepassxc_16x16.png │ ├── keepassxc_18x18.png │ ├── keepassxc_19x19.png │ ├── keepassxc_32x32.png │ ├── keepassxc_36x36.png │ ├── keepassxc_38x38.png │ ├── keepassxc_48x48.png │ ├── keepassxc_64x64.png │ ├── keepassxc_96x96.png │ ├── key.svg │ ├── locked.svg │ ├── otp.svg │ └── toolbar │ │ ├── colored │ │ ├── icon_bang.png │ │ ├── icon_bang.svg │ │ ├── icon_cross.png │ │ ├── icon_cross.svg │ │ ├── icon_dark.png │ │ ├── icon_dark.svg │ │ ├── icon_locked.png │ │ ├── icon_locked.svg │ │ ├── icon_new_bang.png │ │ ├── icon_new_bang.svg │ │ ├── icon_new_cross.png │ │ ├── icon_new_cross.svg │ │ ├── icon_new_locked.png │ │ ├── icon_new_locked.svg │ │ ├── icon_new_normal.png │ │ ├── icon_new_normal.svg │ │ ├── icon_normal.png │ │ └── icon_normal.svg │ │ ├── dark │ │ ├── icon_bang.png │ │ ├── icon_bang.svg │ │ ├── icon_cross.png │ │ ├── icon_cross.svg │ │ ├── icon_dark.png │ │ ├── icon_dark.svg │ │ ├── icon_locked.png │ │ ├── icon_locked.svg │ │ ├── icon_new_bang.png │ │ ├── icon_new_bang.svg │ │ ├── icon_new_cross.png │ │ ├── icon_new_cross.svg │ │ ├── icon_new_locked.png │ │ ├── icon_new_locked.svg │ │ ├── icon_new_normal.png │ │ ├── icon_new_normal.svg │ │ ├── icon_normal.png │ │ └── icon_normal.svg │ │ └── light │ │ ├── icon_bang.png │ │ ├── icon_bang.svg │ │ ├── icon_cross.png │ │ ├── icon_cross.svg │ │ ├── icon_dark.png │ │ ├── icon_dark.svg │ │ ├── icon_locked.png │ │ ├── icon_locked.svg │ │ ├── icon_new_bang.png │ │ ├── icon_new_bang.svg │ │ ├── icon_new_cross.png │ │ ├── icon_new_cross.svg │ │ ├── icon_new_locked.png │ │ ├── icon_new_locked.svg │ │ ├── icon_new_normal.png │ │ ├── icon_new_normal.svg │ │ ├── icon_normal.png │ │ └── icon_normal.svg ├── managed_storage.json ├── manifest.json ├── offscreen │ ├── offscreen.html │ └── offscreen.js ├── options │ ├── http-auth-dialog.png │ ├── options.css │ ├── options.html │ ├── options.js │ ├── shortcuts.css │ ├── shortcuts.html │ └── shortcuts.js └── popups │ ├── popup.css │ ├── popup.html │ ├── popup.js │ ├── popup_functions.js │ ├── popup_httpauth.html │ ├── popup_httpauth.js │ ├── popup_login.html │ └── popup_login.js ├── keepassxc-protocol.md ├── package-lock.json ├── package.json ├── playwright.config.ts └── tests ├── .eslintrc ├── assert.js ├── content-scripts.spec.ts ├── global-setup.ts ├── global-teardown.ts ├── global.spec.ts ├── page.spec.ts ├── scripts ├── div1.js ├── div2.js ├── div3.js └── div4.js ├── tests.html └── tests.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | end_of_line = lf 8 | insert_final_newline = true 9 | charset = utf-8 10 | indent_style = space 11 | max_line_length = 120 12 | 13 | # Get rid of whitespace to avoid diffs with a bunch of EOL changes 14 | trim_trailing_whitespace = true 15 | 16 | [*.{html,json}] 17 | indent_size = 2 18 | 19 | [*.{js,css}] 20 | indent_size = 4 21 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [varjolintu] 2 | patreon: keepassxcbrowser 3 | open_collective: keepassxc 4 | liberapay: keepassxc 5 | custom: ["https://keepassxc.org/donate"] 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: Provide information about a problem you are experiencing. 3 | type: Bug 4 | 5 | body: 6 | - type: checkboxes 7 | attributes: 8 | label: Have you searched for an existing issue? 9 | description: | 10 | Use the issue search box to see if one already exists for the bug you encountered. 11 | Also take a moment to review our pinned issues, and please check the 12 | troubleshooting guide: 13 | https://github.com/keepassxreboot/keepassxc-browser/wiki/Troubleshooting-guide 14 | options: 15 | - label: Yes, I tried searching and reviewed the pinned issues 16 | required: true 17 | 18 | - type: textarea 19 | id: summary 20 | attributes: 21 | label: Brief Summary 22 | description: | 23 | Provide an overview of the problem, include any information that may help us triage this issue. 24 | Provide screenshots and logs if possible, but do NOT show sensitive data. 25 | validations: 26 | required: true 27 | 28 | - type: textarea 29 | id: expected_vs_actual 30 | attributes: 31 | label: Expected Versus Actual Behavior 32 | description: Tell us what you expected to happen and what actually happened. 33 | 34 | - type: textarea 35 | id: steps 36 | attributes: 37 | label: Steps to Reproduce 38 | description: Provide a simple set of steps to reproduce this bug. 39 | placeholder: | 40 | 1. 41 | 2. 42 | 3. 43 | validations: 44 | required: true 45 | 46 | - type: textarea 47 | id: debug_info 48 | attributes: 49 | label: KeePassXC-Browser Debug Information 50 | placeholder: "Paste the output of: Extension settings -> About -> Copy debug info to clipboard." 51 | render: Text 52 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Tell us about a new feature you want. 3 | type: Feature 4 | 5 | body: 6 | - type: checkboxes 7 | attributes: 8 | label: Have you searched for an existing feature request? 9 | description: Use the issue search box to see if one already exists for the feature you want. 10 | options: 11 | - label: Yes, I tried searching 12 | required: true 13 | 14 | - type: textarea 15 | id: summary 16 | attributes: 17 | label: Brief Summary 18 | description: | 19 | Provide an overview of the feature you are interested in adding. 20 | Provide screenshots if possible, but do NOT show sensitive data. 21 | validations: 22 | required: true 23 | 24 | - type: textarea 25 | id: example 26 | attributes: 27 | label: Example 28 | description: Provide an example of how this feature would be used. 29 | 30 | - type: textarea 31 | id: context 32 | attributes: 33 | label: Context 34 | description: Why does this feature matter to you? What unique circumstances do you have? 35 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | [NOTE]: # ( Describe your changes in detail, why is this change required? ) 2 | [NOTE]: # ( Explain large or complex code modifications. ) 3 | [NOTE]: # ( If it fixes an open issue, please add "Fixes #XXX". ) 4 | 5 | 6 | ## Screenshots or videos 7 | [NOTE]: # ( Use if available. ) 8 | [NOTE]: # ( Do not include screenshots of your actual database credentials! ) 9 | 10 | 11 | ## Testing strategy 12 | [NOTE]: # ( Please describe in detail how you tested your changes. ) 13 | [TIP]: # ( Also describe how to test the changes manually. ) 14 | 15 | 16 | ## Type of change 17 | [NOTE]: # ( Please remove all lines which don't apply. ) 18 | - ✅ Bug fix (non-breaking change that fixes an issue) 19 | - ✅ New feature (change that adds functionality) 20 | - ✅ Breaking change (causes existing functionality to change) 21 | - ✅ Refactor (significant modification to existing code) 22 | - ✅ Documentation (non-code change) 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | 3 | *.swp 4 | *~ 5 | *.tmp 6 | 7 | *.zip 8 | *.crx 9 | .bz 10 | .bz2 11 | .xz 12 | node_modules 13 | test-results 14 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # This repository does not use prettier for formatting but if you want to use it for testing then you can comment out the following line: 2 | * 3 | 4 | # Should never format these files: 5 | *.min.js 6 | *.min.css 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "bracketSpacing": true, 4 | "singleQuote": true, 5 | "tabWidth": 4, 6 | "trailingComma": "all" 7 | } 8 | -------------------------------------------------------------------------------- /.tx/config: -------------------------------------------------------------------------------- 1 | [main] 2 | host = https://www.transifex.com 3 | 4 | [o:keepassxc:p:keepassxc-browser:r:messagesjson] 5 | file_filter = keepassxc-browser/_locales//messages.json 6 | source_file = keepassxc-browser/_locales/en/messages.json 7 | type = CHROME 8 | minimum_perc = 60 9 | resource_name = messages.json 10 | 11 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.rulers": [ 3 | 120 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # KeePassXC-Browser 2 | 3 | Browser extension for [KeePassXC](https://keepassxc.org/) with [Native Messaging](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Native_messaging). 4 | 5 | Based on [pfn](https://github.com/pfn)'s [chromeIPass](https://github.com/pfn/passifox). 6 | Some changes merged also from [smorks](https://github.com/smorks)' [KeePassHttp-Connector](https://github.com/smorks/keepasshttp-connector). 7 | 8 | ## Download and use 9 | 10 | This browser extension was first supported in KeePassXC 2.3.0 (release end of 2017). In general it is advised to only use the latest available release. 11 | 12 | Get the extension for [Firefox](https://addons.mozilla.org/en-US/firefox/addon/keepassxc-browser/) or [Chrome/Chromium](https://chromewebstore.google.com/detail/keepassxc-browser/oboonakemofpalcgghocfoadofidjkkk) or [Microsoft Edge](https://microsoftedge.microsoft.com/addons/detail/pdffhmdngciaglkoonimfcmckehcpafo) (requires KeePassXC 2.5.3 or newer). 13 | 14 | Please see this [document](https://keepassxc.org/docs/KeePassXC_GettingStarted.html#_browser_integration) for instructions how to configure KeePassXC in order to connect the database correctly. 15 | 16 | ## How it works 17 | 18 | KeePassXC-Browser communicates with KeePassXC through _keepassxc-proxy_. The proxy handles listening to STDIN/STDOUT 19 | and transfers these messages through Unix domain sockets / named pipes to KeePassXC. This means KeePassXC can be used and started normally without inteference from 20 | Native Messaging API. KeePassXC-Browser starts only the proxy application and there's no risk of shutting down KeePassXC or losing any unsaved changes. You don't need to install keepassxc-proxy separately. It is included in the KeePassXC application package. Alternatively you can use 21 | [keepassxc-proxy-rust](https://github.com/varjolintu/keepassxc-proxy-rust) as a proxy if you prefer a non-Qt solution. 22 | 23 | ## Requested permissions 24 | 25 | KeePassXC-Browser extension requests the following permissions: 26 | 27 | | Name | Reason | 28 | | ----- | ----- | 29 | | `activeTab` | To get URL of the current tab | 30 | | `contextMenus` | To show context menu items | 31 | | `cookies` | To access browser's internal Public Suffix List | 32 | | `clipboardWrite` | Allows password to be copied from password generator to clipboard | 33 | | `nativeMessaging` | Allows communication with KeePassXC application | 34 | | `notifications` | To show browser notifications | 35 | | `offscreen` | For accessing system theme when setting icon colors (Chrome only) | 36 | | `storage` | For storing extension settings (always stored locally in the browser, they are never synced) | 37 | | `tabs` | To request tab URL's and other info | 38 | | `webNavigation` | To show browser notifications on install or update | 39 | | `webRequest` | For handling HTTP Basic Auth | 40 | | `webRequestBlocking` | For handling HTTP Basic Auth | 41 | | `http://*/*` | To allow using KeePassXC-Browser on all websites | 42 | | `https://*/*` | To allow using KeePassXC-Browser on all websites | 43 | | `https://api.github.com/` | For checking the latest KeePassXC version from GitHub | 44 | 45 | ## Protocol 46 | 47 | Check [keepassxc-protocol](keepassxc-protocol.md) for the details about the messaging protocol used between the browser extension and KeePassXC. 48 | 49 | ## Translations 50 | 51 | Translations are managed on [Transifex](https://www.transifex.com/keepassxc/keepassxc-browser/) which offers a web interface. Please join an existing language team or request a new one if there is none. 52 | 53 | ## Development and testing 54 | 55 | See [wiki](https://github.com/keepassxreboot/keepassxc-browser/wiki/Loading-the-extension-manually). 56 | 57 | ## Help! 58 | 59 | See our [Troubleshooting Guide](https://github.com/keepassxreboot/keepassxc-browser/wiki/Troubleshooting-guide) for solving problems if previously listed issues and solutions are not working. 60 | -------------------------------------------------------------------------------- /build.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | const fs = require('@npmcli/fs'); 5 | const util = require('util'); 6 | const exec = util.promisify(require('child_process').exec); 7 | 8 | const DEST = 'keepassxc-browser'; 9 | const DEFAULT = 'manifest_default.json'; 10 | const BROWSERS = { 11 | 'Firefox': 'manifest_firefox.json', 12 | 'Chromium': 'manifest_chromium.json', 13 | }; 14 | 15 | const getVersion = async () => { 16 | const manifestFile = await fs.readFile(DEFAULT, { encoding: 'utf8' }); 17 | const data = JSON.parse(manifestFile); 18 | return data['version']; 19 | }; 20 | 21 | const setVersion = async (manifest, version) => { 22 | const manifestFile = await fs.readFile(manifest, { encoding: 'utf8' }); 23 | const data = JSON.parse(manifestFile); 24 | 25 | data['version'] = version; 26 | if (Object.hasOwn(data, 'version_name')) { 27 | data['version_name'] = version; 28 | } 29 | fs.writeFile(manifest, JSON.stringify(data, null, 4)); 30 | }; 31 | 32 | const getDestinationFilename = async (manifest, version) => { 33 | const browser = manifest.substring(manifest.indexOf('_') + 1, manifest.indexOf('.')); 34 | return `keepassxc-browser_${version}_${browser}.zip`; 35 | }; 36 | 37 | const updateTranslations = async () => { 38 | console.log('Pulling translations from Transifex, please wait...'); 39 | const { stdout } = await exec('tx pull -af'); 40 | console.log(stdout); 41 | }; 42 | 43 | const createZipFile = async (fileName, path) => { 44 | await exec(`cd ${path} && tar -a -cf ../${fileName} * && cd ..`); 45 | }; 46 | 47 | (async() => { 48 | const params = process.argv.slice(2); 49 | if (!params.includes('--skip-translations')) { 50 | await updateTranslations(); 51 | } 52 | 53 | await fs.copyFile(`${DEST}/manifest.json`, `./${DEFAULT}`); 54 | const version = await getVersion(); 55 | 56 | for (const browser in BROWSERS) { 57 | console.log(`KeePassXC-Browser: Creating extension package for ${browser}`); 58 | 59 | const fileName = await getDestinationFilename(BROWSERS[browser], version); 60 | setVersion(`./dist/${BROWSERS[browser]}`, version); 61 | await fs.copyFile(`./dist/${BROWSERS[browser]}`, `${DEST}/manifest.json`); 62 | 63 | if (await fs.exists(fileName)) { 64 | await fs.rm(fileName); 65 | } 66 | 67 | await createZipFile(fileName, DEST); 68 | console.log('Done'); 69 | } 70 | 71 | fs.renameSync(DEFAULT, `${DEST}/manifest.json`); 72 | })(); 73 | -------------------------------------------------------------------------------- /dev-resources/readme.md: -------------------------------------------------------------------------------- 1 | # Dev Resources 2 | 3 | ## Toolbar Icons 4 | 5 | The file `toolbar_icon_source.svg` contains the complete set of graphics in different layers. To create an individual icon out of it, follow this procedure: 6 | 7 | 1. Open `toolbar_icon_source.svg`, e.g. in Inkscape 8 | 2. Hide/show the layers needed to compose your desired icon 9 | 3. "Export" the icon to PNG, 36x36 pixel size (for Chrome/Edge) 10 | 4. "Save as copy" the icon as "Optimized SVG" (for Firefox) 11 | 5. Repeat for all desired icon versions 12 | 13 | **Note:** The extension will choose the icon to show by _filename_, so make sure that is correct (take a look at the existing icon sets for reference). 14 | -------------------------------------------------------------------------------- /keepassxc-browser/background/background_service.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | try { 4 | importScripts( 5 | '../common/browser-polyfill.min.js', 6 | '../common/global.js', 7 | '../common/sites.js', 8 | 'nacl.min.js', 9 | 'nacl-util.min.js', 10 | 'client.js', 11 | 'keepass.js', 12 | 'httpauth.js', 13 | 'offscreen.js', 14 | 'browserAction.js', 15 | 'page.js', 16 | 'event.js', 17 | 'init.js' 18 | ); 19 | } catch (e) { 20 | console.log('Cannot import background scripts: ', e); 21 | } 22 | -------------------------------------------------------------------------------- /keepassxc-browser/background/browserAction.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const browserActionWrapper = browser.action || browser.browserAction; 4 | const browserAction = {}; 5 | 6 | browserAction.show = async function(tab, popupData) { 7 | popupData ??= page.popupData; 8 | page.popupData = popupData; 9 | 10 | browserActionWrapper.setIcon({ 11 | path: await browserAction.generateIconName(popupData.iconType) 12 | }); 13 | 14 | if (popupData.popup && tab?.id) { 15 | browserActionWrapper.setPopup({ 16 | tabId: tab.id, 17 | popup: `popups/${popupData.popup}.html` 18 | }); 19 | 20 | let badgeText = ''; 21 | if (popupData.popup === 'popup_login') { 22 | badgeText = page.tabs[tab.id]?.loginList?.length; 23 | } else if (popupData.popup === 'popup_httpauth') { 24 | badgeText = page.tabs[tab.id]?.loginList?.logins?.length; 25 | } 26 | 27 | browserAction.setBadgeText(tab?.id, badgeText); 28 | } 29 | }; 30 | 31 | browserAction.showDefault = async function(tab) { 32 | const popupData = { 33 | iconType: 'normal', 34 | popup: 'popup' 35 | }; 36 | 37 | const response = await keepass.isConfigured().catch((err) => { 38 | logError('Cannot show default popup: ' + err); 39 | }); 40 | 41 | if (!response && !keepass.isKeePassXCAvailable) { 42 | popupData.iconType = 'cross'; 43 | } else if (!keepass.isAssociated() && !keepass.isDatabaseClosed) { 44 | popupData.iconType = 'bang'; 45 | } else if (keepass.isKeePassXCAvailable && keepass.isDatabaseClosed) { 46 | popupData.iconType = 'locked'; 47 | } 48 | 49 | // Get the current tab if no tab given 50 | tab ??= await getCurrentTab(); 51 | if (!tab) { 52 | return; 53 | } 54 | 55 | if (page?.tabs[tab.id]?.loginList.length > 0) { 56 | popupData.iconType = 'normal'; 57 | popupData.popup = 'popup_login'; 58 | browserAction.setBadgeText(tab?.id, page.tabs[tab.id]?.loginList.length); 59 | } 60 | 61 | await browserAction.show(tab, popupData); 62 | }; 63 | 64 | browserAction.setBadgeText = function(tabId, badgeText) { 65 | if (!tabId) { 66 | return; 67 | } 68 | 69 | browserActionWrapper.setBadgeBackgroundColor({ color: '#666666' }); 70 | browserActionWrapper.setBadgeText({ text: String(badgeText), tabId: tabId }); 71 | }; 72 | 73 | browserAction.generateIconName = async function(iconType) { 74 | let name = 'icon_'; 75 | name += (await keepass.keePassXCUpdateAvailable()) ? 'new_' : ''; 76 | name += (!iconType || iconType === 'normal') ? 'normal' : iconType; 77 | 78 | let style = 'colored'; 79 | if (page?.settings?.useMonochromeToolbarIcon) { 80 | if (page.settings.colorTheme === 'system') { 81 | style = await retrieveColorScheme(); 82 | } else { 83 | style = page.settings.colorTheme; 84 | } 85 | } 86 | const filetype = (isFirefox() ? 'svg' : 'png'); 87 | return `/icons/toolbar/${style}/${name}.${filetype}`; 88 | }; 89 | 90 | browserAction.ignoreSite = async function(url) { 91 | await browser.windows.getCurrent(); 92 | const tab = await getCurrentTab(); 93 | 94 | // Send the message to the current tab's content script 95 | if (tab?.id) { 96 | browser.tabs.sendMessage(tab.id, { 97 | action: 'ignore_site', 98 | args: [ url ] 99 | }); 100 | } 101 | }; 102 | -------------------------------------------------------------------------------- /keepassxc-browser/background/httpauth.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const httpAuth = {}; 4 | 5 | httpAuth.requests = []; 6 | httpAuth.pendingCallbacks = []; 7 | 8 | httpAuth.init = function() { 9 | let handleReq = httpAuth.handleRequestPromise; 10 | let reqType = 'blocking'; 11 | 12 | if (!isFirefox()) { 13 | handleReq = httpAuth.handleRequestCallback; 14 | reqType = 'asyncBlocking'; 15 | } 16 | 17 | if (browser.webRequest.onAuthRequired.hasListener(handleReq)) { 18 | browser.webRequest.onAuthRequired.removeListener(handleReq); 19 | browser.webRequest.onCompleted.removeListener(httpAuth.requestCompleted); 20 | browser.webRequest.onErrorOccurred.removeListener(httpAuth.requestCompleted); 21 | } 22 | 23 | // Only intercept http auth requests if the option is turned on. 24 | if (page.settings.autoFillAndSend) { 25 | const opts = { urls: [ '' ] }; 26 | 27 | browser.webRequest.onAuthRequired.addListener(handleReq, opts, [ reqType ]); 28 | browser.webRequest.onCompleted.addListener(httpAuth.requestCompleted, opts); 29 | browser.webRequest.onErrorOccurred.addListener(httpAuth.requestCompleted, opts); 30 | } 31 | }; 32 | 33 | httpAuth.requestCompleted = function(details) { 34 | const index = httpAuth.requests.indexOf(details.requestId); 35 | if (index >= 0) { 36 | httpAuth.requests.splice(index, 1); 37 | } 38 | }; 39 | 40 | httpAuth.handleRequestPromise = function(details) { 41 | return new Promise((resolve, reject) => { 42 | httpAuth.processPendingCallbacks(details, resolve, reject); 43 | }); 44 | }; 45 | 46 | httpAuth.handleRequestCallback = function(details, callback) { 47 | httpAuth.processPendingCallbacks(details, callback, callback); 48 | }; 49 | 50 | httpAuth.retrieveCredentials = async function(tabId, url, submitUrl) { 51 | return await keepass.retrieveCredentials(tabId, [ url, submitUrl, false, true ]).catch((err) => { 52 | logError('httpAuth.retrieveCredentials error: ' + err); 53 | return Promise.reject(); 54 | }); 55 | }; 56 | 57 | httpAuth.processPendingCallbacks = async function(details, resolve, reject) { 58 | if (httpAuth.requests.indexOf(details.requestId) >= 0 || !page.tabs[details.tabId]) { 59 | reject({ cancel: false }); 60 | return; 61 | } 62 | 63 | httpAuth.requests.push(details.requestId); 64 | 65 | if (details.challenger) { 66 | // Non-HTTP proxies are possible with PAC scripts, while currently only 67 | // Firefox provides info about the proxy protocol used [1]. 68 | // [1] https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/webRequest/onAuthRequired 69 | const scheme = details.proxyInfo ? details.proxyInfo.type : 'http'; 70 | details.proxyUrl = scheme + '://' + details.challenger.host; 71 | } 72 | 73 | details.searchUrl = (details.isProxy && details.proxyUrl) ? details.proxyUrl : details.url; 74 | 75 | const logins = await httpAuth.retrieveCredentials({ 'id': details.tabId }, details.searchUrl, details.searchUrl); 76 | httpAuth.loginOrShowCredentials(logins, details, resolve, reject); 77 | }; 78 | 79 | httpAuth.loginOrShowCredentials = function(logins, details, resolve, reject) { 80 | // At least one login found --> use first to login 81 | if (logins.length > 0 && page.settings.autoFillAndSend) { 82 | if (logins.length === 1) { 83 | resolve({ 84 | authCredentials: { 85 | username: logins[0].login, 86 | password: logins[0].password 87 | } 88 | }); 89 | } else { 90 | if (page.settings.showNotifications) { 91 | showNotification(tr('multipleCredentialsDetected')); 92 | } 93 | kpxcEvent.onHTTPAuthPopup({ 'id': details.tabId }, { 'logins': logins, 'url': details.searchUrl, 'resolve': resolve }); 94 | } 95 | } else { 96 | logError('No logins found for HTTP Basic Auth.'); 97 | reject({ cancel: false }); // No logins found 98 | } 99 | }; 100 | -------------------------------------------------------------------------------- /keepassxc-browser/background/nacl-util.min.js: -------------------------------------------------------------------------------- 1 | !function(e,n){"use strict";"undefined"!=typeof module&&module.exports?module.exports=n():e.nacl?e.nacl.util=n():(e.nacl={},e.nacl.util=n())}(this,function(){"use strict";function e(e){if(!/^(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?$/.test(e))throw new TypeError("invalid encoding")}var n={};return n.decodeUTF8=function(e){if("string"!=typeof e)throw new TypeError("expected string");var n,r=unescape(encodeURIComponent(e)),t=new Uint8Array(r.length);for(n=0;n a === e.target); 12 | if (field) { 13 | await this.showList(field); 14 | this.updateSearch(); 15 | } 16 | }; 17 | 18 | CredentialAutocomplete.prototype.itemClick = async function(e, input, uuid) { 19 | if (!e.isTrusted) { 20 | return; 21 | } 22 | 23 | e.stopPropagation(); 24 | 25 | const index = Array.prototype.indexOf.call(e.currentTarget.parentElement.childNodes, e.currentTarget); 26 | const usernameValue = e.currentTarget.getElementsByTagName('input')[0]?.value; 27 | await this.fillPassword(usernameValue, index, uuid); 28 | 29 | this.closeList(); 30 | input.focus(); 31 | }; 32 | 33 | CredentialAutocomplete.prototype.itemEnter = async function(index, item) { 34 | const usernameValue = item?.getElementsByTagName('input')[0].value; 35 | const uuid = item?.getAttribute('uuid'); 36 | this.fillPassword(usernameValue, index, uuid); 37 | }; 38 | 39 | CredentialAutocomplete.prototype.fillPassword = async function(value, index, uuid) { 40 | const combination = await kpxcFields.getCombination(this.input); 41 | combination.loginId = index; 42 | 43 | await sendMessage('page_set_login_id', uuid); 44 | 45 | const manualFill = await sendMessage('page_get_manual_fill'); 46 | await kpxcFill.fillInCredentials(combination, value, uuid, manualFill === ManualFill.PASSWORD); 47 | }; 48 | 49 | const kpxcUserAutocomplete = new CredentialAutocomplete(); 50 | -------------------------------------------------------------------------------- /keepassxc-browser/content/icons.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @Object kpxcIcons 5 | * Icon handling. 6 | */ 7 | const kpxcIcons = {}; 8 | kpxcIcons.icons = []; 9 | kpxcIcons.iconTypes = { USERNAME: 0, PASSWORD: 1, TOTP: 2 }; 10 | 11 | // Adds an icon to input field 12 | kpxcIcons.addIcon = async function(field, iconType) { 13 | if (!field || iconType < 0 || iconType > 2) { 14 | return; 15 | } 16 | 17 | let iconSet = false; 18 | if (iconType === kpxcIcons.iconTypes.USERNAME && kpxcUsernameIcons.isValid(field)) { 19 | kpxcUsernameIcons.newIcon(field, kpxc.databaseState); 20 | iconSet = true; 21 | } else if (iconType === kpxcIcons.iconTypes.PASSWORD && kpxcPasswordIcons.isValid(field)) { 22 | kpxcPasswordIcons.newIcon(field, kpxc.databaseState); 23 | iconSet = true; 24 | } else if (iconType === kpxcIcons.iconTypes.TOTP && kpxcTOTPIcons.isValid(field)) { 25 | kpxcTOTPIcons.newIcon(field, kpxc.databaseState); 26 | iconSet = true; 27 | } 28 | 29 | if (iconSet) { 30 | kpxcIcons.icons.push({ 31 | field: field, 32 | iconType: iconType 33 | }); 34 | } 35 | }; 36 | 37 | // Adds all icons from a form struct 38 | kpxcIcons.addIconsFromForm = async function(form) { 39 | const addUsernameIcons = async function(c) { 40 | if (kpxc.settings.showLoginFormIcon && await kpxc.passwordFilledWithExceptions(c) === false) { 41 | // Special case where everything else has been hidden, but a single password field is now displayed. 42 | // For example PayPal and Amazon is handled like this. 43 | if (c.username && !c.password && c.passwordInputs.length === 1) { 44 | kpxcIcons.addIcon(c.passwordInputs[0], kpxcIcons.iconTypes.USERNAME); 45 | } 46 | 47 | if (c.username && !c.username.readOnly) { 48 | kpxcIcons.addIcon(c.username, kpxcIcons.iconTypes.USERNAME); 49 | } else if (c.password && (!c.username || (c.username && c.username.readOnly))) { 50 | // Single password field 51 | kpxcIcons.addIcon(c.password, kpxcIcons.iconTypes.USERNAME); 52 | } 53 | } 54 | }; 55 | 56 | const addPasswordIcons = async function(c) { 57 | // Show password icons also with forms without any username field 58 | if (kpxc.settings.usePasswordGeneratorIcons 59 | && ((c.username && c.password) || (!c.username && c.passwordInputs.length > 0))) { 60 | for (const input of c.passwordInputs) { 61 | kpxcIcons.addIcon(input, kpxcIcons.iconTypes.PASSWORD); 62 | } 63 | } 64 | }; 65 | 66 | const addTOTPIcons = async function(c) { 67 | if (c.totp && kpxc.settings.showOTPIcon) { 68 | kpxcIcons.addIcon(c.totp, kpxcIcons.iconTypes.TOTP); 69 | } 70 | }; 71 | 72 | await Promise.all([ 73 | await addUsernameIcons(form), 74 | await addPasswordIcons(form), 75 | await addTOTPIcons(form) 76 | ]); 77 | }; 78 | 79 | // Delete all icons that have been hidden from the page view 80 | kpxcIcons.deleteHiddenIcons = function() { 81 | kpxcUsernameIcons.deleteHiddenIcons(); 82 | kpxcPasswordIcons.deleteHiddenIcons(); 83 | kpxcTOTPIcons.deleteHiddenIcons(); 84 | }; 85 | 86 | // Initializes all icons needed to be shown 87 | kpxcIcons.initIcons = async function(combinations = []) { 88 | if (combinations.length === 0) { 89 | return; 90 | } 91 | 92 | for (const form of kpxcForm.savedForms) { 93 | await kpxcIcons.addIconsFromForm(form); 94 | } 95 | 96 | // Check for other combinations that are not in any form, 97 | // or there's a form that wasn't present in savedForms (and it's not null) 98 | for (const c of combinations) { 99 | if (!c.form || (c.form && !kpxcForm.savedForms.some(sf => sf.form === c.form))) { 100 | await kpxcIcons.addIconsFromForm(c); 101 | } 102 | } 103 | }; 104 | 105 | kpxcIcons.hasIcon = function(field) { 106 | return !field ? false : kpxcIcons.icons.some(i => i.field === field); 107 | }; 108 | 109 | // Sets the icons to corresponding database lock status 110 | kpxcIcons.switchIcons = async function() { 111 | const uuid = await sendMessage('page_get_login_id'); 112 | 113 | kpxcUsernameIcons.switchIcon(kpxc.databaseState, uuid); 114 | kpxcPasswordIcons.switchIcon(kpxc.databaseState, uuid); 115 | kpxcTOTPIcons.switchIcon(kpxc.databaseState, uuid); 116 | }; 117 | -------------------------------------------------------------------------------- /keepassxc-browser/content/passkeys-inject.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const PASSKEYS_NO_LOGINS_FOUND = 15; 4 | const PASSKEYS_CREDENTIAL_IS_EXCLUDED = 21; 5 | const PASSKEYS_WAIT_FOR_LIFETIMER = 30; 6 | 7 | // Apply a script to the page for intercepting Passkeys (WebAuthn) requests 8 | const enablePasskeys = async function() { 9 | const passkeysLogDebug = function(message, extra) { 10 | if (kpxcPasskeysUtils.debugLogging) { 11 | debugLogMessage(message, extra); 12 | } 13 | }; 14 | 15 | const passkeys = document.createElement('script'); 16 | passkeys.src = chrome.runtime.getURL('content/passkeys.js'); 17 | document.documentElement.appendChild(passkeys); 18 | 19 | const startTimer = function(timeout) { 20 | return setTimeout(() => { 21 | throw new DOMException('lifetimeTimer has expired', 'NotAllowedError'); 22 | }, timeout); 23 | }; 24 | 25 | const stopTimer = function(lifetimeTimer) { 26 | if (lifetimeTimer) { 27 | clearTimeout(lifetimeTimer); 28 | } 29 | }; 30 | 31 | const letTimerRunOut = function (errorCode) { 32 | return ( 33 | errorCode === PASSKEYS_WAIT_FOR_LIFETIMER || 34 | errorCode === PASSKEYS_CREDENTIAL_IS_EXCLUDED || 35 | errorCode === PASSKEYS_NO_LOGINS_FOUND 36 | ); 37 | }; 38 | 39 | const sendResponse = async function(command, publicKey, callback) { 40 | const lifetimeTimer = startTimer(publicKey?.timeout); 41 | 42 | const ret = await chrome.runtime.sendMessage({ action: command, args: [ publicKey, window.location.origin ] }); 43 | if (ret) { 44 | let errorMessage; 45 | if (ret.response && ret.response.errorCode) { 46 | errorMessage = await chrome.runtime.sendMessage({ 47 | action: 'get_error_message', 48 | args: ret.response.errorCode, 49 | }); 50 | kpxcUI.createNotification('error', errorMessage); 51 | 52 | if (kpxcPasskeysUtils.passkeysFallback) { 53 | kpxcPasskeysUtils.sendPasskeysResponse(undefined, ret.response?.errorCode, errorMessage); 54 | } else if (letTimerRunOut(ret?.response?.errorCode)) { 55 | return; 56 | } 57 | } 58 | 59 | passkeysLogDebug('Passkey response', ret.response); 60 | kpxcPasskeysUtils.sendPasskeysResponse(ret.response, ret.response?.errorCode, errorMessage); 61 | stopTimer(lifetimeTimer); 62 | } 63 | }; 64 | 65 | document.addEventListener('kpxc-passkeys-request', async (ev) => { 66 | if (!window.isSecureContext) { 67 | kpxcUI.createNotification('error', tr('errorMessagePasskeysContextIsNotSecure')); 68 | return; 69 | } 70 | 71 | if (ev.detail.action === 'passkeys_create') { 72 | const publicKey = kpxcPasskeysUtils.buildCredentialCreationOptions( 73 | ev.detail.publicKey, 74 | ev.detail.sameOriginWithAncestors, 75 | ); 76 | passkeysLogDebug('Passkey request', publicKey); 77 | await sendResponse('passkeys_register', publicKey); 78 | } else if (ev.detail.action === 'passkeys_get') { 79 | const publicKey = kpxcPasskeysUtils.buildCredentialRequestOptions( 80 | ev.detail.publicKey, 81 | ev.detail.sameOriginWithAncestors, 82 | ); 83 | passkeysLogDebug('Passkey request', publicKey); 84 | await sendResponse('passkeys_get', publicKey); 85 | } 86 | }); 87 | }; 88 | 89 | const initContent = async () => { 90 | if (document?.documentElement?.ownerDocument?.contentType !== 'text/html' 91 | && document?.documentElement?.ownerDocument?.contentType !== 'application/xhtml+xml' 92 | ) { 93 | return; 94 | } 95 | 96 | const settings = await chrome.runtime.sendMessage({ action: 'load_settings' }); 97 | if (!settings) { 98 | console.log('Error: Cannot load extension settings'); 99 | return; 100 | } 101 | 102 | if (await chrome.runtime.sendMessage({ action: 'is_site_ignored', args: window.self.location.href })) { 103 | console.log('This site is ignored in Site Preferences.'); 104 | return; 105 | } 106 | 107 | if (settings.passkeys) { 108 | kpxcPasskeysUtils.debugLogging = settings?.debugLogging; 109 | kpxcPasskeysUtils.passkeysFallback = settings?.passkeysFallback; 110 | enablePasskeys(); 111 | } 112 | }; 113 | 114 | initContent(); 115 | -------------------------------------------------------------------------------- /keepassxc-browser/content/passkeys-utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const MINIMUM_TIMEOUT = 15000; 4 | const DEFAULT_TIMEOUT = 30000; 5 | const DISCOURAGED_TIMEOUT = 120000; 6 | 7 | // From ArrayBuffer to URL encoded base64 string 8 | const kpxcArrayBufferToBase64 = function(buf) { 9 | const str = [ ...new Uint8Array(buf) ].map(c => String.fromCharCode(c)).join(''); 10 | return window.btoa(str).replaceAll('+', '-').replaceAll('/', '_').replaceAll('=', ''); 11 | }; 12 | 13 | const checkErrors = function(pkOptions, sameOriginWithAncestors) { 14 | if (!pkOptions) { 15 | throw new Error('No publicKey configuration options were provided'); 16 | } 17 | 18 | if (pkOptions.signal && pkOptions.signal.aborted) { 19 | throw new DOMException('Abort signalled', DOMException.AbortError); 20 | } 21 | 22 | if (!sameOriginWithAncestors) { 23 | throw new DOMException('Cross-origin register or authentication is not allowed.', DOMException.NotAllowedError); 24 | } 25 | 26 | if (pkOptions.challenge.length < 16) { 27 | throw new TypeError('challenge is shorter than required minimum length.'); 28 | } 29 | }; 30 | 31 | const getTimeout = function(userVerification, timeout) { 32 | if (!timeout || Number(timeout) === 0 || isNaN(Number(timeout))) { 33 | return userVerification === 'discouraged' ? DISCOURAGED_TIMEOUT : DEFAULT_TIMEOUT; 34 | } 35 | 36 | // Note: A suggested reasonable range for the timeout member of options is 15 seconds to 120 seconds. 37 | if (Number(timeout) < MINIMUM_TIMEOUT || Number(timeout) > DISCOURAGED_TIMEOUT) { 38 | return DEFAULT_TIMEOUT; 39 | } 40 | 41 | return Number(timeout); 42 | }; 43 | 44 | const kpxcPasskeysUtils = {}; 45 | 46 | // Sends response from KeePassXC back to the injected script 47 | kpxcPasskeysUtils.sendPasskeysResponse = function(publicKey, errorCode, errorMessage) { 48 | const response = errorCode 49 | ? { errorCode: errorCode, errorMessage: errorMessage, fallback: kpxcPasskeysUtils?.passkeysFallback } 50 | : { publicKey: publicKey, fallback: kpxcPasskeysUtils?.passkeysFallback }; 51 | const details = isFirefox() ? cloneInto(response, document.defaultView) : response; 52 | document.dispatchEvent(new CustomEvent('kpxc-passkeys-response', { detail: details })); 53 | }; 54 | 55 | // Create a new object with base64 strings for KeePassXC 56 | kpxcPasskeysUtils.buildCredentialCreationOptions = function(pkOptions, sameOriginWithAncestors) { 57 | try { 58 | checkErrors(pkOptions, sameOriginWithAncestors); 59 | 60 | const publicKey = {}; 61 | publicKey.attestation = pkOptions?.attestation; 62 | publicKey.authenticatorSelection = pkOptions?.authenticatorSelection; 63 | publicKey.challenge = kpxcArrayBufferToBase64(pkOptions.challenge); 64 | publicKey.extensions = pkOptions?.extensions; 65 | 66 | // Make sure integers are used for "alg". Set to reserved if not found. 67 | // https://www.iana.org/assignments/cose/cose.xhtml#algorithms 68 | publicKey.pubKeyCredParams = []; 69 | if (pkOptions.pubKeyCredParams) { 70 | for (const credParam of pkOptions.pubKeyCredParams) { 71 | publicKey.pubKeyCredParams.push({ 72 | type: credParam?.type, 73 | alg: credParam.alg ? Number(credParam.alg) : 0 74 | }); 75 | } 76 | } 77 | 78 | publicKey.rp = pkOptions?.rp; 79 | publicKey.timeout = getTimeout(publicKey?.authenticatorSelection?.userVerification, pkOptions?.timeout); 80 | 81 | publicKey.excludeCredentials = []; 82 | if (pkOptions.excludeCredentials && pkOptions.excludeCredentials.length > 0) { 83 | for (const cred of pkOptions.excludeCredentials) { 84 | publicKey.excludeCredentials.push({ 85 | id: kpxcArrayBufferToBase64(cred.id), 86 | transports: cred.transports, 87 | type: cred.type 88 | }); 89 | } 90 | } 91 | 92 | publicKey.user = {}; 93 | publicKey.user.displayName = pkOptions.user.displayName; 94 | publicKey.user.id = kpxcArrayBufferToBase64(pkOptions.user.id); 95 | publicKey.user.name = pkOptions.user.name; 96 | 97 | return publicKey; 98 | } catch (e) { 99 | console.log(e); 100 | } 101 | }; 102 | 103 | // Create a new object with base64 strings for KeePassXC 104 | kpxcPasskeysUtils.buildCredentialRequestOptions = function(pkOptions, sameOriginWithAncestors) { 105 | try { 106 | checkErrors(pkOptions, sameOriginWithAncestors); 107 | 108 | const publicKey = {}; 109 | publicKey.challenge = kpxcArrayBufferToBase64(pkOptions.challenge); 110 | publicKey.enterpriseAttestationPossible = false; 111 | publicKey.extensions = pkOptions?.extensions; 112 | publicKey.rpId = pkOptions?.rpId; 113 | publicKey.timeout = getTimeout(publicKey?.userVerification, pkOptions?.timeout); 114 | publicKey.userVerification = pkOptions?.userVerification; 115 | 116 | publicKey.allowCredentials = []; 117 | if (pkOptions.allowCredentials && pkOptions.allowCredentials.length > 0) { 118 | for (const cred of pkOptions.allowCredentials) { 119 | const transports = []; 120 | if (cred.transports) { 121 | for (const tp of cred.transports) { 122 | transports.push(tp); 123 | } 124 | } 125 | 126 | const arr = { 127 | id: kpxcArrayBufferToBase64(cred.id), 128 | transports: [ ...transports, 'internal' ], 129 | type: cred.type 130 | }; 131 | 132 | publicKey.allowCredentials.push(arr); 133 | } 134 | } 135 | 136 | return publicKey; 137 | } catch (e) { 138 | console.log(e); 139 | } 140 | }; 141 | -------------------------------------------------------------------------------- /keepassxc-browser/content/pwgen.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const kpxcPasswordIcons = {}; 4 | kpxcPasswordIcons.icons = []; 5 | 6 | kpxcPasswordIcons.newIcon = function(field, databaseState = DatabaseState.DISCONNECTED) { 7 | kpxcPasswordIcons.icons.push(new PasswordIcon(field, databaseState)); 8 | }; 9 | 10 | kpxcPasswordIcons.switchIcon = function(state) { 11 | kpxcPasswordIcons.icons.forEach(u => u.switchIcon(state)); 12 | }; 13 | 14 | kpxcPasswordIcons.deleteHiddenIcons = function() { 15 | kpxcUI.deleteHiddenIcons(kpxcPasswordIcons.icons); 16 | }; 17 | 18 | kpxcPasswordIcons.isValid = function(field) { 19 | if (!field 20 | || field.readOnly 21 | || field.offsetWidth < MIN_INPUT_FIELD_OFFSET_WIDTH 22 | || kpxcIcons.hasIcon(field) 23 | || !kpxcFields.isVisible(field)) { 24 | return false; 25 | } 26 | 27 | return true; 28 | }; 29 | 30 | 31 | class PasswordIcon extends Icon { 32 | constructor(field, databaseState = DatabaseState.DISCONNECTED) { 33 | super(field, databaseState); 34 | this.nextFieldExists = false; 35 | 36 | this.initField(field); 37 | kpxcUI.monitorIconPosition(this); 38 | } 39 | } 40 | 41 | PasswordIcon.prototype.initField = function(field) { 42 | // Observer the visibility 43 | if (this.observer) { 44 | this.observer.observe(field); 45 | } 46 | 47 | this.createIcon(field); 48 | this.inputField = field; 49 | }; 50 | 51 | PasswordIcon.prototype.createIcon = function(field) { 52 | const className = (isFirefox() ? 'key-moz' : 'key'); 53 | const size = (field.offsetHeight > 28) ? 24 : 16; 54 | const offset = kpxcUI.calculateIconOffset(field, size); 55 | 56 | const icon = kpxcUI.createElement('div', 'kpxc kpxc-pwgen-icon ' + className, 57 | { 58 | 'title': tr('passwordGeneratorGenerateText'), 59 | 'size': size, 60 | 'offset': offset, 61 | 'kpxc-pwgen-field-id': field.getAttribute('data-kpxc-id') // Needed? 62 | }); 63 | 64 | icon.style.zIndex = '10000000'; 65 | icon.style.width = Pixels(size); 66 | icon.style.height = Pixels(size); 67 | 68 | if (this.databaseState === DatabaseState.DISCONNECTED || this.databaseState === DatabaseState.LOCKED) { 69 | icon.style.filter = 'saturate(0%)'; 70 | } 71 | 72 | icon.addEventListener('click', async function(e) { 73 | if (!e.isTrusted) { 74 | return; 75 | } 76 | 77 | if (e.shiftKey) { 78 | icon.style.display = 'none'; 79 | return; 80 | } 81 | 82 | e.stopPropagation(); 83 | kpxcPasswordGenerator.showPasswordGenerator(field); 84 | }); 85 | 86 | icon.addEventListener('mousedown', ev => ev.stopPropagation()); 87 | icon.addEventListener('mouseup', ev => ev.stopPropagation()); 88 | 89 | kpxcUI.setIconPosition(icon, field, this.rtl); 90 | this.icon = icon; 91 | this.createWrapper('css/pwgen.css'); 92 | }; 93 | 94 | 95 | const kpxcPasswordGenerator = {}; 96 | 97 | kpxcPasswordGenerator.showPasswordGenerator = async function(field) { 98 | kpxcPasswordGenerator.generate(field ?? document.activeElement); 99 | }; 100 | 101 | kpxcPasswordGenerator.generate = async function(field) { 102 | if (!await isPasswordGeneratorSupported()) { 103 | kpxcUI.createNotification('error', tr('passwordGeneratorNotSupported')); 104 | return; 105 | } 106 | 107 | kpxcPasswordGenerator.fill(field, await sendMessage('generate_password')); 108 | }; 109 | 110 | kpxcPasswordGenerator.fill = function(elem, password) { 111 | if (!elem || !password) { 112 | return; 113 | } 114 | 115 | if (password.length === 0) { 116 | kpxcUI.createNotification('error', tr('usernameLockedFieldText')); 117 | return; 118 | } 119 | 120 | if (elem.getAttribute('maxlength')) { 121 | if (password.length > elem.getAttribute('maxlength')) { 122 | const message = 123 | tr('passwordGeneratorErrorTooLong') + 124 | '\r\n' + 125 | tr('passwordGeneratorErrorTooLongCut') + 126 | '\r\n' + 127 | tr('passwordGeneratorErrorTooLongRemember'); 128 | message.style.whiteSpace = 'pre'; 129 | kpxcUI.createNotification('error', message); 130 | return; 131 | } 132 | } 133 | 134 | elem.value = password; 135 | elem.dispatchEvent(new Event('input', { bubbles: true })); 136 | elem.dispatchEvent(new Event('change', { bubbles: true })); 137 | 138 | // Fill next password field if found 139 | if (kpxc.inputs.length > 0) { 140 | const index = kpxc.inputs.indexOf(elem); 141 | const next = kpxc.inputs[index + 1]; 142 | 143 | const nextField = next && next.getLowerCaseAttribute('type') === 'password' ? next : undefined; 144 | if (nextField) { 145 | nextField.value = password; 146 | nextField.dispatchEvent(new Event('input', { bubbles: true })); 147 | nextField.dispatchEvent(new Event('change', { bubbles: true })); 148 | } 149 | } 150 | }; 151 | 152 | const isPasswordGeneratorSupported = async function() { 153 | const response = await browser.runtime.sendMessage({ 154 | action: 'get_keepassxc_versions' 155 | }); 156 | 157 | const result = await browser.runtime.sendMessage({ 158 | action: 'compare_versions', 159 | args: [ [ '2.7.0' ], response.current ] 160 | }); 161 | 162 | return result['2.7.0'] || false; 163 | }; 164 | -------------------------------------------------------------------------------- /keepassxc-browser/content/totp-autocomplete.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | class TOTPAutocomplete extends Autocomplete {} 4 | TOTPAutocomplete.prototype.click = async function(e, input) { 5 | if (!e.isTrusted) { 6 | return; 7 | } 8 | 9 | await kpxc.updateTOTPList(); 10 | this.showList(input, true); 11 | }; 12 | 13 | TOTPAutocomplete.prototype.itemClick = async function(e, input, uuid) { 14 | if (!e.isTrusted) { 15 | return; 16 | } 17 | 18 | const index = Array.prototype.indexOf.call(e.currentTarget.parentElement.childNodes, e.currentTarget); 19 | await this.fillTotp(index, uuid, input); 20 | 21 | this.closeList(); 22 | input.focus(); 23 | }; 24 | 25 | TOTPAutocomplete.prototype.itemEnter = async function(index, item) { 26 | const uuid = item?.getAttribute('uuid'); 27 | this.fillTotp(index, uuid); 28 | }; 29 | 30 | TOTPAutocomplete.prototype.fillTotp = async function(index, uuid, currentInput) { 31 | const combination = await kpxcFields.getCombination(this.input, 'totp') 32 | || await kpxcFields.getCombination(this.input, 'totpInputs'); 33 | if (combination) { 34 | combination.loginId = index; 35 | } 36 | 37 | kpxcFill.fillTOTPFromUuid(this.input || currentInput, uuid); 38 | }; 39 | 40 | const kpxcTOTPAutocomplete = new TOTPAutocomplete(); 41 | -------------------------------------------------------------------------------- /keepassxc-browser/content/username-field.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const kpxcUsernameIcons = {}; 4 | kpxcUsernameIcons.icons = []; 5 | kpxcUsernameIcons.detectedFields = []; 6 | 7 | kpxcUsernameIcons.newIcon = function(field, databaseState = DatabaseState.DISCONNECTED) { 8 | kpxcUsernameIcons.icons.push(new UsernameFieldIcon(field, databaseState)); 9 | }; 10 | 11 | kpxcUsernameIcons.switchIcon = function(state) { 12 | kpxcUsernameIcons.icons.forEach(u => u.switchIcon(state)); 13 | }; 14 | 15 | kpxcUsernameIcons.deleteHiddenIcons = function() { 16 | kpxcUI.deleteHiddenIcons(kpxcUsernameIcons.icons); 17 | }; 18 | 19 | kpxcUsernameIcons.isValid = function(field) { 20 | if (!field 21 | || field.offsetWidth < MIN_INPUT_FIELD_OFFSET_WIDTH 22 | || field.readOnly 23 | || kpxcIcons.hasIcon(field) 24 | || (!kpxcFields.isCustomLoginFieldsUsed() && !kpxcFields.isVisible(field))) { 25 | return false; 26 | } 27 | 28 | return true; 29 | }; 30 | 31 | 32 | class UsernameFieldIcon extends Icon { 33 | constructor(field, databaseState = DatabaseState.DISCONNECTED) { 34 | super(field, databaseState); 35 | 36 | this.initField(field); 37 | kpxcUI.monitorIconPosition(this); 38 | } 39 | 40 | switchIcon(state) { 41 | if (!this.icon) { 42 | return; 43 | } else { 44 | this.observer.disconnect(); 45 | } 46 | 47 | this.icon.classList.remove('lock', 'lock-moz', 'unlock', 'unlock-moz', 'disconnected', 'disconnected-moz'); 48 | this.icon.classList.add(getIconClassName(state)); 49 | this.icon.title = getIconText(state); 50 | 51 | if (kpxc.credentials.length === 0 && state === DatabaseState.UNLOCKED) { 52 | this.icon.style.filter = 'saturate(0%)'; 53 | } else { 54 | this.icon.style.filter = 'saturate(100%)'; 55 | } 56 | } 57 | } 58 | 59 | UsernameFieldIcon.prototype.initField = function(field) { 60 | // Observer the visibility 61 | if (this.observer) { 62 | this.observer.observe(field); 63 | } 64 | 65 | this.createIcon(field); 66 | this.inputField = field; 67 | }; 68 | 69 | UsernameFieldIcon.prototype.createIcon = function(field) { 70 | const className = getIconClassName(this.databaseState); 71 | 72 | // Size the icon dynamically, but not greater than 24 or smaller than 14 73 | const size = Math.max(Math.min(24, field.offsetHeight - 4), 14); 74 | 75 | // Don't create the icon if the input field is too small 76 | if (field.offsetWidth < (size * 1.5) || field.offsetHeight < size) { 77 | this.observer.unobserve(field); 78 | return; 79 | } 80 | 81 | const offset = kpxcUI.calculateIconOffset(field, size); 82 | 83 | const icon = kpxcUI.createElement('div', 'kpxc kpxc-username-icon ' + className, 84 | { 85 | 'title': getIconText(this.databaseState), 86 | 'size': size, 87 | 'offset': offset, 88 | 'kpxc-pwgen-field-id': field.getAttribute('data-kpxc-id') 89 | }); 90 | icon.style.zIndex = '10000000'; 91 | icon.style.width = Pixels(size); 92 | icon.style.height = Pixels(size); 93 | 94 | icon.addEventListener('click', function(e) { 95 | if (!e.isTrusted) { 96 | return; 97 | } 98 | 99 | if (e.shiftKey) { 100 | icon.style.display = 'none'; 101 | return; 102 | } 103 | 104 | e.stopPropagation(); 105 | iconClicked(field, icon); 106 | }); 107 | 108 | icon.addEventListener('mousedown', ev => ev.stopPropagation()); 109 | icon.addEventListener('mouseup', ev => ev.stopPropagation()); 110 | 111 | kpxcUI.setIconPosition(icon, field, this.rtl); 112 | this.icon = icon; 113 | this.createWrapper('css/username.css'); 114 | }; 115 | 116 | const iconClicked = async function(field, icon) { 117 | if (!kpxcFields.isCustomLoginFieldsUsed() && !kpxcFields.isVisible(field)) { 118 | icon.parentNode.removeChild(icon); 119 | return; 120 | } 121 | 122 | // Try to reconnect if KeePassXC for the case we're not currently connected 123 | const connected = await kpxc.reconnect(); 124 | if (!connected) { 125 | return; 126 | } 127 | 128 | if (kpxc.databaseState !== DatabaseState.UNLOCKED) { 129 | // Triggers database unlock 130 | await sendMessage('page_set_manual_fill', ManualFill.BOTH); 131 | await sendMessage('get_database_hash', [ false, true ]); // Set triggerUnlock to true 132 | field.focus(); 133 | } 134 | 135 | if (icon.className.includes('unlock')) { 136 | fillCredentials(field); 137 | } 138 | }; 139 | 140 | const getIconClassName = function(state = DatabaseState.UNLOCKED) { 141 | if (state === DatabaseState.LOCKED) { 142 | return (isFirefox() ? 'lock-moz' : 'lock'); 143 | } else if (state === DatabaseState.DISCONNECTED) { 144 | return (isFirefox() ? 'disconnected-moz' : 'disconnected'); 145 | } 146 | 147 | return (isFirefox() ? 'unlock-moz' : 'unlock'); 148 | }; 149 | 150 | const getIconText = function(state) { 151 | if (state === DatabaseState.LOCKED) { 152 | return tr('usernameLockedFieldText'); 153 | } else if (state === DatabaseState.DISCONNECTED) { 154 | return tr('usernameDisconnectedFieldText'); 155 | } 156 | 157 | return kpxc.credentials.length === 0 ? tr('usernameFieldTextNoCredentials') : tr('usernameFieldText'); 158 | }; 159 | 160 | const fillCredentials = async function(field) { 161 | const combination = await kpxcFields.getCombination(field); 162 | kpxcFill.fillFromUsernameIcon(combination); 163 | }; 164 | -------------------------------------------------------------------------------- /keepassxc-browser/css/autocomplete.css: -------------------------------------------------------------------------------- 1 | .kpxcAutocomplete-container { 2 | background-color: var(--kpxc-background-color); 3 | border: var(--kpxc-autocomplete-menu-border); 4 | border-radius: 8px; 5 | box-shadow: var(--kpxc-box-shadow); 6 | box-sizing: border-box; 7 | display: none; 8 | font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 9 | font-size: 14px; 10 | line-height: 1em; 11 | min-width: 16em; 12 | overflow: hidden; /* Prevent the content from bleeding over the border radius. */ 13 | padding: 6px; 14 | position: absolute !important; 15 | scrollbar-color: --var(--kpxc-scrollbar-thumb) --var(--kpxc-scrollbar-background); 16 | z-index: 2147483646; 17 | } 18 | 19 | .kpxcAutocomplete-container--visible { 20 | display: block; 21 | } 22 | 23 | .kpxcAutocomplete-items { 24 | max-height: 250px; 25 | overflow-y: auto; 26 | } 27 | 28 | .kpxcAutocomplete-item { 29 | background-color: var(--kpxc-background-color); 30 | border-radius: 4px; 31 | color: var(--kpxc-text-color); 32 | cursor: pointer; 33 | letter-spacing: normal !important; 34 | line-height: 1.25em; 35 | padding: 12px; 36 | text-align: start !important; 37 | text-rendering: auto !important; 38 | width: auto; 39 | word-spacing: normal !important; 40 | } 41 | 42 | .kpxcAutocomplete-container--compact .kpxcAutocomplete-items { 43 | cursor: pointer; 44 | display: grid; 45 | grid-template-columns: repeat(3, auto); 46 | max-height: 250px; 47 | max-width: 600px; 48 | overflow-y: auto; 49 | overflow-x: hidden; 50 | } 51 | 52 | .kpxcAutocomplete-container--compact .kpxcAutocomplete-item { 53 | display: contents; 54 | } 55 | 56 | .kpxcAutocomplete-container--compact .kpxcAutocomplete-item__header { 57 | align-self: flex-start; 58 | border-radius: 4px 0 0 4px; 59 | display: grid; 60 | height: 14px; 61 | padding: 6px 24px 6px 6px; 62 | pointer-events: auto; 63 | } 64 | 65 | .kpxcAutocomplete-container--compact .kpxcAutocomplete-item__group { 66 | align-self: center; 67 | border-radius: 0 4px 4px 0; 68 | display: flex; 69 | height: 14px; 70 | justify-content: flex-end; 71 | padding: 6px 6px 6px 24px; 72 | pointer-events: auto; 73 | } 74 | 75 | .kpxcAutocomplete-container--compact .kpxcAutocomplete-item__value { 76 | align-self: flex-end; 77 | height: 14px; 78 | justify-self: start; 79 | margin: 0; 80 | padding: 6px 0 6px 0; 81 | pointer-events: auto; 82 | width: 100%; 83 | } 84 | 85 | /* Separate class for value if group is not shown */ 86 | .kpxcAutocomplete-container--compact .kpxcAutocomplete-item__value--last { 87 | align-self: center; 88 | border-radius: 0 4px 4px 0; 89 | color: var(--kpxc-lighter-text-color); 90 | display: flex; 91 | font-size: .85em; 92 | height: 14px; 93 | justify-content: flex-end; 94 | overflow: hidden; 95 | padding: 6px 6px 6px 24px; 96 | pointer-events: auto; 97 | text-overflow: ellipsis; 98 | white-space: nowrap; 99 | } 100 | 101 | .kpxcAutocomplete-item:last-child { 102 | border-bottom: none; 103 | } 104 | 105 | .kpxcAutocomplete-item__header { 106 | align-items: center; 107 | display: flex; 108 | flex-direction: row-reverse; 109 | gap: 6px; 110 | justify-content: space-between; 111 | } 112 | 113 | .kpxcAutocomplete-item__group { 114 | color: var(--kpxc-light-text-color); 115 | font-size: .7em; 116 | font-weight: bold; 117 | letter-spacing: 0.05em; 118 | overflow: hidden; 119 | pointer-events: none; 120 | text-overflow: ellipsis; 121 | text-transform: uppercase; 122 | white-space: nowrap; 123 | } 124 | 125 | .kpxcAutocomplete-item__label { 126 | font-size: .9em; 127 | font-weight: bold; 128 | overflow: hidden; 129 | pointer-events: none; 130 | text-overflow: ellipsis; 131 | white-space: nowrap 132 | } 133 | 134 | .kpxcAutocomplete-item__value { 135 | color: var(--kpxc-lighter-text-color); 136 | font-size: .85em; 137 | margin-top: 4px; 138 | overflow: hidden; 139 | pointer-events: none; 140 | text-overflow: ellipsis; 141 | white-space: nowrap; 142 | } 143 | 144 | .kpxcAutocomplete-item--active { 145 | background-color: #28a745 !important; 146 | color: #ffffff !important; 147 | } 148 | 149 | .kpxcAutocomplete-item--active * { 150 | background-color: #28a745 !important; 151 | color: #ffffff !important; 152 | } 153 | 154 | .kpxcAutocomplete-container footer { 155 | background-color: var(--kpxc-autocomplete-footer-color); 156 | border-radius: 0 0 4px 4px; 157 | color: var(--kpxc-lighter-text-color); 158 | font-size: max(9px, .8em) !important; 159 | padding: 5px; 160 | width: auto; 161 | } 162 | 163 | @media (prefers-color-scheme: dark) { 164 | .kpxcAutocomplete-container { 165 | border: var(--kpxc-autocomplete-menu-border); 166 | } 167 | 168 | .kpxcAutocomplete-items { 169 | background: var(--kpxc-background-color); 170 | color: var(--kpxc-text-color); 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /keepassxc-browser/css/banner.css: -------------------------------------------------------------------------------- 1 | div.kpxc-banner { 2 | animation-duration: 0.2s; 3 | animation-name: kpxc-slidein; 4 | background-color: var(--kpxc-background-color); 5 | box-sizing: border-box !important; 6 | color: var(--kpxc-text-color) !important; 7 | display: flex !important; 8 | font-size: 1em !important; 9 | height: auto !important; 10 | justify-content: space-between !important; 11 | left: 0px !important; 12 | line-height: 1 !important; 13 | margin: 0 auto !important; 14 | min-height: 2em !important; 15 | max-height: 4em !important; 16 | overflow-x: hidden !important; 17 | padding: 4px !important; 18 | position: fixed !important; 19 | width: 100% !important; 20 | } 21 | 22 | div.kpxc-banner:hover { 23 | cursor: grab; 24 | } 25 | 26 | .kpxc-banner-on-bottom { 27 | border-bottom: 1px solid #38383d !important; 28 | bottom: 0px !important; 29 | box-shadow: 0 -4px 6px 0 hsla(0, 0%, 0%, 0.2) !important; 30 | } 31 | 32 | .kpxc-banner-on-top { 33 | border-top: 1px solid #38383d !important; 34 | box-shadow: 0 4px 6px 0 hsla(0, 0%, 0%, 0.2) !important; 35 | top: 0px !important; 36 | } 37 | 38 | @keyframes kpxc-slidein { 39 | from { 40 | max-height: 0em; 41 | } 42 | 43 | to { 44 | max-height: 4em; 45 | } 46 | } 47 | 48 | div.kpxc-banner .banner-info > span { 49 | margin: 4px; 50 | } 51 | 52 | div.kpxc-banner div.banner-info, div.banner-buttons { 53 | display: inline-flex; 54 | align-items: center; 55 | justify-content: center; 56 | padding: 8px; 57 | } 58 | 59 | div.kpxc-banner .small { 60 | font-size: 80%; 61 | color: #787878; 62 | } 63 | 64 | div.kpxc-banner .kpxc-banner-icon { 65 | width: 24px; 66 | height: 24px; 67 | overflow: hidden; 68 | background: url('chrome-extension://__MSG_@@extension_id__/icons/keepassxc.svg') right no-repeat; 69 | background-size: contain; 70 | } 71 | 72 | div.kpxc-banner .kpxc-banner-icon-moz { 73 | width: 24px; 74 | height: 24px; 75 | overflow: hidden; 76 | background: url('moz-extension://__MSG_@@extension_id__/icons/keepassxc.svg') right no-repeat; 77 | background-size: contain; 78 | } 79 | 80 | div.kpxc-banner .kpxc-help-icon { 81 | width: 24px; 82 | height: 24px; 83 | overflow: hidden; 84 | background: url('chrome-extension://__MSG_@@extension_id__/icons/help.svg') right no-repeat; 85 | background-size: contain; 86 | } 87 | 88 | div.kpxc-banner .kpxc-help-icon-moz { 89 | width: 24px; 90 | height: 24px; 91 | overflow: hidden; 92 | background: url('moz-extension://__MSG_@@extension_id__/icons/help.svg') right no-repeat; 93 | background-size: contain; 94 | } 95 | 96 | .kpxc-separator { 97 | border-left: 1px solid #ccc; 98 | height: 100% !important; 99 | margin: 10px !important; 100 | } 101 | 102 | .kpxc-pick-info-text { 103 | margin-left: 8px; 104 | margin-right: 8px; 105 | } 106 | 107 | div.kpxc-banner .kpxc-checkbox { 108 | margin: 2px !important; 109 | } 110 | 111 | div.kpxc-banner label { 112 | font-family: 'Lato', sans-serif !important; 113 | font-size: 12px !important; 114 | font-weight: normal !important; 115 | margin: 4px !important; 116 | } 117 | 118 | div.kpxc-banner-dialog { 119 | background-color: var(--kpxc-background-color); 120 | border: var(--kpxc-container-border); 121 | color: var(--kpxc-text-color); 122 | display: block !important; 123 | margin: 0 auto; 124 | max-height: 60%; 125 | max-width: 460px; 126 | overflow: hidden !important; 127 | overflow-y: scroll !important; 128 | padding: 8px; 129 | position: fixed !important; 130 | width: 680px; 131 | z-index: 10000000; 132 | } 133 | 134 | div.kpxc-banner-dialog-bottom { 135 | border-radius: 4px 4px 0 0; 136 | box-shadow: 0 -4px 6px 0 hsla(0, 0%, 0%, 0.2); 137 | } 138 | 139 | div.kpxc-banner-dialog-top { 140 | border-radius: 0 0 4px 4px; 141 | box-shadow: 0 4px 6px 0 hsla(0, 0%, 0%, 0.2); 142 | } 143 | 144 | div.kpxc-banner-dialog > ul { 145 | list-style-type: disc !important; 146 | margin-bottom: 0 !important; 147 | margin-top: 0 !important; 148 | padding-left: 0 !important; 149 | } 150 | 151 | div.kpxc-banner-dialog > p > span.strong { 152 | font-weight: bold !important; 153 | } 154 | 155 | div.kpxc-banner-dialog .list-group { 156 | font-size: .9em !important; 157 | } 158 | 159 | div.kpxc-banner-dialog .list-group-item { 160 | background-color: var(--kpxc-input-background-color) !important; 161 | border: 1px solid rgba(0,0,0,.125) !important; 162 | border-bottom: 0px !important; 163 | color: var(--kpxc-text-color) !important; 164 | display: block !important; 165 | padding: 7px 14px; 166 | position: relative !important; 167 | text-decoration: none !important; 168 | } 169 | 170 | div.kpxc-banner-dialog .list-group-item:first-child { 171 | border-top-width: 1px !important; 172 | border-top-left-radius: 4px !important; 173 | border-top-right-radius: 4px !important; 174 | } 175 | 176 | div.kpxc-banner-dialog .list-group-item:last-child { 177 | border-bottom: 1px solid rgba(0,0,0,.125) !important; 178 | border-bottom-right-radius: 4px !important; 179 | border-bottom-left-radius: 4px !important; 180 | margin-bottom: 0 !important; 181 | } 182 | 183 | div.kpxc-banner-dialog .list-group-item:hover { 184 | cursor: pointer !important; 185 | background-color: var(--kpxc-table-hover-color) !important; 186 | } 187 | 188 | @media (prefers-color-scheme: dark) { 189 | div.kpxc-banner, div.kpxc-banner-dialog { 190 | background-color: var(--kpxc-input-background-color) !important; 191 | color: var(--kpxc-text-color) !important; 192 | } 193 | 194 | div.kpxc-banner-dialog .list-group-item:hover { 195 | background-color: var(--kpxc-table-hover-color) !important; 196 | color: #000; 197 | } 198 | 199 | div.kpxc-banner-dialog .list-group-item { 200 | background-color: var(--kpxc-input-background-color) !important; 201 | color: var(--kpxc-text-color) !important; 202 | } 203 | } 204 | 205 | @media print { 206 | div.kpxc-banner { 207 | visibility: hidden; 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /keepassxc-browser/css/button.css: -------------------------------------------------------------------------------- 1 | .kpxc-button { 2 | background-color: #fff; 3 | background-image: none !important; 4 | border: 1px solid #ccc !important; 5 | border-radius: .25em !important; 6 | font-size: 12px !important; 7 | font-family: 'Lato', sans-serif !important; 8 | height: 28px !important; 9 | margin: .2em !important; 10 | padding: 1px 7px 2px !important; 11 | } 12 | 13 | .kpxc-white-button { 14 | background-color: #f8f9fa !important; 15 | border-color: #f8f9fa !important; 16 | color: #212529 !important; 17 | } 18 | 19 | .kpxc-red-button { 20 | background-color: #dc3545 !important; 21 | border-color: #dc3545 !important; 22 | color: #fff !important; 23 | } 24 | 25 | .kpxc-orange-button { 26 | background-color: #ffc107 !important; 27 | border-color: #ffc107 !important; 28 | color: #212529 !important; 29 | } 30 | 31 | .kpxc-blue-button { 32 | background-color: #007bff !important; 33 | border-color: #007bff !important; 34 | color: #fff !important; 35 | } 36 | 37 | .kpxc-green-button { 38 | background-color: #28a745 !important; 39 | border-color: #28a745 !important; 40 | color: #fff !important; 41 | } 42 | 43 | .kpxc-button:hover { 44 | cursor: pointer !important; 45 | filter: brightness(92%) !important; 46 | transition: all .15s; 47 | } 48 | 49 | .kpxc-button:disabled, .kpxc-gray-button { 50 | background-color: #777777 !important; 51 | border-color: #444444 !important; 52 | color: #fff !important; 53 | } 54 | 55 | .kpxc-button:disabled:hover { 56 | background-color: #777777 !important; 57 | } 58 | -------------------------------------------------------------------------------- /keepassxc-browser/css/colors.css: -------------------------------------------------------------------------------- 1 | :root, 2 | :host { 3 | --kpxc-autocomplete-footer-color: #f2f2f2; 4 | --kpxc-autocomplete-menu-border: 1px solid #ddd; 5 | --kpxc-background-color: #fff; 6 | --kpxc-box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1); 7 | --kpxc-card-background-color: #fff; 8 | --kpxc-card-border-color: rgba(0, 0, 0, .125); 9 | --kpxc-card-header-color: #fafafa; 10 | --kpxc-checkbox-color: #5b5b5d; 11 | --kpxc-checkbox-background-color: #2b2a2a; 12 | --kpxc-container-border: 1px solid #ccc; 13 | --kpxc-input-active-border-color: #2b4d1a; 14 | --kpxc-input-background-color: #fff; 15 | --kpxc-input-border: 1px solid #2b4d1a; 16 | --kpxc-input-border-color: #292a2a; 17 | --kpxc-input-main-border-color: rgba(0, 0, 0, .125); 18 | --kpxc-link-color: #3a8233; 19 | --kpxc-link-hover-color: #429f14; 20 | --kpxc-scrollbar-background: #fcfcfc; 21 | --kpxc-scrollbar-thumb: #8b8b8b; 22 | --kpxc-sidebar-background-color: #27272a; 23 | --kpxc-table-border-color: rgb(222, 226, 230); 24 | --kpxc-table-hover-color: #f2f2f2; 25 | --kpxc-table-odd-color: #f2f2f2; 26 | --kpxc-text-color: #000; 27 | --kpxc-lighter-text-color: rgba(0, 0, 0, 0.8); 28 | --kpxc-light-text-color: rgba(0, 0, 0, 0.5); 29 | } 30 | 31 | @media (prefers-color-scheme: dark) { 32 | 33 | :root, 34 | :host { 35 | --kpxc-autocomplete-footer-color: #2b2a2a; 36 | --kpxc-autocomplete-menu-border: 1px solid #3b3b3d; 37 | --kpxc-background-color: #3b3b3d; 38 | --kpxc-card-background-color: #2b2a2a; 39 | --kpxc-card-border-color: #292a2a; 40 | --kpxc-card-header-color: #27272a; 41 | --kpxc-checkbox-color: #3b3b3d; 42 | --kpxc-checkbox-background-color: #5b5b5d; 43 | --kpxc-container-border: 1px solid #000; 44 | --kpxc-input-active-border-color: #2b4d1a; 45 | --kpxc-input-background-color: #27272a; 46 | --kpxc-input-border: 1px solid #2b4d1a; 47 | --kpxc-input-border-color: #292a2a; 48 | --kpxc-input-main-border-color: #3b3d3c; 49 | --kpxc-link-color: #6cac4d; 50 | --kpxc-link-hover-color: #429f14; 51 | --kpxc-scrollbar-background: #2c2c2c; 52 | --kpxc-scrollbar-thumb: #9f9f9f; 53 | --kpxc-table-border-color: #a0a0a0; 54 | --kpxc-table-hover-color: #474948; 55 | --kpxc-table-odd-color: #383a39; 56 | --kpxc-text-color: #cbcfcb; 57 | --kpxc-lighter-text-color: rgba(203, 207, 203, 0.8); 58 | --kpxc-light-text-color: rgba(203, 207, 203, 0.5); 59 | } 60 | } 61 | 62 | /* Same colors for manual switching */ 63 | [data-bs-theme='dark'] { 64 | --kpxc-autocomplete-footer-color: #2b2a2a; 65 | --kpxc-autocomplete-menu-border: 1px solid #3b3b3d; 66 | --kpxc-background-color: #3b3b3d; 67 | --kpxc-card-background-color: #2b2a2a; 68 | --kpxc-card-border-color: #292a2a; 69 | --kpxc-card-header-color: #27272a; 70 | --kpxc-checkbox-color: #3b3b3d; 71 | --kpxc-checkbox-background-color: #2b2a2a; 72 | --kpxc-container-border: 1px solid #000; 73 | --kpxc-input-active-border-color: #2b4d1a; 74 | --kpxc-input-background-color: #27272a; 75 | --kpxc-input-border: 1px solid #2b4d1a; 76 | --kpxc-input-border-color: #292a2a; 77 | --kpxc-input-main-border-color: #3b3d3c; 78 | --kpxc-link-color: #6cac4d; 79 | --kpxc-link-hover-color: #429f14; 80 | --kpxc-scrollbar-background: #2c2c2c; 81 | --kpxc-scrollbar-thumb: #9f9f9f; 82 | --kpxc-table-border-color: #a0a0a0; 83 | --kpxc-table-hover-color: #474948; 84 | --kpxc-table-odd-color: #383a39; 85 | --kpxc-text-color: #cbcfcb; 86 | --kpxc-lighter-text-color: rgba(203, 207, 203, 0.8); 87 | --kpxc-light-text-color: rgba(203, 207, 203, 0.5); 88 | } 89 | 90 | [data-bs-theme='light'] { 91 | --kpxc-autocomplete-footer-color: #f2f2f2; 92 | --kpxc-autocomplete-menu-border: 1px solid #ddd; 93 | --kpxc-background-color: #fff; 94 | --kpxc-box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); 95 | --kpxc-card-background-color: #fff; 96 | --kpxc-card-border-color: rgba(0, 0, 0, .125); 97 | --kpxc-card-header-color: #fafafa; 98 | --kpxc-checkbox-color: #5b5b5d; 99 | --kpxc-checkbox-background-color: #5b5b5d; 100 | --kpxc-container-border: 1px solid #ccc; 101 | --kpxc-input-active-border-color: #2b4d1a; 102 | --kpxc-input-background-color: #fff; 103 | --kpxc-input-border: 1px solid #2b4d1a; 104 | --kpxc-input-border-color: #292a2a; 105 | --kpxc-input-main-border-color: rgba(0, 0, 0, .125); 106 | --kpxc-link-color: #3a8233; 107 | --kpxc-link-hover-color: #429f14; 108 | --kpxc-scrollbar-background: #fcfcfc; 109 | --kpxc-scrollbar-thumb: #8b8b8b; 110 | --kpxc-table-border-color: rgb(222, 226, 230); 111 | --kpxc-table-hover-color: #f2f2f2; 112 | --kpxc-table-odd-color: #f2f2f2; 113 | --kpxc-text-color: #000; 114 | --kpxc-lighter-text-color: rgba(0, 0, 0, 0.8); 115 | --kpxc-light-text-color: rgba(0, 0, 0, 0.5); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /keepassxc-browser/css/define.css: -------------------------------------------------------------------------------- 1 | #kpxcDefine-fields { 2 | z-index: 2147483646; 3 | } 4 | 5 | .kpxcDefine-fixed-field { 6 | align-items: center; 7 | background-color: rgba(239,239,239,0.4); 8 | border: 2px solid #efefef; 9 | color: #fff; 10 | cursor: pointer; 11 | display: flex; 12 | font-weight: bold; 13 | justify-content: center; 14 | position: absolute; 15 | z-index: 2147483646; 16 | } 17 | 18 | .kpxcDefine-fixed-field-dark { 19 | background-color: rgba(45, 45, 45, 0.4); 20 | border: 2px solid #0f0f0f; 21 | color: #fff; 22 | } 23 | 24 | .kpxcDefine-fixed-hover-field { 25 | background-color: rgba(255, 165, 238, 0.631); 26 | border: 2px solid orange; 27 | } 28 | 29 | .kpxcDefine-fixed-hover-field-dark { 30 | background-color: rgba(255, 165, 238, 0.599); 31 | border: 2px solid orange; 32 | color: #505050; 33 | } 34 | 35 | .kpxcDefine-fixed-password-field { 36 | background-color: rgba(255,0,0,0.4); 37 | border: 2px solid red; 38 | color: #efefef; 39 | } 40 | 41 | .kpxcDefine-fixed-username-field { 42 | background-color: rgba(232, 252, 3, 0.4); 43 | border: 2px solid yellow; 44 | color: #efefef; 45 | } 46 | 47 | .kpxcDefine-fixed-totp-field { 48 | background-color: rgba(50,205,50,0.4); 49 | border: 2px solid limegreen; 50 | color: #efefef; 51 | } 52 | 53 | .kpxcDefine-fixed-string-field { 54 | background-color: rgba(30,144,255,0.4); 55 | border: 2px solid deepskyblue; 56 | color: #efefef; 57 | } 58 | 59 | .kpxcDefine-dark-text { 60 | color: #505050; 61 | } 62 | -------------------------------------------------------------------------------- /keepassxc-browser/css/notification.css: -------------------------------------------------------------------------------- 1 | .kpxc-notification { 2 | border: 1px solid transparent; 3 | border-radius: 4px; 4 | box-shadow: 0 4px 6px 0 hsla(0, 0%, 0%, 0.2); 5 | cursor: pointer; 6 | font-family: "Helvetica Neue",Helvetica,Arial,sans-serif; 7 | font-size: 14px; 8 | letter-spacing: normal !important; 9 | word-spacing: normal !important; 10 | text-rendering: auto !important; 11 | margin: 0px auto; 12 | margin-bottom: 20px; 13 | padding: 15px; 14 | padding-right: 35px; 15 | position: fixed; 16 | right: 0px; 17 | top: 0px; 18 | width: 520px; 19 | z-index: 2147483646; 20 | } 21 | 22 | .kpxc-notification span { 23 | padding: 2px; 24 | } 25 | 26 | .kpxc-notification .kpxc-banner-icon { 27 | width: 24px; 28 | height: 24px; 29 | padding: 10px; 30 | margin-right: 4px; 31 | overflow: hidden; 32 | background: url('chrome-extension://__MSG_@@extension_id__/icons/keepassxc.svg') right no-repeat; 33 | background-size: contain; 34 | } 35 | 36 | .kpxc-notification .kpxc-banner-icon-moz { 37 | width: 24px; 38 | height: 24px; 39 | padding: 10px; 40 | margin-right: 4px; 41 | overflow: hidden; 42 | background: url('moz-extension://__MSG_@@extension_id__/icons/keepassxc.svg') right no-repeat; 43 | background-size: contain; 44 | } 45 | 46 | .kpxc-notification .kpxc-label { 47 | font-weight: bold; 48 | } 49 | 50 | .kpxc-notification-success { 51 | color: #155724; 52 | background-color: #d4edda; 53 | border-color: #c3e6cb; 54 | } 55 | 56 | .kpxc-notification-warning { 57 | color: #856404; 58 | background-color: #fff3cd; 59 | border-color: #ffeeba; 60 | } 61 | 62 | .kpxc-notification-info { 63 | color: #004085; 64 | background-color: #cce5ff; 65 | border-color: #b8daff; 66 | } 67 | 68 | .kpxc-notification-error { 69 | color: #721c24; 70 | background-color: #f8d7da; 71 | border-color: #f5c6cb; 72 | } 73 | -------------------------------------------------------------------------------- /keepassxc-browser/css/pwgen.css: -------------------------------------------------------------------------------- 1 | .kpxc-pwgen-icon { 2 | cursor: pointer; 3 | position: absolute; 4 | } 5 | 6 | .kpxc-pwgen-icon.key { 7 | background: url('chrome-extension://__MSG_@@extension_id__/icons/key.svg') right no-repeat; 8 | background-size: contain; 9 | } 10 | 11 | .kpxc-pwgen-icon.key-moz { 12 | background: url('moz-extension://__MSG_@@extension_id__/icons/key.svg') right no-repeat; 13 | background-size: contain; 14 | } 15 | -------------------------------------------------------------------------------- /keepassxc-browser/css/totp.css: -------------------------------------------------------------------------------- 1 | .kpxc-totp-icon { 2 | position: absolute; 3 | cursor: pointer; 4 | } 5 | 6 | .kpxc-totp-icon.default { 7 | background: url('chrome-extension://__MSG_@@extension_id__/icons/otp.svg') right no-repeat; 8 | background-size: contain; 9 | } 10 | 11 | .kpxc-totp-icon.moz { 12 | background: url('moz-extension://__MSG_@@extension_id__/icons/otp.svg') right no-repeat; 13 | background-size: contain; 14 | } -------------------------------------------------------------------------------- /keepassxc-browser/css/username.css: -------------------------------------------------------------------------------- 1 | .kpxc-username-icon { 2 | position: absolute; 3 | cursor: pointer; 4 | } 5 | 6 | .kpxc-username-icon.disconnected { 7 | background: url('chrome-extension://__MSG_@@extension_id__/icons/disconnected.svg') right no-repeat; 8 | background-size: contain; 9 | } 10 | 11 | .kpxc-username-icon.disconnected-moz { 12 | background: url('moz-extension://__MSG_@@extension_id__/icons/disconnected.svg') right no-repeat; 13 | background-size: contain; 14 | } 15 | 16 | .kpxc-username-icon.lock { 17 | background: url('chrome-extension://__MSG_@@extension_id__/icons/locked.svg') right no-repeat; 18 | background-size: contain; 19 | } 20 | 21 | .kpxc-username-icon.lock-moz { 22 | background: url('moz-extension://__MSG_@@extension_id__/icons/locked.svg') right no-repeat; 23 | background-size: contain; 24 | } 25 | 26 | .kpxc-username-icon.unlock { 27 | background: url('chrome-extension://__MSG_@@extension_id__/icons/keepassxc.svg') right no-repeat; 28 | background-size: contain; 29 | } 30 | 31 | .kpxc-username-icon.unlock-moz { 32 | background: url('moz-extension://__MSG_@@extension_id__/icons/keepassxc.svg') right no-repeat; 33 | background-size: contain; 34 | } 35 | -------------------------------------------------------------------------------- /keepassxc-browser/fonts/forkawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keepassxreboot/keepassxc-browser/dac9ceceffadca3ff9312ec608a1ae5987f6b5ed/keepassxc-browser/fonts/forkawesome-webfont.woff2 -------------------------------------------------------------------------------- /keepassxc-browser/icons/custom_login_fields.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Choose Custom Login Fields icon 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | ** 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /keepassxc-browser/icons/disconnected.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /keepassxc-browser/icons/help.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /keepassxc-browser/icons/keepassxc-dark_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keepassxreboot/keepassxc-browser/dac9ceceffadca3ff9312ec608a1ae5987f6b5ed/keepassxc-browser/icons/keepassxc-dark_128x128.png -------------------------------------------------------------------------------- /keepassxc-browser/icons/keepassxc-dark_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keepassxreboot/keepassxc-browser/dac9ceceffadca3ff9312ec608a1ae5987f6b5ed/keepassxc-browser/icons/keepassxc-dark_16x16.png -------------------------------------------------------------------------------- /keepassxc-browser/icons/keepassxc-dark_18x18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keepassxreboot/keepassxc-browser/dac9ceceffadca3ff9312ec608a1ae5987f6b5ed/keepassxc-browser/icons/keepassxc-dark_18x18.png -------------------------------------------------------------------------------- /keepassxc-browser/icons/keepassxc-dark_19x19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keepassxreboot/keepassxc-browser/dac9ceceffadca3ff9312ec608a1ae5987f6b5ed/keepassxc-browser/icons/keepassxc-dark_19x19.png -------------------------------------------------------------------------------- /keepassxc-browser/icons/keepassxc-dark_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keepassxreboot/keepassxc-browser/dac9ceceffadca3ff9312ec608a1ae5987f6b5ed/keepassxc-browser/icons/keepassxc-dark_32x32.png -------------------------------------------------------------------------------- /keepassxc-browser/icons/keepassxc-dark_36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keepassxreboot/keepassxc-browser/dac9ceceffadca3ff9312ec608a1ae5987f6b5ed/keepassxc-browser/icons/keepassxc-dark_36x36.png -------------------------------------------------------------------------------- /keepassxc-browser/icons/keepassxc-dark_38x38.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keepassxreboot/keepassxc-browser/dac9ceceffadca3ff9312ec608a1ae5987f6b5ed/keepassxc-browser/icons/keepassxc-dark_38x38.png -------------------------------------------------------------------------------- /keepassxc-browser/icons/keepassxc-dark_48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keepassxreboot/keepassxc-browser/dac9ceceffadca3ff9312ec608a1ae5987f6b5ed/keepassxc-browser/icons/keepassxc-dark_48x48.png -------------------------------------------------------------------------------- /keepassxc-browser/icons/keepassxc-dark_64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keepassxreboot/keepassxc-browser/dac9ceceffadca3ff9312ec608a1ae5987f6b5ed/keepassxc-browser/icons/keepassxc-dark_64x64.png -------------------------------------------------------------------------------- /keepassxc-browser/icons/keepassxc_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keepassxreboot/keepassxc-browser/dac9ceceffadca3ff9312ec608a1ae5987f6b5ed/keepassxc-browser/icons/keepassxc_128x128.png -------------------------------------------------------------------------------- /keepassxc-browser/icons/keepassxc_16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keepassxreboot/keepassxc-browser/dac9ceceffadca3ff9312ec608a1ae5987f6b5ed/keepassxc-browser/icons/keepassxc_16x16.png -------------------------------------------------------------------------------- /keepassxc-browser/icons/keepassxc_18x18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keepassxreboot/keepassxc-browser/dac9ceceffadca3ff9312ec608a1ae5987f6b5ed/keepassxc-browser/icons/keepassxc_18x18.png -------------------------------------------------------------------------------- /keepassxc-browser/icons/keepassxc_19x19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keepassxreboot/keepassxc-browser/dac9ceceffadca3ff9312ec608a1ae5987f6b5ed/keepassxc-browser/icons/keepassxc_19x19.png -------------------------------------------------------------------------------- /keepassxc-browser/icons/keepassxc_32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keepassxreboot/keepassxc-browser/dac9ceceffadca3ff9312ec608a1ae5987f6b5ed/keepassxc-browser/icons/keepassxc_32x32.png -------------------------------------------------------------------------------- /keepassxc-browser/icons/keepassxc_36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keepassxreboot/keepassxc-browser/dac9ceceffadca3ff9312ec608a1ae5987f6b5ed/keepassxc-browser/icons/keepassxc_36x36.png -------------------------------------------------------------------------------- /keepassxc-browser/icons/keepassxc_38x38.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keepassxreboot/keepassxc-browser/dac9ceceffadca3ff9312ec608a1ae5987f6b5ed/keepassxc-browser/icons/keepassxc_38x38.png -------------------------------------------------------------------------------- /keepassxc-browser/icons/keepassxc_48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keepassxreboot/keepassxc-browser/dac9ceceffadca3ff9312ec608a1ae5987f6b5ed/keepassxc-browser/icons/keepassxc_48x48.png -------------------------------------------------------------------------------- /keepassxc-browser/icons/keepassxc_64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keepassxreboot/keepassxc-browser/dac9ceceffadca3ff9312ec608a1ae5987f6b5ed/keepassxc-browser/icons/keepassxc_64x64.png -------------------------------------------------------------------------------- /keepassxc-browser/icons/keepassxc_96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keepassxreboot/keepassxc-browser/dac9ceceffadca3ff9312ec608a1ae5987f6b5ed/keepassxc-browser/icons/keepassxc_96x96.png -------------------------------------------------------------------------------- /keepassxc-browser/icons/key.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /keepassxc-browser/icons/locked.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /keepassxc-browser/icons/otp.svg: -------------------------------------------------------------------------------- 1 | OTP icon*** -------------------------------------------------------------------------------- /keepassxc-browser/icons/toolbar/colored/icon_bang.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keepassxreboot/keepassxc-browser/dac9ceceffadca3ff9312ec608a1ae5987f6b5ed/keepassxc-browser/icons/toolbar/colored/icon_bang.png -------------------------------------------------------------------------------- /keepassxc-browser/icons/toolbar/colored/icon_bang.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /keepassxc-browser/icons/toolbar/colored/icon_cross.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keepassxreboot/keepassxc-browser/dac9ceceffadca3ff9312ec608a1ae5987f6b5ed/keepassxc-browser/icons/toolbar/colored/icon_cross.png -------------------------------------------------------------------------------- /keepassxc-browser/icons/toolbar/colored/icon_cross.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /keepassxc-browser/icons/toolbar/colored/icon_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keepassxreboot/keepassxc-browser/dac9ceceffadca3ff9312ec608a1ae5987f6b5ed/keepassxc-browser/icons/toolbar/colored/icon_dark.png -------------------------------------------------------------------------------- /keepassxc-browser/icons/toolbar/colored/icon_dark.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /keepassxc-browser/icons/toolbar/colored/icon_locked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keepassxreboot/keepassxc-browser/dac9ceceffadca3ff9312ec608a1ae5987f6b5ed/keepassxc-browser/icons/toolbar/colored/icon_locked.png -------------------------------------------------------------------------------- /keepassxc-browser/icons/toolbar/colored/icon_locked.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /keepassxc-browser/icons/toolbar/colored/icon_new_bang.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keepassxreboot/keepassxc-browser/dac9ceceffadca3ff9312ec608a1ae5987f6b5ed/keepassxc-browser/icons/toolbar/colored/icon_new_bang.png -------------------------------------------------------------------------------- /keepassxc-browser/icons/toolbar/colored/icon_new_bang.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /keepassxc-browser/icons/toolbar/colored/icon_new_cross.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keepassxreboot/keepassxc-browser/dac9ceceffadca3ff9312ec608a1ae5987f6b5ed/keepassxc-browser/icons/toolbar/colored/icon_new_cross.png -------------------------------------------------------------------------------- /keepassxc-browser/icons/toolbar/colored/icon_new_cross.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /keepassxc-browser/icons/toolbar/colored/icon_new_locked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keepassxreboot/keepassxc-browser/dac9ceceffadca3ff9312ec608a1ae5987f6b5ed/keepassxc-browser/icons/toolbar/colored/icon_new_locked.png -------------------------------------------------------------------------------- /keepassxc-browser/icons/toolbar/colored/icon_new_locked.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /keepassxc-browser/icons/toolbar/colored/icon_new_normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keepassxreboot/keepassxc-browser/dac9ceceffadca3ff9312ec608a1ae5987f6b5ed/keepassxc-browser/icons/toolbar/colored/icon_new_normal.png -------------------------------------------------------------------------------- /keepassxc-browser/icons/toolbar/colored/icon_new_normal.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /keepassxc-browser/icons/toolbar/colored/icon_normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keepassxreboot/keepassxc-browser/dac9ceceffadca3ff9312ec608a1ae5987f6b5ed/keepassxc-browser/icons/toolbar/colored/icon_normal.png -------------------------------------------------------------------------------- /keepassxc-browser/icons/toolbar/colored/icon_normal.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /keepassxc-browser/icons/toolbar/dark/icon_bang.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keepassxreboot/keepassxc-browser/dac9ceceffadca3ff9312ec608a1ae5987f6b5ed/keepassxc-browser/icons/toolbar/dark/icon_bang.png -------------------------------------------------------------------------------- /keepassxc-browser/icons/toolbar/dark/icon_bang.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /keepassxc-browser/icons/toolbar/dark/icon_cross.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keepassxreboot/keepassxc-browser/dac9ceceffadca3ff9312ec608a1ae5987f6b5ed/keepassxc-browser/icons/toolbar/dark/icon_cross.png -------------------------------------------------------------------------------- /keepassxc-browser/icons/toolbar/dark/icon_cross.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /keepassxc-browser/icons/toolbar/dark/icon_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keepassxreboot/keepassxc-browser/dac9ceceffadca3ff9312ec608a1ae5987f6b5ed/keepassxc-browser/icons/toolbar/dark/icon_dark.png -------------------------------------------------------------------------------- /keepassxc-browser/icons/toolbar/dark/icon_dark.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /keepassxc-browser/icons/toolbar/dark/icon_locked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keepassxreboot/keepassxc-browser/dac9ceceffadca3ff9312ec608a1ae5987f6b5ed/keepassxc-browser/icons/toolbar/dark/icon_locked.png -------------------------------------------------------------------------------- /keepassxc-browser/icons/toolbar/dark/icon_locked.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /keepassxc-browser/icons/toolbar/dark/icon_new_bang.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keepassxreboot/keepassxc-browser/dac9ceceffadca3ff9312ec608a1ae5987f6b5ed/keepassxc-browser/icons/toolbar/dark/icon_new_bang.png -------------------------------------------------------------------------------- /keepassxc-browser/icons/toolbar/dark/icon_new_bang.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /keepassxc-browser/icons/toolbar/dark/icon_new_cross.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keepassxreboot/keepassxc-browser/dac9ceceffadca3ff9312ec608a1ae5987f6b5ed/keepassxc-browser/icons/toolbar/dark/icon_new_cross.png -------------------------------------------------------------------------------- /keepassxc-browser/icons/toolbar/dark/icon_new_cross.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /keepassxc-browser/icons/toolbar/dark/icon_new_locked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keepassxreboot/keepassxc-browser/dac9ceceffadca3ff9312ec608a1ae5987f6b5ed/keepassxc-browser/icons/toolbar/dark/icon_new_locked.png -------------------------------------------------------------------------------- /keepassxc-browser/icons/toolbar/dark/icon_new_locked.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /keepassxc-browser/icons/toolbar/dark/icon_new_normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keepassxreboot/keepassxc-browser/dac9ceceffadca3ff9312ec608a1ae5987f6b5ed/keepassxc-browser/icons/toolbar/dark/icon_new_normal.png -------------------------------------------------------------------------------- /keepassxc-browser/icons/toolbar/dark/icon_new_normal.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /keepassxc-browser/icons/toolbar/dark/icon_normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keepassxreboot/keepassxc-browser/dac9ceceffadca3ff9312ec608a1ae5987f6b5ed/keepassxc-browser/icons/toolbar/dark/icon_normal.png -------------------------------------------------------------------------------- /keepassxc-browser/icons/toolbar/dark/icon_normal.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /keepassxc-browser/icons/toolbar/light/icon_bang.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keepassxreboot/keepassxc-browser/dac9ceceffadca3ff9312ec608a1ae5987f6b5ed/keepassxc-browser/icons/toolbar/light/icon_bang.png -------------------------------------------------------------------------------- /keepassxc-browser/icons/toolbar/light/icon_bang.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /keepassxc-browser/icons/toolbar/light/icon_cross.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keepassxreboot/keepassxc-browser/dac9ceceffadca3ff9312ec608a1ae5987f6b5ed/keepassxc-browser/icons/toolbar/light/icon_cross.png -------------------------------------------------------------------------------- /keepassxc-browser/icons/toolbar/light/icon_cross.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /keepassxc-browser/icons/toolbar/light/icon_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keepassxreboot/keepassxc-browser/dac9ceceffadca3ff9312ec608a1ae5987f6b5ed/keepassxc-browser/icons/toolbar/light/icon_dark.png -------------------------------------------------------------------------------- /keepassxc-browser/icons/toolbar/light/icon_dark.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /keepassxc-browser/icons/toolbar/light/icon_locked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keepassxreboot/keepassxc-browser/dac9ceceffadca3ff9312ec608a1ae5987f6b5ed/keepassxc-browser/icons/toolbar/light/icon_locked.png -------------------------------------------------------------------------------- /keepassxc-browser/icons/toolbar/light/icon_locked.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /keepassxc-browser/icons/toolbar/light/icon_new_bang.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keepassxreboot/keepassxc-browser/dac9ceceffadca3ff9312ec608a1ae5987f6b5ed/keepassxc-browser/icons/toolbar/light/icon_new_bang.png -------------------------------------------------------------------------------- /keepassxc-browser/icons/toolbar/light/icon_new_bang.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /keepassxc-browser/icons/toolbar/light/icon_new_cross.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keepassxreboot/keepassxc-browser/dac9ceceffadca3ff9312ec608a1ae5987f6b5ed/keepassxc-browser/icons/toolbar/light/icon_new_cross.png -------------------------------------------------------------------------------- /keepassxc-browser/icons/toolbar/light/icon_new_cross.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /keepassxc-browser/icons/toolbar/light/icon_new_locked.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keepassxreboot/keepassxc-browser/dac9ceceffadca3ff9312ec608a1ae5987f6b5ed/keepassxc-browser/icons/toolbar/light/icon_new_locked.png -------------------------------------------------------------------------------- /keepassxc-browser/icons/toolbar/light/icon_new_locked.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /keepassxc-browser/icons/toolbar/light/icon_new_normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keepassxreboot/keepassxc-browser/dac9ceceffadca3ff9312ec608a1ae5987f6b5ed/keepassxc-browser/icons/toolbar/light/icon_new_normal.png -------------------------------------------------------------------------------- /keepassxc-browser/icons/toolbar/light/icon_new_normal.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /keepassxc-browser/icons/toolbar/light/icon_normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keepassxreboot/keepassxc-browser/dac9ceceffadca3ff9312ec608a1ae5987f6b5ed/keepassxc-browser/icons/toolbar/light/icon_normal.png -------------------------------------------------------------------------------- /keepassxc-browser/icons/toolbar/light/icon_normal.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /keepassxc-browser/offscreen/offscreen.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /keepassxc-browser/offscreen/offscreen.js: -------------------------------------------------------------------------------- 1 | chrome.runtime.onMessage.addListener(({ target, action }, sender, sendResponse) => { 2 | if (target !== 'offscreen') { 3 | return; 4 | } 5 | if (action === 'retrieve_color_scheme') { 6 | const style = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; 7 | sendResponse(style); 8 | } 9 | }); 10 | -------------------------------------------------------------------------------- /keepassxc-browser/options/http-auth-dialog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/keepassxreboot/keepassxc-browser/dac9ceceffadca3ff9312ec608a1ae5987f6b5ed/keepassxc-browser/options/http-auth-dialog.png -------------------------------------------------------------------------------- /keepassxc-browser/options/shortcuts.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: var(--kpxc-input-background-color) !important; 3 | color: var(--kpxc-text-color); 4 | font-size: .875rem; 5 | padding-top: 20px; 6 | } 7 | 8 | input { 9 | background-color: var(--kpxc-input-background-color); 10 | border: var(--kpxc-input-border); 11 | border-color: var(--kpxc-input-border-color); 12 | color: var(--kpxc-text-color); 13 | } 14 | 15 | input:active { 16 | border-color: var(--kpxc-input-active-border-color); 17 | } 18 | 19 | .conf-container { 20 | margin: 0 auto; 21 | width: 680px; 22 | max-height: 315px; 23 | background-color: var(--kpxc-background-color); 24 | border: var(--kpxc-container-border); 25 | border-radius: 4px; 26 | box-shadow: 0 4px 6px 0 hsla(0, 0%, 0%, 0.2); 27 | color: var(--kpxc-text-color); 28 | } 29 | 30 | .conf-container .conf-title { 31 | margin: 10px 0 0 20px; 32 | display: flex; 33 | flex-direction: row; 34 | align-items: center; 35 | } 36 | 37 | .conf-title #icon { 38 | width: 3em; 39 | } 40 | 41 | .conf-container > hr { 42 | margin: 10px 0; 43 | } 44 | 45 | .conf-content { 46 | margin: 10px 0 20px 40px; 47 | } 48 | 49 | .conf-row { 50 | display: flex; 51 | align-items: center; 52 | justify-content: flex-start; 53 | } 54 | 55 | .conf-row div:first-child { 56 | width: 200px; 57 | margin: 10px; 58 | } 59 | 60 | .conf-row button { 61 | margin-left: 10px; 62 | } 63 | 64 | .alert { 65 | box-shadow: 0 4px 6px 0 hsla(0, 0%, 0%, 0.2); 66 | margin: 20px auto; 67 | width: 680px; 68 | } 69 | 70 | @media (prefers-color-scheme: dark) { 71 | .bg-danger { 72 | color: #000; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /keepassxc-browser/options/shortcuts.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 |
22 |
23 | logo 24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | 34 | 38 | 42 |
43 |
44 |
45 | 46 | 50 | 54 |
55 |
56 |
57 | 58 | 62 | 66 |
67 |
68 |
69 | 70 | 74 | 78 |
79 |
80 |
81 | 82 | 83 | -------------------------------------------------------------------------------- /keepassxc-browser/popups/popup.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: var(--kpxc-background-color) !important; 3 | color: var(--kpxc-text-color) !important; 4 | display: flex; 5 | font-family: sans-serif; 6 | font-size: 14px !important; 7 | padding-top: 8px; 8 | padding-bottom: 8px; 9 | width: 460px; 10 | } 11 | 12 | code { 13 | color: var(--bs-danger) !important; 14 | font-size: 14px !important; 15 | } 16 | 17 | .container { 18 | display: flex; 19 | flex-direction: column; 20 | max-width: 460px; 21 | } 22 | 23 | .loader { 24 | animation: spin 2s linear infinite; 25 | border: 4px solid #f3f3f3; 26 | border-radius: 50%; 27 | border-top: 4px solid #28a745; 28 | float: left; 29 | height: 1.4em; 30 | margin-right: 1em; 31 | width: 1.4em; 32 | } 33 | 34 | @keyframes spin { 35 | 0% { transform: rotate(0deg); } 36 | 100% { transform: rotate(360deg); } 37 | } 38 | 39 | .list-group { 40 | font-size: .9em !important; 41 | margin-bottom: 1rem !important; 42 | } 43 | 44 | .list-group-item { 45 | padding: 5px 10px !important; 46 | } 47 | 48 | .list-group-item+.list-group-item:first-child { 49 | border-top-width: 1px !important; 50 | border-top-left-radius: .25rem; 51 | border-top-right-radius: .25rem; 52 | } 53 | 54 | .list-group-item:hover { 55 | cursor: pointer 56 | } 57 | 58 | .settings { 59 | display: flex; 60 | flex-wrap: wrap; 61 | justify-content: space-between; 62 | padding-bottom: 10px; 63 | margin-bottom: 10px; 64 | border-bottom: 1px solid rgb(203, 207, 203, 0.25); 65 | font-size: 90%; 66 | text-align: left; 67 | } 68 | 69 | .settings label { 70 | display: block; 71 | } 72 | 73 | .settings label input { 74 | vertical-align: middle; 75 | } 76 | 77 | .settings .description { 78 | margin-top: 3px; 79 | font-size: 80%; 80 | color: #787878; 81 | } 82 | 83 | .small { 84 | margin-top: 3px; 85 | margin-left: 12px; 86 | font-size: 80%; 87 | color: #787878; 88 | } 89 | 90 | .action-buttons-area { 91 | display: flex; 92 | justify-content: flex-start; 93 | gap: 6px; 94 | } 95 | 96 | .lock-button-area { 97 | display: flex; 98 | justify-content: flex-end; 99 | } 100 | 101 | #list { 102 | padding-left: 0px; 103 | } 104 | 105 | #login-list { 106 | overflow-y: auto; 107 | max-height: 380px; 108 | } 109 | 110 | #child { 111 | border-top: 0px; 112 | } 113 | 114 | #popup-alerts { 115 | margin-top: 10px; 116 | text-align: start; 117 | } 118 | 119 | #filter-block, 120 | #not-configured, 121 | #need-reconfigure, 122 | #configured-not-associated, 123 | #configured-and-associated, 124 | #error-encountered, 125 | #database-not-opened, 126 | #username-field-detected, 127 | #update-available, 128 | #getting-started-guide, 129 | #troubleshooting-guide { 130 | display: none; 131 | } 132 | 133 | #options-button { 134 | height: 31px; 135 | width: 2.5rem; 136 | } 137 | 138 | #choose-custom-login-fields-button { 139 | background-image: url('chrome-extension://__MSG_@@extension_id__/icons/custom_login_fields.svg'); 140 | background-position: center; 141 | background-repeat: no-repeat; 142 | background-size: 70%; 143 | height: 31px; 144 | width: 2.5rem; 145 | } 146 | 147 | #choose-custom-login-fields-button-moz { 148 | background-image: url('moz-extension://__MSG_@@extension_id__/icons/custom_login_fields.svg'); 149 | background-position: center; 150 | background-repeat: no-repeat; 151 | background-size: 70%; 152 | height: 31px; 153 | width: 2.5rem; 154 | } 155 | 156 | #lock-database-button { 157 | display: none; 158 | width: 2.5rem; 159 | } 160 | 161 | #login-filter { 162 | outline: none; 163 | border-radius: 4px; 164 | border: 1px solid #ccc; 165 | margin-bottom: 5px; 166 | padding: 2px 10px; 167 | width: 100%; 168 | } 169 | 170 | .right-align { 171 | text-align: right; 172 | } 173 | 174 | #left-margin { 175 | margin-left: 1em; 176 | } 177 | 178 | @media (prefers-color-scheme: dark), (prefers-color-scheme: light) { 179 | body { 180 | background: var(--kpxc-background-color) !important; 181 | color: var(--kpxc-text-color) !important; 182 | } 183 | 184 | input, input[type="checkbox"] { 185 | background-color: var(--kpxc-input-background-color) !important; 186 | border: var(--kpxc-input-border); 187 | border-color: var(--kpxc-input-border-color); 188 | } 189 | 190 | .list-group-item { 191 | background-color: var(--kpxc-input-background-color) !important; 192 | color: var(--kpxc-text-color) !important; 193 | } 194 | 195 | .list-group-item:hover { 196 | background-color: var(--kpxc-table-hover-color) !important; 197 | } 198 | 199 | span.bg-success { 200 | background-color: var(--kpxc-table-hover-color) !important; 201 | color: var(--kpxc-text-color) !important; 202 | } 203 | 204 | #login-filter { 205 | color: var(--kpxc-text-color) !important; 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /keepassxc-browser/popups/popup.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let reloadCount = 0; 4 | 5 | HTMLElement.prototype.show = function() { 6 | this.style.display = 'block'; 7 | }; 8 | 9 | HTMLElement.prototype.hide = function() { 10 | this.style.display = 'none'; 11 | }; 12 | 13 | function statusResponse(r) { 14 | $('#initial-state').hide(); 15 | $('#error-encountered').hide(); 16 | $('#need-reconfigure').hide(); 17 | $('#not-configured').hide(); 18 | $('#configured-and-associated').hide(); 19 | $('#configured-not-associated').hide(); 20 | $('#lock-database-button').hide(); 21 | $('#getting-started-guide').hide(); 22 | $('#database-not-opened').hide(); 23 | 24 | if (!r.keePassXCAvailable) { 25 | $('#error-message').textContent = r.error; 26 | $('#error-encountered').show(); 27 | 28 | if (r.showGettingStartedGuideAlert) { 29 | $('#getting-started-guide').show(); 30 | } 31 | 32 | if (r.showTroubleshootingGuideAlert && reloadCount >= 2) { 33 | $('#troubleshooting-guide').show(); 34 | } else { 35 | $('#troubleshooting-guide').hide(); 36 | } 37 | } else if (r.keePassXCAvailable && r.databaseClosed) { 38 | $('#database-error-message').textContent = r.error; 39 | $('#database-not-opened').show(); 40 | } else if (!r.configured) { 41 | $('#not-configured').show(); 42 | } else if (r.encryptionKeyUnrecognized) { 43 | $('#need-reconfigure').show(); 44 | $('#need-reconfigure-message').textContent = r.error; 45 | } else if (!r.associated) { 46 | $('#need-reconfigure').show(); 47 | $('#need-reconfigure-message').textContent = r.error; 48 | } else if (r.error) { 49 | $('#error-encountered').show(); 50 | $('#error-message').textContent = r.error; 51 | } else { 52 | $('#configured-and-associated').show(); 53 | $('#associated-identifier').textContent = r.identifier; 54 | $('#lock-database-button').show(); 55 | 56 | if (r.usernameFieldDetected) { 57 | $('#username-field-detected').show(); 58 | } 59 | 60 | if (r.iframeDetected) { 61 | $('#iframe-detected').show(); 62 | } 63 | 64 | reloadCount = 0; 65 | } 66 | } 67 | 68 | const sendMessageToTab = async function(message) { 69 | const tab = await getCurrentTab(); 70 | if (!tab) { 71 | return false; // Only the background devtools or a popup are opened 72 | } 73 | 74 | await browser.tabs.sendMessage(tab.id, { 75 | action: message 76 | }); 77 | 78 | return true; 79 | }; 80 | 81 | (async () => { 82 | await initColorTheme(); 83 | 84 | $('#connect-button').addEventListener('click', async () => { 85 | await browser.runtime.sendMessage({ 86 | action: 'associate' 87 | }); 88 | 89 | // This does not work with Firefox because of https://bugzilla.mozilla.org/show_bug.cgi?id=1665380 90 | await sendMessageToTab('retrive_credentials_forced'); 91 | close(); 92 | }); 93 | 94 | $('#reconnect-button').addEventListener('click', async () => { 95 | await browser.runtime.sendMessage({ 96 | action: 'associate' 97 | }); 98 | close(); 99 | }); 100 | 101 | $('#reload-status-button').addEventListener('click', async () => { 102 | statusResponse(await browser.runtime.sendMessage({ 103 | action: 'reconnect' 104 | })); 105 | 106 | // Shows the Troubleshooting Guide alert every third time Reload button is pressed when popup is open 107 | if (reloadCount > 2) { 108 | reloadCount = 0; 109 | } 110 | reloadCount++; 111 | }); 112 | 113 | $('#reopen-database-button').addEventListener('click', async () => { 114 | statusResponse(await browser.runtime.sendMessage({ 115 | action: 'get_status', 116 | args: [ false, true ] // Set forcePopup to true 117 | })); 118 | window.close(); 119 | }); 120 | 121 | $('#redetect-fields-button').addEventListener('click', async () => { 122 | const res = await sendMessageToTab('redetect_fields'); 123 | if (!res) { 124 | return; 125 | } 126 | 127 | statusResponse(await browser.runtime.sendMessage({ 128 | action: 'get_status' 129 | })); 130 | }); 131 | 132 | $('#lock-database-button').addEventListener('click', async () => { 133 | statusResponse(await browser.runtime.sendMessage({ 134 | action: 'lock_database' 135 | })); 136 | }); 137 | 138 | $('#username-only-button').addEventListener('click', async () => { 139 | await sendMessageToTab('add_username_only_option'); 140 | await sendMessageToTab('redetect_fields'); 141 | $('#username-field-detected').hide(); 142 | }); 143 | 144 | $('#allow-iframe-button').addEventListener('click', async () => { 145 | await sendMessageToTab('add_allow_iframes_option'); 146 | await sendMessageToTab('redetect_fields'); 147 | $('#iframe-detected').hide(); 148 | }); 149 | 150 | $('#getting-started-alert-close-button').addEventListener('click', async () => { 151 | await browser.runtime.sendMessage({ 152 | action: 'hide_getting_started_guide_alert' 153 | }); 154 | }); 155 | 156 | $('#troubleshooting-guide-alert-close-button').addEventListener('click', async () => { 157 | await browser.runtime.sendMessage({ 158 | action: 'hide_troubleshooting_guide_alert' 159 | }); 160 | }); 161 | 162 | statusResponse(await browser.runtime.sendMessage({ 163 | action: 'get_status' 164 | }).catch((err) => { 165 | logError('Could not get status: ' + err); 166 | })); 167 | })(); 168 | -------------------------------------------------------------------------------- /keepassxc-browser/popups/popup_functions.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const $ = function(elem) { 4 | return document.querySelector(elem); 5 | }; 6 | 7 | function updateAvailableResponse(available) { 8 | if (available) { 9 | $('#update-available').show(); 10 | } 11 | } 12 | 13 | async function initSettings() { 14 | $('#settings #options-button').addEventListener('click', () => { 15 | browser.runtime.openOptionsPage().then(close()); 16 | }); 17 | 18 | const customLoginFieldsButton = document.body.querySelector('#settings #choose-custom-login-fields-button'); 19 | if (isFirefox()) { 20 | customLoginFieldsButton.id = 'choose-custom-login-fields-button-moz'; 21 | } 22 | 23 | customLoginFieldsButton.addEventListener('click', async () => { 24 | const tab = await getCurrentTab(); 25 | browser.tabs.sendMessage(tab?.id, { 26 | action: 'choose_credential_fields' 27 | }); 28 | close(); 29 | }); 30 | } 31 | 32 | async function initColorTheme() { 33 | let theme = await browser.runtime.sendMessage({ 34 | action: 'get_color_theme' 35 | }); 36 | if (theme === 'system') { 37 | theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; 38 | } 39 | document.documentElement.setAttribute('data-bs-theme', theme); 40 | } 41 | 42 | async function getLoginData() { 43 | const tab = await getCurrentTab(); 44 | if (!tab) { 45 | return []; 46 | } 47 | 48 | const logins = await browser.runtime.sendMessage({ 49 | action: 'get_login_list', 50 | args: tab.id 51 | }); 52 | 53 | return logins; 54 | } 55 | 56 | (async () => { 57 | if (document.readyState === 'complete' || (document.readyState !== 'loading' && !document.documentElement.doScroll)) { 58 | await initSettings(); 59 | } else { 60 | document.addEventListener('DOMContentLoaded', initSettings); 61 | } 62 | 63 | updateAvailableResponse(await browser.runtime.sendMessage({ 64 | action: 'update_available_keepassxc' 65 | })); 66 | })(); 67 | -------------------------------------------------------------------------------- /keepassxc-browser/popups/popup_httpauth.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |
22 | 25 | 26 |
27 |
28 | 31 |
32 |
33 | 34 |
35 | 36 |
37 | . 38 |
39 | 40 |
41 |

42 |
43 |

44 | 48 |

49 |
50 | 51 |
52 |

53 |

54 | 55 |

56 |
57 | 61 |
62 |
63 |
64 | 65 | 66 | -------------------------------------------------------------------------------- /keepassxc-browser/popups/popup_httpauth.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | (async () => { 4 | await initColorTheme(); 5 | 6 | $('#lock-database-button').show(); 7 | 8 | const data = await getLoginData(); 9 | const ll = document.getElementById('login-list'); 10 | 11 | for (const [ i, login ] of data.logins.entries()) { 12 | const a = document.createElement('a'); 13 | a.setAttribute('class', 'list-group-item'); 14 | a.textContent = login.login + ' (' + login.name + ')'; 15 | a.setAttribute('id', '' + i); 16 | 17 | a.addEventListener('click', (e) => { 18 | if (!e.isTrusted) { 19 | return; 20 | } 21 | 22 | const credentials = data.logins[Number(e.target.id)]; 23 | browser.runtime.sendMessage({ 24 | action: 'fill_http_auth', 25 | args: credentials 26 | }); 27 | 28 | close(); 29 | }); 30 | ll.appendChild(a); 31 | } 32 | 33 | $('#lock-database-button').addEventListener('click', function() { 34 | browser.runtime.sendMessage({ 35 | action: 'lock_database' 36 | }); 37 | 38 | $('.credentials').hide(); 39 | $('#btn-dismiss').hide(); 40 | $('#database-not-opened').show(); 41 | $('#lock-database-button').hide(); 42 | $('#database-error-message').textContent = tr('errorMessageDatabaseNotOpened'); 43 | }); 44 | 45 | $('#btn-dismiss').addEventListener('click', async () => { 46 | // Return empty credentials 47 | browser.runtime.sendMessage({ 48 | action: 'fill_http_auth', 49 | args: { login: '', password: '' } 50 | }); 51 | 52 | close(); 53 | }); 54 | })(); 55 | -------------------------------------------------------------------------------- /keepassxc-browser/popups/popup_login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |
22 | 25 | 26 |
27 |
28 | 31 |
32 |
33 | 34 |
35 | 36 |
37 | . 38 |
39 | 40 |
41 |

42 |
43 | 44 | 45 |
46 |
47 |
48 | 49 |
50 |

51 |

52 | 53 |

54 |
55 | 59 |
60 |
61 |
62 | 63 | 64 | -------------------------------------------------------------------------------- /keepassxc-browser/popups/popup_login.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | (async () => { 4 | await initColorTheme(); 5 | 6 | $('#lock-database-button').show(); 7 | 8 | const tab = await getCurrentTab(); 9 | if (!tab) { 10 | return []; 11 | } 12 | 13 | const logins = await getLoginData(); 14 | const ll = document.getElementById('login-list'); 15 | 16 | for (const [ i, login ] of logins.entries()) { 17 | const uuid = login.uuid; 18 | const a = document.createElement('a'); 19 | a.textContent = login.text; 20 | a.setAttribute('class', 'list-group-item'); 21 | a.setAttribute('id', '' + i); 22 | 23 | a.addEventListener('click', (e) => { 24 | if (!e.isTrusted) { 25 | return; 26 | } 27 | 28 | const id = e.target.id; 29 | browser.tabs.sendMessage(tab?.id, { 30 | action: 'fill_user_pass_with_specific_login', 31 | id: Number(id), 32 | uuid: uuid 33 | }); 34 | 35 | close(); 36 | }); 37 | 38 | ll.appendChild(a); 39 | } 40 | 41 | if (logins.length > 1) { 42 | $('#filter-block').show(); 43 | const filter = document.getElementById('login-filter'); 44 | filter.addEventListener('keyup', (e) => { 45 | if (!e.isTrusted) { 46 | return; 47 | } 48 | 49 | const val = filter.value; 50 | const re = new RegExp(val, 'i'); 51 | const links = ll.getElementsByTagName('a'); 52 | for (const i in links) { 53 | if (links.hasOwnProperty(i)) { 54 | const found = String(links[i].textContent).match(re) !== null; 55 | links[i].style = found ? '' : 'display: none;'; 56 | } 57 | } 58 | }); 59 | 60 | filter.focus(); 61 | } 62 | 63 | $('#lock-database-button').addEventListener('click', (e) => { 64 | browser.runtime.sendMessage({ 65 | action: 'lock_database' 66 | }); 67 | 68 | $('#credentialsList').hide(); 69 | $('#database-not-opened').show(); 70 | $('#lock-database-button').hide(); 71 | $('#database-error-message').textContent = tr('errorMessageDatabaseNotOpened'); 72 | }); 73 | 74 | $('#reopen-database-button').addEventListener('click', (e) => { 75 | browser.runtime.sendMessage({ 76 | action: 'get_status', 77 | args: [ false, true ] // Set forcePopup to true 78 | }); 79 | }); 80 | })(); 81 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "keepassxc-browser", 3 | "version": "1.9.8", 4 | "description": "KeePassXC-Browser", 5 | "main": "build.js", 6 | "devDependencies": { 7 | "@playwright/test": "^1.45.1", 8 | "@types/node": "^20.14.10", 9 | "eslint": "^8.49.0" 10 | }, 11 | "dependencies": { 12 | "@npmcli/fs": "^2.1.0" 13 | }, 14 | "scripts": { 15 | "build": "node build.js", 16 | "debug:chromium": "cp dist/manifest_chromium.json keepassxc-browser/manifest.json", 17 | "debug:firefox": "cp dist/manifest_firefox.json keepassxc-browser/manifest.json", 18 | "lint": "npx eslint keepassxc-browser/**/*.js", 19 | "tests": "npx playwright test" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/keepassxreboot/keepassxc-browser.git" 24 | }, 25 | "author": "KeePassXC Team", 26 | "license": "GPL-3.0", 27 | "private": true, 28 | "bugs": { 29 | "url": "https://github.com/keepassxreboot/keepassxc-browser/issues" 30 | }, 31 | "homepage": "https://github.com/keepassxreboot/keepassxc-browser#readme" 32 | } 33 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, devices } from '@playwright/test'; 2 | 3 | export default defineConfig({ 4 | testDir: './tests', 5 | fullyParallel: true, 6 | timeout: 30 * 1000, 7 | expect: { 8 | timeout: 5000 9 | }, 10 | forbidOnly: !!process.env.CI, 11 | globalSetup: require.resolve('./tests/global-setup'), 12 | globalTeardown: require.resolve('./tests/global-teardown'), 13 | retries: process.env.CI ? 2 : 0, 14 | workers: process.env.CI ? 1 : undefined, 15 | reporter: 'list', 16 | use: { 17 | actionTimeout: 0, 18 | trace: 'on-first-retry', 19 | }, 20 | projects: [ 21 | { 22 | name: 'chromium', 23 | use: { ...devices['Desktop Chrome'] }, 24 | }, 25 | { 26 | name: 'firefox', 27 | use: { ...devices['Desktop Firefox'] }, 28 | }, 29 | ], 30 | testMatch: '*.spec.ts', 31 | }); 32 | -------------------------------------------------------------------------------- /tests/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true 4 | }, 5 | "parserOptions": { 6 | "sourceType": "module" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tests/assert.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function kpxcAssert(func, expected, card, testName) { 4 | if (func === expected) { 5 | createResult(card, true, `Test passed: ${testName}`); 6 | return; 7 | } 8 | 9 | createResult(card, false, `Test failed: ${testName}. Result is: ${func}`); 10 | } 11 | 12 | function assertRegex(res, expected, card, testName) { 13 | if ((res === null && expected === false) 14 | || (res && (res.length > 0) === expected) 15 | || (res === expected)) { 16 | createResult(card, true, `Test passed: ${testName}`); 17 | return; 18 | } 19 | 20 | createResult(card, false, `Test failed: ${testName}. Result is: ${res}`); 21 | } 22 | 23 | async function assertInputFields(localDiv, expectedFieldCount, actionElementId) { 24 | const div = document.getElementById(localDiv); 25 | div.style.display = 'block'; 26 | 27 | // An user interaction is required before testing 28 | if (actionElementId) { 29 | const actionElement = div.querySelector(actionElementId); 30 | if (actionElement) { 31 | actionElement.click(); 32 | } 33 | } 34 | 35 | const inputs = kpxcObserverHelper.getInputs(div); 36 | kpxcAssert(inputs.length, expectedFieldCount, Tests.INPUT_FIELDS, `getInputs() for ${localDiv} with ${expectedFieldCount} fields`); 37 | 38 | div.style.display = 'none'; 39 | } 40 | 41 | async function assertPasswordChangeFields(localDiv, expectedNewPassword) { 42 | const div = document.getElementById(localDiv); 43 | div.style.display = 'block'; 44 | 45 | const inputs = kpxcObserverHelper.getInputs(div, true); 46 | const newPassword = kpxcForm.getNewPassword(inputs); 47 | kpxcAssert(newPassword, expectedNewPassword, Tests.PASSWORD_CHANGE, `New password matches for ${localDiv}`); 48 | 49 | div.style.display = 'none'; 50 | } 51 | 52 | async function assertTOTPField(classStr, properties, testName, expectedResult) { 53 | const input = kpxcUI.createElement('input', classStr, properties); 54 | document.body.appendChild(input); 55 | 56 | const isValid = kpxcTOTPIcons.isValid(input); 57 | 58 | document.body.removeChild(input); 59 | kpxcAssert(isValid, expectedResult, Tests.TOTP_FIELDS, testName); 60 | } 61 | 62 | async function assertSearchField(classStr, properties, testName, expectedResult) { 63 | const input = kpxcUI.createElement('input', classStr, properties); 64 | document.body.appendChild(input); 65 | 66 | const isSearchfield = kpxcFields.isSearchField(input); 67 | 68 | document.body.removeChild(input); 69 | kpxcAssert(isSearchfield, expectedResult, Tests.SEARCH_FIELDS, testName); 70 | } 71 | 72 | async function assertSearchForm(properties, testName, expectedResult) { 73 | const form = kpxcUI.createElement('form', '', { action: 'search' }); 74 | const input = kpxcUI.createElement('input', '', properties); 75 | form.appendChild(input); 76 | document.body.appendChild(form); 77 | 78 | const isSearchfield = kpxcFields.isSearchField(input); 79 | 80 | document.body.removeChild(form); 81 | kpxcAssert(isSearchfield, expectedResult, Tests.SEARCH_FIELDS, testName); 82 | } 83 | -------------------------------------------------------------------------------- /tests/content-scripts.spec.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { test, expect } from '@playwright/test'; 4 | import { pathToFileURL } from 'node:url'; 5 | 6 | const DEST = 'keepassxc-browser/tests'; 7 | 8 | let page; 9 | 10 | test.describe('Content script tests', () => { 11 | test.beforeAll(async ({ browser }) => { 12 | page = await browser.newPage(); 13 | await page.goto(pathToFileURL(`${DEST}/tests.html`).toString()); 14 | }); 15 | 16 | test('Input field matching tests', async() => { 17 | await verifyResults('input-field-results'); 18 | }); 19 | 20 | test('Search field tests', async () => { 21 | await verifyResults('search-field-results'); 22 | }); 23 | 24 | test('TOTP field tests', async () => { 25 | await verifyResults('totp-field-results'); 26 | }); 27 | 28 | test('Password change tests', async () => { 29 | await verifyResults('password-change-results'); 30 | }); 31 | }); 32 | 33 | const verifyResults = async(selector) => { 34 | const resultCount = await page.locator(`css=#${selector} >> css=.fa`).count(); 35 | await expect.soft(resultCount).toBeGreaterThan(0); 36 | 37 | for (let i = 0; i < resultCount; i++) { 38 | const elem = await page.locator(`css=#${selector} >> css=.fa`).nth(i); 39 | const id = await elem.getAttribute('id'); 40 | await expect.soft(elem, id).toHaveClass('fa fa-check'); 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /tests/global-setup.ts: -------------------------------------------------------------------------------- 1 | import type { FullConfig } from '@playwright/test'; 2 | import fs from 'fs'; 3 | 4 | const DEST = 'keepassxc-browser/tests'; 5 | 6 | export default async function globalSetup(config: FullConfig) { 7 | // Create a temporary directory and copy tests/* to keepassxc-browser/tests 8 | fs.existsSync(DEST); 9 | fs.cpSync('./tests', DEST, { recursive: true }); 10 | } 11 | -------------------------------------------------------------------------------- /tests/global-teardown.ts: -------------------------------------------------------------------------------- 1 | import type { FullConfig } from '@playwright/test'; 2 | import fs from 'fs'; 3 | 4 | const DEST = 'keepassxc-browser/tests'; 5 | 6 | export default async function globalTeardown(config: FullConfig) { 7 | // Delete previously created temporary directory. Comment for re-running tests manually inside the extension. 8 | fs.rmSync(DEST, { recursive: true }); 9 | } 10 | -------------------------------------------------------------------------------- /tests/global.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | import { 3 | compareVersion, 4 | matchesWithNodeName, 5 | siteMatch, 6 | slashNeededForUrl, 7 | trimURL 8 | } from '../keepassxc-browser/common/global.js'; 9 | 10 | test('Test compareVersion()', async ({ page }) => { 11 | // compareVersion(minimum, current) 12 | expect(compareVersion('2.7.0', '2.7.0')).toBe(true); 13 | expect(compareVersion('2.7.1', '2.7.0')).toBe(false); 14 | expect(compareVersion('2.8.0', '2.7.0')).toBe(false); 15 | expect(compareVersion('2.7.9', '2.7.9-snapshot')).toBe(true); 16 | expect(compareVersion('2.7.9', '2.7.9-beta')).toBe(true); 17 | expect(compareVersion('2.7.9-snapshot', '2.7.9')).toBe(false); // Snapshot cannot be the minimum version 18 | expect(compareVersion('2.7.9-beta', '2.7.9')).toBe(false); // Beta cannot be the minimum version 19 | expect(compareVersion('faulty', '2.7.0')).toBe(false); 20 | expect(compareVersion('2.7.0', 'faulty')).toBe(false); 21 | expect(compareVersion('2.7.0.0.0.0.0', '2.7.0.0.0.0.0')).toBe(true); 22 | expect(compareVersion('2.7.0.0', '2.7.0.1')).toBe(true); 23 | }); 24 | 25 | test('Test matchesWithNodeName()', async ({ page }) => { 26 | const elem1 = { nodeName: 'INPUT' }; 27 | const elem2 = { nodeName: 'input' }; 28 | expect(matchesWithNodeName(elem1, 'INPUT')).toBe(true); 29 | expect(matchesWithNodeName(elem1, 'input')).toBe(true); 30 | expect(matchesWithNodeName(elem2, 'INPUT')).toBe(true); 31 | expect(matchesWithNodeName(elem2, 'input')).toBe(true); 32 | expect(matchesWithNodeName(elem1, 'TEXT')).toBe(false); 33 | expect(matchesWithNodeName(elem1, 'text')).toBe(false); 34 | expect(matchesWithNodeName(undefined, 'INPUT')).toBe(false); 35 | expect(matchesWithNodeName(undefined, undefined)).toBe(false); 36 | }); 37 | 38 | test('Test siteMatch()', async ({ page }) => { 39 | expect(siteMatch('https://example.com/*', 'https://example.com/login_page')).toBe(true); 40 | expect(siteMatch('https://*.lexample.com/*', 'https://example.com/login_page')).toBe(false); 41 | expect(siteMatch('https://example.com/*', 'https://example2.com/login_page')).toBe(false); 42 | expect(siteMatch('https://example.com/*', 'https://subdomain.example.com/login_page')).toBe(false); 43 | expect(siteMatch('https://example.com', 'https://subdomain.example.com/login_page')).toBe(false); 44 | expect(siteMatch('https://*.example.com/*', 'https://example.com/login_page')).toBe(true); 45 | expect(siteMatch('https://*.example.com/*', 'https://test.example.com/login_page')).toBe(true); 46 | expect(siteMatch('https://test.example.com/*', 'https://subdomain.example.com/login_page')).toBe(false); 47 | expect(siteMatch('https://test.example.com/page/*', 'https://test.example.com/page/login_page')).toBe(true); 48 | expect(siteMatch('https://test.example.com/page/*', 'https://test.example.com/page/login_page?dontcare=aboutme')).toBe(true); 49 | expect(siteMatch('https://test.example.com/page/another_page/*', 'https://test.example.com/page/login')).toBe(false); 50 | expect(siteMatch('https://test.example.com/path/another/a/', 'https://test.example.com/path/another/a/')).toBe(true); 51 | expect(siteMatch('https://test.example.com/path/another/a/', 'https://test.example.com/path/another/b/')).toBe(false); 52 | expect(siteMatch('https://test.example.com/*/another/a/', 'https://test.example.com/path/another/a/')).toBe(true); 53 | expect(siteMatch('https://test.example.com/path/*/a/', 'https://test.example.com/path/another/a/')).toBe(true); 54 | expect(siteMatch('https://test.example.com/path2/*/a/', 'https://test.example.com/path/another/a/')).toBe(false); 55 | expect(siteMatch('https://example.com:8448/', 'https://example.com/')).toBe(false); 56 | expect(siteMatch('https://example.com:8448/', 'https://example.com:8448/')).toBe(true); 57 | expect(siteMatch('https://example.com:8448/login/page', 'https://example.com/login/page')).toBe(false); 58 | expect(siteMatch('https://example.com:8448/*', 'https://example.com:8448/login/page')).toBe(true); 59 | expect(siteMatch('https://example.com/$/*', 'https://example.com/$/login_page')).toBe(true); // Special character in URL 60 | expect(siteMatch('https://example.com/*/*', 'https://example.com/$/login_page')).toBe(true); 61 | expect(siteMatch('https://example.com/*/*', 'https://example.com/login_page')).toBe(false); 62 | expect(siteMatch('https://*.com/*', 'https://example.com/$/login_page')).toBe(true); 63 | expect(siteMatch('https://*.com/*', 'https://example.org/$/login_page')).toBe(false); 64 | expect(siteMatch('https://*.*/*', 'https://example.org/$/login_page')).toBe(true); 65 | 66 | // IP based URL's 67 | expect(siteMatch('https://127.128.129.130:8448/', 'https://127.128.129.130:8448/')).toBe(true); 68 | expect(siteMatch('https://127.128.129.*:8448/', 'https://127.128.129.130:8448/')).toBe(true); 69 | expect(siteMatch('https://127.128.*/', 'https://127.128.129.130/')).toBe(true); 70 | expect(siteMatch('https://127.128.*/', 'https://127.1.129.130/')).toBe(false); 71 | expect(siteMatch('https://127.128.129.130/', 'https://127.128.129.130:8448/')).toBe(true); 72 | expect(siteMatch('https://127.128.129.*/', 'https://127.128.129.130:8448/')).toBe(true); 73 | 74 | // Invalid URL's 75 | expect(siteMatch('', 'https://example.com')).toBe(false); 76 | expect(siteMatch('abcdefgetc', 'https://example.com')).toBe(false); 77 | expect(siteMatch('{TOTP}\\no', 'https://example.com')).toBe(false); 78 | expect(siteMatch('https://320.320.320.320', 'https://example.com')).toBe(false); 79 | }); 80 | 81 | test('Test slashNeededForUrl()', async ({ page }) => { 82 | expect(slashNeededForUrl('https://test.com')).not.toBe(null); 83 | expect(slashNeededForUrl('https://test.com/')).toBe(null); 84 | }); 85 | 86 | test('Test trimURL()', async ({ page }) => { 87 | expect(trimURL('https://example.com/path/?login=yes&fallback=no')).toBe('https://example.com/path/'); 88 | expect(trimURL('https://example.com/path/?login=yes')).toBe('https://example.com/path/'); 89 | expect(trimURL('https://example.com/path/')).toBe('https://example.com/path/'); 90 | expect(trimURL('https://example.com/path/#extra')).toBe('https://example.com/path/#extra'); 91 | }); 92 | -------------------------------------------------------------------------------- /tests/page.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | //import { getBaseDomainFromUrl, getTopLevelDomainFromUrl } from '../keepassxc-browser/background/page.js'; 3 | 4 | test.skip('Test getTopLevelDomainFromUrl()', async ({ page }) => { 5 | // TODO: Enable these tests later. Not sure how to tests cookies API which requires to be running in the 6 | // background script. 7 | // Top-Level-Domain tests 8 | /*const tldUrls = [ 9 | [ 'another.example.co.uk', 'co.uk' ], 10 | [ 'www.example.com', 'com' ], 11 | [ 'github.com', 'com' ], 12 | [ 'test.net', 'net' ], 13 | [ 'so.many.subdomains.co.jp', 'co.jp' ], 14 | [ '192.168.0.1', '192.168.0.1' ], 15 | [ 'www.nic.ar','ar' ], 16 | [ 'no.no.no', 'no' ], 17 | [ 'www.blogspot.com.ar', 'blogspot.com.ar' ], // blogspot.com.ar is a TLD 18 | [ 'jap.an.ide.kyoto.jp', 'ide.kyoto.jp' ], // ide.kyoto.jp is a TLD 19 | ]; 20 | 21 | for (const d of tldUrls) { 22 | //kpxcAssert(page.getTopLevelDomainFromUrl(d[0]), d[1], testCard, 'getTopLevelDomainFromUrl() for ' + d[0]); 23 | expect(getTopLevelDomainFromUrl(d[0], 'https://' + d[0])).toBe(d[2]); 24 | } 25 | 26 | // Base domain tests 27 | const domains = [ 28 | [ 'another.example.co.uk', 'example.co.uk' ], 29 | [ 'www.example.com', 'example.com' ], 30 | [ 'test.net', 'test.net' ], 31 | [ 'so.many.subdomains.co.jp', 'subdomains.co.jp' ], 32 | [ '192.168.0.1', '192.168.0.1' ], 33 | [ 'www.nic.ar', 'nic.ar' ], 34 | [ 'www.blogspot.com.ar', 'www.blogspot.com.ar' ], // blogspot.com.ar is a TLD 35 | [ 'www.arpa', 'www.arpa' ], 36 | [ 'jap.an.ide.kyoto.jp', 'an.ide.kyoto.jp' ], // ide.kyoto.jp is a TLD 37 | [ 'kobe.jp', 'kobe.jp' ], 38 | ]; 39 | 40 | for (const d of domains) { 41 | //kpxcAssert(page.getBaseDomainFromUrl(d[0]), d[1], testCard, 'getBaseDomainFromUrl() for ' + d[0]); 42 | expect(getBaseDomainFromUrl(d[0], 'https://' + d[0])).toBe(d[1]); 43 | }*/ 44 | }); 45 | -------------------------------------------------------------------------------- /tests/scripts/div1.js: -------------------------------------------------------------------------------- 1 | 'use script'; 2 | 3 | document.getElementById('toggle1').addEventListener('click', function(e) { 4 | const loginForm = document.getElementById('loginForm1'); 5 | 6 | if (loginForm.style.display === 'none') { 7 | loginForm.style.display = 'block'; 8 | } else { 9 | loginForm.style.display = 'none'; 10 | } 11 | }); 12 | -------------------------------------------------------------------------------- /tests/scripts/div2.js: -------------------------------------------------------------------------------- 1 | 'use script'; 2 | 3 | document.getElementById('toggle2').addEventListener('click', function(e) { 4 | const dialog = document.getElementById('dialog'); 5 | const outer = document.getElementById('outer'); 6 | const inner = document.getElementById('inner'); 7 | 8 | if (dialog.style.zIndex === 'auto') { 9 | dialog.style.zIndex = 9999; 10 | inner.style.margin = '0px'; 11 | outer.style.height = 'auto'; 12 | } else { 13 | dialog.style.zIndex = 'auto'; 14 | inner.style.margin = '-197px'; 15 | outer.style.height = '0px'; 16 | } 17 | }); 18 | -------------------------------------------------------------------------------- /tests/scripts/div3.js: -------------------------------------------------------------------------------- 1 | 'use script'; 2 | 3 | document.getElementById('toggle3').addEventListener('click', function(e) { 4 | const loginForm = document.getElementById('loginForm'); 5 | 6 | if (!loginForm) { 7 | const dialog = document.createElement('div'); 8 | dialog.setAttribute('id', 'loginForm'); 9 | 10 | const usernameInput = document.createElement('input'); 11 | usernameInput.setAttribute('type', 'text'); 12 | usernameInput.setAttribute('name', 'loginField'); 13 | usernameInput.setAttribute('placeholder', 'username'); 14 | dialog.append(usernameInput); 15 | 16 | const passwordInput = document.createElement('input'); 17 | passwordInput.setAttribute('type', 'password'); 18 | passwordInput.setAttribute('name', 'passwordField'); 19 | passwordInput.setAttribute('placeholder', 'password'); 20 | dialog.append(passwordInput); 21 | 22 | e.currentTarget.parentElement.appendChild(dialog); 23 | } 24 | }); 25 | -------------------------------------------------------------------------------- /tests/scripts/div4.js: -------------------------------------------------------------------------------- 1 | 'use script'; 2 | 3 | document.getElementById('toggle4').addEventListener('click', function(e) { 4 | const loginForm = document.getElementById('loginForm4'); 5 | 6 | if (!loginForm) { 7 | const dialog = document.createElement('div'); 8 | dialog.setAttribute('id', 'loginForm4'); 9 | dialog.style.position = 'fixed'; 10 | dialog.style.zIndex = '1002'; 11 | 12 | const wrapper = document.createElement('div'); 13 | wrapper.setAttribute('tabIndex', '-1'); 14 | wrapper.style.height = '100%'; 15 | wrapper.style.width = '100%'; 16 | wrapper.style.outline = '0px'; 17 | wrapper.style.overflow = 'visible'; 18 | 19 | const innerDiv = document.createElement('div'); 20 | innerDiv.setAttribute('id', 'innerDiv'); 21 | 22 | const contentDiv = document.createElement('div'); 23 | contentDiv.setAttribute('id', 'contentDiv'); 24 | 25 | const form = document.createElement('form'); 26 | form.setAttribute('action', 'loginUser'); 27 | form.setAttribute('method', 'post'); 28 | 29 | const divUsernameWithLabel = document.createElement('div'); 30 | const divPasswordWithLabel = document.createElement('div'); 31 | 32 | const usernameInput = document.createElement('input'); 33 | usernameInput.setAttribute('type', 'text'); 34 | usernameInput.setAttribute('name', 'loginField'); 35 | usernameInput.setAttribute('placeholder', 'username'); 36 | divUsernameWithLabel.append(usernameInput); 37 | 38 | const passwordInput = document.createElement('input'); 39 | passwordInput.setAttribute('type', 'password'); 40 | passwordInput.setAttribute('name', 'passwordField'); 41 | passwordInput.setAttribute('placeholder', 'password'); 42 | divPasswordWithLabel.append(passwordInput); 43 | 44 | form.append(divUsernameWithLabel); 45 | form.append(divPasswordWithLabel); 46 | contentDiv.append(form); 47 | innerDiv.append(contentDiv); 48 | wrapper.append(innerDiv); 49 | dialog.append(wrapper); 50 | 51 | e.currentTarget.parentElement.appendChild(dialog); 52 | } 53 | }); 54 | -------------------------------------------------------------------------------- /tests/tests.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Tests = { 4 | GENERAL: '#general-results', 5 | INPUT_FIELDS: '#input-field-results', 6 | TOTP_FIELDS: '#totp-field-results', 7 | SEARCH_FIELDS: '#search-field-results', 8 | PASSWORD_CHANGE: '#password-change-results', 9 | }; 10 | 11 | function createResult(card, res, text) { 12 | const icon = kpxcUI.createElement('i', res ? 'fa fa-check' : 'fa fa-close', { id: text }); 13 | const span = kpxcUI.createElement('span', '', '', text); 14 | const br = document.createElement('br'); 15 | 16 | document.querySelector(card).appendMultiple(icon, span, br); 17 | } 18 | 19 | // Input field matching (keepassxc-browser.js) 20 | async function testInputFields() { 21 | // Div ID, expected fields, action element ID (a button to be clicked) 22 | const testDivs = [ 23 | [ 'basic1', 2 ], // Username/passwd fields 24 | [ 'basic2', 1 ], // Only username field 25 | [ 'basic3', 1 ], // Only password field 26 | [ 'basic4', 3 ], // Username/passwd/TOTP fields 27 | [ 'div1', 2, '#toggle1' ], // Fields are behind a button that must be pressed 28 | [ 'div2', 2, '#toggle2' ], // Fields are behind a button that must be pressed behind a JavaScript 29 | [ 'div3', 2, '#toggle3' ], // Fields are behind a button that must be pressed 30 | [ 'div4', 2, '#toggle4' ], // Fields are behind a button that must be pressed 31 | [ 'hiddenFields1', 0 ], // Two hidden fields 32 | [ 'hiddenFields2', 1 ], // Two hidden fields with one visible 33 | ]; 34 | 35 | for (const div of testDivs) { 36 | await assertInputFields(div[0], div[1], div[2]); 37 | } 38 | } 39 | 40 | // Search fields (kpxcFields 41 | async function testSearchFields() { 42 | const searchFields = [ 43 | [ '', { id: 'otp_field', name: 'otp', type: 'text', maxLength: '8' }, 'Generic 2FA field', false ], 44 | [ '', { placeholder: 'search', type: 'text', id: 'username' }, 'Placeholder only', true ], 45 | [ '', { ariaLabel: 'search', type: 'text', id: 'username' }, 'aria-label only', true ], 46 | 47 | ]; 48 | 49 | for (const field of searchFields) { 50 | assertSearchField(field[0], field[1], field[2], field[3]); 51 | } 52 | 53 | assertSearchForm({ id: 'username', type: 'text', }, 'Generic input field under search form', true); 54 | } 55 | 56 | // TOTP fields (kpxcTOTPIcons) 57 | async function testTotpFields() { 58 | const totpFields = [ 59 | [ '', { id: 'otp_field', name: 'otp', type: 'text', maxLength: '8' }, 'Generic 2FA field', true ], 60 | [ '', { id: '2fa', type: 'text', maxLength: '6' }, 'Generic 2FA field', true ], 61 | [ '', { id: '2fa', type: 'text', maxLength: '4' }, 'Ignore if field maxLength too small', false ], 62 | [ '', { id: '2fa', type: 'text', maxLength: '12' }, 'Ignore if field maxLength too long', false ], 63 | [ '', { id: 'encode', type: 'text', }, 'encode id is not acceptable', false ], 64 | [ '', { id: 'encodedText', type: 'text', }, 'encodedText is not acceptable', false ], 65 | [ '', { id: 'decoder', type: 'text', }, 'decoder id is not acceptable', false ], 66 | [ '', { id: 'code', type: 'text', }, 'code id is accepted', true ], 67 | [ '', { id: '2fa', type: 'text', maxLength: '12', autocomplete: 'one-time-code' }, 'Accept if one-time-code', true ], 68 | [ '', { id: 'username', type: 'text', }, 'Ignore a generic input field', false ], 69 | [ '', { type: 'password', }, 'Ignore a password input field', false ], 70 | [ // Protonmail 71 | 'TwoFA-input ng-empty ng-invalid ng-invalid-required ng-valid-minlength ng-valid-maxlength ng-touched', 72 | { autocapitalize: 'off', autocorrect: 'off', id: 'twoFactorCode', type: 'text', placeholder: 'Two-factor passcode', name: 'twoFactorCode' }, 73 | 'Protonmail 2FA', 74 | true 75 | ], 76 | [ // Nextcloud 77 | '', 78 | { minlength: '6', maxLength: '10', name: 'challenge', placeholder: 'Authentication code', type: 'tel', }, 79 | 'Nextcloud 2FA', 80 | true 81 | ], 82 | [ // GMail 83 | 'whsOnd zHQkBf', 84 | { autocomplete: 'off', id: 'idvPin', tabindex: '0', name: 'idvPin', pattern: '[0-9 ]*', type: 'tel', spellcheck: 'false' }, 85 | 'GMail 2FA', 86 | true 87 | ], 88 | [ // Live.com 89 | 'form-control', 90 | { autocomplete: 'off', id: 'idTxtBx_SAOTCC_OTC', maxLength: '8', tabindex: '0', name: 'otc', placeholder: 'Code', type: 'tel' }, 91 | 'Live.com 2FA', 92 | true 93 | ], 94 | ]; 95 | 96 | for (const field of totpFields) { 97 | assertTOTPField(field[0], field[1], field[2], field[3]); 98 | } 99 | } 100 | 101 | // Password change 102 | async function testPasswordChange() { 103 | // Div ID, expected new password 104 | const localDivs = [ 105 | [ 'passwordChange1', 'newPassword' ], // Default order without form 106 | [ 'passwordChange2', 'newPassword' ], // Reversed order without form 107 | [ 'passwordChange3', 'newPassword' ], // Default order with form 108 | [ 'passwordChange4', 'newPassword' ], // Reversed order with form 109 | [ 'passwordChange5', 'newPassword' ], // Each field has own form 110 | ]; 111 | 112 | for (const div of localDivs) { 113 | await assertPasswordChangeFields(div[0], div[1]); 114 | } 115 | } 116 | 117 | // Run tests 118 | (async () => { 119 | await Promise.all([ 120 | await testInputFields(), 121 | await testSearchFields(), 122 | await testTotpFields(), 123 | await testPasswordChange(), 124 | ]); 125 | })(); 126 | --------------------------------------------------------------------------------