seed.py 50 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660
  1. #!/usr/bin/env python3
  2. #
  3. # mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
  4. # Copyright (C)2013-2019 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. import os
  22. from mmgen.common import *
  23. from mmgen.obj import *
  24. from mmgen.crypto import *
  25. from mmgen.baseconv import *
  26. pnm = g.proj_name
  27. def check_usr_seed_len(seed_len):
  28. if opt.seed_len != seed_len and 'seed_len' in opt.set_by_user:
  29. m = "ERROR: requested seed length ({}) doesn't match seed length of source ({})"
  30. die(1,m.format((opt.seed_len,seed_len)))
  31. def _is_mnemonic(s,fmt):
  32. oq_save = opt.quiet
  33. opt.quiet = True
  34. try:
  35. SeedSource(in_data=s,in_fmt=fmt)
  36. ret = True
  37. except:
  38. ret = False
  39. finally:
  40. opt.quiet = oq_save
  41. return ret
  42. def is_bip39_mnemonic(s): return _is_mnemonic(s,fmt='bip39')
  43. def is_mmgen_mnemonic(s): return _is_mnemonic(s,fmt='words')
  44. class SeedBase(MMGenObject):
  45. data = MMGenImmutableAttr('data',bytes,typeconv=False)
  46. sid = MMGenImmutableAttr('sid',SeedID,typeconv=False)
  47. def __init__(self,seed_bin=None):
  48. if not seed_bin:
  49. # Truncate random data for smaller seed lengths
  50. seed_bin = sha256(get_random(1033)).digest()[:opt.seed_len//8]
  51. elif len(seed_bin)*8 not in g.seed_lens:
  52. die(3,'{}: invalid seed length'.format(len(seed_bin)))
  53. self.data = seed_bin
  54. self.sid = SeedID(seed=self)
  55. @property
  56. def bitlen(self):
  57. return len(self.data) * 8
  58. @property
  59. def byte_len(self):
  60. return len(self.data)
  61. @property
  62. def hexdata(self):
  63. return self.data.hex()
  64. @property
  65. def fn_stem(self):
  66. return self.sid
  67. class SubSeedList(MMGenObject):
  68. have_short = True
  69. nonce_start = 0
  70. def __init__(self,parent_seed):
  71. self.member_type = SubSeed
  72. self.parent_seed = parent_seed
  73. self.data = { 'long': IndexedDict(), 'short': IndexedDict() }
  74. def __len__(self):
  75. return len(self.data['long'])
  76. def get_subseed_by_ss_idx(self,ss_idx_in,print_msg=False):
  77. ss_idx = SubSeedIdx(ss_idx_in)
  78. if print_msg:
  79. msg_r('{} {} of {}...'.format(
  80. green('Generating subseed'),
  81. ss_idx.hl(),
  82. self.parent_seed.sid.hl(),
  83. ))
  84. if ss_idx.idx > len(self):
  85. self._generate(ss_idx.idx)
  86. sid = self.data[ss_idx.type].key(ss_idx.idx-1)
  87. idx,nonce = self.data[ss_idx.type][sid]
  88. if idx != ss_idx.idx:
  89. m = "{} != {}: self.data[{t!r}].key(i) does not match self.data[{t!r}][i]!"
  90. die(3,m.format(idx,ss_idx.idx,t=ss_idx.type))
  91. if print_msg:
  92. msg('\b\b\b => {}'.format(SeedID.hlc(sid)))
  93. seed = self.member_type(self,idx,nonce,length=ss_idx.type)
  94. assert seed.sid == sid,'{} != {}: Seed ID mismatch!'.format(seed.sid,sid)
  95. return seed
  96. def get_subseed_by_seed_id(self,sid,last_idx=None,print_msg=False):
  97. def get_existing_subseed_by_seed_id(sid):
  98. for k in ('long','short') if self.have_short else ('long',):
  99. if sid in self.data[k]:
  100. idx,nonce = self.data[k][sid]
  101. return self.member_type(self,idx,nonce,length=k)
  102. def do_msg(subseed):
  103. if print_msg:
  104. qmsg('{} {} ({}:{})'.format(
  105. green('Found subseed'),
  106. subseed.sid.hl(),
  107. self.parent_seed.sid.hl(),
  108. subseed.ss_idx.hl(),
  109. ))
  110. if last_idx == None:
  111. last_idx = g.subseeds
  112. subseed = get_existing_subseed_by_seed_id(sid)
  113. if subseed:
  114. do_msg(subseed)
  115. return subseed
  116. if len(self) >= last_idx:
  117. return None
  118. self._generate(last_idx,last_sid=sid)
  119. subseed = get_existing_subseed_by_seed_id(sid)
  120. if subseed:
  121. do_msg(subseed)
  122. return subseed
  123. def _collision_debug_msg(self,sid,idx,nonce,nonce_desc='nonce',debug_last_share=False):
  124. slen = 'short' if sid in self.data['short'] else 'long'
  125. m1 = 'add_subseed(idx={},{}):'.format(idx,slen)
  126. if sid == self.parent_seed.sid:
  127. m2 = 'collision with parent Seed ID {},'.format(sid)
  128. else:
  129. if debug_last_share:
  130. sl = g.debug_last_share_sid_len
  131. colliding_idx = [d[:sl] for d in self.data[slen].keys].index(sid[:sl]) + 1
  132. sid = sid[:sl]
  133. else:
  134. colliding_idx = self.data[slen][sid][0]
  135. m2 = 'collision with ID {} (idx={},{}),'.format(sid,colliding_idx,slen)
  136. msg('{:30} {:46} incrementing {} to {}'.format(m1,m2,nonce_desc,nonce+1))
  137. def _generate(self,last_idx=None,last_sid=None):
  138. if last_idx == None:
  139. last_idx = g.subseeds
  140. first_idx = len(self) + 1
  141. if first_idx > last_idx:
  142. return None
  143. if last_sid != None:
  144. last_sid = SeedID(sid=last_sid)
  145. def add_subseed(idx,length):
  146. for nonce in range(self.nonce_start,self.member_type.max_nonce+1): # handle SeedID collisions
  147. sid = make_chksum_8(self.member_type.make_subseed_bin(self,idx,nonce,length))
  148. if sid in self.data['long'] or sid in self.data['short'] or sid == self.parent_seed.sid:
  149. if g.debug_subseed: # should get ≈450 collisions for first 1,000,000 subseeds
  150. self._collision_debug_msg(sid,idx,nonce)
  151. else:
  152. self.data[length][sid] = (idx,nonce)
  153. return last_sid == sid
  154. else: # must exit here, as this could leave self.data in inconsistent state
  155. raise SubSeedNonceRangeExceeded('add_subseed(): nonce range exceeded')
  156. for idx in SubSeedIdxRange(first_idx,last_idx).iterate():
  157. match1 = add_subseed(idx,'long')
  158. match2 = add_subseed(idx,'short') if self.have_short else False
  159. if match1 or match2: break
  160. def format(self,first_idx,last_idx):
  161. r = SubSeedIdxRange(first_idx,last_idx)
  162. if len(self) < last_idx:
  163. self._generate(last_idx)
  164. fs1 = '{:>18} {:>18}\n'
  165. fs2 = '{i:>7}L: {:8} {i:>7}S: {:8}\n'
  166. hdr = '{:>16} {} ({} bits)\n\n'.format('Parent Seed:',self.parent_seed.sid.hl(),self.parent_seed.bitlen)
  167. hdr += fs1.format('Long Subseeds','Short Subseeds')
  168. hdr += fs1.format('-------------','--------------')
  169. sl = self.data['long'].keys
  170. ss = self.data['short'].keys
  171. body = (fs2.format(sl[n-1],ss[n-1],i=n) for n in r.iterate())
  172. return hdr + ''.join(body)
  173. class Seed(SeedBase):
  174. def __init__(self,seed_bin=None):
  175. self.subseeds = SubSeedList(self)
  176. SeedBase.__init__(self,seed_bin=seed_bin)
  177. def subseed(self,ss_idx_in,print_msg=False):
  178. return self.subseeds.get_subseed_by_ss_idx(ss_idx_in,print_msg=print_msg)
  179. def subseed_by_seed_id(self,sid,last_idx=None,print_msg=False):
  180. return self.subseeds.get_subseed_by_seed_id(sid,last_idx=last_idx,print_msg=print_msg)
  181. def split(self,count,id_str=None,master_idx=None):
  182. return SeedShareList(self,count,id_str,master_idx)
  183. @staticmethod
  184. def join_shares(seed_list,master_idx=None,id_str=None):
  185. if not hasattr(seed_list,'__next__'): # seed_list can be iterator or iterable
  186. seed_list = iter(seed_list)
  187. class d(object):
  188. byte_len,ret,count = None,0,0
  189. def add_share(ss):
  190. if d.byte_len:
  191. assert ss.byte_len == d.byte_len,'Seed length mismatch! {} != {}'.format(ss.byte_len,d.byte_len)
  192. else:
  193. d.byte_len = ss.byte_len
  194. d.ret ^= int(ss.data.hex(),16)
  195. d.count += 1
  196. if master_idx:
  197. master_share = next(seed_list)
  198. for ss in seed_list:
  199. add_share(ss)
  200. if master_idx:
  201. add_share(SeedShareMasterJoining(master_idx,master_share,id_str,d.count+1).derived_seed)
  202. SeedShareCount(d.count)
  203. return Seed(seed_bin=d.ret.to_bytes(d.byte_len,'big'))
  204. class SubSeed(SeedBase):
  205. idx = MMGenImmutableAttr('idx',int,typeconv=False)
  206. nonce = MMGenImmutableAttr('nonce',int,typeconv=False)
  207. ss_idx = MMGenImmutableAttr('ss_idx',SubSeedIdx)
  208. max_nonce = 1000
  209. def __init__(self,parent_list,idx,nonce,length):
  210. self.idx = idx
  211. self.nonce = nonce
  212. self.ss_idx = str(idx) + { 'long': 'L', 'short': 'S' }[length]
  213. self.parent_list = parent_list
  214. SeedBase.__init__(self,seed_bin=type(self).make_subseed_bin(parent_list,idx,nonce,length))
  215. @staticmethod
  216. def make_subseed_bin(parent_list,idx:int,nonce:int,length:str):
  217. seed = parent_list.parent_seed
  218. short = { 'short': True, 'long': False }[length]
  219. # field maximums: idx: 4294967295 (1000000), nonce: 65535 (1000), short: 255 (1)
  220. scramble_key = idx.to_bytes(4,'big') + nonce.to_bytes(2,'big') + short.to_bytes(1,'big')
  221. return scramble_seed(seed.data,scramble_key)[:16 if short else seed.byte_len]
  222. class SeedShareList(SubSeedList):
  223. have_short = False
  224. split_type = 'N-of-N'
  225. count = MMGenImmutableAttr('count',SeedShareCount)
  226. id_str = MMGenImmutableAttr('id_str',SeedSplitIDString)
  227. def __init__(self,parent_seed,count,id_str=None,master_idx=None,debug_last_share=False):
  228. self.member_type = SeedShare
  229. self.parent_seed = parent_seed
  230. self.id_str = id_str or 'default'
  231. self.count = count
  232. def make_master_share():
  233. for nonce in range(SeedShare.max_nonce+1):
  234. ms = SeedShareMaster(self,master_idx,nonce)
  235. if ms.sid == parent_seed.sid:
  236. if g.debug_subseed:
  237. m = 'master_share seed ID collision with parent seed, incrementing nonce to {}'
  238. msg(m.format(nonce+1))
  239. else:
  240. return ms
  241. raise SubSeedNonceRangeExceeded('nonce range exceeded')
  242. def last_share_debug(last_share):
  243. if not debug_last_share:
  244. return False
  245. sid_len = g.debug_last_share_sid_len
  246. lsid = last_share.sid[:sid_len]
  247. psid = parent_seed.sid[:sid_len]
  248. ssids = [d[:sid_len] for d in self.data['long'].keys]
  249. return (lsid in ssids or lsid == psid)
  250. self.master_share = make_master_share() if master_idx else None
  251. for nonce in range(SeedShare.max_nonce+1):
  252. self.nonce_start = nonce
  253. self.data = { 'long': IndexedDict(), 'short': IndexedDict() } # 'short' is required as a placeholder
  254. if self.master_share:
  255. self.data['long'][self.master_share.sid] = (1,self.master_share.nonce)
  256. self._generate(count-1)
  257. self.last_share = ls = SeedShareLast(self)
  258. if last_share_debug(ls) or ls.sid in self.data['long'] or ls.sid == parent_seed.sid:
  259. # collision: throw out entire split list and redo with new start nonce
  260. if g.debug_subseed:
  261. self._collision_debug_msg(ls.sid,count,nonce,'nonce_start',debug_last_share)
  262. else:
  263. self.data['long'][ls.sid] = (count,nonce)
  264. break
  265. else:
  266. raise SubSeedNonceRangeExceeded('nonce range exceeded')
  267. if g.debug_subseed:
  268. A = parent_seed.data
  269. B = self.join().data
  270. assert A == B,'Data mismatch!\noriginal seed: {!r}\nrejoined seed: {!r}'.format(A,B)
  271. def get_share_by_idx(self,idx,base_seed=False):
  272. if idx < 1 or idx > self.count:
  273. raise RangeError('{}: share index out of range'.format(idx))
  274. elif idx == self.count:
  275. return self.last_share
  276. elif self.master_share and idx == 1:
  277. return self.master_share if base_seed else self.master_share.derived_seed
  278. else:
  279. ss_idx = SubSeedIdx(str(idx) + 'L')
  280. return self.get_subseed_by_ss_idx(ss_idx)
  281. def get_share_by_seed_id(self,sid,base_seed=False):
  282. if sid == self.data['long'].key(self.count-1):
  283. return self.last_share
  284. elif self.master_share and sid == self.data['long'].key(0):
  285. return self.master_share if base_seed else self.master_share.derived_seed
  286. else:
  287. return self.get_subseed_by_seed_id(sid)
  288. def join(self):
  289. return Seed.join_shares(self.get_share_by_idx(i+1) for i in range(len(self)))
  290. def format(self):
  291. assert self.split_type == 'N-of-N'
  292. fs1 = ' {}\n'
  293. fs2 = '{i:>5}: {}\n'
  294. mfs1,mfs2,midx,msid = ('','','','')
  295. if self.master_share:
  296. mfs1,mfs2 = (' with master share #{} ({})',' (master share #{})')
  297. midx,msid = (self.master_share.idx,self.master_share.sid)
  298. hdr = ' {} {} ({} bits)\n'.format('Seed:',self.parent_seed.sid.hl(),self.parent_seed.bitlen)
  299. hdr += ' {} {c}-of-{c} (XOR){m}\n'.format('Split Type:',c=self.count,m=mfs1.format(midx,msid))
  300. hdr += ' {} {}\n\n'.format('ID String:',self.id_str.hl())
  301. hdr += fs1.format('Shares')
  302. hdr += fs1.format('------')
  303. sl = self.data['long'].keys
  304. body1 = fs2.format(sl[0]+mfs2.format(midx),i=1)
  305. body = (fs2.format(sl[n],i=n+1) for n in range(1,len(self)))
  306. return hdr + body1 + ''.join(body)
  307. class SeedShareBase(MMGenObject):
  308. @property
  309. def fn_stem(self):
  310. pl = self.parent_list
  311. msdata = '_with_master{}'.format(pl.master_share.idx) if pl.master_share else ''
  312. return '{}-{}-{}of{}{}[{}]'.format(
  313. pl.parent_seed.sid,
  314. pl.id_str,
  315. self.idx,
  316. pl.count,
  317. msdata,
  318. self.sid)
  319. @property
  320. def desc(self):
  321. return self.get_desc()
  322. def get_desc(self,ui=False):
  323. pl = self.parent_list
  324. mss = ', with master share #{}'.format(pl.master_share.idx) if pl.master_share else ''
  325. if ui:
  326. m = ( yellow("(share {} of {} of ")
  327. + pl.parent_seed.sid.hl()
  328. + yellow(', split id ')
  329. + pl.id_str.hl(encl="''")
  330. + yellow('{})') )
  331. else:
  332. m = "share {} of {} of " + pl.parent_seed.sid + ", split id '" + pl.id_str + "'{}"
  333. return m.format(self.idx,pl.count,mss)
  334. class SeedShare(SeedShareBase,SubSeed):
  335. @staticmethod
  336. def make_subseed_bin(parent_list,idx:int,nonce:int,length:str):
  337. seed = parent_list.parent_seed
  338. assert parent_list.have_short == False
  339. assert length == 'long'
  340. # field maximums: id_str: none (256 chars), count: 65535 (1024), idx: 65535 (1024), nonce: 65535 (1000)
  341. scramble_key = '{}:{}:'.format(parent_list.split_type,parent_list.id_str).encode() + \
  342. parent_list.count.to_bytes(2,'big') + idx.to_bytes(2,'big') + nonce.to_bytes(2,'big')
  343. if parent_list.master_share:
  344. scramble_key += b':master:' + parent_list.master_share.idx.to_bytes(2,'big')
  345. return scramble_seed(seed.data,scramble_key)[:seed.byte_len]
  346. class SeedShareLast(SeedShareBase,SeedBase):
  347. idx = MMGenImmutableAttr('idx',SeedShareIdx)
  348. nonce = 0
  349. def __init__(self,parent_list):
  350. self.idx = parent_list.count
  351. self.parent_list = parent_list
  352. SeedBase.__init__(self,seed_bin=self.make_subseed_bin(parent_list))
  353. @staticmethod
  354. def make_subseed_bin(parent_list):
  355. seed_list = (parent_list.get_share_by_idx(i+1) for i in range(len(parent_list)))
  356. seed = parent_list.parent_seed
  357. ret = int(seed.data.hex(),16)
  358. for ss in seed_list:
  359. ret ^= int(ss.data.hex(),16)
  360. return ret.to_bytes(seed.byte_len,'big')
  361. class SeedShareMaster(SeedBase,SeedShareBase):
  362. idx = MMGenImmutableAttr('idx',MasterShareIdx)
  363. nonce = MMGenImmutableAttr('nonce',int,typeconv=False)
  364. def __init__(self,parent_list,idx,nonce):
  365. self.idx = idx
  366. self.nonce = nonce
  367. self.parent_list = parent_list
  368. SeedBase.__init__(self,self.make_base_seed_bin())
  369. self.derived_seed = SeedBase(self.make_derived_seed_bin(parent_list.id_str,parent_list.count))
  370. @property
  371. def fn_stem(self):
  372. return '{}-MASTER{}[{}]'.format(
  373. self.parent_list.parent_seed.sid,
  374. self.idx,
  375. self.sid)
  376. def make_base_seed_bin(self):
  377. seed = self.parent_list.parent_seed
  378. # field maximums: idx: 65535 (1024)
  379. scramble_key = b'master_share:' + self.idx.to_bytes(2,'big') + self.nonce.to_bytes(2,'big')
  380. return scramble_seed(seed.data,scramble_key)[:seed.byte_len]
  381. # Don't bother with avoiding seed ID collision here, as sid of derived seed is not used
  382. # by user as an identifier
  383. def make_derived_seed_bin(self,id_str,count):
  384. # field maximums: id_str: none (256 chars), count: 65535 (1024)
  385. scramble_key = id_str.encode() + b':' + count.to_bytes(2,'big')
  386. return scramble_seed(self.data,scramble_key)[:self.byte_len]
  387. def get_desc(self,ui=False):
  388. psid = self.parent_list.parent_seed.sid
  389. mss = 'master share #{} of '.format(self.idx)
  390. return yellow('(' + mss) + psid.hl() + yellow(')') if ui else mss + psid
  391. class SeedShareMasterJoining(SeedShareMaster):
  392. id_str = MMGenImmutableAttr('id_str',SeedSplitIDString)
  393. count = MMGenImmutableAttr('count',SeedShareCount)
  394. def __init__(self,idx,base_seed,id_str,count):
  395. SeedBase.__init__(self,seed_bin=base_seed.data)
  396. self.id_str = id_str or 'default'
  397. self.count = count
  398. self.derived_seed = SeedBase(self.make_derived_seed_bin(self.id_str,self.count))
  399. class SeedSource(MMGenObject):
  400. desc = g.proj_name + ' seed source'
  401. file_mode = 'text'
  402. stdin_ok = False
  403. ask_tty = True
  404. no_tty = False
  405. op = None
  406. _msg = {}
  407. class SeedSourceData(MMGenObject): pass
  408. def __new__(cls,fn=None,ss=None,seed_bin=None,seed=None,
  409. passchg=False,in_data=None,ignore_in_fmt=False,in_fmt=None):
  410. in_fmt = in_fmt or opt.in_fmt
  411. def die_on_opt_mismatch(opt,sstype):
  412. opt_sstype = cls.fmt_code_to_type(opt)
  413. compare_or_die(
  414. opt_sstype.__name__, 'input format requested on command line',
  415. sstype.__name__, 'input file format'
  416. )
  417. if seed or seed_bin:
  418. sstype = cls.fmt_code_to_type(opt.out_fmt)
  419. me = super(cls,cls).__new__(sstype or Wallet) # default to Wallet
  420. me.seed = seed or Seed(seed_bin=seed_bin)
  421. me.op = 'new'
  422. elif ss:
  423. sstype = ss.__class__ if passchg else cls.fmt_code_to_type(opt.out_fmt)
  424. me = super(cls,cls).__new__(sstype or Wallet)
  425. me.seed = ss.seed
  426. me.ss_in = ss
  427. me.op = ('conv','pwchg_new')[bool(passchg)]
  428. elif fn or opt.hidden_incog_input_params:
  429. from mmgen.filename import Filename
  430. if fn:
  431. f = Filename(fn)
  432. else:
  433. # permit comma in filename
  434. fn = ','.join(opt.hidden_incog_input_params.split(',')[:-1])
  435. f = Filename(fn,ftype=IncogWalletHidden)
  436. if in_fmt and not ignore_in_fmt:
  437. die_on_opt_mismatch(in_fmt,f.ftype)
  438. me = super(cls,cls).__new__(f.ftype)
  439. me.infile = f
  440. me.op = ('old','pwchg_old')[bool(passchg)]
  441. elif in_fmt:
  442. sstype = cls.fmt_code_to_type(in_fmt)
  443. me = super(cls,cls).__new__(sstype)
  444. me.op = ('old','pwchg_old')[bool(passchg)]
  445. else: # called with no arguments: initialize with random seed
  446. sstype = cls.fmt_code_to_type(opt.out_fmt)
  447. me = super(cls,cls).__new__(sstype or Wallet)
  448. me.seed = Seed(None)
  449. me.op = 'new'
  450. return me
  451. def __init__(self,fn=None,ss=None,seed_bin=None,seed=None,
  452. passchg=False,in_data=None,ignore_in_fmt=False,in_fmt=None):
  453. self.ssdata = self.SeedSourceData()
  454. self.msg = {}
  455. self.in_data = in_data
  456. for c in reversed(self.__class__.__mro__):
  457. if hasattr(c,'_msg'):
  458. self.msg.update(c._msg)
  459. if hasattr(self,'seed'):
  460. self._encrypt()
  461. return
  462. elif hasattr(self,'infile') or self.in_data or not g.stdin_tty:
  463. self._deformat_once()
  464. self._decrypt_retry()
  465. else:
  466. if not self.stdin_ok:
  467. die(1,'Reading from standard input not supported for {} format'.format(self.desc))
  468. self._deformat_retry()
  469. self._decrypt_retry()
  470. m = ('',', seed length {}'.format(self.seed.bitlen))[self.seed.bitlen!=256]
  471. qmsg('Valid {} for Seed ID {}{}'.format(self.desc,self.seed.sid.hl(),m))
  472. def _get_data(self):
  473. if hasattr(self,'infile'):
  474. self.fmt_data = get_data_from_file(self.infile.name,self.desc,binary=self.file_mode=='binary')
  475. elif self.in_data:
  476. self.fmt_data = self.in_data
  477. else:
  478. self.fmt_data = self._get_data_from_user(self.desc)
  479. def _get_data_from_user(self,desc):
  480. return get_data_from_user(desc)
  481. def _deformat_once(self):
  482. self._get_data()
  483. if not self._deformat():
  484. die(2,'Invalid format for input data')
  485. def _deformat_retry(self):
  486. while True:
  487. self._get_data()
  488. if self._deformat(): break
  489. msg('Trying again...')
  490. def _decrypt_retry(self):
  491. while True:
  492. if self._decrypt(): break
  493. if opt.passwd_file:
  494. die(2,'Passphrase from password file, so exiting')
  495. msg('Trying again...')
  496. @classmethod
  497. def get_subclasses_str(cls): # returns name of calling class too
  498. return cls.__name__ + ' ' + ''.join([c.get_subclasses_str() for c in cls.__subclasses__()])
  499. @classmethod
  500. def get_subclasses_easy(cls,acc=[]):
  501. return [globals()[c] for c in cls.get_subclasses_str().split()]
  502. @classmethod
  503. def get_subclasses(cls): # returns calling class too
  504. def GetSubclassesTree(cls,acc):
  505. acc += [cls]
  506. for c in cls.__subclasses__(): GetSubclassesTree(c,acc)
  507. acc = []
  508. GetSubclassesTree(cls,acc)
  509. return acc
  510. @classmethod
  511. def get_extensions(cls):
  512. return [s.ext for s in cls.get_subclasses() if hasattr(s,'ext')]
  513. @classmethod
  514. def fmt_code_to_type(cls,fmt_code):
  515. if not fmt_code: return None
  516. for c in cls.get_subclasses():
  517. if hasattr(c,'fmt_codes') and fmt_code in c.fmt_codes:
  518. return c
  519. return None
  520. @classmethod
  521. def ext_to_type(cls,ext):
  522. if not ext: return None
  523. for c in cls.get_subclasses():
  524. if hasattr(c,'ext') and ext == c.ext:
  525. return c
  526. return None
  527. @classmethod
  528. def format_fmt_codes(cls):
  529. d = [(c.__name__,('.'+c.ext if c.ext else str(c.ext)),','.join(c.fmt_codes))
  530. for c in cls.get_subclasses()
  531. if hasattr(c,'fmt_codes')]
  532. w = max(len(i[0]) for i in d)
  533. ret = ['{:<{w}} {:<9} {}'.format(a,b,c,w=w) for a,b,c in [
  534. ('Format','FileExt','Valid codes'),
  535. ('------','-------','-----------')
  536. ] + sorted(d)]
  537. return '\n'.join(ret) + ('','-α')[g.debug_utf8] + '\n'
  538. def get_fmt_data(self):
  539. self._format()
  540. return self.fmt_data
  541. def write_to_file(self,outdir='',desc=''):
  542. self._format()
  543. kwargs = {
  544. 'desc': desc or self.desc,
  545. 'ask_tty': self.ask_tty,
  546. 'no_tty': self.no_tty,
  547. 'binary': self.file_mode == 'binary'
  548. }
  549. # write_data_to_file(): outfile with absolute path overrides opt.outdir
  550. if outdir:
  551. of = os.path.abspath(os.path.join(outdir,self._filename()))
  552. write_data_to_file(of if outdir else self._filename(),self.fmt_data,**kwargs)
  553. class SeedSourceUnenc(SeedSource):
  554. def _decrypt_retry(self): pass
  555. def _encrypt(self): pass
  556. def _filename(self):
  557. s = self.seed
  558. return '{}[{}]{x}.{}'.format(
  559. s.fn_stem,
  560. s.bitlen,
  561. self.ext,
  562. x='-α' if g.debug_utf8 else '')
  563. def _choose_seedlen(self,desc,ok_lens,subtype):
  564. from mmgen.term import get_char
  565. def choose_len():
  566. prompt = self.choose_seedlen_prompt
  567. while True:
  568. r = get_char('\r'+prompt).decode()
  569. if is_int(r) and 1 <= int(r) <= len(ok_lens):
  570. break
  571. msg_r(('\r','\n')[g.test_suite] + ' '*len(prompt) + '\r')
  572. return ok_lens[int(r)-1]
  573. m1 = blue('{} type:'.format(capfirst(desc)))
  574. m2 = yellow(subtype)
  575. msg('{} {}'.format(m1,m2))
  576. while True:
  577. usr_len = choose_len()
  578. prompt = self.choose_seedlen_confirm.format(usr_len)
  579. if keypress_confirm(prompt,default_yes=True,no_nl=not g.test_suite):
  580. return usr_len
  581. class SeedSourceEnc(SeedSource):
  582. _msg = {
  583. 'choose_passphrase': """
  584. You must choose a passphrase to encrypt your new {} with.
  585. A key will be generated from your passphrase using a hash preset of '{}'.
  586. Please note that no strength checking of passphrases is performed. For
  587. an empty passphrase, just hit ENTER twice.
  588. """.strip()
  589. }
  590. def _get_hash_preset_from_user(self,hp,desc_suf=''):
  591. # hp=a,
  592. n = ('','old ')[self.op=='pwchg_old']
  593. m,n = (('to accept the default',n),('to reuse the old','new '))[
  594. int(self.op=='pwchg_new')]
  595. fs = "Enter {}hash preset for {}{}{},\n or hit ENTER {} value ('{}'): "
  596. p = fs.format(
  597. n,
  598. ('','new ')[self.op=='new'],
  599. self.desc,
  600. ('',' '+desc_suf)[bool(desc_suf)],
  601. m,
  602. hp
  603. )
  604. while True:
  605. ret = my_raw_input(p)
  606. if ret:
  607. if ret in g.hash_presets:
  608. self.ssdata.hash_preset = ret
  609. return ret
  610. else:
  611. msg('Invalid input. Valid choices are {}'.format(', '.join(g.hash_presets)))
  612. else:
  613. self.ssdata.hash_preset = hp
  614. return hp
  615. def _get_hash_preset(self,desc_suf=''):
  616. if hasattr(self,'ss_in') and hasattr(self.ss_in.ssdata,'hash_preset'):
  617. old_hp = self.ss_in.ssdata.hash_preset
  618. if opt.keep_hash_preset:
  619. qmsg("Reusing hash preset '{}' at user request".format(old_hp))
  620. self.ssdata.hash_preset = old_hp
  621. elif 'hash_preset' in opt.set_by_user:
  622. hp = self.ssdata.hash_preset = opt.hash_preset
  623. qmsg("Using hash preset '{}' requested on command line".format(opt.hash_preset))
  624. else: # Prompt, using old value as default
  625. hp = self._get_hash_preset_from_user(old_hp,desc_suf)
  626. if (not opt.keep_hash_preset) and self.op == 'pwchg_new':
  627. m = ("changed to '{}'".format(hp),'unchanged')[hp==old_hp]
  628. qmsg('Hash preset {}'.format(m))
  629. elif 'hash_preset' in opt.set_by_user:
  630. self.ssdata.hash_preset = opt.hash_preset
  631. qmsg("Using hash preset '{}' requested on command line".format(opt.hash_preset))
  632. else:
  633. self._get_hash_preset_from_user(opt.hash_preset,desc_suf)
  634. def _get_new_passphrase(self):
  635. desc = '{}passphrase for {}{}'.format(
  636. ('','new ')[self.op=='pwchg_new'],
  637. ('','new ')[self.op in ('new','conv')],
  638. self.desc
  639. )
  640. if opt.passwd_file:
  641. w = pwfile_reuse_warning()
  642. pw = ' '.join(get_words_from_file(opt.passwd_file,desc,quiet=w))
  643. elif opt.echo_passphrase:
  644. pw = ' '.join(get_words_from_user('Enter {}: '.format(desc)))
  645. else:
  646. mswin_pw_warning()
  647. for i in range(g.passwd_max_tries):
  648. pw = ' '.join(get_words_from_user('Enter {}: '.format(desc)))
  649. pw2 = ' '.join(get_words_from_user('Repeat passphrase: '))
  650. dmsg('Passphrases: [{}] [{}]'.format(pw,pw2))
  651. if pw == pw2:
  652. vmsg('Passphrases match'); break
  653. else: msg('Passphrases do not match. Try again.')
  654. else:
  655. die(2,'User failed to duplicate passphrase in {} attempts'.format(g.passwd_max_tries))
  656. if pw == '': qmsg('WARNING: Empty passphrase')
  657. self.ssdata.passwd = pw
  658. return pw
  659. def _get_passphrase(self,desc_suf=''):
  660. desc = '{}passphrase for {}{}'.format(
  661. ('','old ')[self.op=='pwchg_old'],
  662. self.desc,
  663. ('',' '+desc_suf)[bool(desc_suf)]
  664. )
  665. if opt.passwd_file:
  666. w = pwfile_reuse_warning()
  667. ret = ' '.join(get_words_from_file(opt.passwd_file,desc,quiet=w))
  668. else:
  669. mswin_pw_warning()
  670. ret = ' '.join(get_words_from_user('Enter {}: '.format(desc)))
  671. self.ssdata.passwd = ret
  672. def _get_first_pw_and_hp_and_encrypt_seed(self):
  673. d = self.ssdata
  674. self._get_hash_preset()
  675. if hasattr(self,'ss_in') and hasattr(self.ss_in.ssdata,'passwd'):
  676. old_pw = self.ss_in.ssdata.passwd
  677. if opt.keep_passphrase:
  678. d.passwd = old_pw
  679. qmsg('Reusing passphrase at user request')
  680. else:
  681. pw = self._get_new_passphrase()
  682. if self.op == 'pwchg_new':
  683. m = ('changed','unchanged')[pw==old_pw]
  684. qmsg('Passphrase {}'.format(m))
  685. else:
  686. qmsg(self.msg['choose_passphrase'].format(self.desc,d.hash_preset))
  687. self._get_new_passphrase()
  688. d.salt = sha256(get_random(128)).digest()[:g.salt_len]
  689. key = make_key(d.passwd, d.salt, d.hash_preset)
  690. d.key_id = make_chksum_8(key)
  691. d.enc_seed = encrypt_seed(self.seed.data,key)
  692. class MMGenMnemonic(SeedSourceUnenc):
  693. stdin_ok = True
  694. fmt_codes = 'mmwords','words','mnemonic','mnem','mn','m'
  695. desc = 'MMGen native mnemonic data'
  696. wclass = 'mnemonic'
  697. mn_type = 'MMGen native'
  698. ext = 'mmwords'
  699. wl_id = 'mmgen'
  700. conv_cls = baseconv
  701. choose_seedlen_prompt = 'Choose a mnemonic length: 1) 12 words, 2) 18 words, 3) 24 words: '
  702. choose_seedlen_confirm = 'Mnemonic length of {} words chosen. OK?'
  703. @property
  704. def mn_lens(self):
  705. return sorted(self.conv_cls.seedlen_map_rev[self.wl_id])
  706. def _get_data_from_user(self,desc):
  707. if not g.stdin_tty:
  708. return get_data_from_user(desc)
  709. mn_len = self._choose_seedlen(self.wclass,self.mn_lens,self.mn_type)
  710. self.conv_cls.init_mn(self.wl_id)
  711. wl = self.conv_cls.digits[self.wl_id]
  712. longest_word = max(len(w) for w in wl)
  713. m = 'Enter your {ml}-word seed phrase, hitting ENTER or SPACE after each word.\n'
  714. m += "Optionally, you may use pad characters. Anything you type that's not a\n"
  715. m += 'lowercase letter will be treated as a {lq}pad character{rq}, i.e. it will simply\n'
  716. m += 'be discarded. Pad characters may be typed before, after, or in the middle\n'
  717. m += "of words. For each word, once you've typed {lw} characters total (including\n"
  718. m += 'pad characters) any pad character will enter the word.'
  719. msg(m.format(ml=mn_len,lw=longest_word,lq=g.lq,rq=g.rq))
  720. from string import ascii_lowercase
  721. from mmgen.term import get_char_raw
  722. def get_word():
  723. s,pad = '',0
  724. while True:
  725. ch = get_char_raw('',num_chars=1).decode()
  726. if ch in '\b\x7f':
  727. if s: s = s[:-1]
  728. elif ch in '\n\r ':
  729. if s: break
  730. elif ch not in ascii_lowercase:
  731. pad += 1
  732. if s and pad + len(s) > longest_word:
  733. break
  734. else:
  735. s += ch
  736. return s
  737. def in_list(w):
  738. from bisect import bisect_left
  739. idx = bisect_left(wl,w)
  740. return(True,False)[idx == len(wl) or w != wl[idx]]
  741. p = ('Enter word #{}: ','Incorrect entry. Repeat word #{}: ')
  742. words,err = [],0
  743. while len(words) < mn_len:
  744. msg_r('{r}{s}{r}'.format(r='\r',s=' '*40))
  745. if err == 1: time.sleep(0.1)
  746. msg_r(p[err].format(len(words)+1))
  747. s = get_word()
  748. if in_list(s): words.append(s)
  749. err = (1,0)[in_list(s)]
  750. msg('')
  751. qmsg('Mnemonic successfully entered')
  752. return ' '.join(words)
  753. @staticmethod
  754. def _mn2hex_pad(mn): return len(mn) * 8 // 3
  755. @staticmethod
  756. def _hex2mn_pad(hexnum): return len(hexnum) * 3 // 8
  757. def _format(self):
  758. hexseed = self.seed.hexdata
  759. mn = self.conv_cls.fromhex(hexseed,self.wl_id,self._hex2mn_pad(hexseed))
  760. ret = self.conv_cls.tohex(mn,self.wl_id,self._mn2hex_pad(mn))
  761. # Internal error, so just die on fail
  762. compare_or_die(ret,'recomputed seed',hexseed,'original',e='Internal error')
  763. self.ssdata.mnemonic = mn
  764. self.fmt_data = ' '.join(mn) + '\n'
  765. def _deformat(self):
  766. self.conv_cls.init_mn(self.wl_id)
  767. mn = self.fmt_data.split()
  768. if len(mn) not in self.mn_lens:
  769. m = 'Invalid mnemonic ({} words). Valid numbers of words: {}'
  770. msg(m.format(len(mn),', '.join(map(str,self.mn_lens))))
  771. return False
  772. for n,w in enumerate(mn,1):
  773. if w not in self.conv_cls.digits[self.wl_id]:
  774. msg('Invalid mnemonic: word #{} is not in the {} wordlist'.format(n,self.wl_id.upper()))
  775. return False
  776. hexseed = self.conv_cls.tohex(mn,self.wl_id,self._mn2hex_pad(mn))
  777. ret = self.conv_cls.fromhex(hexseed,self.wl_id,self._hex2mn_pad(hexseed))
  778. if len(hexseed) * 4 not in g.seed_lens:
  779. msg('Invalid mnemonic (produces too large a number)')
  780. return False
  781. # Internal error, so just die
  782. compare_or_die(' '.join(ret),'recomputed mnemonic',' '.join(mn),'original',e='Internal error')
  783. self.seed = Seed(bytes.fromhex(hexseed))
  784. self.ssdata.mnemonic = mn
  785. check_usr_seed_len(self.seed.bitlen)
  786. return True
  787. class BIP39Mnemonic(MMGenMnemonic):
  788. fmt_codes = ('bip39',)
  789. desc = 'BIP39 mnemonic data'
  790. mn_type = 'BIP39'
  791. ext = 'bip39'
  792. wl_id = 'bip39'
  793. def __init__(self,*args,**kwargs):
  794. from mmgen.bip39 import bip39
  795. self.conv_cls = bip39
  796. super().__init__(*args,**kwargs)
  797. class MMGenSeedFile(SeedSourceUnenc):
  798. stdin_ok = True
  799. fmt_codes = 'mmseed','seed','s'
  800. desc = 'seed data'
  801. ext = 'mmseed'
  802. def _format(self):
  803. b58seed = baseconv.frombytes(self.seed.data,'b58',pad='seed',tostr=True)
  804. self.ssdata.chksum = make_chksum_6(b58seed)
  805. self.ssdata.b58seed = b58seed
  806. self.fmt_data = '{} {}\n'.format(self.ssdata.chksum,split_into_cols(4,b58seed))
  807. def _deformat(self):
  808. desc = self.desc
  809. ld = self.fmt_data.split()
  810. if not (7 <= len(ld) <= 12): # 6 <= padded b58 data (ld[1:]) <= 11
  811. msg('Invalid data length ({}) in {}'.format(len(ld),desc))
  812. return False
  813. a,b = ld[0],''.join(ld[1:])
  814. if not is_chksum_6(a):
  815. msg("'{}': invalid checksum format in {}".format(a, desc))
  816. return False
  817. if not is_b58_str(b):
  818. msg("'{}': not a base 58 string, in {}".format(b, desc))
  819. return False
  820. vmsg_r('Validating {} checksum...'.format(desc))
  821. if not compare_chksums(a,'file',make_chksum_6(b),'computed',verbose=True):
  822. return False
  823. ret = baseconv.tobytes(b,'b58',pad='seed')
  824. if ret == False:
  825. msg('Invalid base-58 encoded seed: {}'.format(val))
  826. return False
  827. self.seed = Seed(ret)
  828. self.ssdata.chksum = a
  829. self.ssdata.b58seed = b
  830. check_usr_seed_len(self.seed.bitlen)
  831. return True
  832. class DieRollSeedFile(SeedSourceUnenc):
  833. stdin_ok = True
  834. fmt_codes = 'b6d','die','dieroll',
  835. desc = 'base6d die roll seed data'
  836. ext = 'b6d'
  837. conv_cls = baseconv
  838. wclass = 'dieroll'
  839. wl_id = 'b6d'
  840. mn_type = 'base6d'
  841. choose_seedlen_prompt = 'Choose a seed length: 1) 128 bits, 2) 192 bits, 3) 256 bits: '
  842. choose_seedlen_confirm = 'Seed length of {} bits chosen. OK?'
  843. user_entropy_prompt = 'Would you like to provide some additional entropy from the keyboard?'
  844. interactive_input = False
  845. def _format(self):
  846. d = baseconv.frombytes(self.seed.data,'b6d',pad='seed',tostr=True) + '\n'
  847. self.fmt_data = block_format(d,gw=5,cols=5)
  848. def _deformat(self):
  849. d = remove_whitespace(self.fmt_data)
  850. rmap = self.conv_cls.seedlen_map_rev['b6d']
  851. if not len(d) in rmap:
  852. m = '{!r}: invalid length for {} (must be one of {})'
  853. raise SeedLengthError(m.format(len(d),self.desc,list(rmap)))
  854. # truncate seed to correct length, discarding high bits
  855. seed_len = rmap[len(d)]
  856. seed_bytes = baseconv.tobytes(d,'b6d',pad='seed')[-seed_len:]
  857. if self.interactive_input and opt.usr_randchars:
  858. if keypress_confirm(self.user_entropy_prompt):
  859. seed_bytes = add_user_random(seed_bytes,'die roll data')
  860. self.desc += ' plus user-supplied entropy'
  861. self.seed = Seed(seed_bytes)
  862. self.ssdata.hexseed = seed_bytes.hex()
  863. check_usr_seed_len(self.seed.bitlen)
  864. return True
  865. def _get_data_from_user(self,desc):
  866. if not g.stdin_tty:
  867. return get_data_from_user(desc)
  868. seed_bitlens = [n*8 for n in sorted(self.conv_cls.seedlen_map['b6d'])]
  869. seed_bitlen = self._choose_seedlen(self.wclass,seed_bitlens,self.mn_type)
  870. nDierolls = self.conv_cls.seedlen_map['b6d'][seed_bitlen // 8]
  871. m = 'For a {sb}-bit seed you must roll the die {nd} times. After each die roll,\n'
  872. m += 'enter the result on the keyboard as a digit. If you make an invalid entry,\n'
  873. m += "you'll be prompted to re-enter it."
  874. msg('\n'+m.format(sb=seed_bitlen,nd=nDierolls)+'\n')
  875. b6d_digits = self.conv_cls.digits['b6d']
  876. from mmgen.term import get_char,get_char
  877. def get_digit(n):
  878. p = '\b\b\b \rEnter die roll #{}: '+ CUR_SHOW
  879. sleep = 0.3
  880. while True:
  881. ch = get_char(p.format(n),num_chars=1,sleep=sleep).decode()
  882. if ch in b6d_digits:
  883. msg_r(CUR_HIDE + ' OK')
  884. return ch
  885. else:
  886. msg_r(CUR_HIDE + '\rInvalid entry ')
  887. sleep = 0.7
  888. p = '\r' + ' '*25 + CUR_SHOW + p
  889. dierolls,n = [],1
  890. while len(dierolls) < nDierolls:
  891. dierolls.append(get_digit(n))
  892. n += 1
  893. msg('Die rolls successfully entered' + CUR_SHOW)
  894. self.interactive_input = True
  895. return ''.join(dierolls)
  896. class PlainHexSeedFile(SeedSourceUnenc):
  897. stdin_ok = True
  898. fmt_codes = 'hex','rawhex','plainhex'
  899. desc = 'plain hexadecimal seed data'
  900. ext = 'hex'
  901. def _format(self):
  902. self.fmt_data = self.seed.hexdata + '\n'
  903. def _deformat(self):
  904. desc = self.desc
  905. d = self.fmt_data.strip()
  906. if not is_hex_str_lc(d):
  907. msg("'{}': not a lowercase hexidecimal string, in {}".format(d,desc))
  908. return False
  909. if not len(d)*4 in g.seed_lens:
  910. msg('Invalid data length ({}) in {}'.format(len(d),desc))
  911. return False
  912. self.seed = Seed(bytes.fromhex(d))
  913. self.ssdata.hexseed = d
  914. check_usr_seed_len(self.seed.bitlen)
  915. return True
  916. class MMGenHexSeedFile(SeedSourceUnenc):
  917. stdin_ok = True
  918. fmt_codes = 'seedhex','hexseed','mmhex'
  919. desc = 'hexadecimal seed data with checksum'
  920. ext = 'mmhex'
  921. def _format(self):
  922. h = self.seed.hexdata
  923. self.ssdata.chksum = make_chksum_6(h)
  924. self.ssdata.hexseed = h
  925. self.fmt_data = '{} {}\n'.format(self.ssdata.chksum, split_into_cols(4,h))
  926. def _deformat(self):
  927. desc = self.desc
  928. d = self.fmt_data.split()
  929. try:
  930. d[1]
  931. chk,hstr = d[0],''.join(d[1:])
  932. except:
  933. msg("'{}': invalid {}".format(self.fmt_data.strip(),desc))
  934. return False
  935. if not len(hstr)*4 in g.seed_lens:
  936. msg('Invalid data length ({}) in {}'.format(len(hstr),desc))
  937. return False
  938. if not is_chksum_6(chk):
  939. msg("'{}': invalid checksum format in {}".format(chk, desc))
  940. return False
  941. if not is_hex_str(hstr):
  942. msg("'{}': not a hexadecimal string, in {}".format(hstr, desc))
  943. return False
  944. vmsg_r('Validating {} checksum...'.format(desc))
  945. if not compare_chksums(chk,'file',make_chksum_6(hstr),'computed',verbose=True):
  946. return False
  947. self.seed = Seed(bytes.fromhex(hstr))
  948. self.ssdata.chksum = chk
  949. self.ssdata.hexseed = hstr
  950. check_usr_seed_len(self.seed.bitlen)
  951. return True
  952. class Wallet (SeedSourceEnc):
  953. fmt_codes = 'wallet','w'
  954. desc = g.proj_name + ' wallet'
  955. ext = 'mmdat'
  956. def _get_label_from_user(self,old_lbl=''):
  957. d = "to reuse the label '{}'".format(old_lbl.hl()) if old_lbl else 'for no label'
  958. p = 'Enter a wallet label, or hit ENTER {}: '.format(d)
  959. while True:
  960. msg_r(p)
  961. ret = my_raw_input('')
  962. if ret:
  963. self.ssdata.label = MMGenWalletLabel(ret,on_fail='return')
  964. if self.ssdata.label:
  965. break
  966. else:
  967. msg('Invalid label. Trying again...')
  968. else:
  969. self.ssdata.label = old_lbl or MMGenWalletLabel('No Label')
  970. break
  971. return self.ssdata.label
  972. # nearly identical to _get_hash_preset() - factor?
  973. def _get_label(self):
  974. if hasattr(self,'ss_in') and hasattr(self.ss_in.ssdata,'label'):
  975. old_lbl = self.ss_in.ssdata.label
  976. if opt.keep_label:
  977. qmsg("Reusing label '{}' at user request".format(old_lbl.hl()))
  978. self.ssdata.label = old_lbl
  979. elif opt.label:
  980. qmsg("Using label '{}' requested on command line".format(opt.label.hl()))
  981. lbl = self.ssdata.label = opt.label
  982. else: # Prompt, using old value as default
  983. lbl = self._get_label_from_user(old_lbl)
  984. if (not opt.keep_label) and self.op == 'pwchg_new':
  985. m = ("changed to '{}'".format(lbl),'unchanged')[lbl==old_lbl]
  986. qmsg('Label {}'.format(m))
  987. elif opt.label:
  988. qmsg("Using label '{}' requested on command line".format(opt.label.hl()))
  989. self.ssdata.label = opt.label
  990. else:
  991. self._get_label_from_user()
  992. def _encrypt(self):
  993. self._get_first_pw_and_hp_and_encrypt_seed()
  994. self._get_label()
  995. d = self.ssdata
  996. d.pw_status = ('NE','E')[len(d.passwd)==0]
  997. d.timestamp = make_timestamp()
  998. def _format(self):
  999. d = self.ssdata
  1000. s = self.seed
  1001. slt_fmt = baseconv.frombytes(d.salt,'b58',pad='seed',tostr=True)
  1002. es_fmt = baseconv.frombytes(d.enc_seed,'b58',pad='seed',tostr=True)
  1003. lines = (
  1004. d.label,
  1005. '{} {} {} {} {}'.format(s.sid.lower(), d.key_id.lower(),
  1006. s.bitlen, d.pw_status, d.timestamp),
  1007. '{}: {} {} {}'.format(d.hash_preset,*get_hash_params(d.hash_preset)),
  1008. '{} {}'.format(make_chksum_6(slt_fmt),split_into_cols(4,slt_fmt)),
  1009. '{} {}'.format(make_chksum_6(es_fmt), split_into_cols(4,es_fmt))
  1010. )
  1011. chksum = make_chksum_6(' '.join(lines).encode())
  1012. self.fmt_data = '\n'.join((chksum,)+lines) + '\n'
  1013. def _deformat(self):
  1014. def check_master_chksum(lines,desc):
  1015. if len(lines) != 6:
  1016. msg('Invalid number of lines ({}) in {} data'.format(len(lines),desc))
  1017. return False
  1018. if not is_chksum_6(lines[0]):
  1019. msg('Incorrect master checksum ({}) in {} data'.format(lines[0],desc))
  1020. return False
  1021. chk = make_chksum_6(' '.join(lines[1:]))
  1022. if not compare_chksums(lines[0],'master',chk,'computed',
  1023. hdr='For wallet master checksum',verbose=True):
  1024. return False
  1025. return True
  1026. lines = self.fmt_data.splitlines()
  1027. if not check_master_chksum(lines,self.desc): return False
  1028. d = self.ssdata
  1029. d.label = MMGenWalletLabel(lines[1])
  1030. d1,d2,d3,d4,d5 = lines[2].split()
  1031. d.seed_id = d1.upper()
  1032. d.key_id = d2.upper()
  1033. check_usr_seed_len(int(d3))
  1034. d.pw_status,d.timestamp = d4,d5
  1035. hpdata = lines[3].split()
  1036. d.hash_preset = hp = hpdata[0][:-1] # a string!
  1037. qmsg("Hash preset of wallet: '{}'".format(hp))
  1038. if 'hash_preset' in opt.set_by_user:
  1039. uhp = opt.hash_preset
  1040. if uhp != hp:
  1041. qmsg("Warning: ignoring user-requested hash preset '{}'".format(uhp))
  1042. hash_params = list(map(int,hpdata[1:]))
  1043. if hash_params != get_hash_params(d.hash_preset):
  1044. msg("Hash parameters '{}' don't match hash preset '{}'".format(' '.join(hash_params),d.hash_preset))
  1045. return False
  1046. lmin,foo,lmax = sorted(baseconv.seedlen_map_rev['b58']) # 22,33,44
  1047. for i,key in (4,'salt'),(5,'enc_seed'):
  1048. l = lines[i].split(' ')
  1049. chk = l.pop(0)
  1050. b58_val = ''.join(l)
  1051. if len(b58_val) < lmin or len(b58_val) > lmax:
  1052. msg('Invalid format for {} in {}: {}'.format(key,self.desc,l))
  1053. return False
  1054. if not compare_chksums(chk,key,
  1055. make_chksum_6(b58_val),'computed checksum',verbose=True):
  1056. return False
  1057. val = baseconv.tobytes(b58_val,'b58',pad='seed')
  1058. if val == False:
  1059. msg('Invalid base 58 number: {}'.format(b58_val))
  1060. return False
  1061. setattr(d,key,val)
  1062. return True
  1063. def _decrypt(self):
  1064. d = self.ssdata
  1065. # Needed for multiple transactions with {}-txsign
  1066. suf = ('',os.path.basename(self.infile.name))[bool(opt.quiet)]
  1067. self._get_passphrase(desc_suf=suf)
  1068. key = make_key(d.passwd, d.salt, d.hash_preset)
  1069. ret = decrypt_seed(d.enc_seed, key, d.seed_id, d.key_id)
  1070. if ret:
  1071. self.seed = Seed(ret)
  1072. return True
  1073. else:
  1074. return False
  1075. def _filename(self):
  1076. s = self.seed
  1077. d = self.ssdata
  1078. return '{}-{}[{},{}]{x}.{}'.format(
  1079. s.fn_stem,
  1080. d.key_id,
  1081. s.bitlen,
  1082. d.hash_preset,
  1083. self.ext,
  1084. x='-α' if g.debug_utf8 else '')
  1085. class Brainwallet (SeedSourceEnc):
  1086. stdin_ok = True
  1087. fmt_codes = 'mmbrain','brainwallet','brain','bw','b'
  1088. desc = 'brainwallet'
  1089. ext = 'mmbrain'
  1090. # brainwallet warning message? TODO
  1091. def get_bw_params(self):
  1092. # already checked
  1093. a = opt.brain_params.split(',')
  1094. return int(a[0]),a[1]
  1095. def _deformat(self):
  1096. self.brainpasswd = ' '.join(self.fmt_data.split())
  1097. return True
  1098. def _decrypt(self):
  1099. d = self.ssdata
  1100. # Don't set opt.seed_len! In txsign, BW seed len might differ from other seed srcs
  1101. if opt.brain_params:
  1102. seed_len,d.hash_preset = self.get_bw_params()
  1103. else:
  1104. if 'seed_len' not in opt.set_by_user:
  1105. m1 = 'Using default seed length of {} bits\n'
  1106. m2 = 'If this is not what you want, use the --seed-len option'
  1107. qmsg((m1+m2).format(yellow(str(opt.seed_len))))
  1108. self._get_hash_preset()
  1109. seed_len = opt.seed_len
  1110. qmsg_r('Hashing brainwallet data. Please wait...')
  1111. # Use buflen arg of scrypt.hash() to get seed of desired length
  1112. seed = scrypt_hash_passphrase(self.brainpasswd.encode(),b'',d.hash_preset,buflen=seed_len//8)
  1113. qmsg('Done')
  1114. self.seed = Seed(seed)
  1115. msg('Seed ID: {}'.format(self.seed.sid))
  1116. qmsg('Check this value against your records')
  1117. return True
  1118. def _format(self):
  1119. raise NotImplementedError('Brainwallet not supported as an output format')
  1120. def _encrypt(self):
  1121. raise NotImplementedError('Brainwallet not supported as an output format')
  1122. class IncogWallet (SeedSourceEnc):
  1123. file_mode = 'binary'
  1124. fmt_codes = 'mmincog','incog','icg','i'
  1125. desc = 'incognito data'
  1126. ext = 'mmincog'
  1127. no_tty = True
  1128. _msg = {
  1129. 'check_incog_id': """
  1130. Check the generated Incog ID above against your records. If it doesn't
  1131. match, then your incognito data is incorrect or corrupted.
  1132. """,
  1133. 'record_incog_id': """
  1134. Make a record of the Incog ID but keep it secret. You will use it to
  1135. identify your incog wallet data in the future.
  1136. """,
  1137. 'incorrect_incog_passphrase_try_again': """
  1138. Incorrect passphrase, hash preset, or maybe old-format incog wallet.
  1139. Try again? (Y)es, (n)o, (m)ore information:
  1140. """.strip(),
  1141. 'confirm_seed_id': """
  1142. If the Seed ID above is correct but you're seeing this message, then you need
  1143. to exit and re-run the program with the '--old-incog-fmt' option.
  1144. """.strip(),
  1145. 'dec_chk': " {} hash preset"
  1146. }
  1147. def _make_iv_chksum(self,s): return sha256(s).hexdigest()[:8].upper()
  1148. def _get_incog_data_len(self,seed_len):
  1149. e = (g.hincog_chk_len,0)[bool(opt.old_incog_fmt)]
  1150. return g.aesctr_iv_len + g.salt_len + e + seed_len//8
  1151. def _incog_data_size_chk(self):
  1152. # valid sizes: 56, 64, 72
  1153. dlen = len(self.fmt_data)
  1154. valid_dlen = self._get_incog_data_len(opt.seed_len)
  1155. if dlen == valid_dlen:
  1156. return True
  1157. else:
  1158. if opt.old_incog_fmt:
  1159. msg('WARNING: old-style incognito format requested. Are you sure this is correct?')
  1160. m = 'Invalid incognito data size ({} bytes) for this seed length ({} bits)'
  1161. msg(m.format(dlen,opt.seed_len))
  1162. msg('Valid data size for this seed length: {} bytes'.format(valid_dlen))
  1163. for sl in g.seed_lens:
  1164. if dlen == self._get_incog_data_len(sl):
  1165. die(1,'Valid seed length for this data size: {} bits'.format(sl))
  1166. msg('This data size ({} bytes) is invalid for all available seed lengths'.format(dlen))
  1167. return False
  1168. def _encrypt (self):
  1169. self._get_first_pw_and_hp_and_encrypt_seed()
  1170. if opt.old_incog_fmt:
  1171. die(1,'Writing old-format incog wallets is unsupported')
  1172. d = self.ssdata
  1173. # IV is used BOTH to initialize counter and to salt password!
  1174. d.iv = get_random(g.aesctr_iv_len)
  1175. d.iv_id = self._make_iv_chksum(d.iv)
  1176. msg('New Incog Wallet ID: {}'.format(d.iv_id))
  1177. qmsg('Make a record of this value')
  1178. vmsg(self.msg['record_incog_id'])
  1179. d.salt = get_random(g.salt_len)
  1180. key = make_key(d.passwd, d.salt, d.hash_preset, 'incog wallet key')
  1181. chk = sha256(self.seed.data).digest()[:8]
  1182. d.enc_seed = encrypt_data(chk+self.seed.data, key, g.aesctr_dfl_iv, 'seed')
  1183. d.wrapper_key = make_key(d.passwd, d.iv, d.hash_preset, 'incog wrapper key')
  1184. d.key_id = make_chksum_8(d.wrapper_key)
  1185. vmsg('Key ID: {}'.format(d.key_id))
  1186. d.target_data_len = self._get_incog_data_len(self.seed.bitlen)
  1187. def _format(self):
  1188. d = self.ssdata
  1189. self.fmt_data = d.iv + encrypt_data(d.salt+d.enc_seed, d.wrapper_key, d.iv, self.desc)
  1190. def _filename(self):
  1191. s = self.seed
  1192. d = self.ssdata
  1193. return '{}-{}-{}[{},{}]{x}.{}'.format(
  1194. s.fn_stem,
  1195. d.key_id,
  1196. d.iv_id,
  1197. s.bitlen,
  1198. d.hash_preset,
  1199. self.ext,
  1200. x='-α' if g.debug_utf8 else '')
  1201. def _deformat(self):
  1202. if not self._incog_data_size_chk(): return False
  1203. d = self.ssdata
  1204. d.iv = self.fmt_data[0:g.aesctr_iv_len]
  1205. d.incog_id = self._make_iv_chksum(d.iv)
  1206. d.enc_incog_data = self.fmt_data[g.aesctr_iv_len:]
  1207. msg('Incog Wallet ID: {}'.format(d.incog_id))
  1208. qmsg('Check this value against your records')
  1209. vmsg(self.msg['check_incog_id'])
  1210. return True
  1211. def _verify_seed_newfmt(self,data):
  1212. chk,seed = data[:8],data[8:]
  1213. if sha256(seed).digest()[:8] == chk:
  1214. qmsg('Passphrase{} are correct'.format(self.msg['dec_chk'].format('and')))
  1215. return seed
  1216. else:
  1217. msg('Incorrect passphrase{}'.format(self.msg['dec_chk'].format('or')))
  1218. return False
  1219. def _verify_seed_oldfmt(self,seed):
  1220. m = 'Seed ID: {}. Is the Seed ID correct?'.format(make_chksum_8(seed))
  1221. if keypress_confirm(m, True):
  1222. return seed
  1223. else:
  1224. return False
  1225. def _decrypt(self):
  1226. d = self.ssdata
  1227. self._get_hash_preset(desc_suf=d.incog_id)
  1228. self._get_passphrase(desc_suf=d.incog_id)
  1229. # IV is used BOTH to initialize counter and to salt password!
  1230. key = make_key(d.passwd, d.iv, d.hash_preset, 'wrapper key')
  1231. dd = decrypt_data(d.enc_incog_data, key, d.iv, 'incog data')
  1232. d.salt = dd[0:g.salt_len]
  1233. d.enc_seed = dd[g.salt_len:]
  1234. key = make_key(d.passwd, d.salt, d.hash_preset, 'main key')
  1235. qmsg('Key ID: {}'.format(make_chksum_8(key)))
  1236. verify_seed = getattr(self,'_verify_seed_'+
  1237. ('newfmt','oldfmt')[bool(opt.old_incog_fmt)])
  1238. seed = verify_seed(decrypt_seed(d.enc_seed, key, '', ''))
  1239. if seed:
  1240. self.seed = Seed(seed)
  1241. msg('Seed ID: {}'.format(self.seed.sid))
  1242. return True
  1243. else:
  1244. return False
  1245. class IncogWalletHex (IncogWallet):
  1246. file_mode = 'text'
  1247. desc = 'hex incognito data'
  1248. fmt_codes = 'mmincox','incox','incog_hex','xincog','ix','xi'
  1249. ext = 'mmincox'
  1250. no_tty = False
  1251. def _deformat(self):
  1252. ret = decode_pretty_hexdump(self.fmt_data)
  1253. if ret:
  1254. self.fmt_data = ret
  1255. return IncogWallet._deformat(self)
  1256. else:
  1257. return False
  1258. def _format(self):
  1259. IncogWallet._format(self)
  1260. self.fmt_data = pretty_hexdump(self.fmt_data)
  1261. class IncogWalletHidden (IncogWallet):
  1262. desc = 'hidden incognito data'
  1263. fmt_codes = 'incog_hidden','hincog','ih','hi'
  1264. ext = None
  1265. _msg = {
  1266. 'choose_file_size': """
  1267. You must choose a size for your new hidden incog data. The minimum size is
  1268. {} bytes, which puts the incog data right at the end of the file. Since you
  1269. probably want to hide your data somewhere in the middle of the file where it's
  1270. harder to find, you're advised to choose a much larger file size than this.
  1271. """.strip(),
  1272. 'check_incog_id': """
  1273. Check generated Incog ID above against your records. If it doesn't
  1274. match, then your incognito data is incorrect or corrupted, or you
  1275. may have specified an incorrect offset.
  1276. """,
  1277. 'record_incog_id': """
  1278. Make a record of the Incog ID but keep it secret. You will used it to
  1279. identify the incog wallet data in the future and to locate the offset
  1280. where the data is hidden in the event you forget it.
  1281. """,
  1282. 'dec_chk': ', hash preset, offset {} seed length'
  1283. }
  1284. def _get_hincog_params(self,wtype):
  1285. a = getattr(opt,'hidden_incog_'+ wtype +'_params').split(',')
  1286. return ','.join(a[:-1]),int(a[-1]) # permit comma in filename
  1287. def _check_valid_offset(self,fn,action):
  1288. d = self.ssdata
  1289. m = ('Input','Destination')[action=='write']
  1290. if fn.size < d.hincog_offset + d.target_data_len:
  1291. fs = "{} file '{}' has length {}, too short to {} {} bytes of data at offset {}"
  1292. die(1,fs.format(m,fn.name,fn.size,action,d.target_data_len,d.hincog_offset))
  1293. def _get_data(self):
  1294. d = self.ssdata
  1295. d.hincog_offset = self._get_hincog_params('input')[1]
  1296. qmsg("Getting hidden incog data from file '{}'".format(self.infile.name))
  1297. # Already sanity-checked:
  1298. d.target_data_len = self._get_incog_data_len(opt.seed_len)
  1299. self._check_valid_offset(self.infile,'read')
  1300. flgs = os.O_RDONLY|os.O_BINARY if g.platform == 'win' else os.O_RDONLY
  1301. fh = os.open(self.infile.name,flgs)
  1302. os.lseek(fh,int(d.hincog_offset),os.SEEK_SET)
  1303. self.fmt_data = os.read(fh,d.target_data_len)
  1304. os.close(fh)
  1305. qmsg("Data read from file '{}' at offset {}".format(self.infile.name,d.hincog_offset))
  1306. # overrides method in SeedSource
  1307. def write_to_file(self):
  1308. d = self.ssdata
  1309. self._format()
  1310. compare_or_die(d.target_data_len, 'target data length',
  1311. len(self.fmt_data),'length of formatted ' + self.desc)
  1312. k = ('output','input')[self.op=='pwchg_new']
  1313. fn,d.hincog_offset = self._get_hincog_params(k)
  1314. if opt.outdir and not os.path.dirname(fn):
  1315. fn = os.path.join(opt.outdir,fn)
  1316. check_offset = True
  1317. try:
  1318. os.stat(fn)
  1319. except:
  1320. if keypress_confirm("Requested file '{}' does not exist. Create?".format(fn),default_yes=True):
  1321. min_fsize = d.target_data_len + d.hincog_offset
  1322. msg(self.msg['choose_file_size'].format(min_fsize))
  1323. while True:
  1324. fsize = parse_bytespec(my_raw_input('Enter file size: '))
  1325. if fsize >= min_fsize: break
  1326. msg('File size must be an integer no less than {}'.format(min_fsize))
  1327. from mmgen.tool import MMGenToolCmd
  1328. MMGenToolCmd().rand2file(fn,str(fsize))
  1329. check_offset = False
  1330. else:
  1331. die(1,'Exiting at user request')
  1332. from mmgen.filename import Filename
  1333. f = Filename(fn,ftype=type(self),write=True)
  1334. dmsg('{} data len {}, offset {}'.format(capfirst(self.desc),d.target_data_len,d.hincog_offset))
  1335. if check_offset:
  1336. self._check_valid_offset(f,'write')
  1337. if not opt.quiet:
  1338. confirm_or_raise('',"alter file '{}'".format(f.name))
  1339. flgs = os.O_RDWR|os.O_BINARY if g.platform == 'win' else os.O_RDWR
  1340. fh = os.open(f.name,flgs)
  1341. os.lseek(fh, int(d.hincog_offset), os.SEEK_SET)
  1342. os.write(fh, self.fmt_data)
  1343. os.close(fh)
  1344. msg("{} written to file '{}' at offset {}".format(capfirst(self.desc),f.name,d.hincog_offset))