minor fixes and cleanups

This commit is contained in:
The MMGen Project 2019-06-12 14:21:28 +00:00
commit 47fa896135
Signed by: mmgen
GPG key ID: 3F8B1861E32B7DA2
8 changed files with 72 additions and 54 deletions

View file

@ -111,11 +111,11 @@ the more prosaic 2048-word [BIP39 wordlist][bw] used in most wallets today.
brainwallet, incognito wallet) and three unencrypted (mnemonic, mmseed,
hexseed).
- **[Subwallets][U]:** Subwallets have many applications, the most notable being
online hot wallets and travel wallets. MMGen subwallets are functionally and
externally identical to ordinary wallets, which provides a key security
benefit: only the user who generated the subwallet knows that it is indeed a
subwallet. Subwallets don’t need to be backed up, as they can always be
regenerated from their parent.
online hot wallets, decoy wallets and travel wallets. MMGen subwallets are
functionally and externally identical to ordinary wallets, which provides a
key security benefit: only the user who generated the subwallet knows that it
is indeed a subwallet. Subwallets don’t need to be backed up, as they can
always be regenerated from their parent.
- **[Transaction autosigning][X]:** This feature puts your offline signing
machine into “hands-off” mode, allowing you to transact directly from cold
storage securely and conveniently. Additional LED signaling support is

View file

@ -41,6 +41,8 @@ invoked_as = {
'mmgen-subwalletgen': 'subgen',
}[g.prog_name]
dsw = 'the default or specified {pnm} wallet'
# full: defhHiJkKlLmoOpPqrSvz-
if invoked_as == 'gen':
desc = 'Generate an {pnm} wallet from a random seed'
@ -49,19 +51,19 @@ if invoked_as == 'gen':
oaction = 'output'
nargs = 0
elif invoked_as == 'conv':
desc = 'Convert an {pnm} wallet from one format to another'
desc = 'Convert ' + dsw + ' from one format to another'
opt_filter = 'dehHiJkKlLmoOpPqrSvz-'
elif invoked_as == 'chk':
desc = 'Check validity of an {pnm} wallet'
desc = 'Check validity of ' + dsw
opt_filter = 'ehiHOlpPqrvz-'
iaction = 'input'
elif invoked_as == 'passchg':
desc = 'Change the passphrase, hash preset or label of an {pnm} wallet'
desc = 'Change the passphrase, hash preset or label of ' + dsw
opt_filter = 'efhdiHkKOlLmpPqrSvz-'
iaction = 'input'
do_bw_note = False
elif invoked_as == 'subgen':
desc = 'Generate a subwallet from an {pnm} wallet'
desc = 'Generate a subwallet from ' + dsw
opt_filter = 'dehHiJkKlLmoOpPqrSvz-' # omitted: f
usage = '[opts] [infile] <Subseed Index>'
do_sw_note = True

View file

@ -681,7 +681,7 @@ class TwMMGenID(str,Hilite,InitErrors,MMGenObject):
me.type = idtype
return me
# contains TwMMGenID,TwComment. Not for display
# non-displaying container for TwMMGenID,TwComment
class TwLabel(str,InitErrors,MMGenObject):
def __new__(cls,s,on_fail='die'):
if type(s) == cls: return s
@ -829,8 +829,6 @@ class MMGenLabel(str,Hilite,InitErrors):
for ch in k: assert type(ch) == str and len(ch) == 1
try:
s = s.strip()
if type(s) != str:
s = s.decode('utf8')
for ch in s:
# Allow: (L)etter,(N)umber,(P)unctuation,(S)ymbol,(Z)space
# Disallow: (C)ontrol,(M)combining

View file

