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 {
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",
});

View File

@ -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[]
);
};

View File

@ -33,11 +33,24 @@ async function signAndSendMessage<T extends PostMessage | CommentMessage | VoteM
signedMessage = await messageSigning.signMessage(message);
if (!signedMessage) {
toast({
title: "Key Delegation Required",
description: "Please delegate a signing key for better UX.",
variant: "destructive",
});
// Check if delegation exists but is expired
const isDelegationExpired = messageSigning['keyDelegation'] &&
!messageSigning['keyDelegation'].isDelegationValid() &&
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;
}
} else {
@ -48,9 +61,20 @@ async function signAndSendMessage<T extends PostMessage | CommentMessage | VoteM
return signedMessage;
} catch (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({
title: "Message Error",
description: "Failed to sign and send message. Please try again.",
description: errorMessage,
variant: "destructive",
});
return null;

View File

@ -1,26 +1,55 @@
import { Cell, Post, Comment } from '@/types';
import { CellMessage, CommentMessage, PostMessage } from '@/lib/waku/types';
import { Cell, Post, Comment, OpchanMessage } from '@/types';
import { CellMessage, CommentMessage, MessageType, PostMessage, VoteMessage } from '@/lib/waku/types';
import messageManager from '@/lib/waku';
// Type for the verification function
type VerifyFunction = (message: OpchanMessage) => 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;
};
// Transform comments
const comments = Object.values(messageManager.messageCache.comments).map(transformComment);
// 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 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 };
};

View File

@ -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<T extends OpchanMessage>(message: T): T | null {
signMessage<T extends OpchanMessage>(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;
}
}

View File

@ -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 {