← Volver a VOZ.PE

Cómo protegemos tu voto

Las 4 capas que hacen matemáticamente detectable cualquier intento de alterar los resultados. Sin «confía en nosotros» — te enseñamos exactamente cómo verificarlo.

🛡️

Un voto por persona

Cada voto pasa cuatro filtros combinados: tu IP solo puede emitir un voto (si alguien en tu red ya votó te sugerimos cambiar a datos móviles); la firma única de tu dispositivo queda bloqueada permanentemente; un pequeño puzzle computacional le sale caro a los bots; y reCAPTCHA de Google verifica comportamiento humano. Mil votos falsos cuestan decenas de minutos de CPU + proxies residenciales + servicios de captcha-solver.

No pedimos DNI. Tu identidad real queda fuera del sistema.
🔐

Tu voto viaja cifrado

Antes de salir de tu dispositivo, tu voto se encripta con una llave pública. Solo el servidor — con su llave privada guardada lejos de la web — puede descifrarlo. Aunque alguien esté espiando tu WiFi, solo ve ruido.

Estándar RSA-2048 + AES-256, el mismo que usa tu banca por internet.
🔗

Cadena que no se puede romper

Cada voto lleva una «huella digital» (hash) que incluye la huella del voto anterior. Si alguien intenta cambiar un voto viejo, su huella se rompe y arrastra la falla a todos los votos posteriores — queda evidente de inmediato.

Tu voto tiene una passphrase tipo "Alacrán Camionero Perla" que puedes compartir para que otros lo encuentren en la cadena.
🌳

Prueba matemática pública

Cada hora firmamos un «resumen» de toda la cadena y lo publicamos. Cualquiera puede tomar los votos, aplicar el mismo algoritmo, y comprobar que el resumen coincide. Si coincide — nada fue alterado. Si no — alarma.

Se llama Merkle Tree. Es la misma técnica que usa Bitcoin para verificar transacciones.

¿Eres programador o investigador? La versión técnica arriba explica todo con detalle, endpoints y código para verificar tú mismo.

1Identidad sin DNI: cuatro filtros combinados

No usamos DNI ni verificación biométrica. Cada voto pasa por cuatro señales independientes que, combinadas, encarecen cualquier intento de inflar resultados:

  • Una IP = un voto, doble defensa: (a) el servidor hashea el IP del request con SHA-256 y revisa si ya existe antes del INSERT (rechaza con HTTP 429 y mensaje que sugiere datos móviles); (b) un TRIGGER BEFORE INSERT en MariaDB con SIGNAL SQLSTATE '45000' re-valida a nivel DB para defense-in-depth.
  • Device fingerprint: SHA-256 de señales del navegador incluyendo canvas+WebGL. UNIQUE en survey_voters.device_fp. Detecta re-votos desde el mismo dispositivo aunque cambie IP vía VPN. También computamos un stable_fp secundario (sin canvas/WebGL, no randomizado en modo privado iOS) que se usa solo como gating del endpoint GET para evitar exponer la passphrase del vecino en la misma red.
  • Proof of Work: puzzle client-side. El navegador busca un nonce tal que SHA-256(challenge + ":" + nonce) produzca un hash con suficientes ceros leading. Parámetros calibrados para ~segundos en un dispositivo normal, pero suman costoso a escala — mil votos falsos equivalen a decenas de minutos de CPU al 100%.
  • reCAPTCHA v3: Google analiza comportamiento (mouse, scroll, timing) y emite un score ∈ [0,1]. Aceptamos solo scores sobre un umbral conservador, atados a una action específica. Filtra bots conocidos.

Ninguna capa es infalible sola. Juntas convierten el ataque de "segundos gratis" a "decenas de minutos + gastos reales en proxies residenciales + servicios de captcha-solver".

2Encriptación extremo-a-extremo: RSA-2048 + AES-256-GCM

Antes de enviar el voto, el cliente ejecuta este flujo en el browser (window.crypto.subtle):

// 1. Obtener la public key (RSA-2048 SPKI) del endpoint
const pem = await fetch('/api/pubkey.php').then(r => r.text());
const pub = await crypto.subtle.importKey('spki', pemToBuffer(pem),
    {name:'RSA-OAEP', hash:'SHA-1'}, false, ['encrypt']);

// 2. Generar AES-256-GCM key + IV random
const aes = await crypto.subtle.generateKey({name:'AES-GCM', length:256}, true, ['encrypt']);
const iv = crypto.getRandomValues(new Uint8Array(12));

// 3. Cifrar {q1,q2,q3,q4,q5} con AES-GCM
const ct = await crypto.subtle.encrypt({name:'AES-GCM', iv}, aes, encoded);

// 4. Cifrar la AES key con RSA-OAEP
const rawAes = await crypto.subtle.exportKey('raw', aes);
const encKey = await crypto.subtle.encrypt({name:'RSA-OAEP'}, pub, rawAes);

// 5. POST {enc_payload, enc_key, iv, device_fp, pow, recaptcha_token}

En el servidor se descifra con la private key (guardada fuera del document root, no accesible vía HTTP):

openssl_private_decrypt(base64_decode(enc_key), $aesKey, $priv, OPENSSL_PKCS1_OAEP_PADDING);
$plain = openssl_decrypt($ct, 'aes-256-gcm', $aesKey, OPENSSL_RAW_DATA, $iv, $tag);

Implica que nadie en la red entre tu navegador y el servidor — ni tu ISP, ni operadores de WiFi, ni cualquier proxy corporativo — puede leer las respuestas. Si alguien intercepta el POST, ve solo enc_payload y enc_key en base64.

