From 47fa8961350cd9bb76d19c5587e52cefbdc5d0d1 Mon Sep 17 00:00:00 2001 From: MMGen Date: Wed, 12 Jun 2019 14:21:28 +0000 Subject: [PATCH] minor fixes and cleanups --- README.md | 10 ++-- mmgen/main_wallet.py | 10 ++-- mmgen/obj.py | 4 +- mmgen/seed.py | 73 ++++++++++++++++------------- mmgen/tool.py | 9 ++-- test/common.py | 5 +- test/objtest_py_d/ot_btc_mainnet.py | 9 +++- test/unit_tests_d/ut_seedsplit.py | 6 +-- 8 files changed, 72 insertions(+), 54 deletions(-) diff --git a/README.md b/README.md index 5203b2f7..da4b4bba 100644 --- a/README.md +++ b/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 diff --git a/mmgen/main_wallet.py b/mmgen/main_wallet.py index 0369bea7..c23a2348 100755 --- a/mmgen/main_wallet.py +++ b/mmgen/main_wallet.py @@ -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] ' do_sw_note = True diff --git a/mmgen/obj.py b/mmgen/obj.py index 1d20935b..47079b16 100755 --- a/mmgen/obj.py +++ b/mmgen/obj.py @@ -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 diff --git a/mmgen/seed.py b/mmgen/seed.py index e9c63fa2..5cf59b2d 100755 --- a/mmgen/seed.py +++ b/mmgen/seed.py @@ -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): diff --git a/mmgen/tool.py b/mmgen/tool.py index 296ed1b9..76de9cf5 100755 --- a/mmgen/tool.py +++ b/mmgen/tool.py @@ -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" diff --git a/test/common.py b/test/common.py index 6b688249..4c99a401 100755 --- a/test/common.py +++ b/test/common.py @@ -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 diff --git a/test/objtest_py_d/ot_btc_mainnet.py b/test/objtest_py_d/ot_btc_mainnet.py index ceeff1a3..2b1826a4 100755 --- a/test/objtest_py_d/ot_btc_mainnet.py +++ b/test/objtest_py_d/ot_btc_mainnet.py @@ -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' :/\\') diff --git a/test/unit_tests_d/ut_seedsplit.py b/test/unit_tests_d/ut_seedsplit.py index 6b8131d0..9dfaf2ee 100755 --- a/test/unit_tests_d/ut_seedsplit.py +++ b/test/unit_tests_d/ut_seedsplit.py @@ -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')