seed.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476
  1. #!/usr/bin/env python3
  2. #
  3. # mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
  4. # Copyright (C)2013-2021 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. seed.py: Seed-related classes and methods for the MMGen suite
  20. """
  21. from .common import *
  22. from .obj import *
  23. from .crypto import get_random,scramble_seed
  24. class SeedBase(MMGenObject):
  25. data = ImmutableAttr(bytes,typeconv=False)
  26. sid = ImmutableAttr(SeedID,typeconv=False)
  27. def __init__(self,seed_bin=None):
  28. if not seed_bin:
  29. # Truncate random data for smaller seed lengths
  30. seed_bin = sha256(get_random(1033)).digest()[:(opt.seed_len or g.dfl_seed_len)//8]
  31. elif len(seed_bin)*8 not in g.seed_lens:
  32. die(3,'{}: invalid seed length'.format(len(seed_bin)))
  33. self.data = seed_bin
  34. self.sid = SeedID(seed=self)
  35. @property
  36. def bitlen(self):
  37. return len(self.data) * 8
  38. @property
  39. def byte_len(self):
  40. return len(self.data)
  41. @property
  42. def hexdata(self):
  43. return self.data.hex()
  44. @property
  45. def fn_stem(self):
  46. return self.sid
  47. class SubSeedList(MMGenObject):
  48. have_short = True
  49. nonce_start = 0
  50. debug_last_share_sid_len = 3
  51. def __init__(self,parent_seed):
  52. self.member_type = SubSeed
  53. self.parent_seed = parent_seed
  54. self.data = { 'long': IndexedDict(), 'short': IndexedDict() }
  55. def __len__(self):
  56. return len(self.data['long'])
  57. def get_subseed_by_ss_idx(self,ss_idx_in,print_msg=False):
  58. ss_idx = SubSeedIdx(ss_idx_in)
  59. if print_msg:
  60. msg_r('{} {} of {}...'.format(
  61. green('Generating subseed'),
  62. ss_idx.hl(),
  63. self.parent_seed.sid.hl(),
  64. ))
  65. if ss_idx.idx > len(self):
  66. self._generate(ss_idx.idx)
  67. sid = self.data[ss_idx.type].key(ss_idx.idx-1)
  68. idx,nonce = self.data[ss_idx.type][sid]
  69. if idx != ss_idx.idx:
  70. m = "{} != {}: self.data[{t!r}].key(i) does not match self.data[{t!r}][i]!"
  71. die(3,m.format(idx,ss_idx.idx,t=ss_idx.type))
  72. if print_msg:
  73. msg('\b\b\b => {}'.format(SeedID.hlc(sid)))
  74. seed = self.member_type(self,idx,nonce,length=ss_idx.type)
  75. assert seed.sid == sid,'{} != {}: Seed ID mismatch!'.format(seed.sid,sid)
  76. return seed
  77. def get_subseed_by_seed_id(self,sid,last_idx=None,print_msg=False):
  78. def get_existing_subseed_by_seed_id(sid):
  79. for k in ('long','short') if self.have_short else ('long',):
  80. if sid in self.data[k]:
  81. idx,nonce = self.data[k][sid]
  82. return self.member_type(self,idx,nonce,length=k)
  83. def do_msg(subseed):
  84. if print_msg:
  85. qmsg('{} {} ({}:{})'.format(
  86. green('Found subseed'),
  87. subseed.sid.hl(),
  88. self.parent_seed.sid.hl(),
  89. subseed.ss_idx.hl(),
  90. ))
  91. if last_idx == None:
  92. last_idx = g.subseeds
  93. subseed = get_existing_subseed_by_seed_id(sid)
  94. if subseed:
  95. do_msg(subseed)
  96. return subseed
  97. if len(self) >= last_idx:
  98. return None
  99. self._generate(last_idx,last_sid=sid)
  100. subseed = get_existing_subseed_by_seed_id(sid)
  101. if subseed:
  102. do_msg(subseed)
  103. return subseed
  104. def _collision_debug_msg(self,sid,idx,nonce,nonce_desc='nonce',debug_last_share=False):
  105. slen = 'short' if sid in self.data['short'] else 'long'
  106. m1 = 'add_subseed(idx={},{}):'.format(idx,slen)
  107. if sid == self.parent_seed.sid:
  108. m2 = 'collision with parent Seed ID {},'.format(sid)
  109. else:
  110. if debug_last_share:
  111. sl = self.debug_last_share_sid_len
  112. colliding_idx = [d[:sl] for d in self.data[slen].keys].index(sid[:sl]) + 1
  113. sid = sid[:sl]
  114. else:
  115. colliding_idx = self.data[slen][sid][0]
  116. m2 = 'collision with ID {} (idx={},{}),'.format(sid,colliding_idx,slen)
  117. msg('{:30} {:46} incrementing {} to {}'.format(m1,m2,nonce_desc,nonce+1))
  118. def _generate(self,last_idx=None,last_sid=None):
  119. if last_idx == None:
  120. last_idx = g.subseeds
  121. first_idx = len(self) + 1
  122. if first_idx > last_idx:
  123. return None
  124. if last_sid != None:
  125. last_sid = SeedID(sid=last_sid)
  126. def add_subseed(idx,length):
  127. for nonce in range(self.nonce_start,self.member_type.max_nonce+1): # handle SeedID collisions
  128. sid = make_chksum_8(self.member_type.make_subseed_bin(self,idx,nonce,length))
  129. if sid in self.data['long'] or sid in self.data['short'] or sid == self.parent_seed.sid:
  130. if g.debug_subseed: # should get ≈450 collisions for first 1,000,000 subseeds
  131. self._collision_debug_msg(sid,idx,nonce)
  132. else:
  133. self.data[length][sid] = (idx,nonce)
  134. return last_sid == sid
  135. else: # must exit here, as this could leave self.data in inconsistent state
  136. raise SubSeedNonceRangeExceeded('add_subseed(): nonce range exceeded')
  137. for idx in SubSeedIdxRange(first_idx,last_idx).iterate():
  138. match1 = add_subseed(idx,'long')
  139. match2 = add_subseed(idx,'short') if self.have_short else False
  140. if match1 or match2:
  141. break
  142. def format(self,first_idx,last_idx):
  143. r = SubSeedIdxRange(first_idx,last_idx)
  144. if len(self) < last_idx:
  145. self._generate(last_idx)
  146. fs1 = '{:>18} {:>18}\n'
  147. fs2 = '{i:>7}L: {:8} {i:>7}S: {:8}\n'
  148. hdr = '{:>16} {} ({} bits)\n\n'.format('Parent Seed:',self.parent_seed.sid.hl(),self.parent_seed.bitlen)
  149. hdr += fs1.format('Long Subseeds','Short Subseeds')
  150. hdr += fs1.format('-------------','--------------')
  151. sl = self.data['long'].keys
  152. ss = self.data['short'].keys
  153. body = (fs2.format(sl[n-1],ss[n-1],i=n) for n in r.iterate())
  154. return hdr + ''.join(body)
  155. class Seed(SeedBase):
  156. def __init__(self,seed_bin=None):
  157. self.subseeds = SubSeedList(self)
  158. SeedBase.__init__(self,seed_bin=seed_bin)
  159. def subseed(self,ss_idx_in,print_msg=False):
  160. return self.subseeds.get_subseed_by_ss_idx(ss_idx_in,print_msg=print_msg)
  161. def subseed_by_seed_id(self,sid,last_idx=None,print_msg=False):
  162. return self.subseeds.get_subseed_by_seed_id(sid,last_idx=last_idx,print_msg=print_msg)
  163. def split(self,count,id_str=None,master_idx=None):
  164. return SeedShareList(self,count,id_str,master_idx)
  165. @staticmethod
  166. def join_shares(seed_list,master_idx=None,id_str=None):
  167. if not hasattr(seed_list,'__next__'): # seed_list can be iterator or iterable
  168. seed_list = iter(seed_list)
  169. class d(object):
  170. byte_len,ret,count = None,0,0
  171. def add_share(ss):
  172. if d.byte_len:
  173. assert ss.byte_len == d.byte_len,'Seed length mismatch! {} != {}'.format(ss.byte_len,d.byte_len)
  174. else:
  175. d.byte_len = ss.byte_len
  176. d.ret ^= int(ss.data.hex(),16)
  177. d.count += 1
  178. if master_idx:
  179. master_share = next(seed_list)
  180. for ss in seed_list:
  181. add_share(ss)
  182. if master_idx:
  183. add_share(SeedShareMasterJoining(master_idx,master_share,id_str,d.count+1).derived_seed)
  184. SeedShareCount(d.count)
  185. return Seed(seed_bin=d.ret.to_bytes(d.byte_len,'big'))
  186. class SubSeed(SeedBase):
  187. idx = ImmutableAttr(int,typeconv=False)
  188. nonce = ImmutableAttr(int,typeconv=False)
  189. ss_idx = ImmutableAttr(SubSeedIdx)
  190. max_nonce = 1000
  191. def __init__(self,parent_list,idx,nonce,length):
  192. self.idx = idx
  193. self.nonce = nonce
  194. self.ss_idx = str(idx) + { 'long': 'L', 'short': 'S' }[length]
  195. self.parent_list = parent_list
  196. SeedBase.__init__(self,seed_bin=type(self).make_subseed_bin(parent_list,idx,nonce,length))
  197. @staticmethod
  198. def make_subseed_bin(parent_list,idx:int,nonce:int,length:str):
  199. seed = parent_list.parent_seed
  200. short = { 'short': True, 'long': False }[length]
  201. # field maximums: idx: 4294967295 (1000000), nonce: 65535 (1000), short: 255 (1)
  202. scramble_key = idx.to_bytes(4,'big') + nonce.to_bytes(2,'big') + short.to_bytes(1,'big')
  203. return scramble_seed(seed.data,scramble_key)[:16 if short else seed.byte_len]
  204. class SeedShareList(SubSeedList):
  205. have_short = False
  206. split_type = 'N-of-N'
  207. count = ImmutableAttr(SeedShareCount)
  208. id_str = ImmutableAttr(SeedSplitIDString)
  209. def __init__(self,parent_seed,count,id_str=None,master_idx=None,debug_last_share=False):
  210. self.member_type = SeedShare
  211. self.parent_seed = parent_seed
  212. self.id_str = id_str or 'default'
  213. self.count = count
  214. def make_master_share():
  215. for nonce in range(SeedShare.max_nonce+1):
  216. ms = SeedShareMaster(self,master_idx,nonce)
  217. if ms.sid == parent_seed.sid:
  218. if g.debug_subseed:
  219. m = 'master_share seed ID collision with parent seed, incrementing nonce to {}'
  220. msg(m.format(nonce+1))
  221. else:
  222. return ms
  223. raise SubSeedNonceRangeExceeded('nonce range exceeded')
  224. def last_share_debug(last_share):
  225. if not debug_last_share:
  226. return False
  227. sid_len = self.debug_last_share_sid_len
  228. lsid = last_share.sid[:sid_len]
  229. psid = parent_seed.sid[:sid_len]
  230. ssids = [d[:sid_len] for d in self.data['long'].keys]
  231. return (lsid in ssids or lsid == psid)
  232. self.master_share = make_master_share() if master_idx else None
  233. for nonce in range(SeedShare.max_nonce+1):
  234. self.nonce_start = nonce
  235. self.data = { 'long': IndexedDict(), 'short': IndexedDict() } # 'short' is required as a placeholder
  236. if self.master_share:
  237. self.data['long'][self.master_share.sid] = (1,self.master_share.nonce)
  238. self._generate(count-1)
  239. self.last_share = ls = SeedShareLast(self)
  240. if last_share_debug(ls) or ls.sid in self.data['long'] or ls.sid == parent_seed.sid:
  241. # collision: throw out entire split list and redo with new start nonce
  242. if g.debug_subseed:
  243. self._collision_debug_msg(ls.sid,count,nonce,'nonce_start',debug_last_share)
  244. else:
  245. self.data['long'][ls.sid] = (count,nonce)
  246. break
  247. else:
  248. raise SubSeedNonceRangeExceeded('nonce range exceeded')
  249. if g.debug_subseed:
  250. A = parent_seed.data
  251. B = self.join().data
  252. assert A == B,'Data mismatch!\noriginal seed: {!r}\nrejoined seed: {!r}'.format(A,B)
  253. def get_share_by_idx(self,idx,base_seed=False):
  254. if idx < 1 or idx > self.count:
  255. raise RangeError('{}: share index out of range'.format(idx))
  256. elif idx == self.count:
  257. return self.last_share
  258. elif self.master_share and idx == 1:
  259. return self.master_share if base_seed else self.master_share.derived_seed
  260. else:
  261. ss_idx = SubSeedIdx(str(idx) + 'L')
  262. return self.get_subseed_by_ss_idx(ss_idx)
  263. def get_share_by_seed_id(self,sid,base_seed=False):
  264. if sid == self.data['long'].key(self.count-1):
  265. return self.last_share
  266. elif self.master_share and sid == self.data['long'].key(0):
  267. return self.master_share if base_seed else self.master_share.derived_seed
  268. else:
  269. return self.get_subseed_by_seed_id(sid)
  270. def join(self):
  271. return Seed.join_shares(self.get_share_by_idx(i+1) for i in range(len(self)))
  272. def format(self):
  273. assert self.split_type == 'N-of-N'
  274. fs1 = ' {}\n'
  275. fs2 = '{i:>5}: {}\n'
  276. mfs1,mfs2,midx,msid = ('','','','')
  277. if self.master_share:
  278. mfs1,mfs2 = (' with master share #{} ({})',' (master share #{})')
  279. midx,msid = (self.master_share.idx,self.master_share.sid)
  280. hdr = ' {} {} ({} bits)\n'.format('Seed:',self.parent_seed.sid.hl(),self.parent_seed.bitlen)
  281. hdr += ' {} {c}-of-{c} (XOR){m}\n'.format('Split Type:',c=self.count,m=mfs1.format(midx,msid))
  282. hdr += ' {} {}\n\n'.format('ID String:',self.id_str.hl())
  283. hdr += fs1.format('Shares')
  284. hdr += fs1.format('------')
  285. sl = self.data['long'].keys
  286. body1 = fs2.format(sl[0]+mfs2.format(midx),i=1)
  287. body = (fs2.format(sl[n],i=n+1) for n in range(1,len(self)))
  288. return hdr + body1 + ''.join(body)
  289. class SeedShareBase(MMGenObject):
  290. @property
  291. def fn_stem(self):
  292. pl = self.parent_list
  293. msdata = '_with_master{}'.format(pl.master_share.idx) if pl.master_share else ''
  294. return '{}-{}-{}of{}{}[{}]'.format(
  295. pl.parent_seed.sid,
  296. pl.id_str,
  297. self.idx,
  298. pl.count,
  299. msdata,
  300. self.sid)
  301. @property
  302. def desc(self):
  303. return self.get_desc()
  304. def get_desc(self,ui=False):
  305. pl = self.parent_list
  306. mss = ', with master share #{}'.format(pl.master_share.idx) if pl.master_share else ''
  307. if ui:
  308. m = ( yellow("(share {} of {} of ")
  309. + pl.parent_seed.sid.hl()
  310. + yellow(', split id ')
  311. + pl.id_str.hl(encl="''")
  312. + yellow('{})') )
  313. else:
  314. m = "share {} of {} of " + pl.parent_seed.sid + ", split id '" + pl.id_str + "'{}"
  315. return m.format(self.idx,pl.count,mss)
  316. class SeedShare(SeedShareBase,SubSeed):
  317. @staticmethod
  318. def make_subseed_bin(parent_list,idx:int,nonce:int,length:str):
  319. seed = parent_list.parent_seed
  320. assert parent_list.have_short == False
  321. assert length == 'long'
  322. # field maximums: id_str: none (256 chars), count: 65535 (1024), idx: 65535 (1024), nonce: 65535 (1000)
  323. scramble_key = '{}:{}:'.format(parent_list.split_type,parent_list.id_str).encode() + \
  324. parent_list.count.to_bytes(2,'big') + idx.to_bytes(2,'big') + nonce.to_bytes(2,'big')
  325. if parent_list.master_share:
  326. scramble_key += b':master:' + parent_list.master_share.idx.to_bytes(2,'big')
  327. return scramble_seed(seed.data,scramble_key)[:seed.byte_len]
  328. class SeedShareLast(SeedShareBase,SeedBase):
  329. idx = ImmutableAttr(SeedShareIdx)
  330. nonce = 0
  331. def __init__(self,parent_list):
  332. self.idx = parent_list.count
  333. self.parent_list = parent_list
  334. SeedBase.__init__(self,seed_bin=self.make_subseed_bin(parent_list))
  335. @staticmethod
  336. def make_subseed_bin(parent_list):
  337. seed_list = (parent_list.get_share_by_idx(i+1) for i in range(len(parent_list)))
  338. seed = parent_list.parent_seed
  339. ret = int(seed.data.hex(),16)
  340. for ss in seed_list:
  341. ret ^= int(ss.data.hex(),16)
  342. return ret.to_bytes(seed.byte_len,'big')
  343. class SeedShareMaster(SeedBase,SeedShareBase):
  344. idx = ImmutableAttr(MasterShareIdx)
  345. nonce = ImmutableAttr(int,typeconv=False)
  346. def __init__(self,parent_list,idx,nonce):
  347. self.idx = idx
  348. self.nonce = nonce
  349. self.parent_list = parent_list
  350. SeedBase.__init__(self,self.make_base_seed_bin())
  351. self.derived_seed = SeedBase(self.make_derived_seed_bin(parent_list.id_str,parent_list.count))
  352. @property
  353. def fn_stem(self):
  354. return '{}-MASTER{}[{}]'.format(
  355. self.parent_list.parent_seed.sid,
  356. self.idx,
  357. self.sid)
  358. def make_base_seed_bin(self):
  359. seed = self.parent_list.parent_seed
  360. # field maximums: idx: 65535 (1024)
  361. scramble_key = b'master_share:' + self.idx.to_bytes(2,'big') + self.nonce.to_bytes(2,'big')
  362. return scramble_seed(seed.data,scramble_key)[:seed.byte_len]
  363. # Don't bother with avoiding seed ID collision here, as sid of derived seed is not used
  364. # by user as an identifier
  365. def make_derived_seed_bin(self,id_str,count):
  366. # field maximums: id_str: none (256 chars), count: 65535 (1024)
  367. scramble_key = id_str.encode() + b':' + count.to_bytes(2,'big')
  368. return scramble_seed(self.data,scramble_key)[:self.byte_len]
  369. def get_desc(self,ui=False):
  370. psid = self.parent_list.parent_seed.sid
  371. mss = 'master share #{} of '.format(self.idx)
  372. return yellow('(' + mss) + psid.hl() + yellow(')') if ui else mss + psid
  373. class SeedShareMasterJoining(SeedShareMaster):
  374. id_str = ImmutableAttr(SeedSplitIDString)
  375. count = ImmutableAttr(SeedShareCount)
  376. def __init__(self,idx,base_seed,id_str,count):
  377. SeedBase.__init__(self,seed_bin=base_seed.data)
  378. self.id_str = id_str or 'default'
  379. self.count = count
  380. self.derived_seed = SeedBase(self.make_derived_seed_bin(self.id_str,self.count))