passkey.js 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. /*
  2. Copyright (C) 2025 QuantumNous
  3. This program is free software: you can redistribute it and/or modify
  4. it under the terms of the GNU Affero General Public License as
  5. published by the Free Software Foundation, either version 3 of the
  6. License, or (at your option) any later version.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. GNU Affero General Public License for more details.
  11. You should have received a copy of the GNU Affero General Public License
  12. along with this program. If not, see <https://www.gnu.org/licenses/>.
  13. For commercial licensing, please contact support@quantumnous.com
  14. */
  15. export function base64UrlToBuffer(base64url) {
  16. if (!base64url) return new ArrayBuffer(0);
  17. let padding = '='.repeat((4 - (base64url.length % 4)) % 4);
  18. const base64 = (base64url + padding).replace(/-/g, '+').replace(/_/g, '/');
  19. const rawData = window.atob(base64);
  20. const buffer = new ArrayBuffer(rawData.length);
  21. const uintArray = new Uint8Array(buffer);
  22. for (let i = 0; i < rawData.length; i += 1) {
  23. uintArray[i] = rawData.charCodeAt(i);
  24. }
  25. return buffer;
  26. }
  27. export function bufferToBase64Url(buffer) {
  28. if (!buffer) return '';
  29. const uintArray = new Uint8Array(buffer);
  30. let binary = '';
  31. for (let i = 0; i < uintArray.byteLength; i += 1) {
  32. binary += String.fromCharCode(uintArray[i]);
  33. }
  34. return window
  35. .btoa(binary)
  36. .replace(/\+/g, '-')
  37. .replace(/\//g, '_')
  38. .replace(/=+$/g, '');
  39. }
  40. export function prepareCredentialCreationOptions(payload) {
  41. const options =
  42. payload?.publicKey ||
  43. payload?.PublicKey ||
  44. payload?.response ||
  45. payload?.Response;
  46. if (!options) {
  47. throw new Error('无法从服务端响应中解析 Passkey 注册参数');
  48. }
  49. const publicKey = {
  50. ...options,
  51. challenge: base64UrlToBuffer(options.challenge),
  52. user: {
  53. ...options.user,
  54. id: base64UrlToBuffer(options.user?.id),
  55. },
  56. };
  57. if (Array.isArray(options.excludeCredentials)) {
  58. publicKey.excludeCredentials = options.excludeCredentials.map((item) => ({
  59. ...item,
  60. id: base64UrlToBuffer(item.id),
  61. }));
  62. }
  63. if (
  64. Array.isArray(options.attestationFormats) &&
  65. options.attestationFormats.length === 0
  66. ) {
  67. delete publicKey.attestationFormats;
  68. }
  69. return publicKey;
  70. }
  71. export function prepareCredentialRequestOptions(payload) {
  72. const options =
  73. payload?.publicKey ||
  74. payload?.PublicKey ||
  75. payload?.response ||
  76. payload?.Response;
  77. if (!options) {
  78. throw new Error('无法从服务端响应中解析 Passkey 登录参数');
  79. }
  80. const publicKey = {
  81. ...options,
  82. challenge: base64UrlToBuffer(options.challenge),
  83. };
  84. if (Array.isArray(options.allowCredentials)) {
  85. publicKey.allowCredentials = options.allowCredentials.map((item) => ({
  86. ...item,
  87. id: base64UrlToBuffer(item.id),
  88. }));
  89. }
  90. return publicKey;
  91. }
  92. export function buildRegistrationResult(credential) {
  93. if (!credential) return null;
  94. const { response } = credential;
  95. const transports =
  96. typeof response.getTransports === 'function'
  97. ? response.getTransports()
  98. : undefined;
  99. return {
  100. id: credential.id,
  101. rawId: bufferToBase64Url(credential.rawId),
  102. type: credential.type,
  103. authenticatorAttachment: credential.authenticatorAttachment,
  104. response: {
  105. attestationObject: bufferToBase64Url(response.attestationObject),
  106. clientDataJSON: bufferToBase64Url(response.clientDataJSON),
  107. transports,
  108. },
  109. clientExtensionResults: credential.getClientExtensionResults?.() ?? {},
  110. };
  111. }
  112. export function buildAssertionResult(assertion) {
  113. if (!assertion) return null;
  114. const { response } = assertion;
  115. return {
  116. id: assertion.id,
  117. rawId: bufferToBase64Url(assertion.rawId),
  118. type: assertion.type,
  119. authenticatorAttachment: assertion.authenticatorAttachment,
  120. response: {
  121. authenticatorData: bufferToBase64Url(response.authenticatorData),
  122. clientDataJSON: bufferToBase64Url(response.clientDataJSON),
  123. signature: bufferToBase64Url(response.signature),
  124. userHandle: response.userHandle
  125. ? bufferToBase64Url(response.userHandle)
  126. : null,
  127. },
  128. clientExtensionResults: assertion.getClientExtensionResults?.() ?? {},
  129. };
  130. }
  131. export async function isPasskeySupported() {
  132. if (typeof window === 'undefined' || !window.PublicKeyCredential) {
  133. return false;
  134. }
  135. if (
  136. typeof window.PublicKeyCredential.isConditionalMediationAvailable ===
  137. 'function'
  138. ) {
  139. try {
  140. const available =
  141. await window.PublicKeyCredential.isConditionalMediationAvailable();
  142. if (available) return true;
  143. } catch (error) {
  144. // ignore
  145. }
  146. }
  147. if (
  148. typeof window.PublicKeyCredential
  149. .isUserVerifyingPlatformAuthenticatorAvailable === 'function'
  150. ) {
  151. try {
  152. return await window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
  153. } catch (error) {
  154. return false;
  155. }
  156. }
  157. return true;
  158. }