seed.py 41 KB

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