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.
