mirror of
https://github.com/logos-messaging/OpChan.git
synced 2026-01-04 05:43:10 +00:00
feat: key delegation
This commit is contained in:
parent
8859b85367
commit
8bfbb41bd8
@ -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",
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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[]
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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 };
|
||||||
};
|
};
|
||||||
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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 {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user