fix: delegation reset + verification auth msg

This commit is contained in:
Danish Arora 2025-09-18 09:52:29 +05:30
parent a82bbe1243
commit a07fa3f7ba
No known key found for this signature in database
GPG Key ID: 1C6EF37CDAE1426E
7 changed files with 153 additions and 21 deletions

View File

@ -212,7 +212,13 @@ export function DelegationStep({
{delegationInfo?.isValid && ( {delegationInfo?.isValid && (
<div className="flex justify-end"> <div className="flex justify-end">
<Button <Button
onClick={clearDelegation} onClick={async () => {
const ok = await clearDelegation();
if (ok) {
// Refresh status so UI immediately reflects cleared state
getDelegationStatus().then(setDelegationInfo).catch(console.error);
}
}}
variant="outline" variant="outline"
size="sm" size="sm"
className="text-red-400 border-red-400 hover:bg-red-400 hover:text-white" className="text-red-400 border-red-400 hover:bg-red-400 hover:text-white"

View File

@ -499,7 +499,8 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
const updatedUser = { const updatedUser = {
...currentUser, ...currentUser,
delegationExpiry: undefined, delegationExpiry: undefined,
browserPublicKey: undefined, browserPubKey: undefined,
delegationSignature: undefined,
}; };
setCurrentUser(updatedUser); setCurrentUser(updatedUser);
await saveUser(updatedUser); await saveUser(updatedUser);

View File

@ -39,6 +39,7 @@ export function useAuthActions(): AuthActions {
verifyOwnership, verifyOwnership,
delegateKey: delegateKeyFromContext, delegateKey: delegateKeyFromContext,
getDelegationStatus, getDelegationStatus,
clearDelegation: clearDelegationFromContext,
} = useAuthContext(); } = useAuthContext();
const { toast } = useToast(); const { toast } = useToast();
@ -255,13 +256,8 @@ export function useAuthActions(): AuthActions {
} }
try { try {
// This would clear the delegation // Use the real clear implementation from AuthContext (includes toast)
// The actual implementation would use the DelegationManager await clearDelegationFromContext();
toast({
title: 'Delegation Cleared',
description: 'Your key delegation has been cleared.',
});
return true; return true;
} catch (error) { } catch (error) {
console.error('Failed to clear delegation:', error); console.error('Failed to clear delegation:', error);
@ -272,7 +268,7 @@ export function useAuthActions(): AuthActions {
}); });
return false; return false;
} }
}, [getDelegationStatus, toast]); }, [getDelegationStatus, clearDelegationFromContext, toast]);
// Renew delegation // Renew delegation
const renewDelegation = useCallback( const renewDelegation = useCallback(

View File

@ -77,12 +77,29 @@ export class LocalDatabase {
signature?: unknown; signature?: unknown;
browserPubKey?: unknown; browserPubKey?: unknown;
}; };
// Get a detailed validation report for clearer diagnostics
try {
const report = await this.validator.getValidationReport(message);
console.warn('LocalDatabase: Rejecting invalid message', { console.warn('LocalDatabase: Rejecting invalid message', {
messageId: partialMsg?.id, messageId: partialMsg?.id,
messageType: partialMsg?.type, messageType: partialMsg?.type,
hasSignature: !!partialMsg?.signature, hasSignature: !!partialMsg?.signature,
hasBrowserPubKey: !!partialMsg?.browserPubKey, hasBrowserPubKey: !!partialMsg?.browserPubKey,
hasValidSignature: report.hasValidSignature,
missingFields: report.missingFields,
invalidFields: report.invalidFields,
warnings: report.warnings,
errors: report.errors,
}); });
} catch (error) {
console.warn('LocalDatabase: Rejecting invalid message (no report)', {
messageId: partialMsg?.id,
messageType: partialMsg?.type,
hasSignature: !!partialMsg?.signature,
hasBrowserPubKey: !!partialMsg?.browserPubKey,
error: error instanceof Error ? error.message : String(error),
});
}
return false; return false;
} }

View File

