From f7abde462fde1fbc287520e3f894ddae66d81a1d Mon Sep 17 00:00:00 2001 From: Arnaud Dagnelies Date: Mon, 27 Nov 2023 08:36:16 +0000 Subject: [PATCH] made counter optional (fixes Counters don't work on macbook #38) --- README.md | 40 +++++++++++++++++++++++++++------------- dist/webauthn.min.js | 2 +- dist/webauthn.min.js.map | 4 ++-- package-lock.json | 4 ++-- src/server.ts | 4 ++-- 5 files changed, 34 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index c38809c..eade2de 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,7 @@ It is an [open source](https://github.com/passwordless-id/webauthn), minimalisti Try out the playground to see how this library works: -- [Testing Playground](https://webauthn.passwordless.id/demos/playground.html) - ---- +### *[Testing Playground](https://webauthn.passwordless.id/demos/playground.html)* Other demos with minial examples: @@ -210,8 +208,8 @@ The credential key is the most important part and should be stored in a database }, ``` -Please note that unlike traditional systems, a user might have multiple credential keys, one per device. - +*Please note that unlike traditional systems, you might allow a user to have multiple credential keys. +For example, if you allow the user to use multiple device-bound keys and/or registering keys for multiple platforms.* Authentication @@ -294,12 +292,15 @@ const expected = { challenge: "56535b13-5d93-4194-a282-f234c1c24500", // whatever was randomly generated by the server. origin: "http://localhost:8080", userVerified: true, // should be set if `userVerification` was set to `required` in the authentication options (default) - counter: 0 // for better security, you should verify the authenticator "usage" counter increased since last time + counter: 123 // Optional. For device-bound credentials, you should verify the authenticator "usage" counter increased since last time. } ``` -> On iOS/MacOS, the `counter` value for the first authentication will be 0, while on Android/Windows the `counter` will start with 1. -> So using a counter value of `-1` seems best to cover the initial case. Please note that the specs do not mandate "+1" increases, it could theoretically increase by any amount. +Regarding the `counter`, it might or might not be implemented by the authenticator. +Typically, it's implemented by hardware-bound keys to detect and avoid the risk of cloning the authenticator and starts with 1 during registration. +On the opposite, for password managers syncing keys in the cloud, the counter is typically always 0 since in that case cloning is a "feature". +For example, device-bound keys on Android and Windows do have an increasing `counter`, USB security keys also, while MacOS/iOS do not. +Lastly, please note that the specs do not mandate "+1" increases, it could theoretically increase by any amount. Often, it might also be more practical to use functions to verify challenge or origin. This is possible too: @@ -308,7 +309,7 @@ const expected = { challenge: async (challenge) => { /* async call to DB for example */ return true }, origin: (origin) => listOfAllowedOrigins.includes(origin), userVerified: true, // no function allowed here - counter: 0 // no function allowed here + counter: 123 // optional, no function allowed here } ``` @@ -334,8 +335,7 @@ Otherwise, your implementation might become vulnerable to replay attacks. ### There can be multiple credentials per user ID -Unlike traditional authentication, you can have multiple public/private key pairs per user: one per device. - +Unlike traditional authentication, you might have multiple credential keys per user. ### Authentication does *not* provide `username` out of the box @@ -344,9 +344,23 @@ Only `credentialId` is provided during the authentication. So either you maintain a mapping `credentialId -> username` in your database, or you add the `username` in your frontend to backend communication. -### Let the platform choose the user +### Passkeys a.k.a "discoverable" credentials + +If the credential is [discoverable](https://w3c.github.io/webauthn/#client-side-discoverable-public-key-credential-source), the credential id and user information is kept on the system. Although you can "discourage" it, there is no option in the spec to "forbid" it. For example, iOS always use "discoverable" keys, even if the option is set to `"discouraged"` + +Afterwards, you can use `client.authenticate([], ...)` without specifying credential IDs. In that case, the platform will pop-up a default dialog to let you pick a user and perform authentication. Of course, the look and feel is platform specific. + + +### Disable synced credentials + +That is sadly impossible, the spec authors refuse to add an option to disable syncing. + +See: + +- https://github.com/w3c/webauthn/issues/1714 +- https://github.com/w3c/webauthn/issues/1739 -You can *not* specify any credential ids during authentication. In that case, the platform will pop-up a default dialog to let you pick a user and perform authentication. Of course, the look and feel is platform specific. +Note that in practice, and although not precised by the specs, the "discoverable" property has an impact on syncing in the cloud. As of end 2023, for Microsoft and Android, when marked as `discoverable: "discouraged"`, they are not synced in the cloud ...but still discoverable. Since this is not covered by the specs, this might change in the future. ### This library simplifies a few things by using sensible defaults diff --git a/dist/webauthn.min.js b/dist/webauthn.min.js index 0a079ab..baa668b 100644 --- a/dist/webauthn.min.js +++ b/dist/webauthn.min.js @@ -1,2 +1,2 @@ -var N=Object.defineProperty;var b=(e,a)=>{for(var t in a)N(e,t,{get:a[t],enumerable:!0})};var w={};b(w,{authenticate:()=>Y,isAvailable:()=>H,isLocalAuthenticator:()=>A,register:()=>v});var u={};b(u,{bufferToHex:()=>p,concatenateBuffers:()=>h,isBase64url:()=>m,parseBase64url:()=>c,parseBuffer:()=>O,randomChallenge:()=>T,sha256:()=>d,toBase64url:()=>s,toBuffer:()=>l});function T(){return crypto.randomUUID()}function l(e){return Uint8Array.from(e,a=>a.charCodeAt(0)).buffer}function O(e){return String.fromCharCode(...new Uint8Array(e))}function m(e){return e.match(/^[a-zA-Z0-9\-_]+=*$/)!==null}function s(e){return btoa(O(e)).replaceAll("+","-").replaceAll("/","_")}function c(e){return e=e.replaceAll("-","+").replaceAll("_","/"),l(atob(e))}async function d(e){return await crypto.subtle.digest("SHA-256",e)}function p(e){return[...new Uint8Array(e)].map(a=>a.toString(16).padStart(2,"0")).join("")}function h(e,a){var t=new Uint8Array(e.byteLength+a.byteLength);return t.set(new Uint8Array(e),0),t.set(new Uint8Array(a),e.byteLength),t}function H(){return!!window.PublicKeyCredential}async function A(){return await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()}async function R(e){if(e==="local")return"platform";if(e==="roaming"||e==="extern")return"cross-platform";if(e!=="both")try{return await A()?"platform":"cross-platform"}catch{return}}function V(e){switch(e){case-7:return"ES256";case-257:return"RS256";default:throw new Error(`Unknown algorithm code: ${e}`)}}async function v(e,a,t){if(t=t??{},!m(a))throw new Error("Provided challenge is not properly encoded in Base64url");let n={challenge:c(a),rp:{id:window.location.hostname,name:window.location.hostname},user:{id:t.userHandle?l(t.userHandle):await d(new TextEncoder().encode("passwordless.id-user:"+e)),name:e,displayName:e},pubKeyCredParams:[{alg:-7,type:"public-key"},{alg:-257,type:"public-key"}],timeout:t.timeout??6e4,authenticatorSelection:{userVerification:t.userVerification??"required",authenticatorAttachment:await R(t.authenticatorType??"auto"),residentKey:t.discoverable??"preferred",requireResidentKey:t.discoverable==="required"},attestation:t.attestation?"direct":"none"};t.debug&&console.debug(n);let r=await navigator.credentials.create({publicKey:n});t.debug&&console.debug(r);let i=r.response,o={username:e,credential:{id:r.id,publicKey:s(i.getPublicKey()),algorithm:V(r.response.getPublicKeyAlgorithm())},authenticatorData:s(i.getAuthenticatorData()),clientData:s(i.clientDataJSON)};return t.attestation&&(o.attestationData=s(i.attestationObject)),o}async function k(e){let a=["internal"],t=["hybrid","usb","ble","nfc"];if(e==="local")return a;if(e=="roaming"||e==="extern")return t;if(e==="both")return[...a,...t];try{return await A()?a:t}catch{return[...a,...t]}}async function Y(e,a,t){if(t=t??{},!m(a))throw new Error("Provided challenge is not properly encoded in Base64url");let n=await k(t.authenticatorType??"auto"),r={challenge:c(a),rpId:window.location.hostname,allowCredentials:e.map(f=>({id:c(f),type:"public-key",transports:n})),userVerification:t.userVerification??"required",timeout:t.timeout??6e4};t.debug&&console.debug(r);let i=await navigator.credentials.get({publicKey:r,mediation:t.mediation});t.debug&&console.debug(i);let o=i.response;return{credentialId:i.id,authenticatorData:s(o.authenticatorData),clientData:s(o.clientDataJSON),signature:s(o.signature)}}var I={};b(I,{verifyAuthentication:()=>j,verifyRegistration:()=>_,verifySignature:()=>U});var D={};b(D,{parseAttestation:()=>x,parseAuthentication:()=>B,parseAuthenticator:()=>C,parseClient:()=>P,parseRegistration:()=>S});var K={"9c835346-796b-4c27-8898-d6032f515cc5":{name:"Cryptnox FIDO2"},"c5ef55ff-ad9a-4b9f-b580-adebafe026d0":{name:"YubiKey 5Ci"},"39a5647e-1853-446c-a1f6-a79bae9f5bc7":{name:"Vancosys Android Authenticator"},"3789da91-f943-46bc-95c3-50ea2012f03a":{name:"NEOWAVE Winkeo FIDO2"},"fa2b99dc-9e39-4257-8f92-4a30d23c4118":{name:"YubiKey 5 Series with NFC"},"4e768f2c-5fab-48b3-b300-220eb487752b":{name:"Hideez Key 4 FIDO2 SDK"},"931327dd-c89b-406c-a81e-ed7058ef36c6":{name:"Swissbit iShield FIDO2"},"e1a96183-5016-4f24-b55b-e3ae23614cc6":{name:"ATKey.Pro CTAP2.0"},"08987058-cadc-4b81-b6e1-30de50dcbe96":{name:"Windows Hello Hardware Authenticator"},"d91c5288-0ef0-49b7-b8ae-21ca0aa6b3f3":{name:"KEY-ID FIDO2 Authenticator"},"ee041bce-25e5-4cdb-8f86-897fd6418464":{name:"Feitian ePass FIDO2-NFC Authenticator"},"73bb0cd4-e502-49b8-9c6f-b59445bf720b":{name:"YubiKey 5 FIPS Series"},"149a2021-8ef6-4133-96b8-81f8d5b7f1f5":{name:"Security Key by Yubico with NFC"},"3b1adb99-0dfe-46fd-90b8-7f7614a4de2a":{name:"GoTrust Idem Key FIDO2 Authenticator"},"f8a011f3-8c0a-4d15-8006-17111f9edc7d":{name:"Security Key by Yubico"},"2c0df832-92de-4be1-8412-88a8f074df4a":{name:"Feitian FIDO Smart Card"},"c5703116-972b-4851-a3e7-ae1259843399":{name:"NEOWAVE Badgeo FIDO2"},"820d89ed-d65a-409e-85cb-f73f0578f82a":{name:"Vancosys iOS Authenticator"},"b6ede29c-3772-412c-8a78-539c1f4c62d2":{name:"Feitian BioPass FIDO2 Plus Authenticator"},"85203421-48f9-4355-9bc8-8a53846e5083":{name:"YubiKey 5Ci FIPS"},"d821a7d4-e97c-4cb6-bd82-4237731fd4be":{name:"Hyper FIDO Bio Security Key"},"516d3969-5a57-5651-5958-4e7a49434167":{name:"SmartDisplayer BobeePass (NFC-BLE FIDO2 Authenticator)"},"b93fd961-f2e6-462f-b122-82002247de78":{name:"Android Authenticator with SafetyNet Attestation"},"2fc0579f-8113-47ea-b116-bb5a8db9202a":{name:"YubiKey 5 Series with NFC"},"9ddd1817-af5a-4672-a2b9-3e3dd95000a9":{name:"Windows Hello VBS Hardware Authenticator"},"d8522d9f-575b-4866-88a9-ba99fa02f35b":{name:"YubiKey Bio Series"},"692db549-7ae5-44d5-a1e5-dd20a493b723":{name:"HID Crescendo Key"},"3e22415d-7fdf-4ea4-8a0c-dd60c4249b9d":{name:"Feitian iePass FIDO Authenticator"},"aeb6569c-f8fb-4950-ac60-24ca2bbe2e52":{name:"HID Crescendo C2300"},"9f0d8150-baa5-4c00-9299-ad62c8bb4e87":{name:"GoTrust Idem Card FIDO2 Authenticator"},"12ded745-4bed-47d4-abaa-e713f51d6393":{name:"Feitian AllinOne FIDO2 Authenticator"},"88bbd2f0-342a-42e7-9729-dd158be5407a":{name:"Precision InnaIT Key FIDO 2 Level 2 certified"},"34f5766d-1536-4a24-9033-0e294e510fb0":{name:"YubiKey 5 Series CTAP2.1 Preview 1 "},"83c47309-aabb-4108-8470-8be838b573cb":{name:"YubiKey Bio Series (Enterprise Profile)"},"be727034-574a-f799-5c76-0929e0430973":{name:"Crayonic KeyVault K1 (USB-NFC-BLE FIDO2 Authenticator)"},"b92c3f9a-c014-4056-887f-140a2501163b":{name:"Security Key by Yubico"},"54d9fee8-e621-4291-8b18-7157b99c5bec":{name:"HID Crescendo Enabled"},"6028b017-b1d4-4c02-b4b3-afcdafc96bb2":{name:"Windows Hello Software Authenticator"},"6d44ba9b-f6ec-2e49-b930-0c8fe920cb73":{name:"Security Key by Yubico with NFC"},"e416201b-afeb-41ca-a03d-2281c28322aa":{name:"ATKey.Pro CTAP2.1"},"9f77e279-a6e2-4d58-b700-31e5943c6a98":{name:"Hyper FIDO Pro"},"73402251-f2a8-4f03-873e-3cb6db604b03":{name:"uTrust FIDO2 Security Key"},"c1f9a0bc-1dd2-404a-b27f-8e29047a43fd":{name:"YubiKey 5 FIPS Series with NFC"},"504d7149-4e4c-3841-4555-55445a677357":{name:"WiSECURE AuthTron USB FIDO2 Authenticator"},"cb69481e-8ff7-4039-93ec-0a2729a154a8":{name:"YubiKey 5 Series"},"ee882879-721c-4913-9775-3dfcce97072a":{name:"YubiKey 5 Series"},"8c97a730-3f7b-41a6-87d6-1e9b62bda6f0":{name:"FT-JCOS FIDO Fingerprint Card"},"a1f52be5-dfab-4364-b51c-2bd496b14a56":{name:"OCTATCO EzFinger2 FIDO2 AUTHENTICATOR"},"3e078ffd-4c54-4586-8baa-a77da113aec5":{name:"Hideez Key 3 FIDO2"},"d41f5a69-b817-4144-a13c-9ebd6d9254d6":{name:"ATKey.Card CTAP2.0"},"bc2fe499-0d8e-4ffe-96f3-94a82840cf8c":{name:"OCTATCO EzQuant FIDO2 AUTHENTICATOR"},"1c086528-58d5-f211-823c-356786e36140":{name:"Atos CardOS FIDO2"},"77010bd7-212a-4fc9-b236-d2ca5e9d4084":{name:"Feitian BioPass FIDO2 Authenticator"},"833b721a-ff5f-4d00-bb2e-bdda3ec01e29":{name:"Feitian ePass FIDO2 Authenticator"}};function F(e){console.debug(e);let a=new DataView(e.slice(32,33)).getUint8(0);console.debug(a);let t={rpIdHash:s(e.slice(0,32)),flags:{userPresent:!!(a&1),userVerified:!!(a&4),backupEligibility:!!(a&8),backupState:!!(a&16),attestedData:!!(a&64),extensionsIncluded:!!(a&128)},counter:new DataView(e.slice(33,37)).getUint32(0,!1)};if(e.byteLength>37){let n=$(e);t={...t,aaguid:n,name:W(n)}}return t}function $(e){return L(e.slice(37,53))}function L(e){let a=p(e);return a=a.substring(0,8)+"-"+a.substring(8,12)+"-"+a.substring(12,16)+"-"+a.substring(16,20)+"-"+a.substring(20,32),a}function W(e){return(J??K)[e]?.name}var J=null;var q=new TextDecoder("utf-8");function P(e){return typeof e=="string"&&(e=c(e)),JSON.parse(q.decode(e))}function C(e){return typeof e=="string"&&(e=c(e)),F(e)}function x(e){return typeof e=="string"&&(e=c(e)),"Really complex to parse. Good luck with that one!"}function S(e){return{username:e.username,credential:e.credential,client:P(e.clientData),authenticator:C(e.authenticatorData),attestation:e.attestationData?x(e.attestationData):null}}function B(e){return{credentialId:e.credentialId,client:P(e.clientData),authenticator:C(e.authenticatorData),signature:e.signature}}async function z(e,a){if(typeof e=="function"){let t=e(a);return t instanceof Promise?await t:t}return e===a}async function g(e,a){return!await z(e,a)}async function _(e,a){let t=S(e);if(t.client.challenge,t.client.type!=="webauthn.create")throw new Error(`Unexpected ClientData type: ${t.client.type}`);if(await g(a.origin,t.client.origin))throw new Error(`Unexpected ClientData origin: ${t.client.origin}`);if(await g(a.challenge,t.client.challenge))throw new Error(`Unexpected ClientData challenge: ${t.client.challenge}`);return t}async function j(e,a,t){if(e.credentialId!==a.id)throw new Error(`Credential ID mismatch: ${e.credentialId} vs ${a.id}`);if(!await U({algorithm:a.algorithm,publicKey:a.publicKey,authenticatorData:e.authenticatorData,clientData:e.clientData,signature:e.signature}))throw new Error(`Invalid signature: ${e.signature}`);let r=B(e);if(r.client.type!=="webauthn.get")throw new Error(`Unexpected clientData type: ${r.client.type}`);if(await g(t.origin,r.client.origin))throw new Error(`Unexpected ClientData origin: ${r.client.origin}`);if(await g(t.challenge,r.client.challenge))throw new Error(`Unexpected ClientData challenge: ${r.client.challenge}`);let i=new URL(r.client.origin).hostname,o=s(await d(l(i)));if(r.authenticator.rpIdHash!==o)throw new Error(`Unexpected RpIdHash: ${r.authenticator.rpIdHash} vs ${o}`);if(!r.authenticator.flags.userPresent)throw new Error("Unexpected authenticator flags: missing userPresent");if(!r.authenticator.flags.userVerified&&t.userVerified)throw new Error("Unexpected authenticator flags: missing userVerified");if(r.authenticator.counter<=t.counter)throw new Error(`Unexpected authenticator counter: ${r.authenticator.counter} (should be > ${t.counter})`);return r}function G(e){switch(e){case"RS256":return{name:"RSASSA-PKCS1-v1_5",hash:"SHA-256"};case"ES256":return{name:"ECDSA",namedCurve:"P-256",hash:"SHA-256"};default:throw new Error(`Unknown or unsupported crypto algorithm: ${e}. Only 'RS256' and 'ES256' are supported.`)}}async function Q(e,a){let t=c(a);return crypto.subtle.importKey("spki",t,e,!1,["verify"])}async function U({algorithm:e,publicKey:a,authenticatorData:t,clientData:n,signature:r}){let i=G(e),o=await Q(i,a);console.debug(o);let E=await d(c(n)),f=h(c(t),E);console.debug("Crypto Algo: "+JSON.stringify(i)),console.debug("Public key: "+a),console.debug("Data: "+s(f)),console.debug("Signature: "+r);let y=c(r);return e=="ES256"&&(y=Z(y)),await crypto.subtle.verify(i,o,y,f)}function Z(e){let a=new Uint8Array(e),t=a[4]===0?5:4,n=t+32,r=a[n+2]===0?n+3:n+2,i=a.slice(t,n),o=a.slice(r);return new Uint8Array([...i,...o])}var ne={client:w,server:I,parsers:D,utils:u};export{w as client,ne as default,D as parsers,I as server,u as utils}; +var N=Object.defineProperty;var b=(e,a)=>{for(var t in a)N(e,t,{get:a[t],enumerable:!0})};var w={};b(w,{authenticate:()=>Y,isAvailable:()=>H,isLocalAuthenticator:()=>A,register:()=>v});var u={};b(u,{bufferToHex:()=>h,concatenateBuffers:()=>p,isBase64url:()=>m,parseBase64url:()=>c,parseBuffer:()=>O,randomChallenge:()=>T,sha256:()=>d,toBase64url:()=>s,toBuffer:()=>l});function T(){return crypto.randomUUID()}function l(e){return Uint8Array.from(e,a=>a.charCodeAt(0)).buffer}function O(e){return String.fromCharCode(...new Uint8Array(e))}function m(e){return e.match(/^[a-zA-Z0-9\-_]+=*$/)!==null}function s(e){return btoa(O(e)).replaceAll("+","-").replaceAll("/","_")}function c(e){return e=e.replaceAll("-","+").replaceAll("_","/"),l(atob(e))}async function d(e){return await crypto.subtle.digest("SHA-256",e)}function h(e){return[...new Uint8Array(e)].map(a=>a.toString(16).padStart(2,"0")).join("")}function p(e,a){var t=new Uint8Array(e.byteLength+a.byteLength);return t.set(new Uint8Array(e),0),t.set(new Uint8Array(a),e.byteLength),t}function H(){return!!window.PublicKeyCredential}async function A(){return await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()}async function R(e){if(e==="local")return"platform";if(e==="roaming"||e==="extern")return"cross-platform";if(e!=="both")try{return await A()?"platform":"cross-platform"}catch{return}}function V(e){switch(e){case-7:return"ES256";case-257:return"RS256";default:throw new Error(`Unknown algorithm code: ${e}`)}}async function v(e,a,t){if(t=t??{},!m(a))throw new Error("Provided challenge is not properly encoded in Base64url");let n={challenge:c(a),rp:{id:window.location.hostname,name:window.location.hostname},user:{id:t.userHandle?l(t.userHandle):await d(new TextEncoder().encode("passwordless.id-user:"+e)),name:e,displayName:e},pubKeyCredParams:[{alg:-7,type:"public-key"},{alg:-257,type:"public-key"}],timeout:t.timeout??6e4,authenticatorSelection:{userVerification:t.userVerification??"required",authenticatorAttachment:await R(t.authenticatorType??"auto"),residentKey:t.discoverable??"preferred",requireResidentKey:t.discoverable==="required"},attestation:t.attestation?"direct":"none"};t.debug&&console.debug(n);let r=await navigator.credentials.create({publicKey:n});t.debug&&console.debug(r);let i=r.response,o={username:e,credential:{id:r.id,publicKey:s(i.getPublicKey()),algorithm:V(r.response.getPublicKeyAlgorithm())},authenticatorData:s(i.getAuthenticatorData()),clientData:s(i.clientDataJSON)};return t.attestation&&(o.attestationData=s(i.attestationObject)),o}async function k(e){let a=["internal"],t=["hybrid","usb","ble","nfc"];if(e==="local")return a;if(e=="roaming"||e==="extern")return t;if(e==="both")return[...a,...t];try{return await A()?a:t}catch{return[...a,...t]}}async function Y(e,a,t){if(t=t??{},!m(a))throw new Error("Provided challenge is not properly encoded in Base64url");let n=await k(t.authenticatorType??"auto"),r={challenge:c(a),rpId:window.location.hostname,allowCredentials:e.map(f=>({id:c(f),type:"public-key",transports:n})),userVerification:t.userVerification??"required",timeout:t.timeout??6e4};t.debug&&console.debug(r);let i=await navigator.credentials.get({publicKey:r,mediation:t.mediation});t.debug&&console.debug(i);let o=i.response;return{credentialId:i.id,authenticatorData:s(o.authenticatorData),clientData:s(o.clientDataJSON),signature:s(o.signature)}}var I={};b(I,{verifyAuthentication:()=>j,verifyRegistration:()=>_,verifySignature:()=>U});var D={};b(D,{parseAttestation:()=>x,parseAuthentication:()=>B,parseAuthenticator:()=>C,parseClient:()=>P,parseRegistration:()=>S});var K={"9c835346-796b-4c27-8898-d6032f515cc5":{name:"Cryptnox FIDO2"},"c5ef55ff-ad9a-4b9f-b580-adebafe026d0":{name:"YubiKey 5Ci"},"39a5647e-1853-446c-a1f6-a79bae9f5bc7":{name:"Vancosys Android Authenticator"},"3789da91-f943-46bc-95c3-50ea2012f03a":{name:"NEOWAVE Winkeo FIDO2"},"fa2b99dc-9e39-4257-8f92-4a30d23c4118":{name:"YubiKey 5 Series with NFC"},"4e768f2c-5fab-48b3-b300-220eb487752b":{name:"Hideez Key 4 FIDO2 SDK"},"931327dd-c89b-406c-a81e-ed7058ef36c6":{name:"Swissbit iShield FIDO2"},"e1a96183-5016-4f24-b55b-e3ae23614cc6":{name:"ATKey.Pro CTAP2.0"},"08987058-cadc-4b81-b6e1-30de50dcbe96":{name:"Windows Hello Hardware Authenticator"},"d91c5288-0ef0-49b7-b8ae-21ca0aa6b3f3":{name:"KEY-ID FIDO2 Authenticator"},"ee041bce-25e5-4cdb-8f86-897fd6418464":{name:"Feitian ePass FIDO2-NFC Authenticator"},"73bb0cd4-e502-49b8-9c6f-b59445bf720b":{name:"YubiKey 5 FIPS Series"},"149a2021-8ef6-4133-96b8-81f8d5b7f1f5":{name:"Security Key by Yubico with NFC"},"3b1adb99-0dfe-46fd-90b8-7f7614a4de2a":{name:"GoTrust Idem Key FIDO2 Authenticator"},"f8a011f3-8c0a-4d15-8006-17111f9edc7d":{name:"Security Key by Yubico"},"2c0df832-92de-4be1-8412-88a8f074df4a":{name:"Feitian FIDO Smart Card"},"c5703116-972b-4851-a3e7-ae1259843399":{name:"NEOWAVE Badgeo FIDO2"},"820d89ed-d65a-409e-85cb-f73f0578f82a":{name:"Vancosys iOS Authenticator"},"b6ede29c-3772-412c-8a78-539c1f4c62d2":{name:"Feitian BioPass FIDO2 Plus Authenticator"},"85203421-48f9-4355-9bc8-8a53846e5083":{name:"YubiKey 5Ci FIPS"},"d821a7d4-e97c-4cb6-bd82-4237731fd4be":{name:"Hyper FIDO Bio Security Key"},"516d3969-5a57-5651-5958-4e7a49434167":{name:"SmartDisplayer BobeePass (NFC-BLE FIDO2 Authenticator)"},"b93fd961-f2e6-462f-b122-82002247de78":{name:"Android Authenticator with SafetyNet Attestation"},"2fc0579f-8113-47ea-b116-bb5a8db9202a":{name:"YubiKey 5 Series with NFC"},"9ddd1817-af5a-4672-a2b9-3e3dd95000a9":{name:"Windows Hello VBS Hardware Authenticator"},"d8522d9f-575b-4866-88a9-ba99fa02f35b":{name:"YubiKey Bio Series"},"692db549-7ae5-44d5-a1e5-dd20a493b723":{name:"HID Crescendo Key"},"3e22415d-7fdf-4ea4-8a0c-dd60c4249b9d":{name:"Feitian iePass FIDO Authenticator"},"aeb6569c-f8fb-4950-ac60-24ca2bbe2e52":{name:"HID Crescendo C2300"},"9f0d8150-baa5-4c00-9299-ad62c8bb4e87":{name:"GoTrust Idem Card FIDO2 Authenticator"},"12ded745-4bed-47d4-abaa-e713f51d6393":{name:"Feitian AllinOne FIDO2 Authenticator"},"88bbd2f0-342a-42e7-9729-dd158be5407a":{name:"Precision InnaIT Key FIDO 2 Level 2 certified"},"34f5766d-1536-4a24-9033-0e294e510fb0":{name:"YubiKey 5 Series CTAP2.1 Preview 1 "},"83c47309-aabb-4108-8470-8be838b573cb":{name:"YubiKey Bio Series (Enterprise Profile)"},"be727034-574a-f799-5c76-0929e0430973":{name:"Crayonic KeyVault K1 (USB-NFC-BLE FIDO2 Authenticator)"},"b92c3f9a-c014-4056-887f-140a2501163b":{name:"Security Key by Yubico"},"54d9fee8-e621-4291-8b18-7157b99c5bec":{name:"HID Crescendo Enabled"},"6028b017-b1d4-4c02-b4b3-afcdafc96bb2":{name:"Windows Hello Software Authenticator"},"6d44ba9b-f6ec-2e49-b930-0c8fe920cb73":{name:"Security Key by Yubico with NFC"},"e416201b-afeb-41ca-a03d-2281c28322aa":{name:"ATKey.Pro CTAP2.1"},"9f77e279-a6e2-4d58-b700-31e5943c6a98":{name:"Hyper FIDO Pro"},"73402251-f2a8-4f03-873e-3cb6db604b03":{name:"uTrust FIDO2 Security Key"},"c1f9a0bc-1dd2-404a-b27f-8e29047a43fd":{name:"YubiKey 5 FIPS Series with NFC"},"504d7149-4e4c-3841-4555-55445a677357":{name:"WiSECURE AuthTron USB FIDO2 Authenticator"},"cb69481e-8ff7-4039-93ec-0a2729a154a8":{name:"YubiKey 5 Series"},"ee882879-721c-4913-9775-3dfcce97072a":{name:"YubiKey 5 Series"},"8c97a730-3f7b-41a6-87d6-1e9b62bda6f0":{name:"FT-JCOS FIDO Fingerprint Card"},"a1f52be5-dfab-4364-b51c-2bd496b14a56":{name:"OCTATCO EzFinger2 FIDO2 AUTHENTICATOR"},"3e078ffd-4c54-4586-8baa-a77da113aec5":{name:"Hideez Key 3 FIDO2"},"d41f5a69-b817-4144-a13c-9ebd6d9254d6":{name:"ATKey.Card CTAP2.0"},"bc2fe499-0d8e-4ffe-96f3-94a82840cf8c":{name:"OCTATCO EzQuant FIDO2 AUTHENTICATOR"},"1c086528-58d5-f211-823c-356786e36140":{name:"Atos CardOS FIDO2"},"77010bd7-212a-4fc9-b236-d2ca5e9d4084":{name:"Feitian BioPass FIDO2 Authenticator"},"833b721a-ff5f-4d00-bb2e-bdda3ec01e29":{name:"Feitian ePass FIDO2 Authenticator"}};function F(e){console.debug(e);let a=new DataView(e.slice(32,33)).getUint8(0);console.debug(a);let t={rpIdHash:s(e.slice(0,32)),flags:{userPresent:!!(a&1),userVerified:!!(a&4),backupEligibility:!!(a&8),backupState:!!(a&16),attestedData:!!(a&64),extensionsIncluded:!!(a&128)},counter:new DataView(e.slice(33,37)).getUint32(0,!1)};if(e.byteLength>37){let n=$(e);t={...t,aaguid:n,name:W(n)}}return t}function $(e){return L(e.slice(37,53))}function L(e){let a=h(e);return a=a.substring(0,8)+"-"+a.substring(8,12)+"-"+a.substring(12,16)+"-"+a.substring(16,20)+"-"+a.substring(20,32),a}function W(e){return(J??K)[e]?.name}var J=null;var q=new TextDecoder("utf-8");function P(e){return typeof e=="string"&&(e=c(e)),JSON.parse(q.decode(e))}function C(e){return typeof e=="string"&&(e=c(e)),F(e)}function x(e){return typeof e=="string"&&(e=c(e)),"Really complex to parse. Good luck with that one!"}function S(e){return{username:e.username,credential:e.credential,client:P(e.clientData),authenticator:C(e.authenticatorData),attestation:e.attestationData?x(e.attestationData):null}}function B(e){return{credentialId:e.credentialId,client:P(e.clientData),authenticator:C(e.authenticatorData),signature:e.signature}}async function z(e,a){if(typeof e=="function"){let t=e(a);return t instanceof Promise?await t:t}return e===a}async function g(e,a){return!await z(e,a)}async function _(e,a){let t=S(e);if(t.client.challenge,t.client.type!=="webauthn.create")throw new Error(`Unexpected ClientData type: ${t.client.type}`);if(await g(a.origin,t.client.origin))throw new Error(`Unexpected ClientData origin: ${t.client.origin}`);if(await g(a.challenge,t.client.challenge))throw new Error(`Unexpected ClientData challenge: ${t.client.challenge}`);return t}async function j(e,a,t){if(e.credentialId!==a.id)throw new Error(`Credential ID mismatch: ${e.credentialId} vs ${a.id}`);if(!await U({algorithm:a.algorithm,publicKey:a.publicKey,authenticatorData:e.authenticatorData,clientData:e.clientData,signature:e.signature}))throw new Error(`Invalid signature: ${e.signature}`);let r=B(e);if(r.client.type!=="webauthn.get")throw new Error(`Unexpected clientData type: ${r.client.type}`);if(await g(t.origin,r.client.origin))throw new Error(`Unexpected ClientData origin: ${r.client.origin}`);if(await g(t.challenge,r.client.challenge))throw new Error(`Unexpected ClientData challenge: ${r.client.challenge}`);let i=new URL(r.client.origin).hostname,o=s(await d(l(i)));if(r.authenticator.rpIdHash!==o)throw new Error(`Unexpected RpIdHash: ${r.authenticator.rpIdHash} vs ${o}`);if(!r.authenticator.flags.userPresent)throw new Error("Unexpected authenticator flags: missing userPresent");if(!r.authenticator.flags.userVerified&&t.userVerified)throw new Error("Unexpected authenticator flags: missing userVerified");if(t.counter&&r.authenticator.counter<=t.counter)throw new Error(`Unexpected authenticator counter: ${r.authenticator.counter} (should be > ${t.counter})`);return r}function G(e){switch(e){case"RS256":return{name:"RSASSA-PKCS1-v1_5",hash:"SHA-256"};case"ES256":return{name:"ECDSA",namedCurve:"P-256",hash:"SHA-256"};default:throw new Error(`Unknown or unsupported crypto algorithm: ${e}. Only 'RS256' and 'ES256' are supported.`)}}async function Q(e,a){let t=c(a);return crypto.subtle.importKey("spki",t,e,!1,["verify"])}async function U({algorithm:e,publicKey:a,authenticatorData:t,clientData:n,signature:r}){let i=G(e),o=await Q(i,a);console.debug(o);let E=await d(c(n)),f=p(c(t),E);console.debug("Crypto Algo: "+JSON.stringify(i)),console.debug("Public key: "+a),console.debug("Data: "+s(f)),console.debug("Signature: "+r);let y=c(r);return e=="ES256"&&(y=Z(y)),await crypto.subtle.verify(i,o,y,f)}function Z(e){let a=new Uint8Array(e),t=a[4]===0?5:4,n=t+32,r=a[n+2]===0?n+3:n+2,i=a.slice(t,n),o=a.slice(r);return new Uint8Array([...i,...o])}var ne={client:w,server:I,parsers:D,utils:u};export{w as client,ne as default,D as parsers,I as server,u as utils}; //# sourceMappingURL=webauthn.min.js.map diff --git a/dist/webauthn.min.js.map b/dist/webauthn.min.js.map index 353a553..d679ef3 100644 --- a/dist/webauthn.min.js.map +++ b/dist/webauthn.min.js.map @@ -1,7 +1,7 @@ { "version": 3, "sources": ["../src/client.ts", "../src/utils.ts", "../src/server.ts", "../src/parsers.ts", "../src/authenticatorMetadata.ts", "../src/authenticators.ts", "../src/index.ts"], - "sourcesContent": ["import * as utils from './utils.js'\nimport { AuthenticateOptions, AuthenticationEncoded, AuthType, NamedAlgo, NumAlgo, RegisterOptions, RegistrationEncoded } from './types.js'\n\n/**\n * Returns whether passwordless authentication is available on this browser/platform or not.\n */\n export function isAvailable() :boolean {\n return !!window.PublicKeyCredential\n}\n\n/**\n * Returns whether the device itself can be used as authenticator.\n */\nexport async function isLocalAuthenticator() :Promise {\n return await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()\n}\n\n\n\n\nasync function getAuthAttachment(authType :AuthType) :Promise {\n if(authType === \"local\")\n return \"platform\";\n if(authType === \"roaming\" || authType === \"extern\")\n return \"cross-platform\";\n if(authType === \"both\")\n return undefined // The webauthn protocol considers `null` as invalid but `undefined` as \"both\"!\n\n // the default case: \"auto\", depending on device capabilities\n try {\n if(await isLocalAuthenticator())\n return \"platform\"\n else\n return \"cross-platform\"\n } catch(e) {\n // might happen due to some security policies\n // see https://w3c.github.io/webauthn/#sctn-isUserVerifyingPlatformAuthenticatorAvailable\n return undefined // The webauthn protocol considers `null` as invalid but `undefined` as \"both\"!\n }\n}\n\n\n\nfunction getAlgoName(num :NumAlgo) :NamedAlgo {\n switch(num) {\n case -7: return \"ES256\"\n // case -8 ignored to to its rarity\n case -257: return \"RS256\"\n default: throw new Error(`Unknown algorithm code: ${num}`)\n }\n}\n\n\n\n/**\n * Creates a cryptographic key pair, in order to register the public key for later passwordless authentication.\n *\n * @param {string} username\n * @param {string} challenge A server-side randomly generated string.\n * @param {Object} [options] Optional parameters.\n * @param {number} [options.timeout=60000] Number of milliseconds the user has to respond to the biometric/PIN check.\n * @param {'required'|'preferred'|'discouraged'} [options.userVerification='required'] Whether to prompt for biometric/PIN check or not.\n * @param {'auto'|'local'|'roaming'|'both'} [options.authenticatorType='auto'] Which device to use as authenticator.\n * 'auto': if the local device can be used as authenticator it will be preferred. Otherwise it will prompt for a roaming device.\n * 'local': use the local device (using TouchID, FaceID, Windows Hello or PIN)\n * 'roaming': use a roaming device (security key or connected phone)\n * 'both': prompt the user to choose between local or roaming device. The UI and user interaction in this case is platform specific.\n * @param {boolean} [options.attestation=false] If enabled, the device attestation and clientData will be provided as Base64url encoded binary data.\n * Note that this is not available on some platforms.\n * @param {'discouraged'|'preferred'|'required'} [options.discoverable] If the credential is \"discoverable\", it can be selected using `authenticate` without providing credential IDs.\n * A native pop-up will appear for user selection. This may have an impact on \"passkeys\" user experience and syncing behavior.\n */\nexport async function register(username :string, challenge :string, options? :RegisterOptions) :Promise {\n options = options ?? {}\n\n if(!utils.isBase64url(challenge))\n throw new Error('Provided challenge is not properly encoded in Base64url')\n\n const creationOptions :PublicKeyCredentialCreationOptions = {\n challenge: utils.parseBase64url(challenge),\n rp: {\n id: window.location.hostname,\n name: window.location.hostname\n },\n user: {\n id: options.userHandle ? utils.toBuffer(options.userHandle) : await utils.sha256(new TextEncoder().encode('passwordless.id-user:' + username)), // ID should not be directly \"identifiable\" for privacy concerns\n name: username,\n displayName: username,\n },\n pubKeyCredParams: [\n {alg: -7, type: \"public-key\"}, // ES256 (Webauthn's default algorithm)\n {alg: -257, type: \"public-key\"}, // RS256 (for Windows Hello and others)\n ],\n timeout: options.timeout ?? 60000,\n authenticatorSelection: {\n userVerification: options.userVerification ?? \"required\", // Webauthn default is \"preferred\"\n authenticatorAttachment: await getAuthAttachment(options.authenticatorType ?? \"auto\"),\n residentKey: options.discoverable ?? 'preferred', // official default is 'discouraged'\n requireResidentKey: (options.discoverable === 'required') // mainly for backwards compatibility, see https://www.w3.org/TR/webauthn/#dictionary-authenticatorSelection\n },\n attestation: options.attestation ? \"direct\" : \"none\"\n }\n\n if(options.debug)\n console.debug(creationOptions)\n\n const credential = await navigator.credentials.create({publicKey: creationOptions}) as any //PublicKeyCredential\n \n if(options.debug)\n console.debug(credential)\n \n const response = credential.response as any // AuthenticatorAttestationResponse\n \n let registration :RegistrationEncoded = {\n username: username,\n credential: {\n id: credential.id,\n publicKey: utils.toBase64url(response.getPublicKey()),\n algorithm: getAlgoName(credential.response.getPublicKeyAlgorithm())\n },\n authenticatorData: utils.toBase64url(response.getAuthenticatorData()),\n clientData: utils.toBase64url(response.clientDataJSON),\n }\n\n if(options.attestation) {\n registration.attestationData = utils.toBase64url(response.attestationObject)\n }\n\n return registration\n}\n\n\nasync function getTransports(authType :AuthType) :Promise {\n const local :AuthenticatorTransport[] = ['internal']\n\n // 'hybrid' was added mid-2022 in the specs and currently not yet available in the official dom types\n // @ts-ignore\n const roaming :AuthenticatorTransport[] = ['hybrid', 'usb', 'ble', 'nfc']\n \n if(authType === \"local\")\n return local\n if(authType == \"roaming\" || authType === \"extern\")\n return roaming\n if(authType === \"both\")\n return [...local, ...roaming]\n\n // the default case: \"auto\", depending on device capabilities\n try {\n if(await isLocalAuthenticator())\n return local\n else\n return roaming\n } catch(e) {\n return [...local, ...roaming]\n }\n}\n\n\n/**\n * Signs a challenge using one of the provided credentials IDs in order to authenticate the user.\n *\n * @param {string[]} credentialIds The list of credential IDs that can be used for signing.\n * @param {string} challenge A server-side randomly generated string, the base64 encoded version will be signed.\n * @param {Object} [options] Optional parameters.\n * @param {number} [options.timeout=60000] Number of milliseconds the user has to respond to the biometric/PIN check.\n * @param {'required'|'preferred'|'discouraged'} [options.userVerification='required'] Whether to prompt for biometric/PIN check or not.\n * @param {'optional'|'conditional'|'required'|'silent'} [options.mediation='optional'] https://developer.mozilla.org/en-US/docs/Web/API/CredentialsContainer/get#mediation\n */\nexport async function authenticate(credentialIds :string[], challenge :string, options? :AuthenticateOptions) :Promise {\n options = options ?? {}\n\n if(!utils.isBase64url(challenge))\n throw new Error('Provided challenge is not properly encoded in Base64url')\n\n const transports = await getTransports(options.authenticatorType ?? \"auto\");\n\n let authOptions :PublicKeyCredentialRequestOptions = {\n challenge: utils.parseBase64url(challenge),\n rpId: window.location.hostname,\n allowCredentials: credentialIds.map(id => { return {\n id: utils.parseBase64url(id),\n type: 'public-key',\n transports: transports,\n }}),\n userVerification: options.userVerification ?? \"required\",\n timeout: options.timeout ?? 60000,\n }\n\n if(options.debug)\n console.debug(authOptions)\n\n let auth = await navigator.credentials.get({publicKey: authOptions, mediation: options.mediation}) as PublicKeyCredential\n \n if(options.debug)\n console.debug(auth)\n\n const response = auth.response as AuthenticatorAssertionResponse\n \n const authentication :AuthenticationEncoded = {\n credentialId: auth.id,\n //userHash: utils.toBase64url(response.userHandle), // unreliable, optional for authenticators\n authenticatorData: utils.toBase64url(response.authenticatorData),\n clientData: utils.toBase64url(response.clientDataJSON),\n signature: utils.toBase64url(response.signature),\n }\n\n return authentication\n}\n\n", "/********************************\n Encoding/Decoding Utils\n********************************/\n\n/*\nlet webCrypto :any = null\n\nexport async function getCrypto() {\n if(!webCrypto) {\n console.log(window?.crypto)\n webCrypto = window?.crypto ?? (await import(\"crypto\")).webcrypto\n console.log(webCrypto)\n }\n return webCrypto\n}\n*/\n\n\nexport function randomChallenge() {\n return crypto.randomUUID()\n}\n\n\nexport function toBuffer(txt :string) :ArrayBuffer {\n return Uint8Array.from(txt, c => c.charCodeAt(0)).buffer\n}\n\nexport function parseBuffer(buffer :ArrayBuffer) :string {\n return String.fromCharCode(...new Uint8Array(buffer))\n}\n\n\nexport function isBase64url(txt :string) :boolean {\n return txt.match(/^[a-zA-Z0-9\\-_]+=*$/) !== null\n}\n\nexport function toBase64url(buffer :ArrayBuffer) :string {\n const txt = btoa(parseBuffer(buffer)) // base64\n return txt.replaceAll('+', '-').replaceAll('/', '_')\n}\n\nexport function parseBase64url(txt :string) :ArrayBuffer {\n txt = txt.replaceAll('-', '+').replaceAll('_', '/') // base64url -> base64\n return toBuffer(atob(txt))\n}\n\n\nexport async function sha256(buffer :ArrayBuffer) :Promise {\n return await crypto.subtle.digest('SHA-256', buffer)\n}\n\nexport function bufferToHex (buffer :ArrayBuffer) :string {\n return [...new Uint8Array (buffer)]\n .map (b => b.toString (16).padStart (2, \"0\"))\n .join (\"\");\n}\n\n\nexport function concatenateBuffers(buffer1 :ArrayBuffer, buffer2 :ArrayBuffer) {\n var tmp = new Uint8Array(buffer1.byteLength + buffer2.byteLength);\n tmp.set(new Uint8Array(buffer1), 0);\n tmp.set(new Uint8Array(buffer2), buffer1.byteLength);\n return tmp;\n };", "import { parseAuthentication, parseRegistration } from \"./parsers.js\";\nimport { AuthenticationEncoded, AuthenticationParsed, CredentialKey, NamedAlgo, RegistrationEncoded, RegistrationParsed } from \"./types.js\";\nimport * as utils from './utils.js'\n\n\nasync function isValid(validator :any, value :any) :Promise {\n if(typeof validator === 'function') {\n const res = validator(value)\n if(res instanceof Promise)\n return await res\n else\n return res\n }\n // the validator can be a single value too\n return validator === value\n}\n\nasync function isNotValid(validator :any, value :any) :Promise {\n return !(await isValid(validator, value))\n}\n\ninterface RegistrationChecks {\n challenge: string | Function,\n origin: string | Function\n}\n\n\nexport async function verifyRegistration(registrationRaw: RegistrationEncoded, expected: RegistrationChecks): Promise {\n const registration = parseRegistration(registrationRaw)\n registration.client.challenge\n\n if (registration.client.type !== \"webauthn.create\")\n throw new Error(`Unexpected ClientData type: ${registration.client.type}`)\n\n if (await isNotValid(expected.origin, registration.client.origin))\n throw new Error(`Unexpected ClientData origin: ${registration.client.origin}`)\n\n if (await isNotValid(expected.challenge, registration.client.challenge))\n throw new Error(`Unexpected ClientData challenge: ${registration.client.challenge}`)\n\n return registration\n}\n\n\ninterface AuthenticationChecks {\n challenge: string | Function,\n origin: string | Function,\n userVerified: boolean,\n counter: number\n}\n\n\nexport async function verifyAuthentication(authenticationRaw: AuthenticationEncoded, credential: CredentialKey, expected: AuthenticationChecks): Promise {\n if (authenticationRaw.credentialId !== credential.id)\n throw new Error(`Credential ID mismatch: ${authenticationRaw.credentialId} vs ${credential.id}`)\n\n const isValidSignature: boolean = await verifySignature({\n algorithm: credential.algorithm,\n publicKey: credential.publicKey,\n authenticatorData: authenticationRaw.authenticatorData,\n clientData: authenticationRaw.clientData,\n signature: authenticationRaw.signature\n })\n\n if(!isValidSignature)\n throw new Error(`Invalid signature: ${authenticationRaw.signature}`)\n\n const authentication = parseAuthentication(authenticationRaw)\n\n if (authentication.client.type !== \"webauthn.get\")\n throw new Error(`Unexpected clientData type: ${authentication.client.type}`)\n\n if (await isNotValid(expected.origin, authentication.client.origin))\n throw new Error(`Unexpected ClientData origin: ${authentication.client.origin}`)\n\n if (await isNotValid(expected.challenge, authentication.client.challenge))\n throw new Error(`Unexpected ClientData challenge: ${authentication.client.challenge}`)\n\n // this only works because we consider `rp.origin` and `rp.id` to be the same during authentication/registration\n const rpId = new URL(authentication.client.origin).hostname\n const expectedRpIdHash = utils.toBase64url(await utils.sha256(utils.toBuffer(rpId)))\n if (authentication.authenticator.rpIdHash !== expectedRpIdHash)\n throw new Error(`Unexpected RpIdHash: ${authentication.authenticator.rpIdHash} vs ${expectedRpIdHash}`)\n\n if (!authentication.authenticator.flags.userPresent)\n throw new Error(`Unexpected authenticator flags: missing userPresent`)\n\n if (!authentication.authenticator.flags.userVerified && expected.userVerified)\n throw new Error(`Unexpected authenticator flags: missing userVerified`)\n\n if (authentication.authenticator.counter <= expected.counter)\n throw new Error(`Unexpected authenticator counter: ${authentication.authenticator.counter} (should be > ${expected.counter})`)\n\n return authentication\n}\n\n\n// https://w3c.github.io/webauthn/#sctn-public-key-easy\n// https://www.iana.org/assignments/cose/cose.xhtml#algorithms\n/*\nUser agents MUST be able to return a non-null value for getPublicKey() when the credential public key has a COSEAlgorithmIdentifier value of:\n\n-7 (ES256), where kty is 2 (with uncompressed points) and crv is 1 (P-256).\n\n-257 (RS256).\n\n-8 (EdDSA), where crv is 6 (Ed25519).\n*/\nfunction getAlgoParams(algorithm: NamedAlgo): any {\n switch (algorithm) {\n case 'RS256':\n return {\n name: 'RSASSA-PKCS1-v1_5',\n hash: 'SHA-256'\n };\n case 'ES256':\n return {\n name: 'ECDSA',\n namedCurve: 'P-256',\n hash: 'SHA-256',\n };\n // case 'EdDSA': Not supported by browsers\n default:\n throw new Error(`Unknown or unsupported crypto algorithm: ${algorithm}. Only 'RS256' and 'ES256' are supported.`)\n }\n}\n\ntype AlgoParams = AlgorithmIdentifier | RsaHashedImportParams | EcKeyImportParams | HmacImportParams | AesKeyAlgorithm\n\nasync function parseCryptoKey(algoParams: AlgoParams, publicKey: string): Promise {\n const buffer = utils.parseBase64url(publicKey)\n return crypto.subtle.importKey('spki', buffer, algoParams, false, ['verify'])\n}\n\n\n\ntype VerifyParams = {\n algorithm: NamedAlgo,\n publicKey: string, // Base64url encoded\n authenticatorData: string, // Base64url encoded\n clientData: string, // Base64url encoded\n signature: string, // Base64url encoded\n}\n\n\n// https://w3c.github.io/webauthn/#sctn-verifying-assertion\n// https://w3c.github.io/webauthn/#sctn-signature-attestation-types\n/* Emphasis mine:\n\n6.5.6. Signature Formats for Packed Attestation, FIDO U2F Attestation, and **Assertion Signatures**\n\n[...] For COSEAlgorithmIdentifier -7 (ES256) [...] the sig value MUST be encoded as an ASN.1 [...]\n[...] For COSEAlgorithmIdentifier -257 (RS256) [...] The signature is not ASN.1 wrapped.\n[...] For COSEAlgorithmIdentifier -37 (PS256) [...] The signature is not ASN.1 wrapped.\n*/\n// see also https://gist.github.com/philholden/50120652bfe0498958fd5926694ba354\nexport async function verifySignature({ algorithm, publicKey, authenticatorData, clientData, signature }: VerifyParams): Promise {\n const algoParams = getAlgoParams(algorithm)\n let cryptoKey = await parseCryptoKey(algoParams, publicKey)\n console.debug(cryptoKey)\n\n let clientHash = await utils.sha256(utils.parseBase64url(clientData));\n\n // during \"login\", the authenticatorData is exactly 37 bytes\n let comboBuffer = utils.concatenateBuffers(utils.parseBase64url(authenticatorData), clientHash)\n\n console.debug('Crypto Algo: ' + JSON.stringify(algoParams))\n console.debug('Public key: ' + publicKey)\n console.debug('Data: ' + utils.toBase64url(comboBuffer))\n console.debug('Signature: ' + signature)\n\n // https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/verify\n let signatureBuffer = utils.parseBase64url(signature)\n if(algorithm == 'ES256')\n signatureBuffer = convertASN1toRaw(signatureBuffer)\n\n const isValid = await crypto.subtle.verify(algoParams, cryptoKey, signatureBuffer, comboBuffer)\n\n return isValid\n}\n\nfunction convertASN1toRaw(signatureBuffer :ArrayBuffer) {\n // Convert signature from ASN.1 sequence to \"raw\" format\n const usignature = new Uint8Array(signatureBuffer);\n const rStart = usignature[4] === 0 ? 5 : 4;\n const rEnd = rStart + 32;\n const sStart = usignature[rEnd + 2] === 0 ? rEnd + 3 : rEnd + 2;\n const r = usignature.slice(rStart, rEnd);\n const s = usignature.slice(sStart);\n return new Uint8Array([...r, ...s]);\n}", "import * as authenticators from './authenticators.js'\nimport * as utils from './utils.js'\nimport { AuthenticatorInfo, ClientInfo, RegistrationEncoded, RegistrationParsed, AuthenticationEncoded, AuthenticationParsed } from './types'\n\nconst utf8Decoder = new TextDecoder('utf-8')\n\nexport function parseClient(data :string|ArrayBuffer) :ClientInfo {\n if(typeof data == 'string')\n data = utils.parseBase64url(data)\n return JSON.parse(utf8Decoder.decode(data))\n}\n\n\nexport function parseAuthenticator(data :string|ArrayBuffer) :AuthenticatorInfo {\n if(typeof data == 'string')\n data = utils.parseBase64url(data)\n return authenticators.parseAuthBuffer(data)\n}\n\n\nexport function parseAttestation(data :string|ArrayBuffer) :unknown {\n if(typeof data == 'string')\n data = utils.parseBase64url(data)\n return 'Really complex to parse. Good luck with that one!'\n}\n\n\n\nexport function parseRegistration(registration :RegistrationEncoded) :RegistrationParsed {\n return {\n username: registration.username,\n credential: registration.credential,\n\n client: parseClient(registration.clientData),\n authenticator: parseAuthenticator(registration.authenticatorData),\n attestation: registration.attestationData ? parseAttestation(registration.attestationData) : null\n }\n}\n\nexport function parseAuthentication(authentication :AuthenticationEncoded) :AuthenticationParsed {\n return {\n credentialId: authentication.credentialId,\n client: parseClient(authentication.clientData),\n authenticator: parseAuthenticator(authentication.authenticatorData),\n signature: authentication.signature\n }\n}", "export default {\n \"9c835346-796b-4c27-8898-d6032f515cc5\": {\n \"name\": \"Cryptnox FIDO2\"\n },\n \"c5ef55ff-ad9a-4b9f-b580-adebafe026d0\": {\n \"name\": \"YubiKey 5Ci\"\n },\n \"39a5647e-1853-446c-a1f6-a79bae9f5bc7\": {\n \"name\": \"Vancosys Android Authenticator\"\n },\n \"3789da91-f943-46bc-95c3-50ea2012f03a\": {\n \"name\": \"NEOWAVE Winkeo FIDO2\"\n },\n \"fa2b99dc-9e39-4257-8f92-4a30d23c4118\": {\n \"name\": \"YubiKey 5 Series with NFC\"\n },\n \"4e768f2c-5fab-48b3-b300-220eb487752b\": {\n \"name\": \"Hideez Key 4 FIDO2 SDK\"\n },\n \"931327dd-c89b-406c-a81e-ed7058ef36c6\": {\n \"name\": \"Swissbit iShield FIDO2\"\n },\n \"e1a96183-5016-4f24-b55b-e3ae23614cc6\": {\n \"name\": \"ATKey.Pro CTAP2.0\"\n },\n \"08987058-cadc-4b81-b6e1-30de50dcbe96\": {\n \"name\": \"Windows Hello Hardware Authenticator\"\n },\n \"d91c5288-0ef0-49b7-b8ae-21ca0aa6b3f3\": {\n \"name\": \"KEY-ID FIDO2 Authenticator\"\n },\n \"ee041bce-25e5-4cdb-8f86-897fd6418464\": {\n \"name\": \"Feitian ePass FIDO2-NFC Authenticator\"\n },\n \"73bb0cd4-e502-49b8-9c6f-b59445bf720b\": {\n \"name\": \"YubiKey 5 FIPS Series\"\n },\n \"149a2021-8ef6-4133-96b8-81f8d5b7f1f5\": {\n \"name\": \"Security Key by Yubico with NFC\"\n },\n \"3b1adb99-0dfe-46fd-90b8-7f7614a4de2a\": {\n \"name\": \"GoTrust Idem Key FIDO2 Authenticator\"\n },\n \"f8a011f3-8c0a-4d15-8006-17111f9edc7d\": {\n \"name\": \"Security Key by Yubico\"\n },\n \"2c0df832-92de-4be1-8412-88a8f074df4a\": {\n \"name\": \"Feitian FIDO Smart Card\"\n },\n \"c5703116-972b-4851-a3e7-ae1259843399\": {\n \"name\": \"NEOWAVE Badgeo FIDO2\"\n },\n \"820d89ed-d65a-409e-85cb-f73f0578f82a\": {\n \"name\": \"Vancosys iOS Authenticator\"\n },\n \"b6ede29c-3772-412c-8a78-539c1f4c62d2\": {\n \"name\": \"Feitian BioPass FIDO2 Plus Authenticator\"\n },\n \"85203421-48f9-4355-9bc8-8a53846e5083\": {\n \"name\": \"YubiKey 5Ci FIPS\"\n },\n \"d821a7d4-e97c-4cb6-bd82-4237731fd4be\": {\n \"name\": \"Hyper FIDO Bio Security Key\"\n },\n \"516d3969-5a57-5651-5958-4e7a49434167\": {\n \"name\": \"SmartDisplayer BobeePass (NFC-BLE FIDO2 Authenticator)\"\n },\n \"b93fd961-f2e6-462f-b122-82002247de78\": {\n \"name\": \"Android Authenticator with SafetyNet Attestation\"\n },\n \"2fc0579f-8113-47ea-b116-bb5a8db9202a\": {\n \"name\": \"YubiKey 5 Series with NFC\"\n },\n \"9ddd1817-af5a-4672-a2b9-3e3dd95000a9\": {\n \"name\": \"Windows Hello VBS Hardware Authenticator\"\n },\n \"d8522d9f-575b-4866-88a9-ba99fa02f35b\": {\n \"name\": \"YubiKey Bio Series\"\n },\n \"692db549-7ae5-44d5-a1e5-dd20a493b723\": {\n \"name\": \"HID Crescendo Key\"\n },\n \"3e22415d-7fdf-4ea4-8a0c-dd60c4249b9d\": {\n \"name\": \"Feitian iePass FIDO Authenticator\"\n },\n \"aeb6569c-f8fb-4950-ac60-24ca2bbe2e52\": {\n \"name\": \"HID Crescendo C2300\"\n },\n \"9f0d8150-baa5-4c00-9299-ad62c8bb4e87\": {\n \"name\": \"GoTrust Idem Card FIDO2 Authenticator\"\n },\n \"12ded745-4bed-47d4-abaa-e713f51d6393\": {\n \"name\": \"Feitian AllinOne FIDO2 Authenticator\"\n },\n \"88bbd2f0-342a-42e7-9729-dd158be5407a\": {\n \"name\": \"Precision InnaIT Key FIDO 2 Level 2 certified\"\n },\n \"34f5766d-1536-4a24-9033-0e294e510fb0\": {\n \"name\": \"YubiKey 5 Series CTAP2.1 Preview 1 \"\n },\n \"83c47309-aabb-4108-8470-8be838b573cb\": {\n \"name\": \"YubiKey Bio Series (Enterprise Profile)\"\n },\n \"be727034-574a-f799-5c76-0929e0430973\": {\n \"name\": \"Crayonic KeyVault K1 (USB-NFC-BLE FIDO2 Authenticator)\"\n },\n \"b92c3f9a-c014-4056-887f-140a2501163b\": {\n \"name\": \"Security Key by Yubico\"\n },\n \"54d9fee8-e621-4291-8b18-7157b99c5bec\": {\n \"name\": \"HID Crescendo Enabled\"\n },\n \"6028b017-b1d4-4c02-b4b3-afcdafc96bb2\": {\n \"name\": \"Windows Hello Software Authenticator\"\n },\n \"6d44ba9b-f6ec-2e49-b930-0c8fe920cb73\": {\n \"name\": \"Security Key by Yubico with NFC\"\n },\n \"e416201b-afeb-41ca-a03d-2281c28322aa\": {\n \"name\": \"ATKey.Pro CTAP2.1\"\n },\n \"9f77e279-a6e2-4d58-b700-31e5943c6a98\": {\n \"name\": \"Hyper FIDO Pro\"\n },\n \"73402251-f2a8-4f03-873e-3cb6db604b03\": {\n \"name\": \"uTrust FIDO2 Security Key\"\n },\n \"c1f9a0bc-1dd2-404a-b27f-8e29047a43fd\": {\n \"name\": \"YubiKey 5 FIPS Series with NFC\"\n },\n \"504d7149-4e4c-3841-4555-55445a677357\": {\n \"name\": \"WiSECURE AuthTron USB FIDO2 Authenticator\"\n },\n \"cb69481e-8ff7-4039-93ec-0a2729a154a8\": {\n \"name\": \"YubiKey 5 Series\"\n },\n \"ee882879-721c-4913-9775-3dfcce97072a\": {\n \"name\": \"YubiKey 5 Series\"\n },\n \"8c97a730-3f7b-41a6-87d6-1e9b62bda6f0\": {\n \"name\": \"FT-JCOS FIDO Fingerprint Card\"\n },\n \"a1f52be5-dfab-4364-b51c-2bd496b14a56\": {\n \"name\": \"OCTATCO EzFinger2 FIDO2 AUTHENTICATOR\"\n },\n \"3e078ffd-4c54-4586-8baa-a77da113aec5\": {\n \"name\": \"Hideez Key 3 FIDO2\"\n },\n \"d41f5a69-b817-4144-a13c-9ebd6d9254d6\": {\n \"name\": \"ATKey.Card CTAP2.0\"\n },\n \"bc2fe499-0d8e-4ffe-96f3-94a82840cf8c\": {\n \"name\": \"OCTATCO EzQuant FIDO2 AUTHENTICATOR\"\n },\n \"1c086528-58d5-f211-823c-356786e36140\": {\n \"name\": \"Atos CardOS FIDO2\"\n },\n \"77010bd7-212a-4fc9-b236-d2ca5e9d4084\": {\n \"name\": \"Feitian BioPass FIDO2 Authenticator\"\n },\n \"833b721a-ff5f-4d00-bb2e-bdda3ec01e29\": {\n \"name\": \"Feitian ePass FIDO2 Authenticator\"\n }\n}", "import authenticatorMetadata from './authenticatorMetadata.js' //assert {type: 'json'}\nimport * as utils from './utils.js'\n\n\nexport function parseAuthBuffer(authData :ArrayBuffer) {\n console.debug(authData)\n let flags = new DataView(authData.slice(32,33)).getUint8(0)\n console.debug(flags)\n\n // https://w3c.github.io/webauthn/#sctn-authenticator-data\n let parsed :any = {\n rpIdHash: utils.toBase64url(authData.slice(0,32)),\n flags: {\n userPresent: !!(flags & 1),\n //reserved1: !!(flags & 2),\n userVerified: !!(flags & 4),\n backupEligibility: !!(flags & 8),\n backupState: !!(flags & 16),\n //reserved2: !!(flags & 32),\n attestedData: !!(flags & 64),\n extensionsIncluded: !!(flags & 128)\n },\n counter: new DataView(authData.slice(33,37)).getUint32(0, false), // Big-Endian!\n }\n\n if(authData.byteLength > 37) {\n // registration contains additional data\n\n const aaguid = extractAaguid(authData)\n // https://w3c.github.io/webauthn/#attested-credential-data\n //let credentialLength = new DataView(authData.slice(53,55)).getUint16(0, false) // Big-Endian!\n \n parsed = {\n ...parsed,\n aaguid, // bytes 37->53\n name: resolveAuthenticatorName(aaguid)\n // credentialBytes, // bytes 53->55: credential length\n // credentialId: utils.toBase64url(authData.slice(55, 55+credentialLength)),\n //publicKey: until where? ...and it's encoded using a strange format, let's better avoid it\n //extensions: starting where?\n }\n }\n\n return parsed\n}\n\nexport function extractAaguid(authData :ArrayBuffer) :string {\n return formatAaguid(authData.slice(37, 53)) // 16 bytes\n}\n\nfunction formatAaguid(buffer :ArrayBuffer) :string {\n let aaguid = utils.bufferToHex(buffer)\n aaguid = aaguid.substring(0,8) + '-' + aaguid.substring(8,12) + '-' + aaguid.substring(12,16) + '-' + aaguid.substring(16,20) + '-' + aaguid.substring(20,32)\n return aaguid // example: \"d41f5a69-b817-4144-a13c-9ebd6d9254d6\"\n}\n\nexport function resolveAuthenticatorName(aaguid :string) :string {\n const aaguidMetadata = updatedAuthenticatorMetadata ?? authenticatorMetadata //await getAaguidMetadata()\n return aaguidMetadata[aaguid]?.name\n}\n\nlet updatedAuthenticatorMetadata :any = null\n\n// List of AAGUIDs are encoded as JWT here: https://mds.fidoalliance.org/\nexport async function updateDevicesMetadata() {\n // this function is rather resource intensive and time consuming\n // therefore, the result is cached in local storage\n const jwt = await (await fetch(\"https://mds.fidoalliance.org\")).text()\n\n // the response is a JWT including all AAGUIDs and their metadata\n console.debug(jwt)\n\n // let us ignore the JWT verification, since this is solely for descriptive purposes, not signed data\n const payload = jwt.split('.')[1].replaceAll('-', '+').replaceAll('_', '/')\n const json = JSON.parse(atob(payload))\n console.debug(json)\n\n let aaguidMetadata :any = {}\n for(const e of json.entries) {\n if(!e.aaguid || !e.metadataStatement)\n continue\n\n aaguidMetadata[e.aaguid] = {name: e.metadataStatement.description}\n }\n\n console.debug(aaguidMetadata)\n updatedAuthenticatorMetadata = aaguidMetadata\n}\n", "/*\nexport * from './types'\nexport * from './webauthn'\nexport * from './parsers'\nexport * from './validation'\n*/\nimport * as client from './client.js';\nimport * as server from './server.js';\nimport * as parsers from './parsers.js';\nimport * as utils from './utils.js';\n\nexport { client, server, parsers, utils }\nexport default { client, server, parsers, utils }\n"], - "mappings": "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", + "sourcesContent": ["import * as utils from './utils.js'\nimport { AuthenticateOptions, AuthenticationEncoded, AuthType, NamedAlgo, NumAlgo, RegisterOptions, RegistrationEncoded } from './types.js'\n\n/**\n * Returns whether passwordless authentication is available on this browser/platform or not.\n */\n export function isAvailable() :boolean {\n return !!window.PublicKeyCredential\n}\n\n/**\n * Returns whether the device itself can be used as authenticator.\n */\nexport async function isLocalAuthenticator() :Promise {\n return await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()\n}\n\n\n\n\nasync function getAuthAttachment(authType :AuthType) :Promise {\n if(authType === \"local\")\n return \"platform\";\n if(authType === \"roaming\" || authType === \"extern\")\n return \"cross-platform\";\n if(authType === \"both\")\n return undefined // The webauthn protocol considers `null` as invalid but `undefined` as \"both\"!\n\n // the default case: \"auto\", depending on device capabilities\n try {\n if(await isLocalAuthenticator())\n return \"platform\"\n else\n return \"cross-platform\"\n } catch(e) {\n // might happen due to some security policies\n // see https://w3c.github.io/webauthn/#sctn-isUserVerifyingPlatformAuthenticatorAvailable\n return undefined // The webauthn protocol considers `null` as invalid but `undefined` as \"both\"!\n }\n}\n\n\n\nfunction getAlgoName(num :NumAlgo) :NamedAlgo {\n switch(num) {\n case -7: return \"ES256\"\n // case -8 ignored to to its rarity\n case -257: return \"RS256\"\n default: throw new Error(`Unknown algorithm code: ${num}`)\n }\n}\n\n\n\n/**\n * Creates a cryptographic key pair, in order to register the public key for later passwordless authentication.\n *\n * @param {string} username\n * @param {string} challenge A server-side randomly generated string.\n * @param {Object} [options] Optional parameters.\n * @param {number} [options.timeout=60000] Number of milliseconds the user has to respond to the biometric/PIN check.\n * @param {'required'|'preferred'|'discouraged'} [options.userVerification='required'] Whether to prompt for biometric/PIN check or not.\n * @param {'auto'|'local'|'roaming'|'both'} [options.authenticatorType='auto'] Which device to use as authenticator.\n * 'auto': if the local device can be used as authenticator it will be preferred. Otherwise it will prompt for a roaming device.\n * 'local': use the local device (using TouchID, FaceID, Windows Hello or PIN)\n * 'roaming': use a roaming device (security key or connected phone)\n * 'both': prompt the user to choose between local or roaming device. The UI and user interaction in this case is platform specific.\n * @param {boolean} [options.attestation=false] If enabled, the device attestation and clientData will be provided as Base64url encoded binary data.\n * Note that this is not available on some platforms.\n * @param {'discouraged'|'preferred'|'required'} [options.discoverable] A \"discoverable\" credential can be selected using `authenticate(...)` without providing credential IDs.\n * Instead, a native pop-up will appear for user selection.\n * This may have an impact on the \"passkeys\" user experience and syncing behavior of the key.\n */\nexport async function register(username :string, challenge :string, options? :RegisterOptions) :Promise {\n options = options ?? {}\n\n if(!utils.isBase64url(challenge))\n throw new Error('Provided challenge is not properly encoded in Base64url')\n\n const creationOptions :PublicKeyCredentialCreationOptions = {\n challenge: utils.parseBase64url(challenge),\n rp: {\n id: window.location.hostname,\n name: window.location.hostname\n },\n user: {\n id: options.userHandle ? utils.toBuffer(options.userHandle) : await utils.sha256(new TextEncoder().encode('passwordless.id-user:' + username)), // ID should not be directly \"identifiable\" for privacy concerns\n name: username,\n displayName: username,\n },\n pubKeyCredParams: [\n {alg: -7, type: \"public-key\"}, // ES256 (Webauthn's default algorithm)\n {alg: -257, type: \"public-key\"}, // RS256 (for Windows Hello and others)\n ],\n timeout: options.timeout ?? 60000,\n authenticatorSelection: {\n userVerification: options.userVerification ?? \"required\", // Webauthn default is \"preferred\"\n authenticatorAttachment: await getAuthAttachment(options.authenticatorType ?? \"auto\"),\n residentKey: options.discoverable ?? 'preferred', // official default is 'discouraged'\n requireResidentKey: (options.discoverable === 'required') // mainly for backwards compatibility, see https://www.w3.org/TR/webauthn/#dictionary-authenticatorSelection\n },\n attestation: options.attestation ? \"direct\" : \"none\"\n }\n\n if(options.debug)\n console.debug(creationOptions)\n\n const credential = await navigator.credentials.create({publicKey: creationOptions}) as any //PublicKeyCredential\n \n if(options.debug)\n console.debug(credential)\n \n const response = credential.response as any // AuthenticatorAttestationResponse\n \n let registration :RegistrationEncoded = {\n username: username,\n credential: {\n id: credential.id,\n publicKey: utils.toBase64url(response.getPublicKey()),\n algorithm: getAlgoName(credential.response.getPublicKeyAlgorithm())\n },\n authenticatorData: utils.toBase64url(response.getAuthenticatorData()),\n clientData: utils.toBase64url(response.clientDataJSON),\n }\n\n if(options.attestation) {\n registration.attestationData = utils.toBase64url(response.attestationObject)\n }\n\n return registration\n}\n\n\nasync function getTransports(authType :AuthType) :Promise {\n const local :AuthenticatorTransport[] = ['internal']\n\n // 'hybrid' was added mid-2022 in the specs and currently not yet available in the official dom types\n // @ts-ignore\n const roaming :AuthenticatorTransport[] = ['hybrid', 'usb', 'ble', 'nfc']\n \n if(authType === \"local\")\n return local\n if(authType == \"roaming\" || authType === \"extern\")\n return roaming\n if(authType === \"both\")\n return [...local, ...roaming]\n\n // the default case: \"auto\", depending on device capabilities\n try {\n if(await isLocalAuthenticator())\n return local\n else\n return roaming\n } catch(e) {\n return [...local, ...roaming]\n }\n}\n\n\n/**\n * Signs a challenge using one of the provided credentials IDs in order to authenticate the user.\n *\n * @param {string[]} credentialIds The list of credential IDs that can be used for signing.\n * @param {string} challenge A server-side randomly generated string, the base64 encoded version will be signed.\n * @param {Object} [options] Optional parameters.\n * @param {number} [options.timeout=60000] Number of milliseconds the user has to respond to the biometric/PIN check.\n * @param {'required'|'preferred'|'discouraged'} [options.userVerification='required'] Whether to prompt for biometric/PIN check or not.\n * @param {'optional'|'conditional'|'required'|'silent'} [options.mediation='optional'] https://developer.mozilla.org/en-US/docs/Web/API/CredentialsContainer/get#mediation\n */\nexport async function authenticate(credentialIds :string[], challenge :string, options? :AuthenticateOptions) :Promise {\n options = options ?? {}\n\n if(!utils.isBase64url(challenge))\n throw new Error('Provided challenge is not properly encoded in Base64url')\n\n const transports = await getTransports(options.authenticatorType ?? \"auto\");\n\n let authOptions :PublicKeyCredentialRequestOptions = {\n challenge: utils.parseBase64url(challenge),\n rpId: window.location.hostname,\n allowCredentials: credentialIds.map(id => { return {\n id: utils.parseBase64url(id),\n type: 'public-key',\n transports: transports,\n }}),\n userVerification: options.userVerification ?? \"required\",\n timeout: options.timeout ?? 60000,\n }\n\n if(options.debug)\n console.debug(authOptions)\n\n let auth = await navigator.credentials.get({publicKey: authOptions, mediation: options.mediation}) as PublicKeyCredential\n \n if(options.debug)\n console.debug(auth)\n\n const response = auth.response as AuthenticatorAssertionResponse\n \n const authentication :AuthenticationEncoded = {\n credentialId: auth.id,\n //userHash: utils.toBase64url(response.userHandle), // unreliable, optional for authenticators\n authenticatorData: utils.toBase64url(response.authenticatorData),\n clientData: utils.toBase64url(response.clientDataJSON),\n signature: utils.toBase64url(response.signature),\n }\n\n return authentication\n}\n\n", "/********************************\n Encoding/Decoding Utils\n********************************/\n\n/*\nlet webCrypto :any = null\n\nexport async function getCrypto() {\n if(!webCrypto) {\n console.log(window?.crypto)\n webCrypto = window?.crypto ?? (await import(\"crypto\")).webcrypto\n console.log(webCrypto)\n }\n return webCrypto\n}\n*/\n\n\nexport function randomChallenge() {\n return crypto.randomUUID()\n}\n\n\nexport function toBuffer(txt :string) :ArrayBuffer {\n return Uint8Array.from(txt, c => c.charCodeAt(0)).buffer\n}\n\nexport function parseBuffer(buffer :ArrayBuffer) :string {\n return String.fromCharCode(...new Uint8Array(buffer))\n}\n\n\nexport function isBase64url(txt :string) :boolean {\n return txt.match(/^[a-zA-Z0-9\\-_]+=*$/) !== null\n}\n\nexport function toBase64url(buffer :ArrayBuffer) :string {\n const txt = btoa(parseBuffer(buffer)) // base64\n return txt.replaceAll('+', '-').replaceAll('/', '_')\n}\n\nexport function parseBase64url(txt :string) :ArrayBuffer {\n txt = txt.replaceAll('-', '+').replaceAll('_', '/') // base64url -> base64\n return toBuffer(atob(txt))\n}\n\n\nexport async function sha256(buffer :ArrayBuffer) :Promise {\n return await crypto.subtle.digest('SHA-256', buffer)\n}\n\nexport function bufferToHex (buffer :ArrayBuffer) :string {\n return [...new Uint8Array (buffer)]\n .map (b => b.toString (16).padStart (2, \"0\"))\n .join (\"\");\n}\n\n\nexport function concatenateBuffers(buffer1 :ArrayBuffer, buffer2 :ArrayBuffer) {\n var tmp = new Uint8Array(buffer1.byteLength + buffer2.byteLength);\n tmp.set(new Uint8Array(buffer1), 0);\n tmp.set(new Uint8Array(buffer2), buffer1.byteLength);\n return tmp;\n };", "import { parseAuthentication, parseRegistration } from \"./parsers.js\";\nimport { AuthenticationEncoded, AuthenticationParsed, CredentialKey, NamedAlgo, RegistrationEncoded, RegistrationParsed } from \"./types.js\";\nimport * as utils from './utils.js'\n\n\nasync function isValid(validator :any, value :any) :Promise {\n if(typeof validator === 'function') {\n const res = validator(value)\n if(res instanceof Promise)\n return await res\n else\n return res\n }\n // the validator can be a single value too\n return validator === value\n}\n\nasync function isNotValid(validator :any, value :any) :Promise {\n return !(await isValid(validator, value))\n}\n\ninterface RegistrationChecks {\n challenge: string | Function,\n origin: string | Function\n}\n\n\nexport async function verifyRegistration(registrationRaw: RegistrationEncoded, expected: RegistrationChecks): Promise {\n const registration = parseRegistration(registrationRaw)\n registration.client.challenge\n\n if (registration.client.type !== \"webauthn.create\")\n throw new Error(`Unexpected ClientData type: ${registration.client.type}`)\n\n if (await isNotValid(expected.origin, registration.client.origin))\n throw new Error(`Unexpected ClientData origin: ${registration.client.origin}`)\n\n if (await isNotValid(expected.challenge, registration.client.challenge))\n throw new Error(`Unexpected ClientData challenge: ${registration.client.challenge}`)\n\n return registration\n}\n\n\ninterface AuthenticationChecks {\n challenge: string | Function,\n origin: string | Function,\n userVerified: boolean,\n counter?: number // Made optional according to https://github.com/passwordless-id/webauthn/issues/38\n}\n\n\nexport async function verifyAuthentication(authenticationRaw: AuthenticationEncoded, credential: CredentialKey, expected: AuthenticationChecks): Promise {\n if (authenticationRaw.credentialId !== credential.id)\n throw new Error(`Credential ID mismatch: ${authenticationRaw.credentialId} vs ${credential.id}`)\n\n const isValidSignature: boolean = await verifySignature({\n algorithm: credential.algorithm,\n publicKey: credential.publicKey,\n authenticatorData: authenticationRaw.authenticatorData,\n clientData: authenticationRaw.clientData,\n signature: authenticationRaw.signature\n })\n\n if(!isValidSignature)\n throw new Error(`Invalid signature: ${authenticationRaw.signature}`)\n\n const authentication = parseAuthentication(authenticationRaw)\n\n if (authentication.client.type !== \"webauthn.get\")\n throw new Error(`Unexpected clientData type: ${authentication.client.type}`)\n\n if (await isNotValid(expected.origin, authentication.client.origin))\n throw new Error(`Unexpected ClientData origin: ${authentication.client.origin}`)\n\n if (await isNotValid(expected.challenge, authentication.client.challenge))\n throw new Error(`Unexpected ClientData challenge: ${authentication.client.challenge}`)\n\n // this only works because we consider `rp.origin` and `rp.id` to be the same during authentication/registration\n const rpId = new URL(authentication.client.origin).hostname\n const expectedRpIdHash = utils.toBase64url(await utils.sha256(utils.toBuffer(rpId)))\n if (authentication.authenticator.rpIdHash !== expectedRpIdHash)\n throw new Error(`Unexpected RpIdHash: ${authentication.authenticator.rpIdHash} vs ${expectedRpIdHash}`)\n\n if (!authentication.authenticator.flags.userPresent)\n throw new Error(`Unexpected authenticator flags: missing userPresent`)\n\n if (!authentication.authenticator.flags.userVerified && expected.userVerified)\n throw new Error(`Unexpected authenticator flags: missing userVerified`)\n\n if (expected.counter && authentication.authenticator.counter <= expected.counter)\n throw new Error(`Unexpected authenticator counter: ${authentication.authenticator.counter} (should be > ${expected.counter})`)\n\n return authentication\n}\n\n\n// https://w3c.github.io/webauthn/#sctn-public-key-easy\n// https://www.iana.org/assignments/cose/cose.xhtml#algorithms\n/*\nUser agents MUST be able to return a non-null value for getPublicKey() when the credential public key has a COSEAlgorithmIdentifier value of:\n\n-7 (ES256), where kty is 2 (with uncompressed points) and crv is 1 (P-256).\n\n-257 (RS256).\n\n-8 (EdDSA), where crv is 6 (Ed25519).\n*/\nfunction getAlgoParams(algorithm: NamedAlgo): any {\n switch (algorithm) {\n case 'RS256':\n return {\n name: 'RSASSA-PKCS1-v1_5',\n hash: 'SHA-256'\n };\n case 'ES256':\n return {\n name: 'ECDSA',\n namedCurve: 'P-256',\n hash: 'SHA-256',\n };\n // case 'EdDSA': Not supported by browsers\n default:\n throw new Error(`Unknown or unsupported crypto algorithm: ${algorithm}. Only 'RS256' and 'ES256' are supported.`)\n }\n}\n\ntype AlgoParams = AlgorithmIdentifier | RsaHashedImportParams | EcKeyImportParams | HmacImportParams | AesKeyAlgorithm\n\nasync function parseCryptoKey(algoParams: AlgoParams, publicKey: string): Promise {\n const buffer = utils.parseBase64url(publicKey)\n return crypto.subtle.importKey('spki', buffer, algoParams, false, ['verify'])\n}\n\n\n\ntype VerifyParams = {\n algorithm: NamedAlgo,\n publicKey: string, // Base64url encoded\n authenticatorData: string, // Base64url encoded\n clientData: string, // Base64url encoded\n signature: string, // Base64url encoded\n}\n\n\n// https://w3c.github.io/webauthn/#sctn-verifying-assertion\n// https://w3c.github.io/webauthn/#sctn-signature-attestation-types\n/* Emphasis mine:\n\n6.5.6. Signature Formats for Packed Attestation, FIDO U2F Attestation, and **Assertion Signatures**\n\n[...] For COSEAlgorithmIdentifier -7 (ES256) [...] the sig value MUST be encoded as an ASN.1 [...]\n[...] For COSEAlgorithmIdentifier -257 (RS256) [...] The signature is not ASN.1 wrapped.\n[...] For COSEAlgorithmIdentifier -37 (PS256) [...] The signature is not ASN.1 wrapped.\n*/\n// see also https://gist.github.com/philholden/50120652bfe0498958fd5926694ba354\nexport async function verifySignature({ algorithm, publicKey, authenticatorData, clientData, signature }: VerifyParams): Promise {\n const algoParams = getAlgoParams(algorithm)\n let cryptoKey = await parseCryptoKey(algoParams, publicKey)\n console.debug(cryptoKey)\n\n let clientHash = await utils.sha256(utils.parseBase64url(clientData));\n\n // during \"login\", the authenticatorData is exactly 37 bytes\n let comboBuffer = utils.concatenateBuffers(utils.parseBase64url(authenticatorData), clientHash)\n\n console.debug('Crypto Algo: ' + JSON.stringify(algoParams))\n console.debug('Public key: ' + publicKey)\n console.debug('Data: ' + utils.toBase64url(comboBuffer))\n console.debug('Signature: ' + signature)\n\n // https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/verify\n let signatureBuffer = utils.parseBase64url(signature)\n if(algorithm == 'ES256')\n signatureBuffer = convertASN1toRaw(signatureBuffer)\n\n const isValid = await crypto.subtle.verify(algoParams, cryptoKey, signatureBuffer, comboBuffer)\n\n return isValid\n}\n\nfunction convertASN1toRaw(signatureBuffer :ArrayBuffer) {\n // Convert signature from ASN.1 sequence to \"raw\" format\n const usignature = new Uint8Array(signatureBuffer);\n const rStart = usignature[4] === 0 ? 5 : 4;\n const rEnd = rStart + 32;\n const sStart = usignature[rEnd + 2] === 0 ? rEnd + 3 : rEnd + 2;\n const r = usignature.slice(rStart, rEnd);\n const s = usignature.slice(sStart);\n return new Uint8Array([...r, ...s]);\n}", "import * as authenticators from './authenticators.js'\nimport * as utils from './utils.js'\nimport { AuthenticatorInfo, ClientInfo, RegistrationEncoded, RegistrationParsed, AuthenticationEncoded, AuthenticationParsed } from './types'\n\nconst utf8Decoder = new TextDecoder('utf-8')\n\nexport function parseClient(data :string|ArrayBuffer) :ClientInfo {\n if(typeof data == 'string')\n data = utils.parseBase64url(data)\n return JSON.parse(utf8Decoder.decode(data))\n}\n\n\nexport function parseAuthenticator(data :string|ArrayBuffer) :AuthenticatorInfo {\n if(typeof data == 'string')\n data = utils.parseBase64url(data)\n return authenticators.parseAuthBuffer(data)\n}\n\n\nexport function parseAttestation(data :string|ArrayBuffer) :unknown {\n if(typeof data == 'string')\n data = utils.parseBase64url(data)\n return 'Really complex to parse. Good luck with that one!'\n}\n\n\n\nexport function parseRegistration(registration :RegistrationEncoded) :RegistrationParsed {\n return {\n username: registration.username,\n credential: registration.credential,\n\n client: parseClient(registration.clientData),\n authenticator: parseAuthenticator(registration.authenticatorData),\n attestation: registration.attestationData ? parseAttestation(registration.attestationData) : null\n }\n}\n\nexport function parseAuthentication(authentication :AuthenticationEncoded) :AuthenticationParsed {\n return {\n credentialId: authentication.credentialId,\n client: parseClient(authentication.clientData),\n authenticator: parseAuthenticator(authentication.authenticatorData),\n signature: authentication.signature\n }\n}", "export default {\n \"9c835346-796b-4c27-8898-d6032f515cc5\": {\n \"name\": \"Cryptnox FIDO2\"\n },\n \"c5ef55ff-ad9a-4b9f-b580-adebafe026d0\": {\n \"name\": \"YubiKey 5Ci\"\n },\n \"39a5647e-1853-446c-a1f6-a79bae9f5bc7\": {\n \"name\": \"Vancosys Android Authenticator\"\n },\n \"3789da91-f943-46bc-95c3-50ea2012f03a\": {\n \"name\": \"NEOWAVE Winkeo FIDO2\"\n },\n \"fa2b99dc-9e39-4257-8f92-4a30d23c4118\": {\n \"name\": \"YubiKey 5 Series with NFC\"\n },\n \"4e768f2c-5fab-48b3-b300-220eb487752b\": {\n \"name\": \"Hideez Key 4 FIDO2 SDK\"\n },\n \"931327dd-c89b-406c-a81e-ed7058ef36c6\": {\n \"name\": \"Swissbit iShield FIDO2\"\n },\n \"e1a96183-5016-4f24-b55b-e3ae23614cc6\": {\n \"name\": \"ATKey.Pro CTAP2.0\"\n },\n \"08987058-cadc-4b81-b6e1-30de50dcbe96\": {\n \"name\": \"Windows Hello Hardware Authenticator\"\n },\n \"d91c5288-0ef0-49b7-b8ae-21ca0aa6b3f3\": {\n \"name\": \"KEY-ID FIDO2 Authenticator\"\n },\n \"ee041bce-25e5-4cdb-8f86-897fd6418464\": {\n \"name\": \"Feitian ePass FIDO2-NFC Authenticator\"\n },\n \"73bb0cd4-e502-49b8-9c6f-b59445bf720b\": {\n \"name\": \"YubiKey 5 FIPS Series\"\n },\n \"149a2021-8ef6-4133-96b8-81f8d5b7f1f5\": {\n \"name\": \"Security Key by Yubico with NFC\"\n },\n \"3b1adb99-0dfe-46fd-90b8-7f7614a4de2a\": {\n \"name\": \"GoTrust Idem Key FIDO2 Authenticator\"\n },\n \"f8a011f3-8c0a-4d15-8006-17111f9edc7d\": {\n \"name\": \"Security Key by Yubico\"\n },\n \"2c0df832-92de-4be1-8412-88a8f074df4a\": {\n \"name\": \"Feitian FIDO Smart Card\"\n },\n \"c5703116-972b-4851-a3e7-ae1259843399\": {\n \"name\": \"NEOWAVE Badgeo FIDO2\"\n },\n \"820d89ed-d65a-409e-85cb-f73f0578f82a\": {\n \"name\": \"Vancosys iOS Authenticator\"\n },\n \"b6ede29c-3772-412c-8a78-539c1f4c62d2\": {\n \"name\": \"Feitian BioPass FIDO2 Plus Authenticator\"\n },\n \"85203421-48f9-4355-9bc8-8a53846e5083\": {\n \"name\": \"YubiKey 5Ci FIPS\"\n },\n \"d821a7d4-e97c-4cb6-bd82-4237731fd4be\": {\n \"name\": \"Hyper FIDO Bio Security Key\"\n },\n \"516d3969-5a57-5651-5958-4e7a49434167\": {\n \"name\": \"SmartDisplayer BobeePass (NFC-BLE FIDO2 Authenticator)\"\n },\n \"b93fd961-f2e6-462f-b122-82002247de78\": {\n \"name\": \"Android Authenticator with SafetyNet Attestation\"\n },\n \"2fc0579f-8113-47ea-b116-bb5a8db9202a\": {\n \"name\": \"YubiKey 5 Series with NFC\"\n },\n \"9ddd1817-af5a-4672-a2b9-3e3dd95000a9\": {\n \"name\": \"Windows Hello VBS Hardware Authenticator\"\n },\n \"d8522d9f-575b-4866-88a9-ba99fa02f35b\": {\n \"name\": \"YubiKey Bio Series\"\n },\n \"692db549-7ae5-44d5-a1e5-dd20a493b723\": {\n \"name\": \"HID Crescendo Key\"\n },\n \"3e22415d-7fdf-4ea4-8a0c-dd60c4249b9d\": {\n \"name\": \"Feitian iePass FIDO Authenticator\"\n },\n \"aeb6569c-f8fb-4950-ac60-24ca2bbe2e52\": {\n \"name\": \"HID Crescendo C2300\"\n },\n \"9f0d8150-baa5-4c00-9299-ad62c8bb4e87\": {\n \"name\": \"GoTrust Idem Card FIDO2 Authenticator\"\n },\n \"12ded745-4bed-47d4-abaa-e713f51d6393\": {\n \"name\": \"Feitian AllinOne FIDO2 Authenticator\"\n },\n \"88bbd2f0-342a-42e7-9729-dd158be5407a\": {\n \"name\": \"Precision InnaIT Key FIDO 2 Level 2 certified\"\n },\n \"34f5766d-1536-4a24-9033-0e294e510fb0\": {\n \"name\": \"YubiKey 5 Series CTAP2.1 Preview 1 \"\n },\n \"83c47309-aabb-4108-8470-8be838b573cb\": {\n \"name\": \"YubiKey Bio Series (Enterprise Profile)\"\n },\n \"be727034-574a-f799-5c76-0929e0430973\": {\n \"name\": \"Crayonic KeyVault K1 (USB-NFC-BLE FIDO2 Authenticator)\"\n },\n \"b92c3f9a-c014-4056-887f-140a2501163b\": {\n \"name\": \"Security Key by Yubico\"\n },\n \"54d9fee8-e621-4291-8b18-7157b99c5bec\": {\n \"name\": \"HID Crescendo Enabled\"\n },\n \"6028b017-b1d4-4c02-b4b3-afcdafc96bb2\": {\n \"name\": \"Windows Hello Software Authenticator\"\n },\n \"6d44ba9b-f6ec-2e49-b930-0c8fe920cb73\": {\n \"name\": \"Security Key by Yubico with NFC\"\n },\n \"e416201b-afeb-41ca-a03d-2281c28322aa\": {\n \"name\": \"ATKey.Pro CTAP2.1\"\n },\n \"9f77e279-a6e2-4d58-b700-31e5943c6a98\": {\n \"name\": \"Hyper FIDO Pro\"\n },\n \"73402251-f2a8-4f03-873e-3cb6db604b03\": {\n \"name\": \"uTrust FIDO2 Security Key\"\n },\n \"c1f9a0bc-1dd2-404a-b27f-8e29047a43fd\": {\n \"name\": \"YubiKey 5 FIPS Series with NFC\"\n },\n \"504d7149-4e4c-3841-4555-55445a677357\": {\n \"name\": \"WiSECURE AuthTron USB FIDO2 Authenticator\"\n },\n \"cb69481e-8ff7-4039-93ec-0a2729a154a8\": {\n \"name\": \"YubiKey 5 Series\"\n },\n \"ee882879-721c-4913-9775-3dfcce97072a\": {\n \"name\": \"YubiKey 5 Series\"\n },\n \"8c97a730-3f7b-41a6-87d6-1e9b62bda6f0\": {\n \"name\": \"FT-JCOS FIDO Fingerprint Card\"\n },\n \"a1f52be5-dfab-4364-b51c-2bd496b14a56\": {\n \"name\": \"OCTATCO EzFinger2 FIDO2 AUTHENTICATOR\"\n },\n \"3e078ffd-4c54-4586-8baa-a77da113aec5\": {\n \"name\": \"Hideez Key 3 FIDO2\"\n },\n \"d41f5a69-b817-4144-a13c-9ebd6d9254d6\": {\n \"name\": \"ATKey.Card CTAP2.0\"\n },\n \"bc2fe499-0d8e-4ffe-96f3-94a82840cf8c\": {\n \"name\": \"OCTATCO EzQuant FIDO2 AUTHENTICATOR\"\n },\n \"1c086528-58d5-f211-823c-356786e36140\": {\n \"name\": \"Atos CardOS FIDO2\"\n },\n \"77010bd7-212a-4fc9-b236-d2ca5e9d4084\": {\n \"name\": \"Feitian BioPass FIDO2 Authenticator\"\n },\n \"833b721a-ff5f-4d00-bb2e-bdda3ec01e29\": {\n \"name\": \"Feitian ePass FIDO2 Authenticator\"\n }\n}", "import authenticatorMetadata from './authenticatorMetadata.js' //assert {type: 'json'}\nimport * as utils from './utils.js'\n\n\nexport function parseAuthBuffer(authData :ArrayBuffer) {\n console.debug(authData)\n let flags = new DataView(authData.slice(32,33)).getUint8(0)\n console.debug(flags)\n\n // https://w3c.github.io/webauthn/#sctn-authenticator-data\n let parsed :any = {\n rpIdHash: utils.toBase64url(authData.slice(0,32)),\n flags: {\n userPresent: !!(flags & 1),\n //reserved1: !!(flags & 2),\n userVerified: !!(flags & 4),\n backupEligibility: !!(flags & 8),\n backupState: !!(flags & 16),\n //reserved2: !!(flags & 32),\n attestedData: !!(flags & 64),\n extensionsIncluded: !!(flags & 128)\n },\n counter: new DataView(authData.slice(33,37)).getUint32(0, false), // Big-Endian!\n }\n\n if(authData.byteLength > 37) {\n // registration contains additional data\n\n const aaguid = extractAaguid(authData)\n // https://w3c.github.io/webauthn/#attested-credential-data\n //let credentialLength = new DataView(authData.slice(53,55)).getUint16(0, false) // Big-Endian!\n \n parsed = {\n ...parsed,\n aaguid, // bytes 37->53\n name: resolveAuthenticatorName(aaguid)\n // credentialBytes, // bytes 53->55: credential length\n // credentialId: utils.toBase64url(authData.slice(55, 55+credentialLength)),\n //publicKey: until where? ...and it's encoded using a strange format, let's better avoid it\n //extensions: starting where?\n }\n }\n\n return parsed\n}\n\nexport function extractAaguid(authData :ArrayBuffer) :string {\n return formatAaguid(authData.slice(37, 53)) // 16 bytes\n}\n\nfunction formatAaguid(buffer :ArrayBuffer) :string {\n let aaguid = utils.bufferToHex(buffer)\n aaguid = aaguid.substring(0,8) + '-' + aaguid.substring(8,12) + '-' + aaguid.substring(12,16) + '-' + aaguid.substring(16,20) + '-' + aaguid.substring(20,32)\n return aaguid // example: \"d41f5a69-b817-4144-a13c-9ebd6d9254d6\"\n}\n\nexport function resolveAuthenticatorName(aaguid :string) :string {\n const aaguidMetadata = updatedAuthenticatorMetadata ?? authenticatorMetadata //await getAaguidMetadata()\n return aaguidMetadata[aaguid]?.name\n}\n\nlet updatedAuthenticatorMetadata :any = null\n\n// List of AAGUIDs are encoded as JWT here: https://mds.fidoalliance.org/\nexport async function updateDevicesMetadata() {\n // this function is rather resource intensive and time consuming\n // therefore, the result is cached in local storage\n const jwt = await (await fetch(\"https://mds.fidoalliance.org\")).text()\n\n // the response is a JWT including all AAGUIDs and their metadata\n console.debug(jwt)\n\n // let us ignore the JWT verification, since this is solely for descriptive purposes, not signed data\n const payload = jwt.split('.')[1].replaceAll('-', '+').replaceAll('_', '/')\n const json = JSON.parse(atob(payload))\n console.debug(json)\n\n let aaguidMetadata :any = {}\n for(const e of json.entries) {\n if(!e.aaguid || !e.metadataStatement)\n continue\n\n aaguidMetadata[e.aaguid] = {name: e.metadataStatement.description}\n }\n\n console.debug(aaguidMetadata)\n updatedAuthenticatorMetadata = aaguidMetadata\n}\n", "/*\nexport * from './types'\nexport * from './webauthn'\nexport * from './parsers'\nexport * from './validation'\n*/\nimport * as client from './client.js';\nimport * as server from './server.js';\nimport * as parsers from './parsers.js';\nimport * as utils from './utils.js';\n\nexport { client, server, parsers, utils }\nexport default { client, server, parsers, utils }\n"], + "mappings": "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", "names": ["client_exports", "__export", "authenticate", "isAvailable", "isLocalAuthenticator", "register", "utils_exports", "__export", "bufferToHex", "concatenateBuffers", "isBase64url", "parseBase64url", "parseBuffer", "randomChallenge", "sha256", "toBase64url", "toBuffer", "txt", "c", "buffer", "b", "buffer1", "buffer2", "tmp", "isAvailable", "isLocalAuthenticator", "getAuthAttachment", "authType", "getAlgoName", "num", "register", "username", "challenge", "options", "isBase64url", "creationOptions", "parseBase64url", "toBuffer", "sha256", "credential", "response", "registration", "toBase64url", "getTransports", "local", "roaming", "authenticate", "credentialIds", "transports", "authOptions", "id", "auth", "server_exports", "__export", "verifyAuthentication", "verifyRegistration", "verifySignature", "parsers_exports", "__export", "parseAttestation", "parseAuthentication", "parseAuthenticator", "parseClient", "parseRegistration", "authenticatorMetadata_default", "parseAuthBuffer", "authData", "flags", "parsed", "toBase64url", "aaguid", "extractAaguid", "resolveAuthenticatorName", "formatAaguid", "buffer", "bufferToHex", "updatedAuthenticatorMetadata", "authenticatorMetadata_default", "utf8Decoder", "parseClient", "data", "parseBase64url", "parseAuthenticator", "parseAuthBuffer", "parseAttestation", "parseRegistration", "registration", "parseAuthentication", "authentication", "isValid", "validator", "value", "res", "isNotValid", "verifyRegistration", "registrationRaw", "expected", "registration", "parseRegistration", "verifyAuthentication", "authenticationRaw", "credential", "verifySignature", "authentication", "parseAuthentication", "rpId", "expectedRpIdHash", "toBase64url", "sha256", "toBuffer", "getAlgoParams", "algorithm", "parseCryptoKey", "algoParams", "publicKey", "buffer", "parseBase64url", "authenticatorData", "clientData", "signature", "cryptoKey", "clientHash", "comboBuffer", "concatenateBuffers", "signatureBuffer", "convertASN1toRaw", "usignature", "rStart", "rEnd", "sStart", "r", "s", "src_default", "client_exports", "server_exports", "parsers_exports", "utils_exports"] } diff --git a/package-lock.json b/package-lock.json index f6b93f1..77ebec8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@passwordless-id/webauthn", - "version": "1.2.6", + "version": "1.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@passwordless-id/webauthn", - "version": "1.2.6", + "version": "1.3.0", "license": "MIT", "devDependencies": { "@babel/preset-typescript": "^7.21.4", diff --git a/src/server.ts b/src/server.ts index 6074f83..b28b359 100644 --- a/src/server.ts +++ b/src/server.ts @@ -46,7 +46,7 @@ interface AuthenticationChecks { challenge: string | Function, origin: string | Function, userVerified: boolean, - counter: number + counter?: number // Made optional according to https://github.com/passwordless-id/webauthn/issues/38 } @@ -88,7 +88,7 @@ export async function verifyAuthentication(authenticationRaw: AuthenticationEnco if (!authentication.authenticator.flags.userVerified && expected.userVerified) throw new Error(`Unexpected authenticator flags: missing userVerified`) - if (authentication.authenticator.counter <= expected.counter) + if (expected.counter && authentication.authenticator.counter <= expected.counter) throw new Error(`Unexpected authenticator counter: ${authentication.authenticator.counter} (should be > ${expected.counter})`) return authentication