Browse Source

new classes: SeedShareMaster, SeedShareMasterJoining

- create multiple seed splits with a single “master share”.  The master share
  is a deterministic pseudorandom value which is HMAC’ed with the split’s ID
  string and split count to produce the first share of the split.  To ensure
  uniqueness between splits, the remaining pseudorandom shares have the master
  share’s index appended to their HMAC key.  Each seed has 1024 numerically
  indexed master shares.
MMGen 5 years ago
parent
commit
237567bca6
2 changed files with 104 additions and 11 deletions
  1. 70 7
      mmgen/seed.py
  2. 34 4
      test/unit_tests_d/ut_seedsplit.py

+ 70 - 7
mmgen/seed.py

@@ -206,11 +206,11 @@ class Seed(SeedBase):
 	def subseed_by_seed_id(self,sid,last_idx=None,print_msg=False):
 	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)
 		return self.subseeds.get_subseed_by_seed_id(sid,last_idx=last_idx,print_msg=print_msg)
 
 
-	def split(self,count,id_str=None):
-		return SeedShareList(self,count,id_str)
+	def split(self,count,id_str=None,master_idx=None):
+		return SeedShareList(self,count,id_str,master_idx)
 
 
 	@staticmethod
 	@staticmethod
-	def join_shares(seed_list):
+	def join_shares(seed_list,use_master=False,master_idx=1,id_str=None):
 		if not hasattr(seed_list,'__next__'): # seed_list can be iterator or iterable
 		if not hasattr(seed_list,'__next__'): # seed_list can be iterator or iterable
 			seed_list = iter(seed_list)
 			seed_list = iter(seed_list)
 
 
@@ -227,9 +227,15 @@ class Seed(SeedBase):
 			d.ret ^= int(ss.data.hex(),16)
 			d.ret ^= int(ss.data.hex(),16)
 			d.count += 1
 			d.count += 1
 
 
+		if use_master:
+			master_share = next(seed_list)
+
 		for ss in seed_list:
 		for ss in seed_list:
 			add_share(ss)
 			add_share(ss)
 
 
