mirror of
https://github.com/logos-messaging/OpChan.git
synced 2026-01-08 07:43:08 +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 && (
|
{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"
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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,
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user