Skip to content

Commit

Permalink
Add support for WebAuthn (Passkeys)
Browse files Browse the repository at this point in the history
  • Loading branch information
varjolintu authored and varjolintu committed Oct 11, 2023
1 parent f54e16e commit 6592708
Show file tree
Hide file tree
Showing 13 changed files with 731 additions and 169 deletions.
1 change: 1 addition & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@
"kpxcUsernameIcons": true,
"logDebug": true,
"logError": true,
"kpxcPasskeysUtils": true,
"ManualFill": true,
"MAX_AUTOCOMPLETE_NAME_LEN": true,
"MAX_OPACITY": true,
Expand Down
52 changes: 52 additions & 0 deletions keepassxc-browser/_locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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."
Expand Down
18 changes: 17 additions & 1 deletion keepassxc-browser/background/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -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') },
Expand All @@ -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) {
Expand Down
3 changes: 3 additions & 0 deletions keepassxc-browser/background/event.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down
100 changes: 89 additions & 11 deletions keepassxc-browser/background/keepass.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -450,7 +444,6 @@ keepass.lockDatabase = async function(tab) {
action: kpAction
};


try {
const response = await keepassClient.sendMessage(kpAction, tab, messageData, nonce);
if (response) {
Expand Down Expand Up @@ -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
//--------------------------------------------------------------------------
Expand Down Expand Up @@ -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
//--------------------------------------------------------------------------
Expand Down Expand Up @@ -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();
};
Expand Down
6 changes: 4 additions & 2 deletions keepassxc-browser/background/page.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -29,7 +31,7 @@ const defaultSettings = {
showOTPIcon: true,
useObserver: true,
usePredefinedSites: true,
usePasswordGeneratorIcons: false
usePasswordGeneratorIcons: false,
};

const AUTO_SUBMIT_TIMEOUT = 5000;
Expand Down
42 changes: 42 additions & 0 deletions keepassxc-browser/content/keepassxc-browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -803,6 +841,10 @@ const initContentScript = async function() {
return;
}

if (kpxc.settings.passkeys) {
kpxc.enablePasskeys();
}

await kpxc.updateDatabaseState();
await kpxc.initCredentialFields();

Expand Down
Loading

0 comments on commit 6592708

Please sign in to comment.