aboutsummaryrefslogtreecommitdiffstats
path: root/lib/passkey.ts
blob: 2875e388b4ad428748c0769dc9da66ff03387149 (plain) (blame)
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
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
import {
  generateRegistrationOptions,
  verifyRegistrationResponse,
  generateAuthenticationOptions,
  verifyAuthenticationResponse,
} from "@simplewebauthn/server";
// @ts-ignore
import type { AuthenticatorTransportFuture } from "@simplewebauthn/types";
import { db, g, s } from "$lib/db";
import { fail } from "$lib/utils";
import { v4 } from "uuid";

const rpName = "coinos";
const androidOrigin =
  "android:apk-key-hash:DaYNHto1fsy7jrhOfRaDDy4HCRNqFo8H0gf3DmW7bOw";

function getRpID(origin: string) {
  try {
    return new URL(origin).hostname;
  } catch {
    return "localhost";
  }
}

function getExpectedOrigins(origin: string) {
  return [origin, androidOrigin];
}

export async function generatePasskeyRegistration(user: any, origin: string) {
  const rpID = getRpID(origin);
  const passkeys = user.passkeys || [];

  const options = await generateRegistrationOptions({
    rpName,
    rpID,
    userName: user.username,
    userID: new TextEncoder().encode(user.id) as any,
    attestationType: "none",
    excludeCredentials: passkeys.map((p: any) => ({
      id: p.credentialID,
      transports: p.transports as AuthenticatorTransportFuture[],
    })),
    authenticatorSelection: {
      residentKey: "required",
      userVerification: "preferred",
    },
  });

  await db.set(`challenge:${user.id}`, options.challenge, { EX: 300 });

  return options;
}

export async function verifyPasskeyRegistration(user: any, response: any, origin: string) {
  const rpID = getRpID(origin);
  const expectedChallenge = await db.get(`challenge:${user.id}`);
  if (!expectedChallenge) fail("Challenge expired");

  const verification = await verifyRegistrationResponse({
    response,
    expectedChallenge: expectedChallenge as string,
    expectedOrigin: getExpectedOrigins(origin),
    expectedRPID: rpID,
  });

  if (!verification.verified || !verification.registrationInfo) {
    fail("Passkey verification failed");
  }

  await db.del(`challenge:${user.id}`);

  const { credential } = verification.registrationInfo;

  const cred = {
    credentialID: credential.id,
    credentialPublicKey: Buffer.from(credential.publicKey).toString("base64url"),
    counter: credential.counter,
    transports: response.response.transports || [],
    createdAt: Date.now(),
  };

  await db.set(`passkey:${credential.id}`, user.id);

  return cred;
}

export async function generatePasskeyLogin(origin: string) {
  const rpID = getRpID(origin);

  const options = await generateAuthenticationOptions({
    rpID,
    userVerification: "preferred",
  });

  const challengeId = v4();
  await db.set(`challenge:passkey:${challengeId}`, options.challenge, { EX: 300 });

  return { ...options, challengeId };
}

export async function verifyPasskeyLogin(response: any, challengeId: string, origin: string) {
  const rpID = getRpID(origin);
  const userId = await db.get(`passkey:${response.id}`);
  if (!userId) fail("Passkey not recognized");

  const user = await g(`user:${userId}`);
  if (!user) fail("User not found");

  const passkeys = user.passkeys || [];
  const passkey = passkeys.find((p: any) => p.credentialID === response.id);
  if (!passkey) fail("Passkey not found");

  const expectedChallenge = await db.get(`challenge:passkey:${challengeId}`);
  if (!expectedChallenge) fail("Challenge expired");

  const verification = await verifyAuthenticationResponse({
    response,
    expectedChallenge: expectedChallenge as string,
    expectedOrigin: getExpectedOrigins(origin),
    expectedRPID: rpID,
    requireUserVerification: false,
    credential: {
      id: passkey.credentialID,
      publicKey: Buffer.from(passkey.credentialPublicKey, "base64url") as any,
      counter: passkey.counter,
      transports: passkey.transports as AuthenticatorTransportFuture[],
    } as any,
  });

  if (!verification.verified) fail("Passkey authentication failed");

  await db.del(`challenge:passkey:${challengeId}`);

  passkey.counter = verification.authenticationInfo.newCounter;
  await s(`user:${user.id}`, user);

  return user;
}