|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function base64UrlToBuffer(base64url) { |
|
|
if (!base64url) return new ArrayBuffer(0); |
|
|
let padding = '='.repeat((4 - (base64url.length % 4)) % 4); |
|
|
const base64 = (base64url + padding).replace(/-/g, '+').replace(/_/g, '/'); |
|
|
const rawData = window.atob(base64); |
|
|
const buffer = new ArrayBuffer(rawData.length); |
|
|
const uintArray = new Uint8Array(buffer); |
|
|
for (let i = 0; i < rawData.length; i += 1) { |
|
|
uintArray[i] = rawData.charCodeAt(i); |
|
|
} |
|
|
return buffer; |
|
|
} |
|
|
|
|
|
export function bufferToBase64Url(buffer) { |
|
|
if (!buffer) return ''; |
|
|
const uintArray = new Uint8Array(buffer); |
|
|
let binary = ''; |
|
|
for (let i = 0; i < uintArray.byteLength; i += 1) { |
|
|
binary += String.fromCharCode(uintArray[i]); |
|
|
} |
|
|
return window |
|
|
.btoa(binary) |
|
|
.replace(/\+/g, '-') |
|
|
.replace(/\//g, '_') |
|
|
.replace(/=+$/g, ''); |
|
|
} |
|
|
|
|
|
export function prepareCredentialCreationOptions(payload) { |
|
|
const options = |
|
|
payload?.publicKey || |
|
|
payload?.PublicKey || |
|
|
payload?.response || |
|
|
payload?.Response; |
|
|
if (!options) { |
|
|
throw new Error('无法从服务端响应中解析 Passkey 注册参数'); |
|
|
} |
|
|
const publicKey = { |
|
|
...options, |
|
|
challenge: base64UrlToBuffer(options.challenge), |
|
|
user: { |
|
|
...options.user, |
|
|
id: base64UrlToBuffer(options.user?.id), |
|
|
}, |
|
|
}; |
|
|
|
|
|
if (Array.isArray(options.excludeCredentials)) { |
|
|
publicKey.excludeCredentials = options.excludeCredentials.map((item) => ({ |
|
|
...item, |
|
|
id: base64UrlToBuffer(item.id), |
|
|
})); |
|
|
} |
|
|
|
|
|
if ( |
|
|
Array.isArray(options.attestationFormats) && |
|
|
options.attestationFormats.length === 0 |
|
|
) { |
|
|
delete publicKey.attestationFormats; |
|
|
} |
|
|
|
|
|
return publicKey; |
|
|
} |
|
|
|
|
|
export function prepareCredentialRequestOptions(payload) { |
|
|
const options = |
|
|
payload?.publicKey || |
|
|
payload?.PublicKey || |
|
|
payload?.response || |
|
|
payload?.Response; |
|
|
if (!options) { |
|
|
throw new Error('无法从服务端响应中解析 Passkey 登录参数'); |
|
|
} |
|
|
const publicKey = { |
|
|
...options, |
|
|
challenge: base64UrlToBuffer(options.challenge), |
|
|
}; |
|
|
|
|
|
if (Array.isArray(options.allowCredentials)) { |
|
|
publicKey.allowCredentials = options.allowCredentials.map((item) => ({ |
|
|
...item, |
|
|
id: base64UrlToBuffer(item.id), |
|
|
})); |
|
|
} |
|
|
|
|
|
return publicKey; |
|
|
} |
|
|
|
|
|
export function buildRegistrationResult(credential) { |
|
|
if (!credential) return null; |
|
|
|
|
|
const { response } = credential; |
|
|
const transports = |
|
|
typeof response.getTransports === 'function' |
|
|
? response.getTransports() |
|
|
: undefined; |
|
|
|
|
|
return { |
|
|
id: credential.id, |
|
|
rawId: bufferToBase64Url(credential.rawId), |
|
|
type: credential.type, |
|
|
authenticatorAttachment: credential.authenticatorAttachment, |
|
|
response: { |
|
|
attestationObject: bufferToBase64Url(response.attestationObject), |
|
|
clientDataJSON: bufferToBase64Url(response.clientDataJSON), |
|
|
transports, |
|
|
}, |
|
|
clientExtensionResults: credential.getClientExtensionResults?.() ?? {}, |
|
|
}; |
|
|
} |
|
|
|
|
|
export function buildAssertionResult(assertion) { |
|
|
if (!assertion) return null; |
|
|
|
|
|
const { response } = assertion; |
|
|
|
|
|
return { |
|
|
id: assertion.id, |
|
|
rawId: bufferToBase64Url(assertion.rawId), |
|
|
type: assertion.type, |
|
|
authenticatorAttachment: assertion.authenticatorAttachment, |
|
|
response: { |
|
|
authenticatorData: bufferToBase64Url(response.authenticatorData), |
|
|
clientDataJSON: bufferToBase64Url(response.clientDataJSON), |
|
|
signature: bufferToBase64Url(response.signature), |
|
|
userHandle: response.userHandle |
|
|
? bufferToBase64Url(response.userHandle) |
|
|
: null, |
|
|
}, |
|
|
clientExtensionResults: assertion.getClientExtensionResults?.() ?? {}, |
|
|
}; |
|
|
} |
|
|
|
|
|
export async function isPasskeySupported() { |
|
|
if (typeof window === 'undefined' || !window.PublicKeyCredential) { |
|
|
return false; |
|
|
} |
|
|
if ( |
|
|
typeof window.PublicKeyCredential.isConditionalMediationAvailable === |
|
|
'function' |
|
|
) { |
|
|
try { |
|
|
const available = |
|
|
await window.PublicKeyCredential.isConditionalMediationAvailable(); |
|
|
if (available) return true; |
|
|
} catch (error) { |
|
|
|
|
|
} |
|
|
} |
|
|
if ( |
|
|
typeof window.PublicKeyCredential |
|
|
.isUserVerifyingPlatformAuthenticatorAvailable === 'function' |
|
|
) { |
|
|
try { |
|
|
return await window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(); |
|
|
} catch (error) { |
|
|
return false; |
|
|
} |
|
|
} |
|
|
return true; |
|
|
} |
|
|
|