From 6592708e936266bb243283615bb9084c5db39712 Mon Sep 17 00:00:00 2001 From: varjolintu Date: Mon, 21 Mar 2022 17:15:02 +0200 Subject: [PATCH] Add support for WebAuthn (Passkeys) --- .eslintrc | 1 + keepassxc-browser/_locales/en/messages.json | 52 +++ keepassxc-browser/background/client.js | 18 +- keepassxc-browser/background/event.js | 3 + keepassxc-browser/background/keepass.js | 100 ++++- keepassxc-browser/background/page.js | 6 +- .../content/keepassxc-browser.js | 42 ++ keepassxc-browser/content/passkeys-utils.js | 171 ++++++++ keepassxc-browser/content/passkeys.js | 74 ++++ keepassxc-browser/manifest.json | 6 +- keepassxc-browser/options/options.html | 27 ++ keepassxc-browser/options/options.js | 10 + keepassxc-protocol.md | 390 +++++++++++------- 13 files changed, 731 insertions(+), 169 deletions(-) create mode 100644 keepassxc-browser/content/passkeys-utils.js create mode 100644 keepassxc-browser/content/passkeys.js diff --git a/.eslintrc b/.eslintrc index 2f63d646..01a3c1ff 100644 --- a/.eslintrc +++ b/.eslintrc @@ -144,6 +144,7 @@ "kpxcUsernameIcons": true, "logDebug": true, "logError": true, + "kpxcPasskeysUtils": true, "ManualFill": true, "MAX_AUTOCOMPLETE_NAME_LEN": true, "MAX_OPACITY": true, diff --git a/keepassxc-browser/_locales/en/messages.json b/keepassxc-browser/_locales/en/messages.json index b61158d5..c1e885ab 100644 --- a/keepassxc-browser/_locales/en/messages.json +++ b/keepassxc-browser/_locales/en/messages.json @@ -107,6 +107,38 @@ "message": "Filled password is longer than field's allowed max length.", "description": "Error notification text shown when filled password is longer than defined maxLength of the input field." }, + "errorMessageNoGroupsFound": { + "message": "No groups found.", + "description": "No groups found." + }, + "errorMessageCannotCreateNewGroup": { + "message": "Cannot create new group.", + "description": "Cannot create new group." + }, + "errorMessageNoValidUuidProvided": { + "message": "No valid UUID provided.", + "description": "No valid UUID provided." + }, + "errorMessagePasskeysAttestationNotSupported": { + "message": "Attestation not supported.", + "description": "Attestation not supported." + }, + "errorMessagePasskeysCredentialIsExcluded": { + "message": "Credential is excluded.", + "description": "Credential is excluded." + }, + "errorMessagePasskeysRequestCanceled": { + "message": "Passkeys request canceled.", + "description": "Passkeys request canceled." + }, + "errorMessagePasskeysInvalidUserVerification": { + "message": "Invalid user verification.", + "description": "Invalid user verification." + }, + "errorMessagePasskeysEmptyPublicKey": { + "message": "Empty public key.", + "description": "Empty public key." + }, "errorNotConnected": { "message": "Not connected to KeePassXC.", "description": "Error notification shown when not connected to KeePassXC" @@ -1191,6 +1223,26 @@ "message": "Extension", "description": "Extension title in settings page" }, + "optionsPasskeysTitle": { + "message": "Passkeys", + "description": "Passkeys settings title in settings page." + }, + "optionsPasskeysEnable": { + "message": "Enable Passkeys", + "description": "Enabled Passkeys option text." + }, + "optionsPasskeysEnableHelpText": { + "message": "Enable support for Web Authentication.", + "description": "Passkeys option help text." + }, + "optionsPasskeysEnableFallback": { + "message": "Enable Passkeys fallback", + "description": "Enabled Passkeys fallback option text." + }, + "optionsPasskeysEnableFallbackHelpText": { + "message": "When enabled, a failed or canceled request to KeePassXC will trigger the browser's own internal Passkeys request. If disabled, connection to KeePassXC is required and canceled request will fail. Default: enabled.", + "description": "Passkeys fallback option help text." + }, "openNewTab": { "message": "Opens a new tab", "description": "Title attribute text." diff --git a/keepassxc-browser/background/client.js b/keepassxc-browser/background/client.js index 0f6a5dff..0158790b 100644 --- a/keepassxc-browser/background/client.js +++ b/keepassxc-browser/background/client.js @@ -23,6 +23,14 @@ const kpErrors = { EMPTY_MESSAGE_RECEIVED: 13, NO_URL_PROVIDED: 14, NO_LOGINS_FOUND: 15, + NO_GROUPS_FOUND: 16, + CANNOT_CREATE_NEW_GROUP: 17, + NO_VALID_UUID_PROVIDED: 18, + PASSKEYS_ATTESTATION_NOT_SUPPORTED: 19, + PASSKEYS_CREDENTIAL_IS_EXCLUDED: 20, + PASSKEYS_REQUEST_CANCELED: 21, + PASSKEYS_INVALID_USER_VERIFICATION: 22, + PASSKEYS_EMPTY_PUBLIC_KEY: 23, errorMessages: { 0: { msg: tr('errorMessageUnknown') }, @@ -40,7 +48,15 @@ const kpErrors = { 12: { msg: tr('errorMessageIncorrectAction') }, 13: { msg: tr('errorMessageEmptyMessage') }, 14: { msg: tr('errorMessageNoURL') }, - 15: { msg: tr('errorMessageNoLogins') } + 15: { msg: tr('errorMessageNoLogins') }, + 16: { msg: tr('errorMessageNoGroupsFound') }, + 17: { msg: tr('errorMessageCannotCreateNewGroup') }, + 18: { msg: tr('errorMessageNoValidUuidProvided') }, + 19: { msg: tr('errorMessagePasskeysAttestationNotSupported') }, + 20: { msg: tr('errorMessagePasskeysCredentialIsExcluded') }, + 21: { msg: tr('errorMessagePasskeysRequestCanceled') }, + 22: { msg: tr('errorMessagePasskeysInvalidUserVerification') }, + 23: { msg: tr('errorMessagePasskeysEmptyPublicKey') }, }, getError(errorCode) { diff --git a/keepassxc-browser/background/event.js b/keepassxc-browser/background/event.js index db8afbd4..e77e6073 100755 --- a/keepassxc-browser/background/event.js +++ b/keepassxc-browser/background/event.js @@ -243,6 +243,7 @@ kpxcEvent.messageHandlers = { 'get_connected_database': kpxcEvent.onGetConnectedDatabase, 'get_database_hash': keepass.getDatabaseHash, 'get_database_groups': keepass.getDatabaseGroups, + 'get_error_message': keepass.getErrorMessage, 'get_keepassxc_versions': kpxcEvent.onGetKeePassXCVersions, 'get_login_list': page.getLoginList, 'get_status': kpxcEvent.onGetStatus, @@ -266,6 +267,8 @@ kpxcEvent.messageHandlers = { 'page_set_login_id': page.setLoginId, 'page_set_manual_fill': page.setManualFill, 'page_set_submitted': page.setSubmitted, + 'passkeys_get': keepass.passkeysGet, + 'passkeys_register': keepass.passkeysRegister, 'password_get_filled': kpxcEvent.passwordGetFilled, 'password_set_filled': kpxcEvent.passwordSetFilled, 'popup_login': kpxcEvent.onLoginPopup, diff --git a/keepassxc-browser/background/keepass.js b/keepassxc-browser/background/keepass.js index 19ea3b11..cc50daad 100755 --- a/keepassxc-browser/background/keepass.js +++ b/keepassxc-browser/background/keepass.js @@ -31,7 +31,9 @@ const kpActions = { GET_DATABASE_GROUPS: 'get-database-groups', CREATE_NEW_GROUP: 'create-new-group', GET_TOTP: 'get-totp', - REQUEST_AUTOTYPE: 'request-autotype' + REQUEST_AUTOTYPE: 'request-autotype', + PASSKEYS_REGISTER: 'passkeys-register', + PASSKEYS_GET: 'passkeys-get' }; browser.storage.local.get({ 'latestKeePassXC': { 'version': '', 'lastChecked': null }, 'keyRing': {} }).then((item) => { @@ -117,23 +119,15 @@ keepass.retrieveCredentials = async function(tab, args = []) { } let entries = []; - const keys = []; const kpAction = kpActions.GET_LOGINS; const nonce = keepassClient.getNonce(); const [ dbid ] = keepass.getCryptoKey(); - for (const keyHash in keepass.keyRing) { - keys.push({ - id: keepass.keyRing[keyHash].id, - key: keepass.keyRing[keyHash].key - }); - } - const messageData = { action: kpAction, id: dbid, url: url, - keys: keys + keys: keepass.getCryptoKeys() }; if (submiturl) { @@ -450,7 +444,6 @@ keepass.lockDatabase = async function(tab) { action: kpAction }; - try { const response = await keepassClient.sendMessage(kpAction, tab, messageData, nonce); if (response) { @@ -605,6 +598,74 @@ keepass.requestAutotype = async function(tab, args = []) { } }; +keepass.passkeysRegister = async function(tab, args = []) { + try { + const taResponse = await keepass.testAssociation(tab, [ false ]); + if (!taResponse || !keepass.isConnected || args.length < 2) { + browserAction.showDefault(tab); + return []; + } + + const kpAction = kpActions.PASSKEYS_REGISTER; + const nonce = keepassClient.getNonce(); + + // Parse publicKey + const publicKey = args[0]; + const origin = args[1]; + + const messageData = { + action: kpAction, + publicKey: JSON.parse(JSON.stringify(publicKey)), + origin: origin, + keys: keepass.getCryptoKeys() + }; + + const response = await keepassClient.sendMessage(kpAction, tab, messageData, nonce); + if (response) { + return response; + } + + browserAction.showDefault(tab); + return []; + } catch (err) { + logError(`passkeysRegister failed: ${err}`); + return []; + } +}; + +keepass.passkeysGet = async function(tab, args = []) { + try { + const taResponse = await keepass.testAssociation(tab, [ false ]); + if (!taResponse || !keepass.isConnected || args.length < 2) { + browserAction.showDefault(tab); + return []; + } + + const kpAction = kpActions.PASSKEYS_GET; + const nonce = keepassClient.getNonce(); + const publicKey = args[0]; + const origin = args[1]; + + const messageData = { + action: kpAction, + publicKey: JSON.parse(JSON.stringify(publicKey)), + origin: origin, + keys: keepass.getCryptoKeys() + }; + + const response = await keepassClient.sendMessage(kpAction, tab, messageData, nonce); + if (response) { + return response; + } + + browserAction.showDefault(tab); + return []; + } catch (err) { + logError(`passkeysGet failed: ${err}`); + return []; + } +}; + //-------------------------------------------------------------------------- // Keyring //-------------------------------------------------------------------------- @@ -704,6 +765,19 @@ keepass.setCryptoKey = function(id, key) { keepass.saveKey(keepass.databaseHash, id, key); }; +keepass.getCryptoKeys = function() { + const keys = []; + + for (const keyHash in keepass.keyRing) { + keys.push({ + id: keepass.keyRing[keyHash].id, + key: keepass.keyRing[keyHash].key + }); + } + + return keys; +}; + //-------------------------------------------------------------------------- // Connection //-------------------------------------------------------------------------- @@ -756,6 +830,10 @@ keepass.reconnect = async function(tab, connectionTimeout) { // Utils //-------------------------------------------------------------------------- +keepass.getErrorMessage = async function(tab, errorCode) { + return kpErrors.getError(errorCode); +}; + keepass.generateNewKeyPair = function() { keepass.keyPair = nacl.box.keyPair(); }; diff --git a/keepassxc-browser/background/page.js b/keepassxc-browser/background/page.js index 1c72400b..0774047a 100755 --- a/keepassxc-browser/background/page.js +++ b/keepassxc-browser/background/page.js @@ -18,7 +18,9 @@ const defaultSettings = { defaultGroup: '', defaultGroupAlwaysAsk: false, downloadFaviconAfterSave: false, - redirectAllowance: 3, + passkeys: false, + passkeysFallback: true, + redirectAllowance: 1, saveDomainOnly: true, showGettingStartedGuideAlert: true, showTroubleshootingGuideAlert: true, @@ -29,7 +31,7 @@ const defaultSettings = { showOTPIcon: true, useObserver: true, usePredefinedSites: true, - usePasswordGeneratorIcons: false + usePasswordGeneratorIcons: false, }; const AUTO_SUBMIT_TIMEOUT = 5000; diff --git a/keepassxc-browser/content/keepassxc-browser.js b/keepassxc-browser/content/keepassxc-browser.js index e59f929f..69cf7931 100755 --- a/keepassxc-browser/content/keepassxc-browser.js +++ b/keepassxc-browser/content/keepassxc-browser.js @@ -784,6 +784,44 @@ kpxc.updateTOTPList = async function() { return []; }; +// Apply a script to the page for intercepting Passkeys (WebAuthn) requests +kpxc.enablePasskeys = function() { + const passkeys = document.createElement('script'); + passkeys.src = browser.runtime.getURL('content/passkeys.js'); + document.documentElement.appendChild(passkeys); + + document.addEventListener('kpxc-passkeys-request', async (ev) => { + if (ev.detail.action === 'passkeys_create') { + const publicKey = kpxcPasskeysUtils.buildCredentialCreationOptions(ev.detail.publicKey); + logDebug(publicKey); + + const ret = await sendMessage('passkeys_register', [ publicKey, window.location.origin ]); + if (ret) { + if (ret.response && ret.response.errorCode) { + const errorMessage = await sendMessage('get_error_message', ret.response.errorCode); + kpxcUI.createNotification('error', errorMessage); + } + + const responsePublicKey = kpxcPasskeysUtils.parsePublicKeyCredential(ret.response); + kpxcPasskeysUtils.sendPasskeysResponse(responsePublicKey); + } + } else if (ev.detail.action === 'passkeys_get') { + const publicKey = kpxcPasskeysUtils.buildCredentialRequestOptions(ev.detail.publicKey); + logDebug(publicKey); + + const ret = await sendMessage('passkeys_get', [ publicKey, window.location.origin ]); + if (ret) { + if (ret.response && ret.response.errorCode) { + const errorMessage = await sendMessage('get_error_message', ret.response.errorCode); + kpxcUI.createNotification('error', errorMessage); + } + + const responsePublicKey = kpxcPasskeysUtils.parseGetPublicKeyCredential(ret.response); + kpxcPasskeysUtils.sendPasskeysResponse(responsePublicKey); + } + } + }); +}; /** * Content script initialization. @@ -803,6 +841,10 @@ const initContentScript = async function() { return; } + if (kpxc.settings.passkeys) { + kpxc.enablePasskeys(); + } + await kpxc.updateDatabaseState(); await kpxc.initCredentialFields(); diff --git a/keepassxc-browser/content/passkeys-utils.js b/keepassxc-browser/content/passkeys-utils.js new file mode 100644 index 00000000..b437c1df --- /dev/null +++ b/keepassxc-browser/content/passkeys-utils.js @@ -0,0 +1,171 @@ +'use strict'; + +const stringToArrayBuffer = function(str) { + const arr = Uint8Array.from(str, c => c.charCodeAt(0)); + return arr.buffer; +}; + +// From URL encoded base64 string to ArrayBuffer +const base64ToArrayBuffer = function(str) { + return stringToArrayBuffer(window.atob(str.replaceAll('-', '+').replaceAll('_', '/'))); +}; + +// From ArrayBuffer to URL encoded base64 string +const arrayBufferToBase64 = function(buf) { + const str = [ ...new Uint8Array(buf) ].map(c => String.fromCharCode(c)).join(''); + return window.btoa(str).replaceAll('+', '-').replaceAll('/', '_').replaceAll('=', ''); +}; + +// Error checks for both registration and authentication +const checkErrors = function(pkOptions) { + if (pkOptions.sameOriginWithAncestors !== undefined && pkOptions.sameOriginWithAncestors === false) { + throw new DOMException('Cross-origin register is not allowed.', DOMException.NotAllowedError); + } + + if (pkOptions.challenge.length < 16) { + throw new TypeError('challenge is shorter than required minimum length.'); + } +}; + +const kpxcPasskeysUtils = {}; + +// Sends response from KeePassXC back to the injected script +kpxcPasskeysUtils.sendPasskeysResponse = function(publicKey) { + const response = { publicKey: publicKey, fallback: kpxc.settings.passkeysFallback }; + const details = isFirefox() ? cloneInto(response, document.defaultView) : response; + document.dispatchEvent(new CustomEvent('kpxc-passkeys-response', { detail: details })); +}; + +// Create a new object with base64 strings for KeePassXC +kpxcPasskeysUtils.buildCredentialCreationOptions = function(pkOptions) { + try { + checkErrors(pkOptions); + + if (pkOptions.user.id && (pkOptions.user.id.length < 1 || pkOptions.user.id.length > 64)) { + throw new TypeError('user.id does not match the required length.'); + } + + if (!pkOptions.rp.id) { + pkOptions.rp.id = window.location.host; + pkOptions.rp.name = window.location.host; + } else if (!window.location.host.endsWith(pkOptions.rp.id)) { + throw new DOMException('Site domain differs from RP ID', DOMException.SecurityError); + } + + if (!pkOptions.pubKeyCredParams || pkOptions.pubKeyCredParams.length === 0) { + pkOptions.pubKeyCredParams.push({ + 'type': 'public-key', + 'alg': -7 + }); + pkOptions.pubKeyCredParams.push({ + 'type': 'public-key', + 'alg': -257 + }); + } + + const publicKey = {}; + publicKey.attestation = pkOptions.attestation; + publicKey.authenticatorSelection = pkOptions.authenticatorSelection; + publicKey.challenge = arrayBufferToBase64(pkOptions.challenge); + publicKey.extensions = pkOptions.extensions; + publicKey.pubKeyCredParams = pkOptions.pubKeyCredParams; + publicKey.rp = pkOptions.rp; + publicKey.timeout = pkOptions.timeout; + + publicKey.excludeCredentials = []; + if (pkOptions.excludeCredentials && pkOptions.excludeCredentials.length > 0) { + for (const cred of pkOptions.excludeCredentials) { + const arr = { + id: arrayBufferToBase64(cred.id), + transports: cred.transports, + type: cred.type + }; + + publicKey.excludeCredentials.push(arr); + } + } + + publicKey.user = {}; + publicKey.user.displayName = pkOptions.user.displayName; + publicKey.user.id = arrayBufferToBase64(pkOptions.user.id); + publicKey.user.name = pkOptions.user.name; + + return publicKey; + } catch (e) { + console.log(e); + } +}; + +// Create a new object with base64 strings for KeePassXC +kpxcPasskeysUtils.buildCredentialRequestOptions = function(pkOptions) { + try { + checkErrors(pkOptions); + + if (!pkOptions.rpId) { + pkOptions.rpId = window.location.host; + } else if (!window.location.host.endsWith(pkOptions.rpId)) { + throw new DOMException('Site domain differs from RP ID', DOMException.SecurityError); + } + + const publicKey = {}; + publicKey.challenge = arrayBufferToBase64(pkOptions.challenge); + publicKey.rpId = pkOptions.rpId; + publicKey.timeout = pkOptions.timeout; + publicKey.userVerification = pkOptions.userVerification; + + publicKey.allowCredentials = []; + if (pkOptions.allowCredentials && pkOptions.allowCredentials.length > 0) { + for (const cred of pkOptions.allowCredentials) { + const transports = []; + if (cred.transports) { + for (const tp of cred.transports) { + transports.push(tp); + } + } + + const arr = { + id: arrayBufferToBase64(cred.id), + transports: transports, + type: cred.type + }; + + publicKey.allowCredentials.push(arr); + } + } + + return publicKey; + } catch (e) { + console.log(e); + } +}; + +// Parse register response back from base64 strings to ByteArrays +kpxcPasskeysUtils.parsePublicKeyCredential = function(publicKeyCredential) { + if (!publicKeyCredential || !publicKeyCredential.type) { + return undefined; + } + + publicKeyCredential.rawId = base64ToArrayBuffer(publicKeyCredential.id); + publicKeyCredential.response.attestationObject = base64ToArrayBuffer(publicKeyCredential.response.attestationObject); + publicKeyCredential.response.clientDataJSON = base64ToArrayBuffer(publicKeyCredential.response.clientDataJSON); + + return publicKeyCredential; +}; + +// Parse authentication response back from base64 strings to ByteArrays +kpxcPasskeysUtils.parseGetPublicKeyCredential = function(publicKeyCredential) { + if (!publicKeyCredential || !publicKeyCredential.type) { + return undefined; + } + + publicKeyCredential.rawId = base64ToArrayBuffer(publicKeyCredential.id); + publicKeyCredential.response.authenticatorData = base64ToArrayBuffer(publicKeyCredential.response.authenticatorData); + publicKeyCredential.response.clientDataJSON = base64ToArrayBuffer(publicKeyCredential.response.clientDataJSON); + publicKeyCredential.response.signature = base64ToArrayBuffer(publicKeyCredential.response.signature); + + if (publicKeyCredential.response.userHandle) { + publicKeyCredential.response.userHandle = base64ToArrayBuffer(publicKeyCredential.response.userHandle); + } + + return publicKeyCredential; +}; diff --git a/keepassxc-browser/content/passkeys.js b/keepassxc-browser/content/passkeys.js new file mode 100644 index 00000000..2aa6fb5a --- /dev/null +++ b/keepassxc-browser/content/passkeys.js @@ -0,0 +1,74 @@ +'use strict'; + +// Posts a message to extension's content script and waits for response +const postMessageToExtension = function(request) { + return new Promise((resolve, reject) => { + const ev = document; + + const listener = ((messageEvent) => { + const handler = (msg) => { + if (msg && msg.type === 'kpxc-passkeys-response' && msg.detail) { + messageEvent.removeEventListener('kpxc-passkeys-response', listener); + resolve(msg.detail); + return; + } + }; + return handler; + })(ev); + ev.addEventListener('kpxc-passkeys-response', listener); + + // Send the request + document.dispatchEvent(new CustomEvent('kpxc-passkeys-request', { detail: request })); + }); +}; + +(async () => { + const originalCredentials = navigator.credentials; + + const passkeysCredentials = { + async create(options) { + if (!options.publicKey) { + return null; + } + + const response = await postMessageToExtension({ action: 'passkeys_create', publicKey: options.publicKey }); + if (!response.publicKey) { + return response.fallback ? originalCredentials.create(options) : null; + } + + response.publicKey.getClientExtensionResults = () => {}; + response.publicKey.clientExtensionResults = () => {}; + return response.publicKey; + }, + async get(options) { + if (!options.publicKey) { + return null; + } + + if (options.mediation === 'conditional') { + return originalCredentials.get(options); + } + + const response = await postMessageToExtension({ action: 'passkeys_get', publicKey: options.publicKey }); + if (!response.publicKey) { + return response.fallback ? originalCredentials.get(options) : null; + } + + response.publicKey.getClientExtensionResults = () => {}; + response.publicKey.clientExtensionResults = () => {}; + return response.publicKey; + } + }; + + const isConditionalMediationAvailable = async() => false; + + // Overwrite navigator.credentials and PublicKeyCredential.isConditionalMediationAvailable. + // The latter requires user to select which device to use for authentication, but for now browsers cannot + // select a software authenticator. This could be removed in the future. + try { + Object.defineProperty(navigator, 'credentials', { value: passkeysCredentials }); + Object.defineProperty(window.PublicKeyCredential, 'isConditionalMediationAvailable', { value: isConditionalMediationAvailable }); + } catch (err) { + console.log('Cannot override navigator.credentials: ', err); + } +})(); diff --git a/keepassxc-browser/manifest.json b/keepassxc-browser/manifest.json index e3a41774..7bae962c 100755 --- a/keepassxc-browser/manifest.json +++ b/keepassxc-browser/manifest.json @@ -69,7 +69,8 @@ "content/pwgen.js", "content/totp-autocomplete.js", "content/totp-field.js", - "content/username-field.js" + "content/username-field.js", + "content/passkeys-utils.js" ], "run_at": "document_idle", "all_frames": true @@ -138,7 +139,8 @@ "css/notification.css", "css/pwgen.css", "css/username.css", - "css/totp.css" + "css/totp.css", + "content/passkeys.js" ], "permissions": [ "activeTab", diff --git a/keepassxc-browser/options/options.html b/keepassxc-browser/options/options.html index 4ea88ae4..35e0f9c4 100644 --- a/keepassxc-browser/options/options.html +++ b/keepassxc-browser/options/options.html @@ -332,6 +332,33 @@

+ +
+
+ + +
+
+ +
+
+ + +
+
+
+ + +
+
+ + +
+
+
+
+
+
diff --git a/keepassxc-browser/options/options.js b/keepassxc-browser/options/options.js index 3fd5a2fe..1552c3d1 100644 --- a/keepassxc-browser/options/options.js +++ b/keepassxc-browser/options/options.js @@ -327,6 +327,16 @@ options.showKeePassXCVersions = async function(response) { if (!version270Result) { $('#tab-general-settings #downloadFaviconAfterSaveFormGroup').hide(); } + + // Hide certain options with older KeePassXC versions than 2.8.0 + const version280Result = await browser.runtime.sendMessage({ + action: 'compare_version', + args: [ '2.8.0', response.current ] + }); + + if (!version280Result) { + $('#tab-general-settings #passkeysOptionsCard').hide(); + } }; options.getPartiallyHiddenKey = function(key) { diff --git a/keepassxc-protocol.md b/keepassxc-protocol.md index d6dd4a05..90d3454a 100644 --- a/keepassxc-protocol.md +++ b/keepassxc-protocol.md @@ -23,17 +23,53 @@ Encrypted messages are built with these JSON parameters: - requestID (optional) - A random 8 character string. Used to identify error responses. Currently used only with `generate-password`. Currently these messages are implemented: -- `change-public-keys`: Request for passing public keys from client to server and back. -- `get-databasehash`: Request for receiving the database hash (SHA256) of the current active database. - `associate`: Request for associating a new client with KeePassXC. -- `test-associate`: Request for testing if the client has been associated with KeePassXC. -- `generate-password`: Request for generating a password. KeePassXC's settings are used. -- `get-logins`: Requests for receiving credentials for the current URL match. -- `set-login`: Request for adding or updating credentials to the database. -- `lock-database`: Request for locking the database from client. +- `change-public-keys`: Request for passing public keys from client to server and back. +- `create-new-group`: Request for creating a new group to database. - `database-locked`: A signal from KeePassXC, the current active database is locked. - `database-unlocked`: A signal from KeePassXC, the current active database is unlocked. +- `generate-password`: Request for generating a password. KeePassXC's settings are used. +- `get-database-groups`: Returns all groups from the active database. +- `get-databasehash`: Request for receiving the database hash (SHA256) of the current active database. +- `get-logins`: Requests for receiving credentials for the current URL match. - `get-totp`: Request for receiving the current TOTP. +- `lock-database`: Request for locking the database from client. +- `passkeys-get`: Request for Passkeys authentication. +- `passkeys-register`: Request for Passkeys credential registration. +- `request-autotype`: Performs Global Auto-Type. +- `set-login`: Request for adding or updating credentials to the database. +- `test-associate`: Request for testing if the client has been associated with KeePassXC. + +### associate +Unencrypted message: +```json +{ + "action": "associate", + "key": "", + "idKey": "" +} +``` + +Request: +```json +{ + "action": "associate", + "message": "", + "nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q", + "clientID": "" +} +``` + +Response message data (success, decrypted): +```json +{ + "hash": "29234e32274a32276e25666a42", + "version": "2.7.0", + "success": "true", + "id": "testclient", + "nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q" +} +``` ### change-public-keys Request: @@ -56,18 +92,19 @@ Response (success): } ``` -### get-databasehash +### create-new-group Unencrypted message: ```json { - "action": "get-databasehash" + "action": "create-new-group", + "groupName": "" } ``` Request: ```json { - "action": "get-databasehash", + "action": "create-new-group", "message": "", "nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q", "clientID": "" @@ -77,57 +114,54 @@ Request: Response message data (success, decrypted): ```json { - "action": "hash", - "hash": "29234e32274a32276e25666a42", - "version": "2.2.0" + "name": "", + "uuid": "" } ``` -### associate -Unencrypted message: +### generate-password +Request (no unencrypted message is needed): ```json { - "action": "associate", - "key": "", - "idKey": "" + "action": "generate-password", + "nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q", + "clientID": "", + "requestID": "" } ``` -Request: +Response message data (success, decrypted): ```json { - "action": "associate", - "message": "", - "nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q", - "clientID": "" + "version": "2.7.0", + "password": "testclientpassword" + "success": "true", + "nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q" } ``` -Response message data (success, decrypted): +Response message data (success, decrypted, KeePassXC 2.7.0 and later): ```json { - "hash": "29234e32274a32276e25666a42", "version": "2.7.0", + "password": "thePassword", "success": "true", - "id": "testclient", "nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q" } ``` -### test-associate +### get-database-groups Unencrypted message: ```json { - "action": "test-associate", - "id": "", - "key": "" + "action": "get-database-groups" } ``` Request: ```json { - "action": "test-associate", + "action": "get-database-groups", "message": "", "nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q", "clientID": "" @@ -137,42 +171,81 @@ Request: Response message data (success, decrypted): ```json { - "version": "2.7.0", - "nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q", - "hash": "29234e32274a32276e25666a42", - "id": "testclient", - "success": "true" + "defaultGroup": "", + "defaultGroupAlwaysAllow": false, + "groups": [ + { + "name": "Root", + "uuid": "", + "children": [ + { + "name": "KeePassXC-Browser Passwords", + "uuid": "", + "children": [] + }, + { + "name": "SecondRoot", + "uuid": "", + "children": [ + { + "name": "Child", + "uuid": "", + "children": [ + { + "name": "GrandChild", + "uuid": "", + "children": [] + } + ] + } + ] + }, + { + "name": "ThirdRoot", + "uuid": "", + "children": [ + { + "name": "Child2", + "uuid": "", + "children": [] + } + ] + }, + { + "name": "Child2", + "uuid": "", + "children": [] + } + ] + } + ] } ``` -### generate-password -Request (no unencrypted message is needed): +### get-databasehash +Unencrypted message: ```json { - "action": "generate-password", - "nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q", - "clientID": "", - "requestID": "" + "action": "get-databasehash" } ``` -Response message data (success, decrypted): +Request: ```json { - "version": "2.7.0", - "password": "testclientpassword" - "success": "true", - "nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q" + "action": "get-databasehash", + "message": "", + "nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q", + "clientID": "" } ``` -Response message data (success, decrypted, KeePassXC 2.7.0 and later): +Response message data (success, decrypted): ```json { - "version": "2.7.0", - "password": "thePassword", - "success": "true", - "nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q" + "action": "hash", + "hash": "29234e32274a32276e25666a42", + "version": "2.2.0" } ``` @@ -227,44 +300,22 @@ Response message data (success, decrypted): } ``` -### set-login -Unencrypted message (downloadFavicon supported in KeePassXC 2.7.0 and later, but not when updating credentials): -```json -{ - "action": "set-login", - "url": "", - "submitUrl": "", - "id": "testclient", - "nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q", - "login": "user1", - "password": "passwd1", - "group": "", - "groupUuid": "", - "uuid": "", - "downloadFavicon": "true" -} -``` - -Request: +### get-totp (KeePassXC 2.6.1 and newer) +Request (no unencrypted message is needed): ```json { - "action": "set-login", - "message": "", - "nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q", - "clientID": "" + "action": "get-totp", + "uuid": "" } ``` Response message data (success, decrypted): ```json { - "count": null, - "entries" : null, - "error": "", - "nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q", + "totp": "", + "version": "2.2.0", "success": "true", - "hash": "29234e32274a32276e25666a42", - "version": "2.2.0" + "nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q" } ``` @@ -296,18 +347,46 @@ Response message data (success always returns an error, decrypted): } ``` -### get-database-groups -Unencrypted message: +### request-autotype (KeePassXC 2.7.0 and newer) +Request (no unencrypted message is needed): ```json { - "action": "get-database-groups" + "action": "request-autotype", + "search": "" +} +``` + +Response message data (success, decrypted): +```json +{ + "version": "2.7.0", + "success": "true", + "nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q" +} +``` + +### set-login +Unencrypted message (downloadFavicon supported in KeePassXC 2.7.0 and later, but not when updating credentials): +```json +{ + "action": "set-login", + "url": "", + "submitUrl": "", + "id": "testclient", + "nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q", + "login": "user1", + "password": "passwd1", + "group": "", + "groupUuid": "", + "uuid": "", + "downloadFavicon": "true" } ``` Request: ```json { - "action": "get-database-groups", + "action": "set-login", "message": "", "nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q", "clientID": "" @@ -317,69 +396,30 @@ Request: Response message data (success, decrypted): ```json { - "defaultGroup": "", - "defaultGroupAlwaysAllow": false, - "groups": [ - { - "name": "Root", - "uuid": "", - "children": [ - { - "name": "KeePassXC-Browser Passwords", - "uuid": "", - "children": [] - }, - { - "name": "SecondRoot", - "uuid": "", - "children": [ - { - "name": "Child", - "uuid": "", - "children": [ - { - "name": "GrandChild", - "uuid": "", - "children": [] - } - ] - } - ] - }, - { - "name": "ThirdRoot", - "uuid": "", - "children": [ - { - "name": "Child2", - "uuid": "", - "children": [] - } - ] - }, - { - "name": "Child2", - "uuid": "", - "children": [] - } - ] - } - ] + "count": null, + "entries" : null, + "error": "", + "nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q", + "success": "true", + "hash": "29234e32274a32276e25666a42", + "version": "2.2.0" } ``` -### create-new-group + +### test-associate Unencrypted message: ```json { - "action": "create-new-group", - "groupName": "" + "action": "test-associate", + "id": "", + "key": "" } ``` Request: ```json { - "action": "create-new-group", + "action": "test-associate", "message": "", "nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q", "clientID": "" @@ -389,44 +429,88 @@ Request: Response message data (success, decrypted): ```json { - "name": "", - "uuid": "" + "version": "2.7.0", + "nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q", + "hash": "29234e32274a32276e25666a42", + "id": "testclient", + "success": "true" } ``` -### get-totp (KeePassXC 2.6.1 and newer) -Request (no unencrypted message is needed): +### passkeys-get (decrypted, KeePassXC 2.8.0 and newer) +Unencrypted message: ```json { - "action": "get-totp", - "uuid": "" + "action": "passkeys-get", + "publicKey": PublicKeyCredentialRequestOptions, + "origin": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q", + "keys: [ + { + "id": "", + "key": "" + }, + ... + ] } ``` -Response message data (success, decrypted): +Response (success, decrypted): ```json { - "totp": , - "version": "2.2.0", + "version": "2.8.0", "success": "true", - "nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q" + "nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q", + "response": PublicKeyCredential } ``` -### request-autotype (KeePassXC 2.7.0 and newer) -Request (no unencrypted message is needed): +Response (error, decrypted): ```json { - "action": "request-autotype", - "search": "" + "version": "2.8.0", + "success": "true", + "nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q", + "response": { + "errorCode": "" + } } ``` -Response message data (success, decrypted): +### passkeys-register (decrypted, KeePassXC 2.8.0 and newer) +Unencrypted message: ```json { - "version": "2.7.0", + "action": "passkeys-register", + "publicKey": PublicKeyCredentialCreationOptions, + "origin": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q", + "keys: [ + { + "id": "", + "key": "" + }, + ... + ] +} +``` + +Response (success, decrypted): +```json +{ + "version": "2.8.0", "success": "true", "nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q" + "response": PublicKeyCredential +} +``` + +Response (error, decrypted): +```json +{ + "version": "2.8.0", + "success": "true", + "nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q", + "response": { + "errorCode": "" + } } ```