minor fixes and cleanups
This commit is contained in:
parent
448e2c24bf
commit
47fa896135
8 changed files with 72 additions and 54 deletions
10
README.md
10
README.md
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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' :/\\')
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue