seedsplit.py 10 KB

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