diff --git a/keepassxc-browser/_locales/en/messages.json b/keepassxc-browser/_locales/en/messages.json index 8b839b0..fd64dd8 100644 --- a/keepassxc-browser/_locales/en/messages.json +++ b/keepassxc-browser/_locales/en/messages.json @@ -31,6 +31,10 @@ "message": "Fill attribute…", "description": "Context menu parent item for filling a custom attribute." }, + "contextMenuRequestGlobalAutoType": { + "message": "Request Global Auto-Type", + "description": "Global Auto-Type shortcut text." + }, "multipleCredentialsDetected": { "message": "HTTP authentication with multiple credentials detected. Click on the extension icon to choose the correct one.", "description": "Notification when HTTP Authentication has multiple credentials detected." diff --git a/keepassxc-browser/background/event.js b/keepassxc-browser/background/event.js index 69a5fdc..b69f8e1 100755 --- a/keepassxc-browser/background/event.js +++ b/keepassxc-browser/background/event.js @@ -236,6 +236,7 @@ kpxcEvent.messageHandlers = { 'popup_login': kpxcEvent.onLoginPopup, 'reconnect': kpxcEvent.onReconnect, 'remove_credentials_from_tab_information': kpxcEvent.onRemoveCredentialsFromTabInformation, + 'request_autotype': keepass.requestAutotype, 'retrieve_credentials': page.retrieveCredentials, 'show_default_browseraction': browserAction.showDefault, 'update_credentials': keepass.updateCredentials, diff --git a/keepassxc-browser/background/init.js b/keepassxc-browser/background/init.js index e02d495..4e76085 100644 --- a/keepassxc-browser/background/init.js +++ b/keepassxc-browser/background/init.js @@ -114,7 +114,8 @@ const contextMenuItems = [ { title: tr('contextMenuFillTOTP'), action: 'fill_totp' }, { title: tr('contextMenuFillAttribute'), id: 'fill_attribute', visible: false }, { title: tr('contextMenuShowPasswordGenerator'), action: 'show_password_generator' }, - { title: tr('contextMenuSaveCredentials'), action: 'save_credentials' } + { title: tr('contextMenuSaveCredentials'), action: 'save_credentials' }, + { title: tr('contextMenuRequestGlobalAutoType'), action: 'request_autotype' } ]; const menuContexts = [ 'editable' ]; diff --git a/keepassxc-browser/background/keepass.js b/keepassxc-browser/background/keepass.js index c13e350..c98faff 100755 --- a/keepassxc-browser/background/keepass.js +++ b/keepassxc-browser/background/keepass.js @@ -37,7 +37,8 @@ const kpActions = { DATABASE_UNLOCKED: 'database-unlocked', GET_DATABASE_GROUPS: 'get-database-groups', CREATE_NEW_GROUP: 'create-new-group', - GET_TOTP: 'get-totp' + GET_TOTP: 'get-totp', + REQUEST_AUTOTYPE: 'request-autotype' }; const kpErrors = { @@ -852,6 +853,50 @@ keepass.getTotp = async function(tab, args = []) { } }; +keepass.requestAutotype = async function(tab, args = []) { + if (!keepass.isConnected) { + keepass.handleError(tab, kpErrors.TIMEOUT_OR_NOT_CONNECTED); + return false; + } + + const kpAction = kpActions.REQUEST_AUTOTYPE; + const [ nonce, incrementedNonce ] = keepass.getNonces(); + const search = getTopLevelDomainFromUrl(args[0]); + + const messageData = { + action: kpAction, + search: search + }; + + const request = keepass.buildRequest(kpAction, keepass.encrypt(messageData, nonce), nonce, keepass.clientID); + + try { + const response = await keepass.sendNativeMessage(request); + if (response.message && response.nonce) { + const res = keepass.decrypt(response.message, response.nonce); + if (!res) { + keepass.handleError(tab, kpErrors.CANNOT_DECRYPT_MESSAGE); + return false; + } + + const message = nacl.util.encodeUTF8(res); + const parsed = JSON.parse(message); + + if (keepass.verifyResponse(parsed, incrementedNonce)) { + return true; + } + } else if (response.error && response.errorCode) { + keepass.handleError(tab, response.errorCode, response.error); + } + + return false; + } catch (err) { + console.log('requestAutotype failed: ', err); + return false; + } +}; + + keepass.generateNewKeyPair = function() { keepass.keyPair = nacl.box.keyPair(); //console.log(nacl.util.encodeBase64(keepass.keyPair.publicKey) + ' ' + nacl.util.encodeBase64(keepass.keyPair.secretKey)); diff --git a/keepassxc-browser/common/global.js b/keepassxc-browser/common/global.js index 4fd79ae..2f8ffd7 100755 --- a/keepassxc-browser/common/global.js +++ b/keepassxc-browser/common/global.js @@ -140,6 +140,19 @@ const slashNeededForUrl = function(pattern) { return matchPattern.exec(pattern); }; +// Returns the top level domain, e.g. https://another.example.co.uk -> example.co.uk +// This is done because a top level domain will probably give better matches with Auto-Type than a full hostname. +const getTopLevelDomainFromUrl = function(hostname) { + const domainRegex = new RegExp(/(\w+).(com|net|org|edu|co)*(.\w+)$/g); + const domainMatch = domainRegex.exec(hostname); + + if (domainMatch) { + return domainMatch[0]; + } + + return hostname; +}; + function tr(key, params) { return browser.i18n.getMessage(key, params); } diff --git a/keepassxc-browser/content/keepassxc-browser.js b/keepassxc-browser/content/keepassxc-browser.js index 5b897d1..9cceb0c 100755 --- a/keepassxc-browser/content/keepassxc-browser.js +++ b/keepassxc-browser/content/keepassxc-browser.js @@ -2114,6 +2114,8 @@ browser.runtime.onMessage.addListener(async function(req, sender) { await kpxc.retrieveCredentials(true); } else if (req.action === 'show_password_generator') { kpxcPasswordDialog.trigger(); + } else if (req.action === 'request_autotype') { + sendMessage('request_autotype', [ window.location.hostname ]); } } }); diff --git a/keepassxc-browser/manifest.json b/keepassxc-browser/manifest.json index 571fcbd..c5bdddc 100755 --- a/keepassxc-browser/manifest.json +++ b/keepassxc-browser/manifest.json @@ -108,6 +108,9 @@ }, "retrive_credentials_forced": { "description": "__MSG_popupReopenButton__" + }, + "request_autotype": { + "description": "__MSG_contextMenuRequestGlobalAutoType__" } }, "web_accessible_resources": [ diff --git a/keepassxc-protocol.md b/keepassxc-protocol.md index 8f7829b..be529e5 100644 --- a/keepassxc-protocol.md +++ b/keepassxc-protocol.md @@ -404,3 +404,21 @@ Response message data (success, decrypted): "nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q" } ``` + +### request-autotype (KeePassXC 2.7.0 and newer) +Request (no unencrypted message is needed): +```json +{ + "action": "request-autotype", + "search": "" +} +``` + +Response message data (success, decrypted): +```json +{ + "version": "2.7.0", + "success": "true", + "nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q" +} +``` diff --git a/tests/tests.js b/tests/tests.js index ee6144a..61a7099 100644 --- a/tests/tests.js +++ b/tests/tests.js @@ -43,6 +43,20 @@ async function testGeneral() { for (const m of matches) { assertRegex(siteMatch(m[0], m[1]), m[2], testCard, `siteMatch() for ${m[1]}`); } + + // Base domain parsing (window.location.hostname) + const domains = [ + [ 'another.example.co.uk', 'example.co.uk' ], + [ 'www.example.com', 'example.com' ], + [ 'test.net', 'test.net' ], + [ 'so.many.subdomains.co.jp', 'subdomains.co.jp' ], + [ 'test.site.example.com.au', 'example.com.au' ], + [ '192.168.0.1', '192.168.0.1' ] + ]; + + for (const d of domains) { + kpxcAssert(getTopLevelDomainFromUrl(d[0]), d[1], testCard, 'getBaseDomainFromUrl() for ' + d[0]); + } } // Input field matching (keepassxc-browser.js)