Skip to content

Repository files navigation

BIP-39 Tools

Misc BIP39 related tools/scripts.

Run the automated tests from this directory with:

python3 -m unittest discover -s tests -v

Update checksums

sha256sum *.py *.sh *.txt *.sed > SHA256SUMS

1. Dice rolls BIP39(SHA256(rolls_ascii))

Technical specification for transforming dice rolls into seed words:

  1. The input is the dice-roll string as ASCII digits 1 through 6. Whitespace is ignored by the helper scripts.
  2. For a 12-word use 50 rolls. For a 24-word seed, use 99 rolls.
  3. Compute digest = SHA256(rolls_ascii) (not part of BIP39, but removes possiblle statistical bias from rolls).
  4. Select the BIP39 entropy bytes from digest:
    • 12 words: entropy = digest[0:16], 128 bits.
    • 24 words: entropy = digest[0:32], 256 bits.
  5. Compute the BIP39 checksum from the selected entropy bytes, by taking the first len(entropy) // 32 bits of its SHA256:
    • checksum = SHA256(entropy).
    • 12 words: append the first 4 checksum bits.
    • 24 words: append the first 8 checksum bits.
  6. Write entropy || checksum_bits as a big-endian bit string: most significant bit first for each byte.
  7. Split that bit string into 11-bit groups. A 12-word seed has 132 bits and therefore 12 groups; a 24-word seed has 264 bits and therefore 24 groups.
  8. Interpret each 11-bit group as an unsigned integer 0..2047.
  9. Use that integer as a zero-based index into bip39-eng.txt; equivalently, word index n is line n + 1.
  10. Join the selected words with spaces.
# Generate 50 random dice faces for a 12-word seed test. Dependencies: tr, head.
tr -dc '1-6' </dev/urandom | head -c 50; echo
# Use python to convert the dice rolls into BIP39 mnemonic.
# Dependencies: python3+std libraries: hashlib.sha256, argparse, pathlib
./dice.py 12 44266664153554464254321232633466466235664323326523
# Use bash tools to convert the dice rolls to BIP39 mnemonic, by using
# bc base-2048 output and a checked-in sed map from base-2048 digits to BIP39 words.
# Dependencies: bash, sha256sum, cut, xxd, bc, xargs, sed, paste
./dice.sh 12 44266664153554464254321232633466466235664323326523
# Result:
# vacuum ethics glimpse cable grit comfort reason festival nothing balance grant design
# Regenerate the map of 11-bit integers 0..2047 into words if bip39-eng.txt ever changes.
awk '{printf "s/^%04d$/%s/\n", NR-1, $0}' bip39-eng.txt > bip39-bc2048.sed

Compare these three methods to each other with:

python3 -m unittest tests/test_dice_word_mapping_fuzz.py

2. BIP85 child BIP39 seed words.

bip85.py derives English BIP39 child mnemonics using the BIP85 BIP39 application path m/83696968'/39'/0'/{word_count}'/{index}'. The parent mnemonic is converted to its BIP39 seed with an empty BIP39 passphrase.

# Prompts for the parent mnemonic.
python3 bip85.py 12 0

Supported child word counts are 12, 15, 18, 21, and 24.

Run the tests:

python3 -m unittest tests/test_bip85.py

3. BIP39 root extended private key

xprv.py derives a mainnet BIP32 root extended private key from a valid English BIP39 mnemonic. It uses an empty BIP39 passphrase.

# Prompts for the mnemonic.
python3 xprv.py

The resulting xprv contains the root private key and chain code. Wallet addresses also depend on the address type and derivation path. For example, a mainnet native Segwit single-signature wallet compatible with SeedSigner's m/84'/0'/0' account uses this Bitcoin Core descriptor:

