summaryrefslogtreecommitdiff
path: root/lib/passkey.ts
blob: 6deb656e07cca0cc3dfa04f0825b3d120ca73967 (plain)
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
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
"use client";

import * as age from "age-encryption";
import { Platform } from "react-native";
import * as SecureStore from "expo-secure-store";
import type { AsyncRes } from "./types";

const PASSKEY_CREDENTIAL_ID_KEY = "urbit_wallet_passkey_id";
const RELYING_PARTY_ID = "wallet.urbit.org"; // Change this to your domain
const RELYING_PARTY_NAME = "Urbit Wallet";

export async function createFancyPasskey() {
  const credential = await age.webauthn.createCredential({
    keyName: "pasukii",
  });
  return credential;
}
export async function pkEncrypt(mticket: string) {
  const e = new age.Encrypter();
  e.addRecipient(new age.webauthn.WebAuthnRecipient());
  const ciphertext = await e.encrypt(mticket);
  const armored = age.armor.encode(ciphertext);
  return armored;
}
export async function pkDecrypt(data: string) {
  const d = new age.Decrypter();
  d.addIdentity(new age.webauthn.WebAuthnIdentity());
  const decoded = age.armor.decode(data);
  const out = await d.decrypt(decoded, "text");
  return out;
}

export async function createPasskey(): AsyncRes<PublicKeyCredential> {
  const pkok =
    await window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
  console.log({ pkok });

  // if (Platform.OS !== "web") {
  //   return false;
  // }
  console.log("creating passkey");

  try {
    // Generate a random user ID
    const userId = new Uint8Array(16);
    crypto.getRandomValues(userId);

    // Generate a random challenge
    const challenge = new Uint8Array(32);
    crypto.getRandomValues(challenge);

    const createCredentialOptions: CredentialCreationOptions = {
      publicKey: {
        // REQUIRED: Random challenge to prevent replay attacks
        // Must be at least 16 bytes, we use 32 for extra security
        challenge,

        // REQUIRED: Relying Party (your website/app)
        rp: {
          // REQUIRED: Human-readable name shown in UI prompts
          name: RELYING_PARTY_NAME,

          // OPTIONAL: Domain that owns this credential
          // If omitted, defaults to current domain
          // Must match or be a registrable suffix of the current domain
          id:
            window.location.hostname === "localhost"
              ? "localhost"
              : RELYING_PARTY_ID,
        },

        // REQUIRED: User information
        user: {
          // REQUIRED: Unique user ID (must not contain PII)
          // Used by authenticator to distinguish between accounts
          id: userId,

          // REQUIRED: Unique username/identifier for this RP
          // Shown in some UI contexts, should be recognizable to user
          name: "urbit-user",

          // REQUIRED: Human-readable name for display
          // Shown in account selectors and prompts
          displayName: "Urbit Wallet User",
        },

        // REQUIRED: List of acceptable public key algorithms
        // Order matters - authenticator will use first supported algorithm
        pubKeyCredParams: [
          { alg: -7, type: "public-key" }, // ES256 (ECDSA with SHA-256) - most common
          { alg: -257, type: "public-key" }, // RS256 (RSASSA-PKCS1-v1_5) - fallback
        ],

        // OPTIONAL: Criteria for authenticator selection
        authenticatorSelection: {
          // OPTIONAL: "platform" (built-in like Touch ID) or "cross-platform" (USB key)
          // Omitting allows both types
          // authenticatorAttachment: "platform",

          // OPTIONAL: User verification requirement
          // "required" - must verify user (biometric/PIN)
          // "preferred" - verify if possible, continue if not (default)
          // "discouraged" - don't verify unless required by RP
          userVerification: "preferred",

          // OPTIONAL: Whether to create a discoverable credential (passkey)
          // "required" - must create discoverable credential
          // "preferred" - create if possible (default)
          // "discouraged" - don't create discoverable credential
          residentKey: "preferred",

          // DEPRECATED: Use residentKey instead
          // Kept for backwards compatibility with older authenticators
          requireResidentKey: false,
        },

        // OPTIONAL: Time limit in milliseconds (default varies by browser)
        // 60 seconds is reasonable for user interaction
        timeout: 60000,

        // OPTIONAL: Attestation preference (proof of authenticator legitimacy)
        // "none" - no attestation needed (default, best for privacy)
        // "indirect" - RP prefers attestation but allows anonymization
        // "direct" - RP needs attestation from authenticator
        // "enterprise" - RP needs attestation and device info (requires permission)
        attestation: "none",

        // OPTIONAL: List of credentials to exclude (prevent duplicates)
        // excludeCredentials: [
        //   { id: existingCredentialId, type: "public-key" }
        // ],

        // OPTIONAL: Extensions for additional features
        // extensions: {
        //   credProps: true,  // Request additional credential properties
        //   hmacCreateSecret: true,  // For symmetric key operations
        // },
      },
    };

    const credential = (await navigator.credentials.create(
      createCredentialOptions,
    )) as PublicKeyCredential;
    console.log({ credential });

    if (credential) {
      // Store the credential ID for later use
      await SecureStore.setItemAsync(PASSKEY_CREDENTIAL_ID_KEY, credential.id);

      console.log("Passkey created successfully", credential.id);
      return { ok: credential };
    }

    return { error: "Failed to create passkey" };
  } catch (error) {
    console.error("Error creating passkey:", error);
    return { error: `${error}` };
  }
}

export async function authenticateWithPasskey(): AsyncRes<PublicKeyCredential> {
  // if (Platform.OS !== "web") {
  //   return false;
  // }

  try {
    const credentialId = await SecureStore.getItemAsync(
      PASSKEY_CREDENTIAL_ID_KEY,
    );

    if (!credentialId) {
      console.log("No passkey found");
      return { error: "No passkey found" };
    }

    // Generate a random challenge
    const challenge = new Uint8Array(32);
    crypto.getRandomValues(challenge);

    const getCredentialOptions: CredentialRequestOptions = {
      publicKey: {
        challenge,
        rpId:
          window.location.hostname === "localhost"
            ? "localhost"
            : RELYING_PARTY_ID,
        allowCredentials: [
          {
            id: Uint8Array.from(atob(credentialId), (c) => c.charCodeAt(0)),
            type: "public-key",
            transports: ["internal", "hybrid"] as AuthenticatorTransport[],
          },
        ],
        userVerification: "preferred",
        timeout: 60000,
      },
    };

    const credential = (await navigator.credentials.get(
      getCredentialOptions,
    )) as PublicKeyCredential;

    if (credential) {
      // In a production app, you would send the assertion to your server
      // to verify it. For this demo, we're just checking if we got a credential.
      console.log("Passkey authentication successful");
      return { ok: credential };
    }

    return { error: "failed to generate creds" };
  } catch (error) {
    return { error: `${error}` };
  }
}