addrfile.py 12 KB

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