Iniciativa Cidadã Independente · Transparência Parlamentar
TS54 linhas2.067 bytes

apps/worker/src/services/assinatura.ts

SHA-256

be1f0828aa2bbe5c33cc2ff5aa55222bff889d1c36c797e1d298c720cbe5b581

Somente leiturafonte-0531042d9f3e
/**
 * 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)
}

/**
 * Selo de integridade do certificado: amarra o código ao conteúdo impresso.
 * Quem altera o registro no banco sem o segredo não consegue refazer o selo.
 */
export function assinarCertificado(secret: string, codigo: string, dados: string): Promise<string> {
  return hmacHex(secret, `certificado:${codigo}:${dados}`)
}

export async function sha256Hex(dados: string | Uint8Array): Promise<string> {
  const bytes = typeof dados === 'string' ? encoder.encode(dados) : dados
  const hash = await crypto.subtle.digest('SHA-256', bytes)
  return Array.from(new Uint8Array(hash), (b) => b.toString(16).padStart(2, '0')).join('')
}