Nota sobre OAEP-SHA1: usamos SHA-1 como función hash del padding OAEP porque es el default de openssl_private_decrypt en PHP. OAEP-SHA1 sigue siendo criptográficamente sólido (RFC 8017) — lo que se considera roto en SHA-1 es la resistencia a colisiones (relevante en firmas), no la pre-imagen que OAEP usa. El resto del pipeline (AES-GCM-256 para el payload, SHA-256 para hashes de integridad/cadena/Merkle) queda en la moderna.

3Passphrase: un nombre memorable por cada hash

Cada voto produce un hash SHA-256 único, pero 64 caracteres hex son impronunciables. Por eso derivamos una passphrase compuesta de 3 palabras: un animal, una característica, y un color. Por ejemplo Alacrán Camionero Perla.

Los vocabularios son 200 animales × 1,101 características × 50 colores = 11M combinaciones. Para el 99% de usuarios la passphrase es única. Para el 1% que colisiona, el sistema agrega un sufijo numérico (Alacrán Camionero Perla 02) garantizado único por UNIQUE constraint en la base de datos.

La asignación es determinística a partir del hash del voto en el primer intento, con check de colisión en base de datos como fallback. El mismo voto siempre produce la misma passphrase, lo cual permite que el servidor la re-derive sin almacenarla si hiciera falta.

4Hash del voto: contenido inmutable

Para cada voto guardamos un hash SHA-256 derivado de inputs públicos:

hash = SHA-256(
    ip_hash + '|' +
    q1 + q2 + q3 + q4 + q5 + '|' +
    created_at + '|' +
    'vote#' + id
)

Cualquiera con esos inputs puede recomputar el hash y verificar que coincide con el almacenado. Efecto avalancha del SHA-256: si alguien altera cualquier campo — aunque sea un solo bit — el hash cambia radicalmente.

La verificación en vivo la hace verificar.php: al buscar un voto, recomputa el hash desde los inputs y muestra un badge ✓ Hash verificado o ✗ Integridad fallida.

5Cadena encadenada: prev_hash

Cada voto guarda además el hash del voto inmediatamente anterior en la columna prev_hash. El primer voto tiene prev_hash 0000…0000 (génesis).

vote #1:  prev_hash = 0x000…  →  hash = A
vote #2:  prev_hash = A         →  hash = B
vote #3:  prev_hash = B         →  hash = C
...

Para validar la cadena, un auditor camina todas las filas ordenadas por id ASC y verifica que cada row.prev_hash === previous_row.hash. Si alguien intenta alterar el contenido del voto #500, su hash (ya almacenado) recomputa diferente → el prev_hash de #501 ya no coincide → cadena rota en ese punto.

Verificación auto: /api/audit-chain.php?verify=1 — devuelve {ok: true, chain_length: N, breaks: []}.

6Merkle Tree + Commitment Scheme

Un proceso automatizado toma periódicamente todos los hashes de votos en orden cronológico, construye un Merkle binary tree, y guarda el root + vote_count + head_hash + timestamp en una tabla de snapshots. La construcción es estándar:

function buildTree(hashes):
    levels = [hashes]
    while length(last_level) > 1:
        next = []
        for i in 0..len step 2:
            L = current[i]
            R = current[i+1] or L          # duplicar si impar
            next.push(SHA-256(L + R))
        levels.push(next)
    root = last_level[0]

Cada snapshot es un commitment público: en ese instante la cadena estaba así, y el root lo prueba criptográficamente. Como es determinístico, cualquiera con la misma lista de hashes llega al mismo root. Alterar cualquier voto pasado produciría un root distinto al que ya publicamos, detectándolo inmediatamente.

La prueba de inclusión (Merkle proof) de un voto específico es una lista de log₂(N) siblings. Para 1,000 votos son solo 10 siblings. El verificador /verificar.php la muestra expandible.

7Endpoints públicos de auditoría

Todo lo necesario para replicar los cálculos desde tu máquina está abierto:

Ejemplo de verificación independiente con curl + python:

curl -s 'https://voz.pe/api/audit-chain.php?verify=1' | jq '.verify'
# {"ok": true, "chain_length": N, "breaks": []}

curl -s 'https://voz.pe/api/merkle.php' | jq '.merkle_root'
# "377f8d0b0c5d29ee6cc5dacf83546731c9e0d1681c1690339ea4022587b6bc94"

8Separación de identidad y contenido

La identidad del votante (ip_hash, device_fp) se guarda en una tabla separada (survey_voters) con UNIQUE constraint solo en device_fp — suficiente para enforzar "un dispositivo, un voto" al nivel DB. Las respuestas viven en survey_responses junto con el hash y la passphrase. El enforcement de "una IP, un voto" está en el servidor + el trigger enforce_one_vote_per_ip como red de seguridad.

Un admin puede saber que alguien votó (existe su ip_hash en voters), pero no qué votó (no hay link explícito entre las tablas). Para cerrar más este círculo haría falta threshold decryption (múltiples keyholders cooperando para descifrar agregados) o zero-knowledge proofs — roadmap futuro.

9Qué medimos

Somos una encuestadora del pulso digital. A diferencia de las tradicionales que muestrean por teléfono o presencial, nosotros medimos a quien está online y decidió responder — ponderado por el peso electoral real de cada región (padrón ONPE 2021). Nuestro sesgo: solo capturamos a quien tiene conexión y participó, somos representativos del Perú conectado. Nuestra fortaleza: transparencia total del método y los datos, con toda la matemática abierta para auditar.

10Reporta un hallazgo

La seguridad del sistema depende de que la comunidad la examine. Si encuentras un bug, un vector de ataque no documentado, o una mejora metodológica, escríbenos a contacto@voz.pe. Respondemos todo.