Skip to content
Open
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
12 changes: 12 additions & 0 deletions keepassxc-browser/_locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,18 @@
"message": "user.id does not match the required length.",
"description": "user.id does not match the required length."
},
"errorMessagePasskeysEvalByCredentialNotSupported": {
"message": "evalByCredential is not supported at registration.",
"description": "evalByCredential is not supported at registration."
},
"errorMessagePasskeysEvalByCredentialNotEmpty": {
"message": "evalByCredential is not empty, but allowedCredentials is.",
"description": "evalByCredential is not empty, but allowedCredentials is."
},
"errorMessagePasskeysEvalByCredentialNotFound": {
"message": "Credential ID provided in evalByCredential not found.",
"description": "Credential ID provided in evalByCredential not found."
},
"errorMessagePasskeysContextIsNotSecure": {
"message": "Context is not secure.",
"description": "Context is not secure."
Expand Down
6 changes: 6 additions & 0 deletions keepassxc-browser/background/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ const kpErrors = {
PASSKEYS_UNKNOWN_ERROR: 31,
PASSKEYS_INVALID_CHALLENGE: 32,
PASSKEYS_INVALID_USER_ID: 33,
PASSKEYS_EVAL_BY_CREDENTIAL_NOT_SUPPORTED: 34,
PASSKEYS_EVAL_BY_CREDENTIAL_NOT_EMPTY: 35,
PASSKEYS_EVAL_BY_CREDENTIAL_NOT_FOUND: 36,

errorMessages: {
0: { msg: tr('errorMessageUnknown') },
Expand Down Expand Up @@ -80,6 +83,9 @@ const kpErrors = {
31: { msg: tr('errorMessagePasskeysUnknownError') },
32: { msg: tr('errorMessagePasskeysInvalidChallenge') },
33: { msg: tr('errorMessagePasskeysInvalidUserId') },
34: { msg: tr('errorMessagePasskeysEvalByCredentialNotSupported') },
35: { msg: tr('errorMessagePasskeysEvalByCredentialNotEmpty') },
36: { msg: tr('errorMessagePasskeysEvalByCredentialNotFound') }
},

getError(errorCode) {
Expand Down
10 changes: 10 additions & 0 deletions keepassxc-browser/content/passkeys-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ kpxcPasskeysUtils.buildCredentialCreationOptions = function(pkOptions, sameOrigi
publicKey.challenge = kpxcArrayBufferToBase64(pkOptions.challenge);
publicKey.extensions = pkOptions?.extensions;

const prfSalt = publicKey?.extensions?.prf?.eval?.first;
if (prfSalt) {
publicKey.extensions.prf.eval.first = kpxcArrayBufferToBase64(prfSalt);
}

// Make sure integers are used for "alg". Set to reserved if not found.
// https://www.iana.org/assignments/cose/cose.xhtml#algorithms
publicKey.pubKeyCredParams = [];
Expand Down Expand Up @@ -113,6 +118,11 @@ kpxcPasskeysUtils.buildCredentialRequestOptions = function(pkOptions, sameOrigin
publicKey.timeout = getTimeout(publicKey?.userVerification, pkOptions?.timeout);
publicKey.userVerification = pkOptions?.userVerification;

const prfSalt = publicKey?.extensions?.prf?.eval?.first;
if (prfSalt) {
publicKey.extensions.prf.eval.first = kpxcArrayBufferToBase64(prfSalt);
}

publicKey.allowCredentials = [];
if (pkOptions.allowCredentials && pkOptions.allowCredentials.length > 0) {
for (const cred of pkOptions.allowCredentials) {
Expand Down
37 changes: 34 additions & 3 deletions keepassxc-browser/content/passkeys.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
const PASSKEYS_UNKNOWN_ERROR = 31;
const PASSKEYS_INVALID_CHALLENGE = 32;
const PASSKEYS_INVALID_USER_ID = 33;
const PASSKEYS_EVAL_BY_CREDENTIAL_NOT_SUPPORTED = 34;
const PASSKEYS_EVAL_BY_CREDENTIAL_NOT_EMPTY = 35;
const PASSKEYS_EVAL_BY_CREDENTIAL_NOT_FOUND = 36;

const kpxcStringToArrayBuffer = function(str) {
const arr = Uint8Array.from(str, c => c.charCodeAt(0));
Expand Down Expand Up @@ -82,6 +85,17 @@
getPublicKeyAlgorithm: () => publicKey.response?.publicKeyAlgorithm,
getTransports: () => [ 'internal' ]
};

const prfResponse = publicKey.response?.clientExtensionResults?.prf;
if (prfResponse) {
if (prfResponse?.results?.first) {
response['clientExtensionResults'] =
{ prf: { results: { first: kpxcBase64ToArrayBuffer(prfResponse?.results?.first) } } };
} else if (prfResponse?.enabled) {
response['clientExtensionResults'] = { prf: prfResponse };
}
}

return Object.setPrototypeOf(response, AuthenticatorAttestationResponse.prototype);
};

Expand All @@ -94,6 +108,11 @@
userHandle: publicKey.response?.userHandle ? kpxcBase64ToArrayBuffer(publicKey.response?.userHandle) : null
};

const prfResponse = publicKey.response?.clientExtensionResults?.prf?.results?.first;
if (prfResponse) {
response['clientExtensionResults'] = { prf: { results: { first: kpxcBase64ToArrayBuffer(prfResponse) } } };
}

return Object.setPrototypeOf(response, AuthenticatorAssertionResponse.prototype);
};

Expand All @@ -102,14 +121,16 @@
const authenticatorResponse = publicKey?.response?.attestationObject
? createAttestationResponse(publicKey)
: createAssertionResponse(publicKey);
const clientExtensionResults =
authenticatorResponse?.clientExtensionResults || publicKey?.response?.clientExtensionResults || {};
const publicKeyCredential = {
authenticatorAttachment: publicKey.authenticatorAttachment,
id: publicKey.id,
rawId: kpxcBase64ToArrayBuffer(publicKey.id),
response: authenticatorResponse,
type: publicKey.type,
clientExtensionResults: () => publicKey?.response?.clientExtensionResults || {},
getClientExtensionResults: () => publicKey?.response?.clientExtensionResults || {},
clientExtensionResults: () => clientExtensionResults,
getClientExtensionResults: () => clientExtensionResults,
toJSON: () => kpxcPublicKeyCredentialJson(publicKeyCredential, publicKey)
};

Expand Down Expand Up @@ -179,10 +200,20 @@
throw new DOMException(errorMessage, DOMException.SECURITY_ERR);
}

if (errorCode === PASSKEYS_NO_SUPPORTED_ALGORITHMS) {
if (
[
PASSKEYS_NO_SUPPORTED_ALGORITHMS,
PASSKEYS_EVAL_BY_CREDENTIAL_NOT_SUPPORTED,
PASSKEYS_EVAL_BY_CREDENTIAL_NOT_EMPTY
].includes(errorCode)
) {
throw new DOMException(errorMessage, DOMException.NOT_SUPPORTED_ERR);
}

if (errorCode === PASSKEYS_EVAL_BY_CREDENTIAL_NOT_FOUND) {
throw new DOMException(errorMessage, DOMException.SYNTAX_ERR);
}

if ([ PASSKEYS_INVALID_CHALLENGE, PASSKEYS_INVALID_USER_ID ].includes(errorCode)) {
throw new TypeError(errorMessage);
}
Expand Down