Browse Source

seed.py: move seedsplit related classes to seedsplit.py

The MMGen Project 3 years ago
parent
commit
ff450b0ec5

+ 1 - 1
mmgen/help.py

@@ -134,7 +134,7 @@ default wallet.
 	pnl = g.proj_name.lower() )
 
 		def seedsplit():
-			from .obj import SeedShareIdx,SeedShareCount,MasterShareIdx
+			from .seedsplit import SeedShareIdx,SeedShareCount,MasterShareIdx
 			return """
 COMMAND NOTES:
 

+ 3 - 2
mmgen/main_seedjoin.py

@@ -22,8 +22,9 @@ mmgen/main_seedjoin: Regenerate an MMGen deterministic wallet from seed shares
 """
 
 from .common import *
-from .obj import MasterShareIdx,SeedSplitIDString,MMGenWalletLabel
-from .seed import Seed,SeedShareMasterJoining
+from .obj import MMGenWalletLabel
+from .seed import Seed
+from .seedsplit import SeedSplitIDString,MasterShareIdx,SeedShareMasterJoining
 from .wallet import Wallet
 
 opts_data = {

+ 4 - 2
mmgen/main_wallet.py

@@ -22,9 +22,10 @@ mmgen/main_wallet:  Entry point for MMGen wallet-related scripts
 
 import os
 from .common import *
+from .obj import MMGenWalletLabel
+from .seedsplit import MasterShareIdx
 from .wallet import Wallet,MMGenWallet
 from .filename import find_file_in_dir
-from .obj import MMGenWalletLabel,MasterShareIdx
 
 usage = '[opts] [infile]'
 nargs = 1
@@ -148,7 +149,8 @@ if invoked_as == 'subgen':
 	from .obj import SubSeedIdx
 	ss_idx = SubSeedIdx(cmd_args.pop())
 elif invoked_as == 'seedsplit':
-	from .obj import get_obj,SeedSplitSpecifier
+	from .obj import get_obj
+	from .seedsplit import SeedSplitSpecifier
 	master_share = MasterShareIdx(opt.master_share) if opt.master_share else None
 	if cmd_args:
 		sss = get_obj(SeedSplitSpecifier,s=cmd_args.pop(),silent=True)

+ 0 - 25
mmgen/obj.py

@@ -65,7 +65,6 @@ def get_obj(objname,*args,**kwargs):
 def is_mmgen_seed_id(s):   return get_obj(SeedID,     sid=s, silent=True,return_bool=True)
 def is_mmgen_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_seed_split_specifier(s): return get_obj(SeedSplitSpecifier, s=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)
 def is_coin_addr(proto,s): return get_obj(CoinAddr, proto=proto, addr=s,   silent=True,return_bool=True)
@@ -367,9 +366,6 @@ class MMGenListItem(MMGenObject):
 		return dict((k,v) for k,v in self.__dict__.items() if k in self.valid_attrs)
 
 class MMGenIdx(Int): min_val = 1
-class SeedShareIdx(MMGenIdx): max_val = 1024
-class SeedShareCount(SeedShareIdx): min_val = 2
-class MasterShareIdx(MMGenIdx): max_val = 1024
 class AddrIdx(MMGenIdx): max_digits = 7
 
 class AddrIdxList(list,InitErrors,MMGenObject):
@@ -943,27 +939,6 @@ class MMGenPWIDString(MMGenLabel):
 	forbidden = list(' :/\\')
 	trunc_ok = False
 
-class SeedSplitSpecifier(str,Hilite,InitErrors,MMGenObject):
-	color = 'red'
-	def __new__(cls,s):
-		if type(s) == cls:
-			return s
-		try:
-			arr = s.split(':')
-			assert len(arr) in (2,3), 'cannot be parsed'
-			a,b,c = arr if len(arr) == 3 else ['default'] + arr
-			me = str.__new__(cls,s)
-			me.id = SeedSplitIDString(a)
-			me.idx = SeedShareIdx(b)
-			me.count = SeedShareCount(c)
-			assert me.idx <= me.count, 'share index greater than share count'
-			return me
-		except Exception as e:
-			return cls.init_fail(e,s)
-
-class SeedSplitIDString(MMGenPWIDString):
-	desc = 'seed split ID string'
-
 class IPPort(str,Hilite,InitErrors,MMGenObject):
 	color = 'yellow'
 	width = 0

+ 6 - 250
mmgen/seed.py

@@ -208,36 +208,14 @@ 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,*args,**kwargs):
+		from .seedsplit import SeedShareList
+		return SeedShareList(self,*args,**kwargs)
 
 	@staticmethod
