/** * Canonical JSON serialization. * Order keys, normalize encoding, handle arrays consistently. */ export function canonicalizeJson(obj: Record): string { return JSON.stringify(obj, Object.keys(obj).sort()); } /** * Calculate canonical hash of a message. * Excludes: hash, signatures, cles_de_chiffrement. */ export function calculateCanonicalHash( message: Record, algo: string = 'sha256', ): string { const { hash, signatures, cles_de_chiffrement, ...canonical } = message; const canonicalJson = canonicalizeJson(canonical); return hashString(canonicalJson, algo); } /** * Hash a string with specified algorithm. */ export function hashString(input: string, algo: string): string { if (algo === 'sha256') { const encoder = new TextEncoder(); const data = encoder.encode(input); return crypto.subtle.digest('SHA-256', data).then((hashBuffer) => { const hashArray = Array.from(new Uint8Array(hashBuffer)); return hashArray.map((b) => b.toString(16).padStart(2, '0')).join(''); }) as unknown as string; } throw new Error(`Unsupported hash algorithm: ${algo}`); } /** * Async version of hashString. */ export async function hashStringAsync(input: string, algo: string): Promise { if (algo === 'sha256') { const encoder = new TextEncoder(); const data = encoder.encode(input); const hashBuffer = await crypto.subtle.digest('SHA-256', data); const hashArray = Array.from(new Uint8Array(hashBuffer)); return hashArray.map((b) => b.toString(16).padStart(2, '0')).join(''); } throw new Error(`Unsupported hash algorithm: ${algo}`); }