wallet.py 33 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176
  1. #!/usr/bin/env python3
  2. #
  3. # mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
  4. # Copyright (C)2013-2022 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. wallet.py: Wallet classes and methods for the MMGen suite
  20. """
  21. import os
  22. from .common import *
  23. from .obj import *
  24. from .baseconv import *
  25. from .seed import Seed
  26. import mmgen.crypto as crypto
  27. def check_usr_seed_len(seed_len):
  28. if opt.seed_len and opt.seed_len != seed_len:
  29. die(1,f'ERROR: requested seed length ({opt.seed_len}) doesn’t match seed length of source ({seed_len})')
  30. class WalletMeta(type):
  31. wallet_classes = set() # one-instance class, so store data in class attr
  32. def __init__(cls,name,bases,namespace):
  33. cls.wallet_classes.add(cls)
  34. cls.wallet_classes -= set(bases)
  35. class Wallet(MMGenObject,metaclass=WalletMeta):
  36. desc = g.proj_name + ' seed source'
  37. file_mode = 'text'
  38. filename_api = True
  39. stdin_ok = False
  40. ask_tty = True
  41. no_tty = False
  42. op = None
  43. _msg = {}
  44. class WalletData(MMGenObject): pass
  45. def __new__(cls,
  46. fn = None,
  47. ss = None,
  48. seed_bin = None,
  49. seed = None,
  50. passchg = False,
  51. in_data = None,
  52. ignore_in_fmt = False,
  53. in_fmt = None,
  54. passwd_file = None ):
  55. in_fmt = in_fmt or opt.in_fmt
  56. if opt.out_fmt:
  57. out_cls = cls.fmt_code_to_type(opt.out_fmt)
  58. if not out_cls:
  59. die(1,f'{opt.out_fmt!r}: unrecognized output format')
  60. else:
  61. out_cls = None
  62. def die_on_opt_mismatch(opt,sstype):
  63. compare_or_die(
  64. cls.fmt_code_to_type(opt).__name__, 'input format requested on command line',
  65. sstype.__name__, 'input file format' )
  66. if seed or seed_bin:
  67. me = super(cls,cls).__new__(out_cls or MMGenWallet) # default to MMGenWallet
  68. me.seed = seed or Seed(seed_bin=seed_bin)
  69. me.op = 'new'
  70. elif ss:
  71. me = super(cls,cls).__new__((ss.__class__ if passchg else out_cls) or MMGenWallet)
  72. me.seed = ss.seed
  73. me.ss_in = ss
  74. me.op = ('conv','pwchg_new')[bool(passchg)]
  75. elif fn or opt.hidden_incog_input_params:
  76. from .filename import Filename
  77. if fn:
  78. f = Filename(fn,base_class=cls)
  79. else:
  80. # permit comma in filename
  81. fn = ','.join(opt.hidden_incog_input_params.split(',')[:-1])
  82. f = Filename(fn,subclass=IncogWalletHidden)
  83. if in_fmt and not ignore_in_fmt:
  84. die_on_opt_mismatch(in_fmt,f.subclass)
  85. me = super(cls,cls).__new__(f.subclass)
  86. me.infile = f
  87. me.op = ('old','pwchg_old')[bool(passchg)]
  88. elif in_fmt:
  89. me = super(cls,cls).__new__(cls.fmt_code_to_type(in_fmt))
  90. me.op = ('old','pwchg_old')[bool(passchg)]
  91. else: # called with no arguments: initialize with random seed
  92. me = super(cls,cls).__new__(out_cls or MMGenWallet)
  93. me.seed = Seed(None)
  94. me.op = 'new'
  95. return me
  96. def __init__(self,
  97. fn = None,
  98. ss = None,
  99. seed_bin = None,
  100. seed = None,
  101. passchg = False,
  102. in_data = None,
  103. ignore_in_fmt = False,
  104. in_fmt = None,
  105. passwd_file = None ):
  106. self.passwd_file = passwd_file or opt.passwd_file
  107. self.ssdata = self.WalletData()
  108. self.msg = {}
  109. self.in_data = in_data
  110. for c in reversed(self.__class__.__mro__):
  111. if hasattr(c,'_msg'):
  112. self.msg.update(c._msg)
  113. if hasattr(self,'seed'):
  114. self._encrypt()
  115. return
  116. elif hasattr(self,'infile') or self.in_data or not g.stdin_tty:
  117. self._deformat_once()
  118. self._decrypt_retry()
  119. else:
  120. if not self.stdin_ok:
  121. die(1,f'Reading from standard input not supported for {self.desc} format')
  122. self._deformat_retry()
  123. self._decrypt_retry()
  124. qmsg('Valid {} for Seed ID {}{}'.format(
  125. self.desc,
  126. self.seed.sid.hl(),
  127. (f', seed length {self.seed.bitlen}' if self.seed.bitlen != 256 else '')
  128. ))
  129. def _get_data(self):
  130. if hasattr(self,'infile'):
  131. from .fileutil import get_data_from_file
  132. self.fmt_data = get_data_from_file(self.infile.name,self.desc,binary=self.file_mode=='binary')
  133. elif self.in_data:
  134. self.fmt_data = self.in_data
  135. else:
  136. self.fmt_data = self._get_data_from_user(self.desc)
  137. def _get_data_from_user(self,desc):
  138. return get_data_from_user(desc)
  139. def _deformat_once(self):
  140. self._get_data()
  141. if not self._deformat():
  142. die(2,'Invalid format for input data')
  143. def _deformat_retry(self):
  144. while True:
  145. self._get_data()
  146. if self._deformat():
  147. break
  148. msg('Trying again...')
  149. def _decrypt_retry(self):
  150. while True:
  151. if self._decrypt():
  152. break
  153. if self.passwd_file:
  154. die(2,'Passphrase from password file, so exiting')
  155. msg('Trying again...')
  156. @classmethod
  157. def get_extensions(cls):
  158. return [c.ext for c in cls.wallet_classes if hasattr(c,'ext')]
  159. @classmethod
  160. def fmt_code_to_type(cls,fmt_code):
  161. if fmt_code:
  162. for c in cls.wallet_classes:
  163. if fmt_code in getattr(c,'fmt_codes',[]):
  164. return c
  165. return None
  166. @classmethod
  167. def ext_to_type(cls,ext,proto=None):
  168. for c in cls.wallet_classes:
  169. if ext == getattr(c,'ext',None):
  170. return c
  171. @classmethod
  172. def format_fmt_codes(cls):
  173. d = [(c.__name__,('.'+c.ext if c.ext else str(c.ext)),','.join(c.fmt_codes))
  174. for c in cls.wallet_classes
  175. if hasattr(c,'fmt_codes')]
  176. w = max(len(i[0]) for i in d)
  177. ret = [f'{a:<{w}} {b:<9} {c}' for a,b,c in [
  178. ('Format','FileExt','Valid codes'),
  179. ('------','-------','-----------')
  180. ] + sorted(d)]
  181. return '\n'.join(ret) + ('','-α')[g.debug_utf8] + '\n'
  182. def get_fmt_data(self):
  183. self._format()
  184. return self.fmt_data
  185. def write_to_file(self,outdir='',desc=''):
  186. self._format()
  187. kwargs = {
  188. 'desc': desc or self.desc,
  189. 'ask_tty': self.ask_tty,
  190. 'no_tty': self.no_tty,
  191. 'binary': self.file_mode == 'binary'
  192. }
  193. # write_data_to_file(): outfile with absolute path overrides opt.outdir
  194. if outdir:
  195. of = os.path.abspath(os.path.join(outdir,self._filename()))
  196. from .fileutil import write_data_to_file
  197. write_data_to_file(of if outdir else self._filename(),self.fmt_data,**kwargs)
  198. class WalletUnenc(Wallet):
  199. def _decrypt_retry(self): pass
  200. def _encrypt(self): pass
  201. def _filename(self):
  202. s = self.seed
  203. return '{}[{}]{x}.{}'.format(
  204. s.fn_stem,
  205. s.bitlen,
  206. self.ext,
  207. x='-α' if g.debug_utf8 else '')
  208. def _choose_seedlen(self,desc,ok_lens,subtype):
  209. from .term import get_char
  210. def choose_len():
  211. prompt = self.choose_seedlen_prompt
  212. while True:
  213. r = get_char('\r'+prompt)
  214. if is_int(r) and 1 <= int(r) <= len(ok_lens):
  215. break
  216. msg_r(('\r','\n')[g.test_suite] + ' '*len(prompt) + '\r')
  217. return ok_lens[int(r)-1]
  218. msg('{} {}'.format(
  219. blue(f'{capfirst(desc)} type:'),
  220. yellow(subtype)
  221. ))
  222. while True:
  223. usr_len = choose_len()
  224. prompt = self.choose_seedlen_confirm.format(usr_len)
  225. if keypress_confirm(prompt,default_yes=True,no_nl=not g.test_suite):
  226. return usr_len
  227. class WalletEnc(Wallet):
  228. _msg = {
  229. 'choose_passphrase': """
  230. You must choose a passphrase to encrypt your new {} with.
  231. A key will be generated from your passphrase using a hash preset of '{}'.
  232. Please note that no strength checking of passphrases is performed.
  233. For an empty passphrase, just hit ENTER twice.
  234. """
  235. }
  236. def _get_hash_preset_from_user(self,hp,add_desc=''):
  237. prompt = 'Enter {}hash preset for {}{}{},\nor hit ENTER to {} value ({!r}): '.format(
  238. ('old ' if self.op=='pwchg_old' else 'new ' if self.op=='pwchg_new' else ''),
  239. ('','new ')[self.op=='new'],
  240. self.desc,
  241. ('',' '+add_desc)[bool(add_desc)],
  242. ('accept the default','reuse the old')[self.op=='pwchg_new'],
  243. hp )
  244. return crypto.get_hash_preset_from_user( hash_preset=hp, prompt=prompt )
  245. def _get_hash_preset(self,add_desc=''):
  246. if hasattr(self,'ss_in') and hasattr(self.ss_in.ssdata,'hash_preset'):
  247. old_hp = self.ss_in.ssdata.hash_preset
  248. if opt.keep_hash_preset:
  249. hp = old_hp
  250. qmsg(f'Reusing hash preset {hp!r} at user request')
  251. elif opt.hash_preset:
  252. hp = opt.hash_preset
  253. qmsg(f'Using hash preset {hp!r} requested on command line')
  254. else: # Prompt, using old value as default
  255. hp = self._get_hash_preset_from_user(old_hp,add_desc)
  256. if (not opt.keep_hash_preset) and self.op == 'pwchg_new':
  257. qmsg('Hash preset {}'.format( 'unchanged' if hp == old_hp else f'changed to {hp!r}' ))
  258. elif opt.hash_preset:
  259. hp = opt.hash_preset
  260. qmsg(f'Using hash preset {hp!r} requested on command line')
  261. else:
  262. hp = self._get_hash_preset_from_user(g.dfl_hash_preset,add_desc)
  263. self.ssdata.hash_preset = hp
  264. def _get_new_passphrase(self):
  265. self.ssdata.passwd = crypto.get_new_passphrase(
  266. data_desc = ('new ' if self.op in ('new','conv') else '') + self.desc,
  267. hash_preset = self.ssdata.hash_preset,
  268. passwd_file = self.passwd_file,
  269. pw_desc = ('new ' if self.op=='pwchg_new' else '') + 'passphrase' )
  270. return self.ssdata.passwd
  271. def _get_passphrase(self,add_desc=''):
  272. self.ssdata.passwd = crypto.get_passphrase(
  273. data_desc = self.desc + (f' {add_desc}' if add_desc else ''),
  274. passwd_file = self.passwd_file,
  275. pw_desc = ('old ' if self.op == 'pwchg_old' else '') + 'passphrase' )
  276. def _get_first_pw_and_hp_and_encrypt_seed(self):
  277. d = self.ssdata
  278. self._get_hash_preset()
  279. if hasattr(self,'ss_in') and hasattr(self.ss_in.ssdata,'passwd'):
  280. old_pw = self.ss_in.ssdata.passwd
  281. if opt.keep_passphrase:
  282. d.passwd = old_pw
  283. qmsg('Reusing passphrase at user request')
  284. else:
  285. pw = self._get_new_passphrase()
  286. if self.op == 'pwchg_new':
  287. qmsg('Passphrase {}'.format( 'unchanged' if pw == old_pw else 'changed' ))
  288. else:
  289. self._get_new_passphrase()
  290. from hashlib import sha256
  291. d.salt = sha256( crypto.get_random(128) ).digest()[:crypto.salt_len]
  292. key = crypto.make_key( d.passwd, d.salt, d.hash_preset )
  293. d.key_id = make_chksum_8(key)
  294. d.enc_seed = crypto.encrypt_seed( self.seed.data, key )
  295. class Mnemonic(WalletUnenc):
  296. stdin_ok = True
  297. wclass = 'mnemonic'
  298. conv_cls = baseconv
  299. choose_seedlen_prompt = 'Choose a mnemonic length: 1) 12 words, 2) 18 words, 3) 24 words: '
  300. choose_seedlen_confirm = 'Mnemonic length of {} words chosen. OK?'
  301. @property
  302. def mn_lens(self):
  303. return sorted(self.conv_cls(self.wl_id).seedlen_map_rev)
  304. def _get_data_from_user(self,desc):
  305. if not g.stdin_tty:
  306. return get_data_from_user(desc)
  307. from .mn_entry import mn_entry # import here to catch cfg var errors
  308. mn_len = self._choose_seedlen(self.wclass,self.mn_lens,self.mn_type)
  309. return mn_entry(self.wl_id).get_mnemonic_from_user(mn_len)
  310. def _format(self):
  311. hexseed = self.seed.hexdata
  312. bc = self.conv_cls(self.wl_id)
  313. mn = bc.fromhex( hexseed, 'seed' )
  314. rev = bc.tohex( mn, 'seed' )
  315. # Internal error, so just die on fail
  316. compare_or_die( rev, 'recomputed seed', hexseed, 'original', e='Internal error' )
  317. self.ssdata.mnemonic = mn
  318. self.fmt_data = ' '.join(mn) + '\n'
  319. def _deformat(self):
  320. bc = self.conv_cls(self.wl_id)
  321. mn = self.fmt_data.split()
  322. if len(mn) not in self.mn_lens:
  323. msg('Invalid mnemonic ({} words). Valid numbers of words: {}'.format(
  324. len(mn),
  325. ', '.join(map(str,self.mn_lens)) ))
  326. return False
  327. for n,w in enumerate(mn,1):
  328. if w not in bc.digits:
  329. msg(f'Invalid mnemonic: word #{n} is not in the {self.wl_id.upper()} wordlist')
  330. return False
  331. hexseed = bc.tohex( mn, 'seed' )
  332. rev = bc.fromhex( hexseed, 'seed' )
  333. if len(hexseed) * 4 not in Seed.lens:
  334. msg('Invalid mnemonic (produces too large a number)')
  335. return False
  336. # Internal error, so just die
  337. compare_or_die( ' '.join(rev), 'recomputed mnemonic', ' '.join(mn), 'original', e='Internal error' )
  338. self.seed = Seed(bytes.fromhex(hexseed))
  339. self.ssdata.mnemonic = mn
  340. check_usr_seed_len(self.seed.bitlen)
  341. return True
  342. class MMGenMnemonic(Mnemonic):
  343. fmt_codes = ('mmwords','words','mnemonic','mnem','mn','m')
  344. desc = 'MMGen native mnemonic data'
  345. mn_type = 'MMGen native'
  346. ext = 'mmwords'
  347. wl_id = 'mmgen'
  348. class BIP39Mnemonic(Mnemonic):
  349. fmt_codes = ('bip39',)
  350. desc = 'BIP39 mnemonic data'
  351. mn_type = 'BIP39'
  352. ext = 'bip39'
  353. wl_id = 'bip39'
  354. def __init__(self,*args,**kwargs):
  355. from .bip39 import bip39
  356. self.conv_cls = bip39
  357. super().__init__(*args,**kwargs)
  358. class MMGenSeedFile(WalletUnenc):
  359. stdin_ok = True
  360. fmt_codes = ('mmseed','seed','s')
  361. desc = 'seed data'
  362. ext = 'mmseed'
  363. def _format(self):
  364. b58seed = baseconv('b58').frombytes(self.seed.data,pad='seed',tostr=True)
  365. self.ssdata.chksum = make_chksum_6(b58seed)
  366. self.ssdata.b58seed = b58seed
  367. self.fmt_data = '{} {}\n'.format(
  368. self.ssdata.chksum,
  369. split_into_cols(4,b58seed) )
  370. def _deformat(self):
  371. desc = self.desc
  372. ld = self.fmt_data.split()
  373. if not (7 <= len(ld) <= 12): # 6 <= padded b58 data (ld[1:]) <= 11
  374. msg(f'Invalid data length ({len(ld)}) in {desc}')
  375. return False
  376. a,b = ld[0],''.join(ld[1:])
  377. if not is_chksum_6(a):
  378. msg(f'{a!r}: invalid checksum format in {desc}')
  379. return False
  380. if not is_b58_str(b):
  381. msg(f'{b!r}: not a base 58 string, in {desc}')
  382. return False
  383. vmsg_r(f'Validating {desc} checksum...')
  384. if not compare_chksums(a,'file',make_chksum_6(b),'computed',verbose=True):
  385. return False
  386. ret = baseconv('b58').tobytes(b,pad='seed')
  387. if ret == False:
  388. msg(f'Invalid base-58 encoded seed: {val}')
  389. return False
  390. self.seed = Seed(ret)
  391. self.ssdata.chksum = a
  392. self.ssdata.b58seed = b
  393. check_usr_seed_len(self.seed.bitlen)
  394. return True
  395. class DieRollSeedFile(WalletUnenc):
  396. stdin_ok = True
  397. fmt_codes = ('b6d','die','dieroll')
  398. desc = 'base6d die roll seed data'
  399. ext = 'b6d'
  400. conv_cls = baseconv
  401. wclass = 'dieroll'
  402. wl_id = 'b6d'
  403. mn_type = 'base6d'
  404. choose_seedlen_prompt = 'Choose a seed length: 1) 128 bits, 2) 192 bits, 3) 256 bits: '
  405. choose_seedlen_confirm = 'Seed length of {} bits chosen. OK?'
  406. user_entropy_prompt = 'Would you like to provide some additional entropy from the keyboard?'
  407. interactive_input = False
  408. def _format(self):
  409. d = baseconv('b6d').frombytes(self.seed.data,pad='seed',tostr=True) + '\n'
  410. self.fmt_data = block_format(d,gw=5,cols=5)
  411. def _deformat(self):
  412. d = remove_whitespace(self.fmt_data)
  413. bc = baseconv('b6d')
  414. rmap = bc.seedlen_map_rev
  415. if not len(d) in rmap:
  416. die( 'SeedLengthError', '{!r}: invalid length for {} (must be one of {})'.format(
  417. len(d),
  418. self.desc,
  419. list(rmap) ))
  420. # truncate seed to correct length, discarding high bits
  421. seed_len = rmap[len(d)]
  422. seed_bytes = bc.tobytes( d, pad='seed' )[-seed_len:]
  423. if self.interactive_input and opt.usr_randchars:
  424. if keypress_confirm(self.user_entropy_prompt):
  425. seed_bytes = crypto.add_user_random(
  426. rand_bytes = seed_bytes,
  427. desc = 'gathered from your die rolls' )
  428. self.desc += ' plus user-supplied entropy'
  429. self.seed = Seed(seed_bytes)
  430. self.ssdata.hexseed = seed_bytes.hex()
  431. check_usr_seed_len(self.seed.bitlen)
  432. return True
  433. def _get_data_from_user(self,desc):
  434. if not g.stdin_tty:
  435. return get_data_from_user(desc)
  436. bc = baseconv('b6d')
  437. seed_bitlens = [ n*8 for n in sorted(bc.seedlen_map) ]
  438. seed_bitlen = self._choose_seedlen( self.wclass, seed_bitlens, self.mn_type )
  439. nDierolls = bc.seedlen_map[seed_bitlen // 8]
  440. m = """
  441. For a {sb}-bit seed you must roll the die {nd} times. After each die roll,
  442. enter the result on the keyboard as a digit. If you make an invalid entry,
  443. you'll be prompted to re-enter it.
  444. """
  445. msg('\n'+fmt(m.strip()).format(sb=seed_bitlen,nd=nDierolls)+'\n')
  446. CUR_HIDE = '\033[?25l'
  447. CUR_SHOW = '\033[?25h'
  448. cr = '\n' if g.test_suite else '\r'
  449. prompt_fs = f'\b\b\b {cr}Enter die roll #{{}}: {CUR_SHOW}'
  450. clear_line = '' if g.test_suite else '\r' + ' ' * 25
  451. invalid_msg = CUR_HIDE + cr + 'Invalid entry' + ' ' * 11
  452. from .term import get_char
  453. def get_digit(n):
  454. p = prompt_fs
  455. sleep = g.short_disp_timeout
  456. while True:
  457. ch = get_char(p.format(n),num_chars=1,sleep=sleep)
  458. if ch in bc.digits:
  459. msg_r(CUR_HIDE + ' OK')
  460. return ch
  461. else:
  462. msg_r(invalid_msg)
  463. sleep = g.err_disp_timeout
  464. p = clear_line + prompt_fs
  465. dierolls,n = [],1
  466. while len(dierolls) < nDierolls:
  467. dierolls.append(get_digit(n))
  468. n += 1
  469. msg('Die rolls successfully entered' + CUR_SHOW)
  470. self.interactive_input = True
  471. return ''.join(dierolls)
  472. class PlainHexSeedFile(WalletUnenc):
  473. stdin_ok = True
  474. fmt_codes = ('hex','rawhex','plainhex')
  475. desc = 'plain hexadecimal seed data'
  476. ext = 'hex'
  477. def _format(self):
  478. self.fmt_data = self.seed.hexdata + '\n'
  479. def _deformat(self):
  480. desc = self.desc
  481. d = self.fmt_data.strip()
  482. if not is_hex_str_lc(d):
  483. msg(f'{d!r}: not a lowercase hexadecimal string, in {desc}')
  484. return False
  485. if not len(d)*4 in Seed.lens:
  486. msg(f'Invalid data length ({len(d)}) in {desc}')
  487. return False
  488. self.seed = Seed(bytes.fromhex(d))
  489. self.ssdata.hexseed = d
  490. check_usr_seed_len(self.seed.bitlen)
  491. return True
  492. class MMGenHexSeedFile(WalletUnenc):
  493. stdin_ok = True
  494. fmt_codes = ('seedhex','hexseed','mmhex')
  495. desc = 'hexadecimal seed data with checksum'
  496. ext = 'mmhex'
  497. def _format(self):
  498. h = self.seed.hexdata
  499. self.ssdata.chksum = make_chksum_6(h)
  500. self.ssdata.hexseed = h
  501. self.fmt_data = '{} {}\n'.format(
  502. self.ssdata.chksum,
  503. split_into_cols(4,h) )
  504. def _deformat(self):
  505. desc = self.desc
  506. d = self.fmt_data.split()
  507. try:
  508. d[1]
  509. chk,hstr = d[0],''.join(d[1:])
  510. except:
  511. msg(f'{self.fmt_data.strip()!r}: invalid {desc}')
  512. return False
  513. if not len(hstr)*4 in Seed.lens:
  514. msg(f'Invalid data length ({len(hstr)}) in {desc}')
  515. return False
  516. if not is_chksum_6(chk):
  517. msg(f'{chk!r}: invalid checksum format in {desc}')
  518. return False
  519. if not is_hex_str(hstr):
  520. msg(f'{hstr!r}: not a hexadecimal string, in {desc}')
  521. return False
  522. vmsg_r(f'Validating {desc} checksum...')
  523. if not compare_chksums(chk,'file',make_chksum_6(hstr),'computed',verbose=True):
  524. return False
  525. self.seed = Seed(bytes.fromhex(hstr))
  526. self.ssdata.chksum = chk
  527. self.ssdata.hexseed = hstr
  528. check_usr_seed_len(self.seed.bitlen)
  529. return True
  530. class MMGenWallet(WalletEnc):
  531. fmt_codes = ('wallet','w')
  532. desc = g.proj_name + ' wallet'
  533. ext = 'mmdat'
  534. def __init__(self,*args,**kwargs):
  535. if opt.label:
  536. self.label = MMGenWalletLabel(
  537. opt.label,
  538. msg = "Error in option '--label'" )
  539. else:
  540. self.label = None
  541. super().__init__(*args,**kwargs)
  542. # logic identical to _get_hash_preset_from_user()
  543. def _get_label_from_user(self,old_lbl=''):
  544. prompt = 'Enter a wallet label, or hit ENTER {}: '.format(
  545. 'to reuse the label {}'.format(old_lbl.hl(encl="''")) if old_lbl else
  546. 'for no label' )
  547. while True:
  548. ret = line_input(prompt)
  549. if ret:
  550. lbl = get_obj(MMGenWalletLabel,s=ret)
  551. if lbl:
  552. return lbl
  553. else:
  554. msg('Invalid label. Trying again...')
  555. else:
  556. return old_lbl or MMGenWalletLabel('No Label')
  557. # logic identical to _get_hash_preset()
  558. def _get_label(self):
  559. if hasattr(self,'ss_in') and hasattr(self.ss_in.ssdata,'label'):
  560. old_lbl = self.ss_in.ssdata.label
  561. if opt.keep_label:
  562. lbl = old_lbl
  563. qmsg('Reusing label {} at user request'.format( lbl.hl(encl="''") ))
  564. elif self.label:
  565. lbl = self.label
  566. qmsg('Using label {} requested on command line'.format( lbl.hl(encl="''") ))
  567. else: # Prompt, using old value as default
  568. lbl = self._get_label_from_user(old_lbl)
  569. if (not opt.keep_label) and self.op == 'pwchg_new':
  570. qmsg('Label {}'.format( 'unchanged' if lbl == old_lbl else f'changed to {lbl!r}' ))
  571. elif self.label:
  572. lbl = self.label
  573. qmsg('Using label {} requested on command line'.format( lbl.hl(encl="''") ))
  574. else:
  575. lbl = self._get_label_from_user()
  576. self.ssdata.label = lbl
  577. def _encrypt(self):
  578. self._get_first_pw_and_hp_and_encrypt_seed()
  579. self._get_label()
  580. d = self.ssdata
  581. d.pw_status = ('NE','E')[len(d.passwd)==0]
  582. d.timestamp = make_timestamp()
  583. def _format(self):
  584. d = self.ssdata
  585. s = self.seed
  586. bc = baseconv('b58')
  587. slt_fmt = bc.frombytes(d.salt,pad='seed',tostr=True)
  588. es_fmt = bc.frombytes(d.enc_seed,pad='seed',tostr=True)
  589. lines = (
  590. d.label,
  591. '{} {} {} {} {}'.format( s.sid.lower(), d.key_id.lower(), s.bitlen, d.pw_status, d.timestamp ),
  592. '{}: {} {} {}'.format( d.hash_preset, *crypto.get_hash_params(d.hash_preset) ),
  593. '{} {}'.format( make_chksum_6(slt_fmt), split_into_cols(4,slt_fmt) ),
  594. '{} {}'.format( make_chksum_6(es_fmt), split_into_cols(4,es_fmt) )
  595. )
  596. chksum = make_chksum_6(' '.join(lines).encode())
  597. self.fmt_data = '\n'.join((chksum,)+lines) + '\n'
  598. def _deformat(self):
  599. def check_master_chksum(lines,desc):
  600. if len(lines) != 6:
  601. msg(f'Invalid number of lines ({len(lines)}) in {desc} data')
  602. return False
  603. if not is_chksum_6(lines[0]):
  604. msg(f'Incorrect master checksum ({lines[0]}) in {desc} data')
  605. return False
  606. chk = make_chksum_6(' '.join(lines[1:]))
  607. if not compare_chksums(lines[0],'master',chk,'computed',
  608. hdr='For wallet master checksum',verbose=True):
  609. return False
  610. return True
  611. lines = self.fmt_data.splitlines()
  612. if not check_master_chksum(lines,self.desc):
  613. return False
  614. d = self.ssdata
  615. d.label = MMGenWalletLabel(lines[1])
  616. d1,d2,d3,d4,d5 = lines[2].split()
  617. d.seed_id = d1.upper()
  618. d.key_id = d2.upper()
  619. check_usr_seed_len(int(d3))
  620. d.pw_status,d.timestamp = d4,d5
  621. hpdata = lines[3].split()
  622. d.hash_preset = hp = hpdata[0][:-1] # a string!
  623. qmsg(f'Hash preset of wallet: {hp!r}')
  624. if opt.hash_preset and opt.hash_preset != hp:
  625. qmsg(f'Warning: ignoring user-requested hash preset {opt.hash_preset!r}')
  626. hash_params = tuple(map(int,hpdata[1:]))
  627. if hash_params != crypto.get_hash_params(d.hash_preset):
  628. msg(f'Hash parameters {" ".join(hash_params)!r} don’t match hash preset {d.hash_preset!r}')
  629. return False
  630. lmin,foo,lmax = sorted(baseconv('b58').seedlen_map_rev) # 22,33,44
  631. for i,key in (4,'salt'),(5,'enc_seed'):
  632. l = lines[i].split(' ')
  633. chk = l.pop(0)
  634. b58_val = ''.join(l)
  635. if len(b58_val) < lmin or len(b58_val) > lmax:
  636. msg(f'Invalid format for {key} in {self.desc}: {l}')
  637. return False
  638. if not compare_chksums(chk,key,
  639. make_chksum_6(b58_val),'computed checksum',verbose=True):
  640. return False
  641. val = baseconv('b58').tobytes(b58_val,pad='seed')
  642. if val == False:
  643. msg(f'Invalid base 58 number: {b58_val}')
  644. return False
  645. setattr(d,key,val)
  646. return True
  647. def _decrypt(self):
  648. d = self.ssdata
  649. # Needed for multiple transactions with {}-txsign
  650. self._get_passphrase(
  651. add_desc = os.path.basename(self.infile.name) if opt.quiet else '' )
  652. key = crypto.make_key( d.passwd, d.salt, d.hash_preset )
  653. ret = crypto.decrypt_seed( d.enc_seed, key, d.seed_id, d.key_id )
  654. if ret:
  655. self.seed = Seed(ret)
  656. return True
  657. else:
  658. return False
  659. def _filename(self):
  660. s = self.seed
  661. d = self.ssdata
  662. return '{}-{}[{},{}]{x}.{}'.format(
  663. s.fn_stem,
  664. d.key_id,
  665. s.bitlen,
  666. d.hash_preset,
  667. self.ext,
  668. x='-α' if g.debug_utf8 else '')
  669. class Brainwallet(WalletEnc):
  670. stdin_ok = True
  671. fmt_codes = ('mmbrain','brainwallet','brain','bw')
  672. desc = 'brainwallet'
  673. ext = 'mmbrain'
  674. # brainwallet warning message? TODO
  675. def get_bw_params(self):
  676. # already checked
  677. a = opt.brain_params.split(',')
  678. return int(a[0]),a[1]
  679. def _deformat(self):
  680. self.brainpasswd = ' '.join(self.fmt_data.split())
  681. return True
  682. def _decrypt(self):
  683. d = self.ssdata
  684. if opt.brain_params:
  685. """
  686. Don't set opt.seed_len! When using multiple wallets, BW seed len might differ from others
  687. """
  688. bw_seed_len,d.hash_preset = self.get_bw_params()
  689. else:
  690. if not opt.seed_len:
  691. qmsg(f'Using default seed length of {yellow(str(Seed.dfl_len))} bits\n'
  692. + 'If this is not what you want, use the --seed-len option' )
  693. self._get_hash_preset()
  694. bw_seed_len = opt.seed_len or Seed.dfl_len
  695. qmsg_r('Hashing brainwallet data. Please wait...')
  696. # Use buflen arg of scrypt.hash() to get seed of desired length
  697. seed = crypto.scrypt_hash_passphrase(
  698. self.brainpasswd.encode(),
  699. b'',
  700. d.hash_preset,
  701. buflen = bw_seed_len // 8 )
  702. qmsg('Done')
  703. self.seed = Seed(seed)
  704. msg(f'Seed ID: {self.seed.sid}')
  705. qmsg('Check this value against your records')
  706. return True
  707. def _format(self):
  708. raise NotImplementedError('Brainwallet not supported as an output format')
  709. def _encrypt(self):
  710. raise NotImplementedError('Brainwallet not supported as an output format')
  711. class IncogWalletBase(WalletEnc):
  712. _msg = {
  713. 'check_incog_id': """
  714. Check the generated Incog ID above against your records. If it doesn't
  715. match, then your incognito data is incorrect or corrupted.
  716. """,
  717. 'record_incog_id': """
  718. Make a record of the Incog ID but keep it secret. You will use it to
  719. identify your incog wallet data in the future.
  720. """,
  721. 'incorrect_incog_passphrase_try_again': """
  722. Incorrect passphrase, hash preset, or maybe old-format incog wallet.
  723. Try again? (Y)es, (n)o, (m)ore information:
  724. """.strip(),
  725. 'confirm_seed_id': """
  726. If the Seed ID above is correct but you're seeing this message, then you need
  727. to exit and re-run the program with the '--old-incog-fmt' option.
  728. """.strip(),
  729. 'dec_chk': " {} hash preset"
  730. }
  731. def _make_iv_chksum(self,s):
  732. from hashlib import sha256
  733. return sha256(s).hexdigest()[:8].upper()
  734. def _get_incog_data_len(self,seed_len):
  735. return (
  736. crypto.aesctr_iv_len
  737. + crypto.salt_len
  738. + (0 if opt.old_incog_fmt else crypto.hincog_chk_len)
  739. + seed_len//8 )
  740. def _incog_data_size_chk(self):
  741. # valid sizes: 56, 64, 72
  742. dlen = len(self.fmt_data)
  743. seed_len = opt.seed_len or Seed.dfl_len
  744. valid_dlen = self._get_incog_data_len(seed_len)
  745. if dlen == valid_dlen:
  746. return True
  747. else:
  748. if opt.old_incog_fmt:
  749. msg('WARNING: old-style incognito format requested. Are you sure this is correct?')
  750. msg(f'Invalid incognito data size ({dlen} bytes) for this seed length ({seed_len} bits)')
  751. msg(f'Valid data size for this seed length: {valid_dlen} bytes')
  752. for sl in Seed.lens:
  753. if dlen == self._get_incog_data_len(sl):
  754. die(1,f'Valid seed length for this data size: {sl} bits')
  755. msg(f'This data size ({dlen} bytes) is invalid for all available seed lengths')
  756. return False
  757. def _encrypt (self):
  758. self._get_first_pw_and_hp_and_encrypt_seed()
  759. if opt.old_incog_fmt:
  760. die(1,'Writing old-format incog wallets is unsupported')
  761. d = self.ssdata
  762. # IV is used BOTH to initialize counter and to salt password!
  763. d.iv = crypto.get_random( crypto.aesctr_iv_len )
  764. d.iv_id = self._make_iv_chksum(d.iv)
  765. msg(f'New Incog Wallet ID: {d.iv_id}')
  766. qmsg('Make a record of this value')
  767. vmsg(self.msg['record_incog_id'])
  768. d.salt = crypto.get_random( crypto.salt_len )
  769. key = crypto.make_key( d.passwd, d.salt, d.hash_preset, 'incog wallet key' )
  770. from hashlib import sha256
  771. chk = sha256(self.seed.data).digest()[:8]
  772. d.enc_seed = crypto.encrypt_data(
  773. chk + self.seed.data,
  774. key,
  775. crypto.aesctr_dfl_iv,
  776. 'seed' )
  777. d.wrapper_key = crypto.make_key( d.passwd, d.iv, d.hash_preset, 'incog wrapper key' )
  778. d.key_id = make_chksum_8(d.wrapper_key)
  779. vmsg(f'Key ID: {d.key_id}')
  780. d.target_data_len = self._get_incog_data_len(self.seed.bitlen)
  781. def _format(self):
  782. d = self.ssdata
  783. self.fmt_data = d.iv + crypto.encrypt_data(
  784. d.salt + d.enc_seed,
  785. d.wrapper_key,
  786. d.iv,
  787. self.desc )
  788. def _filename(self):
  789. s = self.seed
  790. d = self.ssdata
  791. return '{}-{}-{}[{},{}]{x}.{}'.format(
  792. s.fn_stem,
  793. d.key_id,
  794. d.iv_id,
  795. s.bitlen,
  796. d.hash_preset,
  797. self.ext,
  798. x='-α' if g.debug_utf8 else '')
  799. def _deformat(self):
  800. if not self._incog_data_size_chk():
  801. return False
  802. d = self.ssdata
  803. d.iv = self.fmt_data[0:crypto.aesctr_iv_len]
  804. d.incog_id = self._make_iv_chksum(d.iv)
  805. d.enc_incog_data = self.fmt_data[crypto.aesctr_iv_len:]
  806. msg(f'Incog Wallet ID: {d.incog_id}')
  807. qmsg('Check this value against your records')
  808. vmsg(self.msg['check_incog_id'])
  809. return True
  810. def _verify_seed_newfmt(self,data):
  811. chk,seed = data[:8],data[8:]
  812. from hashlib import sha256
  813. if sha256(seed).digest()[:8] == chk:
  814. qmsg('Passphrase{} are correct'.format( self.msg['dec_chk'].format('and') ))
  815. return seed
  816. else:
  817. msg('Incorrect passphrase{}'.format( self.msg['dec_chk'].format('or') ))
  818. return False
  819. def _verify_seed_oldfmt(self,seed):
  820. m = f'Seed ID: {make_chksum_8(seed)}. Is the Seed ID correct?'
  821. if keypress_confirm(m, True):
  822. return seed
  823. else:
  824. return False
  825. def _decrypt(self):
  826. d = self.ssdata
  827. self._get_hash_preset(add_desc=d.incog_id)
  828. self._get_passphrase(add_desc=d.incog_id)
  829. # IV is used BOTH to initialize counter and to salt password!
  830. key = crypto.make_key( d.passwd, d.iv, d.hash_preset, 'wrapper key' )
  831. dd = crypto.decrypt_data( d.enc_incog_data, key, d.iv, 'incog data' )
  832. d.salt = dd[0:crypto.salt_len]
  833. d.enc_seed = dd[crypto.salt_len:]
  834. key = crypto.make_key( d.passwd, d.salt, d.hash_preset, 'main key' )
  835. qmsg(f'Key ID: {make_chksum_8(key)}')
  836. verify_seed = getattr(self,'_verify_seed_'+
  837. ('newfmt','oldfmt')[bool(opt.old_incog_fmt)])
  838. seed = verify_seed( crypto.decrypt_seed(d.enc_seed, key, '', '') )
  839. if seed:
  840. self.seed = Seed(seed)
  841. msg(f'Seed ID: {self.seed.sid}')
  842. return True
  843. else:
  844. return False
  845. class IncogWallet(IncogWalletBase):
  846. desc = 'incognito data'
  847. fmt_codes = ('mmincog','incog','icg','i')
  848. ext = 'mmincog'
  849. file_mode = 'binary'
  850. no_tty = True
  851. class IncogWalletHex(IncogWalletBase):
  852. desc = 'hex incognito data'
  853. fmt_codes = ('mmincox','incox','incog_hex','xincog','ix','xi')
  854. ext = 'mmincox'
  855. file_mode = 'text'
  856. no_tty = False
  857. def _deformat(self):
  858. ret = decode_pretty_hexdump(self.fmt_data)
  859. if ret:
  860. self.fmt_data = ret
  861. return super()._deformat()
  862. else:
  863. return False
  864. def _format(self):
  865. super()._format()
  866. self.fmt_data = pretty_hexdump(self.fmt_data)
  867. class IncogWalletHidden(IncogWalletBase):
  868. desc = 'hidden incognito data'
  869. fmt_codes = ('incog_hidden','hincog','ih','hi')
  870. ext = None
  871. file_mode = 'binary'
  872. no_tty = True
  873. _msg = {
  874. 'choose_file_size': """
  875. You must choose a size for your new hidden incog data. The minimum size is
  876. {} bytes, which puts the incog data right at the end of the file. Since you
  877. probably want to hide your data somewhere in the middle of the file where it's
  878. harder to find, you're advised to choose a much larger file size than this.
  879. """.strip(),
  880. 'check_incog_id': """
  881. Check generated Incog ID above against your records. If it doesn't
  882. match, then your incognito data is incorrect or corrupted, or you
  883. may have specified an incorrect offset.
  884. """,
  885. 'record_incog_id': """
  886. Make a record of the Incog ID but keep it secret. You will used it to
  887. identify the incog wallet data in the future and to locate the offset
  888. where the data is hidden in the event you forget it.
  889. """,
  890. 'dec_chk': ', hash preset, offset {} seed length'
  891. }
  892. def _get_hincog_params(self,wtype):
  893. a = getattr(opt,'hidden_incog_'+ wtype +'_params').split(',')
  894. return ','.join(a[:-1]),int(a[-1]) # permit comma in filename
  895. def _check_valid_offset(self,fn,action):
  896. d = self.ssdata
  897. m = ('Input','Destination')[action=='write']
  898. if fn.size < d.hincog_offset + d.target_data_len:
  899. die(1,'{} file {!r} has length {}, too short to {} {} bytes of data at offset {}'.format(
  900. m,
  901. fn.name,
  902. fn.size,
  903. action,
  904. d.target_data_len,
  905. d.hincog_offset ))
  906. def _get_data(self):
  907. d = self.ssdata
  908. d.hincog_offset = self._get_hincog_params('input')[1]
  909. qmsg(f'Getting hidden incog data from file {self.infile.name!r}')
  910. # Already sanity-checked:
  911. d.target_data_len = self._get_incog_data_len(opt.seed_len or Seed.dfl_len)
  912. self._check_valid_offset(self.infile,'read')
  913. flgs = os.O_RDONLY|os.O_BINARY if g.platform == 'win' else os.O_RDONLY
  914. fh = os.open(self.infile.name,flgs)
  915. os.lseek(fh,int(d.hincog_offset),os.SEEK_SET)
  916. self.fmt_data = os.read(fh,d.target_data_len)
  917. os.close(fh)
  918. qmsg(f'Data read from file {self.infile.name!r} at offset {d.hincog_offset}')
  919. # overrides method in Wallet
  920. def write_to_file(self):
  921. d = self.ssdata
  922. self._format()
  923. compare_or_die(d.target_data_len, 'target data length',
  924. len(self.fmt_data),'length of formatted ' + self.desc)
  925. k = ('output','input')[self.op=='pwchg_new']
  926. fn,d.hincog_offset = self._get_hincog_params(k)
  927. if opt.outdir and not os.path.dirname(fn):
  928. fn = os.path.join(opt.outdir,fn)
  929. check_offset = True
  930. try:
  931. os.stat(fn)
  932. except:
  933. if keypress_confirm(
  934. f'Requested file {fn!r} does not exist. Create?',
  935. default_yes = True ):
  936. min_fsize = d.target_data_len + d.hincog_offset
  937. msg(self.msg['choose_file_size'].format(min_fsize))
  938. while True:
  939. fsize = parse_bytespec(line_input('Enter file size: '))
  940. if fsize >= min_fsize:
  941. break
  942. msg(f'File size must be an integer no less than {min_fsize}')
  943. from .tool.fileutil import tool_cmd
  944. tool_cmd().rand2file(fn,str(fsize))
  945. check_offset = False
  946. else:
  947. die(1,'Exiting at user request')
  948. from .filename import Filename
  949. f = Filename(fn,subclass=type(self),write=True)
  950. dmsg('{} data len {}, offset {}'.format(
  951. capfirst(self.desc),
  952. d.target_data_len,
  953. d.hincog_offset ))
  954. if check_offset:
  955. self._check_valid_offset(f,'write')
  956. if not opt.quiet:
  957. confirm_or_raise( '', f'alter file {f.name!r}' )
  958. flgs = os.O_RDWR|os.O_BINARY if g.platform == 'win' else os.O_RDWR
  959. fh = os.open(f.name,flgs)
  960. os.lseek(fh, int(d.hincog_offset), os.SEEK_SET)
  961. os.write(fh, self.fmt_data)
  962. os.close(fh)
  963. msg('{} written to file {!r} at offset {}'.format(
  964. capfirst(self.desc),
  965. f.name,
  966. d.hincog_offset ))