-	def join_shares(seed_list,master_idx=None,id_str=None):
-		if not hasattr(seed_list,'__next__'): # seed_list can be iterator or iterable
-			seed_list = iter(seed_list)
-
-		class d(object):
-			byte_len,ret,count = None,0,0
-
-		def add_share(ss):
-			if d.byte_len:
-				assert ss.byte_len == d.byte_len, f'Seed length mismatch! {ss.byte_len} != {d.byte_len}'
-			else:
-				d.byte_len = ss.byte_len
-			d.ret ^= int(ss.data.hex(),16)
-			d.count += 1
-
-		if master_idx:
-			master_share = next(seed_list)
-
-		for ss in seed_list:
-			add_share(ss)
-
-		if master_idx:
-			add_share(SeedShareMasterJoining(master_idx,master_share,id_str,d.count+1).derived_seed)
-
-		SeedShareCount(d.count)
-		return Seed(seed_bin=d.ret.to_bytes(d.byte_len,'big'))
+	def join_shares(*args,**kwargs):
+		from .seedsplit import join_shares
+		return join_shares(*args,**kwargs)
 
 class SubSeed(SeedBase):
 
@@ -260,225 +238,3 @@ class SubSeed(SeedBase):
 		# field maximums: idx: 4294967295 (1000000), nonce: 65535 (1000), short: 255 (1)
 		scramble_key  = idx.to_bytes(4,'big') + nonce.to_bytes(2,'big') + short.to_bytes(1,'big')
 		return scramble_seed(seed.data,scramble_key)[:16 if short else seed.byte_len]
