diff --git a/src/components/ui/delegation-step.tsx b/src/components/ui/delegation-step.tsx index 675f86e..d1821ef 100644 --- a/src/components/ui/delegation-step.tsx +++ b/src/components/ui/delegation-step.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { Button } from './button'; import { useAuth } from '@/contexts/useAuth'; import { CheckCircle, AlertCircle, Trash2 } from 'lucide-react'; -import { DelegationDuration } from '@/lib/identity/signatures/key-delegation'; +import { DelegationDuration } from '@/lib/identity/services/CryptoService'; interface DelegationStepProps { onComplete: () => void; diff --git a/src/contexts/AuthContext.tsx b/src/contexts/AuthContext.tsx index e49abd0..d14634a 100644 --- a/src/contexts/AuthContext.tsx +++ b/src/contexts/AuthContext.tsx @@ -1,10 +1,9 @@ import React, { createContext, useContext, useState, useEffect, useRef } from 'react'; import { useToast } from '@/components/ui/use-toast'; -import { User } from '@/types'; -import { AuthService, AuthResult } from '@/lib/identity/services/AuthService'; -import { OpchanMessage } from '@/types'; +import { User, OpchanMessage, EVerificationStatus } from '@/types/forum'; +import { AuthService, CryptoService, MessageService, DelegationDuration } from '@/lib/identity/services'; +import { AuthResult } from '@/lib/identity/services/AuthService'; import { useAppKitAccount, useDisconnect, modal } from '@reown/appkit/react'; -import { DelegationDuration } from '@/lib/identity/signatures/key-delegation'; export type VerificationStatus = 'unverified' | 'verified-none' | 'verified-basic' | 'verified-owner' | 'verifying'; @@ -47,8 +46,10 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { const activeAccount = isBitcoinConnected ? bitcoinAccount : ethereumAccount; const address = activeAccount.address; - // Create ref for AuthService so it persists between renders - const authServiceRef = useRef(new AuthService()); + // Create service instances that persist between renders + const cryptoServiceRef = useRef(new CryptoService()); + const authServiceRef = useRef(new AuthService(cryptoServiceRef.current)); + const messageServiceRef = useRef(new MessageService(authServiceRef.current, cryptoServiceRef.current)); // Set AppKit instance and accounts in AuthService useEffect(() => { @@ -76,7 +77,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { const newUser: User = { address, walletType: isBitcoinConnected ? 'bitcoin' : 'ethereum', - verificationStatus: 'verified-basic', // Connected wallets get basic verification by default + verificationStatus: EVerificationStatus.VERIFIED_BASIC, // Connected wallets get basic verification by default lastChecked: Date.now(), }; @@ -88,7 +89,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { ...newUser, ensOwnership: true, ensName: walletInfo.ensName, - verificationStatus: 'verified-owner' as const, + verificationStatus: EVerificationStatus.VERIFIED_OWNER, }; setCurrentUser(updatedUser); setVerificationStatus('verified-owner'); @@ -296,15 +297,15 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { }; const isDelegationValid = (): boolean => { - return authServiceRef.current.isDelegationValid(); + return cryptoServiceRef.current.isDelegationValid(); }; const delegationTimeRemaining = (): number => { - return authServiceRef.current.getDelegationTimeRemaining(); + return cryptoServiceRef.current.getDelegationTimeRemaining(); }; const clearDelegation = (): void => { - authServiceRef.current.clearDelegation(); + cryptoServiceRef.current.clearDelegation(); // Update the current user to remove delegation info if (currentUser) { @@ -329,10 +330,10 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { const messageSigning = { signMessage: async (message: OpchanMessage): Promise => { - return authServiceRef.current.signMessage(message); + return cryptoServiceRef.current.signMessage(message); }, verifyMessage: (message: OpchanMessage): boolean => { - return authServiceRef.current.verifyMessage(message); + return cryptoServiceRef.current.verifyMessage(message); } }; diff --git a/src/lib/forum/__tests__/relevance.test.ts b/src/lib/forum/__tests__/relevance.test.ts index e2a149a..a285800 100644 --- a/src/lib/forum/__tests__/relevance.test.ts +++ b/src/lib/forum/__tests__/relevance.test.ts @@ -1,5 +1,5 @@ import { RelevanceCalculator } from '../relevance'; -import { Post, Comment, User, UserVerificationStatus } from '@/types/forum'; +import { Post, Comment, User, UserVerificationStatus, EVerificationStatus } from '@/types/forum'; import { VoteMessage, MessageType } from '@/lib/waku/types'; import { expect, describe, beforeEach, it } from 'vitest'; @@ -58,7 +58,7 @@ describe('RelevanceCalculator', () => { const verifiedUser: User = { address: 'user1', walletType: 'ethereum', - verificationStatus: 'verified-owner', + verificationStatus: EVerificationStatus.VERIFIED_OWNER, ensOwnership: true, ensName: 'test.eth', lastChecked: Date.now() @@ -72,7 +72,7 @@ describe('RelevanceCalculator', () => { const verifiedUser: User = { address: 'user3', walletType: 'bitcoin', - verificationStatus: 'verified-owner', + verificationStatus: EVerificationStatus.VERIFIED_OWNER, ordinalOwnership: true, lastChecked: Date.now() }; @@ -85,7 +85,7 @@ describe('RelevanceCalculator', () => { const unverifiedUser: User = { address: 'user2', walletType: 'ethereum', - verificationStatus: 'unverified', + verificationStatus: EVerificationStatus.UNVERIFIED, ensOwnership: false, lastChecked: Date.now() }; @@ -184,7 +184,7 @@ describe('RelevanceCalculator', () => { { address: 'user1', walletType: 'ethereum', - verificationStatus: 'verified-owner', + verificationStatus: EVerificationStatus.VERIFIED_OWNER, ensOwnership: true, ensName: 'test.eth', lastChecked: Date.now() @@ -192,7 +192,7 @@ describe('RelevanceCalculator', () => { { address: 'user2', walletType: 'bitcoin', - verificationStatus: 'unverified', + verificationStatus: EVerificationStatus.UNVERIFIED, ordinalOwnership: false, lastChecked: Date.now() } diff --git a/src/lib/forum/actions.ts b/src/lib/forum/actions.ts index 16fd8f0..ffb6fc3 100644 --- a/src/lib/forum/actions.ts +++ b/src/lib/forum/actions.ts @@ -9,8 +9,7 @@ import { } from '@/lib/waku/types'; import { Cell, Comment, Post, User } from '@/types/forum'; import { transformCell, transformComment, transformPost } from './transformers'; -import { MessageService } from '@/lib/identity/services/MessageService'; -import { AuthService } from '@/lib/identity/services/AuthService'; +import { MessageService, AuthService, CryptoService } from '@/lib/identity/services'; type ToastFunction = (props: { title: string; @@ -70,8 +69,9 @@ export const createPost = async ( author: currentUser.address, }; - const messageService = new MessageService(authService!); - const result = await messageService.signAndSendMessage(postMessage); + const cryptoService = new CryptoService(); + const messageService = new MessageService(authService!, cryptoService); + const result = await messageService.sendMessage(postMessage); if (!result.success) { toast({ title: 'Post Failed', @@ -141,8 +141,9 @@ export const createComment = async ( author: currentUser.address, }; - const messageService = new MessageService(authService!); - const result = await messageService.signAndSendMessage(commentMessage); + const cryptoService = new CryptoService(); + const messageService = new MessageService(authService!, cryptoService); + const result = await messageService.sendMessage(commentMessage); if (!result.success) { toast({ title: 'Comment Failed', @@ -199,8 +200,9 @@ export const createCell = async ( author: currentUser.address, }; - const messageService = new MessageService(authService!); - const result = await messageService.signAndSendMessage(cellMessage); + const cryptoService = new CryptoService(); + const messageService = new MessageService(authService!, cryptoService); + const result = await messageService.sendMessage(cellMessage); if (!result.success) { toast({ title: 'Cell Failed', @@ -275,8 +277,9 @@ export const vote = async ( author: currentUser.address, }; - const messageService = new MessageService(authService!); - const result = await messageService.signAndSendMessage(voteMessage); + const cryptoService = new CryptoService(); + const messageService = new MessageService(authService!, cryptoService); + const result = await messageService.sendMessage(voteMessage); if (!result.success) { toast({ title: 'Vote Failed', @@ -340,8 +343,9 @@ export const moderatePost = async ( timestamp: Date.now(), author: currentUser.address, }; - const messageService = new MessageService(authService!); - const result = await messageService.signAndSendMessage(modMsg); + const cryptoService = new CryptoService(); + const messageService = new MessageService(authService!, cryptoService); + const result = await messageService.sendMessage(modMsg); if (!result.success) { toast({ title: 'Moderation Failed', description: result.error || 'Failed to moderate post. Please try again.', variant: 'destructive' }); return false; @@ -389,8 +393,9 @@ export const moderateComment = async ( timestamp: Date.now(), author: currentUser.address, }; - const messageService = new MessageService(authService!); - const result = await messageService.signAndSendMessage(modMsg); + const cryptoService = new CryptoService(); + const messageService = new MessageService(authService!, cryptoService); + const result = await messageService.sendMessage(modMsg); if (!result.success) { toast({ title: 'Moderation Failed', description: result.error || 'Failed to moderate comment. Please try again.', variant: 'destructive' }); return false; @@ -437,8 +442,9 @@ export const moderateUser = async ( signature: '', browserPubKey: currentUser.browserPubKey, }; - const messageService = new MessageService(authService!); - const result = await messageService.signAndSendMessage(modMsg); + const cryptoService = new CryptoService(); + const messageService = new MessageService(authService!, cryptoService); + const result = await messageService.sendMessage(modMsg); if (!result.success) { toast({ title: 'Moderation Failed', description: result.error || 'Failed to moderate user. Please try again.', variant: 'destructive' }); return false; diff --git a/src/lib/identity/services/AuthService.ts b/src/lib/identity/services/AuthService.ts index 45a18e6..54d1c6e 100644 --- a/src/lib/identity/services/AuthService.ts +++ b/src/lib/identity/services/AuthService.ts @@ -1,11 +1,9 @@ -import { User } from '@/types'; import { WalletService } from '../wallets/index'; import { UseAppKitAccountReturn } from '@reown/appkit/react'; import { AppKit } from '@reown/appkit'; import { OrdinalAPI } from '../ordinal'; -import { MessageSigning } from '../signatures/message-signing'; -import { KeyDelegation, DelegationDuration } from '../signatures/key-delegation'; -import { OpchanMessage } from '@/types'; +import { CryptoService, DelegationDuration } from './CryptoService'; +import { EVerificationStatus, User } from '@/types/forum'; export interface AuthResult { success: boolean; @@ -13,17 +11,37 @@ export interface AuthResult { error?: string; } -export class AuthService { +export interface AuthServiceInterface { + // Wallet operations + setAccounts(bitcoinAccount: UseAppKitAccountReturn, ethereumAccount: UseAppKitAccountReturn): void; + setAppKit(appKit: AppKit): void; + connectWallet(): Promise; + disconnectWallet(): Promise; + + // Verification + verifyOwnership(user: User): Promise; + + // Delegation setup + delegateKey(user: User, duration?: DelegationDuration): Promise; + + // User persistence + loadStoredUser(): User | null; + saveUser(user: User): void; + clearStoredUser(): void; + + // Wallet info + getWalletInfo(): Promise; +} + +export class AuthService implements AuthServiceInterface { private walletService: WalletService; private ordinalApi: OrdinalAPI; - private messageSigning: MessageSigning; - private keyDelegation: KeyDelegation; + private cryptoService: CryptoService; - constructor() { + constructor(cryptoService: CryptoService) { this.walletService = new WalletService(); this.ordinalApi = new OrdinalAPI(); - this.keyDelegation = new KeyDelegation(); - this.messageSigning = new MessageSigning(this.keyDelegation); + this.cryptoService = cryptoService; } /** @@ -98,7 +116,7 @@ export class AuthService { const user: User = { address: address, walletType: walletType, - verificationStatus: 'unverified', + verificationStatus: EVerificationStatus.UNVERIFIED, lastChecked: Date.now(), }; @@ -132,7 +150,7 @@ export class AuthService { */ async disconnectWallet(): Promise { // Clear any existing delegations when disconnecting - this.keyDelegation.clearDelegation(); + this.cryptoService.clearDelegation(); this.walletService.clearDelegation('bitcoin'); this.walletService.clearDelegation('ethereum'); @@ -140,13 +158,6 @@ export class AuthService { this.clearStoredUser(); } - /** - * Clear delegation for current wallet - */ - clearDelegation(): void { - this.keyDelegation.clearDelegation(); - } - /** * Verify ordinal ownership for Bitcoin users or ENS ownership for Ethereum users */ @@ -182,7 +193,7 @@ export class AuthService { const updatedUser = { ...user, ordinalOwnership: hasOperators, - verificationStatus: hasOperators ? 'verified-owner' : 'verified-basic', + verificationStatus: hasOperators ? EVerificationStatus.VERIFIED_OWNER : EVerificationStatus.VERIFIED_BASIC, lastChecked: Date.now(), }; @@ -207,7 +218,7 @@ export class AuthService { ...user, ensOwnership: hasENS, ensName: ensName, - verificationStatus: hasENS ? 'verified-owner' : 'verified-basic', + verificationStatus: hasENS ? EVerificationStatus.VERIFIED_OWNER : EVerificationStatus.VERIFIED_BASIC, lastChecked: Date.now(), }; @@ -223,7 +234,7 @@ export class AuthService { ...user, ensOwnership: false, ensName: undefined, - verificationStatus: 'verified-basic', + verificationStatus: EVerificationStatus.VERIFIED_BASIC, lastChecked: Date.now(), }; @@ -262,7 +273,7 @@ export class AuthService { const delegationStatus = this.walletService.getDelegationStatus(walletType); // Get the actual browser public key from the delegation - const browserPublicKey = this.keyDelegation.getBrowserPublicKey(); + const browserPublicKey = this.cryptoService.getBrowserPublicKey(); const updatedUser = { ...user, @@ -283,44 +294,6 @@ export class AuthService { } } - /** - * Sign a message using delegated key - */ - async signMessage(message: OpchanMessage): Promise { - return this.messageSigning.signMessage(message); - } - - /** - * Verify a message signature - */ - verifyMessage(message: OpchanMessage): boolean { - return this.messageSigning.verifyMessage(message); - } - - /** - * Check if delegation is valid - */ - isDelegationValid(): boolean { - // Only check the currently connected wallet type - const activeWalletType = this.getActiveWalletType(); - if (!activeWalletType) return false; - - const status = this.walletService.getDelegationStatus(activeWalletType); - return status.isValid; - } - - /** - * Get delegation time remaining - */ - getDelegationTimeRemaining(): number { - // Only check the currently connected wallet type - const activeWalletType = this.getActiveWalletType(); - if (!activeWalletType) return 0; - - const status = this.walletService.getDelegationStatus(activeWalletType); - return status.timeRemaining || 0; - } - /** * Get current wallet info */ diff --git a/src/lib/identity/signatures/key-delegation.ts b/src/lib/identity/services/CryptoService.ts similarity index 50% rename from src/lib/identity/signatures/key-delegation.ts rename to src/lib/identity/services/CryptoService.ts index 0ec916c..0cf3dd6 100644 --- a/src/lib/identity/signatures/key-delegation.ts +++ b/src/lib/identity/services/CryptoService.ts @@ -1,22 +1,57 @@ /** - * Key delegation for Bitcoin wallets + * CryptoService - Unified cryptographic operations * - * This module handles the creation of browser-based keypairs and - * delegation of signing authority from Bitcoin wallets to these keypairs. + * Combines key delegation and message signing functionality into a single, + * cohesive service focused on all cryptographic operations. */ import * as ed from '@noble/ed25519'; import { sha512 } from '@noble/hashes/sha512'; import { bytesToHex, hexToBytes } from '@/lib/utils'; import { LOCAL_STORAGE_KEYS } from '@/lib/waku/constants'; -import { DelegationInfo } from './types'; +import { OpchanMessage } from '@/types/forum'; + +export interface DelegationSignature { + signature: string; // Signature from wallet + expiryTimestamp: number; // When this delegation expires + browserPublicKey: string; // Browser-generated public key that was delegated to + walletAddress: string; // Wallet address that signed the delegation + walletType: 'bitcoin' | 'ethereum'; // Type of wallet that created the delegation + } + + export interface DelegationInfo extends DelegationSignature { + browserPrivateKey: string; + } ed.etc.sha512Sync = (...m) => sha512(ed.etc.concatBytes(...m)); export type DelegationDuration = '7days' | '30days'; -export class KeyDelegation { - private static readonly DEFAULT_EXPIRY_HOURS = 24; +export interface CryptoServiceInterface { + // Delegation management + createDelegation( + walletAddress: string, + signature: string, + browserPublicKey: string, + browserPrivateKey: string, + duration: DelegationDuration, + walletType: 'bitcoin' | 'ethereum' + ): void; + isDelegationValid(currentAddress?: string, currentWalletType?: 'bitcoin' | 'ethereum'): boolean; + getDelegationTimeRemaining(): number; + getBrowserPublicKey(): string | null; + clearDelegation(): void; + + // Keypair generation + generateKeypair(): { publicKey: string; privateKey: string }; + createDelegationMessage(browserPublicKey: string, walletAddress: string, expiryTimestamp: number): string; + + // Message operations + signMessage(message: T): T | null; + verifyMessage(message: OpchanMessage): boolean; +} + +export class CryptoService implements CryptoServiceInterface { private static readonly STORAGE_KEY = LOCAL_STORAGE_KEYS.KEY_DELEGATION; // Duration options in hours @@ -24,24 +59,27 @@ export class KeyDelegation { '7days': 24 * 7, // 168 hours '30days': 24 * 30 // 720 hours } as const; - + /** * Get the number of hours for a given duration */ static getDurationHours(duration: DelegationDuration): number { - return KeyDelegation.DURATION_HOURS[duration]; + return CryptoService.DURATION_HOURS[duration]; } - + /** * Get available duration options */ static getAvailableDurations(): DelegationDuration[] { - return Object.keys(KeyDelegation.DURATION_HOURS) as DelegationDuration[]; + return Object.keys(CryptoService.DURATION_HOURS) as DelegationDuration[]; } - + + // ============================================================================ + // KEYPAIR GENERATION + // ============================================================================ + /** * Generates a new browser-based keypair for signing messages - * @returns Promise with keypair object containing hex-encoded public and private keys */ generateKeypair(): { publicKey: string; privateKey: string } { const privateKey = ed.utils.randomPrivateKey(); @@ -55,13 +93,9 @@ export class KeyDelegation { publicKey: publicKeyHex }; } - + /** * Creates a delegation message to be signed by the wallet - * @param browserPublicKey The browser-generated public key - * @param walletAddress The user's wallet address - * @param expiryTimestamp When the delegation will expire - * @returns The message to be signed */ createDelegationMessage( browserPublicKey: string, @@ -71,15 +105,12 @@ export class KeyDelegation { return `I, ${walletAddress}, delegate authority to this pubkey: ${browserPublicKey} until ${expiryTimestamp}`; } + // ============================================================================ + // DELEGATION MANAGEMENT + // ============================================================================ + /** - * Creates a delegation object from the signed message - * @param walletAddress The wallet address that signed the delegation - * @param signature The signature from the wallet - * @param browserPublicKey The browser-generated public key - * @param browserPrivateKey The browser-generated private key - * @param duration The duration of the delegation ('1week' or '30days') - * @param walletType The type of wallet (bitcoin or ethereum) - * @returns DelegationInfo object + * Creates and stores a delegation */ createDelegation( walletAddress: string, @@ -88,11 +119,11 @@ export class KeyDelegation { browserPrivateKey: string, duration: DelegationDuration = '7days', walletType: 'bitcoin' | 'ethereum' - ): DelegationInfo { - const expiryHours = KeyDelegation.getDurationHours(duration); + ): void { + const expiryHours = CryptoService.getDurationHours(duration); const expiryTimestamp = Date.now() + (expiryHours * 60 * 60 * 1000); - return { + const delegationInfo: DelegationInfo = { signature, expiryTimestamp, browserPublicKey, @@ -100,22 +131,15 @@ export class KeyDelegation { walletAddress, walletType }; + + localStorage.setItem(CryptoService.STORAGE_KEY, JSON.stringify(delegationInfo)); } - - /** - * Stores delegation information in local storage - * @param delegationInfo The delegation information to store - */ - storeDelegation(delegationInfo: DelegationInfo): void { - localStorage.setItem(KeyDelegation.STORAGE_KEY, JSON.stringify(delegationInfo)); - } - + /** * Retrieves delegation information from local storage - * @returns The stored delegation information or null if not found */ - retrieveDelegation(): DelegationInfo | null { - const delegationJson = localStorage.getItem(KeyDelegation.STORAGE_KEY); + private retrieveDelegation(): DelegationInfo | null { + const delegationJson = localStorage.getItem(CryptoService.STORAGE_KEY); if (!delegationJson) return null; try { @@ -125,12 +149,9 @@ export class KeyDelegation { return null; } } - + /** - * Checks if a delegation is valid (exists, not expired, and matches current wallet) - * @param currentAddress Optional current wallet address to validate against - * @param currentWalletType Optional current wallet type to validate against - * @returns boolean indicating if the delegation is valid + * Checks if a delegation is valid */ isDelegationValid(currentAddress?: string, currentWalletType?: 'bitcoin' | 'ethereum'): boolean { const delegation = this.retrieveDelegation(); @@ -152,13 +173,42 @@ export class KeyDelegation { return true; } - + /** - * Signs a message using the browser-generated private key - * @param message The message to sign - * @returns Promise resolving to the signature as a hex string, or null if no valid delegation + * Gets the time remaining on the current delegation */ - signMessage(message: string): string | null { + getDelegationTimeRemaining(): number { + const delegation = this.retrieveDelegation(); + if (!delegation) return 0; + + const now = Date.now(); + return Math.max(0, delegation.expiryTimestamp - now); + } + + /** + * Gets the browser public key from the current delegation + */ + getBrowserPublicKey(): string | null { + const delegation = this.retrieveDelegation(); + if (!delegation) return null; + return delegation.browserPublicKey; + } + + /** + * Clears the stored delegation + */ + clearDelegation(): void { + localStorage.removeItem(CryptoService.STORAGE_KEY); + } + + // ============================================================================ + // MESSAGE SIGNING & VERIFICATION + // ============================================================================ + + /** + * Signs a raw string message using the browser-generated private key + */ + signRawMessage(message: string): string | null { const delegation = this.retrieveDelegation(); if (!delegation || !this.isDelegationValid()) return null; @@ -166,22 +216,18 @@ export class KeyDelegation { const privateKeyBytes = hexToBytes(delegation.browserPrivateKey); const messageBytes = new TextEncoder().encode(message); - const signature = ed.sign(messageBytes, privateKeyBytes); + const signature = ed.sign(messageBytes, privateKeyBytes); return bytesToHex(signature); } catch (error) { console.error('Error signing with browser key:', error); return null; } } - + /** * Verifies a signature made with the browser key - * @param message The original message - * @param signature The signature to verify (hex string) - * @param publicKey The public key to verify against (hex string) - * @returns Promise resolving to a boolean indicating if the signature is valid */ - verifySignature( + private verifyRawSignature( message: string, signature: string, publicKey: string @@ -197,43 +243,66 @@ export class KeyDelegation { return false; } } - + /** - * Gets the current delegation's Bitcoin address, if available - * @returns The Bitcoin address or null if no valid delegation exists + * Signs an OpchanMessage with the delegated browser key */ - getDelegatingAddress(): string | null { - const delegation = this.retrieveDelegation(); - if (!delegation || !this.isDelegationValid()) return null; - return delegation.walletAddress; - } - - /** - * Gets the browser public key from the current delegation - * @returns The browser public key or null if no valid delegation exists - */ - getBrowserPublicKey(): string | null { + signMessage(message: T): T | null { + if (!this.isDelegationValid()) { + console.error('No valid key delegation found. Cannot sign message.'); + return null; + } + const delegation = this.retrieveDelegation(); if (!delegation) return null; - return delegation.browserPublicKey; - } - - /** - * Clears the stored delegation - */ - clearDelegation(): void { - localStorage.removeItem(KeyDelegation.STORAGE_KEY); - } - - /** - * Gets the time remaining on the current delegation - * @returns Time remaining in milliseconds, or 0 if expired/no delegation - */ - getDelegationTimeRemaining(): number { - const delegation = this.retrieveDelegation(); - if (!delegation) return 0; - const now = Date.now(); - return Math.max(0, delegation.expiryTimestamp - now); + // Create the message content to sign (without signature fields) + const messageToSign = JSON.stringify({ + ...message, + signature: undefined, + browserPubKey: undefined + }); + + const signature = this.signRawMessage(messageToSign); + if (!signature) return null; + + return { + ...message, + signature, + browserPubKey: delegation.browserPublicKey + }; } -} \ No newline at end of file + + /** + * Verifies an OpchanMessage signature + */ + verifyMessage(message: OpchanMessage): boolean { + // Check for required signature fields + if (!message.signature || !message.browserPubKey) { + const messageId = 'id' in message ? message.id : `${message.type}-${message.timestamp}`; + console.warn('Message is missing signature information', messageId); + return false; + } + + // Reconstruct the original signed content + const signedContent = JSON.stringify({ + ...message, + signature: undefined, + browserPubKey: undefined + }); + + // Verify the signature + const isValid = this.verifyRawSignature( + signedContent, + message.signature, + message.browserPubKey + ); + + if (!isValid) { + const messageId = 'id' in message ? message.id : `${message.type}-${message.timestamp}`; + console.warn(`Invalid signature for message ${messageId}`); + } + + return isValid; + } +} diff --git a/src/lib/identity/services/MessageService.ts b/src/lib/identity/services/MessageService.ts index 1f0e956..3850cfe 100644 --- a/src/lib/identity/services/MessageService.ts +++ b/src/lib/identity/services/MessageService.ts @@ -1,5 +1,6 @@ -import { OpchanMessage } from '@/types'; +import { OpchanMessage } from '@/types/forum'; import { AuthService } from './AuthService'; +import { CryptoService } from './CryptoService'; import messageManager from '@/lib/waku'; export interface MessageResult { @@ -8,23 +9,30 @@ export interface MessageResult { error?: string; } -export class MessageService { - private authService: AuthService; +export interface MessageServiceInterface { + sendMessage(message: OpchanMessage): Promise; + verifyMessage(message: OpchanMessage): boolean; +} - constructor(authService: AuthService) { +export class MessageService implements MessageServiceInterface { + private authService: AuthService; + private cryptoService: CryptoService; + + constructor(authService: AuthService, cryptoService: CryptoService) { this.authService = authService; + this.cryptoService = cryptoService; } /** * Sign and send a message to the Waku network */ - async signAndSendMessage(message: OpchanMessage): Promise { + async sendMessage(message: OpchanMessage): Promise { try { - const signedMessage = await this.authService.signMessage(message); + const signedMessage = this.cryptoService.signMessage(message); if (!signedMessage) { // Check if delegation exists but is expired - const isDelegationExpired = this.authService.isDelegationValid() === false; + const isDelegationExpired = this.cryptoService.isDelegationValid() === false; return { success: false, @@ -66,6 +74,6 @@ export class MessageService { * Verify a message signature */ verifyMessage(message: OpchanMessage): boolean { - return this.authService.verifyMessage(message); + return this.cryptoService.verifyMessage(message); } -} \ No newline at end of file +} \ No newline at end of file diff --git a/src/lib/identity/services/index.ts b/src/lib/identity/services/index.ts index fde9bbf..8a49d31 100644 --- a/src/lib/identity/services/index.ts +++ b/src/lib/identity/services/index.ts @@ -1,2 +1,3 @@ -export { AuthService } from './AuthService'; -export { MessageService } from './MessageService'; \ No newline at end of file +export { AuthService, type AuthServiceInterface } from './AuthService'; +export { MessageService, type MessageServiceInterface } from './MessageService'; +export { CryptoService, type CryptoServiceInterface, type DelegationDuration } from './CryptoService'; \ No newline at end of file diff --git a/src/lib/identity/signatures/message-signing.ts b/src/lib/identity/signatures/message-signing.ts deleted file mode 100644 index 8c59ff9..0000000 --- a/src/lib/identity/signatures/message-signing.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { OpchanMessage } from '@/types'; -import { KeyDelegation } from './key-delegation'; - - - -export class MessageSigning { - private keyDelegation: KeyDelegation; - - constructor(keyDelegation: KeyDelegation) { - this.keyDelegation = keyDelegation; - } - - signMessage(message: T): T | null { - if (!this.keyDelegation.isDelegationValid()) { - console.error('No valid key delegation found. Cannot sign message.'); - return null; - } - - const delegation = this.keyDelegation.retrieveDelegation(); - if (!delegation) return null; - - const messageToSign = JSON.stringify({ - ...message, - signature: undefined, - browserPubKey: undefined - }); - - const signature = this.keyDelegation.signMessage(messageToSign); - if (!signature) return null; - - return { - ...message, - signature, - browserPubKey: delegation.browserPublicKey - }; - } - - verifyMessage(message: OpchanMessage): boolean { - // Check for required signature fields - if (!message.signature || !message.browserPubKey) { - const messageId = 'id' in message ? message.id : `${message.type}-${message.timestamp}`; - console.warn('Message is missing signature information', messageId); - return false; - } - - - - // Reconstruct the original signed content - const signedContent = JSON.stringify({ - ...message, - signature: undefined, - browserPubKey: undefined - }); - - // Verify the signature - const isValid = this.keyDelegation.verifySignature( - signedContent, - message.signature, - message.browserPubKey - ); - - if (!isValid) { - const messageId = 'id' in message ? message.id : `${message.type}-${message.timestamp}`; - console.warn(`Invalid signature for message ${messageId}`); - } - - return isValid; - } - - -} \ No newline at end of file diff --git a/src/lib/identity/signatures/types.ts b/src/lib/identity/signatures/types.ts deleted file mode 100644 index 62fa08a..0000000 --- a/src/lib/identity/signatures/types.ts +++ /dev/null @@ -1,11 +0,0 @@ -export interface DelegationSignature { - signature: string; // Signature from wallet - expiryTimestamp: number; // When this delegation expires - browserPublicKey: string; // Browser-generated public key that was delegated to - walletAddress: string; // Wallet address that signed the delegation - walletType: 'bitcoin' | 'ethereum'; // Type of wallet that created the delegation -} - -export interface DelegationInfo extends DelegationSignature { - browserPrivateKey: string; -} \ No newline at end of file diff --git a/src/lib/identity/wallets/ReOwnWalletService.ts b/src/lib/identity/wallets/ReOwnWalletService.ts index 98c7506..3c4066e 100644 --- a/src/lib/identity/wallets/ReOwnWalletService.ts +++ b/src/lib/identity/wallets/ReOwnWalletService.ts @@ -1,5 +1,5 @@ import { UseAppKitAccountReturn } from '@reown/appkit/react'; -import { KeyDelegation, DelegationDuration } from '../signatures/key-delegation'; +import { CryptoService, DelegationDuration } from '../services/CryptoService'; import { AppKit } from '@reown/appkit'; import { getEnsName } from '@wagmi/core'; import { ChainNamespace } from '@reown/appkit-common'; @@ -14,13 +14,13 @@ export interface WalletInfo { } export class ReOwnWalletService { - private keyDelegation: KeyDelegation; + private cryptoService: CryptoService; private bitcoinAccount?: UseAppKitAccountReturn; private ethereumAccount?: UseAppKitAccountReturn; private appKit?: AppKit; constructor() { - this.keyDelegation = new KeyDelegation(); + this.cryptoService = new CryptoService(); } /** @@ -130,12 +130,12 @@ export class ReOwnWalletService { } // Generate a new browser keypair - const keypair = this.keyDelegation.generateKeypair(); + const keypair = this.cryptoService.generateKeypair(); // Create delegation message with expiry - const expiryHours = KeyDelegation.getDurationHours(duration); + const expiryHours = CryptoService.getDurationHours(duration); const expiryTimestamp = Date.now() + (expiryHours * 60 * 60 * 1000); - const delegationMessage = this.keyDelegation.createDelegationMessage( + const delegationMessage = this.cryptoService.createDelegationMessage( keypair.publicKey, account.address, expiryTimestamp @@ -147,7 +147,7 @@ export class ReOwnWalletService { const signature = await this.signMessage(messageBytes, walletType); // Create and store the delegation - const delegationInfo = this.keyDelegation.createDelegation( + this.cryptoService.createDelegation( account.address, signature, keypair.publicKey, @@ -155,8 +155,6 @@ export class ReOwnWalletService { duration, walletType ); - - this.keyDelegation.storeDelegation(delegationInfo); return true; } catch (error) { @@ -175,10 +173,10 @@ export class ReOwnWalletService { } // Check if we have a valid delegation for this specific wallet - if (this.keyDelegation.isDelegationValid(account.address, walletType)) { + if (this.cryptoService.isDelegationValid(account.address, walletType)) { // Use delegated key for signing const messageString = new TextDecoder().decode(messageBytes); - const signature = this.keyDelegation.signMessage(messageString); + const signature = this.cryptoService.signRawMessage(messageString); if (signature) { return signature; @@ -200,9 +198,9 @@ export class ReOwnWalletService { const account = this.getActiveAccount(walletType); const currentAddress = account?.address; - const hasDelegation = this.keyDelegation.retrieveDelegation() !== null; - const isValid = this.keyDelegation.isDelegationValid(currentAddress, walletType); - const timeRemaining = this.keyDelegation.getDelegationTimeRemaining(); + const hasDelegation = this.cryptoService.getBrowserPublicKey() !== null; + const isValid = this.cryptoService.isDelegationValid(currentAddress, walletType); + const timeRemaining = this.cryptoService.getDelegationTimeRemaining(); return { hasDelegation, @@ -215,7 +213,7 @@ export class ReOwnWalletService { * Clear delegation for the connected wallet */ clearDelegation(walletType: 'bitcoin' | 'ethereum'): void { - this.keyDelegation.clearDelegation(); + this.cryptoService.clearDelegation(); } /** diff --git a/src/types/forum.ts b/src/types/forum.ts index 6d5eb5b..e8b2312 100644 --- a/src/types/forum.ts +++ b/src/types/forum.ts @@ -14,7 +14,7 @@ export interface User { ensAvatar?: string; ensOwnership?: boolean; - verificationStatus: 'unverified' | 'verified-none' | 'verified-basic' | 'verified-owner' | 'verifying'; + verificationStatus: EVerificationStatus; signature?: string; lastChecked?: number; @@ -23,6 +23,14 @@ export interface User { delegationExpiry?: number; // When the delegation expires } +export enum EVerificationStatus { + UNVERIFIED = 'unverified', + VERIFIED_NONE = 'verified-none', + VERIFIED_BASIC = 'verified-basic', + VERIFIED_OWNER = 'verified-owner', + VERIFYING = 'verifying', +} + export interface Cell { id: string; name: string;