mmgen.py 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. #!/usr/bin/env python3
  2. #
  3. # mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
  4. # Copyright (C)2013-2023 The MMGen Project <mmgen@tuta.io>
  5. # Licensed under the GNU General Public License, Version 3:
  6. # https://www.gnu.org/licenses
  7. # Public project repositories:
  8. # https://github.com/mmgen/mmgen
  9. # https://gitlab.com/mmgen/mmgen
  10. """
  11. wallet.mmgen: MMGen native wallet class
  12. """
  13. import os
  14. from ..globalvars import g
  15. from ..opts import opt
  16. from ..seed import Seed
  17. from ..util import msg,qmsg,make_timestamp,make_chksum_6,split_into_cols,is_chksum_6,compare_chksums
  18. from ..obj import MMGenWalletLabel,get_obj
  19. from ..baseconv import baseconv
  20. import mmgen.crypto as crypto
  21. from .enc import wallet
  22. class wallet(wallet):
  23. desc = 'MMGen wallet'
  24. def __init__(self,*args,**kwargs):
  25. if opt.label:
  26. self.label = MMGenWalletLabel(
  27. opt.label,
  28. msg = "Error in option '--label'" )
  29. else:
  30. self.label = None
  31. super().__init__(*args,**kwargs)
  32. # logic identical to _get_hash_preset_from_user()
  33. def _get_label_from_user(self,old_lbl=''):
  34. prompt = 'Enter a wallet label, or hit ENTER {}: '.format(
  35. 'to reuse the label {}'.format(old_lbl.hl2(encl='‘’')) if old_lbl else
  36. 'for no label' )
  37. from ..ui import line_input
  38. while True:
  39. ret = line_input(prompt)
  40. if ret:
  41. lbl = get_obj(MMGenWalletLabel,s=ret)
  42. if lbl:
  43. return lbl
  44. else:
  45. msg('Invalid label. Trying again...')
  46. else:
  47. return old_lbl or MMGenWalletLabel('No Label')
  48. # logic identical to _get_hash_preset()
  49. def _get_label(self):
  50. if hasattr(self,'ss_in') and hasattr(self.ss_in.ssdata,'label'):
  51. old_lbl = self.ss_in.ssdata.label
  52. if opt.keep_label:
  53. lbl = old_lbl
  54. qmsg('Reusing label {} at user request'.format( lbl.hl2(encl='‘’') ))
  55. elif self.label:
  56. lbl = self.label
  57. qmsg('Using label {} requested on command line'.format( lbl.hl2(encl='‘’') ))
  58. else: # Prompt, using old value as default
  59. lbl = self._get_label_from_user(old_lbl)
  60. if (not opt.keep_label) and self.op == 'pwchg_new':
  61. qmsg('Label {}'.format( 'unchanged' if lbl == old_lbl else f'changed to {lbl!r}' ))
  62. elif self.label:
  63. lbl = self.label
  64. qmsg('Using label {} requested on command line'.format( lbl.hl2(encl='‘’') ))
  65. else:
  66. lbl = self._get_label_from_user()
  67. self.ssdata.label = lbl
  68. def _encrypt(self):
  69. self._get_first_pw_and_hp_and_encrypt_seed()
  70. self._get_label()
  71. d = self.ssdata
  72. d.pw_status = ('NE','E')[len(d.passwd)==0]
  73. d.timestamp = make_timestamp()
  74. def _format(self):
  75. d = self.ssdata
  76. s = self.seed
  77. bc = baseconv('b58')
  78. slt_fmt = bc.frombytes(d.salt,pad='seed',tostr=True)
  79. es_fmt = bc.frombytes(d.enc_seed,pad='seed',tostr=True)
  80. lines = (
  81. d.label,
  82. '{} {} {} {} {}'.format( s.sid.lower(), d.key_id.lower(), s.bitlen, d.pw_status, d.timestamp ),
  83. '{}: {} {} {}'.format( d.hash_preset, *crypto.get_hash_params(d.hash_preset) ),
  84. '{} {}'.format( make_chksum_6(slt_fmt), split_into_cols(4,slt_fmt) ),
  85. '{} {}'.format( make_chksum_6(es_fmt), split_into_cols(4,es_fmt) )
  86. )
  87. chksum = make_chksum_6(' '.join(lines).encode())
  88. self.fmt_data = '\n'.join((chksum,)+lines) + '\n'
  89. def _deformat(self):
  90. def check_master_chksum(lines,desc):
  91. if len(lines) != 6:
  92. msg(f'Invalid number of lines ({len(lines)}) in {desc} data')
  93. return False
  94. if not is_chksum_6(lines[0]):
  95. msg(f'Incorrect master checksum ({lines[0]}) in {desc} data')
  96. return False
  97. chk = make_chksum_6(' '.join(lines[1:]))
  98. if not compare_chksums(lines[0],'master',chk,'computed',
  99. hdr='For wallet master checksum',verbose=True):
  100. return False
  101. return True
  102. lines = self.fmt_data.splitlines()
  103. if not check_master_chksum(lines,self.desc):
  104. return False
  105. d = self.ssdata
  106. d.label = MMGenWalletLabel(lines[1])
  107. d1,d2,d3,d4,d5 = lines[2].split()
  108. d.seed_id = d1.upper()
  109. d.key_id = d2.upper()
  110. self.check_usr_seed_len(int(d3))
  111. d.pw_status,d.timestamp = d4,d5
  112. hpdata = lines[3].split()
  113. d.hash_preset = hp = hpdata[0][:-1] # a string!
  114. qmsg(f'Hash preset of wallet: {hp!r}')
  115. if opt.hash_preset and opt.hash_preset != hp:
  116. qmsg(f'Warning: ignoring user-requested hash preset {opt.hash_preset!r}')
  117. hash_params = tuple(map(int,hpdata[1:]))
  118. if hash_params != crypto.get_hash_params(d.hash_preset):
  119. msg(f'Hash parameters {" ".join(hash_params)!r} don’t match hash preset {d.hash_preset!r}')
  120. return False
  121. lmin,foo,lmax = sorted(baseconv('b58').seedlen_map_rev) # 22,33,44
  122. for i,key in (4,'salt'),(5,'enc_seed'):
  123. l = lines[i].split(' ')
  124. chk = l.pop(0)
  125. b58_val = ''.join(l)
  126. if len(b58_val) < lmin or len(b58_val) > lmax:
  127. msg(f'Invalid format for {key} in {self.desc}: {l}')
  128. return False
  129. if not compare_chksums(chk,key,
  130. make_chksum_6(b58_val),'computed checksum',verbose=True):
  131. return False
  132. val = baseconv('b58').tobytes(b58_val,pad='seed')
  133. if val == False:
  134. msg(f'Invalid base 58 number: {b58_val}')
  135. return False
  136. setattr(d,key,val)
  137. return True
  138. def _decrypt(self):
  139. d = self.ssdata
  140. # Needed for multiple transactions with {}-txsign
  141. d.passwd = self._get_passphrase(
  142. add_desc = os.path.basename(self.infile.name) if opt.quiet else '' )
  143. key = crypto.make_key( d.passwd, d.salt, d.hash_preset )
  144. ret = crypto.decrypt_seed( d.enc_seed, key, d.seed_id, d.key_id )
  145. if ret:
  146. self.seed = Seed(ret)
  147. return True
  148. else:
  149. return False
  150. def _filename(self):
  151. s = self.seed
  152. d = self.ssdata
  153. return '{}-{}[{},{}]{x}.{}'.format(
  154. s.fn_stem,
  155. d.key_id,
  156. s.bitlen,
  157. d.hash_preset,
  158. self.ext,
  159. x='-α' if g.debug_utf8 else '')