diff --git a/cmds/mmgen-seedjoin b/cmds/mmgen-seedjoin new file mode 100755 index 00000000..8870dd2c --- /dev/null +++ b/cmds/mmgen-seedjoin @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 + +# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution +# Copyright (C)2013-2019 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 . + +""" +mmgen-seedjoin: Recreate an MMGen deterministic wallet from its component shares +""" + +from mmgen.main import launch +launch("seedjoin") diff --git a/cmds/mmgen-seedsplit b/cmds/mmgen-seedsplit new file mode 100755 index 00000000..7b27c037 --- /dev/null +++ b/cmds/mmgen-seedsplit @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 + +# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution +# Copyright (C)2013-2019 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 . + +""" +mmgen-seedsplit: Generate a seed split from an MMGen deterministic wallet +""" + +from mmgen.main import launch +launch("seedsplit") diff --git a/mmgen/common.py b/mmgen/common.py index 3b9b7e73..4bccc607 100755 --- a/mmgen/common.py +++ b/mmgen/common.py @@ -28,7 +28,7 @@ from mmgen.opts import opt from mmgen.util import * def help_notes(k): - from mmgen.obj import SubSeedIdxRange + from mmgen.obj import SubSeedIdxRange,SeedShareIdx,SeedShareCount,MasterShareIdx from mmgen.seed import SeedSource from mmgen.tx import MMGenTX def fee_spec_letters(use_quotes=False): @@ -41,6 +41,77 @@ def help_notes(k): return { 'rel_fee_desc': MMGenTX().rel_fee_desc, 'fee_spec_letters': fee_spec_letters(), + 'seedsplit': """ +COMMAND NOTES: + +This command generates shares one at a time. Shares may be output to any +MMGen wallet format, with one limitation: only one share in a given split may +be in hidden incognito format, and it must be the master share in the case of +a master-share split. + +If the command's optional first argument is omitted, the default wallet is +used for the split. + +The last argument is a seed split specifier consisting of an optional split +ID, a share index, and a share count, all separated by colons. The split ID +must be a valid UTF-8 string. If omitted, the ID 'default' is used. The +share index (the index of the share being generated) must be in the range +{sia}-{sib} and the share count (the total number of shares in the split) +in the range {sca}-{scb}. + +Master Shares + +Each seed has a total of {msb} master shares, which can be used as the first +shares in multiple splits if desired. To generate a master share, use the +--master-share (-M) option with an index in the range {msa}-{msb} and omit +the last argument. + +When creating and joining a split using a master share, ensure that the same +master share index is used in all split and join commands. + +EXAMPLES: + + Create a 3-way default split of your default wallet, outputting all shares + to default wallet format. Rejoin the split: + + $ mmgen-seedsplit 1:3 # Step A + $ mmgen-seedsplit 2:3 # Step B + $ mmgen-seedsplit 3:3 # Step C + $ mmgen-seedjoin + + Create a 2-way split of your default wallet with ID string 'alice', + outputting shares to MMGen native mnemonic format. Rejoin the split: + + $ mmgen-seedsplit -o words alice:1:2 # Step D + $ mmgen-seedsplit -o words alice:2:2 # Step E + $ mmgen-seedjoin + + Create a 2-way split of your default wallet with ID string 'bob' using + master share #7, outputting share #1 (the master share) to default wallet + format and share #2 to BIP39 format. Rejoin the split: + + $ mmgen-seedsplit -M7 # Step X + $ mmgen-seedsplit -M7 -o bip39 bob:2:2 # Step Y + $ mmgen-seedjoin -M7 --id-str=bob + + Create a 2-way split of your default wallet with ID string 'alice' using + master share #7. Rejoin the split using master share #7 generated in the + previous example: + + $ mmgen-seedsplit -M7 -o bip39 alice:2:2 # Step Z + $ mmgen-seedjoin -M7 --id-str=alice + + Create a 2-way default split of your default wallet with an incognito-format + master share hidden in file 'my.hincog' at offset 1325. Rejoin the split: + + $ mmgen-seedsplit -M4 -o hincog -J my.hincog,1325 1:2 # Step M (share A) + $ mmgen-seedsplit -M4 -o bip39 2:2 # Step N (share B) + $ mmgen-seedjoin -M4 -H my.hincog,1325 + +""".strip().format( + sia=SeedShareIdx.min_val,sib=SeedShareIdx.max_val, + sca=SeedShareCount.min_val,scb=SeedShareCount.max_val, + msa=MasterShareIdx.min_val,msb=MasterShareIdx.max_val), 'subwallet': """ SUBWALLETS: diff --git a/mmgen/main.py b/mmgen/main.py index a791717f..45549de9 100755 --- a/mmgen/main.py +++ b/mmgen/main.py @@ -22,7 +22,7 @@ main.py - Script launcher for the MMGen suite def launch(mod): - if mod in ('walletgen','walletchk','walletconv','passchg','subwalletgen'): + if mod in ('walletgen','walletchk','walletconv','passchg','subwalletgen','seedsplit'): mod = 'wallet' if mod == 'keygen': mod = 'addrgen' diff --git a/mmgen/main_seedjoin.py b/mmgen/main_seedjoin.py new file mode 100755 index 00000000..45dbedcf --- /dev/null +++ b/mmgen/main_seedjoin.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python3 +# +# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution +# Copyright (C)2013-2019 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 . + +""" +mmgen/main_seedjoin: Regenerate an MMGen deterministic wallet from seed shares + created by 'mmgen-seedsplit' +""" + +from mmgen.common import * +from mmgen.obj import MasterShareIdx,SeedSplitIDString,MMGenWalletLabel +from mmgen.seed import Seed,SeedSource,SeedShareMasterJoining + +opts_data = { + 'text': { + 'desc': """Regenerate an MMGen deterministic wallet from seed shares + created by 'mmgen-seedsplit'""", + 'usage': '[options] share1 share2 [...shareN]', + 'options': """ +-h, --help Print this help message +--, --longhelp Print help message for long options (common options) +-d, --outdir= d Output file to directory 'd' instead of working dir +-e, --echo-passphrase Echo passphrases and other user input to screen +-i, --id-str= s ID String of split (required for master share join only) +-H, --hidden-incog-input-params=f,o Read hidden incognito data from file + 'f' at offset 'o' (comma-separated). NOTE: only the + first share may be in hidden incognito format! +-J, --hidden-incog-output-params=f,o Write hidden incognito data to file + 'f' at offset 'o' (comma-separated). File 'f' will be + created if necessary and filled with random data. +-o, --out-fmt= f Output to wallet format 'f' (see FMT CODES below) +-O, --old-incog-fmt Specify old-format incognito input +-L, --label= l Specify a label 'l' for output wallet +-M, --master-share=i Use a master share with index 'i' (min:{ms_min}, max:{ms_max}) +-p, --hash-preset= p Use the scrypt hash parameters defined by preset 'p' + for password hashing (default: '{g.hash_preset}') +-z, --show-hash-presets Show information on available hash presets +-P, --passwd-file= f Get wallet passphrase from file 'f' +-q, --quiet Produce quieter output; suppress some warnings +-r, --usr-randchars=n Get 'n' characters of additional randomness from user + (min={g.min_urandchars}, max={g.max_urandchars}, default={g.usr_randchars}) +-S, --stdout Write wallet data to stdout instead of file +-v, --verbose Produce more verbose output +""", + 'notes': """ + +COMMAND NOTES: + +When joining with a master share, the master share must be listed first. +The remaining shares may be listed in any order. + +The --id-str option is required only for master share joins. For ordinary +joins it will be ignored. + +For usage examples, see the help screen for the 'mmgen-seedsplit' command. + +{n_pw} + +FMT CODES: + + {f} +""" + }, + 'code': { + 'options': lambda s: s.format( + ms_min=MasterShareIdx.min_val, + ms_max=MasterShareIdx.max_val, + g=g, + ), + 'notes': lambda s: s.format( + f='\n '.join(SeedSource.format_fmt_codes().splitlines()), + n_pw=help_notes('passwd'), + ) + } +} + +def print_shares_info(): + si,out = 0,'\nComputed shares:\n' + if opt.master_share: + fs = '{:3}: {}->{} ' + yellow('(master share #{}, split id ') + '{}' + yellow(', share count {})\n') + out += fs.format( + 1, + shares[0].sid, + share1.sid, + master_idx, + id_str.hl(encl="''"), + len(shares) ) + si = 1 + for n,s in enumerate(shares[si:],si+1): + out += '{:3}: {}\n'.format(n,s.sid) + qmsg(out) + +cmd_args = opts.init(opts_data) + +if len(cmd_args) + bool(opt.hidden_incog_input_params) < 2: + opts.usage() + +if opt.label: + opt.label = MMGenWalletLabel(opt.label,msg="Error in option '--label'") + +if opt.master_share: + master_idx = MasterShareIdx(opt.master_share) + id_str = SeedSplitIDString(opt.id_str or 'default') + +if opt.id_str and not opt.master_share: + die(1,'--id-str option meaningless in context of non-master-share join') + +for arg in cmd_args: + check_wallet_extension(arg) + check_infile(arg) + +do_license_msg() + +qmsg('Input files:\n {}\n'.format('\n '.join(cmd_args))) + +shares = [SeedSource().seed] if opt.hidden_incog_input_params else [] +shares += [SeedSource(fn).seed for fn in cmd_args] + +if opt.master_share: + share1 = SeedShareMasterJoining(master_idx,shares[0],id_str,len(shares)).derived_seed +else: + share1 = shares[0] + +print_shares_info() + +msg_r('Joining {n}-of-{n} XOR split...'.format(n=len(shares))) + +seed_out = Seed.join_shares([share1]+shares[1:]) + +msg('OK\nJoined Seed ID: {}'.format(seed_out.sid.hl())) + +SeedSource(seed=seed_out).write_to_file() diff --git a/mmgen/main_wallet.py b/mmgen/main_wallet.py index bf9fa98a..b7b5027a 100755 --- a/mmgen/main_wallet.py +++ b/mmgen/main_wallet.py @@ -24,7 +24,7 @@ import os from mmgen.common import * from mmgen.seed import SeedSource,Wallet from mmgen.filename import find_file_in_dir -from mmgen.obj import MMGenWalletLabel +from mmgen.obj import MMGenWalletLabel,MasterShareIdx usage = '[opts] [infile]' nargs = 1 @@ -32,6 +32,7 @@ iaction = 'convert' oaction = 'convert' do_bw_note = True do_sw_note = False +do_ss_note = False invoked_as = { 'mmgen-walletgen': 'gen', @@ -39,6 +40,7 @@ invoked_as = { 'mmgen-walletchk': 'chk', 'mmgen-passchg': 'passchg', 'mmgen-subwalletgen': 'subgen', + 'mmgen-seedsplit': 'seedsplit', }[g.prog_name] dsw = 'the default or specified {pnm} wallet' @@ -69,6 +71,13 @@ elif invoked_as == 'subgen': iaction = 'input' oaction = 'output' do_sw_note = True +elif invoked_as == 'seedsplit': + desc = 'Generate a seed share from ' + dsw + opt_filter = 'dehHiJlLMIoOpPqrSvz-' + usage = '[opts] [infile] [:]:' + iaction = 'input' + oaction = 'output' + do_ss_note = True opts_data = { 'text': { @@ -95,6 +104,7 @@ opts_data = { with non-standard (< {g.seed_len}-bit) seed lengths. -L, --label= l Specify a label 'l' for output wallet -m, --keep-label Reuse label of input wallet for output wallet +-M, --master-share=i Use a master share with index 'i' (min:{ms_min}, max:{ms_max}) -p, --hash-preset= p Use the scrypt hash parameters defined by preset 'p' for password hashing (default: '{g.hash_preset}') -z, --show-hash-presets Show information on available hash presets @@ -107,7 +117,7 @@ opts_data = { """, 'notes': """ -{n_sw}{n_pw}{n_bw} +{n_ss}{n_sw}{n_pw}{n_bw} FMT CODES: @@ -118,10 +128,13 @@ FMT CODES: 'options': lambda s: s.format( iaction=capfirst(iaction), oaction=capfirst(oaction), + ms_min=MasterShareIdx.min_val, + ms_max=MasterShareIdx.max_val, g=g, ), 'notes': lambda s: s.format( f='\n '.join(SeedSource.format_fmt_codes().splitlines()), + n_ss=('',help_notes('seedsplit')+'\n\n')[do_ss_note], n_sw=('',help_notes('subwallet')+'\n\n')[do_sw_note], n_pw=help_notes('passwd'), n_bw=('','\n\n'+help_notes('brainwallet'))[do_bw_note] @@ -137,6 +150,24 @@ if opt.label: if invoked_as == 'subgen': from mmgen.obj import SubSeedIdx ss_idx = SubSeedIdx(cmd_args.pop()) +elif invoked_as == 'seedsplit': + from mmgen.obj import SeedSplitSpecifier + master_share = MasterShareIdx(opt.master_share) if opt.master_share else None + if cmd_args: + sss = SeedSplitSpecifier(cmd_args.pop(),on_fail='silent') + if master_share: + if not sss: + sss = SeedSplitSpecifier('1:2') + elif sss.idx == 1: + m1 = 'Share index of 1 meaningless in master share context.' + m2 = 'To generate a master share, omit the seed split specifier.' + die(1,m1+' '+m2) + elif not sss: + opts.usage() + elif master_share: + sss = SeedSplitSpecifier('1:2') + else: + opts.usage() if cmd_args: if invoked_as == 'gen' or len(cmd_args) > 1: @@ -164,10 +195,15 @@ if invoked_as == 'chk': sys.exit(0) if invoked_as != 'gen': - gmsg('Processing output wallet') + gmsg_r('Processing output wallet' + ('\n',' ')[invoked_as == 'seedsplit']) if invoked_as == 'subgen': ss_out = SeedSource(seed_bin=ss_in.seed.subseed(ss_idx,print_msg=True).data) +elif invoked_as == 'seedsplit': + shares = ss_in.seed.split(sss.count,sss.id,master_share) + seed_out = shares.get_share_by_idx(sss.idx,base_seed=True) + msg(seed_out.get_desc(ui=True)) + ss_out = SeedSource(seed=seed_out) else: ss_out = SeedSource(ss=ss_in,passchg=invoked_as=='passchg') diff --git a/mmgen/obj.py b/mmgen/obj.py index 0112938f..305d4cd9 100755 --- a/mmgen/obj.py +++ b/mmgen/obj.py @@ -39,6 +39,7 @@ def is_addrlist_id(s): return AddrListID(s,on_fail='silent') def is_tw_label(s): return TwLabel(s,on_fail='silent') def is_wif(s): return WifKey(s,on_fail='silent') def is_viewkey(s): return ViewKey(s,on_fail='silent') +def is_seed_split_specifier(s): return SeedSplitSpecifier(s,on_fail='silent') def truncate_str(s,width): # width = screen width wide_count = 0 @@ -822,6 +823,23 @@ class MMGenPWIDString(MMGenLabel): forbidden = list(' :/\\') trunc_ok = False +class SeedSplitSpecifier(str,Hilite,InitErrors,MMGenObject): + color = 'red' + def __new__(cls,s,on_fail='raise'): + if type(s) == cls: return s + cls.arg_chk(on_fail) + 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,on_fail=on_fail) + me.idx = SeedShareIdx(b,on_fail=on_fail) + me.count = SeedShareCount(c,on_fail=on_fail) + 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' diff --git a/mmgen/seed.py b/mmgen/seed.py index c4786609..a40f7336 100755 --- a/mmgen/seed.py +++ b/mmgen/seed.py @@ -366,7 +366,38 @@ class SeedShareList(SubSeedList): return hdr + body1 + ''.join(body) -class SeedShare(SubSeed): +class SeedShareBase(MMGenObject): + + @property + def fn_stem(self): + pl = self.parent_list + msdata = '_with_master{}'.format(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 = ', with master share #{}'.format(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): @@ -380,7 +411,7 @@ class SeedShare(SubSeed): 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(SeedBase): +class SeedShareLast(SeedShareBase,SeedBase): idx = MMGenImmutableAttr('idx',SeedShareIdx) nonce = 0 @@ -401,7 +432,7 @@ class SeedShareLast(SeedBase): return ret.to_bytes(seed.byte_len,'big') -class SeedShareMaster(SeedBase): +class SeedShareMaster(SeedBase,SeedShareBase): idx = MMGenImmutableAttr('idx',MasterShareIdx) nonce = MMGenImmutableAttr('nonce',int,typeconv=False) @@ -414,6 +445,13 @@ class SeedShareMaster(SeedBase): 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) @@ -427,6 +465,11 @@ class SeedShareMaster(SeedBase): 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 = 'master share #{} of '.format(self.idx) + return yellow('(' + mss) + psid.hl() + yellow(')') if ui else mss + psid + class SeedShareMasterJoining(SeedShareMaster): id_str = MMGenImmutableAttr('id_str',SeedSplitIDString) diff --git a/mmgen/tool.py b/mmgen/tool.py index 0cf1748f..c5bffe17 100755 --- a/mmgen/tool.py +++ b/mmgen/tool.py @@ -726,6 +726,17 @@ class MMGenToolCmdWallet(MMGenToolCmdBase): from mmgen.seed import SeedSource return SeedSource(sf).seed.subseeds.format(*SubSeedIdxRange(subseed_idx_range)) + def list_shares(self, + share_count:int, + id_str='default', + master_share:"(min:1, max:{}, 0=no master share)".format(MasterShareIdx.max_val)=0, + wallet=''): + "list the Seed IDs of the shares resulting from a split of default or specified wallet" + opt.quiet = True + sf = get_seed_file([wallet] if wallet else [],1) + from mmgen.seed import SeedSource + return SeedSource(sf).seed.split(share_count,id_str,master_share).format() + def gen_key(self,mmgen_addr:str,wallet=''): "generate a single MMGen WIF key from default or specified wallet" return self.gen_addr(mmgen_addr,wallet,target='wif') diff --git a/setup.py b/setup.py index acff062b..a19c961b 100755 --- a/setup.py +++ b/setup.py @@ -142,6 +142,7 @@ setup( 'mmgen.main_autosign', 'mmgen.main_passgen', 'mmgen.main_regtest', + 'mmgen.main_seedjoin', 'mmgen.main_split', 'mmgen.main_tool', 'mmgen.main_txbump', @@ -166,6 +167,8 @@ setup( 'cmds/mmgen-walletchk', 'cmds/mmgen-walletconv', 'cmds/mmgen-walletgen', + 'cmds/mmgen-seedsplit', + 'cmds/mmgen-seedjoin', 'cmds/mmgen-split', 'cmds/mmgen-txcreate', 'cmds/mmgen-txbump', diff --git a/test/objtest_py_d/ot_btc_mainnet.py b/test/objtest_py_d/ot_btc_mainnet.py index a5b9e14c..bb7da71e 100755 --- a/test/objtest_py_d/ot_btc_mainnet.py +++ b/test/objtest_py_d/ot_btc_mainnet.py @@ -13,6 +13,8 @@ from mmgen.obj import * from mmgen.seed import * from .ot_common import * +ssm = str(SeedShareCount.max_val) + tests = OrderedDict([ ('AddrIdx', { 'bad': ('s',1.1,10000000,-1,0), @@ -202,4 +204,10 @@ tests = OrderedDict([ {'s':'P','ret':'P'}, ) }), + ('SeedSplitSpecifier', { + 'bad': ('M','αβ:2',1,'0:1','1:1','2:1','3:2','1:2000','abc:0:2'), + 'good': ( + ('1:2','2:2','alice:2:2','αβ:2:2','1:'+ssm,ssm+':'+ssm) + ) + }), ]) diff --git a/test/test.py b/test/test.py index 888a7c7e..76c78f5a 100755 --- a/test/test.py +++ b/test/test.py @@ -322,6 +322,7 @@ cfgs = { # addr_idx_lists (except 31,32,33,34) must contain exactly 8 addresses }, }, '22': {}, + '23': {}, '31': {}, '32': {}, '33': {}, @@ -448,6 +449,7 @@ class CmdGroupMgr(object): 'ref3': ('TestSuiteRef3Seed',{'is3seed':True,'modname':'ref_3seed'}), 'ref': ('TestSuiteRef',{}), 'ref_altcoin': ('TestSuiteRefAltcoin',{}), + 'seedsplit': ('TestSuiteSeedSplit',{}), 'tool': ('TestSuiteTool',{'modname':'misc','full_data':True}), 'input': ('TestSuiteInput',{'modname':'misc','full_data':True}), 'output': ('TestSuiteOutput',{'modname':'misc','full_data':True}), diff --git a/test/test_py_d/ts_seedsplit.py b/test/test_py_d/ts_seedsplit.py new file mode 100755 index 00000000..6fe75fd9 --- /dev/null +++ b/test/test_py_d/ts_seedsplit.py @@ -0,0 +1,248 @@ +#!/usr/bin/env python3 +# +# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution +# Copyright (C)2013-2019 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 . + +""" +ts_seedsplit.py: Seed split/join tests for the test.py test suite +""" + +from mmgen.globalvars import g +from mmgen.opts import opt + +from test.test_py_d.ts_base import * + +ref_wf = 'test/ref/98831F3A.bip39' +ref_sid = '98831F3A' +wpasswd = 'abc' +sh1_passwd = 'xyz' + +class TestSuiteSeedSplit(TestSuiteBase): + 'splitting and joining seeds' + networks = ('btc',) + tmpdir_nums = [23] + cmd_group = ( + ('ss_walletgen', 'wallet generation'), + ('ss_2way_A_dfl1', '2-way seed split (share A)'), + ('ss_2way_B_dfl1', '2-way seed split (share B)'), + ('ss_2way_join_dfl1', '2-way seed join'), + ('ss_2way_A_dfl2', "2-way seed split 'default' (share A)"), + ('ss_2way_B_dfl2', "2-way seed split 'default' (share B)"), + ('ss_2way_join_dfl2', "2-way seed join 'default'"), + ('ss_2way_A_alice', "2-way seed split 'alice' (share A)"), + ('ss_2way_B_alice', "2-way seed split 'alice' (share B)"), + ('ss_2way_join_alice', "2-way seed join 'alice'"), + ('ss_2way_join_alice_mix', "2-way seed join 'alice' (out of order)"), + ('ss_2way_A_dfl_master3', '2-way seed split with master share #3 (share A)'), + ('ss_2way_B_dfl_master3', '2-way seed split with master share #3 (share B)'), + ('ss_2way_join_dfl_master3', '2-way seed join with master share #3'), + ('ss_2way_A_dfl_usw', '2-way seed split of user-specified wallet (share A)'), + ('ss_2way_B_dfl_usw', '2-way seed split of user-specified wallet (share B)'), + ('ss_2way_join_dfl_usw', '2-way seed join of user-specified wallet'), + ('ss_3way_A_dfl', '3-way seed split (share A)'), + ('ss_3way_B_dfl', '3-way seed split (share B)'), + ('ss_3way_C_dfl', '3-way seed split (share C)'), + ('ss_3way_join_dfl', '3-way seed join'), + ('ss_3way_join_dfl_mix', '3-way seed join (out of order)'), + ('ss_3way_A_foobar_master7', "3-way seed split 'φυβαρ' with master share #7 (share A)"), + ('ss_3way_B_foobar_master7', "3-way seed split 'φυβαρ' with master share #7 (share B)"), + ('ss_3way_C_foobar_master7', "3-way seed split 'φυβαρ' with master share #7 (share C)"), + ('ss_3way_join_foobar_master7', "3-way seed join 'φυβαρ' with master share #7"), + ('ss_3way_join_foobar_master7_mix',"3-way seed join 'φυβαρ' with master share #7 (out of order)"), + + ('ss_3way_join_dfl_bad_invocation',"bad invocation of 'mmgen-seedjoin' - --id-str with non-master join"), + ('ss_bad_invocation1', "bad invocation of 'mmgen-seedsplit' - no arguments"), + ('ss_bad_invocation2', "bad invocation of 'mmgen-seedsplit' - master share with split specifier"), + ('ss_bad_invocation3', "bad invocation of 'mmgen-seedsplit' - nonexistent file"), + ('ss_bad_invocation4', "bad invocation of 'mmgen-seedsplit' - invalid file extension"), + ('ss_bad_invocation5', "bad invocation of 'mmgen-seedjoin' - no arguments"), + ('ss_bad_invocation6', "bad invocation of 'mmgen-seedjoin' - one file argument"), + ('ss_bad_invocation7', "bad invocation of 'mmgen-seedjoin' - invalid file extension"), + ('ss_bad_invocation8', "bad invocation of 'mmgen-seedjoin' - nonexistent file"), + ('ss_bad_invocation9', "bad invocation of 'mmgen-seedsplit' - bad specifier"), + ('ss_bad_invocation10', "bad invocation of 'mmgen-seedsplit' - nonexistent file"), + ('ss_bad_invocation11', "bad invocation of 'mmgen-seedsplit' - invalid file extension"), + ) + + def get_tmp_subdir(self,subdir): + return os.path.join(self.tmpdir,subdir) + + def ss_walletgen(self): + t = self.spawn('mmgen-walletgen', ['-r0','-p1']) + t.passphrase_new('new MMGen wallet',wpasswd) + t.label() + self.write_to_tmpfile('dfl.sid',t.expect_getend('Seed ID: ')) + t.expect('move it to the data directory? (Y/n): ','y') + t.written_to_file('MMGen wallet') + return t + + def ss_splt(self,tdir,ofmt,spec,add_args=[],wf=None,master=None): + try: os.mkdir(self.get_tmp_subdir(tdir)) + except: pass + t = self.spawn('mmgen-seedsplit', + ['-q','-d',self.get_tmp_subdir(tdir),'-r0','-o',ofmt] + + (['-L',(spec or 'label')] if ofmt == 'w' else []) + + add_args + + (['--master-share={}'.format(master)] if master else []) + + ([wf] if wf else []) + + ([spec] if spec else [])) + if not wf: + t.passphrase('MMGen wallet',wpasswd) + if spec: + from mmgen.obj import SeedSplitSpecifier + sss = SeedSplitSpecifier(spec) + pat = "Processing .* {} of {} of .* id '{}'".format(sss.idx,sss.count,sss.id) + else: + pat = "master share #{}".format(master) + t.expect(pat,regex=True) + if ofmt in ('w','incog','incog_hex','hincog'): + desc = {'w': 'MMGen wallet', + 'incog': 'incognito data', + 'incog_hex': 'hex incognito data', + 'hincog': 'hidden incognito data' }[ofmt] + t.hash_preset('new '+desc,'1') + t.passphrase_new('new '+desc,sh1_passwd) + if desc == 'hidden incognito data': + t.hincog_create(1234) + t.written_to_file(capfirst(desc)) + else: + t.written_to_file('data') + return t + + def ss_join(self,tdir,ofmt,in_exts,add_args=[],sid=None,bad_invocation=False,master=None,id_str=None): + td = self.get_tmp_subdir(tdir) + shares = [get_file_with_ext(td,f) for f in in_exts] + if not sid: + sid = self.read_from_tmpfile('dfl.sid') + t = self.spawn('mmgen-seedjoin', + add_args + + (['--master-share={}'.format(master)] if master else []) + + (['--id-str={}'.format(id_str)] if id_str else []) + + ['-d',td,'-o',ofmt] + + (['--label','Joined Wallet Label','-r0'] if ofmt == 'w' else []) + + shares) + if bad_invocation: + t.read() + return t + w_enc = ( 'MMGen wallet' if 'mmdat' in in_exts else + 'incognito data' if 'mmincog' in in_exts else + 'hex incognito data' if 'mmincox' in in_exts else + 'hidden incognito data' if '-H' in add_args else '') + if 'incognito' in w_enc: + t.hash_preset(w_enc,'1') + if w_enc: + t.passphrase(w_enc,sh1_passwd) + if master: + fs = "master share #{}, split id '{}', share count {}" + pat = fs.format(master,id_str or 'default',len(shares)+('hidden' in w_enc)) + t.expect(pat,regex=True) + sid_cmp = t.expect_getend('Joined Seed ID: ') + cmp_or_die(sid,sid_cmp) + if ofmt == 'w': + t.hash_preset('new MMGen wallet','1') + t.passphrase_new('new MMGen wallet',wpasswd) + t.written_to_file('MMGen wallet') + else: + t.written_to_file('data') + return t + + def get_hincog_arg(self,tdir,suf='-default-2of2'): + sid = self.read_from_tmpfile('dfl.sid') + return os.path.join(self.tmpdir,tdir,sid+suf+'.hincog') + ',123' + + def ss_2way_A_dfl1(self): return self.ss_splt('2way_dfl1','w','1:2') + def ss_2way_B_dfl1(self): return self.ss_splt('2way_dfl1','bip39','2:2') + def ss_2way_join_dfl1(self): return self.ss_join('2way_dfl1','w',['mmdat','bip39']) + + def ss_2way_A_dfl2(self): return self.ss_splt('2way_dfl2','seed','default:1:2') + def ss_2way_B_dfl2(self): + return self.ss_splt('2way_dfl2','hincog','default:2:2',['-J',self.get_hincog_arg('2way_dfl2')]) + def ss_2way_join_dfl2(self): + return self.ss_join('2way_dfl2','hex',['mmseed'],['-H',self.get_hincog_arg('2way_dfl2')]) + + def ss_2way_A_alice(self): return self.ss_splt('2way_alice','w','alice:1:2') + def ss_2way_B_alice(self): return self.ss_splt('2way_alice','hex','alice:2:2') + def ss_2way_join_alice(self): return self.ss_join('2way_alice','seed',['mmdat','mmhex']) + def ss_2way_join_alice_mix(self): return self.ss_join('2way_alice','seed',['mmhex','mmdat']) + + def ss_2way_A_dfl_usw(self): return self.ss_splt('2way_dfl_usw','words','1:2',[],wf=ref_wf) + def ss_2way_B_dfl_usw(self): return self.ss_splt('2way_dfl_usw','incog','2:2',[],wf=ref_wf) + def ss_2way_join_dfl_usw(self): return self.ss_join('2way_dfl_usw','hex',['mmwords','mmincog'],sid=ref_sid) + + def ss_3way_A_dfl(self): return self.ss_splt('3way_dfl','words','1:3') + def ss_3way_B_dfl(self): return self.ss_splt('3way_dfl','incog_hex','2:3') + def ss_3way_C_dfl(self): return self.ss_splt('3way_dfl','bip39','3:3') + def ss_3way_join_dfl(self): return self.ss_join('3way_dfl','hex',['mmwords','mmincox','bip39']) + def ss_3way_join_dfl_mix(self): return self.ss_join('3way_dfl','hex',['bip39','mmwords','mmincox']) + + def ss_2way_A_dfl_master3(self): + return self.ss_splt('2way_dfl_master3','w','',master=3) + def ss_2way_B_dfl_master3(self): + return self.ss_splt('2way_dfl_master3','bip39','2:2',master=3) + def ss_2way_join_dfl_master3(self): + return self.ss_join('2way_dfl_master3','hex',['mmdat','bip39'],master=3) + + tdir2 = '3way_foobar_master7' + def ss_3way_C_foobar_master7(self): + return self.ss_splt(self.tdir2,'hincog','', + ['-J',self.get_hincog_arg(self.tdir2,'-master7')],master=7) + def ss_3way_B_foobar_master7(self): + return self.ss_splt(self.tdir2,'bip39','φυβαρ:2:3',master=7) + def ss_3way_A_foobar_master7(self): + return self.ss_splt(self.tdir2,'hex','φυβαρ:3:3',master=7) + def ss_3way_join_foobar_master7(self): + return self.ss_join(self.tdir2,'seed', ['bip39','mmhex'], + ['-H',self.get_hincog_arg(self.tdir2,'-master7')],master=7,id_str='φυβαρ') + def ss_3way_join_foobar_master7_mix(self): + return self.ss_join(self.tdir2,'seed', ['mmhex','bip39'], + ['-H',self.get_hincog_arg(self.tdir2,'-master7')],master=7,id_str='φυβαρ') + + def ss_bad_invocation(self,cmd,args,exit_val): + t = self.spawn(cmd,args) + t.read() + t.req_exit_val = exit_val + return t + + def ss_3way_join_dfl_bad_invocation(self): + t = self.ss_join('3way_dfl','hex', + ['mmwords','mmincox','bip39'], + id_str='foo', + bad_invocation=True) + t.req_exit_val = 1 + return t + + def ss_bad_invocation1(self): + return self.ss_bad_invocation('mmgen-seedsplit',[],1) + def ss_bad_invocation2(self): + return self.ss_bad_invocation('mmgen-seedsplit',['-M1','1:9'],1) + def ss_bad_invocation3(self): + return self.ss_bad_invocation('mmgen-seedsplit',[self.tmpdir+'/no.mmdat','1:9'],1) + def ss_bad_invocation4(self): + return self.ss_bad_invocation('mmgen-seedsplit',[self.tmpdir+'/dfl.sid','1:9'],1) + def ss_bad_invocation5(self): + return self.ss_bad_invocation('mmgen-seedjoin',[],1) + def ss_bad_invocation6(self): + return self.ss_bad_invocation('mmgen-seedjoin',[self.tmpdir+'/a'],1) + def ss_bad_invocation7(self): + return self.ss_bad_invocation('mmgen-seedjoin',[self.tmpdir+'/a',self.tmpdir+'/b'],1) + def ss_bad_invocation8(self): + return self.ss_bad_invocation('mmgen-seedjoin',[self.tmpdir+'/a.mmdat',self.tmpdir+'/b.mmdat'],1) + def ss_bad_invocation9(self): + return self.ss_bad_invocation('mmgen-seedsplit',['x'],1) + def ss_bad_invocation10(self): + return self.ss_bad_invocation('mmgen-seedsplit',[self.tmpdir+'/a.mmdat','1:2'],1) + def ss_bad_invocation11(self): + return self.ss_bad_invocation('mmgen-seedsplit',[self.tmpdir+'/dfl.sid','1:2'],1) diff --git a/test/tooltest2.py b/test/tooltest2.py index 4345e4e6..2cbf9fd8 100755 --- a/test/tooltest2.py +++ b/test/tooltest2.py @@ -330,6 +330,29 @@ tests = { (md5_hash_strip,'996c047e8543d5dde6f82efc3214a6a1') ), ], + 'list_shares': [ + ( ['3','wallet=test/ref/98831F3A.bip39'], + (md5_hash_strip,'84e8bdaebf9c816a8a3bd2ebec5a2e12') + ), + ( ['3','id_str=default','wallet=test/ref/98831F3A.mmwords'], + (md5_hash_strip,'84e8bdaebf9c816a8a3bd2ebec5a2e12') + ), + ( ['3','id_str=foo','wallet=test/ref/98831F3A.bip39'], + (md5_hash_strip,'d2ac20823c4ea26f15234b5ca8df5d6f') + ), + ( ['3','id_str=foo','master_share=0','wallet=test/ref/98831F3A.mmwords'], + (md5_hash_strip,'d2ac20823c4ea26f15234b5ca8df5d6f') + ), + ( ['3','id_str=foo','master_share=5','wallet=test/ref/98831F3A.mmwords'], + (md5_hash_strip,'c4feedce40bb5959011ee4a996710832') + ), + ( ['3','id_str=βαρ','master_share=5','wallet=test/ref/98831F3A.mmwords'], + (md5_hash_strip,'f7d254798fe2e34b94b5f4ff312998db') + ), + ( ['4','id_str=βαρ','master_share=5','wallet=test/ref/98831F3A.bip39'], + (md5_hash_strip,'d3e479f55792181372a9f32a569c04e5') + ), + ], }, 'Coin': { 'addr2pubhash': {