addrfile.py 11 KB

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