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.
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.
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.
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.
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.
¿Eres programador o investigador? La versión técnica arriba explica todo con detalle, endpoints y código para verificar tú mismo.
No usamos DNI ni verificación biométrica. Cada voto pasa por cuatro señales independientes que, combinadas, encarecen cualquier intento de inflar resultados:
TRIGGER BEFORE INSERT en MariaDB con SIGNAL SQLSTATE '45000' re-valida a nivel DB para defense-in-depth.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.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%.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".
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.
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.
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.
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: []}.
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.
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"
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.
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.