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
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
|
"use client";
import "core-js/actual/typed-array";
import * as age from "age-encryption";
import { Platform } from "react-native";
import * as SecureStore from "expo-secure-store";
import type { AsyncRes } from "./types";
import { randomBytes } from "ethers";
// encreeptoor
import { sha256 } from "@noble/hashes/sha256";
import { extract } from "@noble/hashes/hkdf";
import { base64 } from "@scure/base";
import { bytes2hex } from "./utils/bit";
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}` };
}
}
export async function getPasskeyFromUser(): AsyncRes<PublicKeyCredential> {
try {
const nonce = randomBytes(16);
// Generate a random challenge
const challenge = new Uint8Array(32);
crypto.getRandomValues(challenge);
const getCredentialOptions: CredentialRequestOptions = {
publicKey: {
allowCredentials: [],
challenge,
rpId:
window.location.hostname === "localhost"
? "localhost"
: RELYING_PARTY_ID,
userVerification: "required",
extensions: { prf: { eval: prfInputs(nonce) } },
},
};
const credential = (await navigator.credentials.get(
getCredentialOptions,
)) as PublicKeyCredential;
console.log({ credential });
if (credential) {
console.log("User provided passkey:", credential);
const results = credential.getClientExtensionResults().prf;
console.log({ results });
return { ok: credential };
}
return { error: "No passkey provided" };
} catch (error) {
console.error("Error getting passkey from user:", error);
return { error: `${error}` };
}
}
const label = "age-encryption.org/fido2prf";
function prfInputs(nonce: Uint8Array): AuthenticationExtensionsPRFValues {
const prefix = new TextEncoder().encode(label);
const first = new Uint8Array(prefix.length + nonce.length + 1);
first.set(prefix, 0);
first[prefix.length] = 0x01;
first.set(nonce, prefix.length + 1);
const second = new Uint8Array(prefix.length + nonce.length + 1);
second.set(prefix, 0);
second[prefix.length] = 0x02;
second.set(nonce, prefix.length + 1);
return { first, second };
}
function deriveKey(results: AuthenticationExtensionsPRFValues): Uint8Array {
if (results.second === undefined) {
throw Error("Missing second PRF result");
}
const prf = new Uint8Array(
results.first.byteLength + results.second.byteLength,
);
prf.set(new Uint8Array(results.first as ArrayBuffer), 0);
prf.set(
new Uint8Array(results.second as ArrayBuffer),
results.first.byteLength,
);
return extract(sha256, prf, label);
}
async function getCredentialWithPRF(
nonce: Uint8Array,
): Promise<AuthenticationExtensionsPRFValues> {
const credential = (await navigator.credentials.get({
publicKey: {
allowCredentials: [],
challenge: randomBytes(16),
rpId:
window.location.hostname === "localhost"
? "localhost"
: RELYING_PARTY_ID,
userVerification: "required",
extensions: { prf: { eval: prfInputs(nonce) } },
},
})) as PublicKeyCredential;
const results = credential.getClientExtensionResults().prf?.results;
if (results === undefined) {
throw Error("PRF extension not available (need macOS 15+, Chrome 132+)");
}
return results;
}
export async function pkEncryptXOR(dataBytes: Uint8Array): Promise<string> {
const nonce = randomBytes(16);
const results = await getCredentialWithPRF(nonce);
const key = deriveKey(results);
const encrypted = new Uint8Array(dataBytes.length);
console.log({ key: key.byteLength, data: encrypted.byteLength });
// XOR with derived key (repeat key if data is longer)
const hash = Uint8Array.from(key, (v, i) => v ^ encrypted[i]);
for (let i = 0; i < dataBytes.length; i++) {
encrypted[i] = dataBytes[i] ^ key[i % key.length];
}
console.log({ hash, encrypted });
// Return nonce + encrypted data as base64
const result = new Uint8Array(nonce.length + encrypted.length);
result.set(nonce);
result.set(encrypted, nonce.length);
return base64.encode(result);
}
export async function pkDecryptXOR(encryptedData: string): Promise<string> {
const data = base64.decode(encryptedData);
const nonce = data.slice(0, 16);
const encrypted = data.slice(16);
const results = await getCredentialWithPRF(nonce);
const key = deriveKey(results);
const decrypted = new Uint8Array(encrypted.length);
// XOR with derived key
for (let i = 0; i < encrypted.length; i++) {
decrypted[i] = encrypted[i] ^ key[i % key.length];
}
const hash = Uint8Array.from(key, (v, i) => v ^ encrypted[i]);
const one = bytes2hex(decrypted);
const two = bytes2hex(hash);
return one;
}
|