addrfile.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349
  1. #!/usr/bin/env python3
  2. #
  3. # mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
  4. # Copyright (C)2013-2023 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. addrfile: Address and password file classes for the MMGen suite
  20. """
  21. from .util import msg,qmsg,qmsg_r,die,capfirst
  22. from .protocol import init_proto
  23. from .obj import MMGenObject,TwComment,WalletPassword,MMGenPWIDString
  24. from .seed import SeedID,is_seed_id
  25. from .key import PrivKey
  26. from .addr import ViewKey,AddrListID,MMGenAddrType,MMGenPasswordType,is_addr_idx
  27. from .addrlist import KeyList,AddrListData,dmsg_sc
  28. class AddrFile(MMGenObject):
  29. desc = 'addresses'
  30. ext = 'addrs'
  31. line_ctr = 0
  32. file_header = """
  33. # {pnm} address file
  34. #
  35. # This file is editable.
  36. # Everything following a hash symbol '#' is a comment and ignored by {pnm}.
  37. # A text label of {n} screen cells or less may be added to the right of each
  38. # address, and it will be appended to the tracking wallet label upon import.
  39. # The label may contain any printable ASCII symbol.
  40. """
  41. def __init__(self,parent):
  42. self.parent = parent
  43. self.infile = None
  44. def encrypt(self):
  45. from .crypto import mmgen_encrypt,mmenc_ext
  46. from .globalvars import g
  47. self.fmt_data = mmgen_encrypt(
  48. data = self.fmt_data.encode(),
  49. desc = f'new {self.parent.desc} list' )
  50. self.ext += f'.{mmenc_ext}'
  51. @property
  52. def filename(self):
  53. from .globalvars import g
  54. return '{}{x}{}.{}'.format(
  55. self.parent.id_str,
  56. ('.' + self.parent.proto.network) if self.parent.proto.testnet else '',
  57. self.ext,
  58. x = '-α' if g.debug_utf8 else '' )
  59. def write(self,fn=None,ask_tty=True,ask_write_default_yes=False,binary=False,desc=None):
  60. from .opts import opt
  61. from .fileutil import write_data_to_file
  62. write_data_to_file(
  63. fn or self.filename,
  64. self.fmt_data,
  65. desc or self.desc,
  66. ask_tty = self.parent.has_keys and not opt.quiet,
  67. binary = binary )
  68. def make_label(self):
  69. p = self.parent
  70. bc,mt = p.proto.base_coin,p.al_id.mmtype
  71. l_coin = [] if bc == 'BTC' else [p.proto.coin] if bc == 'ETH' else [bc]
  72. l_type = [] if mt == 'E' or (mt == 'L' and not p.proto.testnet) else [mt.name.upper()]
  73. l_tn = [] if not p.proto.testnet else [p.proto.network.upper()]
  74. lbl_p2 = ':'.join(l_coin+l_type+l_tn)
  75. return p.al_id.sid + ('',' ')[bool(lbl_p2)] + lbl_p2
  76. def format(self,add_comments=False):
  77. p = self.parent
  78. fh = (
  79. self.file_header_mn.format(p.pw_fmt.upper())
  80. if p.gen_passwds and p.pw_fmt in ('bip39','xmrseed') else
  81. self.file_header ).strip()
  82. from .globalvars import g
  83. out = [fh.format(pnm=g.proj_name,n=TwComment.max_screen_width) + '\n']
  84. if p.chksum:
  85. out.append(f'# {capfirst(p.desc)} data checksum for {p.id_str}: {p.chksum}')
  86. out.append('# Record this value to a secure location.\n')
  87. lbl = self.make_label()
  88. dmsg_sc('lbl',lbl[9:])
  89. out.append(f'{lbl} {{')
  90. fs = ' {:<%s} {:<34}{}' % len(str(p.data[-1].idx))
  91. for e in p.data:
  92. c = ' ' + e.comment if add_comments and e.comment else ''
  93. if type(p) == KeyList:
  94. out.append(fs.format( e.idx, f'{p.al_id.mmtype.wif_label}: {e.sec.wif}', c ))
  95. elif type(p).__name__ == 'PasswordList':
  96. out.append(fs.format(e.idx,e.passwd,c))
  97. else: # First line with idx
  98. out.append(fs.format(e.idx,e.addr,c))
  99. if p.has_keys:
  100. from .opts import opt
  101. if opt.b16:
  102. out.append(fs.format( '', f'orig_hex: {e.sec.orig_bytes.hex()}', c ))
  103. out.append(fs.format( '', f'{p.al_id.mmtype.wif_label}: {e.sec.wif}', c ))
  104. for k in ('viewkey','wallet_passwd'):
  105. v = getattr(e,k)
  106. if v: out.append(fs.format( '', f'{k}: {v}', c ))
  107. out.append('}')
  108. self.fmt_data = '\n'.join([l.rstrip() for l in out]) + '\n'
  109. def get_line(self,lines):
  110. ret = lines.pop(0).split(None,2)
  111. self.line_ctr += 1
  112. if ret[0] == 'orig_hex:': # hacky
  113. ret = lines.pop(0).split(None,2)
  114. self.line_ctr += 1
  115. return ret if len(ret) == 3 else ret + ['']
  116. def parse_file_body(self,lines):
  117. p = self.parent
  118. ret = AddrListData()
  119. le = p.entry_type
  120. iifs = "{!r}: invalid identifier [expected '{}:']"
  121. while lines:
  122. idx,addr,comment = self.get_line(lines)
  123. assert is_addr_idx(idx), f'invalid address index {idx!r}'
  124. p.check_format(addr)
  125. a = le(**{ 'proto': p.proto, 'idx':int(idx), p.main_attr:addr, 'comment':comment })
  126. if p.has_keys: # order: wif,(orig_hex),viewkey,wallet_passwd
  127. d = self.get_line(lines)
  128. assert d[0] == p.al_id.mmtype.wif_label+':', iifs.format(d[0],p.al_id.mmtype.wif_label)
  129. a.sec = PrivKey(proto=p.proto,wif=d[1])
  130. for k,dtype,add_proto in (
  131. ('viewkey',ViewKey,True),
  132. ('wallet_passwd',WalletPassword,False) ):
  133. if k in p.al_id.mmtype.extra_attrs:
  134. d = self.get_line(lines)
  135. assert d[0] == k+':', iifs.format(d[0],k)
  136. setattr(a,k,dtype( *((p.proto,d[1]) if add_proto else (d[1],)) ) )
  137. ret.append(a)
  138. if p.has_keys and p.ka_validity_chk != False:
  139. def verify_keys():
  140. from .addrgen import KeyGenerator,AddrGenerator
  141. kg = KeyGenerator(p.proto,p.al_id.mmtype.pubkey_type)
  142. ag = AddrGenerator(p.proto,p.al_id.mmtype)
  143. llen = len(ret)
  144. for n,e in enumerate(ret):
  145. qmsg_r(f'\rVerifying keys {n+1}/{llen}')
  146. assert e.addr == ag.to_addr(kg.gen_data(e.sec)),(
  147. f'Key doesn’t match address!\n {e.sec.wif}\n {e.addr}')
  148. qmsg(' - done')
  149. from .opts import opt
  150. if opt.yes or p.ka_validity_chk == True:
  151. verify_keys()
  152. else:
  153. from .ui import keypress_confirm
  154. if keypress_confirm('Check key-to-address validity?'):
  155. verify_keys()
  156. return ret
  157. def parse_file(self,fn,buf=[],exit_on_error=True):
  158. def parse_addrfile_label(lbl):
  159. """
  160. label examples:
  161. - Bitcoin legacy mainnet: no label
  162. - Bitcoin legacy testnet: 'LEGACY:TESTNET'
  163. - Bitcoin Segwit: 'SEGWIT'
  164. - Bitcoin Segwit testnet: 'SEGWIT:TESTNET'
  165. - Bitcoin Bech32 regtest: 'BECH32:REGTEST'
  166. - Litecoin legacy mainnet: 'LTC'
  167. - Litecoin Bech32 mainnet: 'LTC:BECH32'
  168. - Litecoin legacy testnet: 'LTC:LEGACY:TESTNET'
  169. - Ethereum mainnet: 'ETH'
  170. - Ethereum Classic mainnet: 'ETC'
  171. - Ethereum regtest: 'ETH:REGTEST'
  172. """
  173. lbl = lbl.lower()
  174. # remove the network component:
  175. if lbl.endswith(':testnet'):
  176. network = 'testnet'
  177. lbl = lbl[:-8]
  178. elif lbl.endswith(':regtest'):
  179. network = 'regtest'
  180. lbl = lbl[:-8]
  181. else:
  182. network = 'mainnet'
  183. from .proto.btc.params import mainnet
  184. if lbl in [MMGenAddrType(mainnet,key).name for key in mainnet.mmtypes]:
  185. coin,mmtype_key = ( 'BTC', lbl )
  186. elif ':' in lbl: # first component is coin, second is mmtype_key
  187. coin,mmtype_key = lbl.split(':')
  188. else: # only component is coin
  189. coin,mmtype_key = ( lbl, None )
  190. proto = init_proto(coin=coin,network=network)
  191. if mmtype_key == None:
  192. mmtype_key = proto.mmtypes[0]
  193. return ( proto, proto.addr_type(mmtype_key) )
  194. p = self.parent
  195. from .fileutil import get_lines_from_file
  196. lines = get_lines_from_file(fn,p.desc+' data',trim_comments=True)
  197. try:
  198. assert len(lines) >= 3, f'Too few lines in address file ({len(lines)})'
  199. ls = lines[0].split()
  200. assert 1 < len(ls) < 5, f'Invalid first line for {p.gen_desc} file: {lines[0]!r}'
  201. assert ls[-1] == '{', f'{ls!r}: invalid first line'
  202. ls.pop()
  203. assert lines[-1] == '}', f'{lines[-1]!r}: invalid last line'
  204. sid = ls.pop(0)
  205. assert is_seed_id(sid), f'{sid!r}: invalid Seed ID'
  206. if type(p).__name__ == 'PasswordList' and len(ls) == 2:
  207. ss = ls.pop().split(':')
  208. assert len(ss) == 2, f'{ss!r}: invalid password length specifier (must contain colon)'
  209. p.set_pw_fmt(ss[0])
  210. p.set_pw_len(ss[1])
  211. p.pw_id_str = MMGenPWIDString(ls.pop())
  212. modname,funcname = p.pw_info[p.pw_fmt].chk_func.split('.')
  213. import importlib
  214. p.chk_func = getattr(importlib.import_module('mmgen.'+modname),funcname)
  215. proto = init_proto('btc') # FIXME: dummy protocol
  216. mmtype = MMGenPasswordType(proto,'P')
  217. elif len(ls) == 1:
  218. proto,mmtype = parse_addrfile_label(ls[0])
  219. elif len(ls) == 0:
  220. proto = init_proto('btc')
  221. mmtype = proto.addr_type('L')
  222. else:
  223. raise ValueError(f'{lines[0]}: Invalid first line for {p.gen_desc} file {fn!r}')
  224. if type(p).__name__ != 'PasswordList':
  225. if proto.base_coin != p.proto.base_coin or proto.network != p.proto.network:
  226. """
  227. Having caller supply protocol and checking address file protocol against it here
  228. allows us to catch all mismatches in one place. This behavior differs from that of
  229. transaction files, which determine the protocol independently, requiring the caller
  230. to check for protocol mismatches (e.g. mmgen.tx.completed.check_correct_chain())
  231. """
  232. raise ValueError(
  233. f'{p.desc} file is '
  234. + f'{proto.base_coin} {proto.network} but protocol is '
  235. + f'{p.proto.base_coin} {p.proto.network}' )
  236. p.base_coin = proto.base_coin
  237. p.network = proto.network
  238. p.al_id = AddrListID( sid=SeedID(sid=sid), mmtype=mmtype )
  239. data = self.parse_file_body(lines[1:-1])
  240. assert isinstance(data,list),'Invalid file body data'
  241. except Exception as e:
  242. m = 'Invalid data in {} list file {!r}{} ({!s})'.format(
  243. p.desc,
  244. self.infile,
  245. (f', content line {self.line_ctr}' if self.line_ctr else ''),
  246. e )
  247. if exit_on_error:
  248. die(3,m)
  249. else:
  250. msg(m)
  251. return False
  252. return data
  253. class KeyAddrFile(AddrFile):
  254. desc = 'secret keys'
  255. ext = 'akeys'
  256. class KeyFile(KeyAddrFile):
  257. ext = 'keys'
  258. file_header = """
  259. # {pnm} key file
  260. #
  261. # This file is editable.
  262. # Everything following a hash symbol '#' is a comment and ignored by {pnm}.
  263. """
  264. class PasswordFile(AddrFile):
  265. desc = 'passwords'
  266. ext = 'pws'
  267. file_header = """
  268. # {pnm} password file
  269. #
  270. # This file is editable.
  271. # Everything following a hash symbol '#' is a comment and ignored by {pnm}.
  272. # A text label of {n} screen cells or less may be added to the right of each
  273. # password. The label may contain any printable ASCII symbol.
  274. #
  275. """
  276. file_header_mn = """
  277. # {{pnm}} {} password file
  278. #
  279. # This file is editable.
  280. # Everything following a hash symbol '#' is a comment and ignored by {{pnm}}.
  281. #
  282. """
  283. def get_line(self,lines):
  284. self.line_ctr += 1
  285. p = self.parent
  286. if p.pw_fmt in ('bip39','xmrseed'):
  287. ret = lines.pop(0).split(None,p.pw_len+1)
  288. if len(ret) > p.pw_len+1:
  289. m1 = f'extraneous text {ret[p.pw_len+1]!r} found after password'
  290. m2 = '[bare comments not allowed in BIP39 password files]'
  291. m = m1+' '+m2
  292. elif len(ret) < p.pw_len+1:
  293. m = f'invalid password length {len(ret)-1}'
  294. else:
  295. return (ret[0],' '.join(ret[1:p.pw_len+1]),'')
  296. raise ValueError(m)
  297. else:
  298. ret = lines.pop(0).split(None,2)
  299. return ret if len(ret) == 3 else ret + ['']
  300. def make_label(self):
  301. p = self.parent
  302. return f'{p.al_id.sid} {p.pw_id_str} {p.pw_fmt_disp}:{p.pw_len}'