add passkey

This commit is contained in:
Sasha 2025-03-22 23:07:22 +01:00
parent 21692759ce
commit e236b866af
No known key found for this signature in database
3 changed files with 210 additions and 25 deletions

View File

@ -1,14 +1,18 @@
"use client";
import { useState } from 'react';
import { useEffect, useState } from 'react';
import { useRLN } from '../contexts/RLNContext';
import { useWallet } from '../contexts/WalletContext';
import { DecryptedCredentials } from '@waku/rln';
import { DecryptedCredentials, IdentityCredential } from '@waku/rln';
import { usePasskey } from '@/contexts/usePasskey';
export default function RLNMembershipRegistration() {
const { registerMembership, isInitialized, isStarted, rateMinLimit, rateMaxLimit, error, initializeRLN } = useRLN();
const { registerMembership, isInitialized, isStarted, rateMinLimit, rateMaxLimit, error, initializeRLN, rln } = useRLN();
const { isConnected, address, chainId } = useWallet();
const { createPasskey, hasPasskey, getPasskey, getPasskeyCredential } = usePasskey();
const [identity, setIdentity] = useState<IdentityCredential>();
const [rateLimit, setRateLimit] = useState<number>(rateMinLimit);
const [isRegistering, setIsRegistering] = useState(false);
const [isInitializing, setIsInitializing] = useState(false);
@ -20,6 +24,17 @@ export default function RLNMembershipRegistration() {
credentials?: DecryptedCredentials;
}>({});
const handleReadPasskey = () => {
if (rln && hasPasskey()) {
const seed = getPasskey();
getPasskeyCredential(seed!);
const _identity = rln.zerokit.generateSeededIdentityCredential(seed!);
console.log(_identity);
setIdentity(_identity);
}
};
console.log(identity);
const isLineaSepolia = chainId === 59141;
const handleRateLimitChange = (e: React.ChangeEvent<HTMLInputElement>) => {
@ -65,7 +80,7 @@ export default function RLNMembershipRegistration() {
warning: 'Please check your wallet to sign the registration message.'
});
const result = await registerMembership(rateLimit);
const result = await registerMembership(rateLimit, createPasskey);
setRegistrationResult({
...result,
credentials: result.credentials
@ -139,6 +154,19 @@ export default function RLNMembershipRegistration() {
{isInitializing ? "Initializing..." : "Initialize RLN"}
</button>
)}
<button
onClick={handleReadPasskey}
disabled={isInitializing || !isLineaSepolia}
className={`px-3 py-1 text-xs rounded-md transition-colors ${
isInitializing
? "bg-gray-400 text-gray-700 cursor-not-allowed"
: isLineaSepolia
? "bg-blue-600 text-white hover:bg-blue-700"
: "bg-gray-400 text-gray-700 cursor-not-allowed"
}`}
>
Read RLN from passkey
</button>
</div>
{error && (
<p className="text-xs text-red-600 mt-1">{error}</p>
@ -251,7 +279,7 @@ export default function RLNMembershipRegistration() {
Your RLN membership is now registered and can be used with your Waku node.
</p>
{registrationResult.credentials && (
{(registrationResult.credentials && !hasPasskey()) && (
<div className="mt-3 p-3 bg-gray-100 dark:bg-gray-800 rounded-md">
<p className="font-medium mb-2">Your RLN Credentials:</p>
<div className="text-xs font-mono overflow-auto">
@ -269,7 +297,7 @@ export default function RLNMembershipRegistration() {
<span className="font-semibold">ID Trapdoor:</span> {Buffer.from(registrationResult.credentials.identity.IDTrapdoor).toString('hex')}
</p>
<h4 className="font-semibold mt-3 mb-1">Membership:</h4>
{/* <h4 className="font-semibold mt-3 mb-1">Membership:</h4>
<p className="mb-1">
<span className="font-semibold">Chain ID:</span> {registrationResult.credentials.membership.chainId}
</p>
@ -278,7 +306,42 @@ export default function RLNMembershipRegistration() {
</p>
<p className="mb-1">
<span className="font-semibold">Tree Index:</span> {registrationResult.credentials.membership.treeIndex}
</p> */}
</div>
<p className="text-xs mt-2 text-gray-600 dark:text-gray-400">
These credentials are your proof of membership. Store them securely.
</p>
</div>
)}
{identity && (
<div className="mt-3 p-3 bg-gray-100 dark:bg-gray-800 rounded-md">
<p className="font-medium mb-2">Your RLN Credentials:</p>
<div className="text-xs font-mono overflow-auto">
<h4 className="font-semibold mt-2 mb-1">Identity:</h4>
<p className="mb-1">
<span className="font-semibold">ID Commitment:</span> {Buffer.from(identity.IDCommitment).toString('hex')}
</p>
<p className="mb-1">
<span className="font-semibold">ID Secret Hash:</span> {Buffer.from(identity.IDSecretHash).toString('hex')}
</p>
<p className="mb-1">
<span className="font-semibold">ID Nullifier:</span> {Buffer.from(identity.IDNullifier).toString('hex')}
</p>
<p className="mb-3">
<span className="font-semibold">ID Trapdoor:</span> {Buffer.from(identity.IDTrapdoor).toString('hex')}
</p>
{/* <h4 className="font-semibold mt-3 mb-1">Membership:</h4>
<p className="mb-1">
<span className="font-semibold">Chain ID:</span> {registrationResult.credentials.membership.chainId}
</p>
<p className="mb-1">
<span className="font-semibold">Contract Address:</span> {registrationResult.credentials.membership.address}
</p>
<p className="mb-1">
<span className="font-semibold">Tree Index:</span> {registrationResult.credentials.membership.treeIndex}
</p> */}
</div>
<p className="text-xs mt-2 text-gray-600 dark:text-gray-400">
These credentials are your proof of membership. Store them securely.

View File

@ -6,7 +6,7 @@ import { useWallet } from './WalletContext';
import { ethers } from 'ethers';
// Constants
const SIGNATURE_MESSAGE = "Sign this message to generate your RLN credentials";
// const SIGNATURE_MESSAGE = "Sign this message to generate your RLN credentials";
const ERC20_ABI = [
"function allowance(address owner, address spender) view returns (uint256)",
"function approve(address spender, uint256 amount) returns (bool)",
@ -25,7 +25,7 @@ interface RLNContextType {
isStarted: boolean;
error: string | null;
initializeRLN: () => Promise<void>;
registerMembership: (rateLimit: number) => Promise<{ success: boolean; error?: string; credentials?: DecryptedCredentials }>;
registerMembership: (rateLimit: number, createPasskey: (s: ethers.Signer) => Promise<string>) => Promise<{ success: boolean; error?: string; credentials?: DecryptedCredentials }>;
rateMinLimit: number;
rateMaxLimit: number;
}
@ -148,7 +148,7 @@ export function RLNProvider({ children }: { children: ReactNode }) {
}
};
const registerMembership = async (rateLimit: number) => {
const registerMembership = async (rateLimit: number, createPasskey: (s: ethers.Signer) => Promise<string>) => {
console.log("registerMembership called with rate limit:", rateLimit);
if (!rln || !isStarted) {
@ -223,22 +223,20 @@ export function RLNProvider({ children }: { children: ReactNode }) {
console.log("Token allowance already sufficient");
}
// Generate signature for identity
const message = `${SIGNATURE_MESSAGE} ${Date.now()}`;
const signature = await signer.signMessage(message);
const seed = await createPasskey(signer);
const _credentials = await rln.registerMembership({signature: signature});
if (!_credentials) {
throw new Error("Failed to register membership: No credentials returned");
}
if (!_credentials.identity) {
throw new Error("Failed to register membership: Missing identity information");
}
if (!_credentials.membership) {
throw new Error("Failed to register membership: Missing membership information");
}
// const _credentials = await rln.registerMembership({signature: seed});
// if (!_credentials) {
// throw new Error("Failed to register membership: No credentials returned");
// }
// if (!_credentials.identity) {
// throw new Error("Failed to register membership: Missing identity information");
// }
// if (!_credentials.membership) {
// throw new Error("Failed to register membership: Missing membership information");
// }
return { success: true, credentials: _credentials };
return { success: true, credentials: null };
} catch (err) {
let errorMsg = "Failed to register membership";
if (err instanceof Error) {
@ -258,7 +256,7 @@ export function RLNProvider({ children }: { children: ReactNode }) {
} else {
console.log("Wallet not connected or no signer available, skipping RLN initialization");
}
}, [isConnected, signer]);
}, [isConnected, signer, initializeRLN]);
// Debug log for state changes
useEffect(() => {

View File

@ -0,0 +1,124 @@
import { useState, useEffect } from 'react';
export const usePasskey = () => {
const [passkeyId, setPasskeyId] = useState<string | null>(null);
// Load passkey when component mounts
// useEffect(() => {
// const loadPasskey = async () => {
// const storedPasskeyId = localStorage.getItem('rlnPasskeyId');
// if (storedPasskeyId) {
// // Try to get the credential from navigator.credentials
// try {
// const credential = await getPasskeyCredential(storedPasskeyId);
// if (credential) {
// setPasskeyId(storedPasskeyId);
// }
// } catch (error) {
// console.error("Failed to retrieve passkey:", error);
// // If the credential can't be found, clear localStorage
// localStorage.removeItem('rlnPasskeyId');
// }
// }
// };
// loadPasskey();
// }, []);
// Check if a passkey exists
const hasPasskey = (): boolean => {
return localStorage.getItem('rlnPasskeyId') !== null;
};
const getPasskey = (): string | null => {
const storedPasskeyId = localStorage.getItem('rlnPasskeyId');
return storedPasskeyId;
};
const getPasskeyCredential = async (credentialId: string): Promise<PublicKeyCredential | null> => {
try {
const idBuffer = Uint8Array.from(
atob(credentialId.replace(/-/g, '+').replace(/_/g, '/')),
c => c.charCodeAt(0)
);
// Create get options for the credential
const getOptions = {
publicKey: {
challenge: new Uint8Array(32),
allowCredentials: [{
id: idBuffer,
type: 'public-key',
}],
userVerification: 'required',
timeout: 60000
}
};
// Generate random values for the challenge
window.crypto.getRandomValues(getOptions.publicKey.challenge);
// Get the credential
const credential = await navigator.credentials.get(getOptions as any) as PublicKeyCredential;
return credential;
} catch (error) {
console.error("Error retrieving passkey:", error);
return null;
}
};
// Create a new passkey
const createPasskey = async (signer: any): Promise<string> => {
// Generate a random challenge for the passkey
const challenge = new Uint8Array(32);
window.crypto.getRandomValues(challenge);
// Create credential options for the passkey
const credentialCreationOptions = {
publicKey: {
challenge: challenge,
rp: {
name: "RLN Membership",
id: window.location.hostname
},
user: {
id: new Uint8Array([...new TextEncoder().encode(await signer.getAddress())]),
name: "RLN Membership Passkey",
displayName: "RLN Membership Passkey"
},
pubKeyCredParams: [
{ type: "public-key", alg: -7 }, // ES256
{ type: "public-key", alg: -257 } // RS256
],
authenticatorSelection: {
authenticatorAttachment: "platform",
requireResidentKey: true,
userVerification: "required"
},
timeout: 60000
}
};
const credential = await navigator.credentials.create(credentialCreationOptions as any) as PublicKeyCredential;
if (!credential) {
throw new Error("Failed to create passkey");
}
// Store credential ID in state and localStorage
setPasskeyId(credential.id);
localStorage.setItem('rlnPasskeyId', credential.id);
return credential.id;
};
// Return the methods and state for passkey management
return {
hasPasskey,
getPasskey,
passkeyId,
createPasskey,
getPasskeyCredential
};
};