+		if use_master:
+			add_share(SeedShareMasterJoining(master_idx,master_share,id_str,d.count+1).derived_seed)
+
 		SeedShareCount(d.count)
 		SeedShareCount(d.count)
 		return Seed(seed_bin=d.ret.to_bytes(d.slen // 8,'big'))
 		return Seed(seed_bin=d.ret.to_bytes(d.slen // 8,'big'))
 
 
@@ -258,20 +264,26 @@ class SubSeed(SeedBase):
 		return scramble_seed(seed.data,scramble_key)[:byte_len]
 		return scramble_seed(seed.data,scramble_key)[:byte_len]
 
 
 class SeedShareList(SubSeedList):
 class SeedShareList(SubSeedList):
+	master_share = None
 	have_short = False
 	have_short = False
 	split_type = 'N-of-N'
 	split_type = 'N-of-N'
 
 
 	count = MMGenImmutableAttr('count',SeedShareCount)
 	count = MMGenImmutableAttr('count',SeedShareCount)
 	id_str = MMGenImmutableAttr('id_str',SeedShareIDString)
 	id_str = MMGenImmutableAttr('id_str',SeedShareIDString)
 
 
-	def __init__(self,parent_seed,count,id_str=None):
+	def __init__(self,parent_seed,count,id_str=None,master_idx=None):
 		self.member_type = SeedShare
 		self.member_type = SeedShare
 		self.parent_seed = parent_seed
 		self.parent_seed = parent_seed
 		self.id_str = id_str or 'default'
 		self.id_str = id_str or 'default'
 		self.count = count
 		self.count = count
 
 
+		if master_idx:
+			self.master_share = SeedShareMaster(self,master_idx)
+
 		while True:
 		while True:
 			self.data = { 'long': IndexedDict(), 'short': IndexedDict() }
 			self.data = { 'long': IndexedDict(), 'short': IndexedDict() }
+			if master_idx:
+				self.data['long'][self.master_share.derived_seed.sid] = (1,master_idx)
 			self._generate(count-1)
 			self._generate(count-1)
 			self.last_share = SeedShareLast(self)
 			self.last_share = SeedShareLast(self)
 			sid = self.last_share.sid
 			sid = self.last_share.sid
@@ -292,6 +304,8 @@ class SeedShareList(SubSeedList):
 	def get_share_by_idx(self,idx):
 	def get_share_by_idx(self,idx):
 		if idx == self.count:
 		if idx == self.count:
 			return self.last_share
 			return self.last_share
+		elif self.master_share and idx == 1:
+			return self.master_share.derived_seed
 		else:
 		else:
 			ss_idx = SubSeedIdx(str(idx) + 'L')
 			ss_idx = SubSeedIdx(str(idx) + 'L')
 			return self.get_subseed_by_ss_idx(ss_idx)
 			return self.get_subseed_by_ss_idx(ss_idx)
@@ -299,6 +313,8 @@ class SeedShareList(SubSeedList):
 	def get_share_by_seed_id(self,sid,last_idx=None):
 	def get_share_by_seed_id(self,sid,last_idx=None):
 		if sid == self.data['long'].key(self.count-1):
 		if sid == self.data['long'].key(self.count-1):
 			return self.last_share
 			return self.last_share
+		elif self.master_share and sid == self.data['long'].key(0):
+			return self.master_share.derived_seed
 		else:
 		else:
 			return self.get_subseed_by_seed_id(sid,last_idx=last_idx)
 			return self.get_subseed_by_seed_id(sid,last_idx=last_idx)
 
 
@@ -309,17 +325,22 @@ class SeedShareList(SubSeedList):
 		assert self.split_type == 'N-of-N'
 		assert self.split_type == 'N-of-N'
 		fs1 = '    {}\n'
 		fs1 = '    {}\n'
 		fs2 = '{i:>5}: {}\n'
 		fs2 = '{i:>5}: {}\n'
+		mfs1,mfs2,midx,msid = ('','','','')
+		if self.master_share:
+			mfs1,mfs2 = (' with master share #{} ({})',' master #{} ({})')
+			midx,msid = (self.master_share.idx,self.master_share.sid)
 
 
 		hdr  = '    {} {} ({} bits)\n'.format('Seed:',self.parent_seed.sid.hl(),self.parent_seed.length)
 		hdr  = '    {} {} ({} bits)\n'.format('Seed:',self.parent_seed.sid.hl(),self.parent_seed.length)
-		hdr += '    {} {c}-of-{c} (XOR)\n'.format('Split Type:',c=self.count)
+		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 += '    {} {}\n\n'.format('ID String:',self.id_str.hl())
 		hdr += fs1.format('Shares')
 		hdr += fs1.format('Shares')
 		hdr += fs1.format('------')
 		hdr += fs1.format('------')
 
 
 		sl = self.data['long'].keys
 		sl = self.data['long'].keys
-		body = (fs2.format(sl[n],i=n+1) for n in range(len(self)))
+		body1 = fs2.format(sl[0]+mfs2.format(midx,msid),i=1)
+		body = (fs2.format(sl[n],i=n+1) for n in range(1,len(self)))
 
 
-		return hdr + ''.join(body)
+		return hdr + body1 + ''.join(body)
 
 
 class SeedShare(SubSeed):
 class SeedShare(SubSeed):
 
 
@@ -333,6 +354,8 @@ class SeedShare(SubSeed):
 						parent_list.count.to_bytes(2,'big',signed=False) + \
 						parent_list.count.to_bytes(2,'big',signed=False) + \
 						idx.to_bytes(2,'big',signed=False) + \
 						idx.to_bytes(2,'big',signed=False) + \
 						nonce.to_bytes(2,'big',signed=False)
 						nonce.to_bytes(2,'big',signed=False)
+		if parent_list.master_share:
+			scramble_key += b':master:' + parent_list.master_share.idx.to_bytes(2,'big',signed=False)
 		byte_len = seed.length // 8
 		byte_len = seed.length // 8
 		return scramble_seed(seed.data,scramble_key)[:byte_len]
 		return scramble_seed(seed.data,scramble_key)[:byte_len]
 
 
@@ -356,6 +379,46 @@ class SeedShareLast(SubSeed):
 
 
 		return ret.to_bytes(seed.length // 8,'big')
 		return ret.to_bytes(seed.length // 8,'big')
 
 
+class SeedShareMaster(SubSeed):
+
+	idx = MMGenImmutableAttr('idx',MasterShareIdx)
+	nonce = 0
+
+	def __init__(self,parent_list,idx):
+		self.idx = idx
+		self.parent_seed = parent_list.parent_seed
+		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):
+		# 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]
+
+	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',signed=False)
+		byte_len = self.length // 8
+		return scramble_seed(self.data,scramble_key)[:byte_len]
+
+class SeedShareMasterJoining(SeedShareMaster):
+
+	id_str = MMGenImmutableAttr('id_str',SeedShareIDString)
+	count = MMGenImmutableAttr('count',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))
+
 class SeedSource(MMGenObject):
 class SeedSource(MMGenObject):
 
 
 	desc = g.proj_name + ' seed source'
 	desc = g.proj_name + ' seed source'

+ 34 - 4
test/unit_tests_d/ut_seedsplit.py

@@ -11,7 +11,7 @@ class unit_test(object):
 		from mmgen.seed import Seed
 		from mmgen.seed import Seed
 		from mmgen.obj import SeedShareIdx
 		from mmgen.obj import SeedShareIdx
 
 
-		def basic_ops():
+		def basic_ops(master_idx):
 			test_data = {
 			test_data = {
 				'default': (
 				'default': (
 					(8,'4710FBF0','B3D9411B','2670E83D','D1FC57ED','AE49CABE','63FFBA62',256),
 					(8,'4710FBF0','B3D9411B','2670E83D','D1FC57ED','AE49CABE','63FFBA62',256),
@@ -24,9 +24,32 @@ class unit_test(object):
 					(4,'43670520','77140076','EA82CB30','80F7AEDE','D168D768','77BE57AA',128),
 					(4,'43670520','77140076','EA82CB30','80F7AEDE','D168D768','77BE57AA',128),
 				)
 				)
 			}
 			}
+			test_data_master = {
+				'1': {
+					'default': (
+						(8,'4710FBF0','B512A312','3588E156','9374255D','3E87A907','752A2E4E',256),
+						(4,'43670520','05880E2B','C6B438D4','5FF9B5DF','778E9C60','2C01F046',128) ),
+					'φυβαρ': (
+						(8,'4710FBF0','5FA963B0','69A1F56A','25789CC4','9777A750','E17B9B8B',256),
+						(4,'43670520','AF8BFDF8','66F319BE','A5E40978','927549D2','93B2418B',128),
+					)
+				},
+				'5': {
+					'default': (
+						(8,'4710FBF0','A8A34BC0','F69B6CF8','234B5DCD','BB004DC5','08DC9776',256),
+						(4,'43670520','C887A2D6','86AE9445','3188AD3D','07339882','BE3FE72A',128) ),
+
+					'φυβαρ': (
+						(8,'4710FBF0','89C35D99','B1CD5854','8414652C','32C24668','17CA1E19',256),
+						(4,'43670520','06929789','32E8E375','C6AC3C9D','4BEA2AB2','15AFC7F2',128)
+					)
+				}
+			}
+			if master_idx:
+				test_data = test_data_master[str(master_idx)]
 
 
 			for id_str in (None,'default','φυβαρ'):
 			for id_str in (None,'default','φυβαρ'):
-				msg_r('Testing basic ops (id_str={!r})...'.format(id_str))
+				msg_r('Testing basic ops (id_str={!r}, master_idx={})...'.format(id_str,master_idx))
 				vmsg('')
 				vmsg('')
 
 
 				for a,b,c,d,e,f,h,i in test_data[id_str if id_str is not None else 'default']:
 				for a,b,c,d,e,f,h,i in test_data[id_str if id_str is not None else 'default']:
@@ -36,7 +59,7 @@ class unit_test(object):
 
 
 					for share_count,j,k,l in ((2,c,c,d),(5,e,f,h)):
 					for share_count,j,k,l in ((2,c,c,d),(5,e,f,h)):
 
 
-						shares = seed.split(share_count,id_str)
+						shares = seed.split(share_count,id_str,master_idx)
 						A = len(shares)
 						A = len(shares)
 						assert A == share_count, A
 						assert A == share_count, A
 
 
@@ -59,6 +82,11 @@ class unit_test(object):
 						A = shares.join().sid
 						A = shares.join().sid
 						assert A == b, A
 						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
+							assert A == b, A
+
 				msg('OK')
 				msg('OK')
 
 
 		def defaults_and_limits():
 		def defaults_and_limits():
@@ -109,7 +137,9 @@ class unit_test(object):
 			vmsg_r('\n{} collisions, last_sid {}'.format(collisions,last_sid))
 			vmsg_r('\n{} collisions, last_sid {}'.format(collisions,last_sid))
 			msg('OK')
 			msg('OK')
 
 
-		basic_ops()
+		basic_ops(master_idx=None)
+		basic_ops(master_idx=1)
+		basic_ops(master_idx=5)
 		defaults_and_limits()
 		defaults_and_limits()
 		collisions()
 		collisions()