baseconv.py 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  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. baseconv: base conversion class for the MMGen suite
  20. """
  21. from collections import namedtuple
  22. from .util import die
  23. def is_b58_str(s):
  24. return set(list(s)) <= set(baseconv('b58').digits)
  25. def is_b32_str(s):
  26. return set(list(s)) <= set(baseconv('b32').digits)
  27. def is_mmgen_mnemonic(s):
  28. try:
  29. baseconv('mmgen').tobytes(s.split(),pad='seed')
  30. return True
  31. except:
  32. return False
  33. class baseconv(object):
  34. mn_base = 1626
  35. dt = namedtuple('desc_tuple',['short','long'])
  36. constants = {
  37. 'desc': {
  38. 'b58': dt('base58', 'base58-encoded data'),
  39. 'b32': dt('MMGen base32', 'MMGen base32-encoded data created using simple base conversion'),
  40. 'b16': dt('hexadecimal string','base16 (hexadecimal) string data'),
  41. 'b10': dt('base10 string', 'base10 (decimal) string data'),
  42. 'b8': dt('base8 string', 'base8 (octal) string data'),
  43. 'b6d': dt('base6d (die roll)', 'base6 data using the digits from one to six'),
  44. # 'tirosh':('Tirosh mnemonic', 'base1626 mnemonic using truncated Tirosh wordlist'), # not used by wallet
  45. 'mmgen': dt('MMGen native mnemonic',
  46. 'MMGen native mnemonic seed phrase created using old Electrum wordlist and simple base conversion'),
  47. },
  48. # https://en.wikipedia.org/wiki/Base32#RFC_4648_Base32_alphabet
  49. # https://tools.ietf.org/html/rfc4648
  50. 'digits': {
  51. 'b58': tuple('123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'),
  52. 'b32': tuple('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'), # RFC 4648 alphabet
  53. 'b16': tuple('0123456789abcdef'),
  54. 'b10': tuple('0123456789'),
  55. 'b8': tuple('01234567'),
  56. 'b6d': tuple('123456'),
  57. },
  58. 'wl_chksum': {
  59. 'mmgen': '5ca31424',
  60. # 'tirosh': '48f05e1f', # tirosh truncated to mn_base
  61. # 'tirosh1633': '1a5faeff' # tirosh list is 1633 words long!
  62. },
  63. 'seedlen_map': {
  64. 'b58': { 16:22, 24:33, 32:44 },
  65. 'b6d': { 16:50, 24:75, 32:100 },
  66. 'mmgen': { 16:12, 24:18, 32:24 },
  67. },
  68. 'seedlen_map_rev': {
  69. 'b58': { 22:16, 33:24, 44:32 },
  70. 'b6d': { 50:16, 75:24, 100:32 },
  71. 'mmgen': { 12:16, 18:24, 24:32 },
  72. }
  73. }
  74. def __init__(self,wl_id):
  75. if wl_id == 'mmgen':
  76. from .wordlist.electrum import words
  77. self.constants['digits'][wl_id] = words
  78. elif wl_id not in self.constants['digits']:
  79. raise ValueError(f'{wl_id}: unrecognized mnemonic ID')
  80. for k,v in self.constants.items():
  81. if wl_id in v:
  82. setattr(self,k,v[wl_id])
  83. self.wl_id = wl_id
  84. def get_wordlist(self):
  85. return self.digits
  86. def get_wordlist_chksum(self):
  87. from hashlib import sha256
  88. return sha256( ' '.join(self.digits).encode() ).hexdigest()[:8]
  89. def check_wordlist(self,cfg):
  90. wl = self.digits
  91. ret = f'Wordlist: {self.wl_id}\nLength: {len(wl)} words'
  92. new_chksum = self.get_wordlist_chksum()
  93. cfg._util.compare_chksums( new_chksum, 'generated', self.wl_chksum, 'saved', die_on_fail=True )
  94. if tuple(sorted(wl)) == wl:
  95. return ret + '\nList is sorted'
  96. else:
  97. die(3,'ERROR: List is not sorted!')
  98. @staticmethod
  99. def get_pad(pad,seed_pad_func):
  100. """
  101. 'pad' argument to baseconv conversion methods must be either None, 'seed' or an integer.
  102. If None, output of minimum (but never zero) length will be produced.
  103. If 'seed', output length will be mapped from input length using data in seedlen_map.
  104. If an integer, the string, hex string or byte output will be padded to this length.
  105. """
  106. if pad == None:
  107. return 0
  108. elif type(pad) == int:
  109. return pad
  110. elif pad == 'seed':
  111. return seed_pad_func()
  112. else:
  113. die('BaseConversionPadError',f"{pad!r}: illegal value for 'pad' (must be None,'seed' or int)")
  114. def tohex(self,words_arg,pad=None):
  115. "convert string or list data of instance base to a hexadecimal string"
  116. return self.tobytes(words_arg,pad//2 if type(pad)==int else pad).hex()
  117. def tobytes(self,words_arg,pad=None):
  118. "convert string or list data of instance base to byte string"
  119. words = words_arg if isinstance(words_arg,(list,tuple)) else tuple(words_arg.strip())
  120. desc = self.desc.short
  121. if len(words) == 0:
  122. die('BaseConversionError',f'empty {desc} data')
  123. def get_seed_pad():
  124. assert hasattr(self,'seedlen_map_rev'), f'seed padding not supported for base {self.wl_id!r}'
  125. d = self.seedlen_map_rev
  126. if not len(words) in d:
  127. die( 'BaseConversionError',
  128. f'{len(words)}: invalid length for seed-padded {desc} data in base conversion' )
  129. return d[len(words)]
  130. pad_val = max(self.get_pad(pad,get_seed_pad),1)
  131. wl = self.digits
  132. base = len(wl)
  133. if not set(words) <= set(wl):
  134. die( 'BaseConversionError',
  135. ( 'seed data' if pad == 'seed' else f'{words_arg!r}:' ) +
  136. f' not in {desc} format' )
  137. ret = sum([wl.index(words[::-1][i])*(base**i) for i in range(len(words))])
  138. bl = ret.bit_length()
  139. return ret.to_bytes(max(pad_val,bl//8+bool(bl%8)),'big')
  140. def fromhex(self,hexstr,pad=None,tostr=False):
  141. "convert a hexadecimal string to a list or string data of instance base"
  142. from .util import is_hex_str
  143. if not is_hex_str(hexstr):
  144. die( 'HexadecimalStringError',
  145. ( 'seed data' if pad == 'seed' else f'{hexstr!r}:' ) +
  146. ' not a hexadecimal string' )
  147. return self.frombytes( bytes.fromhex(hexstr), pad, tostr )
  148. def frombytes(self,bytestr,pad=None,tostr=False):
  149. "convert byte string to list or string data of instance base"
  150. if not bytestr:
  151. die( 'BaseConversionError', 'empty data not allowed in base conversion' )
  152. def get_seed_pad():
  153. assert hasattr(self,'seedlen_map'), f'seed padding not supported for base {self.wl_id!r}'
  154. d = self.seedlen_map
  155. if not len(bytestr) in d:
  156. die( 'SeedLengthError',
  157. f'{len(bytestr)}: invalid byte length for seed data in seed-padded base conversion' )
  158. return d[len(bytestr)]
  159. pad = max(self.get_pad(pad,get_seed_pad),1)
  160. wl = self.digits
  161. def gen():
  162. num = int.from_bytes(bytestr,'big')
  163. base = len(wl)
  164. while num:
  165. yield num % base
  166. num //= base
  167. ret = list(gen())
  168. o = [wl[n] for n in [0] * (pad-len(ret)) + ret[::-1]]
  169. return (' ' if self.wl_id == 'mmgen' else '').join(o) if tostr else o