subseed.py 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  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. subseed: Subseed classes and methods for the MMGen suite
  20. """
  21. from .color import green
  22. from .util import msg_r, msg, die, make_chksum_8
  23. from .objmethods import MMGenObject, HiliteStr, InitErrors
  24. from .obj import MMGenRange, IndexedDict, ImmutableAttr
  25. from .seed import SeedBase, SeedID
  26. class SubSeedIdxRange(MMGenRange):
  27. min_idx = 1
  28. max_idx = 1000000
  29. class SubSeedIdx(HiliteStr, InitErrors):
  30. color = 'red'
  31. trunc_ok = False
  32. def __new__(cls, s):
  33. if isinstance(s, cls):
  34. return s
  35. try:
  36. assert isinstance(s, str), 'not a string or string subclass'
  37. idx = s[:-1] if s[-1] in 'SsLl' else s
  38. from .util import is_int
  39. assert is_int(idx), "valid format: an integer, plus optional letter 'S', 's', 'L' or 'l'"
  40. idx = int(idx)
  41. assert idx >= SubSeedIdxRange.min_idx, f'subseed index < {SubSeedIdxRange.min_idx:,}'
  42. assert idx <= SubSeedIdxRange.max_idx, f'subseed index > {SubSeedIdxRange.max_idx:,}'
  43. sstype, ltr = ('short', 'S') if s[-1] in 'Ss' else ('long', 'L')
  44. me = str.__new__(cls, str(idx)+ltr)
  45. me.idx = idx
  46. me.type = sstype
  47. return me
  48. except Exception as e:
  49. return cls.init_fail(e, s)
  50. class SubSeed(SeedBase):
  51. idx = ImmutableAttr(int, typeconv=False)
  52. nonce = ImmutableAttr(int, typeconv=False)
  53. ss_idx = ImmutableAttr(SubSeedIdx)
  54. max_nonce = 1000
  55. def __init__(self, parent_list, idx, nonce, length):
  56. self.idx = idx
  57. self.nonce = nonce
  58. self.ss_idx = str(idx) + {'long': 'L', 'short': 'S'}[length]
  59. self.parent_list = parent_list
  60. SeedBase.__init__(
  61. self,
  62. parent_list.parent_seed.cfg,
  63. seed_bin=self.make_subseed_bin(parent_list, idx, nonce, length))
  64. @staticmethod
  65. def make_subseed_bin(parent_list, idx:int, nonce:int, length:str):
  66. seed = parent_list.parent_seed
  67. short = {'short': True, 'long': False}[length]
  68. # field maximums: idx: 4294967295 (1000000), nonce: 65535 (1000), short: 255 (1)
  69. scramble_key = idx.to_bytes(4, 'big') + nonce.to_bytes(2, 'big') + short.to_bytes(1, 'big')
  70. from .crypto import Crypto
  71. return Crypto(parent_list.parent_seed.cfg).scramble_seed(
  72. seed.data, scramble_key)[:16 if short else seed.byte_len]
  73. class SubSeedList(MMGenObject):
  74. have_short = True
  75. nonce_start = 0
  76. debug_last_share_sid_len = 3
  77. dfl_len = 100
  78. def __init__(self, parent_seed, *, length=None):
  79. self.member_type = SubSeed
  80. self.parent_seed = parent_seed
  81. self.data = {'long': IndexedDict(), 'short': IndexedDict()}
  82. self.len = length or self.dfl_len
  83. def __len__(self):
  84. return len(self.data['long'])
  85. def get_subseed_by_ss_idx(self, ss_idx_in, *, print_msg=False):
  86. ss_idx = SubSeedIdx(ss_idx_in)
  87. if print_msg:
  88. msg_r('{} {} of {}...'.format(
  89. green('Generating subseed'),
  90. ss_idx.hl(),
  91. self.parent_seed.sid.hl(),
  92. ))
  93. if ss_idx.idx > len(self):
  94. self._generate(ss_idx.idx)
  95. sid = self.data[ss_idx.type].key(ss_idx.idx-1)
  96. idx, nonce = self.data[ss_idx.type][sid]
  97. if idx != ss_idx.idx:
  98. die(3, "{} != {}: self.data[{t!r}].key(i) does not match self.data[{t!r}][i]!".format(
  99. idx,
  100. ss_idx.idx,
  101. t = ss_idx.type))
  102. if print_msg:
  103. msg(f'\b\b\b => {SeedID.hlc(sid)}')
  104. seed = self.member_type(self, idx, nonce, length=ss_idx.type)
  105. assert seed.sid == sid, f'{seed.sid} != {sid}: Seed ID mismatch!'
  106. return seed
  107. def get_subseed_by_seed_id(self, sid, *, last_idx=None, print_msg=False):
  108. def get_existing_subseed_by_seed_id(sid):
  109. for k in ('long', 'short') if self.have_short else ('long',):
  110. if sid in self.data[k]:
  111. idx, nonce = self.data[k][sid]
  112. return self.member_type(self, idx, nonce, length=k)
  113. def do_msg(subseed):
  114. if print_msg:
  115. self.parent_seed.cfg._util.qmsg('{} {} ({}:{})'.format(
  116. green('Found subseed'),
  117. subseed.sid.hl(),
  118. self.parent_seed.sid.hl(),
  119. subseed.ss_idx.hl(),
  120. ))
  121. if last_idx is None:
  122. last_idx = self.len
  123. subseed = get_existing_subseed_by_seed_id(sid)
  124. if subseed:
  125. do_msg(subseed)
  126. return subseed
  127. if len(self) >= last_idx:
  128. return None
  129. self._generate(last_idx, last_sid=sid)
  130. subseed = get_existing_subseed_by_seed_id(sid)
  131. if subseed:
  132. do_msg(subseed)
  133. return subseed
  134. def _collision_debug_msg(self, sid, idx, nonce, *, nonce_desc='nonce', debug_last_share=False):
  135. slen = 'short' if sid in self.data['short'] else 'long'
  136. m1 = f'add_subseed(idx={idx},{slen}):'
  137. if sid == self.parent_seed.sid:
  138. m2 = f'collision with parent Seed ID {sid},'
  139. else:
  140. if debug_last_share:
  141. sl = self.debug_last_share_sid_len
  142. colliding_idx = [d[:sl] for d in self.data[slen].keys].index(sid[:sl]) + 1
  143. sid = sid[:sl]
  144. else:
  145. colliding_idx = self.data[slen][sid][0]
  146. m2 = f'collision with ID {sid} (idx={colliding_idx},{slen}),'
  147. msg(f'{m1:30} {m2:46} incrementing {nonce_desc} to {nonce+1}')
  148. def _generate(self, last_idx=None, *, last_sid=None):
  149. if last_idx is None:
  150. last_idx = self.len
  151. first_idx = len(self) + 1
  152. if first_idx > last_idx:
  153. return None
  154. if last_sid is not None:
  155. last_sid = SeedID(sid=last_sid)
  156. def add_subseed(idx, length):
  157. for nonce in range(self.nonce_start, self.member_type.max_nonce+1): # handle SeedID collisions
  158. sid = make_chksum_8(self.member_type.make_subseed_bin(self, idx, nonce, length))
  159. if sid in self.data['long'] or sid in self.data['short'] or sid == self.parent_seed.sid:
  160. if self.parent_seed.cfg.debug_subseed: # should get ≈450 collisions for first 1,000,000 subseeds
  161. self._collision_debug_msg(sid, idx, nonce)
  162. else:
  163. self.data[length][sid] = (idx, nonce)
  164. return last_sid == sid
  165. # must exit here, as this could leave self.data in inconsistent state
  166. die('SubSeedNonceRangeExceeded', 'add_subseed(): nonce range exceeded')
  167. for idx in SubSeedIdxRange(first_idx, last_idx).iterate():
  168. match1 = add_subseed(idx, 'long')
  169. match2 = add_subseed(idx, 'short') if self.have_short else False
  170. if match1 or match2:
  171. break
  172. def format(self, first_idx, last_idx):
  173. r = SubSeedIdxRange(first_idx, last_idx)
  174. if len(self) < last_idx:
  175. self._generate(last_idx)
  176. fs1 = '{:>18} {:>18}\n'
  177. fs2 = '{i:>7}L: {:8} {i:>7}S: {:8}\n'
  178. hdr = f' Parent Seed: {self.parent_seed.sid.hl()} ({self.parent_seed.bitlen} bits)\n\n'
  179. hdr += fs1.format('Long Subseeds', 'Short Subseeds')
  180. hdr += fs1.format('-------------', '--------------')
  181. sl = self.data['long'].keys
  182. ss = self.data['short'].keys
  183. body = (fs2.format(sl[n-1], ss[n-1], i=n) for n in r.iterate())
  184. return hdr + ''.join(body)