seedsplit.py 9.7 KB

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