addrfile.py 11 KB

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