269 lines
7.5 KiB
TypeScript
Raw Normal View History

import * as ed from '@noble/ed25519';
2025-08-05 11:42:08 +05:30
import { sha512 } from '@noble/hashes/sha512';
import { bytesToHex, hexToBytes } from '@/lib/utils';
2025-08-28 18:44:35 +05:30
import { OpchanMessage } from '@/types/forum';
2025-08-30 18:34:50 +05:30
import { UnsignedMessage } from '@/types/waku';
import { DelegationDuration, DelegationInfo, DelegationStatus } from './types';
import { DelegationStorage } from './storage';
2025-08-28 18:44:35 +05:30
// Set up ed25519 with sha512
2025-08-05 11:42:08 +05:30
ed.etc.sha512Sync = (...m) => sha512(ed.etc.concatBytes(...m));
export class DelegationManager {
// Duration options in hours
private static readonly DURATION_HOURS = {
2025-08-30 18:34:50 +05:30
'7days': 24 * 7, // 168 hours
'30days': 24 * 30, // 720 hours
} as const;
2025-08-28 18:44:35 +05:30
/**
* Get the number of hours for a given duration
*/
static getDurationHours(duration: DelegationDuration): number {
return DelegationManager.DURATION_HOURS[duration];
}
2025-08-28 18:44:35 +05:30
// ============================================================================
// KEYPAIR GENERATION
// ============================================================================
/**
* Generate a new browser-based keypair for signing messages
*/
generateKeypair(): { publicKey: string; privateKey: string } {
const privateKey = ed.utils.randomPrivateKey();
const privateKeyHex = bytesToHex(privateKey);
2025-08-30 18:34:50 +05:30
const publicKey = ed.getPublicKey(privateKey);
const publicKeyHex = bytesToHex(publicKey);
2025-08-30 18:34:50 +05:30
return {
privateKey: privateKeyHex,
2025-08-30 18:34:50 +05:30
publicKey: publicKeyHex,
};
}
2025-08-28 18:44:35 +05:30
/**
* Create a delegation message to be signed by the wallet
*/
createDelegationMessage(
browserPublicKey: string,
2025-08-06 17:21:56 +05:30
walletAddress: string,
expiryTimestamp: number
): string {
2025-08-06 17:21:56 +05:30
return `I, ${walletAddress}, delegate authority to this pubkey: ${browserPublicKey} until ${expiryTimestamp}`;
}
2025-08-06 17:21:56 +05:30
2025-08-28 18:44:35 +05:30
// ============================================================================
// DELEGATION LIFECYCLE
2025-08-28 18:44:35 +05:30
// ============================================================================
/**
* Create and store a delegation
*/
createDelegation(
2025-08-06 17:21:56 +05:30
walletAddress: string,
signature: string,
browserPublicKey: string,
browserPrivateKey: string,
duration: DelegationDuration = '7days',
2025-08-06 17:21:56 +05:30
walletType: 'bitcoin' | 'ethereum'
2025-08-28 18:44:35 +05:30
): void {
const expiryHours = DelegationManager.getDurationHours(duration);
2025-08-30 18:34:50 +05:30
const expiryTimestamp = Date.now() + expiryHours * 60 * 60 * 1000;
2025-08-28 18:44:35 +05:30
const delegationInfo: DelegationInfo = {
signature,
expiryTimestamp,
browserPublicKey,
browserPrivateKey,
2025-08-06 17:21:56 +05:30
walletAddress,
2025-08-30 18:34:50 +05:30
walletType,
};
2025-08-28 18:44:35 +05:30
DelegationStorage.store(delegationInfo);
}
2025-08-28 18:44:35 +05:30
/**
* Check if a delegation is valid
*/
2025-08-30 18:34:50 +05:30
isDelegationValid(
currentAddress?: string,
currentWalletType?: 'bitcoin' | 'ethereum'
): boolean {
const delegation = DelegationStorage.retrieve();
if (!delegation) return false;
2025-08-30 18:34:50 +05:30
2025-08-06 17:21:56 +05:30
// Check if delegation has expired
const now = Date.now();
2025-08-06 17:21:56 +05:30
if (now >= delegation.expiryTimestamp) return false;
2025-08-30 18:34:50 +05:30
2025-08-06 17:21:56 +05:30
// If a current address is provided, validate it matches the delegation
if (currentAddress && delegation.walletAddress !== currentAddress) {
return false;
}
2025-08-30 18:34:50 +05:30
2025-08-06 17:21:56 +05:30
// If a current wallet type is provided, validate it matches the delegation
if (currentWalletType && delegation.walletType !== currentWalletType) {
return false;
}
2025-08-30 18:34:50 +05:30
2025-08-06 17:21:56 +05:30
return true;
}
2025-08-28 18:44:35 +05:30
/**
* Get the time remaining on the current delegation in milliseconds
2025-08-28 18:44:35 +05:30
*/
getDelegationTimeRemaining(): number {
const delegation = DelegationStorage.retrieve();
2025-08-28 18:44:35 +05:30
if (!delegation) return 0;
2025-08-30 18:34:50 +05:30
2025-08-28 18:44:35 +05:30
const now = Date.now();
return Math.max(0, delegation.expiryTimestamp - now);
}
/**
* Get the browser public key from the current delegation
2025-08-28 18:44:35 +05:30
*/
getBrowserPublicKey(): string | null {
const delegation = DelegationStorage.retrieve();
2025-08-28 18:44:35 +05:30
if (!delegation) return null;
return delegation.browserPublicKey;
}
/**
* Get delegation status
*/
getDelegationStatus(
currentAddress?: string,
currentWalletType?: 'bitcoin' | 'ethereum'
): DelegationStatus {
const hasDelegation = this.getBrowserPublicKey() !== null;
const isValid = this.isDelegationValid(currentAddress, currentWalletType);
const timeRemaining = this.getDelegationTimeRemaining();
return {
hasDelegation,
isValid,
timeRemaining: timeRemaining > 0 ? timeRemaining : undefined,
};
}
/**
* Clear the stored delegation
*/
2025-08-28 18:44:35 +05:30
clearDelegation(): void {
DelegationStorage.clear();
2025-08-28 18:44:35 +05:30
}
// ============================================================================
// MESSAGE SIGNING & VERIFICATION
// ============================================================================
/**
* Sign a raw string message using the browser-generated private key
2025-08-28 18:44:35 +05:30
*/
signRawMessage(message: string): string | null {
const delegation = DelegationStorage.retrieve();
if (!delegation || !this.isDelegationValid()) return null;
2025-08-30 18:34:50 +05:30
try {
const privateKeyBytes = hexToBytes(delegation.browserPrivateKey);
const messageBytes = new TextEncoder().encode(message);
2025-08-30 18:34:50 +05:30
2025-08-28 18:44:35 +05:30
const signature = ed.sign(messageBytes, privateKeyBytes);
return bytesToHex(signature);
} catch (error) {
console.error('Error signing with browser key:', error);
return null;
}
}
2025-08-28 18:44:35 +05:30
/**
* Sign an unsigned message with the delegated browser key
*/
signMessageWithDelegatedKey(message: UnsignedMessage): OpchanMessage | null {
2025-08-28 18:44:35 +05:30
if (!this.isDelegationValid()) {
console.error('No valid key delegation found. Cannot sign message.');
return null;
}
2025-08-30 18:34:50 +05:30
const delegation = DelegationStorage.retrieve();
if (!delegation) return null;
2025-08-30 18:34:50 +05:30
2025-08-28 18:44:35 +05:30
// Create the message content to sign (without signature fields)
const messageToSign = JSON.stringify({
...message,
signature: undefined,
2025-08-30 18:34:50 +05:30
browserPubKey: undefined,
2025-08-28 18:44:35 +05:30
});
2025-08-30 18:34:50 +05:30
2025-08-28 18:44:35 +05:30
const signature = this.signRawMessage(messageToSign);
if (!signature) return null;
2025-08-30 18:34:50 +05:30
2025-08-28 18:44:35 +05:30
return {
...message,
signature,
2025-08-30 18:34:50 +05:30
browserPubKey: delegation.browserPublicKey,
} as OpchanMessage;
}
2025-08-28 18:44:35 +05:30
/**
* Verify an OpchanMessage signature
*/
2025-08-28 18:44:35 +05:30
verifyMessage(message: OpchanMessage): boolean {
// Check for required signature fields
if (!message.signature || !message.browserPubKey) {
2025-08-30 18:34:50 +05:30
const messageId = message.id || `${message.type}-${message.timestamp}`;
2025-08-28 18:44:35 +05:30
console.warn('Message is missing signature information', messageId);
return false;
}
2025-08-30 18:34:50 +05:30
2025-08-28 18:44:35 +05:30
// Reconstruct the original signed content
const signedContent = JSON.stringify({
...message,
signature: undefined,
2025-08-30 18:34:50 +05:30
browserPubKey: undefined,
2025-08-28 18:44:35 +05:30
});
2025-08-30 18:34:50 +05:30
2025-08-28 18:44:35 +05:30
// Verify the signature
const isValid = this.verifyRawSignature(
signedContent,
message.signature,
message.browserPubKey
);
2025-08-30 18:34:50 +05:30
2025-08-28 18:44:35 +05:30
if (!isValid) {
2025-08-30 18:34:50 +05:30
const messageId = message.id || `${message.type}-${message.timestamp}`;
2025-08-28 18:44:35 +05:30
console.warn(`Invalid signature for message ${messageId}`);
}
2025-08-30 18:34:50 +05:30
2025-08-28 18:44:35 +05:30
return isValid;
}
2025-09-02 10:17:42 +05:30
/**
* Verify a signature made with the browser key
2025-09-02 10:17:42 +05:30
*/
private verifyRawSignature(
message: string,
signature: string,
publicKey: string
): boolean {
try {
const messageBytes = new TextEncoder().encode(message);
const signatureBytes = hexToBytes(signature);
const publicKeyBytes = hexToBytes(publicKey);
return ed.verify(signatureBytes, messageBytes, publicKeyBytes);
} catch (error) {
console.error('Error verifying signature:', error);
return false;
}
}
2025-08-28 18:44:35 +05:30
}
// Export singleton instance
export const delegationManager = new DelegationManager();
export * from './types';
export { DelegationStorage };