main_wallet.py 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
  1. #!/usr/bin/env python3
  2. #
  3. # mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
  4. # Copyright (C)2013-2020 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. mmgen/main_wallet: Entry point for MMGen wallet-related scripts
  20. """
  21. import os
  22. from .common import *
  23. from .wallet import Wallet,MMGenWallet
  24. from .filename import find_file_in_dir
  25. from .obj import MMGenWalletLabel,MasterShareIdx
  26. usage = '[opts] [infile]'
  27. nargs = 1
  28. iaction = 'convert'
  29. oaction = 'convert'
  30. do_bw_note = True
  31. do_sw_note = False
  32. do_ss_note = False
  33. invoked_as = {
  34. 'mmgen-walletgen': 'gen',
  35. 'mmgen-walletconv': 'conv',
  36. 'mmgen-walletchk': 'chk',
  37. 'mmgen-passchg': 'passchg',
  38. 'mmgen-subwalletgen': 'subgen',
  39. 'mmgen-seedsplit': 'seedsplit',
  40. }[g.prog_name]
  41. dsw = 'the default or specified {pnm} wallet'
  42. # full: defhHiJkKlLmoOpPqrSvz-
  43. if invoked_as == 'gen':
  44. desc = 'Generate an {pnm} wallet from a random seed'
  45. opt_filter = 'ehdoJlLpPqrSvz-'
  46. usage = '[opts]'
  47. oaction = 'output'
  48. nargs = 0
  49. elif invoked_as == 'conv':
  50. desc = 'Convert ' + dsw + ' from one format to another'
  51. opt_filter = 'dehHiJkKlLmoOpPqrSvz-'
  52. elif invoked_as == 'chk':
  53. desc = 'Check validity of ' + dsw
  54. opt_filter = 'ehiHOlpPqrvz-'
  55. iaction = 'input'
  56. elif invoked_as == 'passchg':
  57. desc = 'Change the passphrase, hash preset or label of ' + dsw
  58. opt_filter = 'efhdiHkKOlLmpPqrSvz-'
  59. iaction = 'input'
  60. do_bw_note = False
  61. elif invoked_as == 'subgen':
  62. desc = 'Generate a subwallet from ' + dsw
  63. opt_filter = 'dehHiJkKlLmoOpPqrSvz-' # omitted: f
  64. usage = '[opts] [infile] <Subseed Index>'
  65. iaction = 'input'
  66. oaction = 'output'
  67. do_sw_note = True
  68. elif invoked_as == 'seedsplit':
  69. desc = 'Generate a seed share from ' + dsw
  70. opt_filter = 'dehHiJlLMIoOpPqrSvz-'
  71. usage = '[opts] [infile] [<Split ID String>:]<index>:<share count>'
  72. iaction = 'input'
  73. oaction = 'output'
  74. do_ss_note = True
  75. opts_data = {
  76. 'text': {
  77. 'desc': desc.format(pnm=g.proj_name),
  78. 'usage': usage,
  79. 'options': """
  80. -h, --help Print this help message
  81. --, --longhelp Print help message for long options (common options)
  82. -d, --outdir= d Output files to directory 'd' instead of working dir
  83. -e, --echo-passphrase Echo passphrases and other user input to screen
  84. -f, --force-update Force update of wallet even if nothing has changed
  85. -i, --in-fmt= f {iaction} from wallet format 'f' (see FMT CODES below)
  86. -o, --out-fmt= f {oaction} to wallet format 'f' (see FMT CODES below)
  87. -H, --hidden-incog-input-params=f,o Read hidden incognito data from file
  88. 'f' at offset 'o' (comma-separated)
  89. -J, --hidden-incog-output-params=f,o Write hidden incognito data to file
  90. 'f' at offset 'o' (comma-separated). File 'f' will be
  91. created if necessary and filled with random data.
  92. -O, --old-incog-fmt Specify old-format incognito input
  93. -k, --keep-passphrase Reuse passphrase of input wallet for output wallet
  94. -K, --keep-hash-preset Reuse hash preset of input wallet for output wallet
  95. -l, --seed-len= l Specify wallet seed length of 'l' bits. This option
  96. is required only for brainwallet and incognito inputs
  97. with non-standard (< {g.seed_len}-bit) seed lengths.
  98. -L, --label= l Specify a label 'l' for output wallet
  99. -m, --keep-label Reuse label of input wallet for output wallet
  100. -M, --master-share=i Use a master share with index 'i' (min:{ms_min}, max:{ms_max})
  101. -p, --hash-preset= p Use the scrypt hash parameters defined by preset 'p'
  102. for password hashing (default: '{g.hash_preset}')
  103. -z, --show-hash-presets Show information on available hash presets
  104. -P, --passwd-file= f Get wallet passphrase from file 'f'
  105. -q, --quiet Produce quieter output; suppress some warnings
  106. -r, --usr-randchars=n Get 'n' characters of additional randomness from user
  107. (min={g.min_urandchars}, max={g.max_urandchars}, default={g.usr_randchars})
  108. -S, --stdout Write wallet data to stdout instead of file
  109. -v, --verbose Produce more verbose output
  110. """,
  111. 'notes': """
  112. {n_ss}{n_sw}{n_pw}{n_bw}
  113. FMT CODES:
  114. {f}
  115. """
  116. },
  117. 'code': {
  118. 'options': lambda s: s.format(
  119. iaction=capfirst(iaction),
  120. oaction=capfirst(oaction),
  121. ms_min=MasterShareIdx.min_val,
  122. ms_max=MasterShareIdx.max_val,
  123. g=g,
  124. ),
  125. 'notes': lambda s: s.format(
  126. f='\n '.join(Wallet.format_fmt_codes().splitlines()),
  127. n_ss=('',help_notes('seedsplit')+'\n\n')[do_ss_note],
  128. n_sw=('',help_notes('subwallet')+'\n\n')[do_sw_note],
  129. n_pw=help_notes('passwd'),
  130. n_bw=('','\n\n'+help_notes('brainwallet'))[do_bw_note]
  131. )
  132. }
  133. }
  134. cmd_args = opts.init(opts_data,opt_filter=opt_filter)
  135. if opt.label:
  136. opt.label = MMGenWalletLabel(opt.label,msg="Error in option '--label'")
  137. if invoked_as == 'subgen':
  138. from .obj import SubSeedIdx
  139. ss_idx = SubSeedIdx(cmd_args.pop())
  140. elif invoked_as == 'seedsplit':
  141. from .obj import SeedSplitSpecifier
  142. master_share = MasterShareIdx(opt.master_share) if opt.master_share else None
  143. if cmd_args:
  144. sss = SeedSplitSpecifier(cmd_args.pop(),on_fail='silent')
  145. if master_share:
  146. if not sss:
  147. sss = SeedSplitSpecifier('1:2')
  148. elif sss.idx == 1:
  149. m1 = 'Share index of 1 meaningless in master share context.'
  150. m2 = 'To generate a master share, omit the seed split specifier.'
  151. die(1,m1+' '+m2)
  152. elif not sss:
  153. opts.usage()
  154. elif master_share:
  155. sss = SeedSplitSpecifier('1:2')
  156. else:
  157. opts.usage()
  158. if cmd_args:
  159. if invoked_as == 'gen' or len(cmd_args) > 1:
  160. opts.usage()
  161. check_infile(cmd_args[0])
  162. sf = get_seed_file(cmd_args,nargs,invoked_as=invoked_as)
  163. if invoked_as != 'chk':
  164. do_license_msg()
  165. if invoked_as == 'gen':
  166. ss_in = None
  167. else:
  168. ss_in = Wallet(sf,passchg=(invoked_as=='passchg'))
  169. m1 = green('Processing input wallet ')
  170. m2 = ss_in.seed.sid.hl()
  171. m3 = yellow(' (default wallet)') if sf and os.path.dirname(sf) == g.data_dir else ''
  172. msg(m1+m2+m3)
  173. if invoked_as == 'chk':
  174. lbl = ss_in.ssdata.label.hl() if hasattr(ss_in.ssdata,'label') else 'NONE'
  175. vmsg('Wallet label: {}'.format(lbl))
  176. # TODO: display creation date
  177. sys.exit(0)
  178. if invoked_as != 'gen':
  179. gmsg_r('Processing output wallet' + ('\n',' ')[invoked_as == 'seedsplit'])
  180. if invoked_as == 'subgen':
  181. ss_out = Wallet(seed_bin=ss_in.seed.subseed(ss_idx,print_msg=True).data)
  182. elif invoked_as == 'seedsplit':
  183. shares = ss_in.seed.split(sss.count,sss.id,master_share)
  184. seed_out = shares.get_share_by_idx(sss.idx,base_seed=True)
  185. msg(seed_out.get_desc(ui=True))
  186. ss_out = Wallet(seed=seed_out)
  187. else:
  188. ss_out = Wallet(ss=ss_in,passchg=invoked_as=='passchg')
  189. if invoked_as == 'gen':
  190. qmsg("This wallet's Seed ID: {}".format(ss_out.seed.sid.hl()))
  191. if invoked_as == 'passchg':
  192. if not (opt.force_update or [k for k in ('passwd','hash_preset','label')
  193. if getattr(ss_out.ssdata,k) != getattr(ss_in.ssdata,k)]):
  194. die(1,'Password, hash preset and label are unchanged. Taking no action')
  195. if invoked_as == 'passchg' and ss_in.infile.dirname == g.data_dir:
  196. m1 = yellow('Confirmation of default wallet update')
  197. m2 = 'update the default wallet'
  198. confirm_or_raise(m1,m2,exit_msg='Password not changed')
  199. ss_out.write_to_file(desc='New wallet',outdir=g.data_dir)
  200. msg('Securely deleting old wallet')
  201. from subprocess import run
  202. wipe_cmd = ['sdelete','-p','20'] if g.platform=='win' else ['wipe','-sf']
  203. try:
  204. run(wipe_cmd + [ss_in.infile.name],check=True)
  205. except:
  206. ymsg("WARNING: '{}' command failed, using regular file delete instead".format(wipe_cmd[0]))
  207. os.unlink(ss_in.infile.name)
  208. else:
  209. try:
  210. assert invoked_as == 'gen','dw'
  211. assert not opt.outdir,'dw'
  212. assert not opt.stdout,'dw'
  213. assert not find_file_in_dir(MMGenWallet,g.data_dir),'dw'
  214. m = 'Make this wallet your default and move it to the data directory?'
  215. assert keypress_confirm(m,default_yes=True),'dw'
  216. except Exception as e:
  217. if e.args[0] != 'dw': raise
  218. ss_out.write_to_file()
  219. else:
  220. ss_out.write_to_file(outdir=g.data_dir)
  221. if invoked_as == 'passchg':
  222. if ss_out.ssdata.passwd == ss_in.ssdata.passwd:
  223. msg('New and old passphrases are the same')
  224. else:
  225. msg('Wallet passphrase has changed')
  226. if ss_out.ssdata.hash_preset != ss_in.ssdata.hash_preset:
  227. msg("Hash preset has been changed to '{}'".format(ss_out.ssdata.hash_preset))