wpkh(XPRV/84h/0h/0h/<0;1>/*)

Replace XPRV with the printed value. Bitcoin Core requires a descriptor checksum for importdescriptors; obtain it with getdescriptorinfo, append it after #, and import the checksummed descriptor into a blank descriptor wallet. Use timestamp 0 when recovering historical transactions, or "now" only for a wallet that has never been used.

4. Encrypt/Decrypt backup archive.

encrypt_backup.py creates a minimal encrypted 7z archive containing one plaintext file named backup.txt. The file contains a generated comment with a fresh random secret, followed by the mnemonic line:

# Generated by bip39tools/encrypt_backup.py. Nonce: <64 hex characters>
mnemonic = "word word ... word"

The scripts use only Python standard libraries and bip39-eng.txt.

# Create an encrypted backup archive. The mnemonic must be 12 or 24 BIP39 words.
python3 encrypt_backup.py backup.7z
# Prompts for the mnemonic, archive passphrase, and passphrase confirmation
# with hidden terminal input.

# Decrypt the archive and print backup.txt to stdout.
python3 decrypt_backup.py backup.7z
# Prompts for the archive passphrase with hidden terminal input.
# # Generated by bip39tools/encrypt_backup.py. Nonce: ...
# mnemonic = "abandon abandon abandon abandon abandon ..."

The archive password is the encryption password for the 7z file. It is not a BIP39 passphrase, and no BIP39-passphrase wallet derivation is performed.

Encrypted archive generation specification

  1. Normalize and validate the input mnemonic:

    • split the provided words on whitespace,
    • lowercase each word,
    • require exactly 12 or 24 words,
    • require every word to be present in bip39-eng.txt.
  2. Build the plaintext as UTF-8 bytes:

    # Generated by bip39tools/encrypt_backup.py. Nonce: <64 hex characters>
    mnemonic = "word word ... word"
    

    The nonce is 32 random bytes encoded with secrets.token_hex(32). The plaintext ends with one newline byte (0x0a).

  3. Generate independent 16-byte salts and 16-byte IVs with secrets.token_bytes for the file data stream and encoded header stream.

  4. Derive each AES-256 key with the 7z AES/SHA-256 KDF:

    password_bytes = password.encode("utf-16-le")
    digest = hashlib.sha256()
    for i in range(16_777_216):  # 1 << ROUNDS_POWER, where ROUNDS_POWER = 24
        digest.update(salt)
        digest.update(password_bytes)
        digest.update(struct.pack("<Q", i))
    key = digest.digest()
  5. Pad the plaintext with zero bytes to at least 1024 bytes, then to a 16-byte boundary. The original plaintext length is stored only in the encrypted header, so standard extractors remove both AES block padding and cover padding during decryption.

  6. Encrypt the padded plaintext with AES-256-CBC using the derived key and IV. No compression is used.

  7. Build the normal single-file 7z header containing the file-data AES/SHA-256 coder (06 f1 07 01), file-data KDF properties, original plaintext size, plaintext CRC32, and one UTF-16-LE filename entry for backup.txt.

  8. Pad and encrypt that normal header as a second AES-256-CBC stream.

  9. Write a minimal single-file 7z container with standard encoded-header metadata:

    • file signature: 37 7a bc af 27 1c,
    • version: major 0, minor 4,
    • next-header CRC32: CRC32 of the 20-byte section header,
    • section header: little-endian uint64 offset to the final next header, little-endian uint64 final next-header length, CRC32 of the final next header,
    • encrypted body: AES-256-CBC ciphertext from step 6,
    • encrypted encoded-header body: AES-256-CBC ciphertext from step 8,
    • final next header: kEncodedHeader (0x17) describing how to decrypt the encrypted encoded-header body.

The generated archives use standard 7z header encryption, so standard archivers can open them with the archive passphrase. Without the passphrase, the exact plaintext size, plaintext CRC32, and backup.txt filename are not present in the clear header. The outer 7z structure still reveals archive size and the fixed 1024-byte encrypted file-data bucket used for these small backups.

All variable-width 7z integers generated by the script use its encode_var64 encoding.

python3 -m unittest tests/test_backup_archive.py

About

Dice-rolls, BIP85, xprv, and encrypted backup tools for BIP39 workflows.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages