2025-04-24 16:30:50 +05:30
|
|
|
import * as ed from '@noble/ed25519';
|
2025-08-05 11:42:08 +05:30
|
|
|
import { sha512 } from '@noble/hashes/sha512';
|
2025-04-24 16:30:50 +05:30
|
|
|
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';
|
2025-09-02 10:48:49 +05:30
|
|
|
import { DelegationDuration, DelegationInfo, DelegationStatus } from './types';
|
|
|
|
|
import { DelegationStorage } from './storage';
|
2025-08-28 18:44:35 +05:30
|
|
|
|
2025-09-02 10:48:49 +05:30
|
|
|
// Set up ed25519 with sha512
|
2025-08-05 11:42:08 +05:30
|
|
|
ed.etc.sha512Sync = (...m) => sha512(ed.etc.concatBytes(...m));
|
|
|
|
|
|
2025-09-02 10:48:49 +05:30
|
|
|
export class DelegationManager {
|
2025-08-13 12:00:40 +05:30
|
|
|
// 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
|
2025-08-13 12:00:40 +05:30
|
|
|
} as const;
|
2025-08-28 18:44:35 +05:30
|
|
|
|
2025-08-13 12:00:40 +05:30
|
|
|
/**
|
|
|
|
|
* Get the number of hours for a given duration
|
|
|
|
|
*/
|
|
|
|
|
static getDurationHours(duration: DelegationDuration): number {
|
2025-09-02 10:48:49 +05:30
|
|
|
return DelegationManager.DURATION_HOURS[duration];
|
2025-08-13 12:00:40 +05:30
|
|
|
}
|
2025-08-28 18:44:35 +05:30
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
// KEYPAIR GENERATION
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
2025-04-24 16:30:50 +05:30
|
|
|
/**
|
2025-09-02 10:48:49 +05:30
|
|
|
* Generate a new browser-based keypair for signing messages
|
2025-04-24 16:30:50 +05:30
|
|
|
*/
|
|
|
|
|
generateKeypair(): { publicKey: string; privateKey: string } {
|
|
|
|
|
const privateKey = ed.utils.randomPrivateKey();
|
|
|
|
|
const privateKeyHex = bytesToHex(privateKey);
|
2025-08-30 18:34:50 +05:30
|
|
|
|
2025-04-24 16:30:50 +05:30
|
|
|
const publicKey = ed.getPublicKey(privateKey);
|
|
|
|
|
const publicKeyHex = bytesToHex(publicKey);
|
2025-08-30 18:34:50 +05:30
|
|
|
|
2025-04-24 16:30:50 +05:30
|
|
|
return {
|
|
|
|
|
privateKey: privateKeyHex,
|
2025-08-30 18:34:50 +05:30
|
|
|
publicKey: publicKeyHex,
|
2025-04-24 16:30:50 +05:30
|
|
|
};
|
|
|
|
|
}
|
2025-08-28 18:44:35 +05:30
|
|
|
|
2025-04-24 16:30:50 +05:30
|
|
|
/**
|
2025-09-02 10:48:49 +05:30
|
|
|
* Create a delegation message to be signed by the wallet
|
2025-04-24 16:30:50 +05:30
|
|
|
*/
|
|
|
|
|
createDelegationMessage(
|
|
|
|
|
browserPublicKey: string,
|
2025-08-06 17:21:56 +05:30
|
|
|
walletAddress: string,
|
2025-04-24 16:30:50 +05:30
|
|
|
expiryTimestamp: number
|
|
|
|
|
): string {
|
2025-08-06 17:21:56 +05:30
|
|
|
return `I, ${walletAddress}, delegate authority to this pubkey: ${browserPublicKey} until ${expiryTimestamp}`;
|
2025-04-24 16:30:50 +05:30
|
|
|
}
|
2025-08-06 17:21:56 +05:30
|
|
|
|
2025-08-28 18:44:35 +05:30
|
|
|
// ============================================================================
|
2025-09-02 10:48:49 +05:30
|
|
|
// DELEGATION LIFECYCLE
|
2025-08-28 18:44:35 +05:30
|
|
|
// ============================================================================
|
|
|
|
|
|
2025-04-24 16:30:50 +05:30
|
|
|
/**
|
2025-09-02 10:48:49 +05:30
|
|
|
* Create and store a delegation
|
2025-04-24 16:30:50 +05:30
|
|
|
*/
|
|
|
|
|
createDelegation(
|
2025-08-06 17:21:56 +05:30
|
|
|
walletAddress: string,
|
2025-04-24 16:30:50 +05:30
|
|
|
signature: string,
|
|
|
|
|
browserPublicKey: string,
|
|
|
|
|
browserPrivateKey: string,
|
2025-08-13 12:00:40 +05:30
|
|
|
duration: DelegationDuration = '7days',
|
2025-08-06 17:21:56 +05:30
|
|
|
walletType: 'bitcoin' | 'ethereum'
|
2025-08-28 18:44:35 +05:30
|
|
|
): void {
|
2025-09-02 10:48:49 +05:30
|
|
|
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 = {
|
2025-04-24 16:30:50 +05:30
|
|
|
signature,
|
|
|
|
|
expiryTimestamp,
|
|
|
|
|
browserPublicKey,
|
|
|
|
|
browserPrivateKey,
|
2025-08-06 17:21:56 +05:30
|
|
|
walletAddress,
|
2025-08-30 18:34:50 +05:30
|
|
|
walletType,
|
2025-04-24 16:30:50 +05:30
|
|
|
};
|
2025-08-28 18:44:35 +05:30
|
|
|
|
2025-09-02 10:48:49 +05:30
|
|
|
DelegationStorage.store(delegationInfo);
|
2025-04-24 16:30:50 +05:30
|
|
|
}
|
2025-08-28 18:44:35 +05:30
|
|
|
|
2025-04-24 16:30:50 +05:30
|
|
|
/**
|
2025-09-02 10:48:49 +05:30
|
|
|
* Check if a delegation is valid
|
2025-04-24 16:30:50 +05:30
|
|
|
*/
|
2025-08-30 18:34:50 +05:30
|
|
|
isDelegationValid(
|
|
|
|
|
currentAddress?: string,
|
|
|
|
|
currentWalletType?: 'bitcoin' | 'ethereum'
|
|
|
|
|
): boolean {
|
2025-09-02 10:48:49 +05:30
|
|
|
const delegation = DelegationStorage.retrieve();
|
2025-04-24 16:30:50 +05:30
|
|
|
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
|
2025-04-24 16:30:50 +05:30
|
|
|
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-04-24 16:30:50 +05:30
|
|
|
}
|
2025-08-28 18:44:35 +05:30
|
|
|
|
|
|
|
|
/**
|
2025-09-02 10:48:49 +05:30
|
|
|
* Get the time remaining on the current delegation in milliseconds
|
2025-08-28 18:44:35 +05:30
|
|
|
*/
|
|
|
|
|
getDelegationTimeRemaining(): number {
|
2025-09-02 10:48:49 +05:30
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2025-09-02 10:48:49 +05:30
|
|
|
* Get the browser public key from the current delegation
|
2025-08-28 18:44:35 +05:30
|
|
|
*/
|
|
|
|
|
getBrowserPublicKey(): string | null {
|
2025-09-02 10:48:49 +05:30
|
|
|
const delegation = DelegationStorage.retrieve();
|
2025-08-28 18:44:35 +05:30
|
|
|
if (!delegation) return null;
|
|
|
|
|
return delegation.browserPublicKey;
|
|
|
|
|
}
|
|
|
|
|
|
2025-04-24 16:30:50 +05:30
|
|
|
/**
|
2025-09-02 10:48:49 +05:30
|
|
|
* 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-04-24 16:30:50 +05:30
|
|
|
*/
|
2025-08-28 18:44:35 +05:30
|
|
|
clearDelegation(): void {
|
2025-09-02 10:48:49 +05:30
|
|
|
DelegationStorage.clear();
|
2025-08-28 18:44:35 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
// MESSAGE SIGNING & VERIFICATION
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
/**
|
2025-09-02 10:48:49 +05:30
|
|
|
* Sign a raw string message using the browser-generated private key
|
2025-08-28 18:44:35 +05:30
|
|
|
*/
|
|
|
|
|
signRawMessage(message: string): string | null {
|
2025-09-02 10:48:49 +05:30
|
|
|
const delegation = DelegationStorage.retrieve();
|
2025-04-24 16:30:50 +05:30
|
|
|
if (!delegation || !this.isDelegationValid()) return null;
|
2025-08-30 18:34:50 +05:30
|
|
|
|
2025-04-24 16:30: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);
|
2025-04-24 16:30:50 +05:30
|
|
|
return bytesToHex(signature);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error signing with browser key:', error);
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-08-28 18:44:35 +05:30
|
|
|
|
2025-04-24 16:30:50 +05:30
|
|
|
/**
|
2025-09-02 10:48:49 +05:30
|
|
|
* Sign an unsigned message with the delegated browser key
|
2025-04-24 16:30:50 +05:30
|
|
|
*/
|
2025-09-02 10:48:49 +05:30
|
|
|
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
|
|
|
|
2025-09-02 10:48:49 +05:30
|
|
|
const delegation = DelegationStorage.retrieve();
|
2025-04-24 16:30:50 +05:30
|
|
|
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-04-24 16:30:50 +05:30
|
|
|
}
|
2025-08-28 18:44:35 +05:30
|
|
|
|
2025-04-24 16:30:50 +05:30
|
|
|
/**
|
2025-09-02 10:48:49 +05:30
|
|
|
* Verify an OpchanMessage signature
|
2025-04-24 16:30:50 +05:30
|
|
|
*/
|
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-04-24 16:30:50 +05:30
|
|
|
}
|
2025-09-02 10:17:42 +05:30
|
|
|
|
|
|
|
|
/**
|
2025-09-02 10:48:49 +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
|
|
|
}
|
2025-09-02 10:48:49 +05:30
|
|
|
|
|
|
|
|
// Export singleton instance
|
|
|
|
|
export const delegationManager = new DelegationManager();
|
|
|
|
|
export * from './types';
|
|
|
|
|
export { DelegationStorage };
|