File size: 3,391 Bytes
45a105b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
// WebAuthn helpers. Challenges are ALWAYS generated by the backend; the browser only
// converts the base64url fields and runs navigator.credentials. Credential responses /
// authenticator data are never logged.

export function b64urlToBuf(s: string): ArrayBuffer {
  const pad = '='.repeat((4 - (s.length % 4)) % 4)
  const b64 = (s + pad).replace(/-/g, '+').replace(/_/g, '/')
  const bin = atob(b64)
  const bytes = new Uint8Array(bin.length)
  for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i)
  return bytes.buffer
}

export function bufToB64url(buf: ArrayBuffer): string {
  const bytes = new Uint8Array(buf)
  let s = ''
  for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i])
  return btoa(s).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '')
}

export function isWebAuthnSupported(): boolean {
  return typeof window !== 'undefined' && !!window.PublicKeyCredential
}

export async function platformAuthenticatorAvailable(): Promise<boolean> {
  try {
    return (
      isWebAuthnSupported() &&
      (await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable())
    )
  } catch {
    return false
  }
}

// Backend register-options -> browser create() input.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function toCreationOptions(opts: any): CredentialCreationOptions {
  const publicKey = { ...opts }
  publicKey.challenge = b64urlToBuf(opts.challenge)
  publicKey.user = { ...opts.user, id: b64urlToBuf(opts.user.id) }
  if (Array.isArray(opts.excludeCredentials)) {
    publicKey.excludeCredentials = opts.excludeCredentials.map(
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      (c: any) => ({ ...c, id: b64urlToBuf(c.id) }),
    )
  }
  return { publicKey }
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function toRequestOptions(opts: any): CredentialRequestOptions {
  const publicKey = { ...opts }
  publicKey.challenge = b64urlToBuf(opts.challenge)
  if (Array.isArray(opts.allowCredentials)) {
    publicKey.allowCredentials = opts.allowCredentials.map(
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      (c: any) => ({ ...c, id: b64urlToBuf(c.id) }),
    )
  }
  return { publicKey }
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function serializeRegistration(cred: PublicKeyCredential): any {
  const r = cred.response as AuthenticatorAttestationResponse
  return {
    id: cred.id,
    rawId: bufToB64url(cred.rawId),
    type: cred.type,
    response: {
      attestationObject: bufToB64url(r.attestationObject),
      clientDataJSON: bufToB64url(r.clientDataJSON),
    },
    clientExtensionResults: cred.getClientExtensionResults?.() ?? {},
  }
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function serializeAssertion(cred: PublicKeyCredential): any {
  const r = cred.response as AuthenticatorAssertionResponse
  return {
    id: cred.id,
    rawId: bufToB64url(cred.rawId),
    type: cred.type,
    response: {
      authenticatorData: bufToB64url(r.authenticatorData),
      clientDataJSON: bufToB64url(r.clientDataJSON),
      signature: bufToB64url(r.signature),
      userHandle: r.userHandle ? bufToB64url(r.userHandle) : null,
    },
  }
}

export class PasskeyCancelled extends Error {}
export class PasskeyUnsupported extends Error {}