seed.py 48 KB

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