seedsplit.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  1. #!/usr/bin/env python3
  2. #
  3. # MMGen Wallet, a terminal-based cryptocurrency wallet
  4. # Copyright (C)2013-2026 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. seedsplit: Seed split classes and methods for the MMGen suite
  20. """
  21. from .color import yellow
  22. from .util import msg, die
  23. from .objmethods import MMGenObject, HiliteStr, InitErrors
  24. from .obj import ImmutableAttr, MMGenPWIDString, MMGenIdx, get_obj, IndexedDict
  25. from .seed import Seed, SeedBase
  26. from .subseed import SubSeedList, SubSeedIdx, SubSeed
  27. from .crypto import Crypto
  28. class SeedShareIdx(MMGenIdx):
  29. max_val = 1024
  30. class SeedShareCount(SeedShareIdx):
  31. min_val = 2
  32. class MasterShareIdx(MMGenIdx):
  33. max_val = 1024
  34. class SeedSplitSpecifier(HiliteStr, InitErrors, MMGenObject):
  35. color = 'red'
  36. def __new__(cls, s):
  37. if isinstance(s, cls):
  38. return s
  39. try:
  40. me = str.__new__(cls, s)
  41. match s.split(':', 2):
  42. case [id_str, idx, count]:
  43. me.id = SeedSplitIDString(id_str)
  44. case [idx, count]:
  45. me.id = SeedSplitIDString('default')
  46. case _:
  47. raise ValueError('seed split specifier cannot be parsed')
  48. me.idx = SeedShareIdx(idx)
  49. me.count = SeedShareCount(count)
  50. assert me.idx <= me.count, 'share index greater than share count'
  51. return me
  52. except Exception as e:
  53. return cls.init_fail(e, s)
  54. def is_seed_split_specifier(s):
  55. return get_obj(SeedSplitSpecifier, s=s, silent=True, return_bool=True)
  56. class SeedSplitIDString(MMGenPWIDString):
  57. desc = 'seed split ID string'
  58. class SeedShareList(SubSeedList):
  59. have_short = False
  60. split_type = 'N-of-N'
  61. count = ImmutableAttr(SeedShareCount)
  62. id_str = ImmutableAttr(SeedSplitIDString)
  63. def __init__(self, parent_seed, count, *, id_str=None, master_idx=None, debug_last_share=False):
  64. self.member_type = SeedShare
  65. self.parent_seed = parent_seed
  66. self.id_str = id_str or 'default'
  67. self.count = count
  68. self.len = 2 # placeholder, always overridden
  69. def make_master_share():
  70. for nonce in range(SeedShare.max_nonce+1):
  71. ms = SeedShareMaster(self, master_idx, nonce)
  72. if ms.sid == parent_seed.sid:
  73. if parent_seed.cfg.debug_subseed:
  74. msg(f'master_share seed ID collision with parent seed, incrementing nonce to {nonce+1}')
  75. else:
  76. return ms
  77. die('SubSeedNonceRangeExceeded', 'nonce range exceeded')
  78. def last_share_debug(last_share):
  79. if not debug_last_share:
  80. return False
  81. sid_len = self.debug_last_share_sid_len
  82. lsid = last_share.sid[:sid_len]
  83. psid = parent_seed.sid[:sid_len]
  84. ssids = [d[:sid_len] for d in self.data['long'].keys]
  85. return (lsid in ssids or lsid == psid)
  86. self.master_share = make_master_share() if master_idx else None
  87. for nonce in range(SeedShare.max_nonce+1):
  88. self.nonce_start = nonce
  89. self.data = {'long': IndexedDict(), 'short': IndexedDict()} # 'short' is required as a placeholder
  90. if self.master_share:
  91. self.data['long'][self.master_share.sid] = (1, self.master_share.nonce)
  92. self._generate(count-1)
  93. self.last_share = ls = SeedShareLast(self)
  94. if last_share_debug(ls) or ls.sid in self.data['long'] or ls.sid == parent_seed.sid:
  95. # collision: throw out entire split list and redo with new start nonce
  96. if parent_seed.cfg.debug_subseed:
  97. self._collision_debug_msg(
  98. ls.sid, count, nonce, nonce_desc='nonce_start', debug_last_share=debug_last_share)
  99. else:
  100. self.data['long'][ls.sid] = (count, nonce)
  101. break
  102. else:
  103. die('SubSeedNonceRangeExceeded', 'nonce range exceeded')
  104. if parent_seed.cfg.debug_subseed:
  105. A = parent_seed.data
  106. B = self.join().data
  107. assert A == B, f'Data mismatch!\noriginal seed: {A!r}\nrejoined seed: {B!r}'
  108. def get_share_by_idx(self, idx, *, base_seed=False):
  109. match idx:
  110. case self.count:
  111. return self.last_share
  112. case 1 if self.master_share:
  113. return self.master_share if base_seed else self.master_share.derived_seed
  114. case x if x >= 1 or x <= self.count:
  115. return self.get_subseed_by_ss_idx(SubSeedIdx(str(idx) + 'L'))
  116. case x:
  117. die('RangeError', f'{x}: share index out of range')
  118. def get_share_by_seed_id(self, sid, *, base_seed=False):
  119. if sid == self.data['long'].key(self.count-1):
  120. return self.last_share
  121. elif self.master_share and sid == self.data['long'].key(0):
  122. return self.master_share if base_seed else self.master_share.derived_seed
  123. else:
  124. return self.get_subseed_by_seed_id(sid)
  125. def join(self):
  126. return Seed.join_shares(
  127. self.parent_seed.cfg,
  128. [self.get_share_by_idx(i+1) for i in range(len(self))])
  129. def format(self):
  130. assert self.split_type == 'N-of-N'
  131. fs1 = ' {}\n'
  132. fs2 = '{i:>5}: {}\n'
  133. mfs1, mfs2, midx, msid = ('', '', '', '')
  134. if self.master_share:
  135. mfs1, mfs2 = (' with master share #{} ({})', ' (master share #{})')
  136. midx, msid = (self.master_share.idx, self.master_share.sid)
  137. hdr = ' {} {} ({} bits)\n'.format('Seed:', self.parent_seed.sid.hl(), self.parent_seed.bitlen)
  138. hdr += ' {} {c}-of-{c} (XOR){m}\n'.format('Split Type:', c=self.count, m=mfs1.format(midx, msid))
  139. hdr += ' {} {}\n\n'.format('ID String:', self.id_str.hl())
  140. hdr += fs1.format('Shares')
  141. hdr += fs1.format('------')
  142. sl = self.data['long'].keys
  143. body1 = fs2.format(sl[0]+mfs2.format(midx), i=1)
  144. body = (fs2.format(sl[n], i=n+1) for n in range(1, len(self)))
  145. return hdr + body1 + ''.join(body)
  146. class SeedShareBase(MMGenObject):
  147. @property
  148. def fn_stem(self):
  149. pl = self.parent_list
  150. msdata = f'_with_master{pl.master_share.idx}' if pl.master_share else ''
  151. return '{}-{}-{}of{}{}[{}]'.format(
  152. pl.parent_seed.sid,
  153. pl.id_str,
  154. self.idx,
  155. pl.count,
  156. msdata,
  157. self.sid)
  158. @property
  159. def desc(self):
  160. return self.get_desc()
  161. def get_desc(self, *, ui=False):
  162. pl = self.parent_list
  163. mss = f', with master share #{pl.master_share.idx}' if pl.master_share else ''
  164. if ui:
  165. m = (yellow("(share {} of {} of ")
  166. + pl.parent_seed.sid.hl()
  167. + yellow(', split id ')
  168. + pl.id_str.hl2(encl='‘’')
  169. + yellow('{})'))
  170. else:
  171. m = "share {} of {} of " + pl.parent_seed.sid + ", split id '" + pl.id_str + "'{}"
  172. return m.format(self.idx, pl.count, mss)
  173. class SeedShare(SeedShareBase, SubSeed):
  174. @staticmethod
  175. def make_subseed_bin(parent_list, idx: int, nonce: int, length: str):
  176. seed = parent_list.parent_seed
  177. assert parent_list.have_short is False
  178. assert length == 'long'
  179. # field maximums: id_str: none (256 chars), count: 65535 (1024), idx: 65535 (1024), nonce: 65535 (1000)
  180. scramble_key = (
  181. f'{parent_list.split_type}:{parent_list.id_str}:'.encode() +
  182. parent_list.count.to_bytes(2, 'big') +
  183. idx.to_bytes(2, 'big') +
  184. nonce.to_bytes(2, 'big')
  185. )
  186. if parent_list.master_share:
  187. scramble_key += (
  188. b':master:' +
  189. parent_list.master_share.idx.to_bytes(2, 'big')
  190. )
  191. return Crypto(parent_list.parent_seed.cfg).scramble_seed(seed.data, scramble_key)[:seed.byte_len]
  192. class SeedShareLast(SeedShareBase, SeedBase):
  193. idx = ImmutableAttr(SeedShareIdx)
  194. nonce = 0
  195. def __init__(self, parent_list):
  196. self.idx = parent_list.count
  197. self.parent_list = parent_list
  198. SeedBase.__init__(
  199. self,
  200. parent_list.parent_seed.cfg,
  201. seed_bin=self.make_subseed_bin(parent_list))
  202. @staticmethod
  203. def make_subseed_bin(parent_list):
  204. seed_list = (parent_list.get_share_by_idx(i+1) for i in range(len(parent_list)))
  205. seed = parent_list.parent_seed
  206. ret = int(seed.data.hex(), 16)
  207. for ss in seed_list:
  208. ret ^= int(ss.data.hex(), 16)
  209. return ret.to_bytes(seed.byte_len, 'big')
  210. class SeedShareMaster(SeedBase, SeedShareBase):
  211. idx = ImmutableAttr(MasterShareIdx)
  212. nonce = ImmutableAttr(int, typeconv=False)
  213. def __init__(self, parent_list, idx, nonce):
  214. self.idx = idx
  215. self.nonce = nonce
  216. self.parent_list = parent_list
  217. self.cfg = parent_list.parent_seed.cfg
  218. SeedBase.__init__(self, self.cfg, seed_bin=self.make_base_seed_bin())
  219. self.derived_seed = SeedBase(
  220. self.cfg,
  221. seed_bin = self.make_derived_seed_bin(parent_list.id_str, parent_list.count))
  222. @property
  223. def fn_stem(self):
  224. return '{}-MASTER{}[{}]'.format(self.parent_list.parent_seed.sid, self.idx, self.sid)
  225. def make_base_seed_bin(self):
  226. seed = self.parent_list.parent_seed
  227. # field maximums: idx: 65535 (1024)
  228. scramble_key = b'master_share:' + self.idx.to_bytes(2, 'big') + self.nonce.to_bytes(2, 'big')
  229. return Crypto(self.cfg).scramble_seed(seed.data, scramble_key)[:seed.byte_len]
  230. # Don't bother with avoiding seed ID collision here, as sid of derived seed is not used
  231. # by user as an identifier
  232. def make_derived_seed_bin(self, id_str, count):
  233. # field maximums: id_str: none (256 chars), count: 65535 (1024)
  234. scramble_key = id_str.encode() + b':' + count.to_bytes(2, 'big')
  235. return Crypto(self.cfg).scramble_seed(self.data, scramble_key)[:self.byte_len]
  236. def get_desc(self, *, ui=False):
  237. psid = self.parent_list.parent_seed.sid
  238. mss = f'master share #{self.idx} of '
  239. return yellow('(' + mss) + psid.hl() + yellow(')') if ui else mss + psid
  240. class SeedShareMasterJoining(SeedShareMaster):
  241. id_str = ImmutableAttr(SeedSplitIDString)
  242. count = ImmutableAttr(SeedShareCount)
  243. def __init__(self, cfg, idx, base_seed, id_str, count):
  244. SeedBase.__init__(self, cfg, seed_bin=base_seed.data)
  245. self.cfg = cfg
  246. self.id_str = id_str or 'default'
  247. self.count = count
  248. self.derived_seed = SeedBase(
  249. cfg,
  250. seed_bin = self.make_derived_seed_bin(self.id_str, self.count))
  251. def join_shares(
  252. cfg,
  253. seed_list,
  254. *,
  255. master_idx = None,
  256. id_str = None):
  257. if not hasattr(seed_list, '__next__'): # seed_list can be iterator or iterable
  258. seed_list = iter(seed_list)
  259. class d:
  260. byte_len, ret, count = None, 0, 0
  261. def add_share(ss):
  262. if d.byte_len:
  263. assert ss.byte_len == d.byte_len, f'Seed length mismatch! {ss.byte_len} != {d.byte_len}'
  264. else:
  265. d.byte_len = ss.byte_len
  266. d.ret ^= int(ss.data.hex(), 16)
  267. d.count += 1
  268. if master_idx:
  269. master_share = next(seed_list)
  270. for ss in seed_list:
  271. add_share(ss)
  272. if master_idx:
  273. add_share(SeedShareMasterJoining(cfg, master_idx, master_share, id_str, d.count+1).derived_seed)
  274. SeedShareCount(d.count) # check that d.count is in valid range
  275. return Seed(cfg, seed_bin=d.ret.to_bytes(d.byte_len, 'big'))