mirror of
https://github.com/logos-messaging/OpChan.git
synced 2026-01-02 12:53:10 +00:00
fix: delegation reset + verification auth msg
This commit is contained in:
parent
a82bbe1243
commit
a07fa3f7ba
@ -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"
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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})`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user