main_wallet.py 9.3 KB

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