From ff450b0ec5e2e12c11f5b30e5f37e69a0bcf9b0d Mon Sep 17 00:00:00 2001 From: The MMGen Project Date: Sat, 15 Jan 2022 14:00:05 +0000 Subject: [PATCH] seed.py: move seedsplit related classes to seedsplit.py --- mmgen/help.py | 2 +- mmgen/main_seedjoin.py | 5 +- mmgen/main_wallet.py | 6 +- mmgen/obj.py | 25 --- mmgen/seed.py | 256 +---------------------- mmgen/seedsplit.py | 307 ++++++++++++++++++++++++++++ mmgen/tool.py | 1 + test/objattrtest_py_d/oat_common.py | 2 +- test/objtest.py | 2 +- test/objtest_py_d/ot_btc_mainnet.py | 2 +- test/test_py_d/ts_seedsplit.py | 2 +- test/unit_tests_d/ut_seedsplit.py | 4 +- 12 files changed, 328 insertions(+), 286 deletions(-) create mode 100755 mmgen/seedsplit.py diff --git a/mmgen/help.py b/mmgen/help.py index c0b232f6..42f63019 100755 --- a/mmgen/help.py +++ b/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: diff --git a/mmgen/main_seedjoin.py b/mmgen/main_seedjoin.py index a738487e..eec8c899 100755 --- a/mmgen/main_seedjoin.py +++ b/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 = { diff --git a/mmgen/main_wallet.py b/mmgen/main_wallet.py index cff12757..4c1e5273 100755 --- a/mmgen/main_wallet.py +++ b/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) diff --git a/mmgen/obj.py b/mmgen/obj.py index 31e48fcf..437449fa 100755 --- a/mmgen/obj.py +++ b/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 diff --git a/mmgen/seed.py b/mmgen/seed.py index e1b31f18..094d5263 100755 --- a/mmgen/seed.py +++ b/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)) diff --git a/mmgen/seedsplit.py b/mmgen/seedsplit.py new file mode 100755 index 00000000..70333527 --- /dev/null +++ b/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 +# +# 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 . + +""" +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')) diff --git a/mmgen/tool.py b/mmgen/tool.py index b586de40..03729e88 100755 --- a/mmgen/tool.py +++ b/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'] diff --git a/test/objattrtest_py_d/oat_common.py b/test/objattrtest_py_d/oat_common.py index 6518c89d..2f3447d8 100755 --- a/test/objattrtest_py_d/oat_common.py +++ b/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 * diff --git a/test/objtest.py b/test/objtest.py index 8c2c7e16..d0a968e7 100755 --- a/test/objtest.py +++ b/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)], diff --git a/test/objtest_py_d/ot_btc_mainnet.py b/test/objtest_py_d/ot_btc_mainnet.py index 415d6ab0..0cbb5299 100755 --- a/test/objtest_py_d/ot_btc_mainnet.py +++ b/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 diff --git a/test/test_py_d/ts_seedsplit.py b/test/test_py_d/ts_seedsplit.py index f8a12868..39f46527 100755 --- a/test/test_py_d/ts_seedsplit.py +++ b/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: diff --git a/test/unit_tests_d/ut_seedsplit.py b/test/unit_tests_d/ut_seedsplit.py index 34f68ceb..86b2e9c1 100755 --- a/test/unit_tests_d/ut_seedsplit.py +++ b/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)