"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 { 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 { // 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 { 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 { 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 { 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) for (let i = 0; i < dataBytes.length; i++) { encrypted[i] = dataBytes[i] ^ key[i % key.length]; } // 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 { 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 one = bytes2hex(decrypted); // const hash = Uint8Array.from(key, (v, i) => v ^ encrypted[i]); // const two = bytes2hex(hash); return one; }