@ -25,8 +25,9 @@ export class DelegationCrypto {
expiryTimestamp: number, expiryTimestamp: number,
nonce: string nonce: string
): string { ): string {
const expiryDate = new Date(expiryTimestamp).toLocaleString(); const isoExpiry = new Date(expiryTimestamp).toISOString();
return `I, ${walletAddress}, authorize browser key ${browserPublicKey} until ${expiryDate} (nonce: ${nonce})`; // Include both human-readable ISO and raw numeric timestamp for deterministic verification
return `I, ${walletAddress}, authorize browser key ${browserPublicKey} until ${isoExpiry} (ts:${expiryTimestamp}) (nonce: ${nonce})`;
} }
/** /**

View File

@ -159,6 +159,53 @@ export class DelegationManager {
); );
} }
/**
* Verify a signed message and return reasons when invalid
*/
async verifyWithReason(
message: OpchanMessage
): Promise<{ isValid: boolean; reasons: string[] }> {
const reasons: string[] = [];
// Check required fields
if (!message.signature) reasons.push('Missing message signature');
if (!message.browserPubKey) reasons.push('Missing browser public key');
if (!message.delegationProof) reasons.push('Missing delegation proof');
if (!message.author) reasons.push('Missing author address');
if (reasons.length > 0) return { isValid: false, reasons };
// Verify message signature
const signedContent = JSON.stringify({
...message,
signature: undefined,
browserPubKey: undefined,
delegationProof: undefined,
});
const signatureOk = DelegationCrypto.verifyRaw(
signedContent,
message.signature,
message.browserPubKey
);
if (!signatureOk) {
reasons.push('Invalid message signature');
return { isValid: false, reasons };
}
// Verify delegation proof with details
const proofResult = await this.verifyProofWithReason(
message.delegationProof,
message.browserPubKey,
message.author
);
if (!proofResult.isValid) {
reasons.push(...proofResult.reasons);
return { isValid: false, reasons };
}
return { isValid: true, reasons: [] };
}
/** /**
* Get delegation status * Get delegation status
*/ */
@ -204,6 +251,9 @@ export class DelegationManager {
*/ */
async clear(): Promise<void> { async clear(): Promise<void> {
await DelegationStorage.clear(); await DelegationStorage.clear();
// Invalidate in-memory cache immediately so UI reflects removal
this.cachedDelegation = null;
this.cachedAt = 0;
} }
// ============================================================================ // ============================================================================
@ -259,6 +309,53 @@ export class DelegationManager {
proof.walletType proof.walletType
); );
} }
/**
* Verify delegation proof with detailed reasons
*/
private async verifyProofWithReason(
proof: DelegationProof,
expectedBrowserKey: string,
expectedWalletAddress: string
): Promise<{ isValid: boolean; reasons: string[] }> {
const reasons: string[] = [];
if (!proof?.walletAddress) reasons.push('Delegation missing wallet address');
if (!proof?.authMessage) reasons.push('Delegation missing auth message');
if (proof?.expiryTimestamp === undefined)
reasons.push('Delegation missing expiry timestamp');
if (reasons.length > 0) return { isValid: false, reasons };
if (proof.walletAddress !== expectedWalletAddress) {
reasons.push('Delegation wallet address does not match author');
}
if (Date.now() >= proof.expiryTimestamp) {
reasons.push('Delegation has expired');
}
if (
!proof.authMessage.includes(expectedWalletAddress) ||
!proof.authMessage.includes(expectedBrowserKey) ||
// Accept either raw numeric timestamp marker or ISO format containing the numeric timestamp marker we embed as ts:<num>
!(proof.authMessage.includes(`ts:${proof.expiryTimestamp}`) ||
proof.authMessage.includes(proof.expiryTimestamp.toString()))
) {
reasons.push('Delegation auth message format mismatch');
}
if (reasons.length > 0) return { isValid: false, reasons };
const walletSigOk = await DelegationCrypto.verifyWalletSignature(
proof.authMessage,
proof.walletSignature,
proof.walletAddress,
proof.walletType
);
if (!walletSigOk) {
return { isValid: false, reasons: ['Invalid wallet signature for delegation'] };
}
return { isValid: true, reasons: [] };
}
} }
// Export singleton instance // Export singleton instance

View File

@ -281,9 +281,22 @@ export class MessageValidator {
errors: string[]; errors: string[];
}> { }> {
const structureReport = this.validateStructure(message); const structureReport = this.validateStructure(message);
const hasValidSignature = structureReport.isValid let hasValidSignature = false;
? await this.isValidMessage(message) let signatureErrors: string[] = [];
: false; if (structureReport.isValid) {
try {
const result = await this.getDelegationManager().verifyWithReason(
message as unknown as OpchanMessage
);
hasValidSignature = result.isValid;
signatureErrors = result.reasons;
} catch (err) {
hasValidSignature = false;
signatureErrors = [
err instanceof Error ? err.message : 'Unknown signature validation error',
];
}
}
return { return {
...structureReport, ...structureReport,
@ -291,6 +304,7 @@ export class MessageValidator {
errors: [ errors: [
...structureReport.missingFields, ...structureReport.missingFields,
...structureReport.invalidFields, ...structureReport.invalidFields,
...signatureErrors,
], ],
}; };
} }