bip39.py 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  1. #!/usr/bin/env python3
  2. #
  3. # mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
  4. # Copyright (C)2013-2023 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. }
  42. def __init__(self,wl_id='bip39'):
  43. assert wl_id == 'bip39', "initialize with 'bip39' for compatibility with baseconv API"
  44. from .wordlist.bip39 import words
  45. self.digits = words
  46. self.wl_id = 'bip39'
  47. @classmethod
  48. def nwords2seedlen(cls,nwords,in_bytes=False,in_hex=False):
  49. for k,v in cls.constants.items():
  50. if v.mn_len == nwords:
  51. return k//8 if in_bytes else k//4 if in_hex else k
  52. die( 'MnemonicError', f'{nwords!r}: invalid word length for BIP39 mnemonic' )
  53. @classmethod
  54. def seedlen2nwords(cls,seed_len,in_bytes=False,in_hex=False):
  55. seed_bits = seed_len * 8 if in_bytes else seed_len * 4 if in_hex else seed_len
  56. try:
  57. return cls.constants[seed_bits].mn_len
  58. except Exception as e:
  59. raise ValueError(f'{seed_bits!r}: invalid seed length for BIP39 mnemonic') from e
  60. def tobytes(self,*args,**kwargs):
  61. raise NotImplementedError('Method not supported')
  62. def frombytes(self,*args,**kwargs):
  63. raise NotImplementedError('Method not supported')
  64. def tohex(self,words_arg,pad=None):
  65. assert isinstance(words_arg,(list,tuple)),'words_arg must be list or tuple'
  66. assert pad in (None,'seed'), f"{pad}: invalid 'pad' argument (must be None or 'seed')"
  67. wl = self.digits
  68. for n,w in enumerate(words_arg):
  69. if w not in wl:
  70. die( 'MnemonicError', f'word #{n+1} is not in the BIP39 word list' )
  71. res = ''.join(f'{wl.index(w):011b}' for w in words_arg)
  72. for k,v in self.constants.items():
  73. if len(words_arg) == v.mn_len:
  74. bitlen = k
  75. break
  76. else:
  77. die( 'MnemonicError', f'{len(words_arg)}: invalid BIP39 seed phrase length' )
  78. seed_bin = res[:bitlen]
  79. chk_bin = res[bitlen:]
  80. seed_hex = f'{int(seed_bin,2):0{bitlen//4}x}'
  81. seed_bytes = bytes.fromhex(seed_hex)
  82. chk_len = self.constants[bitlen].chk_len
  83. chk_hex_chk = sha256(seed_bytes).hexdigest()
  84. chk_bin_chk = f'{int(chk_hex_chk,16):0256b}'[:chk_len]
  85. if chk_bin != chk_bin_chk:
  86. die( 'MnemonicError', f'invalid BIP39 seed phrase checksum ({chk_bin} != {chk_bin_chk})' )
  87. return seed_hex
  88. def fromhex(self,hexstr,pad=None,tostr=False):
  89. assert is_hex_str(hexstr),'seed data not a hexadecimal string'
  90. assert tostr is False,"'tostr' must be False for 'bip39'"
  91. assert pad in (None,'seed'), f"{pad}: invalid 'pad' argument (must be None or 'seed')"
  92. wl = self.digits
  93. seed_bytes = bytes.fromhex(hexstr)
  94. bitlen = len(seed_bytes) * 8
  95. assert bitlen in self.constants, f'{bitlen}: invalid seed bit length'
  96. c = self.constants[bitlen]
  97. chk_hex = sha256(seed_bytes).hexdigest()
  98. seed_bin = f'{int(hexstr,16):0{bitlen}b}'
  99. chk_bin = f'{int(chk_hex,16):0256b}'
  100. res = seed_bin + chk_bin
  101. return tuple(wl[int(res[i*11:(i+1)*11],2)] for i in range(c.mn_len))
  102. def generate_seed(self,words_arg,passwd=''):
  103. self.tohex(words_arg) # validate
  104. from cryptography.hazmat.primitives import hashes
  105. from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
  106. return PBKDF2HMAC(
  107. algorithm = hashes.SHA512(),
  108. length = 64,
  109. salt = b'mnemonic' + passwd.encode(),
  110. iterations = 2048
  111. ).derive(' '.join(words_arg).encode())