feat: key delegation

This commit is contained in:
Danish Arora 2025-04-27 15:54:24 +05:30
parent 8859b85367
commit 8bfbb41bd8
No known key found for this signature in database
GPG Key ID: 1C6EF37CDAE1426E
6 changed files with 191 additions and 50 deletions

View File

@ -218,8 +218,8 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
try { try {
toast({ toast({
title: "Delegating Key", title: "Starting Key Delegation",
description: "Generating a browser keypair for quick signing...", description: "This will let you post, comment, and vote without approving each action for 24 hours.",
}); });
// Generate a browser keypair // Generate a browser keypair
@ -236,9 +236,13 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
expiryTimestamp expiryTimestamp
); );
// Format date for user-friendly display
const expiryDate = new Date(expiryTimestamp);
const formattedExpiry = expiryDate.toLocaleString();
toast({ toast({
title: "Signing Required", title: "Wallet Signature Required",
description: "Please sign the delegation message with your wallet...", description: `Please sign with your wallet to authorize a temporary key valid until ${formattedExpiry}. This improves UX by reducing wallet prompts.`,
}); });
const signature = await phantomWalletRef.current.signMessage(delegationMessage); const signature = await phantomWalletRef.current.signMessage(delegationMessage);
@ -264,8 +268,8 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
localStorage.setItem('opchan-user', JSON.stringify(updatedUser)); localStorage.setItem('opchan-user', JSON.stringify(updatedUser));
toast({ toast({
title: "Key Delegated", title: "Key Delegation Successful",
description: `You won't need to sign every action for the next ${expiryHours} hours.`, description: `You can now interact with the forum without additional wallet approvals until ${formattedExpiry}.`,
}); });
return true; return true;
@ -273,12 +277,20 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
console.error("Error delegating key:", error); console.error("Error delegating key:", error);
let errorMessage = "Failed to delegate key. Please try again."; let errorMessage = "Failed to delegate key. Please try again.";
if (error instanceof Error) { if (error instanceof Error) {
errorMessage = error.message; // Provide specific guidance based on error type
if (error.message.includes("rejected") || error.message.includes("declined") || error.message.includes("denied")) {
errorMessage = "You declined the signature request. Key delegation is optional but improves your experience.";
} else if (error.message.includes("timeout")) {
errorMessage = "Wallet request timed out. Please try again and approve the signature promptly.";
} else {
errorMessage = error.message;
}
} }
toast({ toast({
title: "Delegation Error", title: "Delegation Failed",
description: errorMessage, description: errorMessage,
variant: "destructive", variant: "destructive",
}); });

View File

