bip39.py 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. #!/usr/bin/env python3
  2. #
  3. # MMGen Wallet, a terminal-based cryptocurrency wallet
  4. # Copyright (C)2013-2025 The MMGen Project <mmgen@tuta.io>
  5. #
  6. # This program is free software: you can redistribute it and/or modify
  7. # it under the terms of the GNU General Public License as published by
  8. # the Free Software Foundation, either version 3 of the License, or
  9. # (at your option) any later version.
  10. #
  11. # This program is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU General Public License
  17. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  18. """
  19. bip39.py - Data and routines for BIP39 mnemonic seed phrases
  20. """
  21. from hashlib import sha256
  22. from .baseconv import baseconv
  23. from .util import is_hex_str, die
  24. def is_bip39_mnemonic(s):
  25. return bool(bip39().tohex(s.split()))
  26. # implements a subset of the baseconv API
  27. class bip39(baseconv):
  28. desc = baseconv.dt('BIP39 mnemonic', 'BIP39 mnemonic seed phrase')
  29. wl_chksum = 'f18b9a84'
  30. seedlen_map = {16:12, 24:18, 32:24}
  31. seedlen_map_rev = {12:16, 18:24, 24:32}
  32. from collections import namedtuple
  33. bc = namedtuple('bip39_constants', ['chk_len', 'mn_len'])
  34. # ENT CS MS
  35. constants = {
  36. 128: bc(4, 12),
  37. 160: bc(5, 15),
  38. 192: bc(6, 18),
  39. 224: bc(7, 21),
  40. 256: bc(8, 24)}
  41. def __init__(self, wl_id='bip39'):
  42. assert wl_id == 'bip39', "initialize with 'bip39' for compatibility with baseconv API"
  43. from .wordlist.bip39 import words
  44. self.digits = words
  45. self.wl_id = 'bip39'
  46. @classmethod
  47. def nwords2seedlen(cls, nwords, /, *, in_bytes=False, in_hex=False):
  48. for k, v in cls.constants.items():
  49. if v.mn_len == nwords:
  50. return k//8 if in_bytes else k//4 if in_hex else k
  51. die('MnemonicError', f'{nwords!r}: invalid word length for BIP39 mnemonic')
  52. @classmethod
  53. def seedlen2nwords(cls, seed_len, /, *, in_bytes=False, in_hex=False):
  54. seed_bits = seed_len * 8 if in_bytes else seed_len * 4 if in_hex else seed_len
  55. try:
  56. return cls.constants[seed_bits].mn_len
  57. except Exception as e:
  58. raise ValueError(f'{seed_bits!r}: invalid seed length for BIP39 mnemonic') from e
  59. def tohex(self, words_arg, /, *, pad=None):
  60. return self.tobytes(words_arg, pad=pad).hex()
  61. def tobytes(self, words_arg, /, *, pad=None):
  62. assert isinstance(words_arg, list | tuple), 'words_arg must be list or tuple'
  63. assert pad in (None, 'seed'), f"{pad}: invalid 'pad' argument (must be None or 'seed')"
  64. wl = self.digits
  65. for n, w in enumerate(words_arg):
  66. if w not in wl:
  67. die('MnemonicError', f'word #{n+1} is not in the BIP39 word list')
  68. res = ''.join(f'{wl.index(w):011b}' for w in words_arg)
  69. for k, v in self.constants.items():
  70. if len(words_arg) == v.mn_len:
  71. bitlen = k
  72. break
  73. else:
  74. die('MnemonicError', f'{len(words_arg)}: invalid BIP39 seed phrase length')
  75. seed_bin = res[:bitlen]
  76. chk_bin = res[bitlen:]
  77. seed_hex = f'{int(seed_bin, 2):0{bitlen//4}x}'
  78. seed_bytes = bytes.fromhex(seed_hex)
  79. chk_len = self.constants[bitlen].chk_len
  80. chk_hex_chk = sha256(seed_bytes).hexdigest()
  81. chk_bin_chk = f'{int(chk_hex_chk, 16):0256b}'[:chk_len]
  82. if chk_bin != chk_bin_chk:
  83. die('MnemonicError', f'invalid BIP39 seed phrase checksum ({chk_bin} != {chk_bin_chk})')
  84. return seed_bytes
  85. def fromhex(self, hexstr, /, *, pad=None, tostr=False):
  86. assert is_hex_str(hexstr), 'seed data not a hexadecimal string'
  87. return self.frombytes(bytes.fromhex(hexstr), pad=pad, tostr=tostr)
  88. def frombytes(self, seed_bytes, /, *, pad=None, tostr=False):
  89. assert tostr is False, "'tostr' must be False for 'bip39'"
  90. assert pad in (None, 'seed'), f"{pad}: invalid 'pad' argument (must be None or 'seed')"
  91. wl = self.digits
  92. bitlen = len(seed_bytes) * 8
  93. assert bitlen in self.constants, f'{bitlen}: invalid seed bit length'
  94. c = self.constants[bitlen]
  95. chk_hex = sha256(seed_bytes).hexdigest()
  96. seed_bin = f'{int(seed_bytes.hex(), 16):0{bitlen}b}'
  97. chk_bin = f'{int(chk_hex, 16):0256b}'
  98. res = seed_bin + chk_bin
  99. return tuple(wl[int(res[i*11:(i+1)*11], 2)] for i in range(c.mn_len))
  100. def generate_seed(self, words_arg, /, *, passwd=''):
  101. self.tohex(words_arg) # validate
  102. from hashlib import pbkdf2_hmac
  103. return pbkdf2_hmac(
  104. hash_name = 'sha512',
  105. password = ' '.join(words_arg).encode(),
  106. salt = b'mnemonic' + passwd.encode(),
  107. iterations = 2048,
  108. dklen = 64)