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):
 		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
-	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
 			seed_list = iter(seed_list)
 
@@ -227,9 +227,15 @@ class Seed(SeedBase):
 			d.ret ^= int(ss.data.hex(),16)
 			d.count += 1
 
+		if use_master:
+			master_share = next(seed_list)
+
 		for ss in seed_list:
 			add_share(ss)
 
+		if use_master:
+			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.slen // 8,'big'))
 
@@ -258,20 +264,26 @@ class SubSeed(SeedBase):
 		return scramble_seed(seed.data,scramble_key)[:byte_len]
 
 class SeedShareList(SubSeedList):
+	master_share = None
 	have_short = False
 	split_type = 'N-of-N'
 
 	count = MMGenImmutableAttr('count',SeedShareCount)
 	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.parent_seed = parent_seed
 		self.id_str = id_str or 'default'
 		self.count = count
 
+		if master_idx:
+			self.master_share = SeedShareMaster(self,master_idx)
+
 		while True:
 			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.last_share = SeedShareLast(self)
 			sid = self.last_share.sid
@@ -292,6 +304,8 @@ class SeedShareList(SubSeedList):
 	def get_share_by_idx(self,idx):
 		if idx == self.count:
 			return self.last_share
+		elif self.master_share and idx == 1:
+			return self.master_share.derived_seed
 		else:
 			ss_idx = SubSeedIdx(str(idx) + 'L')
 			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):
 		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
 		else:
 			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'
 		fs1 = '    {}\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 += '    {} {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 += fs1.format('Shares')
 		hdr += fs1.format('------')
 
 		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):
 
@@ -333,6 +354,8 @@ class SeedShare(SubSeed):
 						parent_list.count.to_bytes(2,'big',signed=False) + \
 						idx.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
 		return scramble_seed(seed.data,scramble_key)[:byte_len]
 
@@ -356,6 +379,46 @@ class SeedShareLast(SubSeed):
 
 		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):
 
 	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.obj import SeedShareIdx
 
-		def basic_ops():
+		def basic_ops(master_idx):
 			test_data = {
 				'default': (
 					(8,'4710FBF0','B3D9411B','2670E83D','D1FC57ED','AE49CABE','63FFBA62',256),
@@ -24,9 +24,32 @@ class unit_test(object):
 					(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','φυβαρ'):
-				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('')
 
 				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)):
 
-						shares = seed.split(share_count,id_str)
+						shares = seed.split(share_count,id_str,master_idx)
 						A = len(shares)
 						assert A == share_count, A
 
@@ -59,6 +82,11 @@ class unit_test(object):
 						A = shares.join().sid
 						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')
 
 		def defaults_and_limits():
@@ -109,7 +137,9 @@ class unit_test(object):
 			vmsg_r('\n{} collisions, last_sid {}'.format(collisions,last_sid))
 			msg('OK')
 
-		basic_ops()
+		basic_ops(master_idx=None)
+		basic_ops(master_idx=1)
+		basic_ops(master_idx=5)
 		defaults_and_limits()
 		collisions()