News

El cifrado de la wallet tenía un contador que podía volverse negativo

Bitcoin Core fusionó el 19 de agosto de 2026 un arreglo para una ruta de cifrado de wallet donde un contador de iteraciones por encima de 2.147.483.647 se volvía negativo. Lo interesante es de dónde sale ese número.

4 min de lecturaDerivación de claves
El cifrado de la wallet tenía un contador que podía volverse negativo

Qué pasó

El 19 de agosto de 2026 Bitcoin Core fusionó el pull request 35859, que cambia el tipo de un número en el código de cifrado de la wallet. La descripción plantea el problema con precisión: "Los valores de CMasterKey::nDeriveIterations se deserializan desde los archivos de wallet como enteros de 32 bits sin signo, pero la derivación de claves estrechaba el contador a un int con signo. Un contador por encima de INT_MAX se volvía negativo en la conversión, y el contador del bucle de derivación entonces se desbordaba, lo cual es comportamiento indefinido".

Qué cambia

Cifrar una wallet de Bitcoin Core no cifra las claves privadas con la passphrase. Cifra una clave maestra con la passphrase, y la clave maestra cifra las claves. Esa indirección es la razón por la que cambiar una passphrase es instantáneo en lugar de volver a cifrar cada clave.

Convertir una passphrase en una clave de cifrado es tarea de una función de derivación de claves, y todo el sentido de una es ser lenta, para que adivinar le cueste tiempo real a quien lo intente. Core fija ese costo midiendo la máquina de cada uno. EncryptMasterKey corre la derivación, la cronometra y escala el contador a un target_time de 100 milisegundos, tomando el promedio ponderado de dos corridas. Debajo hay un piso, DEFAULT_DERIVE_ITERATIONS = 25000, con un comentario que envejeció hasta volverse fósil: "25000 rondas son poco menos de 0,1 segundos en un Pentium M de 1,86 GHz".

El contador después vive en el archivo de la wallet, y un archivo es dato. Se guarda como entero de 32 bits sin signo, así que cualquier valor hasta 4.294.967.295 va y vuelve sin perderse. El código de derivación tomaba ese valor como un int con signo. Cualquier cosa por encima de 2.147.483.647 volvía negativa, y el contador del bucle de derivación se desbordaba. Eso es comportamiento indefinido y no una respuesta equivocada: el compilador tiene derecho a asumir que el desbordamiento con signo no puede ocurrir y a generar código sobre esa suposición.

El arreglo mantiene el contador sin signo a lo largo de toda la ruta de derivación para que coincida con el tipo con el que fue serializado, y crypter.h ahora declara SetKeyFromPassphrase y BytesToKeySHA512AES con rondas unsigned int. Con él viajan tres cambios menores, que son los que generalizan: ahora se comprueban los fallos de calibración, el contador calculado se valida antes de la conversión con if (target_iterations < 1 || target_iterations > std::numeric_limits<unsigned int>::max()), la clave maestra de salida queda "sin cambios hasta que la derivación y el cifrado tengan éxito", y los métodos falibles del crypter quedan marcados como [[nodiscard]], de modo que ignorar una derivación fallida deja de compilar.

Qué no cambia

La derivación en sí queda intacta. Lo que cambió es el tipo de un contador, no la función que convierte una passphrase en una clave, así que ninguna passphrase se volvió más fácil ni más difícil de adivinar. El pull request no describe ningún exploit ni nombra ninguna wallet afectada: reporta una discordancia de tipos y el comportamiento indefinido que se sigue de ella, que es cosa distinta de un ataque que funcione. Llegar siquiera a esa ruta exige un archivo de wallet con un contador que la calibración de 100 milisegundos en una máquina real no produce.

El diseño alrededor tampoco cambia, incluida la parte que sorprende: el costo del cifrado de una wallet es una medición de la computadora en la que a uno le tocó cifrarla. Cifrar en una laptop lenta deja el contador bajo, de forma permanente, hasta volver a fijar la passphrase en algo más rápido. El piso sigue siendo 25000 rondas, todavía explicado en el código por un comentario sobre un Pentium M.

Y el cifrado de la wallet sigue haciendo solo lo que siempre hizo. Protege el archivo en reposo, frente a alguien que lo copie. No hace nada mientras la wallet está desbloqueada, nada frente a una máquina comprometida que observa lo que se teclea, y no es un backup: la passphrase cuida el archivo, y la seed phrase es lo que restaura las monedas si el archivo ya no está.

Contexto

Esto cae en un mes en el que el manejo de claves ya era la historia. La reconstrucción de la generación de entropía de Coldcard fue un fallo en cómo se producía una seed. Esto es más pequeño y está más adelante en el mismo camino: no cómo se hizo el azar, sino cómo estaba tipado el número que lo custodia.

Los dos comparten una forma que vale la pena notar. En ambos, la propiedad de seguridad era correcta sobre el papel y el defecto estaba en la representación de un valor, uno que ninguna prueba ejercitaba porque el uso normal nunca lo produce. La respuesta de Core acá es la aburrida, y sirve más allá de este error: usar el mismo tipo con el que se serializa, validar antes de estrechar, y hacer que el compilador se niegue a dejar que quien llama ignore un fallo.

Boletín

Bitcoin, sin ruido

Qué ha pasado en Bitcoin, qué cambia de verdad, y las fuentes para que puedas comprobarnos. Un número cada vez, directo a tu correo.

  • Un correo por número, nunca una secuencia automática
  • Sin píxeles de seguimiento y sin compartir direcciones
  • Baja desde cualquier número con un clic

Recibe el próximo número

Un correo por número, sin píxeles de seguimiento, y te puedes dar de baja desde cualquiera de ellos. No compartimos tu dirección. Política de privacidad