seed.py 48 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590
  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. class SeedSourceEnc(SeedSource):
  564. _msg = {
  565. 'choose_passphrase': """
  566. You must choose a passphrase to encrypt your new {} with.
  567. A key will be generated from your passphrase using a hash preset of '{}'.
  568. Please note that no strength checking of passphrases is performed. For
  569. an empty passphrase, just hit ENTER twice.
  570. """.strip()
  571. }
  572. def _get_hash_preset_from_user(self,hp,desc_suf=''):
  573. # hp=a,
  574. n = ('','old ')[self.op=='pwchg_old']
  575. m,n = (('to accept the default',n),('to reuse the old','new '))[
  576. int(self.op=='pwchg_new')]
  577. fs = "Enter {}hash preset for {}{}{},\n or hit ENTER {} value ('{}'): "
  578. p = fs.format(
  579. n,
  580. ('','new ')[self.op=='new'],
  581. self.desc,
  582. ('',' '+desc_suf)[bool(desc_suf)],
  583. m,
  584. hp
  585. )
  586. while True:
  587. ret = my_raw_input(p)
  588. if ret:
  589. if ret in g.hash_presets:
  590. self.ssdata.hash_preset = ret
  591. return ret
  592. else:
  593. msg('Invalid input. Valid choices are {}'.format(', '.join(g.hash_presets)))
  594. else:
  595. self.ssdata.hash_preset = hp
  596. return hp
  597. def _get_hash_preset(self,desc_suf=''):
  598. if hasattr(self,'ss_in') and hasattr(self.ss_in.ssdata,'hash_preset'):
  599. old_hp = self.ss_in.ssdata.hash_preset
  600. if opt.keep_hash_preset:
  601. qmsg("Reusing hash preset '{}' at user request".format(old_hp))
  602. self.ssdata.hash_preset = old_hp
  603. elif 'hash_preset' in opt.set_by_user:
  604. hp = self.ssdata.hash_preset = opt.hash_preset
  605. qmsg("Using hash preset '{}' requested on command line".format(opt.hash_preset))
  606. else: # Prompt, using old value as default
  607. hp = self._get_hash_preset_from_user(old_hp,desc_suf)
  608. if (not opt.keep_hash_preset) and self.op == 'pwchg_new':
  609. m = ("changed to '{}'".format(hp),'unchanged')[hp==old_hp]
  610. qmsg('Hash preset {}'.format(m))
  611. elif 'hash_preset' in opt.set_by_user:
  612. self.ssdata.hash_preset = opt.hash_preset
  613. qmsg("Using hash preset '{}' requested on command line".format(opt.hash_preset))
  614. else:
  615. self._get_hash_preset_from_user(opt.hash_preset,desc_suf)
  616. def _get_new_passphrase(self):
  617. desc = '{}passphrase for {}{}'.format(
  618. ('','new ')[self.op=='pwchg_new'],
  619. ('','new ')[self.op in ('new','conv')],
  620. self.desc
  621. )
  622. if opt.passwd_file:
  623. w = pwfile_reuse_warning()
  624. pw = ' '.join(get_words_from_file(opt.passwd_file,desc,quiet=w))
  625. elif opt.echo_passphrase:
  626. pw = ' '.join(get_words_from_user('Enter {}: '.format(desc)))
  627. else:
  628. mswin_pw_warning()
  629. for i in range(g.passwd_max_tries):
  630. pw = ' '.join(get_words_from_user('Enter {}: '.format(desc)))
  631. pw2 = ' '.join(get_words_from_user('Repeat passphrase: '))
  632. dmsg('Passphrases: [{}] [{}]'.format(pw,pw2))
  633. if pw == pw2:
  634. vmsg('Passphrases match'); break
  635. else: msg('Passphrases do not match. Try again.')
  636. else:
  637. die(2,'User failed to duplicate passphrase in {} attempts'.format(g.passwd_max_tries))
  638. if pw == '': qmsg('WARNING: Empty passphrase')
  639. self.ssdata.passwd = pw
  640. return pw
  641. def _get_passphrase(self,desc_suf=''):
  642. desc = '{}passphrase for {}{}'.format(
  643. ('','old ')[self.op=='pwchg_old'],
  644. self.desc,
  645. ('',' '+desc_suf)[bool(desc_suf)]
  646. )
  647. if opt.passwd_file:
  648. w = pwfile_reuse_warning()
  649. ret = ' '.join(get_words_from_file(opt.passwd_file,desc,quiet=w))
  650. else:
  651. mswin_pw_warning()
  652. ret = ' '.join(get_words_from_user('Enter {}: '.format(desc)))
  653. self.ssdata.passwd = ret
  654. def _get_first_pw_and_hp_and_encrypt_seed(self):
  655. d = self.ssdata
  656. self._get_hash_preset()
  657. if hasattr(self,'ss_in') and hasattr(self.ss_in.ssdata,'passwd'):
  658. old_pw = self.ss_in.ssdata.passwd
  659. if opt.keep_passphrase:
  660. d.passwd = old_pw
  661. qmsg('Reusing passphrase at user request')
  662. else:
  663. pw = self._get_new_passphrase()
  664. if self.op == 'pwchg_new':
  665. m = ('changed','unchanged')[pw==old_pw]
  666. qmsg('Passphrase {}'.format(m))
  667. else:
  668. qmsg(self.msg['choose_passphrase'].format(self.desc,d.hash_preset))
  669. self._get_new_passphrase()
  670. d.salt = sha256(get_random(128)).digest()[:g.salt_len]
  671. key = make_key(d.passwd, d.salt, d.hash_preset)
  672. d.key_id = make_chksum_8(key)
  673. d.enc_seed = encrypt_seed(self.seed.data,key)
  674. class MMGenMnemonic(SeedSourceUnenc):
  675. stdin_ok = True
  676. fmt_codes = 'mmwords','words','mnemonic','mnem','mn','m'
  677. desc = 'MMGen native mnemonic data'
  678. mn_name = 'MMGen native'
  679. ext = 'mmwords'
  680. mn_lens = [i // 32 * 3 for i in g.seed_lens]
  681. wl_id = 'mmgen'
  682. conv_cls = baseconv
  683. def _get_data_from_user(self,desc):
  684. if not g.stdin_tty:
  685. return get_data_from_user(desc)
  686. from mmgen.term import get_char_raw,get_char
  687. def choose_mn_len():
  688. prompt = 'Choose a mnemonic length: 1) 12 words, 2) 18 words, 3) 24 words: '
  689. urange = [str(i+1) for i in range(len(self.mn_lens))]
  690. while True:
  691. r = get_char('\r'+prompt).decode()
  692. if r in urange: break
  693. msg_r(('\r','\n')[g.test_suite] + ' '*len(prompt) + '\r')
  694. return self.mn_lens[int(r)-1]
  695. msg('{} {}'.format(blue('Mnemonic type:'),yellow(self.mn_name)))
  696. while True:
  697. mn_len = choose_mn_len()
  698. prompt = 'Mnemonic length of {} words chosen. OK?'.format(mn_len)
  699. if keypress_confirm(prompt,default_yes=True,no_nl=not g.test_suite):
  700. break
  701. self.conv_cls.init_mn(self.wl_id)
  702. wl = self.conv_cls.digits[self.wl_id]
  703. longest_word = max(len(w) for w in wl)
  704. from string import ascii_lowercase
  705. m = 'Enter your {ml}-word seed phrase, hitting ENTER or SPACE after each word.\n'
  706. m += "Optionally, you may use pad characters. Anything you type that's not a\n"
  707. m += 'lowercase letter will be treated as a {lq}pad character{rq}, i.e. it will simply\n'
  708. m += 'be discarded. Pad characters may be typed before, after, or in the middle\n'
  709. m += "of words. For each word, once you've typed {lw} characters total (including\n"
  710. m += 'pad characters) any pad character will enter the word.'
  711. # pexpect chokes on these utf8 chars under MSYS2
  712. lq,rq = (('“','”'),('"','"'))[g.test_suite and g.platform=='win']
  713. msg(m.format(ml=mn_len,lw=longest_word,lq=lq,rq=rq))
  714. def get_word():
  715. s,pad = '',0
  716. while True:
  717. ch = get_char_raw('',num_chars=1).decode()
  718. if ch in '\b\x7f':
  719. if s: s = s[:-1]
  720. elif ch in '\n\r ':
  721. if s: break
  722. elif ch not in ascii_lowercase:
  723. pad += 1
  724. if s and pad + len(s) > longest_word:
  725. break
  726. else:
  727. s += ch
  728. return s
  729. def in_list(w):
  730. from bisect import bisect_left
  731. idx = bisect_left(wl,w)
  732. return(True,False)[idx == len(wl) or w != wl[idx]]
  733. p = ('Enter word #{}: ','Incorrect entry. Repeat word #{}: ')
  734. words,err = [],0
  735. while len(words) < mn_len:
  736. msg_r('{r}{s}{r}'.format(r='\r',s=' '*40))
  737. if err == 1: time.sleep(0.1)
  738. msg_r(p[err].format(len(words)+1))
  739. s = get_word()
  740. if in_list(s): words.append(s)
  741. err = (1,0)[in_list(s)]
  742. msg('')
  743. qmsg('Mnemonic successfully entered')
  744. return ' '.join(words)
  745. @staticmethod
  746. def _mn2hex_pad(mn): return len(mn) * 8 // 3
  747. @staticmethod
  748. def _hex2mn_pad(hexnum): return len(hexnum) * 3 // 8
  749. def _format(self):
  750. hexseed = self.seed.hexdata
  751. mn = self.conv_cls.fromhex(hexseed,self.wl_id,self._hex2mn_pad(hexseed))
  752. ret = self.conv_cls.tohex(mn,self.wl_id,self._mn2hex_pad(mn))
  753. # Internal error, so just die on fail
  754. compare_or_die(ret,'recomputed seed',hexseed,'original',e='Internal error')
  755. self.ssdata.mnemonic = mn
  756. self.fmt_data = ' '.join(mn) + '\n'
  757. def _deformat(self):
  758. self.conv_cls.init_mn(self.wl_id)
  759. mn = self.fmt_data.split()
  760. if len(mn) not in self.mn_lens:
  761. m = 'Invalid mnemonic ({} words). Valid numbers of words: {}'
  762. msg(m.format(len(mn),', '.join(map(str,self.mn_lens))))
  763. return False
  764. for n,w in enumerate(mn,1):
  765. if w not in self.conv_cls.digits[self.wl_id]:
  766. msg('Invalid mnemonic: word #{} is not in the {} wordlist'.format(n,self.wl_id.upper()))
  767. return False
  768. hexseed = self.conv_cls.tohex(mn,self.wl_id,self._mn2hex_pad(mn))
  769. ret = self.conv_cls.fromhex(hexseed,self.wl_id,self._hex2mn_pad(hexseed))
  770. if len(hexseed) * 4 not in g.seed_lens:
  771. msg('Invalid mnemonic (produces too large a number)')
  772. return False
  773. # Internal error, so just die
  774. compare_or_die(' '.join(ret),'recomputed mnemonic',' '.join(mn),'original',e='Internal error')
  775. self.seed = Seed(bytes.fromhex(hexseed))
  776. self.ssdata.mnemonic = mn
  777. check_usr_seed_len(self.seed.bitlen)
  778. return True
  779. class BIP39Mnemonic(MMGenMnemonic):
  780. fmt_codes = ('bip39',)
  781. desc = 'BIP39 mnemonic data'
  782. mn_name = 'BIP39'
  783. ext = 'bip39'
  784. wl_id = 'bip39'
  785. def __init__(self,*args,**kwargs):
  786. from mmgen.bip39 import bip39
  787. self.conv_cls = bip39
  788. super().__init__(*args,**kwargs)
  789. class MMGenSeedFile(SeedSourceUnenc):
  790. stdin_ok = True
  791. fmt_codes = 'mmseed','seed','s'
  792. desc = 'seed data'
  793. ext = 'mmseed'
  794. def _format(self):
  795. b58seed = baseconv.frombytes(self.seed.data,'b58',pad='seed',tostr=True)
  796. self.ssdata.chksum = make_chksum_6(b58seed)
  797. self.ssdata.b58seed = b58seed
  798. self.fmt_data = '{} {}\n'.format(self.ssdata.chksum,split_into_cols(4,b58seed))
  799. def _deformat(self):
  800. desc = self.desc
  801. ld = self.fmt_data.split()
  802. if not (7 <= len(ld) <= 12): # 6 <= padded b58 data (ld[1:]) <= 11
  803. msg('Invalid data length ({}) in {}'.format(len(ld),desc))
  804. return False
  805. a,b = ld[0],''.join(ld[1:])
  806. if not is_chksum_6(a):
  807. msg("'{}': invalid checksum format in {}".format(a, desc))
  808. return False
  809. if not is_b58_str(b):
  810. msg("'{}': not a base 58 string, in {}".format(b, desc))
  811. return False
  812. vmsg_r('Validating {} checksum...'.format(desc))
  813. if not compare_chksums(a,'file',make_chksum_6(b),'computed',verbose=True):
  814. return False
  815. ret = baseconv.tobytes(b,'b58',pad='seed')
  816. if ret == False:
  817. msg('Invalid base-58 encoded seed: {}'.format(val))
  818. return False
  819. self.seed = Seed(ret)
  820. self.ssdata.chksum = a
  821. self.ssdata.b58seed = b
  822. check_usr_seed_len(self.seed.bitlen)
  823. return True
  824. class DieRollSeedFile(SeedSourceUnenc):
  825. stdin_ok = True
  826. fmt_codes = 'b6d','die','dieroll',
  827. desc = 'base6d die roll seed data'
  828. ext = 'b6d'
  829. conv_cls = baseconv
  830. def _format(self):
  831. d = baseconv.frombytes(self.seed.data,'b6d',pad='seed',tostr=True) + '\n'
  832. self.fmt_data = block_format(d,gw=5,cols=5)
  833. def _deformat(self):
  834. d = self.fmt_data.translate(dict((ord(ws),None) for ws in '\t\n '))
  835. # truncate seed to correct length, discarding high bits
  836. seed_len = self.conv_cls.seedlen_map_rev['b6d'][len(d)]
  837. seed_bytes = baseconv.tobytes(d,'b6d',pad='seed')[-seed_len:]
  838. self.seed = Seed(seed_bytes)
  839. self.ssdata.hexseed = seed_bytes.hex()
  840. check_usr_seed_len(self.seed.bitlen)
  841. return True
  842. class PlainHexSeedFile(SeedSourceUnenc):
  843. stdin_ok = True
  844. fmt_codes = 'hex','rawhex','plainhex'
  845. desc = 'plain hexadecimal seed data'
  846. ext = 'hex'
  847. def _format(self):
  848. self.fmt_data = self.seed.hexdata + '\n'
  849. def _deformat(self):
  850. desc = self.desc
  851. d = self.fmt_data.strip()
  852. if not is_hex_str_lc(d):
  853. msg("'{}': not a lowercase hexidecimal string, in {}".format(d,desc))
  854. return False
  855. if not len(d)*4 in g.seed_lens:
  856. msg('Invalid data length ({}) in {}'.format(len(d),desc))
  857. return False
  858. self.seed = Seed(bytes.fromhex(d))
  859. self.ssdata.hexseed = d
  860. check_usr_seed_len(self.seed.bitlen)
  861. return True
  862. class MMGenHexSeedFile(SeedSourceUnenc):
  863. stdin_ok = True
  864. fmt_codes = 'seedhex','hexseed','mmhex'
  865. desc = 'hexadecimal seed data with checksum'
  866. ext = 'mmhex'
  867. def _format(self):
  868. h = self.seed.hexdata
  869. self.ssdata.chksum = make_chksum_6(h)
  870. self.ssdata.hexseed = h
  871. self.fmt_data = '{} {}\n'.format(self.ssdata.chksum, split_into_cols(4,h))
  872. def _deformat(self):
  873. desc = self.desc
  874. d = self.fmt_data.split()
  875. try:
  876. d[1]
  877. chk,hstr = d[0],''.join(d[1:])
  878. except:
  879. msg("'{}': invalid {}".format(self.fmt_data.strip(),desc))
  880. return False
  881. if not len(hstr)*4 in g.seed_lens:
  882. msg('Invalid data length ({}) in {}'.format(len(hstr),desc))
  883. return False
  884. if not is_chksum_6(chk):
  885. msg("'{}': invalid checksum format in {}".format(chk, desc))
  886. return False
  887. if not is_hex_str(hstr):
  888. msg("'{}': not a hexadecimal string, in {}".format(hstr, desc))
  889. return False
  890. vmsg_r('Validating {} checksum...'.format(desc))
  891. if not compare_chksums(chk,'file',make_chksum_6(hstr),'computed',verbose=True):
  892. return False
  893. self.seed = Seed(bytes.fromhex(hstr))
  894. self.ssdata.chksum = chk
  895. self.ssdata.hexseed = hstr
  896. check_usr_seed_len(self.seed.bitlen)
  897. return True
  898. class Wallet (SeedSourceEnc):
  899. fmt_codes = 'wallet','w'
  900. desc = g.proj_name + ' wallet'
  901. ext = 'mmdat'
  902. def _get_label_from_user(self,old_lbl=''):
  903. d = "to reuse the label '{}'".format(old_lbl.hl()) if old_lbl else 'for no label'
  904. p = 'Enter a wallet label, or hit ENTER {}: '.format(d)
  905. while True:
  906. msg_r(p)
  907. ret = my_raw_input('')
  908. if ret:
  909. self.ssdata.label = MMGenWalletLabel(ret,on_fail='return')
  910. if self.ssdata.label:
  911. break
  912. else:
  913. msg('Invalid label. Trying again...')
  914. else:
  915. self.ssdata.label = old_lbl or MMGenWalletLabel('No Label')
  916. break
  917. return self.ssdata.label
  918. # nearly identical to _get_hash_preset() - factor?
  919. def _get_label(self):
  920. if hasattr(self,'ss_in') and hasattr(self.ss_in.ssdata,'label'):
  921. old_lbl = self.ss_in.ssdata.label
  922. if opt.keep_label:
  923. qmsg("Reusing label '{}' at user request".format(old_lbl.hl()))
  924. self.ssdata.label = old_lbl
  925. elif opt.label:
  926. qmsg("Using label '{}' requested on command line".format(opt.label.hl()))
  927. lbl = self.ssdata.label = opt.label
  928. else: # Prompt, using old value as default
  929. lbl = self._get_label_from_user(old_lbl)
  930. if (not opt.keep_label) and self.op == 'pwchg_new':
  931. m = ("changed to '{}'".format(lbl),'unchanged')[lbl==old_lbl]
  932. qmsg('Label {}'.format(m))
  933. elif opt.label:
  934. qmsg("Using label '{}' requested on command line".format(opt.label.hl()))
  935. self.ssdata.label = opt.label
  936. else:
  937. self._get_label_from_user()
  938. def _encrypt(self):
  939. self._get_first_pw_and_hp_and_encrypt_seed()
  940. self._get_label()
  941. d = self.ssdata
  942. d.pw_status = ('NE','E')[len(d.passwd)==0]
  943. d.timestamp = make_timestamp()
  944. def _format(self):
  945. d = self.ssdata
  946. s = self.seed
  947. slt_fmt = baseconv.frombytes(d.salt,'b58',pad='seed',tostr=True)
  948. es_fmt = baseconv.frombytes(d.enc_seed,'b58',pad='seed',tostr=True)
  949. lines = (
  950. d.label,
  951. '{} {} {} {} {}'.format(s.sid.lower(), d.key_id.lower(),
  952. s.bitlen, d.pw_status, d.timestamp),
  953. '{}: {} {} {}'.format(d.hash_preset,*get_hash_params(d.hash_preset)),
  954. '{} {}'.format(make_chksum_6(slt_fmt),split_into_cols(4,slt_fmt)),
  955. '{} {}'.format(make_chksum_6(es_fmt), split_into_cols(4,es_fmt))
  956. )
  957. chksum = make_chksum_6(' '.join(lines).encode())
  958. self.fmt_data = '\n'.join((chksum,)+lines) + '\n'
  959. def _deformat(self):
  960. def check_master_chksum(lines,desc):
  961. if len(lines) != 6:
  962. msg('Invalid number of lines ({}) in {} data'.format(len(lines),desc))
  963. return False
  964. if not is_chksum_6(lines[0]):
  965. msg('Incorrect master checksum ({}) in {} data'.format(lines[0],desc))
  966. return False
  967. chk = make_chksum_6(' '.join(lines[1:]))
  968. if not compare_chksums(lines[0],'master',chk,'computed',
  969. hdr='For wallet master checksum',verbose=True):
  970. return False
  971. return True
  972. lines = self.fmt_data.splitlines()
  973. if not check_master_chksum(lines,self.desc): return False
  974. d = self.ssdata
  975. d.label = MMGenWalletLabel(lines[1])
  976. d1,d2,d3,d4,d5 = lines[2].split()
  977. d.seed_id = d1.upper()
  978. d.key_id = d2.upper()
  979. check_usr_seed_len(int(d3))
  980. d.pw_status,d.timestamp = d4,d5
  981. hpdata = lines[3].split()
  982. d.hash_preset = hp = hpdata[0][:-1] # a string!
  983. qmsg("Hash preset of wallet: '{}'".format(hp))
  984. if 'hash_preset' in opt.set_by_user:
  985. uhp = opt.hash_preset
  986. if uhp != hp:
  987. qmsg("Warning: ignoring user-requested hash preset '{}'".format(uhp))
  988. hash_params = list(map(int,hpdata[1:]))
  989. if hash_params != get_hash_params(d.hash_preset):
  990. msg("Hash parameters '{}' don't match hash preset '{}'".format(' '.join(hash_params),d.hash_preset))
  991. return False
  992. lmin,foo,lmax = sorted(baseconv.seedlen_map_rev['b58']) # 22,33,44
  993. for i,key in (4,'salt'),(5,'enc_seed'):
  994. l = lines[i].split(' ')
  995. chk = l.pop(0)
  996. b58_val = ''.join(l)
  997. if len(b58_val) < lmin or len(b58_val) > lmax:
  998. msg('Invalid format for {} in {}: {}'.format(key,self.desc,l))
  999. return False
  1000. if not compare_chksums(chk,key,
  1001. make_chksum_6(b58_val),'computed checksum',verbose=True):
  1002. return False
  1003. val = baseconv.tobytes(b58_val,'b58',pad='seed')
  1004. if val == False:
  1005. msg('Invalid base 58 number: {}'.format(b58_val))
  1006. return False
  1007. setattr(d,key,val)
  1008. return True
  1009. def _decrypt(self):
  1010. d = self.ssdata
  1011. # Needed for multiple transactions with {}-txsign
  1012. suf = ('',os.path.basename(self.infile.name))[bool(opt.quiet)]
  1013. self._get_passphrase(desc_suf=suf)
  1014. key = make_key(d.passwd, d.salt, d.hash_preset)
  1015. ret = decrypt_seed(d.enc_seed, key, d.seed_id, d.key_id)
  1016. if ret:
  1017. self.seed = Seed(ret)
  1018. return True
  1019. else:
  1020. return False
  1021. def _filename(self):
  1022. s = self.seed
  1023. d = self.ssdata
  1024. return '{}-{}[{},{}]{x}.{}'.format(
  1025. s.fn_stem,
  1026. d.key_id,
  1027. s.bitlen,
  1028. d.hash_preset,
  1029. self.ext,
  1030. x='-α' if g.debug_utf8 else '')
  1031. class Brainwallet (SeedSourceEnc):
  1032. stdin_ok = True
  1033. fmt_codes = 'mmbrain','brainwallet','brain','bw','b'
  1034. desc = 'brainwallet'
  1035. ext = 'mmbrain'
  1036. # brainwallet warning message? TODO
  1037. def get_bw_params(self):
  1038. # already checked
  1039. a = opt.brain_params.split(',')
  1040. return int(a[0]),a[1]
  1041. def _deformat(self):
  1042. self.brainpasswd = ' '.join(self.fmt_data.split())
  1043. return True
  1044. def _decrypt(self):
  1045. d = self.ssdata
  1046. # Don't set opt.seed_len! In txsign, BW seed len might differ from other seed srcs
  1047. if opt.brain_params:
  1048. seed_len,d.hash_preset = self.get_bw_params()
  1049. else:
  1050. if 'seed_len' not in opt.set_by_user:
  1051. m1 = 'Using default seed length of {} bits\n'
  1052. m2 = 'If this is not what you want, use the --seed-len option'
  1053. qmsg((m1+m2).format(yellow(str(opt.seed_len))))
  1054. self._get_hash_preset()
  1055. seed_len = opt.seed_len
  1056. qmsg_r('Hashing brainwallet data. Please wait...')
  1057. # Use buflen arg of scrypt.hash() to get seed of desired length
  1058. seed = scrypt_hash_passphrase(self.brainpasswd.encode(),b'',d.hash_preset,buflen=seed_len//8)
  1059. qmsg('Done')
  1060. self.seed = Seed(seed)
  1061. msg('Seed ID: {}'.format(self.seed.sid))
  1062. qmsg('Check this value against your records')
  1063. return True
  1064. def _format(self):
  1065. raise NotImplementedError('Brainwallet not supported as an output format')
  1066. def _encrypt(self):
  1067. raise NotImplementedError('Brainwallet not supported as an output format')
  1068. class IncogWallet (SeedSourceEnc):
  1069. file_mode = 'binary'
  1070. fmt_codes = 'mmincog','incog','icg','i'
  1071. desc = 'incognito data'
  1072. ext = 'mmincog'
  1073. no_tty = True
  1074. _msg = {
  1075. 'check_incog_id': """
  1076. Check the generated Incog ID above against your records. If it doesn't
  1077. match, then your incognito data is incorrect or corrupted.
  1078. """,
  1079. 'record_incog_id': """
  1080. Make a record of the Incog ID but keep it secret. You will use it to
  1081. identify your incog wallet data in the future.
  1082. """,
  1083. 'incorrect_incog_passphrase_try_again': """
  1084. Incorrect passphrase, hash preset, or maybe old-format incog wallet.
  1085. Try again? (Y)es, (n)o, (m)ore information:
  1086. """.strip(),
  1087. 'confirm_seed_id': """
  1088. If the Seed ID above is correct but you're seeing this message, then you need
  1089. to exit and re-run the program with the '--old-incog-fmt' option.
  1090. """.strip(),
  1091. 'dec_chk': " {} hash preset"
  1092. }
  1093. def _make_iv_chksum(self,s): return sha256(s).hexdigest()[:8].upper()
  1094. def _get_incog_data_len(self,seed_len):
  1095. e = (g.hincog_chk_len,0)[bool(opt.old_incog_fmt)]
  1096. return g.aesctr_iv_len + g.salt_len + e + seed_len//8
  1097. def _incog_data_size_chk(self):
  1098. # valid sizes: 56, 64, 72
  1099. dlen = len(self.fmt_data)
  1100. valid_dlen = self._get_incog_data_len(opt.seed_len)
  1101. if dlen == valid_dlen:
  1102. return True
  1103. else:
  1104. if opt.old_incog_fmt:
  1105. msg('WARNING: old-style incognito format requested. Are you sure this is correct?')
  1106. m = 'Invalid incognito data size ({} bytes) for this seed length ({} bits)'
  1107. msg(m.format(dlen,opt.seed_len))
  1108. msg('Valid data size for this seed length: {} bytes'.format(valid_dlen))
  1109. for sl in g.seed_lens:
  1110. if dlen == self._get_incog_data_len(sl):
  1111. die(1,'Valid seed length for this data size: {} bits'.format(sl))
  1112. msg('This data size ({} bytes) is invalid for all available seed lengths'.format(dlen))
  1113. return False
  1114. def _encrypt (self):
  1115. self._get_first_pw_and_hp_and_encrypt_seed()
  1116. if opt.old_incog_fmt:
  1117. die(1,'Writing old-format incog wallets is unsupported')
  1118. d = self.ssdata
  1119. # IV is used BOTH to initialize counter and to salt password!
  1120. d.iv = get_random(g.aesctr_iv_len)
  1121. d.iv_id = self._make_iv_chksum(d.iv)
  1122. msg('New Incog Wallet ID: {}'.format(d.iv_id))
  1123. qmsg('Make a record of this value')
  1124. vmsg(self.msg['record_incog_id'])
  1125. d.salt = get_random(g.salt_len)
  1126. key = make_key(d.passwd, d.salt, d.hash_preset, 'incog wallet key')
  1127. chk = sha256(self.seed.data).digest()[:8]
  1128. d.enc_seed = encrypt_data(chk+self.seed.data, key, g.aesctr_dfl_iv, 'seed')
  1129. d.wrapper_key = make_key(d.passwd, d.iv, d.hash_preset, 'incog wrapper key')
  1130. d.key_id = make_chksum_8(d.wrapper_key)
  1131. vmsg('Key ID: {}'.format(d.key_id))
  1132. d.target_data_len = self._get_incog_data_len(self.seed.bitlen)
  1133. def _format(self):
  1134. d = self.ssdata
  1135. self.fmt_data = d.iv + encrypt_data(d.salt+d.enc_seed, d.wrapper_key, d.iv, self.desc)
  1136. def _filename(self):
  1137. s = self.seed
  1138. d = self.ssdata
  1139. return '{}-{}-{}[{},{}]{x}.{}'.format(
  1140. s.fn_stem,
  1141. d.key_id,
  1142. d.iv_id,
  1143. s.bitlen,
  1144. d.hash_preset,
  1145. self.ext,
  1146. x='-α' if g.debug_utf8 else '')
  1147. def _deformat(self):
  1148. if not self._incog_data_size_chk(): return False
  1149. d = self.ssdata
  1150. d.iv = self.fmt_data[0:g.aesctr_iv_len]
  1151. d.incog_id = self._make_iv_chksum(d.iv)
  1152. d.enc_incog_data = self.fmt_data[g.aesctr_iv_len:]
  1153. msg('Incog Wallet ID: {}'.format(d.incog_id))
  1154. qmsg('Check this value against your records')
  1155. vmsg(self.msg['check_incog_id'])
  1156. return True
  1157. def _verify_seed_newfmt(self,data):
  1158. chk,seed = data[:8],data[8:]
  1159. if sha256(seed).digest()[:8] == chk:
  1160. qmsg('Passphrase{} are correct'.format(self.msg['dec_chk'].format('and')))
  1161. return seed
  1162. else:
  1163. msg('Incorrect passphrase{}'.format(self.msg['dec_chk'].format('or')))
  1164. return False
  1165. def _verify_seed_oldfmt(self,seed):
  1166. m = 'Seed ID: {}. Is the Seed ID correct?'.format(make_chksum_8(seed))
  1167. if keypress_confirm(m, True):
  1168. return seed
  1169. else:
  1170. return False
  1171. def _decrypt(self):
  1172. d = self.ssdata
  1173. self._get_hash_preset(desc_suf=d.incog_id)
  1174. self._get_passphrase(desc_suf=d.incog_id)
  1175. # IV is used BOTH to initialize counter and to salt password!
  1176. key = make_key(d.passwd, d.iv, d.hash_preset, 'wrapper key')
  1177. dd = decrypt_data(d.enc_incog_data, key, d.iv, 'incog data')
  1178. d.salt = dd[0:g.salt_len]
  1179. d.enc_seed = dd[g.salt_len:]
  1180. key = make_key(d.passwd, d.salt, d.hash_preset, 'main key')
  1181. qmsg('Key ID: {}'.format(make_chksum_8(key)))
  1182. verify_seed = getattr(self,'_verify_seed_'+
  1183. ('newfmt','oldfmt')[bool(opt.old_incog_fmt)])
  1184. seed = verify_seed(decrypt_seed(d.enc_seed, key, '', ''))
  1185. if seed:
  1186. self.seed = Seed(seed)
  1187. msg('Seed ID: {}'.format(self.seed.sid))
  1188. return True
  1189. else:
  1190. return False
  1191. class IncogWalletHex (IncogWallet):
  1192. file_mode = 'text'
  1193. desc = 'hex incognito data'
  1194. fmt_codes = 'mmincox','incox','incog_hex','xincog','ix','xi'
  1195. ext = 'mmincox'
  1196. no_tty = False
  1197. def _deformat(self):
  1198. ret = decode_pretty_hexdump(self.fmt_data)
  1199. if ret:
  1200. self.fmt_data = ret
  1201. return IncogWallet._deformat(self)
  1202. else:
  1203. return False
  1204. def _format(self):
  1205. IncogWallet._format(self)
  1206. self.fmt_data = pretty_hexdump(self.fmt_data)
  1207. class IncogWalletHidden (IncogWallet):
  1208. desc = 'hidden incognito data'
  1209. fmt_codes = 'incog_hidden','hincog','ih','hi'
  1210. ext = None
  1211. _msg = {
  1212. 'choose_file_size': """
  1213. You must choose a size for your new hidden incog data. The minimum size is
  1214. {} bytes, which puts the incog data right at the end of the file. Since you
  1215. probably want to hide your data somewhere in the middle of the file where it's
  1216. harder to find, you're advised to choose a much larger file size than this.
  1217. """.strip(),
  1218. 'check_incog_id': """
  1219. Check generated Incog ID above against your records. If it doesn't
  1220. match, then your incognito data is incorrect or corrupted, or you
  1221. may have specified an incorrect offset.
  1222. """,
  1223. 'record_incog_id': """
  1224. Make a record of the Incog ID but keep it secret. You will used it to
  1225. identify the incog wallet data in the future and to locate the offset
  1226. where the data is hidden in the event you forget it.
  1227. """,
  1228. 'dec_chk': ', hash preset, offset {} seed length'
  1229. }
  1230. def _get_hincog_params(self,wtype):
  1231. a = getattr(opt,'hidden_incog_'+ wtype +'_params').split(',')
  1232. return ','.join(a[:-1]),int(a[-1]) # permit comma in filename
  1233. def _check_valid_offset(self,fn,action):
  1234. d = self.ssdata
  1235. m = ('Input','Destination')[action=='write']
  1236. if fn.size < d.hincog_offset + d.target_data_len:
  1237. fs = "{} file '{}' has length {}, too short to {} {} bytes of data at offset {}"
  1238. die(1,fs.format(m,fn.name,fn.size,action,d.target_data_len,d.hincog_offset))
  1239. def _get_data(self):
  1240. d = self.ssdata
  1241. d.hincog_offset = self._get_hincog_params('input')[1]
  1242. qmsg("Getting hidden incog data from file '{}'".format(self.infile.name))
  1243. # Already sanity-checked:
  1244. d.target_data_len = self._get_incog_data_len(opt.seed_len)
  1245. self._check_valid_offset(self.infile,'read')
  1246. flgs = os.O_RDONLY|os.O_BINARY if g.platform == 'win' else os.O_RDONLY
  1247. fh = os.open(self.infile.name,flgs)
  1248. os.lseek(fh,int(d.hincog_offset),os.SEEK_SET)
  1249. self.fmt_data = os.read(fh,d.target_data_len)
  1250. os.close(fh)
  1251. qmsg("Data read from file '{}' at offset {}".format(self.infile.name,d.hincog_offset))
  1252. # overrides method in SeedSource
  1253. def write_to_file(self):
  1254. d = self.ssdata
  1255. self._format()
  1256. compare_or_die(d.target_data_len, 'target data length',
  1257. len(self.fmt_data),'length of formatted ' + self.desc)
  1258. k = ('output','input')[self.op=='pwchg_new']
  1259. fn,d.hincog_offset = self._get_hincog_params(k)
  1260. if opt.outdir and not os.path.dirname(fn):
  1261. fn = os.path.join(opt.outdir,fn)
  1262. check_offset = True
  1263. try:
  1264. os.stat(fn)
  1265. except:
  1266. if keypress_confirm("Requested file '{}' does not exist. Create?".format(fn),default_yes=True):
  1267. min_fsize = d.target_data_len + d.hincog_offset
  1268. msg(self.msg['choose_file_size'].format(min_fsize))
  1269. while True:
  1270. fsize = parse_bytespec(my_raw_input('Enter file size: '))
  1271. if fsize >= min_fsize: break
  1272. msg('File size must be an integer no less than {}'.format(min_fsize))
  1273. from mmgen.tool import MMGenToolCmd
  1274. MMGenToolCmd().rand2file(fn,str(fsize))
  1275. check_offset = False
  1276. else:
  1277. die(1,'Exiting at user request')
  1278. from mmgen.filename import Filename
  1279. f = Filename(fn,ftype=type(self),write=True)
  1280. dmsg('{} data len {}, offset {}'.format(capfirst(self.desc),d.target_data_len,d.hincog_offset))
  1281. if check_offset:
  1282. self._check_valid_offset(f,'write')
  1283. if not opt.quiet:
  1284. confirm_or_raise('',"alter file '{}'".format(f.name))
  1285. flgs = os.O_RDWR|os.O_BINARY if g.platform == 'win' else os.O_RDWR
  1286. fh = os.open(f.name,flgs)
  1287. os.lseek(fh, int(d.hincog_offset), os.SEEK_SET)
  1288. os.write(fh, self.fmt_data)
  1289. os.close(fh)
  1290. msg("{} written to file '{}' at offset {}".format(capfirst(self.desc),f.name,d.hincog_offset))