-
-class SeedShareList(SubSeedList):
-	have_short = False
-	split_type = 'N-of-N'
-
-	count  = ImmutableAttr(SeedShareCount)
-	id_str = ImmutableAttr(SeedSplitIDString)
-
-	def __init__(self,parent_seed,count,id_str=None,master_idx=None,debug_last_share=False):
-		self.member_type = SeedShare
-		self.parent_seed = parent_seed
-		self.id_str = id_str or 'default'
-		self.count = count
-
-		def make_master_share():
-			for nonce in range(SeedShare.max_nonce+1):
-				ms = SeedShareMaster(self,master_idx,nonce)
-				if ms.sid == parent_seed.sid:
-					if g.debug_subseed:
-						msg(f'master_share seed ID collision with parent seed, incrementing nonce to {nonce+1}')
-				else:
-					return ms
-			raise SubSeedNonceRangeExceeded('nonce range exceeded')
-
-		def last_share_debug(last_share):
-			if not debug_last_share:
-				return False
-			sid_len = self.debug_last_share_sid_len
-			lsid = last_share.sid[:sid_len]
-			psid = parent_seed.sid[:sid_len]
-			ssids = [d[:sid_len] for d in self.data['long'].keys]
-			return (lsid in ssids or lsid == psid)
-
-		self.master_share = make_master_share() if master_idx else None
-
-		for nonce in range(SeedShare.max_nonce+1):
-			self.nonce_start = nonce
-			self.data = { 'long': IndexedDict(), 'short': IndexedDict() } # 'short' is required as a placeholder
-			if self.master_share:
-				self.data['long'][self.master_share.sid] = (1,self.master_share.nonce)
-			self._generate(count-1)
-			self.last_share = ls = SeedShareLast(self)
-			if last_share_debug(ls) or ls.sid in self.data['long'] or ls.sid == parent_seed.sid:
-				# collision: throw out entire split list and redo with new start nonce
-				if g.debug_subseed:
-					self._collision_debug_msg(ls.sid,count,nonce,'nonce_start',debug_last_share)
-			else:
-				self.data['long'][ls.sid] = (count,nonce)
-				break
-		else:
-			raise SubSeedNonceRangeExceeded('nonce range exceeded')
-
-		if g.debug_subseed:
-			A = parent_seed.data
-			B = self.join().data
-			assert A == B, f'Data mismatch!\noriginal seed: {A!r}\nrejoined seed: {B!r}'
-
-	def get_share_by_idx(self,idx,base_seed=False):
-		if idx < 1 or idx > self.count:
-			raise RangeError(f'{idx}: share index out of range')
-		elif idx == self.count:
-			return self.last_share
-		elif self.master_share and idx == 1:
-			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,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 if base_seed else self.master_share.derived_seed
-		else:
-			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)))
-
-	def format(self):
-		assert self.split_type == 'N-of-N'
-		fs1 = '    {}\n'
-		fs2 = '{i:>5}: {}\n'
-		mfs1,mfs2,midx,msid = ('','','','')
-		if self.master_share:
-			mfs1,mfs2 = (' with master share #{} ({})',' (master share #{})')
-			midx,msid = (self.master_share.idx,self.master_share.sid)
-
-		hdr  = '    {} {} ({} bits)\n'.format('Seed:',self.parent_seed.sid.hl(),self.parent_seed.bitlen)
-		hdr += '    {} {c}-of-{c} (XOR){m}\n'.format('Split Type:',c=self.count,m=mfs1.format(midx,msid))
-		hdr += '    {} {}\n\n'.format('ID String:',self.id_str.hl())
-		hdr += fs1.format('Shares')
-		hdr += fs1.format('------')
-
-		sl = self.data['long'].keys
-		body1 = fs2.format(sl[0]+mfs2.format(midx),i=1)
-		body = (fs2.format(sl[n],i=n+1) for n in range(1,len(self)))
-
-		return hdr + body1 + ''.join(body)
-
-class SeedShareBase(MMGenObject):
-
-	@property
-	def fn_stem(self):
-		pl = self.parent_list
-		msdata = f'_with_master{pl.master_share.idx}' if pl.master_share else ''
-		return '{}-{}-{}of{}{}[{}]'.format(
-			pl.parent_seed.sid,
-			pl.id_str,
-			self.idx,
-			pl.count,
-			msdata,
-			self.sid)
-
-	@property
-	def desc(self):
-		return self.get_desc()
-
-	def get_desc(self,ui=False):
-		pl = self.parent_list
-		mss = f', with master share #{pl.master_share.idx}' if pl.master_share else ''
-		if ui:
-			m   = ( yellow("(share {} of {} of ")
-					+ pl.parent_seed.sid.hl()
-					+ yellow(', split id ')
-					+ pl.id_str.hl(encl="''")
-					+ yellow('{})') )
-		else:
-			m = "share {} of {} of " + pl.parent_seed.sid + ", split id '" + pl.id_str + "'{}"
-		return m.format(self.idx,pl.count,mss)
-
-class SeedShare(SeedShareBase,SubSeed):
-
-	@staticmethod
-	def make_subseed_bin(parent_list,idx:int,nonce:int,length:str):
-		seed = parent_list.parent_seed
-		assert parent_list.have_short == False
-		assert length == 'long'
-		# field maximums: id_str: none (256 chars), count: 65535 (1024), idx: 65535 (1024), nonce: 65535 (1000)
-		scramble_key = (
-			f'{parent_list.split_type}:{parent_list.id_str}:'.encode() +
-			parent_list.count.to_bytes(2,'big') +
-			idx.to_bytes(2,'big') +
-			nonce.to_bytes(2,'big')
-		)
-		if parent_list.master_share:
-			scramble_key += (
-				b':master:' +
-				parent_list.master_share.idx.to_bytes(2,'big')
-			)
-		return scramble_seed(seed.data,scramble_key)[:seed.byte_len]
-
-class SeedShareLast(SeedShareBase,SeedBase):
-
-	idx = ImmutableAttr(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
-	def make_subseed_bin(parent_list):
-		seed_list = (parent_list.get_share_by_idx(i+1) for i in range(len(parent_list)))
-		seed = parent_list.parent_seed
-
-		ret = int(seed.data.hex(),16)
-		for ss in seed_list:
-			ret ^= int(ss.data.hex(),16)
-
-		return ret.to_bytes(seed.byte_len,'big')
-
-class SeedShareMaster(SeedBase,SeedShareBase):
-
-	idx   = ImmutableAttr(MasterShareIdx)
-	nonce = ImmutableAttr(int,typeconv=False)
-
-	def __init__(self,parent_list,idx,nonce):
-		self.idx = idx
-		self.nonce = nonce
-		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{}[{}]'.format(
-			self.parent_list.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_share:' + self.idx.to_bytes(2,'big') + self.nonce.to_bytes(2,'big')
-		return scramble_seed(seed.data,scramble_key)[:seed.byte_len]
-
-	# Don't bother with avoiding seed ID collision here, as sid of derived seed is not used
-	# by user as an identifier
-	def make_derived_seed_bin(self,id_str,count):
-		# field maximums: id_str: none (256 chars), count: 65535 (1024)
-		scramble_key = id_str.encode() + b':' + count.to_bytes(2,'big')
-		return scramble_seed(self.data,scramble_key)[:self.byte_len]
-
-	def get_desc(self,ui=False):
-		psid = self.parent_list.parent_seed.sid
-		mss = f'master share #{self.idx} of '
-		return yellow('(' + mss) + psid.hl() + yellow(')') if ui else mss + psid
-
-class SeedShareMasterJoining(SeedShareMaster):
-
-	id_str = ImmutableAttr(SeedSplitIDString)
-	count  = ImmutableAttr(SeedShareCount)
-
-	def __init__(self,idx,base_seed,id_str,count):
-		SeedBase.__init__(self,seed_bin=base_seed.data)
-
-		self.id_str = id_str or 'default'
-		self.count = count
-		self.derived_seed = SeedBase(self.make_derived_seed_bin(self.id_str,self.count))

+ 307 - 0
mmgen/seedsplit.py

@@ -0,0 +1,307 @@
+#!/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/>.
+
+"""
+seedsplit.py: Seed split classes and methods for the MMGen suite
+"""
+
+from .exception import RangeError
+from .obj import MMGenPWIDString,MMGenIdx
+from .seed import *
+
+class SeedShareIdx(MMGenIdx):
+	max_val = 1024
+
+class SeedShareCount(SeedShareIdx):
+	min_val = 2
+
+class MasterShareIdx(MMGenIdx):
+	max_val = 1024
+
+class SeedSplitSpecifier(str,Hilite,InitErrors,MMGenObject):
+	color = 'red'
+	def __new__(cls,s):
+		if type(s) == cls:
+			return s
+		try:
+			arr = s.split(':')
+			assert len(arr) in (2,3), 'cannot be parsed'
+			a,b,c = arr if len(arr) == 3 else ['default'] + arr
+			me = str.__new__(cls,s)
+			me.id = SeedSplitIDString(a)
+			me.idx = SeedShareIdx(b)
+			me.count = SeedShareCount(c)
+			assert me.idx <= me.count, 'share index greater than share count'
+			return me
+		except Exception as e:
+			return cls.init_fail(e,s)
+
+def is_seed_split_specifier(s):
+	return get_obj( SeedSplitSpecifier, s=s, silent=True, return_bool=True )
+
+class SeedSplitIDString(MMGenPWIDString):
+	desc = 'seed split ID string'
+
+class SeedShareList(SubSeedList):
+	have_short = False
+	split_type = 'N-of-N'
+
+	count  = ImmutableAttr(SeedShareCount)
+	id_str = ImmutableAttr(SeedSplitIDString)
+
+	def __init__(self,parent_seed,count,id_str=None,master_idx=None,debug_last_share=False):
+		self.member_type = SeedShare
+		self.parent_seed = parent_seed
+		self.id_str = id_str or 'default'
+		self.count = count
+
+		def make_master_share():
+			for nonce in range(SeedShare.max_nonce+1):
+				ms = SeedShareMaster(self,master_idx,nonce)
+				if ms.sid == parent_seed.sid:
+					if g.debug_subseed:
+						msg(f'master_share seed ID collision with parent seed, incrementing nonce to {nonce+1}')
+				else:
+					return ms
+			raise SubSeedNonceRangeExceeded('nonce range exceeded')
+
+		def last_share_debug(last_share):
+			if not debug_last_share:
+				return False
+			sid_len = self.debug_last_share_sid_len
+			lsid = last_share.sid[:sid_len]
+			psid = parent_seed.sid[:sid_len]
+			ssids = [d[:sid_len] for d in self.data['long'].keys]
+			return (lsid in ssids or lsid == psid)
+
+		self.master_share = make_master_share() if master_idx else None
+
+		for nonce in range(SeedShare.max_nonce+1):
+			self.nonce_start = nonce
+			self.data = { 'long': IndexedDict(), 'short': IndexedDict() } # 'short' is required as a placeholder
+			if self.master_share:
+				self.data['long'][self.master_share.sid] = (1,self.master_share.nonce)
+			self._generate(count-1)
+			self.last_share = ls = SeedShareLast(self)
+			if last_share_debug(ls) or ls.sid in self.data['long'] or ls.sid == parent_seed.sid:
+				# collision: throw out entire split list and redo with new start nonce
+				if g.debug_subseed:
+					self._collision_debug_msg(ls.sid,count,nonce,'nonce_start',debug_last_share)
+			else:
+				self.data['long'][ls.sid] = (count,nonce)
+				break
+		else:
+			raise SubSeedNonceRangeExceeded('nonce range exceeded')
+
+		if g.debug_subseed:
+			A = parent_seed.data
+			B = self.join().data
+			assert A == B, f'Data mismatch!\noriginal seed: {A!r}\nrejoined seed: {B!r}'
+
+	def get_share_by_idx(self,idx,base_seed=False):
+		if idx < 1 or idx > self.count:
+			raise RangeError(f'{idx}: share index out of range')
+		elif idx == self.count:
+			return self.last_share
+		elif self.master_share and idx == 1:
+			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,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 if base_seed else self.master_share.derived_seed
+		else:
+			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)))
+
+	def format(self):
+		assert self.split_type == 'N-of-N'
+		fs1 = '    {}\n'
+		fs2 = '{i:>5}: {}\n'
+		mfs1,mfs2,midx,msid = ('','','','')
+		if self.master_share:
+			mfs1,mfs2 = (' with master share #{} ({})',' (master share #{})')
+			midx,msid = (self.master_share.idx,self.master_share.sid)
+
+		hdr  = '    {} {} ({} bits)\n'.format('Seed:',self.parent_seed.sid.hl(),self.parent_seed.bitlen)
+		hdr += '    {} {c}-of-{c} (XOR){m}\n'.format('Split Type:',c=self.count,m=mfs1.format(midx,msid))
+		hdr += '    {} {}\n\n'.format('ID String:',self.id_str.hl())
+		hdr += fs1.format('Shares')
+		hdr += fs1.format('------')
+
+		sl = self.data['long'].keys
+		body1 = fs2.format(sl[0]+mfs2.format(midx),i=1)
+		body = (fs2.format(sl[n],i=n+1) for n in range(1,len(self)))
+
+		return hdr + body1 + ''.join(body)
+
+class SeedShareBase(MMGenObject):
+
+	@property
+	def fn_stem(self):
+		pl = self.parent_list
+		msdata = f'_with_master{pl.master_share.idx}' if pl.master_share else ''
+		return '{}-{}-{}of{}{}[{}]'.format(
+			pl.parent_seed.sid,
+			pl.id_str,
+			self.idx,
+			pl.count,
+			msdata,
+			self.sid)
+
+	@property
+	def desc(self):
+		return self.get_desc()
+
+	def get_desc(self,ui=False):
+		pl = self.parent_list
+		mss = f', with master share #{pl.master_share.idx}' if pl.master_share else ''
+		if ui:
+			m   = ( yellow("(share {} of {} of ")
+					+ pl.parent_seed.sid.hl()
+					+ yellow(', split id ')
+					+ pl.id_str.hl(encl="''")
+					+ yellow('{})') )
+		else:
+			m = "share {} of {} of " + pl.parent_seed.sid + ", split id '" + pl.id_str + "'{}"
+		return m.format(self.idx,pl.count,mss)
+
+class SeedShare(SeedShareBase,SubSeed):
+
+	@staticmethod
+	def make_subseed_bin(parent_list,idx:int,nonce:int,length:str):
+		seed = parent_list.parent_seed
+		assert parent_list.have_short == False
+		assert length == 'long'
+		# field maximums: id_str: none (256 chars), count: 65535 (1024), idx: 65535 (1024), nonce: 65535 (1000)
+		scramble_key = (
+			f'{parent_list.split_type}:{parent_list.id_str}:'.encode() +
+			parent_list.count.to_bytes(2,'big') +
+			idx.to_bytes(2,'big') +
+			nonce.to_bytes(2,'big')
+		)
+		if parent_list.master_share:
+			scramble_key += (
+				b':master:' +
+				parent_list.master_share.idx.to_bytes(2,'big')
+			)
+		return scramble_seed(seed.data,scramble_key)[:seed.byte_len]
+
+class SeedShareLast(SeedShareBase,SeedBase):
+
+	idx = ImmutableAttr(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
+	def make_subseed_bin(parent_list):
+		seed_list = (parent_list.get_share_by_idx(i+1) for i in range(len(parent_list)))
+		seed = parent_list.parent_seed
+
+		ret = int(seed.data.hex(),16)
+		for ss in seed_list:
+			ret ^= int(ss.data.hex(),16)
+
+		return ret.to_bytes(seed.byte_len,'big')
+
+class SeedShareMaster(SeedBase,SeedShareBase):
+
+	idx   = ImmutableAttr(MasterShareIdx)
+	nonce = ImmutableAttr(int,typeconv=False)
+
+	def __init__(self,parent_list,idx,nonce):
+		self.idx = idx
+		self.nonce = nonce
+		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{}[{}]'.format(
+			self.parent_list.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_share:' + self.idx.to_bytes(2,'big') + self.nonce.to_bytes(2,'big')
+		return scramble_seed(seed.data,scramble_key)[:seed.byte_len]
+
+	# Don't bother with avoiding seed ID collision here, as sid of derived seed is not used
+	# by user as an identifier
+	def make_derived_seed_bin(self,id_str,count):
+		# field maximums: id_str: none (256 chars), count: 65535 (1024)
+		scramble_key = id_str.encode() + b':' + count.to_bytes(2,'big')
+		return scramble_seed(self.data,scramble_key)[:self.byte_len]
+
+	def get_desc(self,ui=False):
+		psid = self.parent_list.parent_seed.sid
+		mss = f'master share #{self.idx} of '
+		return yellow('(' + mss) + psid.hl() + yellow(')') if ui else mss + psid
+
+class SeedShareMasterJoining(SeedShareMaster):
+
+	id_str = ImmutableAttr(SeedSplitIDString)
+	count  = ImmutableAttr(SeedShareCount)
+
+	def __init__(self,idx,base_seed,id_str,count):
+		SeedBase.__init__(self,seed_bin=base_seed.data)
+
+		self.id_str = id_str or 'default'
+		self.count = count
+		self.derived_seed = SeedBase(self.make_derived_seed_bin(self.id_str,self.count))
+
+def join_shares(seed_list,master_idx=None,id_str=None):
+	if not hasattr(seed_list,'__next__'): # seed_list can be iterator or iterable
+		seed_list = iter(seed_list)
+
+	class d(object):
+		byte_len,ret,count = None,0,0
+
+	def add_share(ss):
+		if d.byte_len:
+			assert ss.byte_len == d.byte_len, f'Seed length mismatch! {ss.byte_len} != {d.byte_len}'
+		else:
+			d.byte_len = ss.byte_len
+		d.ret ^= int(ss.data.hex(),16)
+		d.count += 1
+
+	if master_idx:
+		master_share = next(seed_list)
+
+	for ss in seed_list:
+		add_share(ss)
+
+	if master_idx:
+		add_share(SeedShareMasterJoining(master_idx,master_share,id_str,d.count+1).derived_seed)
+
+	SeedShareCount(d.count)
+	return Seed(seed_bin=d.ret.to_bytes(d.byte_len,'big'))

+ 1 - 0
mmgen/tool.py

@@ -23,6 +23,7 @@ tool.py:  Routines for the 'mmgen-tool' utility
 from .protocol import hash160
 from .common import *
 from .crypto import *
+from .seedsplit import MasterShareIdx
 from .addr import *
 
 NL = ('\n','\r\n')[g.platform=='win']

+ 1 - 1
test/objattrtest_py_d/oat_common.py

@@ -11,7 +11,7 @@ import os
 from decimal import Decimal
 
 from mmgen.obj import *
-from mmgen.seed import *
+from mmgen.seedsplit import *
 from mmgen.protocol import *
 from mmgen.addr import *
 from mmgen.tx import *

+ 1 - 1
test/objtest.py

@@ -32,7 +32,7 @@ os.environ['MMGEN_TEST_SUITE'] = '1'
 from mmgen.common import *
 from mmgen.obj import *
 from mmgen.altcoins.eth.obj import *
-from mmgen.seed import *
+from mmgen.seedsplit import *
 
 opts_data = {
 	'sets': [('super_silent', True, 'silent', True)],

+ 1 - 1
test/objtest_py_d/ot_btc_mainnet.py

@@ -8,7 +8,7 @@ test.objtest_py_d.ot_btc_mainnet: BTC mainnet test vectors for MMGen data object
 """
 
 from mmgen.obj import *
-from mmgen.seed import *
+from mmgen.seedsplit import *
 from .ot_common import *
 
 from mmgen.protocol import init_proto

+ 1 - 1
test/test_py_d/ts_seedsplit.py

@@ -105,7 +105,7 @@ class TestSuiteSeedSplit(TestSuiteBase):
 		if not wf:
 			t.passphrase(dfl_wcls.desc,wpasswd)
 		if spec:
-			from mmgen.obj import SeedSplitSpecifier
+			from mmgen.seedsplit import SeedSplitSpecifier
 			sss = SeedSplitSpecifier(spec)
 			pat = rf'Processing .*\b{sss.idx}\b of \b{sss.count}\b of .* id .*{sss.id!r}'
 		else:

+ 2 - 2
test/unit_tests_d/ut_seedsplit.py

@@ -8,8 +8,8 @@ from mmgen.common import *
 class unit_test(object):
 
 	def run_test(self,name,ut):
-		from mmgen.seed import Seed,SeedShareList
-		from mmgen.obj import SeedShareIdx
+		from mmgen.seed import Seed
+		from mmgen.seedsplit import SeedShareList,SeedShareIdx
 
 		g.debug_subseed = bool(opt.verbose)