diff --git a/keepassxc-browser/_locales/en/messages.json b/keepassxc-browser/_locales/en/messages.json index c2f723757..4144bad88 100644 --- a/keepassxc-browser/_locales/en/messages.json +++ b/keepassxc-browser/_locales/en/messages.json @@ -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." diff --git a/keepassxc-browser/background/client.js b/keepassxc-browser/background/client.js index 427aa3d3f..648ee7e60 100644 --- a/keepassxc-browser/background/client.js +++ b/keepassxc-browser/background/client.js @@ -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') }, @@ -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) { diff --git a/keepassxc-browser/content/passkeys-utils.js b/keepassxc-browser/content/passkeys-utils.js index b28541251..3c808c888 100644 --- a/keepassxc-browser/content/passkeys-utils.js +++ b/keepassxc-browser/content/passkeys-utils.js @@ -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 = []; @@ -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) { diff --git a/keepassxc-browser/content/passkeys.js b/keepassxc-browser/content/passkeys.js index a516406f4..2b77124d2 100644 --- a/keepassxc-browser/content/passkeys.js +++ b/keepassxc-browser/content/passkeys.js @@ -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)); @@ -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); }; @@ -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); }; @@ -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) }; @@ -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); }