mirror of
https://github.com/keepassxreboot/keepassxc-browser.git
synced 2026-03-11 08:54:43 +00:00
Add support for WebAuthn (Passkeys)
This commit is contained in:
parent
f54e16ec3a
commit
6592708e93
13 changed files with 770 additions and 208 deletions
|
|
@ -144,6 +144,7 @@
|
|||
"kpxcUsernameIcons": true,
|
||||
"logDebug": true,
|
||||
"logError": true,
|
||||
"kpxcPasskeysUtils": true,
|
||||
"ManualFill": true,
|
||||
"MAX_AUTOCOMPLETE_NAME_LEN": true,
|
||||
"MAX_OPACITY": true,
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
171
keepassxc-browser/content/passkeys-utils.js
Normal file
171
keepassxc-browser/content/passkeys-utils.js
Normal file
|
|
@ -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;
|
||||
};
|
||||
74
keepassxc-browser/content/passkeys.js
Normal file
74
keepassxc-browser/content/passkeys.js
Normal file
|
|
@ -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);
|
||||
}
|
||||
})();
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -332,6 +332,33 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Passkeys -->
|
||||
<div class="card my-4 shadow" id="passkeysOptionsCard">
|
||||
<div class="card-header h6 rounded-0">
|
||||
<i class="fa fa-user-circle-o" aria-hidden="true"></i>
|
||||
<span data-i18n="optionsPasskeysTitle"></span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<!-- Enable Passkeys -->
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="passkeys" id="passkeys" value="true" />
|
||||
<label class="form-check-label" for="passkeys" data-i18n="optionsPasskeysEnable"></label>
|
||||
<div class="form-text help-text" data-i18n="optionsPasskeysEnableHelpText"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Enable Passkeys fallback -->
|
||||
<div class="form-group">
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" name="passkeysFallback" id="passkeysFallback" value="true" />
|
||||
<label class="form-check-label" for="passkeysFallback" data-i18n="optionsPasskeysEnableFallback"></label>
|
||||
<div class="form-text help-text" data-i18n="optionsPasskeysEnableFallbackHelpText"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Updates -->
|
||||
<div class="card my-4 shadow">
|
||||
<div class="card-header h6 rounded-0">
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -23,65 +23,22 @@ 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.
|
||||
|
||||
### change-public-keys
|
||||
Request:
|
||||
```json
|
||||
{
|
||||
"action": "change-public-keys",
|
||||
"publicKey": "<client public key>",
|
||||
"nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q",
|
||||
"clientID": "<clientID>"
|
||||
}
|
||||
```
|
||||
|
||||
Response (success):
|
||||
```json
|
||||
{
|
||||
"action": "change-public-keys",
|
||||
"version": "2.7.0",
|
||||
"publicKey": "<host public key>",
|
||||
"success": "true"
|
||||
}
|
||||
```
|
||||
|
||||
### get-databasehash
|
||||
Unencrypted message:
|
||||
```json
|
||||
{
|
||||
"action": "get-databasehash"
|
||||
}
|
||||
```
|
||||
|
||||
Request:
|
||||
```json
|
||||
{
|
||||
"action": "get-databasehash",
|
||||
"message": "<encrypted message>",
|
||||
"nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q",
|
||||
"clientID": "<clientID>"
|
||||
}
|
||||
```
|
||||
|
||||
Response message data (success, decrypted):
|
||||
```json
|
||||
{
|
||||
"action": "hash",
|
||||
"hash": "29234e32274a32276e25666a42",
|
||||
"version": "2.2.0"
|
||||
}
|
||||
```
|
||||
- `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:
|
||||
|
|
@ -114,20 +71,40 @@ Response message data (success, decrypted):
|
|||
}
|
||||
```
|
||||
|
||||
### test-associate
|
||||
### change-public-keys
|
||||
Request:
|
||||
```json
|
||||
{
|
||||
"action": "change-public-keys",
|
||||
"publicKey": "<client public key>",
|
||||
"nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q",
|
||||
"clientID": "<clientID>"
|
||||
}
|
||||
```
|
||||
|
||||
Response (success):
|
||||
```json
|
||||
{
|
||||
"action": "change-public-keys",
|
||||
"version": "2.7.0",
|
||||
"publicKey": "<host public key>",
|
||||
"success": "true"
|
||||
}
|
||||
```
|
||||
|
||||
### create-new-group
|
||||
Unencrypted message:
|
||||
```json
|
||||
{
|
||||
"action": "test-associate",
|
||||
"id": "<saved database identifier received from associate>",
|
||||
"key": "<saved identification public key>"
|
||||
"action": "create-new-group",
|
||||
"groupName": "<group name or path>"
|
||||
}
|
||||
```
|
||||
|
||||
Request:
|
||||
```json
|
||||
{
|
||||
"action": "test-associate",
|
||||
"action": "create-new-group",
|
||||
"message": "<encrypted message>",
|
||||
"nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q",
|
||||
"clientID": "<clientID>"
|
||||
|
|
@ -137,11 +114,8 @@ Request:
|
|||
Response message data (success, decrypted):
|
||||
```json
|
||||
{
|
||||
"version": "2.7.0",
|
||||
"nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q",
|
||||
"hash": "29234e32274a32276e25666a42",
|
||||
"id": "testclient",
|
||||
"success": "true"
|
||||
"name": "<group name>",
|
||||
"uuid": "<group UUID>"
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -176,126 +150,6 @@ Response message data (success, decrypted, KeePassXC 2.7.0 and later):
|
|||
}
|
||||
```
|
||||
|
||||
### get-logins
|
||||
Unencrypted message:
|
||||
```json
|
||||
{
|
||||
"action": "get-logins",
|
||||
"url": "<snip>",
|
||||
"submitUrl": "<optional>",
|
||||
"httpAuth": "<optional>",
|
||||
"keys": [
|
||||
{
|
||||
"id": "<saved database identifier received from associate>",
|
||||
"key": "<saved identification public key>"
|
||||
},
|
||||
...
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Request:
|
||||
```json
|
||||
{
|
||||
"action": "get-logins",
|
||||
"message": "<encrypted message>",
|
||||
"nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q",
|
||||
"clientID": "<clientID>"
|
||||
}
|
||||
```
|
||||
|
||||
Response message data (success, decrypted):
|
||||
```json
|
||||
{
|
||||
"count": "2",
|
||||
"entries" : [
|
||||
{
|
||||
"login": "user1",
|
||||
"name": "user1",
|
||||
"password": "passwd1"
|
||||
},
|
||||
{
|
||||
"login": "user2",
|
||||
"name": "user2",
|
||||
"password": "passwd2",
|
||||
"expired": "true"
|
||||
}],
|
||||
"nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q",
|
||||
"success": "true",
|
||||
"hash": "29234e32274a32276e25666a42",
|
||||
"version": "2.2.0"
|
||||
}
|
||||
```
|
||||
|
||||
### set-login
|
||||
Unencrypted message (downloadFavicon supported in KeePassXC 2.7.0 and later, but not when updating credentials):
|
||||
```json
|
||||
{
|
||||
"action": "set-login",
|
||||
"url": "<snip>",
|
||||
"submitUrl": "<snip>",
|
||||
"id": "testclient",
|
||||
"nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q",
|
||||
"login": "user1",
|
||||
"password": "passwd1",
|
||||
"group": "<group name>",
|
||||
"groupUuid": "<group UUID>",
|
||||
"uuid": "<entry UUID>",
|
||||
"downloadFavicon": "true"
|
||||
}
|
||||
```
|
||||
|
||||
Request:
|
||||
```json
|
||||
{
|
||||
"action": "set-login",
|
||||
"message": "<encrypted message>",
|
||||
"nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q",
|
||||
"clientID": "<clientID>"
|
||||
}
|
||||
```
|
||||
|
||||
Response message data (success, decrypted):
|
||||
```json
|
||||
{
|
||||
"count": null,
|
||||
"entries" : null,
|
||||
"error": "",
|
||||
"nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q",
|
||||
"success": "true",
|
||||
"hash": "29234e32274a32276e25666a42",
|
||||
"version": "2.2.0"
|
||||
}
|
||||
```
|
||||
|
||||
### lock-database
|
||||
Unencrypted message:
|
||||
```json
|
||||
{
|
||||
"action": "lock-database"
|
||||
}
|
||||
```
|
||||
|
||||
Request:
|
||||
```json
|
||||
{
|
||||
"action": "lock-database",
|
||||
"message": "<encrypted message>",
|
||||
"nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q",
|
||||
"clientID": "<clientID>"
|
||||
}
|
||||
```
|
||||
|
||||
Response message data (success always returns an error, decrypted):
|
||||
```json
|
||||
{
|
||||
"action": "lock-database",
|
||||
"errorCode": 1,
|
||||
"error": "Database not opened",
|
||||
"nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q"
|
||||
}
|
||||
```
|
||||
|
||||
### get-database-groups
|
||||
Unencrypted message:
|
||||
```json
|
||||
|
|
@ -367,19 +221,19 @@ Response message data (success, decrypted):
|
|||
]
|
||||
}
|
||||
```
|
||||
### create-new-group
|
||||
|
||||
### get-databasehash
|
||||
Unencrypted message:
|
||||
```json
|
||||
{
|
||||
"action": "create-new-group",
|
||||
"groupName": "<group name or path>"
|
||||
"action": "get-databasehash"
|
||||
}
|
||||
```
|
||||
|
||||
Request:
|
||||
```json
|
||||
{
|
||||
"action": "create-new-group",
|
||||
"action": "get-databasehash",
|
||||
"message": "<encrypted message>",
|
||||
"nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q",
|
||||
"clientID": "<clientID>"
|
||||
|
|
@ -389,8 +243,60 @@ Request:
|
|||
Response message data (success, decrypted):
|
||||
```json
|
||||
{
|
||||
"name": "<group name>",
|
||||
"uuid": "<group UUID>"
|
||||
"action": "hash",
|
||||
"hash": "29234e32274a32276e25666a42",
|
||||
"version": "2.2.0"
|
||||
}
|
||||
```
|
||||
|
||||
### get-logins
|
||||
Unencrypted message:
|
||||
```json
|
||||
{
|
||||
"action": "get-logins",
|
||||
"url": "<snip>",
|
||||
"submitUrl": "<optional>",
|
||||
"httpAuth": "<optional>",
|
||||
"keys": [
|
||||
{
|
||||
"id": "<saved database identifier received from associate>",
|
||||
"key": "<saved identification public key>"
|
||||
},
|
||||
...
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Request:
|
||||
```json
|
||||
{
|
||||
"action": "get-logins",
|
||||
"message": "<encrypted message>",
|
||||
"nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q",
|
||||
"clientID": "<clientID>"
|
||||
}
|
||||
```
|
||||
|
||||
Response message data (success, decrypted):
|
||||
```json
|
||||
{
|
||||
"count": "2",
|
||||
"entries" : [
|
||||
{
|
||||
"login": "user1",
|
||||
"name": "user1",
|
||||
"password": "passwd1"
|
||||
},
|
||||
{
|
||||
"login": "user2",
|
||||
"name": "user2",
|
||||
"password": "passwd2",
|
||||
"expired": "true"
|
||||
}],
|
||||
"nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q",
|
||||
"success": "true",
|
||||
"hash": "29234e32274a32276e25666a42",
|
||||
"version": "2.2.0"
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -406,13 +312,41 @@ Request (no unencrypted message is needed):
|
|||
Response message data (success, decrypted):
|
||||
```json
|
||||
{
|
||||
"totp": <TOTP>,
|
||||
"totp": "<TOTP>",
|
||||
"version": "2.2.0",
|
||||
"success": "true",
|
||||
"nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q"
|
||||
}
|
||||
```
|
||||
|
||||
### lock-database
|
||||
Unencrypted message:
|
||||
```json
|
||||
{
|
||||
"action": "lock-database"
|
||||
}
|
||||
```
|
||||
|
||||
Request:
|
||||
```json
|
||||
{
|
||||
"action": "lock-database",
|
||||
"message": "<encrypted message>",
|
||||
"nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q",
|
||||
"clientID": "<clientID>"
|
||||
}
|
||||
```
|
||||
|
||||
Response message data (success always returns an error, decrypted):
|
||||
```json
|
||||
{
|
||||
"action": "lock-database",
|
||||
"errorCode": 1,
|
||||
"error": "Database not opened",
|
||||
"nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q"
|
||||
}
|
||||
```
|
||||
|
||||
### request-autotype (KeePassXC 2.7.0 and newer)
|
||||
Request (no unencrypted message is needed):
|
||||
```json
|
||||
|
|
@ -430,3 +364,153 @@ Response message data (success, decrypted):
|
|||
"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": "<snip>",
|
||||
"submitUrl": "<snip>",
|
||||
"id": "testclient",
|
||||
"nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q",
|
||||
"login": "user1",
|
||||
"password": "passwd1",
|
||||
"group": "<group name>",
|
||||
"groupUuid": "<group UUID>",
|
||||
"uuid": "<entry UUID>",
|
||||
"downloadFavicon": "true"
|
||||
}
|
||||
```
|
||||
|
||||
Request:
|
||||
```json
|
||||
{
|
||||
"action": "set-login",
|
||||
"message": "<encrypted message>",
|
||||
"nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q",
|
||||
"clientID": "<clientID>"
|
||||
}
|
||||
```
|
||||
|
||||
Response message data (success, decrypted):
|
||||
```json
|
||||
{
|
||||
"count": null,
|
||||
"entries" : null,
|
||||
"error": "",
|
||||
"nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q",
|
||||
"success": "true",
|
||||
"hash": "29234e32274a32276e25666a42",
|
||||
"version": "2.2.0"
|
||||
}
|
||||
```
|
||||
|
||||
### test-associate
|
||||
Unencrypted message:
|
||||
```json
|
||||
{
|
||||
"action": "test-associate",
|
||||
"id": "<saved database identifier received from associate>",
|
||||
"key": "<saved identification public key>"
|
||||
}
|
||||
```
|
||||
|
||||
Request:
|
||||
```json
|
||||
{
|
||||
"action": "test-associate",
|
||||
"message": "<encrypted message>",
|
||||
"nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q",
|
||||
"clientID": "<clientID>"
|
||||
}
|
||||
```
|
||||
|
||||
Response message data (success, decrypted):
|
||||
```json
|
||||
{
|
||||
"version": "2.7.0",
|
||||
"nonce": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q",
|
||||
"hash": "29234e32274a32276e25666a42",
|
||||
"id": "testclient",
|
||||
"success": "true"
|
||||
}
|
||||
```
|
||||
|
||||
### passkeys-get (decrypted, KeePassXC 2.8.0 and newer)
|
||||
Unencrypted message:
|
||||
```json
|
||||
{
|
||||
"action": "passkeys-get",
|
||||
"publicKey": PublicKeyCredentialRequestOptions,
|
||||
"origin": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q",
|
||||
"keys: [
|
||||
{
|
||||
"id": "<saved database identifier received from associate>",
|
||||
"key": "<saved identification public 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": "<error code>"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### passkeys-register (decrypted, KeePassXC 2.8.0 and newer)
|
||||
Unencrypted message:
|
||||
```json
|
||||
{
|
||||
"action": "passkeys-register",
|
||||
"publicKey": PublicKeyCredentialCreationOptions,
|
||||
"origin": "tZvLrBzkQ9GxXq9PvKJj4iAnfPT0VZ3Q",
|
||||
"keys: [
|
||||
{
|
||||
"id": "<saved database identifier received from associate>",
|
||||
"key": "<saved identification public 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": "<error code>"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
|
|
|||
Loading…
Reference in a new issue