From 8bfbb41bd87c6e0482109af81abdaae8471b0209 Mon Sep 17 00:00:00 2001 From: Danish Arora Date: Sun, 27 Apr 2025 15:54:24 +0530 Subject: [PATCH] feat: key delegation --- src/contexts/AuthContext.tsx | 28 +++-- src/contexts/ForumContext.tsx | 31 ++--- src/contexts/forum/actions.ts | 36 +++++- src/contexts/forum/transformers.ts | 109 ++++++++++++++---- .../identity/signatures/message-signing.ts | 35 +++++- src/types/index.ts | 2 + 6 files changed, 191 insertions(+), 50 deletions(-) diff --git a/src/contexts/AuthContext.tsx b/src/contexts/AuthContext.tsx index 5ad524f..f35ec9d 100644 --- a/src/contexts/AuthContext.tsx +++ b/src/contexts/AuthContext.tsx @@ -218,8 +218,8 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { try { toast({ - title: "Delegating Key", - description: "Generating a browser keypair for quick signing...", + title: "Starting Key Delegation", + description: "This will let you post, comment, and vote without approving each action for 24 hours.", }); // Generate a browser keypair @@ -236,9 +236,13 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { expiryTimestamp ); + // Format date for user-friendly display + const expiryDate = new Date(expiryTimestamp); + const formattedExpiry = expiryDate.toLocaleString(); + toast({ - title: "Signing Required", - description: "Please sign the delegation message with your wallet...", + title: "Wallet Signature Required", + 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); @@ -264,8 +268,8 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { localStorage.setItem('opchan-user', JSON.stringify(updatedUser)); toast({ - title: "Key Delegated", - description: `You won't need to sign every action for the next ${expiryHours} hours.`, + title: "Key Delegation Successful", + description: `You can now interact with the forum without additional wallet approvals until ${formattedExpiry}.`, }); return true; @@ -273,12 +277,20 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { console.error("Error delegating key:", error); let errorMessage = "Failed to delegate key. Please try again."; + 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({ - title: "Delegation Error", + title: "Delegation Failed", description: errorMessage, variant: "destructive", }); diff --git a/src/contexts/ForumContext.tsx b/src/contexts/ForumContext.tsx index fe3b578..00d2970 100644 --- a/src/contexts/ForumContext.tsx +++ b/src/contexts/ForumContext.tsx @@ -1,5 +1,5 @@ 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 { useAuth } from '@/contexts/AuthContext'; import { @@ -61,25 +61,30 @@ export function ForumProvider({ children }: { children: React.ReactNode }) { // Transform message cache data to the expected types 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( - Object.values(messageManager.messageCache.cells).map(cell => - transformCell(cell) - ) + Object.values(messageManager.messageCache.cells) + .map(cell => transformCell(cell, verifyFn)) + .filter(cell => cell !== null) as Cell[] ); - // Transform posts + // Transform posts with verification setPosts( - Object.values(messageManager.messageCache.posts).map(post => - transformPost(post) - ) + Object.values(messageManager.messageCache.posts) + .map(post => transformPost(post, verifyFn)) + .filter(post => post !== null) as Post[] ); - // Transform comments + // Transform comments with verification setComments( - Object.values(messageManager.messageCache.comments).map(comment => - transformComment(comment) - ) + Object.values(messageManager.messageCache.comments) + .map(comment => transformComment(comment, verifyFn)) + .filter(comment => comment !== null) as Comment[] ); }; diff --git a/src/contexts/forum/actions.ts b/src/contexts/forum/actions.ts index 41b1c85..661b164 100644 --- a/src/contexts/forum/actions.ts +++ b/src/contexts/forum/actions.ts @@ -33,11 +33,24 @@ async function signAndSendMessage boolean; + // 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 { id: cellMessage.id, name: cellMessage.name, 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 -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 const votes = Object.values(messageManager.messageCache.votes).filter( vote => vote.targetId === postMessage.id ); - const upvotes = votes.filter(vote => vote.value === 1); - const downvotes = votes.filter(vote => vote.value === -1); + // Only include verified votes if verification function is provided + 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 { id: postMessage.id, @@ -30,19 +59,36 @@ export const transformPost = (postMessage: PostMessage): Post => { content: postMessage.content, timestamp: postMessage.timestamp, 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 -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 const votes = Object.values(messageManager.messageCache.votes).filter( vote => vote.targetId === commentMessage.id ); - const upvotes = votes.filter(vote => vote.value === 1); - const downvotes = votes.filter(vote => vote.value === -1); + // Only include verified votes if verification function is provided + 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 { id: commentMessage.id, @@ -51,20 +97,43 @@ export const transformComment = (commentMessage: CommentMessage): Comment => { content: commentMessage.content, timestamp: commentMessage.timestamp, 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 -export const getDataFromCache = () => { - // Transform cells - const cells = Object.values(messageManager.messageCache.cells).map(transformCell); +// Helper function to transform VoteMessage (new) +export const transformVote = ( + voteMessage: VoteMessage, + 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 - const posts = Object.values(messageManager.messageCache.posts).map(transformPost); + return voteMessage; +}; + +// 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 - const comments = Object.values(messageManager.messageCache.comments).map(transformComment); + // Transform posts with verification + 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 }; }; \ No newline at end of file diff --git a/src/lib/identity/signatures/message-signing.ts b/src/lib/identity/signatures/message-signing.ts index 0a40932..4a612a4 100644 --- a/src/lib/identity/signatures/message-signing.ts +++ b/src/lib/identity/signatures/message-signing.ts @@ -1,6 +1,8 @@ import { OpchanMessage } from '@/types'; 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 { private keyDelegation: KeyDelegation; @@ -9,7 +11,7 @@ export class MessageSigning { this.keyDelegation = keyDelegation; } - signMessage(message: T): T | null { + signMessage(message: T): T | null { if (!this.keyDelegation.isDelegationValid()) { console.error('No valid key delegation found. Cannot sign message.'); return null; @@ -35,21 +37,48 @@ export class MessageSigning { } verifyMessage(message: OpchanMessage): boolean { + // Check for required signature fields 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; } + // Reconstruct the original signed content const signedContent = JSON.stringify({ ...message, signature: undefined, browserPubKey: undefined }); - return this.keyDelegation.verifySignature( + // Verify the signature + const isValid = this.keyDelegation.verifySignature( signedContent, message.signature, 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; } } \ No newline at end of file diff --git a/src/types/index.ts b/src/types/index.ts index 70aabfe..c1bb9a9 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -17,6 +17,8 @@ export interface Cell { name: string; description: string; icon: string; + signature?: string; // Message signature + browserPubKey?: string; // Public key that signed the message } export interface Post {