wallet.py 33 KB

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