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 && (
<div className="flex justify-end">
<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"
size="sm"
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 = {
...currentUser,
delegationExpiry: undefined,
browserPublicKey: undefined,
browserPubKey: undefined,
delegationSignature: undefined,
};
setCurrentUser(updatedUser);
await saveUser(updatedUser);

View File

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

View File

@ -77,12 +77,29 @@ export class LocalDatabase {
signature?: unknown;
browserPubKey?: unknown;
};
console.warn('LocalDatabase: Rejecting invalid message', {
messageId: partialMsg?.id,
messageType: partialMsg?.type,
hasSignature: !!partialMsg?.signature,
hasBrowserPubKey: !!partialMsg?.browserPubKey,
});
// Get a detailed validation report for clearer diagnostics
try {
const report = await this.validator.getValidationReport(message);
console.warn('LocalDatabase: Rejecting invalid message', {
messageId: partialMsg?.id,
messageType: partialMsg?.type,
hasSignature: !!partialMsg?.signature,
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;
}

View File

@ -25,8 +25,9 @@ export class DelegationCrypto {
expiryTimestamp: number,
nonce: string
): string {
const expiryDate = new Date(expiryTimestamp).toLocaleString();
return `I, ${walletAddress}, authorize browser key ${browserPublicKey} until ${expiryDate} (nonce: ${nonce})`;
const isoExpiry = new Date(expiryTimestamp).toISOString();
// 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
*/
@ -204,6 +251,9 @@ export class DelegationManager {
*/
async clear(): Promise<void> {
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
);
}
/**
* 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

View File

@ -281,9 +281,22 @@ export class MessageValidator {
errors: string[];
}> {
const structureReport = this.validateStructure(message);
const hasValidSignature = structureReport.isValid
? await this.isValidMessage(message)
: false;
let hasValidSignature = false;
let signatureErrors: string[] = [];
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 {
...structureReport,
@ -291,6 +304,7 @@ export class MessageValidator {
errors: [
...structureReport.missingFields,
...structureReport.invalidFields,
...signatureErrors,
],
};
}