From 012fea543f4d1bfa5b0270e3b26bb2648547fdc8 Mon Sep 17 00:00:00 2001 From: The MMGen Project Date: Sat, 15 Jan 2022 14:00:07 +0000 Subject: [PATCH] addr.py: move address file-related methods to new AddrFile class - AddrFile subclasses: KeyAddrFile,KeyFile,PasswordFile --- mmgen/addr.py | 285 ++--------------------------- mmgen/addrfile.py | 339 +++++++++++++++++++++++++++++++++++ mmgen/main_addrgen.py | 13 +- mmgen/main_passgen.py | 10 +- mmgen/obj.py | 2 +- mmgen/tx.py | 3 +- test/test_py_d/ts_regtest.py | 5 +- 7 files changed, 371 insertions(+), 286 deletions(-) create mode 100755 mmgen/addrfile.py diff --git a/mmgen/addr.py b/mmgen/addr.py index 1a1c5281..1d522473 100755 --- a/mmgen/addr.py +++ b/mmgen/addr.py @@ -362,15 +362,6 @@ class AddrListIDStr(str,Hilite): class AddrList(MMGenObject): # Address info for a single seed ID msgs = { - 'file_header': """ -# {pnm} address file -# -# This file is editable. -# Everything following a hash symbol '#' is a comment and ignored by {pnm}. -# A text label of {n} screen cells or less may be added to the right of each -# address, and it will be appended to the tracking wallet label upon import. -# The label may contain any printable ASCII symbol. -""".strip().format(n=TwComment.max_screen_width,pnm=pnm), 'record_chksum': """ Record this checksum: it will be used to verify the address file in the future """.strip(), @@ -381,17 +372,14 @@ Removed {{}} duplicate WIF key{{}} from keylist (also in {pnm} key-address file } entry_type = AddrListEntry main_attr = 'addr' - data_desc = 'address' - file_desc = 'addresses' + desc = 'address' gen_desc = 'address' gen_desc_pl = 'es' gen_addrs = True gen_passwds = False gen_keys = False has_keys = False - ext = 'addrs' chksum_rec_f = lambda foo,e: (str(e.idx), e.addr) - line_ctr = 0 def __init__(self,proto, addrfile = '', @@ -424,7 +412,7 @@ Removed {{}} duplicate WIF key{{}} from keylist (also in {pnm} key-address file do_chksum = True elif addrfile: # data from MMGen address file self.infile = addrfile - adata = self.parse_file(addrfile) # sets self.al_id + adata = self.get_file().parse_file(addrfile) # sets self.al_id do_chksum = True elif al_id and adata: # data from tracking wallet self.al_id = al_id @@ -457,7 +445,7 @@ Removed {{}} duplicate WIF key{{}} from keylist (also in {pnm} key-address file if do_chksum and not skip_chksum: self.chksum = AddrListChksum(self) qmsg( - f'Checksum for {self.data_desc} data {self.id_str.hl()}: {self.chksum.hl()}\n' + + f'Checksum for {self.desc} data {self.id_str.hl()}: {self.chksum.hl()}\n' + self.msgs[('check_chksum','record_chksum')[src=='gen']] ) def update_msgs(self): @@ -546,17 +534,6 @@ Removed {{}} duplicate WIF key{{}} from keylist (also in {pnm} key-address file dmsg_sc('str',scramble_key) return scramble_seed(seed,scramble_key.encode()) - def encrypt(self,desc='new key list'): - from .crypto import mmgen_encrypt - self.fmt_data = mmgen_encrypt(self.fmt_data.encode(),desc,'') - self.ext += '.'+g.mmenc_ext - - def write_to_file(self,ask_tty=True,ask_write_default_yes=False,binary=False,desc=None): - tn = ('.' + self.proto.network) if self.proto.testnet else '' - fn = '{}{x}{}.{}'.format(self.id_str,tn,self.ext,x='-α' if g.debug_utf8 else '') - ask_tty = self.has_keys and not opt.quiet - write_data_to_file(fn,self.fmt_data,desc or self.file_desc,ask_tty=ask_tty,binary=binary) - def idxs(self): return [e.idx for e in self.data] @@ -628,225 +605,26 @@ Removed {{}} duplicate WIF key{{}} from keylist (also in {pnm} key-address file def list_missing(self,attr): return [d.addr for d in self.data if not getattr(d,attr)] - def make_label(self): - bc,mt = self.proto.base_coin,self.al_id.mmtype - l_coin = [] if bc == 'BTC' else [self.proto.coin] if bc == 'ETH' else [bc] - l_type = [] if mt == 'E' or (mt == 'L' and not self.proto.testnet) else [mt.name.upper()] - l_tn = [] if not self.proto.testnet else [self.proto.network.upper()] - lbl_p2 = ':'.join(l_coin+l_type+l_tn) - return self.al_id.sid + ('',' ')[bool(lbl_p2)] + lbl_p2 - - def format(self,add_comments=False): - - out = [self.msgs['file_header']+'\n'] - if self.chksum: - out.append(f'# {capfirst(self.data_desc)} data checksum for {self.id_str}: {self.chksum}') - out.append('# Record this value to a secure location.\n') - - lbl = self.make_label() - dmsg_sc('lbl',lbl[9:]) - out.append(f'{lbl} {{') - - fs = ' {:<%s} {:<34}{}' % len(str(self.data[-1].idx)) - for e in self.data: - c = ' '+e.label if add_comments and e.label else '' - if type(self) == KeyList: - out.append(fs.format( e.idx, f'{self.al_id.mmtype.wif_label}: {e.sec.wif}', c )) - elif type(self) == PasswordList: - out.append(fs.format(e.idx,e.passwd,c)) - else: # First line with idx - out.append(fs.format(e.idx,e.addr,c)) - if self.has_keys: - if opt.b16: - out.append(fs.format( '', f'orig_hex: {e.sec.orig_hex}', c )) - out.append(fs.format( '', f'{self.al_id.mmtype.wif_label}: {e.sec.wif}', c )) - for k in ('viewkey','wallet_passwd'): - v = getattr(e,k) - if v: out.append(fs.format( '', f'{k}: {v}', c )) - - out.append('}') - self.fmt_data = '\n'.join([l.rstrip() for l in out]) + '\n' - - def get_line(self,lines): - ret = lines.pop(0).split(None,2) - self.line_ctr += 1 - if ret[0] == 'orig_hex:': # hacky - ret = lines.pop(0).split(None,2) - self.line_ctr += 1 - return ret if len(ret) == 3 else ret + [''] - - def parse_file_body(self,lines): - - ret = AddrListData() - le = self.entry_type - iifs = "{!r}: invalid identifier [expected '{}:']" - - while lines: - idx,addr,lbl = self.get_line(lines) - - assert is_mmgen_idx(idx), f'invalid address index {idx!r}' - self.check_format(addr) - - a = le(**{ 'proto': self.proto, 'idx':int(idx), self.main_attr:addr, 'label':lbl }) - - if self.has_keys: # order: wif,(orig_hex),viewkey,wallet_passwd - d = self.get_line(lines) - assert d[0] == self.al_id.mmtype.wif_label+':', iifs.format(d[0],self.al_id.mmtype.wif_label) - a.sec = PrivKey(proto=self.proto,wif=d[1]) - for k,dtype,add_proto in ( - ('viewkey',ViewKey,True), - ('wallet_passwd',WalletPassword,False) ): - if k in self.al_id.mmtype.extra_attrs: - d = self.get_line(lines) - assert d[0] == k+':', iifs.format(d[0],k) - setattr(a,k,dtype( *((self.proto,d[1]) if add_proto else (d[1],)) ) ) - - ret.append(a) - - if self.has_keys and not self.skip_ka_check: - if getattr(opt,'yes',False) or keypress_confirm('Check key-to-address validity?'): - kg = KeyGenerator(self.proto,self.al_id.mmtype) - ag = AddrGenerator(self.proto,self.al_id.mmtype) - llen = len(ret) - for n,e in enumerate(ret): - qmsg_r(f'\rVerifying keys {n+1}/{llen}') - assert e.addr == ag.to_addr(kg.to_pubhex(e.sec)),( - f'Key doesn’t match address!\n {e.sec.wif}\n {e.addr}') - qmsg(' - done') - - return ret - - def parse_file(self,fn,buf=[],exit_on_error=True): - - def parse_addrfile_label(lbl): - """ - label examples: - - Bitcoin legacy mainnet: no label - - Bitcoin legacy testnet: 'LEGACY:TESTNET' - - Bitcoin Segwit: 'SEGWIT' - - Bitcoin Segwit testnet: 'SEGWIT:TESTNET' - - Bitcoin Bech32 regtest: 'BECH32:REGTEST' - - Litecoin legacy mainnet: 'LTC' - - Litecoin Bech32 mainnet: 'LTC:BECH32' - - Litecoin legacy testnet: 'LTC:LEGACY:TESTNET' - - Ethereum mainnet: 'ETH' - - Ethereum Classic mainnet: 'ETC' - - Ethereum regtest: 'ETH:REGTEST' - """ - lbl = lbl.lower() - - # remove the network component: - if lbl.endswith(':testnet'): - network = 'testnet' - lbl = lbl[:-8] - elif lbl.endswith(':regtest'): - network = 'regtest' - lbl = lbl[:-8] - else: - network = 'mainnet' - - if lbl in self.bitcoin_addrtypes: - coin,mmtype_key = ( 'BTC', lbl ) - elif ':' in lbl: # first component is coin, second is mmtype_key - coin,mmtype_key = lbl.split(':') - else: # only component is coin - coin,mmtype_key = ( lbl, None ) - - proto = init_proto(coin=coin,network=network) - - if mmtype_key == None: - mmtype_key = proto.mmtypes[0] - - return ( proto, proto.addr_type(mmtype_key) ) - - lines = get_lines_from_file(fn,self.data_desc+' data',trim_comments=True) - - try: - assert len(lines) >= 3, f'Too few lines in address file ({len(lines)})' - ls = lines[0].split() - assert 1 < len(ls) < 5, f'Invalid first line for {self.gen_desc} file: {lines[0]!r}' - assert ls.pop() == '{', f'{ls!r}: invalid first line' - assert lines[-1] == '}', f'{lines[-1]!r}: invalid last line' - sid = ls.pop(0) - assert is_seed_id(sid), f'{sid!r}: invalid Seed ID' - - if type(self) == PasswordList and len(ls) == 2: - ss = ls.pop().split(':') - assert len(ss) == 2, f'{ss!r}: invalid password length specifier (must contain colon)' - self.set_pw_fmt(ss[0]) - self.set_pw_len(ss[1]) - self.pw_id_str = MMGenPWIDString(ls.pop()) - proto = init_proto('btc')# FIXME: dummy protocol - mmtype = MMGenPasswordType(proto,'P') - elif len(ls) == 1: - proto,mmtype = parse_addrfile_label(ls[0]) - elif len(ls) == 0: - proto = init_proto('btc') - mmtype = proto.addr_type('L') - else: - raise ValueError(f'{lines[0]}: Invalid first line for {self.gen_desc} file {fn!r}') - - if type(self) != PasswordList: - if proto.base_coin != self.proto.base_coin or proto.network != self.proto.network: - """ - Having caller supply protocol and checking address file protocol against it here - allows us to catch all mismatches in one place. This behavior differs from that of - transaction files, which determine the protocol independently, requiring the caller - to check for protocol mismatches (e.g. MMGenTX.check_correct_chain()) - """ - raise ValueError( - f'{self.data_desc} file is ' - + f'{proto.base_coin} {proto.network} but protocol is ' - + f'{self.proto.base_coin} {self.proto.network}' ) - - self.base_coin = proto.base_coin - self.network = proto.network - self.al_id = AddrListID(SeedID(sid=sid),mmtype) - - data = self.parse_file_body(lines[1:-1]) - assert isinstance(data,list),'Invalid file body data' - except Exception as e: - m = 'Invalid data in {} list file {!r}{} ({!s})'.format( - self.data_desc, - self.infile, - (f', content line {self.line_ctr}' if self.line_ctr else ''), - e ) - if exit_on_error: - die(3,m) - else: - msg(m) - return False - - return data + def get_file(self): + import mmgen.addrfile as mod + return getattr( mod, type(self).__name__.replace('List','File') )(self) class KeyAddrList(AddrList): - data_desc = 'key-address' - file_desc = 'secret keys' + desc = 'key-address' gen_desc = 'key/address pair' gen_desc_pl = 's' gen_addrs = True gen_keys = True has_keys = True - ext = 'akeys' chksum_rec_f = lambda foo,e: (str(e.idx), e.addr, e.sec.wif) class KeyList(AddrList): - msgs = { - 'file_header': f""" -# {pnm} key file -# -# This file is editable. -# Everything following a hash symbol '#' is a comment and ignored by {pnm}. -""".strip() - } - data_desc = 'key' - file_desc = 'secret keys' + desc = 'key' gen_desc = 'key' gen_desc_pl = 's' gen_addrs = False gen_keys = True has_keys = True - ext = 'keys' chksum_rec_f = lambda foo,e: (str(e.idx), e.addr, e.sec.wif) def is_bip39_str(s): @@ -859,37 +637,19 @@ def is_xmrseed(s): from collections import namedtuple class PasswordList(AddrList): msgs = { - 'file_header': f""" -# {pnm} password file -# -# This file is editable. -# Everything following a hash symbol '#' is a comment and ignored by {pnm}. -# A text label of {TwComment.max_screen_width} screen cells or less may be added to the right of each -# password. The label may contain any printable ASCII symbol. -# -""".strip(), - 'file_header_mn': f""" -# {pnm} {{}} password file -# -# This file is editable. -# Everything following a hash symbol '#' is a comment and ignored by {pnm}. -# -""".strip(), 'record_chksum': """ Record this checksum: it will be used to verify the password file in the future """.strip() } entry_type = PasswordListEntry main_attr = 'passwd' - data_desc = 'password' - file_desc = 'passwords' + desc = 'password' gen_desc = 'password' gen_desc_pl = 's' gen_addrs = False gen_keys = False gen_passwds = True has_keys = False - ext = 'pws' pw_len = None dfl_pw_fmt = 'b58' pwinfo = namedtuple('passwd_info',['min_len','max_len','dfl_len','valid_lens','desc','chk_func']) @@ -920,7 +680,7 @@ Record this checksum: it will be used to verify the password file in the future if infile: self.infile = infile - self.data = self.parse_file(infile) # sets self.pw_id_str,self.pw_fmt,self.pw_len + self.data = self.get_file().parse_file(infile) # sets self.pw_id_str,self.pw_fmt,self.pw_len else: if not chk_params_only: for k in (seed,pw_idxs): @@ -936,9 +696,6 @@ Record this checksum: it will be used to verify the password file in the future self.al_id = AddrListID(seed.sid,MMGenPasswordType(self.proto,'P')) self.data = self.generate(seed,pw_idxs) - if self.pw_fmt in ('bip39','xmrseed'): - self.msgs['file_header'] = self.msgs['file_header_mn'].format(self.pw_fmt.upper()) - self.num_addrs = len(self.data) self.fmt_data = '' self.chksum = AddrListChksum(self) @@ -946,7 +703,7 @@ Record this checksum: it will be used to verify the password file in the future fs = f'{self.al_id.sid}-{self.pw_id_str}-{self.pw_fmt_disp}-{self.pw_len}[{{}}]' self.id_str = AddrListIDStr(self,fs) qmsg( - f'Checksum for {self.data_desc} data {self.id_str.hl()}: {self.chksum.hl()}\n' + + f'Checksum for {self.desc} data {self.id_str.hl()}: {self.chksum.hl()}\n' + self.msgs[('record_chksum','check_chksum')[bool(infile)]] ) def set_pw_fmt(self,pw_fmt): @@ -1072,26 +829,6 @@ Record this checksum: it will be used to verify the password file in the future dmsg_sc('str',scramble_key) return scramble_seed(seed,scramble_key.encode()) - def get_line(self,lines): - self.line_ctr += 1 - if self.pw_fmt in ('bip39','xmrseed'): - ret = lines.pop(0).split(None,self.pw_len+1) - if len(ret) > self.pw_len+1: - m1 = f'extraneous text {ret[self.pw_len+1]!r} found after password' - m2 = '[bare comments not allowed in BIP39 password files]' - m = m1+' '+m2 - elif len(ret) < self.pw_len+1: - m = f'invalid password length {len(ret)-1}' - else: - return (ret[0],' '.join(ret[1:self.pw_len+1]),'') - raise ValueError(m) - else: - ret = lines.pop(0).split(None,2) - return ret if len(ret) == 3 else ret + [''] - - def make_label(self): - return f'{self.al_id.sid} {self.pw_id_str} {self.pw_fmt_disp}:{self.pw_len}' - class AddrData(MMGenObject): msgs = { 'too_many_acct_addresses': f""" diff --git a/mmgen/addrfile.py b/mmgen/addrfile.py new file mode 100755 index 00000000..9059b0ec --- /dev/null +++ b/mmgen/addrfile.py @@ -0,0 +1,339 @@ +#!/usr/bin/env python3 +# +# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution +# Copyright (C)2013-2022 The MMGen Project +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +addrfile.py: Address and password file classes for the MMGen suite +""" + +from .util import ( + msg, + qmsg, + qmsg_r, + die, + capfirst, + get_lines_from_file, + write_data_to_file, + keypress_confirm, +) +from .protocol import init_proto +from .obj import * +from .seed import SeedID,is_seed_id +from .addr import KeyList,PasswordList,dmsg_sc + +class AddrFile(MMGenObject): + desc = 'addresses' + ext = 'addrs' + line_ctr = 0 + file_header = """ +# {pnm} address file +# +# This file is editable. +# Everything following a hash symbol '#' is a comment and ignored by {pnm}. +# A text label of {n} screen cells or less may be added to the right of each +# address, and it will be appended to the tracking wallet label upon import. +# The label may contain any printable ASCII symbol. +""" + + def __init__(self,parent): + + self.parent = parent + self.infile = None + + def encrypt(self,desc='new key list'): + from .crypto import mmgen_encrypt + from .globalvars import g + self.fmt_data = mmgen_encrypt(self.fmt_data.encode(),desc,'') + self.ext += f'.{g.mmenc_ext}' + + @property + def filename(self): + from .globalvars import g + return '{}{x}{}.{}'.format( + self.parent.id_str, + ('.' + self.parent.proto.network) if self.parent.proto.testnet else '', + self.ext, + x = '-α' if g.debug_utf8 else '' ) + + def write(self,fn=None,ask_tty=True,ask_write_default_yes=False,binary=False,desc=None): + from .opts import opt + write_data_to_file( + fn or self.filename, + self.fmt_data, + desc or self.desc, + ask_tty = self.parent.has_keys and not opt.quiet, + binary = binary ) + + def make_label(self): + p = self.parent + bc,mt = p.proto.base_coin,p.al_id.mmtype + l_coin = [] if bc == 'BTC' else [p.proto.coin] if bc == 'ETH' else [bc] + l_type = [] if mt == 'E' or (mt == 'L' and not p.proto.testnet) else [mt.name.upper()] + l_tn = [] if not p.proto.testnet else [p.proto.network.upper()] + lbl_p2 = ':'.join(l_coin+l_type+l_tn) + return p.al_id.sid + ('',' ')[bool(lbl_p2)] + lbl_p2 + + def format(self,add_comments=False): + p = self.parent + fh = ( + self.file_header_mn.format(p.pw_fmt.upper()) + if p.gen_passwds and p.pw_fmt in ('bip39','xmrseed') else + self.file_header ).strip() + from .globalvars import g + out = [fh.format(pnm=g.proj_name,n=TwComment.max_screen_width) + '\n'] + + if p.chksum: + out.append(f'# {capfirst(p.desc)} data checksum for {p.id_str}: {p.chksum}') + out.append('# Record this value to a secure location.\n') + + lbl = self.make_label() + dmsg_sc('lbl',lbl[9:]) + out.append(f'{lbl} {{') + + fs = ' {:<%s} {:<34}{}' % len(str(p.data[-1].idx)) + for e in p.data: + c = ' '+e.label if add_comments and e.label else '' + if type(p) == KeyList: + out.append(fs.format( e.idx, f'{p.al_id.mmtype.wif_label}: {e.sec.wif}', c )) + elif type(p) == PasswordList: + out.append(fs.format(e.idx,e.passwd,c)) + else: # First line with idx + out.append(fs.format(e.idx,e.addr,c)) + if p.has_keys: + from .opts import opt + if opt.b16: + out.append(fs.format( '', f'orig_hex: {e.sec.orig_bytes.hex()}', c )) + out.append(fs.format( '', f'{p.al_id.mmtype.wif_label}: {e.sec.wif}', c )) + for k in ('viewkey','wallet_passwd'): + v = getattr(e,k) + if v: out.append(fs.format( '', f'{k}: {v}', c )) + + out.append('}') + self.fmt_data = '\n'.join([l.rstrip() for l in out]) + '\n' + + def get_line(self,lines): + ret = lines.pop(0).split(None,2) + self.line_ctr += 1 + if ret[0] == 'orig_hex:': # hacky + ret = lines.pop(0).split(None,2) + self.line_ctr += 1 + return ret if len(ret) == 3 else ret + [''] + + def parse_file_body(self,lines): + + p = self.parent + ret = AddrListData() + le = p.entry_type + iifs = "{!r}: invalid identifier [expected '{}:']" + + while lines: + idx,addr,lbl = self.get_line(lines) + + assert is_addr_idx(idx), f'invalid address index {idx!r}' + p.check_format(addr) + + a = le(**{ 'proto': p.proto, 'idx':int(idx), p.main_attr:addr, 'label':lbl }) + + if p.has_keys: # order: wif,(orig_hex),viewkey,wallet_passwd + d = self.get_line(lines) + assert d[0] == p.al_id.mmtype.wif_label+':', iifs.format(d[0],p.al_id.mmtype.wif_label) + a.sec = PrivKey(proto=p.proto,wif=d[1]) + for k,dtype,add_proto in ( + ('viewkey',ViewKey,True), + ('wallet_passwd',WalletPassword,False) ): + if k in p.al_id.mmtype.extra_attrs: + d = self.get_line(lines) + assert d[0] == k+':', iifs.format(d[0],k) + setattr(a,k,dtype( *((p.proto,d[1]) if add_proto else (d[1],)) ) ) + + ret.append(a) + + if p.has_keys and not p.skip_ka_check: + from .opts import opt + if getattr(opt,'yes',False) or keypress_confirm('Check key-to-address validity?'): + from .addr import KeyGenerator,AddrGenerator + kg = KeyGenerator(p.proto,p.al_id.mmtype.pubkey_type) + ag = AddrGenerator(p.proto,p.al_id.mmtype) + llen = len(ret) + for n,e in enumerate(ret): + qmsg_r(f'\rVerifying keys {n+1}/{llen}') + assert e.addr == ag.to_addr(kg.to_pubhex(e.sec)),( + f'Key doesn’t match address!\n {e.sec.wif}\n {e.addr}') + qmsg(' - done') + + return ret + + def parse_file(self,fn,buf=[],exit_on_error=True): + + def parse_addrfile_label(lbl): + """ + label examples: + - Bitcoin legacy mainnet: no label + - Bitcoin legacy testnet: 'LEGACY:TESTNET' + - Bitcoin Segwit: 'SEGWIT' + - Bitcoin Segwit testnet: 'SEGWIT:TESTNET' + - Bitcoin Bech32 regtest: 'BECH32:REGTEST' + - Litecoin legacy mainnet: 'LTC' + - Litecoin Bech32 mainnet: 'LTC:BECH32' + - Litecoin legacy testnet: 'LTC:LEGACY:TESTNET' + - Ethereum mainnet: 'ETH' + - Ethereum Classic mainnet: 'ETC' + - Ethereum regtest: 'ETH:REGTEST' + """ + lbl = lbl.lower() + + # remove the network component: + if lbl.endswith(':testnet'): + network = 'testnet' + lbl = lbl[:-8] + elif lbl.endswith(':regtest'): + network = 'regtest' + lbl = lbl[:-8] + else: + network = 'mainnet' + + if lbl in p.bitcoin_addrtypes: + coin,mmtype_key = ( 'BTC', lbl ) + elif ':' in lbl: # first component is coin, second is mmtype_key + coin,mmtype_key = lbl.split(':') + else: # only component is coin + coin,mmtype_key = ( lbl, None ) + + proto = init_proto(coin=coin,network=network) + + if mmtype_key == None: + mmtype_key = proto.mmtypes[0] + + return ( proto, proto.addr_type(mmtype_key) ) + + p = self.parent + lines = get_lines_from_file(fn,p.desc+' data',trim_comments=True) + + try: + assert len(lines) >= 3, f'Too few lines in address file ({len(lines)})' + ls = lines[0].split() + assert 1 < len(ls) < 5, f'Invalid first line for {p.gen_desc} file: {lines[0]!r}' + assert ls.pop() == '{', f'{ls!r}: invalid first line' + assert lines[-1] == '}', f'{lines[-1]!r}: invalid last line' + sid = ls.pop(0) + assert is_seed_id(sid), f'{sid!r}: invalid Seed ID' + + if type(p) == PasswordList and len(ls) == 2: + ss = ls.pop().split(':') + assert len(ss) == 2, f'{ss!r}: invalid password length specifier (must contain colon)' + p.set_pw_fmt(ss[0]) + p.set_pw_len(ss[1]) + p.pw_id_str = MMGenPWIDString(ls.pop()) + proto = init_proto('btc') # FIXME: dummy protocol + mmtype = MMGenPasswordType(proto,'P') + elif len(ls) == 1: + proto,mmtype = parse_addrfile_label(ls[0]) + elif len(ls) == 0: + proto = init_proto('btc') + mmtype = proto.addr_type('L') + else: + raise ValueError(f'{lines[0]}: Invalid first line for {p.gen_desc} file {fn!r}') + + if type(p) != PasswordList: + if proto.base_coin != p.proto.base_coin or proto.network != p.proto.network: + """ + Having caller supply protocol and checking address file protocol against it here + allows us to catch all mismatches in one place. This behavior differs from that of + transaction files, which determine the protocol independently, requiring the caller + to check for protocol mismatches (e.g. MMGenTX.check_correct_chain()) + """ + raise ValueError( + f'{p.desc} file is ' + + f'{proto.base_coin} {proto.network} but protocol is ' + + f'{p.proto.base_coin} {p.proto.network}' ) + + p.base_coin = proto.base_coin + p.network = proto.network + p.al_id = AddrListID(SeedID(sid=sid),mmtype) + + data = self.parse_file_body(lines[1:-1]) + assert isinstance(data,list),'Invalid file body data' + except Exception as e: + m = 'Invalid data in {} list file {!r}{} ({!s})'.format( + p.desc, + self.infile, + (f', content line {self.line_ctr}' if self.line_ctr else ''), + e ) + if exit_on_error: + die(3,m) + else: + msg(m) + return False + + return data + +class KeyAddrFile(AddrFile): + desc = 'secret keys' + ext = 'akeys' + +class KeyFile(KeyAddrFile): + ext = 'keys' + file_header = """ +# {pnm} key file +# +# This file is editable. +# Everything following a hash symbol '#' is a comment and ignored by {pnm}. +""" + +class PasswordFile(AddrFile): + desc = 'passwords' + ext = 'pws' + file_header = """ +# {pnm} password file +# +# This file is editable. +# Everything following a hash symbol '#' is a comment and ignored by {pnm}. +# A text label of {n} screen cells or less may be added to the right of each +# password. The label may contain any printable ASCII symbol. +# +""" + file_header_mn = """ +# {{pnm}} {} password file +# +# This file is editable. +# Everything following a hash symbol '#' is a comment and ignored by {{pnm}}. +# +""" + + def get_line(self,lines): + + self.line_ctr += 1 + p = self.parent + + if p.pw_fmt in ('bip39','xmrseed'): + ret = lines.pop(0).split(None,p.pw_len+1) + if len(ret) > p.pw_len+1: + m1 = f'extraneous text {ret[p.pw_len+1]!r} found after password' + m2 = '[bare comments not allowed in BIP39 password files]' + m = m1+' '+m2 + elif len(ret) < p.pw_len+1: + m = f'invalid password length {len(ret)-1}' + else: + return (ret[0],' '.join(ret[1:p.pw_len+1]),'') + raise ValueError(m) + else: + ret = lines.pop(0).split(None,2) + return ret if len(ret) == 3 else ret + [''] + + def make_label(self): + p = self.parent + return f'{p.al_id.sid} {p.pw_id_str} {p.pw_fmt_disp}:{p.pw_len}' diff --git a/mmgen/main_addrgen.py b/mmgen/main_addrgen.py index 661199f3..94ea6652 100755 --- a/mmgen/main_addrgen.py +++ b/mmgen/main_addrgen.py @@ -24,6 +24,7 @@ mmgen-addrgen: Generate a series or range of addresses from an MMGen from .common import * from .crypto import * from .addr import AddrList,KeyAddrList,KeyList,MMGenAddrType,AddrIdxList +from .addrfile import AddrFile from .wallet import Wallet if g.prog_name == 'mmgen-keygen': @@ -161,13 +162,17 @@ al = globals()[gen_clsname]( addr_idxs = idxs, mmtype = addr_type ) -al.format() +af = al.get_file() + +af.format() if al.gen_addrs and opt.print_checksum: Die(0,al.checksum) if al.gen_keys and keypress_confirm('Encrypt key list?'): - al.encrypt() - al.write_to_file(binary=True,desc='encrypted '+al.file_desc) + af.encrypt() + af.write( + binary = True, + desc = f'encrypted {af.desc}' ) else: - al.write_to_file() + af.write() diff --git a/mmgen/main_passgen.py b/mmgen/main_passgen.py index 432a3de6..0b166fe3 100755 --- a/mmgen/main_passgen.py +++ b/mmgen/main_passgen.py @@ -169,12 +169,14 @@ al = PasswordList( pw_len = pw_len, pw_fmt = pw_fmt ) -al.format() +af = al.get_file() + +af.format() if keypress_confirm('Encrypt password list?'): - al.encrypt(desc='password list') - al.write_to_file(binary=True,desc='encrypted password list') + af.encrypt(desc='password list') + af.write(binary=True,desc='encrypted password list') else: if g.test_suite_popen_spawn and g.platform == 'win': time.sleep(0.1) - al.write_to_file(desc='password list') + af.write(desc='password list') diff --git a/mmgen/obj.py b/mmgen/obj.py index 76ba89ad..0f365da8 100755 --- a/mmgen/obj.py +++ b/mmgen/obj.py @@ -57,7 +57,7 @@ def get_obj(objname,*args,**kwargs): else: return True if return_bool else ret -def is_mmgen_idx(s): return get_obj(AddrIdx, n=s, silent=True,return_bool=True) +def is_addr_idx(s): return get_obj(AddrIdx, n=s, silent=True,return_bool=True) def is_addrlist_id(s): return get_obj(AddrListID, sid=s, silent=True,return_bool=True) def is_mmgen_id(proto,s): return get_obj(MMGenID, proto=proto, id_str=s, silent=True,return_bool=True) diff --git a/mmgen/tx.py b/mmgen/tx.py index f5728a59..ef100e39 100755 --- a/mmgen/tx.py +++ b/mmgen/tx.py @@ -724,8 +724,9 @@ class MMGenTX: async def get_outputs_from_cmdline(self,cmd_args): from .addr import AddrList,AddrData,TwAddrData + from .addrfile import AddrFile addrfiles = remove_dups( - tuple(a for a in cmd_args if get_extension(a) == AddrList.ext), + tuple(a for a in cmd_args if get_extension(a) == AddrFile.ext), desc = 'command line', edesc = 'argument', ) diff --git a/test/test_py_d/ts_regtest.py b/test/test_py_d/ts_regtest.py index b2639f10..14b049ba 100755 --- a/test/test_py_d/ts_regtest.py +++ b/test/test_py_d/ts_regtest.py @@ -291,8 +291,9 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared): a.set_comment(idx,get_label()) else: if n % 2: a.set_comment(idx,f'Test address {n}') - a.format(add_comments=True) - write_data_to_file(outfile,a.fmt_data,quiet=True,ignore_opt_outdir=True) + af = a.get_file() + af.format(add_comments=True) + write_data_to_file(outfile,af.fmt_data,quiet=True,ignore_opt_outdir=True) end_silence() def setup(self):