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.
This commit is contained in:
The MMGen Project 2019-06-11 15:55:35 +00:00
commit 237567bca6
Signed by: mmgen
GPG key ID: 3F8B1861E32B7DA2
2 changed files with 104 additions and 11 deletions

View file

@ -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'

View file

@ -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()