main_wallet.py 10 KB

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