addr.py: move address file-related methods to new AddrFile class
- AddrFile subclasses: KeyAddrFile,KeyFile,PasswordFile
This commit is contained in:
parent
74ec5efd63
commit
012fea543f
7 changed files with 371 additions and 286 deletions
285
mmgen/addr.py
285
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"""
|
||||
|
|
|
|||
339
mmgen/addrfile.py
Executable file
339
mmgen/addrfile.py
Executable file
|
|
@ -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 <mmgen@tuta.io>
|
||||
#
|
||||
# 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
"""
|
||||
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}'
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue