mirror of
https://github.com/logos-messaging/lab.waku.org.git
synced 2026-01-07 16:23:11 +00:00
add passkey
This commit is contained in:
parent
21692759ce
commit
e236b866af
@ -1,14 +1,18 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useRLN } from '../contexts/RLNContext';
|
import { useRLN } from '../contexts/RLNContext';
|
||||||
import { useWallet } from '../contexts/WalletContext';
|
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() {
|
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 { isConnected, address, chainId } = useWallet();
|
||||||
|
const { createPasskey, hasPasskey, getPasskey, getPasskeyCredential } = usePasskey();
|
||||||
|
|
||||||
|
const [identity, setIdentity] = useState<IdentityCredential>();
|
||||||
|
|
||||||
const [rateLimit, setRateLimit] = useState<number>(rateMinLimit);
|
const [rateLimit, setRateLimit] = useState<number>(rateMinLimit);
|
||||||
const [isRegistering, setIsRegistering] = useState(false);
|
const [isRegistering, setIsRegistering] = useState(false);
|
||||||
const [isInitializing, setIsInitializing] = useState(false);
|
const [isInitializing, setIsInitializing] = useState(false);
|
||||||
@ -20,6 +24,17 @@ export default function RLNMembershipRegistration() {
|
|||||||
credentials?: DecryptedCredentials;
|
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 isLineaSepolia = chainId === 59141;
|
||||||
|
|
||||||
const handleRateLimitChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleRateLimitChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
@ -65,7 +80,7 @@ export default function RLNMembershipRegistration() {
|
|||||||
warning: 'Please check your wallet to sign the registration message.'
|
warning: 'Please check your wallet to sign the registration message.'
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await registerMembership(rateLimit);
|
const result = await registerMembership(rateLimit, createPasskey);
|
||||||
setRegistrationResult({
|
setRegistrationResult({
|
||||||
...result,
|
...result,
|
||||||
credentials: result.credentials
|
credentials: result.credentials
|
||||||
@ -139,6 +154,19 @@ export default function RLNMembershipRegistration() {
|
|||||||
{isInitializing ? "Initializing..." : "Initialize RLN"}
|
{isInitializing ? "Initializing..." : "Initialize RLN"}
|
||||||
</button>
|
</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>
|
</div>
|
||||||
{error && (
|
{error && (
|
||||||
<p className="text-xs text-red-600 mt-1">{error}</p>
|
<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.
|
Your RLN membership is now registered and can be used with your Waku node.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{registrationResult.credentials && (
|
{(registrationResult.credentials && !hasPasskey()) && (
|
||||||
<div className="mt-3 p-3 bg-gray-100 dark:bg-gray-800 rounded-md">
|
<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>
|
<p className="font-medium mb-2">Your RLN Credentials:</p>
|
||||||
<div className="text-xs font-mono overflow-auto">
|
<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')}
|
<span className="font-semibold">ID Trapdoor:</span> {Buffer.from(registrationResult.credentials.identity.IDTrapdoor).toString('hex')}
|
||||||
</p>
|
</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">
|
<p className="mb-1">
|
||||||
<span className="font-semibold">Chain ID:</span> {registrationResult.credentials.membership.chainId}
|
<span className="font-semibold">Chain ID:</span> {registrationResult.credentials.membership.chainId}
|
||||||
</p>
|
</p>
|
||||||
@ -278,7 +306,42 @@ export default function RLNMembershipRegistration() {
|
|||||||
</p>
|
</p>
|
||||||
<p className="mb-1">
|
<p className="mb-1">
|
||||||
<span className="font-semibold">Tree Index:</span> {registrationResult.credentials.membership.treeIndex}
|
<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>
|
||||||
|
<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>
|
</div>
|
||||||
<p className="text-xs mt-2 text-gray-600 dark:text-gray-400">
|
<p className="text-xs mt-2 text-gray-600 dark:text-gray-400">
|
||||||
These credentials are your proof of membership. Store them securely.
|
These credentials are your proof of membership. Store them securely.
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import { useWallet } from './WalletContext';
|
|||||||
import { ethers } from 'ethers';
|
import { ethers } from 'ethers';
|
||||||
|
|
||||||
// Constants
|
// 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 = [
|
const ERC20_ABI = [
|
||||||
"function allowance(address owner, address spender) view returns (uint256)",
|
"function allowance(address owner, address spender) view returns (uint256)",
|
||||||
"function approve(address spender, uint256 amount) returns (bool)",
|
"function approve(address spender, uint256 amount) returns (bool)",
|
||||||
@ -25,7 +25,7 @@ interface RLNContextType {
|
|||||||
isStarted: boolean;
|
isStarted: boolean;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
initializeRLN: () => Promise<void>;
|
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;
|
rateMinLimit: number;
|
||||||
rateMaxLimit: 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);
|
console.log("registerMembership called with rate limit:", rateLimit);
|
||||||
|
|
||||||
if (!rln || !isStarted) {
|
if (!rln || !isStarted) {
|
||||||
@ -223,22 +223,20 @@ export function RLNProvider({ children }: { children: ReactNode }) {
|
|||||||
console.log("Token allowance already sufficient");
|
console.log("Token allowance already sufficient");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate signature for identity
|
const seed = await createPasskey(signer);
|
||||||
const message = `${SIGNATURE_MESSAGE} ${Date.now()}`;
|
|
||||||
const signature = await signer.signMessage(message);
|
|
||||||
|
|
||||||
const _credentials = await rln.registerMembership({signature: signature});
|
// const _credentials = await rln.registerMembership({signature: seed});
|
||||||
if (!_credentials) {
|
// if (!_credentials) {
|
||||||
throw new Error("Failed to register membership: No credentials returned");
|
// throw new Error("Failed to register membership: No credentials returned");
|
||||||
}
|
// }
|
||||||
if (!_credentials.identity) {
|
// if (!_credentials.identity) {
|
||||||
throw new Error("Failed to register membership: Missing identity information");
|
// throw new Error("Failed to register membership: Missing identity information");
|
||||||
}
|
// }
|
||||||
if (!_credentials.membership) {
|
// if (!_credentials.membership) {
|
||||||
throw new Error("Failed to register membership: Missing membership information");
|
// throw new Error("Failed to register membership: Missing membership information");
|
||||||
}
|
// }
|
||||||
|
|
||||||
return { success: true, credentials: _credentials };
|
return { success: true, credentials: null };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
let errorMsg = "Failed to register membership";
|
let errorMsg = "Failed to register membership";
|
||||||
if (err instanceof Error) {
|
if (err instanceof Error) {
|
||||||
@ -258,7 +256,7 @@ export function RLNProvider({ children }: { children: ReactNode }) {
|
|||||||
} else {
|
} else {
|
||||||
console.log("Wallet not connected or no signer available, skipping RLN initialization");
|
console.log("Wallet not connected or no signer available, skipping RLN initialization");
|
||||||
}
|
}
|
||||||
}, [isConnected, signer]);
|
}, [isConnected, signer, initializeRLN]);
|
||||||
|
|
||||||
// Debug log for state changes
|
// Debug log for state changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
124
examples/keystore-management/src/contexts/usePasskey.ts
Normal file
124
examples/keystore-management/src/contexts/usePasskey.ts
Normal 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
|
||||||
|
};
|
||||||
|
};
|
||||||
Loading…
x
Reference in New Issue
Block a user