seed.py 31 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057
  1. #!/usr/bin/env python
  2. #
  3. # mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
  4. # Copyright (C)2013-2016 Philemon <mmgen-py@yandex.com>
  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 binascii import hexlify,unhexlify
  23. from mmgen.common import *
  24. from mmgen.bitcoin import b58encode_pad,b58decode_pad,b58_lens
  25. from mmgen.obj import *
  26. from mmgen.filename import *
  27. from mmgen.crypto import *
  28. pnm = g.proj_name
  29. def check_usr_seed_len(seed_len):
  30. if opt.seed_len != seed_len and 'seed_len' in opt.set_by_user:
  31. m = 'ERROR: requested seed length (%s) ' + \
  32. "doesn't match seed length of source (%s)"
  33. die(1, m % (opt.seed_len,seed_len))
  34. class Seed(MMGenObject):
  35. def __init__(self,seed_bin=None):
  36. if not seed_bin:
  37. # Truncate random data for smaller seed lengths
  38. seed_bin = sha256(get_random(1033)).digest()[:opt.seed_len/8]
  39. elif len(seed_bin)*8 not in g.seed_lens:
  40. die(3,'%s: invalid seed length' % len(seed_bin))
  41. self.data = seed_bin
  42. self.hexdata = hexlify(seed_bin)
  43. self.sid = SeedID(seed=self)
  44. self.length = len(seed_bin) * 8
  45. def get_data(self):
  46. return self.data
  47. class SeedSource(MMGenObject):
  48. desc = g.proj_name + ' seed source'
  49. file_mode = 'text'
  50. stdin_ok = False
  51. ask_tty = True
  52. no_tty = False
  53. op = None
  54. _msg = {}
  55. class SeedSourceData(MMGenObject): pass
  56. def __new__(cls,fn=None,ss=None,seed=None,ignore_in_fmt=False,passchg=False):
  57. def die_on_opt_mismatch(opt,sstype):
  58. opt_sstype = cls.fmt_code_to_type(opt)
  59. compare_or_die(
  60. opt_sstype.__name__, 'input format requested on command line',
  61. sstype.__name__, 'input file format'
  62. )
  63. if ss:
  64. sstype = ss.__class__ if passchg else cls.fmt_code_to_type(opt.out_fmt)
  65. me = super(cls,cls).__new__(sstype or Wallet) # default: Wallet
  66. me.seed = ss.seed
  67. me.ss_in = ss
  68. me.op = ('conv','pwchg_new')[bool(passchg)]
  69. elif fn or opt.hidden_incog_input_params:
  70. if fn:
  71. f = Filename(fn)
  72. else:
  73. # permit comma in filename
  74. fn = ','.join(opt.hidden_incog_input_params.split(',')[:-1])
  75. f = Filename(fn,ftype=IncogWalletHidden)
  76. if opt.in_fmt and not ignore_in_fmt:
  77. die_on_opt_mismatch(opt.in_fmt,f.ftype)
  78. me = super(cls,cls).__new__(f.ftype)
  79. me.infile = f
  80. me.op = ('old','pwchg_old')[bool(passchg)]
  81. elif opt.in_fmt: # Input format
  82. sstype = cls.fmt_code_to_type(opt.in_fmt)
  83. me = super(cls,cls).__new__(sstype)
  84. me.op = ('old','pwchg_old')[bool(passchg)]
  85. else: # Called with no inputs - initialize with random seed
  86. sstype = cls.fmt_code_to_type(opt.out_fmt)
  87. me = super(cls,cls).__new__(sstype or Wallet) # default: Wallet
  88. me.seed = Seed(seed_bin=seed or None)
  89. me.op = 'new'
  90. # die(1,me.seed.sid.hl()) # DEBUG
  91. return me
  92. def __init__(self,fn=None,ss=None,seed=None,ignore_in_fmt=False,passchg=False):
  93. self.ssdata = self.SeedSourceData()
  94. self.msg = {}
  95. for c in reversed(self.__class__.__mro__):
  96. if hasattr(c,'_msg'):
  97. self.msg.update(c._msg)
  98. if hasattr(self,'seed'):
  99. self._encrypt()
  100. return
  101. elif hasattr(self,'infile'):
  102. self._deformat_once()
  103. self._decrypt_retry()
  104. else:
  105. if not self.stdin_ok:
  106. die(1,'Reading from standard input not supported for %s format'
  107. % self.desc)
  108. self._deformat_retry()
  109. self._decrypt_retry()
  110. m = ('',', seed length %s' % self.seed.length)[self.seed.length!=256]
  111. qmsg('Valid %s for Seed ID %s%s' % (self.desc,self.seed.sid.hl(),m))
  112. def _get_data(self):
  113. if hasattr(self,'infile'):
  114. self.fmt_data = get_data_from_file(self.infile.name,self.desc,
  115. binary=self.file_mode=='binary')
  116. else:
  117. self.fmt_data = get_data_from_user(self.desc)
  118. def _deformat_once(self):
  119. self._get_data()
  120. if not self._deformat():
  121. die(2,'Invalid format for input data')
  122. def _deformat_retry(self):
  123. while True:
  124. self._get_data()
  125. if self._deformat(): break
  126. msg('Trying again...')
  127. def _decrypt_retry(self):
  128. while True:
  129. if self._decrypt(): break
  130. if opt.passwd_file:
  131. die(2,'Passphrase from password file, so exiting')
  132. msg('Trying again...')
  133. @classmethod
  134. def get_subclasses(cls):
  135. if not hasattr(cls,'subclasses'):
  136. gl = globals()
  137. setattr(cls,'subclasses',
  138. [gl[k] for k in gl if type(gl[k]) == type and issubclass(gl[k],cls)])
  139. return cls.subclasses
  140. @classmethod
  141. def get_subclasses_str(cls):
  142. def GetSubclassesTree(cls):
  143. return ''.join([c.__name__ +' '+ GetSubclassesTree(c) for c in cls.__subclasses__()])
  144. return GetSubclassesTree(cls)
  145. @classmethod
  146. def get_extensions(cls):
  147. return [s.ext for s in cls.get_subclasses() if hasattr(s,'ext')]
  148. @classmethod
  149. def fmt_code_to_type(cls,fmt_code):
  150. if not fmt_code: return None
  151. for c in cls.get_subclasses():
  152. if hasattr(c,'fmt_codes') and fmt_code in c.fmt_codes:
  153. return c
  154. return None
  155. @classmethod
  156. def ext_to_type(cls,ext):
  157. if not ext: return None
  158. for c in cls.get_subclasses():
  159. if hasattr(c,'ext') and ext == c.ext:
  160. return c
  161. return None
  162. @classmethod
  163. def format_fmt_codes(cls):
  164. d = [(c.__name__,('.'+c.ext if c.ext else c.ext),','.join(c.fmt_codes))
  165. for c in cls.get_subclasses()
  166. if hasattr(c,'fmt_codes')]
  167. w = max([len(a) for a,b,c in d])
  168. ret = ['{:<{w}} {:<9} {}'.format(a,b,c,w=w) for a,b,c in [
  169. ('Format','FileExt','Valid codes'),
  170. ('------','-------','-----------')
  171. ] + sorted(d)]
  172. return '\n'.join(ret) + '\n'
  173. def get_fmt_data(self):
  174. self._format()
  175. return self.fmt_data
  176. def write_to_file(self,outdir='',desc=''):
  177. self._format()
  178. kwargs = {
  179. 'desc': desc or self.desc,
  180. 'ask_tty': self.ask_tty,
  181. 'no_tty': self.no_tty,
  182. 'binary': self.file_mode == 'binary'
  183. }
  184. # write_data_to_file(): outfile with absolute path overrides opt.outdir
  185. if outdir:
  186. of = os.path.abspath(os.path.join(outdir,self._filename()))
  187. write_data_to_file(of if outdir else self._filename(),self.fmt_data,**kwargs)
  188. class SeedSourceUnenc(SeedSource):
  189. def _decrypt_retry(self): pass
  190. def _encrypt(self): pass
  191. def _filename(self):
  192. return '%s[%s].%s' % (self.seed.sid,self.seed.length,self.ext)
  193. class SeedSourceEnc(SeedSource):
  194. _msg = {
  195. 'choose_passphrase': """
  196. You must choose a passphrase to encrypt your new %s with.
  197. A key will be generated from your passphrase using a hash preset of '%s'.
  198. Please note that no strength checking of passphrases is performed. For
  199. an empty passphrase, just hit ENTER twice.
  200. """.strip()
  201. }
  202. def _get_hash_preset_from_user(self,hp,desc_suf=''):
  203. # hp=a,
  204. n = ('','old ')[self.op=='pwchg_old']
  205. m,n = (('to accept the default',n),('to reuse the old','new '))[
  206. int(self.op=='pwchg_new')]
  207. fs = "Enter {}hash preset for {}{}{},\n or hit ENTER {} value ('{}'): "
  208. p = fs.format(
  209. n,
  210. ('','new ')[self.op=='new'],
  211. self.desc,
  212. ('',' '+desc_suf)[bool(desc_suf)],
  213. m,
  214. hp
  215. )
  216. while True:
  217. ret = my_raw_input(p)
  218. if ret:
  219. if ret in g.hash_presets.keys():
  220. self.ssdata.hash_preset = ret
  221. return ret
  222. else:
  223. msg('Invalid input. Valid choices are %s' %
  224. ', '.join(sorted(g.hash_presets.keys())))
  225. else:
  226. self.ssdata.hash_preset = hp
  227. return hp
  228. def _get_hash_preset(self,desc_suf=''):
  229. if hasattr(self,'ss_in') and hasattr(self.ss_in.ssdata,'hash_preset'):
  230. old_hp = self.ss_in.ssdata.hash_preset
  231. if opt.keep_hash_preset:
  232. qmsg("Reusing hash preset '%s' at user request" % old_hp)
  233. self.ssdata.hash_preset = old_hp
  234. elif 'hash_preset' in opt.set_by_user:
  235. hp = self.ssdata.hash_preset = opt.hash_preset
  236. qmsg("Using hash preset '%s' requested on command line"
  237. % opt.hash_preset)
  238. else: # Prompt, using old value as default
  239. hp = self._get_hash_preset_from_user(old_hp,desc_suf)
  240. if (not opt.keep_hash_preset) and self.op == 'pwchg_new':
  241. m = ("changed to '%s'" % hp,'unchanged')[hp==old_hp]
  242. qmsg('Hash preset %s' % m)
  243. elif 'hash_preset' in opt.set_by_user:
  244. self.ssdata.hash_preset = opt.hash_preset
  245. qmsg("Using hash preset '%s' requested on command line"%opt.hash_preset)
  246. else:
  247. self._get_hash_preset_from_user(opt.hash_preset,desc_suf)
  248. def _get_new_passphrase(self):
  249. desc = '{}passphrase for {}{}'.format(
  250. ('','new ')[self.op=='pwchg_new'],
  251. ('','new ')[self.op in ('new','conv')],
  252. self.desc
  253. )
  254. if opt.passwd_file:
  255. w = pwfile_reuse_warning()
  256. pw = ' '.join(get_words_from_file(opt.passwd_file,desc,silent=w))
  257. elif opt.echo_passphrase:
  258. pw = ' '.join(get_words_from_user('Enter %s: ' % desc))
  259. else:
  260. for i in range(g.passwd_max_tries):
  261. pw = ' '.join(get_words_from_user('Enter %s: ' % desc))
  262. pw2 = ' '.join(get_words_from_user('Repeat passphrase: '))
  263. dmsg('Passphrases: [%s] [%s]' % (pw,pw2))
  264. if pw == pw2:
  265. vmsg('Passphrases match'); break
  266. else: msg('Passphrases do not match. Try again.')
  267. else:
  268. die(2,'User failed to duplicate passphrase in %s attempts' %
  269. g.passwd_max_tries)
  270. if pw == '': qmsg('WARNING: Empty passphrase')
  271. self.ssdata.passwd = pw
  272. return pw
  273. def _get_passphrase(self,desc_suf=''):
  274. desc ='{}passphrase for {}{}'.format(
  275. ('','old ')[self.op=='pwchg_old'],
  276. self.desc,
  277. ('',' '+desc_suf)[bool(desc_suf)]
  278. )
  279. if opt.passwd_file:
  280. w = pwfile_reuse_warning()
  281. ret = ' '.join(get_words_from_file(opt.passwd_file,desc,silent=w))
  282. else:
  283. ret = ' '.join(get_words_from_user('Enter %s: ' % desc))
  284. self.ssdata.passwd = ret
  285. def _get_first_pw_and_hp_and_encrypt_seed(self):
  286. d = self.ssdata
  287. self._get_hash_preset()
  288. if hasattr(self,'ss_in') and hasattr(self.ss_in.ssdata,'passwd'):
  289. old_pw = self.ss_in.ssdata.passwd
  290. if opt.keep_passphrase:
  291. d.passwd = old_pw
  292. qmsg('Reusing passphrase at user request')
  293. else:
  294. pw = self._get_new_passphrase()
  295. if self.op == 'pwchg_new':
  296. m = ('changed','unchanged')[pw==old_pw]
  297. qmsg('Passphrase %s' % m)
  298. else:
  299. qmsg(self.msg['choose_passphrase'] % (self.desc,d.hash_preset))
  300. self._get_new_passphrase()
  301. d.salt = sha256(get_random(128)).digest()[:g.salt_len]
  302. key = make_key(d.passwd, d.salt, d.hash_preset)
  303. d.key_id = make_chksum_8(key)
  304. d.enc_seed = encrypt_seed(self.seed.data,key)
  305. class Mnemonic (SeedSourceUnenc):
  306. stdin_ok = True
  307. fmt_codes = 'mmwords','words','mnemonic','mnem','mn','m'
  308. desc = 'mnemonic data'
  309. ext = 'mmwords'
  310. wl_checksums = {
  311. 'electrum': '5ca31424',
  312. 'tirosh': '1a5faeff'
  313. }
  314. mn_base = 1626
  315. wordlists = sorted(wl_checksums)
  316. dfl_wordlist = 'electrum'
  317. # dfl_wordlist = 'tirosh'
  318. @staticmethod
  319. def _mn2hex_pad(mn): return len(mn) * 8 / 3
  320. @staticmethod
  321. def _hex2mn_pad(hexnum): return len(hexnum) * 3 / 8
  322. @staticmethod
  323. def baseNtohex(base,words_arg,wl,pad=0): # accepts both string and list input
  324. words = words_arg
  325. if type(words) not in (list,tuple):
  326. words = tuple(words.strip())
  327. if not set(words).issubset(set(wl)):
  328. die(2,'{} is not in base-{} format'.format(repr(words_arg),base))
  329. deconv = [wl.index(words[::-1][i])*(base**i)
  330. for i in range(len(words))]
  331. ret = ('{:0%sx}' % pad).format(sum(deconv))
  332. return ('','0')[len(ret) % 2] + ret
  333. @staticmethod
  334. def hextobaseN(base,hexnum,wl,pad=0):
  335. hexnum = hexnum.strip()
  336. if not is_hexstring(hexnum):
  337. die(2,"'%s': not a hexadecimal number" % hexnum)
  338. num,ret = int(hexnum,16),[]
  339. while num:
  340. ret.append(num % base)
  341. num /= base
  342. return [wl[n] for n in [0] * (pad-len(ret)) + ret[::-1]]
  343. @classmethod
  344. def hex2mn(cls,hexnum,wordlist):
  345. wl = cls.get_wordlist(wordlist)
  346. return cls.hextobaseN(cls.mn_base,hexnum,wl,cls._hex2mn_pad(hexnum))
  347. @classmethod
  348. def mn2hex(cls,mn,wordlist):
  349. wl = cls.get_wordlist(wordlist)
  350. return cls.baseNtohex(cls.mn_base,mn,wl,cls._mn2hex_pad(mn))
  351. @classmethod
  352. def get_wordlist(cls,wordlist=None):
  353. wordlist = wordlist or cls.dfl_wordlist
  354. if wordlist not in cls.wordlists:
  355. die(1,"'%s': invalid wordlist. Valid choices: '%s'" %
  356. (wordlist,"' '".join(cls.wordlists)))
  357. return __import__('mmgen.mn_'+wordlist,fromlist=['words']).words.split()
  358. @classmethod
  359. def check_wordlist(cls,wlname):
  360. wl = cls.get_wordlist(wlname)
  361. Msg('Wordlist: %s\nLength: %i words' % (capfirst(wlname),len(wl)))
  362. new_chksum = sha256(' '.join(wl)).hexdigest()[:8]
  363. if (sorted(wl) == wl):
  364. Msg('List is sorted')
  365. else:
  366. die(3,'ERROR: List is not sorted!')
  367. compare_chksums(
  368. new_chksum,'generated checksum',
  369. cls.wl_checksums[wlname],'saved checksum',
  370. die_on_fail=True)
  371. Msg('Checksum %s matches' % new_chksum)
  372. def _format(self):
  373. wl = self.get_wordlist()
  374. seed_hex = self.seed.hexdata
  375. mn = self.hextobaseN(self.mn_base,seed_hex,wl,self._hex2mn_pad(seed_hex))
  376. ret = self.baseNtohex(self.mn_base,mn,wl,self._mn2hex_pad(mn))
  377. # Internal error, so just die on fail
  378. compare_or_die(ret,'recomputed seed',
  379. seed_hex,'original',e='Internal error')
  380. self.ssdata.mnemonic = mn
  381. self.fmt_data = ' '.join(mn) + '\n'
  382. def _deformat(self):
  383. mn = self.fmt_data.split()
  384. wl = self.get_wordlist()
  385. if len(mn) not in g.mn_lens:
  386. msg('Invalid mnemonic (%i words). Allowed numbers of words: %s' %
  387. (len(mn),', '.join([str(i) for i in g.mn_lens])))
  388. return False
  389. for n,w in enumerate(mn,1):
  390. if w not in wl:
  391. msg('Invalid mnemonic: word #%s is not in the wordlist' % n)
  392. return False
  393. seed_hex = self.baseNtohex(self.mn_base,mn,wl,self._mn2hex_pad(mn))
  394. ret = self.hextobaseN(self.mn_base,seed_hex,wl,self._hex2mn_pad(seed_hex))
  395. # Internal error, so just die
  396. compare_or_die(' '.join(ret),'recomputed mnemonic',
  397. ' '.join(mn),'original',e='Internal error')
  398. self.seed = Seed(unhexlify(seed_hex))
  399. self.ssdata.mnemonic = mn
  400. check_usr_seed_len(self.seed.length)
  401. return True
  402. class SeedFile (SeedSourceUnenc):
  403. stdin_ok = True
  404. fmt_codes = 'mmseed','seed','s'
  405. desc = 'seed data'
  406. ext = 'mmseed'
  407. def _format(self):
  408. b58seed = b58encode_pad(self.seed.data)
  409. self.ssdata.chksum = make_chksum_6(b58seed)
  410. self.ssdata.b58seed = b58seed
  411. self.fmt_data = '%s %s\n' % (
  412. self.ssdata.chksum,
  413. split_into_cols(4,b58seed)
  414. )
  415. def _deformat(self):
  416. desc = self.desc
  417. ld = self.fmt_data.split()
  418. if not (7 <= len(ld) <= 12): # 6 <= padded b58 data (ld[1:]) <= 11
  419. msg('Invalid data length (%s) in %s' % (len(ld),desc))
  420. return False
  421. a,b = ld[0],''.join(ld[1:])
  422. if not is_chksum_6(a):
  423. msg("'%s': invalid checksum format in %s" % (a, desc))
  424. return False
  425. if not is_b58string(b):
  426. msg("'%s': not a base 58 string, in %s" % (b, desc))
  427. return False
  428. vmsg_r('Validating %s checksum...' % desc)
  429. if not compare_chksums(a,'file',make_chksum_6(b),'computed',verbose=True):
  430. return False
  431. ret = b58decode_pad(b)
  432. if ret == False:
  433. msg('Invalid base-58 encoded seed: %s' % val)
  434. return False
  435. self.seed = Seed(ret)
  436. self.ssdata.chksum = a
  437. self.ssdata.b58seed = b
  438. check_usr_seed_len(self.seed.length)
  439. return True
  440. class HexSeedFile (SeedSourceUnenc):
  441. stdin_ok = True
  442. fmt_codes = 'seedhex','hexseed','hex','mmhex'
  443. desc = 'hexadecimal seed data'
  444. ext = 'mmhex'
  445. def _format(self):
  446. h = self.seed.hexdata
  447. self.ssdata.chksum = make_chksum_6(h)
  448. self.ssdata.hexseed = h
  449. self.fmt_data = '%s %s\n' % (self.ssdata.chksum, split_into_cols(4,h))
  450. def _deformat(self):
  451. desc = self.desc
  452. d = self.fmt_data.split()
  453. try:
  454. d[1]
  455. chk,hstr = d[0],''.join(d[1:])
  456. except:
  457. msg("'%s': invalid %s" % (self.fmt_data.strip(),desc))
  458. return False
  459. if not len(hstr)*4 in g.seed_lens:
  460. msg('Invalid data length (%s) in %s' % (len(hstr),desc))
  461. return False
  462. if not is_chksum_6(chk):
  463. msg("'%s': invalid checksum format in %s" % (chk, desc))
  464. return False
  465. if not is_hexstring(hstr):
  466. msg("'%s': not a hexadecimal string, in %s" % (hstr, desc))
  467. return False
  468. vmsg_r('Validating %s checksum...' % desc)
  469. if not compare_chksums(chk,'file',make_chksum_6(hstr),'computed',verbose=True):
  470. return False
  471. self.seed = Seed(unhexlify(hstr))
  472. self.ssdata.chksum = chk
  473. self.ssdata.hexseed = hstr
  474. check_usr_seed_len(self.seed.length)
  475. return True
  476. class Wallet (SeedSourceEnc):
  477. fmt_codes = 'wallet','w'
  478. desc = g.proj_name + ' wallet'
  479. ext = 'mmdat'
  480. def _get_label_from_user(self,old_lbl=''):
  481. d = ("to reuse the label '%s'" % old_lbl.hl()) if old_lbl else 'for no label'
  482. p = 'Enter a wallet label, or hit ENTER %s: ' % d
  483. while True:
  484. msg_r(p)
  485. ret = my_raw_input('')
  486. if ret:
  487. self.ssdata.label = MMGenWalletLabel(ret,on_fail='return')
  488. if self.ssdata.label:
  489. break
  490. else:
  491. msg('Invalid label. Trying again...')
  492. else:
  493. self.ssdata.label = old_lbl or MMGenWalletLabel('No Label')
  494. break
  495. return self.ssdata.label
  496. # nearly identical to _get_hash_preset() - factor?
  497. def _get_label(self):
  498. if hasattr(self,'ss_in') and hasattr(self.ss_in.ssdata,'label'):
  499. old_lbl = self.ss_in.ssdata.label
  500. if opt.keep_label:
  501. qmsg("Reusing label '%s' at user request" % old_lbl.hl())
  502. self.ssdata.label = old_lbl
  503. elif opt.label:
  504. qmsg("Using label '%s' requested on command line" % opt.label.hl())
  505. lbl = self.ssdata.label = opt.label
  506. else: # Prompt, using old value as default
  507. lbl = self._get_label_from_user(old_lbl)
  508. if (not opt.keep_label) and self.op == 'pwchg_new':
  509. m = ("changed to '%s'" % lbl,'unchanged')[lbl==old_lbl]
  510. qmsg('Label %s' % m)
  511. elif opt.label:
  512. qmsg("Using label '%s' requested on command line" % opt.label.hl())
  513. self.ssdata.label = opt.label
  514. else:
  515. self._get_label_from_user()
  516. def _encrypt(self):
  517. self._get_first_pw_and_hp_and_encrypt_seed()
  518. self._get_label()
  519. d = self.ssdata
  520. d.pw_status = ('NE','E')[len(d.passwd)==0]
  521. d.timestamp = make_timestamp()
  522. def _format(self):
  523. d = self.ssdata
  524. s = self.seed
  525. slt_fmt = b58encode_pad(d.salt)
  526. es_fmt = b58encode_pad(d.enc_seed)
  527. lines = (
  528. d.label,
  529. '{} {} {} {} {}'.format(s.sid.lower(), d.key_id.lower(),
  530. s.length, d.pw_status, d.timestamp),
  531. '{}: {} {} {}'.format(d.hash_preset,*get_hash_params(d.hash_preset)),
  532. '{} {}'.format(make_chksum_6(slt_fmt),split_into_cols(4,slt_fmt)),
  533. '{} {}'.format(make_chksum_6(es_fmt), split_into_cols(4,es_fmt))
  534. )
  535. chksum = make_chksum_6(' '.join(lines))
  536. self.fmt_data = '%s\n' % '\n'.join((chksum,)+lines)
  537. def _deformat(self):
  538. def check_master_chksum(lines,desc):
  539. if len(lines) != 6:
  540. msg('Invalid number of lines (%s) in %s data' %
  541. (len(lines),desc))
  542. return False
  543. if not is_chksum_6(lines[0]):
  544. msg('Incorrect master checksum (%s) in %s data' %
  545. (lines[0],desc))
  546. return False
  547. chk = make_chksum_6(' '.join(lines[1:]))
  548. if not compare_chksums(lines[0],'master',chk,'computed',
  549. hdr='For wallet master checksum',verbose=True):
  550. return False
  551. return True
  552. lines = self.fmt_data.splitlines()
  553. if not check_master_chksum(lines,self.desc): return False
  554. d = self.ssdata
  555. d.label = MMGenWalletLabel(lines[1])
  556. d1,d2,d3,d4,d5 = lines[2].split()
  557. d.seed_id = d1.upper()
  558. d.key_id = d2.upper()
  559. check_usr_seed_len(int(d3))
  560. d.pw_status,d.timestamp = d4,d5
  561. hpdata = lines[3].split()
  562. d.hash_preset = hp = hpdata[0][:-1] # a string!
  563. qmsg("Hash preset of wallet: '%s'" % hp)
  564. if 'hash_preset' in opt.set_by_user:
  565. uhp = opt.hash_preset
  566. if uhp != hp:
  567. qmsg("Warning: ignoring user-requested hash preset '%s'" % uhp)
  568. hash_params = [int(i) for i in hpdata[1:]]
  569. if hash_params != get_hash_params(d.hash_preset):
  570. msg("Hash parameters '%s' don't match hash preset '%s'" %
  571. (' '.join(hash_params), d.hash_preset))
  572. return False
  573. lmin,lmax = b58_lens[0],b58_lens[-1] # 22,33,44
  574. for i,key in (4,'salt'),(5,'enc_seed'):
  575. l = lines[i].split(' ')
  576. chk = l.pop(0)
  577. b58_val = ''.join(l)
  578. if len(b58_val) < lmin or len(b58_val) > lmax:
  579. msg('Invalid format for %s in %s: %s' % (key,self.desc,l))
  580. return False
  581. if not compare_chksums(chk,key,
  582. make_chksum_6(b58_val),'computed checksum',verbose=True):
  583. return False
  584. val = b58decode_pad(b58_val)
  585. if val == False:
  586. msg('Invalid base 58 number: %s' % b58_val)
  587. return False
  588. setattr(d,key,val)
  589. return True
  590. def _decrypt(self):
  591. d = self.ssdata
  592. # Needed for multiple transactions with {}-txsign
  593. suf = ('',os.path.basename(self.infile.name))[bool(opt.quiet)]
  594. self._get_passphrase(desc_suf=suf)
  595. key = make_key(d.passwd, d.salt, d.hash_preset)
  596. ret = decrypt_seed(d.enc_seed, key, d.seed_id, d.key_id)
  597. if ret:
  598. self.seed = Seed(ret)
  599. return True
  600. else:
  601. return False
  602. def _filename(self):
  603. return '{}-{}[{},{}].{}'.format(
  604. self.seed.sid,
  605. self.ssdata.key_id,
  606. self.seed.length,
  607. self.ssdata.hash_preset,
  608. self.ext
  609. )
  610. class Brainwallet (SeedSourceEnc):
  611. stdin_ok = True
  612. fmt_codes = 'mmbrain','brainwallet','brain','bw','b'
  613. desc = 'brainwallet'
  614. ext = 'mmbrain'
  615. # brainwallet warning message? TODO
  616. def get_bw_params(self):
  617. # already checked
  618. a = opt.brain_params.split(',')
  619. return int(a[0]),a[1]
  620. def _deformat(self):
  621. self.brainpasswd = ' '.join(self.fmt_data.split())
  622. return True
  623. def _decrypt(self):
  624. d = self.ssdata
  625. # Don't set opt.seed_len! In txsign, BW seed len might differ from other seed srcs
  626. if opt.brain_params:
  627. seed_len,d.hash_preset = self.get_bw_params()
  628. else:
  629. if 'seed_len' not in opt.set_by_user:
  630. m1 = 'Using default seed length of %s bits'
  631. m2 = 'If this is not what you want, use the --seed-len option'
  632. qmsg((m1+'\n'+m2) % yellow(str(opt.seed_len)))
  633. self._get_hash_preset()
  634. seed_len = opt.seed_len
  635. qmsg_r('Hashing brainwallet data. Please wait...')
  636. # Use buflen arg of scrypt.hash() to get seed of desired length
  637. seed = scrypt_hash_passphrase(self.brainpasswd, '',
  638. d.hash_preset, buflen=seed_len/8)
  639. qmsg('Done')
  640. self.seed = Seed(seed)
  641. msg('Seed ID: %s' % self.seed.sid)
  642. qmsg('Check this value against your records')
  643. return True
  644. class IncogWallet (SeedSourceEnc):
  645. file_mode = 'binary'
  646. fmt_codes = 'mmincog','incog','icg','i'
  647. desc = 'incognito data'
  648. ext = 'mmincog'
  649. no_tty = True
  650. _msg = {
  651. 'check_incog_id': """
  652. Check the generated Incog ID above against your records. If it doesn't
  653. match, then your incognito data is incorrect or corrupted.
  654. """,
  655. 'record_incog_id': """
  656. Make a record of the Incog ID but keep it secret. You will use it to
  657. identify your incog wallet data in the future.
  658. """,
  659. 'incorrect_incog_passphrase_try_again': """
  660. Incorrect passphrase, hash preset, or maybe old-format incog wallet.
  661. Try again? (Y)es, (n)o, (m)ore information:
  662. """.strip(),
  663. 'confirm_seed_id': """
  664. If the Seed ID above is correct but you're seeing this message, then you need
  665. to exit and re-run the program with the '--old-incog-fmt' option.
  666. """.strip(),
  667. 'dec_chk': " %s hash preset"
  668. }
  669. def _make_iv_chksum(self,s): return sha256(s).hexdigest()[:8].upper()
  670. def _get_incog_data_len(self,seed_len):
  671. e = (g.hincog_chk_len,0)[bool(opt.old_incog_fmt)]
  672. return g.aesctr_iv_len + g.salt_len + e + seed_len/8
  673. def _incog_data_size_chk(self):
  674. # valid sizes: 56, 64, 72
  675. dlen = len(self.fmt_data)
  676. valid_dlen = self._get_incog_data_len(opt.seed_len)
  677. if dlen == valid_dlen:
  678. return True
  679. else:
  680. if opt.old_incog_fmt:
  681. msg('WARNING: old-style incognito format requested. ' +
  682. 'Are you sure this is correct?')
  683. msg(('Invalid incognito data size (%s bytes) for this ' +
  684. 'seed length (%s bits)') % (dlen,opt.seed_len))
  685. msg('Valid data size for this seed length: %s bytes' % valid_dlen)
  686. for sl in g.seed_lens:
  687. if dlen == self._get_incog_data_len(sl):
  688. die(1,'Valid seed length for this data size: %s bits' % sl)
  689. msg(('This data size (%s bytes) is invalid for all available ' +
  690. 'seed lengths') % dlen)
  691. return False
  692. def _encrypt (self):
  693. self._get_first_pw_and_hp_and_encrypt_seed()
  694. if opt.old_incog_fmt:
  695. die(1,'Writing old-format incog wallets is unsupported')
  696. d = self.ssdata
  697. # IV is used BOTH to initialize counter and to salt password!
  698. d.iv = get_random(g.aesctr_iv_len)
  699. d.iv_id = self._make_iv_chksum(d.iv)
  700. msg('New Incog Wallet ID: %s' % d.iv_id)
  701. qmsg('Make a record of this value')
  702. vmsg(self.msg['record_incog_id'])
  703. d.salt = get_random(g.salt_len)
  704. key = make_key(d.passwd, d.salt, d.hash_preset, 'incog wallet key')
  705. chk = sha256(self.seed.data).digest()[:8]
  706. d.enc_seed = encrypt_data(chk + self.seed.data, key, 1, 'seed')
  707. d.wrapper_key = make_key(d.passwd, d.iv, d.hash_preset, 'incog wrapper key')
  708. d.key_id = make_chksum_8(d.wrapper_key)
  709. vmsg('Key ID: %s' % d.key_id)
  710. d.target_data_len = self._get_incog_data_len(self.seed.length)
  711. def _format(self):
  712. d = self.ssdata
  713. # print len(d.iv), len(d.salt), len(d.enc_seed), len(d.wrapper_key)
  714. self.fmt_data = d.iv + encrypt_data(
  715. d.salt + d.enc_seed,
  716. d.wrapper_key,
  717. int(hexlify(d.iv),16),
  718. self.desc)
  719. # print len(self.fmt_data)
  720. def _filename(self):
  721. s = self.seed
  722. d = self.ssdata
  723. return '{}-{}-{}[{},{}].{}'.format(
  724. s.sid,
  725. d.key_id,
  726. d.iv_id,
  727. s.length,
  728. d.hash_preset,
  729. self.ext)
  730. def _deformat(self):
  731. if not self._incog_data_size_chk(): return False
  732. d = self.ssdata
  733. d.iv = self.fmt_data[0:g.aesctr_iv_len]
  734. d.incog_id = self._make_iv_chksum(d.iv)
  735. d.enc_incog_data = self.fmt_data[g.aesctr_iv_len:]
  736. msg('Incog Wallet ID: %s' % d.incog_id)
  737. qmsg('Check this value against your records')
  738. vmsg(self.msg['check_incog_id'])
  739. return True
  740. def _verify_seed_newfmt(self,data):
  741. chk,seed = data[:8],data[8:]
  742. if sha256(seed).digest()[:8] == chk:
  743. qmsg('Passphrase%s are correct' % (self.msg['dec_chk'] % 'and'))
  744. return seed
  745. else:
  746. msg('Incorrect passphrase%s' % (self.msg['dec_chk'] % 'or'))
  747. return False
  748. def _verify_seed_oldfmt(self,seed):
  749. m = 'Seed ID: %s. Is the Seed ID correct?' % make_chksum_8(seed)
  750. if keypress_confirm(m, True):
  751. return seed
  752. else:
  753. return False
  754. def _decrypt(self):
  755. d = self.ssdata
  756. self._get_hash_preset(desc_suf=d.incog_id)
  757. self._get_passphrase(desc_suf=d.incog_id)
  758. # IV is used BOTH to initialize counter and to salt password!
  759. key = make_key(d.passwd, d.iv, d.hash_preset, 'wrapper key')
  760. dd = decrypt_data(d.enc_incog_data, key,
  761. int(hexlify(d.iv),16), 'incog data')
  762. d.salt = dd[0:g.salt_len]
  763. d.enc_seed = dd[g.salt_len:]
  764. key = make_key(d.passwd, d.salt, d.hash_preset, 'main key')
  765. qmsg('Key ID: %s' % make_chksum_8(key))
  766. verify_seed = getattr(self,'_verify_seed_'+
  767. ('newfmt','oldfmt')[bool(opt.old_incog_fmt)])
  768. seed = verify_seed(decrypt_seed(d.enc_seed, key, '', ''))
  769. if seed:
  770. self.seed = Seed(seed)
  771. msg('Seed ID: %s' % self.seed.sid)
  772. return True
  773. else:
  774. return False
  775. class IncogWalletHex (IncogWallet):
  776. file_mode = 'text'
  777. desc = 'hex incognito data'
  778. fmt_codes = 'mmincox','incox','incog_hex','xincog','ix','xi'
  779. ext = 'mmincox'
  780. no_tty = False
  781. def _deformat(self):
  782. ret = decode_pretty_hexdump(self.fmt_data)
  783. if ret:
  784. self.fmt_data = ret
  785. return IncogWallet._deformat(self)
  786. else:
  787. return False
  788. def _format(self):
  789. IncogWallet._format(self)
  790. self.fmt_data = pretty_hexdump(self.fmt_data)
  791. class IncogWalletHidden (IncogWallet):
  792. desc = 'hidden incognito data'
  793. fmt_codes = 'incog_hidden','hincog','ih','hi'
  794. ext = None
  795. _msg = {
  796. 'choose_file_size': """
  797. You must choose a size for your new hidden incog data. The minimum size is
  798. {} bytes, which puts the incog data right at the end of the file. Since you
  799. probably want to hide your data somewhere in the middle of the file where it's
  800. harder to find, you're advised to choose a much larger file size than this.
  801. """.strip(),
  802. 'check_incog_id': """
  803. Check generated Incog ID above against your records. If it doesn't
  804. match, then your incognito data is incorrect or corrupted, or you
  805. may have specified an incorrect offset.
  806. """,
  807. 'record_incog_id': """
  808. Make a record of the Incog ID but keep it secret. You will used it to
  809. identify the incog wallet data in the future and to locate the offset
  810. where the data is hidden in the event you forget it.
  811. """,
  812. 'dec_chk': ', hash preset, offset %s seed length'
  813. }
  814. def _get_hincog_params(self,wtype):
  815. a = getattr(opt,'hidden_incog_'+ wtype +'_params').split(',')
  816. return ','.join(a[:-1]),int(a[-1]) # permit comma in filename
  817. def _check_valid_offset(self,fn,action):
  818. d = self.ssdata
  819. m = ('Input','Destination')[action=='write']
  820. if fn.size < d.hincog_offset + d.target_data_len:
  821. die(1,
  822. "%s file '%s' has length %s, too short to %s %s bytes of data at offset %s"
  823. % (m,fn.name,fn.size,action,d.target_data_len,d.hincog_offset))
  824. def _get_data(self):
  825. d = self.ssdata
  826. d.hincog_offset = self._get_hincog_params('input')[1]
  827. qmsg("Getting hidden incog data from file '%s'" % self.infile.name)
  828. # Already sanity-checked:
  829. d.target_data_len = self._get_incog_data_len(opt.seed_len)
  830. self._check_valid_offset(self.infile,'read')
  831. flgs = os.O_RDONLY|os.O_BINARY if g.platform == 'win' else os.O_RDONLY
  832. fh = os.open(self.infile.name,flgs)
  833. os.lseek(fh,int(d.hincog_offset),os.SEEK_SET)
  834. self.fmt_data = os.read(fh,d.target_data_len)
  835. os.close(fh)
  836. qmsg("Data read from file '%s' at offset %s" %
  837. (self.infile.name,d.hincog_offset), 'Data read from file')
  838. # overrides method in SeedSource
  839. def write_to_file(self):
  840. d = self.ssdata
  841. self._format()
  842. compare_or_die(d.target_data_len, 'target data length',
  843. len(self.fmt_data),'length of formatted ' + self.desc)
  844. k = ('output','input')[self.op=='pwchg_new']
  845. fn,d.hincog_offset = self._get_hincog_params(k)
  846. if opt.outdir and not os.path.dirname(fn):
  847. fn = os.path.join(opt.outdir,fn)
  848. check_offset = True
  849. try:
  850. os.stat(fn)
  851. except:
  852. if keypress_confirm("Requested file '%s' does not exist. Create?"
  853. % fn, default_yes=True):
  854. min_fsize = d.target_data_len + d.hincog_offset
  855. msg(self.msg['choose_file_size'].format(min_fsize))
  856. while True:
  857. fsize = parse_nbytes(my_raw_input('Enter file size: '))
  858. if fsize >= min_fsize: break
  859. msg('File size must be an integer no less than %s' %
  860. min_fsize)
  861. from mmgen.tool import rand2file
  862. rand2file(fn, str(fsize))
  863. check_offset = False
  864. else:
  865. die(1,'Exiting at user request')
  866. f = Filename(fn,ftype=type(self),write=True)
  867. dmsg('%s data len %s, offset %s' % (
  868. capfirst(self.desc),d.target_data_len,d.hincog_offset))
  869. if check_offset:
  870. self._check_valid_offset(f,'write')
  871. if not opt.quiet: confirm_or_exit('',"alter file '%s'" % f.name)
  872. flgs = os.O_RDWR|os.O_BINARY if g.platform == 'win' else os.O_RDWR
  873. fh = os.open(f.name,flgs)
  874. os.lseek(fh, int(d.hincog_offset), os.SEEK_SET)
  875. os.write(fh, self.fmt_data)
  876. os.close(fh)
  877. msg("%s written to file '%s' at offset %s" % (
  878. capfirst(self.desc),f.name,d.hincog_offset))