Iniciativa Cidadã Independente · Transparência Parlamentar
TS40 linhas1.431 bytes

apps/worker/src/services/assinatura.ts

SHA-256

35b41f68ea3f9b90c01c9099021b37288314ad31431f9d2f6b12d2fa1a65d996

Somente leiturafonte-eb8a54281800
/**
 * HMAC-SHA256 com o ADMIN_SECRET — links de ação para o admin (veto da
 * newsletter) e pseudonimização de IP/e-mail no rate limit.
 */

const encoder = new TextEncoder()

async function hmacHex(secret: string, msg: string): Promise<string> {
  const key = await crypto.subtle.importKey(
    'raw',
    encoder.encode(secret),
    { name: 'HMAC', hash: 'SHA-256' },
    false,
    ['sign'],
  )
  const sig = await crypto.subtle.sign('HMAC', key, encoder.encode(msg))
  return Array.from(new Uint8Array(sig), (b) => b.toString(16).padStart(2, '0')).join('')
}

/** Comparação em tempo constante (para strings de mesmo alfabeto). */
export function iguaisTempoConstante(a: string, b: string): boolean {
  if (a.length !== b.length) return false
  let diff = 0
  for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i)
  return diff === 0
}

export function assinarVeto(secret: string, edicao: string): Promise<string> {
  return hmacHex(secret, `newsletter-veto:${edicao}`)
}

export async function verificarVeto(secret: string, edicao: string, sig: string): Promise<boolean> {
  return iguaisTempoConstante(await assinarVeto(secret, edicao), sig)
}

/** Identificador estável e não reversível (sem o segredo) para buckets de rate limit. */
export async function pseudonimo(secret: string, valor: string): Promise<string> {
  return (await hmacHex(secret, `rate-limit:${valor}`)).slice(0, 32)
}