passwdlist.py 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  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. passwdlist.py: Password list class for the MMGen suite
  20. """
  21. from collections import namedtuple
  22. from .exception import InvalidPasswdFormat
  23. from .util import ymsg,is_hex_str,is_int,keypress_confirm
  24. from .obj import ImmutableAttr,ListItemAttr,MMGenPWIDString
  25. from .baseconv import baseconv,is_b32_str,is_b58_str
  26. from .bip39 import is_bip39_str
  27. from .xmrseed import is_xmrseed
  28. from .key import PrivKey
  29. from .addr import MMGenPasswordType,AddrIdx,AddrListID
  30. from .addrlist import (
  31. AddrListChksum,
  32. AddrListIDStr,
  33. AddrListEntryBase,
  34. AddrList,
  35. dmsg_sc,
  36. )
  37. class PasswordListEntry(AddrListEntryBase):
  38. passwd = ListItemAttr(str,typeconv=False) # TODO: create Password type
  39. idx = ImmutableAttr(AddrIdx)
  40. label = ListItemAttr('TwComment',reassign_ok=True)
  41. sec = ListItemAttr(PrivKey,include_proto=True)
  42. class PasswordList(AddrList):
  43. entry_type = PasswordListEntry
  44. main_attr = 'passwd'
  45. desc = 'password'
  46. gen_desc = 'password'
  47. gen_desc_pl = 's'
  48. gen_addrs = False
  49. gen_keys = False
  50. gen_passwds = True
  51. pw_len = None
  52. dfl_pw_fmt = 'b58'
  53. pwinfo = namedtuple('passwd_info',['min_len','max_len','dfl_len','valid_lens','desc','chk_func'])
  54. pw_info = {
  55. 'b32': pwinfo(10, 42 ,24, None, 'base32 password', is_b32_str), # 32**24 < 2**128
  56. 'b58': pwinfo(8, 36 ,20, None, 'base58 password', is_b58_str), # 58**20 < 2**128
  57. 'bip39': pwinfo(12, 24 ,24, [12,18,24],'BIP39 mnemonic', is_bip39_str),
  58. 'xmrseed': pwinfo(25, 25, 25, [25], 'Monero new-style mnemonic',is_xmrseed),
  59. 'hex': pwinfo(32, 64 ,64, [32,48,64],'hexadecimal password', is_hex_str),
  60. }
  61. chksum_rec_f = lambda foo,e: (str(e.idx), e.passwd)
  62. feature_warn_fs = 'WARNING: {!r} is a potentially dangerous feature. Use at your own risk!'
  63. hex2bip39 = False
  64. def __init__(self,proto,
  65. infile = None,
  66. seed = None,
  67. pw_idxs = None,
  68. pw_id_str = None,
  69. pw_len = None,
  70. pw_fmt = None,
  71. chk_params_only = False,
  72. ):
  73. self.proto = proto # proto is ignored
  74. if infile:
  75. self.infile = infile
  76. self.data = self.get_file().parse_file(infile) # sets self.pw_id_str,self.pw_fmt,self.pw_len
  77. else:
  78. if not chk_params_only:
  79. for k in (seed,pw_idxs):
  80. assert k
  81. self.pw_id_str = MMGenPWIDString(pw_id_str)
  82. self.set_pw_fmt(pw_fmt)
  83. self.set_pw_len(pw_len)
  84. if chk_params_only:
  85. return
  86. if self.hex2bip39:
  87. ymsg(self.feature_warn_fs.format(pw_fmt))
  88. self.set_pw_len_vs_seed_len(pw_len,seed)
  89. self.al_id = AddrListID(seed.sid,MMGenPasswordType(self.proto,'P'))
  90. self.data = self.generate(seed,pw_idxs)
  91. self.num_addrs = len(self.data)
  92. self.fmt_data = ''
  93. self.chksum = AddrListChksum(self)
  94. fs = f'{self.al_id.sid}-{self.pw_id_str}-{self.pw_fmt_disp}-{self.pw_len}[{{}}]'
  95. self.id_str = AddrListIDStr(self,fs)
  96. self.do_chksum_msg(record=not infile)
  97. def set_pw_fmt(self,pw_fmt):
  98. if pw_fmt == 'hex2bip39':
  99. self.hex2bip39 = True
  100. self.pw_fmt = 'bip39'
  101. self.pw_fmt_disp = 'hex2bip39'
  102. else:
  103. self.pw_fmt = pw_fmt
  104. self.pw_fmt_disp = pw_fmt
  105. if self.pw_fmt not in self.pw_info:
  106. raise InvalidPasswdFormat(
  107. '{!r}: invalid password format. Valid formats: {}'.format(
  108. self.pw_fmt,
  109. ', '.join(self.pw_info) ))
  110. def chk_pw_len(self,passwd=None):
  111. if passwd is None:
  112. assert self.pw_len,'either passwd or pw_len must be set'
  113. pw_len = self.pw_len
  114. fs = '{l}: invalid user-requested length for {b} ({c}{m})'
  115. else:
  116. pw_len = len(passwd)
  117. fs = '{pw}: {b} has invalid length {l} ({c}{m} characters)'
  118. d = self.pw_info[self.pw_fmt]
  119. if d.valid_lens:
  120. if pw_len not in d.valid_lens:
  121. die(2, fs.format( l=pw_len, b=d.desc, c='not one of ', m=d.valid_lens, pw=passwd ))
  122. elif pw_len > d.max_len:
  123. die(2, fs.format( l=pw_len, b=d.desc, c='>', m=d.max_len, pw=passwd ))
  124. elif pw_len < d.min_len:
  125. die(2, fs.format( l=pw_len, b=d.desc, c='<', m=d.min_len, pw=passwd ))
  126. def set_pw_len(self,pw_len):
  127. d = self.pw_info[self.pw_fmt]
  128. if pw_len is None:
  129. self.pw_len = d.dfl_len
  130. return
  131. if not is_int(pw_len):
  132. die(2,f'{pw_len!r}: invalid user-requested password length (not an integer)')
  133. self.pw_len = int(pw_len)
  134. self.chk_pw_len()
  135. def set_pw_len_vs_seed_len(self,pw_len,seed):
  136. pf = self.pw_fmt
  137. if pf == 'hex':
  138. pw_bytes = self.pw_len // 2
  139. good_pw_len = seed.byte_len * 2
  140. elif pf == 'bip39':
  141. from .bip39 import bip39
  142. pw_bytes = bip39.nwords2seedlen(self.pw_len,in_bytes=True)
  143. good_pw_len = bip39.seedlen2nwords(seed.byte_len,in_bytes=True)
  144. elif pf == 'xmrseed':
  145. from .xmrseed import xmrseed
  146. pw_bytes = xmrseed().seedlen_map_rev[self.pw_len]
  147. try:
  148. good_pw_len = xmrseed().seedlen_map[seed.byte_len]
  149. except:
  150. die(1,f'{seed.byte_len*8}: unsupported seed length for Monero new-style mnemonic')
  151. elif pf in ('b32','b58'):
  152. pw_int = (32 if pf == 'b32' else 58) ** self.pw_len
  153. pw_bytes = pw_int.bit_length() // 8
  154. good_pw_len = len( baseconv(pf).frombytes(b'\xff'*seed.byte_len) )
  155. else:
  156. raise NotImplementedError(f'{pf!r}: unknown password format')
  157. if pw_bytes > seed.byte_len:
  158. die(1,
  159. 'Cannot generate passwords with more entropy than underlying seed! ({} bits)\n'.format(
  160. len(seed.data) * 8 ) + (
  161. 'Re-run the command with --passwd-len={}' if pf in ('bip39','hex') else
  162. 'Re-run the command, specifying a password length of {} or less'
  163. ).format(good_pw_len) )
  164. if pf in ('bip39','hex') and pw_bytes < seed.byte_len:
  165. if not keypress_confirm(
  166. f'WARNING: requested {self.pw_info[pf].desc} length has less entropy ' +
  167. 'than underlying seed!\nIs this what you want?',
  168. default_yes = True ):
  169. die(1,'Exiting at user request')
  170. def gen_passwd(self,secbytes):
  171. assert self.pw_fmt in self.pw_info
  172. if self.pw_fmt == 'hex':
  173. # take most significant part
  174. return secbytes.hex()[:self.pw_len]
  175. elif self.pw_fmt == 'bip39':
  176. from .bip39 import bip39
  177. pw_len_bytes = bip39.nwords2seedlen( self.pw_len, in_bytes=True )
  178. # take most significant part
  179. return ' '.join( bip39().fromhex(secbytes[:pw_len_bytes].hex()) )
  180. elif self.pw_fmt == 'xmrseed':
  181. from .xmrseed import xmrseed
  182. pw_len_bytes = xmrseed().seedlen_map_rev[self.pw_len]
  183. from .protocol import init_proto
  184. bytes_preproc = init_proto('xmr').preprocess_key(
  185. secbytes[:pw_len_bytes], # take most significant part
  186. None )
  187. return ' '.join( xmrseed().frombytes(bytes_preproc) )
  188. else:
  189. # take least significant part
  190. return baseconv(self.pw_fmt).frombytes(
  191. secbytes,
  192. pad = self.pw_len,
  193. tostr = True )[-self.pw_len:]
  194. def check_format(self,pw):
  195. if not self.pw_info[self.pw_fmt].chk_func(pw):
  196. raise ValueError(f'Password is not valid {self.pw_info[self.pw_fmt].desc} data')
  197. pwlen = len(pw.split()) if self.pw_fmt in ('bip39','xmrseed') else len(pw)
  198. if pwlen != self.pw_len:
  199. raise ValueError(f'Password has incorrect length ({pwlen} != {self.pw_len})')
  200. return True
  201. def scramble_seed(self,seed):
  202. # Changing either pw_fmt or pw_len will cause a different, unrelated
  203. # set of passwords to be generated: this is what we want.
  204. # NB: In original implementation, pw_id_str was 'baseN', not 'bN'
  205. scramble_key = f'{self.pw_fmt}:{self.pw_len}:{self.pw_id_str}'
  206. if self.hex2bip39:
  207. from .bip39 import bip39
  208. pwlen = bip39.nwords2seedlen(self.pw_len,in_hex=True)
  209. scramble_key = f'hex:{pwlen}:{self.pw_id_str}'
  210. from .crypto import scramble_seed
  211. dmsg_sc('str',scramble_key)
  212. return scramble_seed(seed,scramble_key.encode())