Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for WebAuthn (Passkeys) #1786

Merged
merged 2 commits into from
Oct 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"
varjolintu marked this conversation as resolved.
Show resolved Hide resolved
Expand Down Expand Up @@ -1191,6 +1223,26 @@
"message": "Extension",
"description": "Extension title in settings page"
varjolintu marked this conversation as resolved.
Show resolved Hide resolved
},
"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', 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', 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