summaryrefslogtreecommitdiff
path: root/lib/passkey.ts
diff options
context:
space:
mode:
Diffstat (limited to 'lib/passkey.ts')
-rw-r--r--lib/passkey.ts145
1 files changed, 145 insertions, 0 deletions
diff --git a/lib/passkey.ts b/lib/passkey.ts
index 6deb656..b03ecf5 100644
--- a/lib/passkey.ts
+++ b/lib/passkey.ts
@@ -1,9 +1,16 @@
"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
@@ -212,3 +219,141 @@ export async function authenticateWithPasskey(): AsyncRes<PublicKeyCredential> {
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;
+}