@ -48,9 +48,7 @@ def is_mnemonic(s):
class SeedBase(MMGenObject):
data = MMGenImmutableAttr('data',bytes,typeconv=False)
hexdata = MMGenImmutableAttr('hexdata',str,typeconv=False)
sid = MMGenImmutableAttr('sid',SeedID,typeconv=False)
length = MMGenImmutableAttr('length',int,typeconv=False)
def __init__(self,seed_bin=None):
if not seed_bin:
@ -60,9 +58,15 @@ class SeedBase(MMGenObject):
die(3,'{}: invalid seed length'.format(len(seed_bin)))
self.data = seed_bin
self.hexdata = seed_bin.hex()
self.sid = SeedID(seed=self)
self.length = len(seed_bin) * 8
@property
def length(self):
return len(self.data) * 8
@property
def hexdata(self):
return self.data.hex()
class SubSeedList(MMGenObject):
have_short = True
@ -206,8 +210,8 @@ class Seed(SeedBase):
def subseed_by_seed_id(self,sid,last_idx=None,print_msg=False):
return self.subseeds.get_subseed_by_seed_id(sid,last_idx=last_idx,print_msg=print_msg)
def split(self,count,id_str=None,master_idx=None):
return SeedShareList(self,count,id_str,master_idx)
def split(self,count,id_str=None,use_master=False,master_idx=1):
return SeedShareList(self,count,id_str,master_idx if use_master else None)
@staticmethod
def join_shares(seed_list,use_master=False,master_idx=1,id_str=None):
@ -250,6 +254,7 @@ class SubSeed(SeedBase):
self.idx = idx
self.nonce = nonce
self.ss_idx = str(idx) + { 'long': 'L', 'short': 'S' }[length]
self.parent_list = parent_list
SeedBase.__init__(self,seed_bin=type(self).make_subseed_bin(parent_list,idx,nonce,length))
@staticmethod
@ -301,22 +306,22 @@ class SeedShareList(SubSeedList):
B = self.join().data
assert A == B,'Data mismatch!\noriginal seed: {!r}\nrejoined seed: {!r}'.format(A,B)
def get_share_by_idx(self,idx):
def get_share_by_idx(self,idx,base_seed=False):
if idx == self.count:
return self.last_share
elif self.master_share and idx == 1:
return self.master_share.derived_seed
return self.master_share if base_seed else self.master_share.derived_seed
else:
ss_idx = SubSeedIdx(str(idx) + 'L')
return self.get_subseed_by_ss_idx(ss_idx)
def get_share_by_seed_id(self,sid,last_idx=None):
def get_share_by_seed_id(self,sid,base_seed=False):
if sid == self.data['long'].key(self.count-1):
return self.last_share
elif self.master_share and sid == self.data['long'].key(0):
return self.master_share.derived_seed
return self.master_share if base_seed else self.master_share.derived_seed
else:
return self.get_subseed_by_seed_id(sid,last_idx=last_idx)
return self.get_subseed_by_seed_id(sid)
def join(self):
return Seed.join_shares(self.get_share_by_idx(i+1) for i in range(len(self)))
@ -359,13 +364,14 @@ class SeedShare(SubSeed):
byte_len = seed.length // 8
return scramble_seed(seed.data,scramble_key)[:byte_len]
class SeedShareLast(SubSeed):
class SeedShareLast(SeedBase):
idx = MMGenImmutableAttr('idx',SeedShareIdx)
nonce = 0
def __init__(self,parent_list):
self.idx = parent_list.count
self.parent_list = parent_list
SeedBase.__init__(self,seed_bin=self.make_subseed_bin(parent_list))
@staticmethod
@ -379,27 +385,24 @@ class SeedShareLast(SubSeed):
return ret.to_bytes(seed.length // 8,'big')
class SeedShareMaster(SubSeed):
class SeedShareMaster(SeedBase):
idx = MMGenImmutableAttr('idx',MasterShareIdx)
nonce = 0
def __init__(self,parent_list,idx):
self.idx = idx
self.parent_seed = parent_list.parent_seed
self.parent_list = parent_list
SeedBase.__init__(self,self.make_base_seed_bin())
self.derived_seed = SeedBase(self.make_derived_seed_bin(parent_list.id_str,parent_list.count))
@property
def fn_stem(self):
return '{}-master_share{}[{}]'.format(self.parent_seed.sid,self.idx,self.sid)
def make_base_seed_bin(self):
seed = self.parent_list.parent_seed
# field maximums: idx: 65535 (1024)
scramble_key = b'master:' + self.idx.to_bytes(2,'big',signed=False)
byte_len = self.parent_seed.length // 8
return scramble_seed(self.parent_seed.data,scramble_key)[:byte_len]
byte_len = seed.length // 8
return scramble_seed(seed.data,scramble_key)[:byte_len]
def make_derived_seed_bin(self,id_str,count):
# field maximums: id_str: none (256 chars), count: 65535 (1024)
@ -427,12 +430,12 @@ class SeedSource(MMGenObject):
ask_tty = True
no_tty = False
op = None
require_utf8_input = False
_msg = {}
class SeedSourceData(MMGenObject): pass
def __new__(cls,fn=None,ss=None,seed=None,ignore_in_fmt=False,passchg=False,in_data=None,in_fmt=None):
def __new__(cls,fn=None,ss=None,seed_bin=None,seed=None,
passchg=False,in_data=None,ignore_in_fmt=False,in_fmt=None):
in_fmt = in_fmt or opt.in_fmt
@ -466,16 +469,17 @@ class SeedSource(MMGenObject):
sstype = cls.fmt_code_to_type(in_fmt)
me = super(cls,cls).__new__(sstype)
me.op = ('old','pwchg_old')[bool(passchg)]
else: # Called with no inputs - initialize with random seed
else: # Called with no args, 'seed' or 'seed_bin' - initialize with random or supplied seed
sstype = cls.fmt_code_to_type(opt.out_fmt)
me = super(cls,cls).__new__(sstype or Wallet) # default: Wallet
me.seed = Seed(seed_bin=seed or None)
me.seed = seed or Seed(seed_bin=seed_bin or None)
me.op = 'new'
# die(1,me.seed.sid.hl()) # DEBUG
return me
def __init__(self,fn=None,ss=None,seed=None,ignore_in_fmt=False,passchg=False,in_data=None,in_fmt=None):
def __init__(self,fn=None,ss=None,seed_bin=None,seed=None,
passchg=False,in_data=None,ignore_in_fmt=False,in_fmt=None):
self.ssdata = self.SeedSourceData()
self.msg = {}
@ -601,7 +605,12 @@ class SeedSourceUnenc(SeedSource):
def _encrypt(self): pass
def _filename(self):
return '{}[{}]{x}.{}'.format(self.seed.sid,self.seed.length,self.ext,x='' if g.debug_utf8 else '')
s = self.seed
return '{}[{}]{x}.{}'.format(
s.sid,
s.length,
self.ext,
x='' if g.debug_utf8 else '')
class SeedSourceEnc(SeedSource):
@ -958,7 +967,6 @@ class Wallet (SeedSourceEnc):
fmt_codes = 'wallet','w'
desc = g.proj_name + ' wallet'
ext = 'mmdat'
require_utf8_input = True # label is UTF-8
def _get_label_from_user(self,old_lbl=''):
d = "to reuse the label '{}'".format(old_lbl.hl()) if old_lbl else 'for no label'
@ -1105,11 +1113,13 @@ class Wallet (SeedSourceEnc):
return False
def _filename(self):
s = self.seed
d = self.ssdata
return '{}-{}[{},{}]{x}.{}'.format(
self.seed.sid,
self.ssdata.key_id,
self.seed.length,
self.ssdata.hash_preset,
s.sid,
d.key_id,
s.length,
d.hash_preset,
self.ext,
x='' if g.debug_utf8 else '')
@ -1119,7 +1129,6 @@ class Brainwallet (SeedSourceEnc):
fmt_codes = 'mmbrain','brainwallet','brain','bw','b'
desc = 'brainwallet'
ext = 'mmbrain'
require_utf8_input = True # brainwallet is user input, so require UTF-8
# brainwallet warning message? TODO
def get_bw_params(self):

View file

@ -481,7 +481,7 @@ class MMGenToolCmdMnemonic(MMGenToolCmdBase):
"convert a 16, 24 or 32-byte hexadecimal number to a mnemonic"
opt.out_fmt = 'words'
from mmgen.seed import SeedSource
s = SeedSource(seed=bytes.fromhex(hexstr))
s = SeedSource(seed_bin=bytes.fromhex(hexstr))
s._format()
return ' '.join(s.ssdata.mnemonic)
@ -497,10 +497,13 @@ class MMGenToolCmdMnemonic(MMGenToolCmdBase):
baseconv.check_wordlist(wordlist)
return True
def mn_printlist(self,wordlist=dfl_wl_id):
def mn_printlist(self,wordlist=dfl_wl_id,enum=False,pager=False):
"print mnemonic wordlist"
wordlist in baseconv.digits or die(1,"'{}': not a valid wordlist".format(wordlist))
return '\n'.join(baseconv.digits[wordlist])
ret = baseconv.digits[wordlist]
if enum:
ret = ['{:>4} {}'.format(n,e) for n,e in enumerate(ret)]
return '\n'.join(ret)
class MMGenToolCmdFile(MMGenToolCmdBase):
"utilities for viewing/checking MMGen address and transaction files"

View file

@ -28,13 +28,14 @@ from mmgen.common import *
ascii_uc = ''.join(map(chr,list(range(65,91)))) # 26 chars
ascii_lc = ''.join(map(chr,list(range(97,123)))) # 26 chars
lat_accent = ''.join(map(chr,list(range(192,383)))) # 191 chars
lat_accent = ''.join(map(chr,list(range(192,383)))) # 191 chars, L,S
ru_uc = ''.join(map(chr,list(range(1040,1072)))) # 32 chars
gr_uc = ''.join(map(chr,list(range(913,930)) + list(range(931,940)))) # 26 chars (930 is ctrl char)
gr_uc_w_ctrl = ''.join(map(chr,list(range(913,940)))) # 27 chars, L,C
lat_cyr_gr = lat_accent[:130:5] + ru_uc + gr_uc # 84 chars
ascii_cyr_gr = ascii_uc + ru_uc + gr_uc # 84 chars
utf8_text = '[α-$ample UTF-8 text-ω]' * 10 # 230 chars, unicode types L,N,P,S,Z
utf8_text = '[α-$ample UTF-8 text-ω]' * 10 # 230 chars, L,N,P,S,Z
utf8_combining = '[α-$ámple UTF-8 téxt-ω]' * 10 # L,N,P,S,Z,M
utf8_ctrl = '[α-$ample\nUTF-8\ntext-ω]' * 10 # L,N,P,S,Z,C

View file

@ -26,6 +26,10 @@ tests = OrderedDict([
'bad': ('s',2.1,1025,-1,0,1),
'good': (('7',7),(2,2),(1024,1024))
}),
('MasterShareIdx', {
'bad': ('s',1.1,1025,-1,0),
'good': (('7',7),(1,1),(1024,1024))
}),
('AddrIdxList', {
'bad': ('x','5,9,1-2-3','8,-11','66,3-2'),
'good': (
@ -152,7 +156,7 @@ tests = OrderedDict([
)
}),
('MMGenWalletLabel', {
'bad': (utf8_text[:49],utf8_combining[:48],utf8_ctrl[:48]),
'bad': (utf8_text[:49],utf8_combining[:48],utf8_ctrl[:48],gr_uc_w_ctrl),
'good': (utf8_text[:48],)
}),
('TwComment', {
@ -160,6 +164,7 @@ tests = OrderedDict([
utf8_ctrl[:40],
text_jp[:41],
text_zh[:41],
gr_uc_w_ctrl,
utf8_text[:81] ),
'good': ( utf8_text[:80],
(ru_uc + gr_uc + utf8_text)[:80],
@ -167,7 +172,7 @@ tests = OrderedDict([
text_zh[:40] )
}),
('MMGenTXLabel',{
'bad': (utf8_text[:73],utf8_combining[:72],utf8_ctrl[:72]),
'bad': (utf8_text[:73],utf8_combining[:72],utf8_ctrl[:72],gr_uc_w_ctrl),
'good': (utf8_text[:72],)
}),
('MMGenPWIDString', { # forbidden = list(u' :/\\')

View file

@ -59,7 +59,7 @@ class unit_test(object):
for share_count,j,k,l in ((2,c,c,d),(5,e,f,h)):
shares = seed.split(share_count,id_str,master_idx)
shares = seed.split(share_count,id_str,bool(master_idx),master_idx)
A = len(shares)
assert A == share_count, A
@ -83,8 +83,8 @@ class unit_test(object):
assert A == b, A
if master_idx:
slist = [shares.get_share_by_idx(i+1) for i in range(1,len(shares))]
A = Seed.join_shares([shares.master_share]+slist,True,master_idx,id_str).sid
slist = [shares.get_share_by_idx(i+1,base_seed=True) for i in range(len(shares))]
A = Seed.join_shares(slist,True,master_idx,id_str).sid
assert A == b, A
msg('OK')