Add support for WebAuthn (Passkeys)

This commit is contained in:
varjolintu 2022-03-21 17:15:02 +02:00 committed by varjolintu
parent f54e16ec3a
commit 6592708e93
13 changed files with 770 additions and 208 deletions

View file

@ -144,6 +144,7 @@
"kpxcUsernameIcons": true,
"logDebug": true,
"logError": true,
"kpxcPasskeysUtils": true,
"ManualFill": true,
"MAX_AUTOCOMPLETE_NAME_LEN": true,
"MAX_OPACITY": true,

View file

@ -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."

View file

@ -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) {

View file

@ -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,

View file

@ -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();
};

View file

@ -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;

View file

@ -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();

View 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;
};

View 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);
}
})();

View file

@ -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",

View file

@ -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">

View file

@ -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) {

View file

@ -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>"
}
}
```