Browse Source

minor fixes and cleanups

MMGen 5 years ago
parent
commit
47fa896135
8 changed files with 72 additions and 54 deletions
  1. 5 5
      README.md
  2. 6 4
      mmgen/main_wallet.py
  3. 1 3
      mmgen/obj.py
  4. 41 32
      mmgen/seed.py
  5. 6 3
      mmgen/tool.py
  6. 3 2
      test/common.py
  7. 7 2
      test/objtest_py_d/ot_btc_mainnet.py
  8. 3 3
      test/unit_tests_d/ut_seedsplit.py

+ 5 - 5
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

+ 6 - 4
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] <Subseed Index>'
 	do_sw_note = True

+ 1 - 3
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

+ 41 - 32
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):

+ 6 - 3
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"

+ 3 - 2
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
 

+ 7 - 2
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' :/\\')

+ 3 - 3
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')