@ -1,5 +1,5 @@
import React, { createContext, useContext, useState, useEffect } from 'react'; import React, { createContext, useContext, useState, useEffect } from 'react';
import { Cell, Post, Comment } from '@/types'; import { Cell, Post, Comment, OpchanMessage } from '@/types';
import { useToast } from '@/components/ui/use-toast'; import { useToast } from '@/components/ui/use-toast';
import { useAuth } from '@/contexts/AuthContext'; import { useAuth } from '@/contexts/AuthContext';
import { import {
@ -61,25 +61,30 @@ export function ForumProvider({ children }: { children: React.ReactNode }) {
// Transform message cache data to the expected types // Transform message cache data to the expected types
const updateStateFromCache = () => { const updateStateFromCache = () => {
// Transform cells // Use the verifyMessage function from messageSigning if available
const verifyFn = isAuthenticated && messageSigning ?
(message: OpchanMessage) => messageSigning.verifyMessage(message) :
undefined;
// Transform cells with verification
setCells( setCells(
Object.values(messageManager.messageCache.cells).map(cell => Object.values(messageManager.messageCache.cells)
transformCell(cell) .map(cell => transformCell(cell, verifyFn))
) .filter(cell => cell !== null) as Cell[]
); );
// Transform posts // Transform posts with verification
setPosts( setPosts(
Object.values(messageManager.messageCache.posts).map(post => Object.values(messageManager.messageCache.posts)
transformPost(post) .map(post => transformPost(post, verifyFn))
) .filter(post => post !== null) as Post[]
); );
// Transform comments // Transform comments with verification
setComments( setComments(
Object.values(messageManager.messageCache.comments).map(comment => Object.values(messageManager.messageCache.comments)
transformComment(comment) .map(comment => transformComment(comment, verifyFn))
) .filter(comment => comment !== null) as Comment[]
); );
}; };

View File

@ -33,11 +33,24 @@ async function signAndSendMessage<T extends PostMessage | CommentMessage | VoteM
signedMessage = await messageSigning.signMessage(message); signedMessage = await messageSigning.signMessage(message);
if (!signedMessage) { if (!signedMessage) {
toast({ // Check if delegation exists but is expired
title: "Key Delegation Required", const isDelegationExpired = messageSigning['keyDelegation'] &&
description: "Please delegate a signing key for better UX.", !messageSigning['keyDelegation'].isDelegationValid() &&
variant: "destructive", messageSigning['keyDelegation'].retrieveDelegation();
});
if (isDelegationExpired) {
toast({
title: "Key Delegation Expired",
description: "Your signing key has expired. Please re-delegate your key through the profile menu.",
variant: "destructive",
});
} else {
toast({
title: "Key Delegation Required",
description: "Please delegate a signing key from your profile menu to post without wallet approval for each action.",
variant: "destructive",
});
}
return null; return null;
} }
} else { } else {
@ -48,9 +61,20 @@ async function signAndSendMessage<T extends PostMessage | CommentMessage | VoteM
return signedMessage; return signedMessage;
} catch (error) { } catch (error) {
console.error("Error signing and sending message:", error); console.error("Error signing and sending message:", error);
let errorMessage = "Failed to sign and send message. Please try again.";
if (error instanceof Error) {
if (error.message.includes("timeout") || error.message.includes("network")) {
errorMessage = "Network issue detected. Please check your connection and try again.";
} else if (error.message.includes("rejected") || error.message.includes("denied")) {
errorMessage = "Wallet signature request was rejected. Please approve signing to continue.";
}
}
toast({ toast({
title: "Message Error", title: "Message Error",
description: "Failed to sign and send message. Please try again.", description: errorMessage,
variant: "destructive", variant: "destructive",
}); });
return null; return null;

View File

@ -1,26 +1,55 @@
import { Cell, Post, Comment } from '@/types'; import { Cell, Post, Comment, OpchanMessage } from '@/types';
import { CellMessage, CommentMessage, PostMessage } from '@/lib/waku/types'; import { CellMessage, CommentMessage, MessageType, PostMessage, VoteMessage } from '@/lib/waku/types';
import messageManager from '@/lib/waku'; import messageManager from '@/lib/waku';
// Type for the verification function
type VerifyFunction = (message: OpchanMessage) => boolean;
// Helper function to transform CellMessage to Cell // Helper function to transform CellMessage to Cell
export const transformCell = (cellMessage: CellMessage): Cell => { export const transformCell = (
cellMessage: CellMessage,
verifyMessage?: VerifyFunction
): Cell | null => {
// Verify the message if a verification function is provided
if (verifyMessage && !verifyMessage(cellMessage)) {
console.warn(`Cell message ${cellMessage.id} failed verification`);
return null;
}
return { return {
id: cellMessage.id, id: cellMessage.id,
name: cellMessage.name, name: cellMessage.name,
description: cellMessage.description, description: cellMessage.description,
icon: cellMessage.icon icon: cellMessage.icon,
// Include signature information for future verification if needed
signature: cellMessage.signature,
browserPubKey: cellMessage.browserPubKey
}; };
}; };
// Helper function to transform PostMessage to Post with vote aggregation // Helper function to transform PostMessage to Post with vote aggregation
export const transformPost = (postMessage: PostMessage): Post => { export const transformPost = (
postMessage: PostMessage,
verifyMessage?: VerifyFunction
): Post | null => {
// Verify the message if a verification function is provided
if (verifyMessage && !verifyMessage(postMessage)) {
console.warn(`Post message ${postMessage.id} failed verification`);
return null;
}
// Find all votes related to this post // Find all votes related to this post
const votes = Object.values(messageManager.messageCache.votes).filter( const votes = Object.values(messageManager.messageCache.votes).filter(
vote => vote.targetId === postMessage.id vote => vote.targetId === postMessage.id
); );
const upvotes = votes.filter(vote => vote.value === 1); // Only include verified votes if verification function is provided
const downvotes = votes.filter(vote => vote.value === -1); const filteredVotes = verifyMessage
? votes.filter(vote => verifyMessage(vote))
: votes;
const upvotes = filteredVotes.filter(vote => vote.value === 1);
const downvotes = filteredVotes.filter(vote => vote.value === -1);
return { return {
id: postMessage.id, id: postMessage.id,
@ -30,19 +59,36 @@ export const transformPost = (postMessage: PostMessage): Post => {
content: postMessage.content, content: postMessage.content,
timestamp: postMessage.timestamp, timestamp: postMessage.timestamp,
upvotes: upvotes, upvotes: upvotes,
downvotes: downvotes downvotes: downvotes,
// Include signature information for future verification if needed
signature: postMessage.signature,
browserPubKey: postMessage.browserPubKey
}; };
}; };
// Helper function to transform CommentMessage to Comment with vote aggregation // Helper function to transform CommentMessage to Comment with vote aggregation
export const transformComment = (commentMessage: CommentMessage): Comment => { export const transformComment = (
commentMessage: CommentMessage,
verifyMessage?: VerifyFunction
): Comment | null => {
// Verify the message if a verification function is provided
if (verifyMessage && !verifyMessage(commentMessage)) {
console.warn(`Comment message ${commentMessage.id} failed verification`);
return null;
}
// Find all votes related to this comment // Find all votes related to this comment
const votes = Object.values(messageManager.messageCache.votes).filter( const votes = Object.values(messageManager.messageCache.votes).filter(
vote => vote.targetId === commentMessage.id vote => vote.targetId === commentMessage.id
); );
const upvotes = votes.filter(vote => vote.value === 1); // Only include verified votes if verification function is provided
const downvotes = votes.filter(vote => vote.value === -1); const filteredVotes = verifyMessage
? votes.filter(vote => verifyMessage(vote))
: votes;
const upvotes = filteredVotes.filter(vote => vote.value === 1);
const downvotes = filteredVotes.filter(vote => vote.value === -1);
return { return {
id: commentMessage.id, id: commentMessage.id,
@ -51,20 +97,43 @@ export const transformComment = (commentMessage: CommentMessage): Comment => {
content: commentMessage.content, content: commentMessage.content,
timestamp: commentMessage.timestamp, timestamp: commentMessage.timestamp,
upvotes: upvotes, upvotes: upvotes,
downvotes: downvotes downvotes: downvotes,
// Include signature information for future verification if needed
signature: commentMessage.signature,
browserPubKey: commentMessage.browserPubKey
}; };
}; };
// Function to update UI state from message cache // Helper function to transform VoteMessage (new)
export const getDataFromCache = () => { export const transformVote = (
// Transform cells voteMessage: VoteMessage,
const cells = Object.values(messageManager.messageCache.cells).map(transformCell); verifyMessage?: VerifyFunction
): VoteMessage | null => {
// Verify the message if a verification function is provided
if (verifyMessage && !verifyMessage(voteMessage)) {
console.warn(`Vote message ${voteMessage.id} failed verification`);
return null;
}
// Transform posts return voteMessage;
const posts = Object.values(messageManager.messageCache.posts).map(transformPost); };
// Function to update UI state from message cache with verification
export const getDataFromCache = (verifyMessage?: VerifyFunction) => {
// Transform cells with verification
const cells = Object.values(messageManager.messageCache.cells)
.map(cell => transformCell(cell, verifyMessage))
.filter(cell => cell !== null) as Cell[];
// Transform comments // Transform posts with verification
const comments = Object.values(messageManager.messageCache.comments).map(transformComment); const posts = Object.values(messageManager.messageCache.posts)
.map(post => transformPost(post, verifyMessage))
.filter(post => post !== null) as Post[];
// Transform comments with verification
const comments = Object.values(messageManager.messageCache.comments)
.map(comment => transformComment(comment, verifyMessage))
.filter(comment => comment !== null) as Comment[];
return { cells, posts, comments }; return { cells, posts, comments };
}; };

View File

@ -1,6 +1,8 @@
import { OpchanMessage } from '@/types'; import { OpchanMessage } from '@/types';
import { KeyDelegation } from './key-delegation'; import { KeyDelegation } from './key-delegation';
// Maximum age of a message in milliseconds (24 hours)
const MAX_MESSAGE_AGE = 24 * 60 * 60 * 1000;
export class MessageSigning { export class MessageSigning {
private keyDelegation: KeyDelegation; private keyDelegation: KeyDelegation;
@ -9,7 +11,7 @@ export class MessageSigning {
this.keyDelegation = keyDelegation; this.keyDelegation = keyDelegation;
} }
signMessage<T extends OpchanMessage>(message: T): T | null { signMessage<T extends OpchanMessage>(message: T): T | null {
if (!this.keyDelegation.isDelegationValid()) { if (!this.keyDelegation.isDelegationValid()) {
console.error('No valid key delegation found. Cannot sign message.'); console.error('No valid key delegation found. Cannot sign message.');
return null; return null;
@ -35,21 +37,48 @@ export class MessageSigning {
} }
verifyMessage(message: OpchanMessage): boolean { verifyMessage(message: OpchanMessage): boolean {
// Check for required signature fields
if (!message.signature || !message.browserPubKey) { if (!message.signature || !message.browserPubKey) {
console.warn('Message is missing signature information'); console.warn('Message is missing signature information', message.id);
return false;
}
// Check if message is too old (anti-replay protection)
if (this.isMessageTooOld(message)) {
console.warn(`Message ${message.id} is too old (timestamp: ${message.timestamp})`);
return false; return false;
} }
// Reconstruct the original signed content
const signedContent = JSON.stringify({ const signedContent = JSON.stringify({
...message, ...message,
signature: undefined, signature: undefined,
browserPubKey: undefined browserPubKey: undefined
}); });
return this.keyDelegation.verifySignature( // Verify the signature
const isValid = this.keyDelegation.verifySignature(
signedContent, signedContent,
message.signature, message.signature,
message.browserPubKey message.browserPubKey
); );
if (!isValid) {
console.warn(`Invalid signature for message ${message.id}`);
}
return isValid;
}
/**
* Checks if a message's timestamp is older than the maximum allowed age
*/
private isMessageTooOld(message: OpchanMessage): boolean {
if (!message.timestamp) return true;
const currentTime = Date.now();
const messageAge = currentTime - message.timestamp;
return messageAge > MAX_MESSAGE_AGE;
} }
} }

View File

@ -17,6 +17,8 @@ export interface Cell {
name: string; name: string;
description: string; description: string;
icon: string; icon: string;
signature?: string; // Message signature
browserPubKey?: string; // Public key that signed the message
} }
export interface Post { export interface Post {