Recuperación de una billetera HD a partir de una frase semilla parcial

Estoy tratando de ayudar a recuperar una billetera donde el propietario solo escribió 11 de las 12 palabras en la frase inicial. Inicialmente, pensé que la tarea sería rápida y bien definida, pero parece ser un poco más compleja de lo que supuse, y el material de referencia es bastante escaso. En caso de que alguien más tenga un problema similar al mío, quiero dejar esta publicación detallando los pasos que seguí (con ejemplos de código de trabajo).

La billetera con la que estoy tratando es Breadwallet, que aparentemente usa una estrategia de derivación diferente (más antigua) mnemotécnica a HD-master-private-key de la mayoría de las billeteras modernas. Por el momento, solo me enfocaré en recuperar frases parciales de Breadwallet, pero planeo expandir la respuesta eventualmente para cubrir estrategias de derivación más nuevas ( BIP44 ) también.

Esto es realmente bueno. ¿Cómo funcionaría si estuviera usando una billetera Electrum (2.x) de 13 palabras, donde solo faltaba la primera palabra semilla? Entonces tenemos posición, pero variabilidad adicional de las diferencias en entropía, etc. con 13 contra 12...
Si falta una palabra y se conoce una posición, es fácil regenerar la semilla. La suma de verificación está integrada en la semilla, por lo que hay muy pocas palabras que incluso tengan la posibilidad de funcionar (probablemente solo un puñado de semillas que necesitaría probar)
No debe hacer preguntas en una sección de respuestas.

Respuestas (1)

(El lenguaje utilizado en esta publicación es Python)

Breadwallet usa BIP39 para generar la semilla maestra de 128 bits a partir del mnemotécnico de 12 palabras. Luego, la semilla maestra se usa para generar un conjunto de billeteras/cuentas que contienen cadenas de direcciones, usando BIP32 .

https://github.com/bitcoin/bips/raw/master/bip-0032/derivation.png


En primer lugar, importe hashlib y binascii, los necesitaremos más adelante.

import hashlib
from binascii import hexlify, unhexlify

Supongamos que tiene 11 de las 12 palabras en su frase semilla. En aras de la simplicidad, usaré las primeras 11 palabras en la lista de palabras BIP39:

partial_seed_phrase = [ 'abandon', 'ability', 'able', 'about', 'above', 'absent', 'absorb', 'abstract', 'absurd', 'abuse', 'access' ]

La lista de palabras contiene 2048 entradas, lo que da a cada palabra 11 bits de entropía (2 11 = 2048). Las 12 palabras tienen 12*11 = 132 bits de entropía en total. La semilla maestra HD tiene una longitud de 128 bits y hay una suma de verificación de 4 bits adjunta al final, lo que eleva el total de bits a 132. Hasta ahora, todo bien.

Si asumimos que wordlistes una lista de 2048 elementos (omitida por limitaciones de espacio), podemos encontrar el índice (en decimal) de los elementos en partial_seed_phrase:

mnemonic_in_decimal = map(wordlist.index, partial_seed_phrase)
# [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

Convirtámoslo mnemonic_in_decimalen una matriz de números binarios de 11 bits de ancho.

mnemonic_in_binary = map('{0:011b}'.format, mnemonic_in_decimal)
# ['00000000000', '00000000001', '00000000010', '00000000011', '00000000100', '00000000101', '00000000110', '00000000111', '00000001000', '00000001001', '00000001010']

Sabemos que falta una sola palabra (11 bits) en algún lugar desconocido de esta matriz. En circunstancias no ideales, tendríamos que comprobar cada una de las 12 ubicaciones en busca de la palabra que falta contra 2048 palabras posibles cada una, para un total de 24576 (12*2048 = 24576) semillas maestras potenciales.

for missing_word_position in range(0,12):
    # The missing word belongs at some index from 0-11 in the final 12-word phrase

    for wordlist_index in range(0, 2048):
        # Iterate over all possibilities for the missing word

        missing_word_binary = '{0:011b}'.format(wordlist_index)
        front_half          = ''.join(mnemonic_in_binary[0:missing_word_position])
        back_half           = ''.join(mnemonic_in_binary[missing_word_position:12])
        seed_and_checksum   = front_half + missing_word_binary + back_half

        seed     = seed_and_checksum[0:128]
        checksum = seed_and_checksum[-4:]

Afortunadamente, tenemos una suma de verificación de 4 bits, lo que significa que solo una de cada 16 semillas (2 4 = 16) será válida. Esto significa que terminaremos con un total final de aproximadamente 1536 semillas maestras (24576/16 = 1536) para verificar los fondos. La suma de comprobación se deriva de los primeros bits (en este caso, 4) devueltos al aplicar la función hash SHA-256 a la semilla, por lo que el número final de semillas maestras válidas puede variar, pero promediará alrededor de 1/16 de la total de semillas posibles.

[Más adelante, si alguien quiere ayudar a escribir descripciones o código para cualquiera de los siguientes pasos, ¡se lo agradecería! ]

Que hacer:

  1. Calcule la suma de verificación real de los primeros 4 bits de sha256 (semilla)
  2. compare la suma de verificación con la suma de verificación real. Si son iguales, inserte la semilla en una matriz de semillas maestras válidas.
  3. Calcule el nodo maestro, que es HMAC-SHA512 (semilla)
  4. Calcular una cuenta desde el nodo maestro
  5. Calcular una cadena de billetera de la cuenta
  6. Calcule las primeras 5 claves privadas en la cadena de billetera
  7. Calcule las primeras 5 claves públicas de esas claves privadas
  8. Escriba una descripción y los pasos para recuperar frases parciales para billeteras BIP44
  9. Escriba un programa para consultar una cadena de bloques localmente o una API de exploración de cadenas de bloques en línea. Pásele la lista combinada de claves públicas generadas y vea si alguna tiene saldo.