From 186f223646e6f560acf7b123a87974aafa427e15 Mon Sep 17 00:00:00 2001 From: The MMGen Project Date: Fri, 25 Oct 2019 15:20:07 +0000 Subject: [PATCH] base conversion: rework pad API, forbid empty input and output --- mmgen/exception.py | 4 ++ mmgen/seed.py | 12 ++-- mmgen/tool.py | 2 +- mmgen/util.py | 106 +++++++++++++++++++++---------- test/tooltest2.py | 18 +++--- test/unit_tests_d/ut_baseconv.py | 68 +++++++++++++++----- 6 files changed, 144 insertions(+), 66 deletions(-) diff --git a/mmgen/exception.py b/mmgen/exception.py index ed1922b3..50eb734f 100755 --- a/mmgen/exception.py +++ b/mmgen/exception.py @@ -38,12 +38,16 @@ class UnrecognizedTokenSymbol(Exception): mmcode = 2 class TokenNotInBlockchain(Exception): mmcode = 2 class TokenNotInWallet(Exception): mmcode = 2 class BadTwComment(Exception): mmcode = 2 +class BaseConversionError(Exception): mmcode = 2 +class BaseConversionPadError(Exception): mmcode = 2 # 3: yellow hl, 'MMGen Error' + exception + message class RPCFailure(Exception): mmcode = 3 class BadTxSizeEstimate(Exception): mmcode = 3 class MaxInputSizeExceeded(Exception): mmcode = 3 class WalletFileError(Exception): mmcode = 3 +class HexadecimalStringError(Exception): mmcode = 3 +class SeedLengthError(Exception): mmcode = 3 # 4: red hl, 'MMGen Fatal Error' + exception + message class BadMMGenTxID(Exception): mmcode = 4 diff --git a/mmgen/seed.py b/mmgen/seed.py index 5930229e..79834ffb 100755 --- a/mmgen/seed.py +++ b/mmgen/seed.py @@ -972,7 +972,7 @@ class MMGenSeedFile(SeedSourceUnenc): ext = 'mmseed' def _format(self): - b58seed = baseconv.b58encode(self.seed.data,pad=True) + b58seed = baseconv.b58encode(self.seed.data,pad='seed') self.ssdata.chksum = make_chksum_6(b58seed) self.ssdata.b58seed = b58seed self.fmt_data = '{} {}\n'.format(self.ssdata.chksum,split_into_cols(4,b58seed)) @@ -1000,7 +1000,7 @@ class MMGenSeedFile(SeedSourceUnenc): if not compare_chksums(a,'file',make_chksum_6(b),'computed',verbose=True): return False - ret = baseconv.b58decode(b,pad=True) + ret = baseconv.b58decode(b,pad='seed') if ret == False: msg('Invalid base-58 encoded seed: {}'.format(val)) @@ -1146,8 +1146,8 @@ class Wallet (SeedSourceEnc): def _format(self): d = self.ssdata s = self.seed - slt_fmt = baseconv.b58encode(d.salt,pad=True) - es_fmt = baseconv.b58encode(d.enc_seed,pad=True) + slt_fmt = baseconv.b58encode(d.salt,pad='seed') + es_fmt = baseconv.b58encode(d.enc_seed,pad='seed') lines = ( d.label, '{} {} {} {} {}'.format(s.sid.lower(), d.key_id.lower(), @@ -1205,7 +1205,7 @@ class Wallet (SeedSourceEnc): msg("Hash parameters '{}' don't match hash preset '{}'".format(' '.join(hash_params),d.hash_preset)) return False - lmin,foo,lmax = [v for k,v in baseconv.b58pad_lens] # 22,33,44 + lmin,foo,lmax = sorted(baseconv.seed_pad_lens_rev['b58']) # 22,33,44 for i,key in (4,'salt'),(5,'enc_seed'): l = lines[i].split(' ') chk = l.pop(0) @@ -1219,7 +1219,7 @@ class Wallet (SeedSourceEnc): make_chksum_6(b58_val),'computed checksum',verbose=True): return False - val = baseconv.b58decode(b58_val,pad=True) + val = baseconv.b58decode(b58_val,pad='seed') if val == False: msg('Invalid base 58 number: {}'.format(b58_val)) return False diff --git a/mmgen/tool.py b/mmgen/tool.py index 575736ca..75e497d0 100755 --- a/mmgen/tool.py +++ b/mmgen/tool.py @@ -314,7 +314,7 @@ class MMGenToolCmdUtil(MMGenToolCmdBase): return make_chksum_8( get_data_from_file(infile,dash=True,quiet=True,binary=True)) - def randb58(self,nbytes=32,pad=True): + def randb58(self,nbytes=32,pad=0): "generate random data (default: 32 bytes) and convert it to base 58" return baseconv.b58encode(get_random(nbytes),pad=pad) diff --git a/mmgen/util.py b/mmgen/util.py index b8161982..4a74674e 100755 --- a/mmgen/util.py +++ b/mmgen/util.py @@ -295,8 +295,12 @@ class baseconv(object): 'tirosh': '48f05e1f', # tirosh truncated to mn_base (1626) # 'tirosh1633': '1a5faeff' } - b58pad_lens = [(16,22), (24,33), (32,44)] - b58pad_lens_rev = [(v,k) for k,v in b58pad_lens] + seed_pad_lens = { + 'b58': { 16:22, 24:33, 32:44 }, + } + seed_pad_lens_rev = { + 'b58': { 22:16, 33:24, 44:32 }, + } @classmethod def init_mn(cls,mn_id): @@ -310,31 +314,6 @@ class baseconv(object): else: # bip39 cls.digits[mn_id] = cls.words - @classmethod - def b58encode(cls,s,pad=None): - pad = cls._get_pad(s,pad,'b58encode',cls.b58pad_lens,(bytes,)) - return cls.fromhex(s.hex(),'b58',pad=pad,tostr=True) - - @classmethod - def b58decode(cls,s,pad=None): - pad = cls._get_pad(s,pad,'b58decode',cls.b58pad_lens_rev,(bytes,str)) - return bytes.fromhex(cls.tohex(s,'b58',pad=pad*2 if pad else None)) - - @staticmethod - def _get_pad(s,pad,op_desc,pad_map,ok_types): - if not isinstance(s,ok_types): - m = "{}() input must be one of {}, not '{}'" - raise ValueError(m.format(op_desc,repr([t.__name__ for t in ok_types]),type(s).__name__)) - if pad: - assert type(pad) == bool,"'pad' must be boolean type" - d = dict(pad_map) - if not len(s) in d: - m = 'Invalid data length for {}(pad=True) (must be one of {})' - raise ValueError(m.format(op_desc,repr([e[0] for e in pad_map]))) - return d[len(s)] - else: - return None - @classmethod def get_wordlist(cls,wl_id): cls.init_mn(wl_id) @@ -364,38 +343,97 @@ class baseconv(object): qmsg('List is sorted') if tuple(sorted(wl)) == wl else die(3,'ERROR: List is not sorted!') + @classmethod + def get_pad(cls,pad,seed_pad_func): + """ + 'pad' argument to applicable baseconv methods must be either None, 'seed' or an integer. + If None, output of minimum (but never zero) length will be produced. + If 'seed', output length will be mapped from input length using seed_pad_lens. + If an integer, it refers to the minimum allowable *string length* of the output. + """ + if pad == None: + return 0 + elif isinstance(pad,int) and type(pad) != bool: + return pad + elif pad == 'seed': + return seed_pad_func() + else: + m = "{!r}: illegal value for 'pad' (must be None,'seed' or int)" + raise BaseConversionPadError(m.format(pad)) + @classmethod def tohex(cls,words_arg,wl_id,pad=None): + "convert string or list data of base specified by 'wl_id' to hex string" words = words_arg if isinstance(words_arg,(list,tuple)) else tuple(words_arg.strip()) + if len(words) == 0: + raise BaseConversionError('empty {} data'.format(wl_id)) + + def get_seed_pad(): + assert wl_id in cls.seed_pad_lens_rev,'seed padding not supported for base {!r}'.format(wl_id) + d = cls.seed_pad_lens_rev[wl_id] + if not len(words) in d: + m = '{}: invalid length for seed-padded {} data in base conversion' + raise BaseConversionError(m.format(len(words),wl_id)) + return d[len(words)] * 2 + + pad = max(cls.get_pad(pad,get_seed_pad),2) wl = cls.digits[wl_id] base = len(wl) if not set(words) <= set(wl): - die(2,'{} is not in {} (base{}) format'.format(repr(words_arg),wl_id,base)) + m = '{!r}: not in {} (base{}) format' + raise BaseConversionError(m.format(words_arg,wl_id,base)) deconv = [wl.index(words[::-1][i])*(base**i) for i in range(len(words))] - ret = ('{:0{w}x}'.format(sum(deconv),w=pad or 0)) + ret = ('{:0{w}x}'.format(sum(deconv),w=pad)) return (('','0')[len(ret) % 2] + ret) @classmethod - def fromhex(cls,hexnum,wl_id,pad=None,tostr=False): + def fromhex(cls,hexstr,wl_id,pad=None,tostr=False): + "convert hex string to list or string data of base specified by 'wl_id'" if wl_id in ('mmgen','tirosh','bip39'): assert tostr == False,"'tostr' must be False for '{}'".format(wl_id) - if not is_hex_str(hexnum): - die(2,"{!r}: not a hexadecimal number".format(hexnum)) + if not is_hex_str(hexstr): + m = '{!r}: not a hexadecimal string' + raise HexadecimalStringError(m.format(hexstr)) + if not hexstr: + m = 'empty hex strings not allowed in base conversion' + raise HexadecimalStringError(m) + + def get_seed_pad(): + assert wl_id in cls.seed_pad_lens,'seed padding not supported for base {!r}'.format(wl_id) + d = cls.seed_pad_lens[wl_id] + slen = len(hexstr) // 2 + if not slen in d: + m = '{}: invalid seed byte length for seed-padded base conversion' + raise SeedLengthError(m.format(slen)) + return d[slen] + + pad = max(cls.get_pad(pad,get_seed_pad),1) wl = cls.digits[wl_id] base = len(wl) - num,ret = int(hexnum,16),[] + + num,ret = int(hexstr,16),[] while num: ret.append(num % base) num //= base - o = [wl[n] for n in [0] * ((pad or 0)-len(ret)) + ret[::-1]] + o = [wl[n] for n in [0] * (pad-len(ret)) + ret[::-1]] return ''.join(o) if tostr else o + @classmethod + def b58decode(cls,s,pad=None): + 'convert base58 string to bytes' + return bytes.fromhex(cls.tohex(s,'b58',pad=pad)) + + @classmethod + def b58encode(cls,s,pad=None): + 'convert bytes to base58 string' + return cls.fromhex(s.hex(),'b58',pad=pad,tostr=True) + def match_ext(addr,ext): return addr.split('.')[-1] == ext diff --git a/test/tooltest2.py b/test/tooltest2.py index 8e5f04cd..dab9a775 100755 --- a/test/tooltest2.py +++ b/test/tooltest2.py @@ -163,7 +163,7 @@ tests = { ( ['deadbeef'], 'DPK3PXP' ), ( ['deadbeefdeadbeef'], 'N5LN657PK3PXP' ), ( ['ffffffffffffffff'], 'P777777777777' ), - ( ['0000000000000000'], '' ), + ( ['0000000000000000'], 'A' ), ( ['0000000000000000','pad=10'], 'AAAAAAAAAA' ), ( ['ff','pad=10'], 'AAAAAAAAH7' ), ], @@ -171,7 +171,7 @@ tests = { ( ['DPK3PXP'], 'deadbeef' ), ( ['N5LN657PK3PXP'], 'deadbeefdeadbeef' ), ( ['P777777777777'], 'ffffffffffffffff' ), - ( ['','pad=16'], '0000000000000000' ), + ( ['A','pad=16'], '0000000000000000' ), ( ['AAAAAAAAAA','pad=16'], '0000000000000000' ), ( ['AAAAAAAAH7','pad=2'], 'ff' ), ], @@ -180,7 +180,7 @@ tests = { ( ['deadbeefdeadbeef'], '5CizhNNRPYpBjrbYX' ), ( ['ffffffffffffffff'], '5qCHTcgbQwprzjWrb' ), ( ['0000000000000000'], '111111114FCKVB' ), - ( [''], '3QJmnh' ), + ( ['00'], '1Wh4bh' ), ( ['000000000000000000000000000000000000000000'], '1111111111111111111114oLvT2' ), ], 'b58chktohex': [ @@ -195,7 +195,7 @@ tests = { ( [b'\xde\xad\xbe\xef'], '6h8cQN' ), ( [b'\xde\xad\xbe\xef\xde\xad\xbe\xef'], 'eFGDJURJykA' ), ( [b'\xff\xff\xff\xff\xff\xff\xff\xff'], 'jpXCZedGfVQ' ), - ( [b'\x00\x00\x00\x00\x00\x00\x00\x00'], '' ), + ( [b'\x00\x00\x00\x00\x00\x00\x00\x00'], '1' ), ( [b'\x00\x00\x00\x00\x00\x00\x00\x00','pad=10'], '1111111111' ), ( [b'\xff','pad=10'], '111111115Q' ), ], @@ -203,7 +203,7 @@ tests = { ( ['6h8cQN'], b'\xde\xad\xbe\xef' ), ( ['eFGDJURJykA'], b'\xde\xad\xbe\xef\xde\xad\xbe\xef' ), ( ['jpXCZedGfVQ'], b'\xff\xff\xff\xff\xff\xff\xff\xff' ), - ( ['','pad=16'], b'\x00\x00\x00\x00\x00\x00\x00\x00' ), + ( ['1','pad=16'], b'\x00\x00\x00\x00\x00\x00\x00\x00' ), ( ['1111111111','pad=16'], b'\x00\x00\x00\x00\x00\x00\x00\x00' ), ( ['111111115Q','pad=2'], b'\xff' ), ], @@ -211,7 +211,7 @@ tests = { ( ['deadbeef'], '6h8cQN' ), ( ['deadbeefdeadbeef'], 'eFGDJURJykA' ), ( ['ffffffffffffffff'], 'jpXCZedGfVQ' ), - ( ['0000000000000000'], '' ), + ( ['0000000000000000'], '1' ), ( ['0000000000000000','pad=10'], '1111111111' ), ( ['ff','pad=10'], '111111115Q' ), ], @@ -219,7 +219,7 @@ tests = { ( ['6h8cQN'], 'deadbeef' ), ( ['eFGDJURJykA'], 'deadbeefdeadbeef' ), ( ['jpXCZedGfVQ'], 'ffffffffffffffff' ), - ( ['','pad=16'], '0000000000000000' ), + ( ['1','pad=16'], '0000000000000000' ), ( ['1111111111','pad=16'], '0000000000000000' ), ( ['111111115Q','pad=2'], 'ff' ), ], @@ -274,9 +274,9 @@ tests = { ( ['nbytes=6'], {'boolfunc':is_hex_str,'len':12}, ['-r0'] ), ], 'randb58': [ - ( [], {'boolfunc':is_b58_str,'len':44}, ['-r0'] ), + ( [], {'boolfunc':is_b58_str}, ['-r0'] ), ( ['nbytes=16'], {'boolfunc':is_b58_str,'len':22}, ['-r0'] ), - ( ['nbytes=12','pad=false'], is_b58_str, ['-r0'] ), + ( ['nbytes=12','pad=0'], is_b58_str, ['-r0'] ), ], }, 'Wallet': { diff --git a/test/unit_tests_d/ut_baseconv.py b/test/unit_tests_d/ut_baseconv.py index 27826947..177a6549 100755 --- a/test/unit_tests_d/ut_baseconv.py +++ b/test/unit_tests_d/ut_baseconv.py @@ -10,7 +10,7 @@ class unit_test(object): vectors = { 'b58': ( - (('00',None),''), + (('00',None),'1'), (('00',1),'1'), (('00',2),'11'), (('01',None),'2'), @@ -21,14 +21,20 @@ class unit_test(object): (('0f',2),'1G'), (('deadbeef',None),'6h8cQN'), (('deadbeef',20),'111111111111116h8cQN'), - (('00000000',None),''), + (('00000000',None),'1'), (('00000000',20),'11111111111111111111'), (('ffffffff',None),'7YXq9G'), (('ffffffff',20),'111111111111117YXq9G'), + (('ff'*16,'seed'),'YcVfxkQb6JRzqk5kF2tNLv'), + (('ff'*24,'seed'),'QLbz7JHiBTspS962RLKV8GndWFwiEaqKL'), + (('ff'*32,'seed'),'JEKNVnkbo3jma5nREBBJCDoXFVeKkD56V3xKrvRmWxFG'), + (('00'*16,'seed'),'1111111111111111111111'), + (('00'*24,'seed'),'111111111111111111111111111111111'), + (('00'*32,'seed'),'11111111111111111111111111111111111111111111'), ), # MMGen-flavored base32 using simple base conversion 'b32': ( - (('00',None),''), + (('00',None),'A'), (('00',1),'A'), (('00',2),'AA'), (('01',None),'B'), @@ -39,13 +45,13 @@ class unit_test(object): (('0f',2),'AP'), (('deadbeef',None),'DPK3PXP'), (('deadbeef',20),'AAAAAAAAAAAAADPK3PXP'), - (('00000000',None),''), + (('00000000',None),'A'), (('00000000',20),'AAAAAAAAAAAAAAAAAAAA'), (('ffffffff',None),'D777777'), (('ffffffff',20),'AAAAAAAAAAAAAD777777'), ), 'b16': ( - (('00',None),''), + (('00',None),'0'), (('00',1),'0'), (('00',2),'00'), (('01',None),'1'), @@ -56,13 +62,13 @@ class unit_test(object): (('0f',2),'0f'), (('deadbeef',None),'deadbeef'), (('deadbeef',20),'000000000000deadbeef'), - (('00000000',None),''), + (('00000000',None),'0'), (('00000000',20),'00000000000000000000'), (('ffffffff',None),'ffffffff'), (('ffffffff',20),'000000000000ffffffff'), ), 'b10': ( - (('00',None),''), + (('00',None),'0'), (('00',1),'0'), (('00',2),'00'), (('01',None),'1'), @@ -73,13 +79,13 @@ class unit_test(object): (('0f',2),'15'), (('deadbeef',None),'3735928559'), (('deadbeef',20),'00000000003735928559'), - (('00000000',None),''), + (('00000000',None),'0'), (('00000000',20),'00000000000000000000'), (('ffffffff',None),'4294967295'), (('ffffffff',20),'00000000004294967295'), ), 'b8': ( - (('00',None),''), + (('00',None),'0'), (('00',1),'0'), (('00',2),'00'), (('01',None),'1'), @@ -90,7 +96,7 @@ class unit_test(object): (('0f',2),'17'), (('deadbeef',None),'33653337357'), (('deadbeef',20),'00000000033653337357'), - (('00000000',None),''), + (('00000000',None),'0'), (('00000000',20),'00000000000000000000'), (('ffffffff',None),'37777777777'), (('ffffffff',20),'00000000037777777777'), @@ -106,32 +112,62 @@ class unit_test(object): rerr = "return value ({!r}) does not match reference value ({!r})" qmsg_r('\nChecking hex-to-base conversion:') - fs = " {h:10} {p:6} {r}" for base,data in self.vectors.items(): + fs = " {h:%s} {p:<6} {r}" % max(len(d[0][0]) for d in data) if not opt.verbose: qmsg_r(' {}'.format(base)) vmsg('\nBase: {}'.format(base)) vmsg(fs.format(h='Input',p='Pad',r='Output')) for (hexstr,pad),ret_chk in data: ret = baseconv.fromhex(hexstr,wl_id=base,pad=pad,tostr=True) - assert len(ret) >= (pad or 0), perr.format(ret,pad) + if pad != 'seed': + assert len(ret) >= (pad or 0), perr.format(ret,pad) assert ret == ret_chk, rerr.format(ret,ret_chk) vmsg(fs.format(h=hexstr,r=ret,p=str(pad))) # msg("(('{h}',{p}),'{r}'),".format(h=hexstr,r=ret,c=ret_chk,p=pad)) # msg('') # return True qmsg_r('\nChecking base-to-hex conversion:') - fs = " {h:24} {p:<6} {r}" for base,data in self.vectors.items(): + fs = " {h:%s} {p:<6} {r}" % max(len(d[1]) for d in data) if not opt.verbose: qmsg_r(' {}'.format(base)) vmsg('\nBase: {}'.format(base)) vmsg(fs.format(h='Input',p='Pad',r='Output')) for (hexstr,pad),ret_chk in data: - ret = baseconv.tohex(ret_chk,wl_id=base,pad=len(hexstr)) - assert ret == hexstr, rerr.format(ret,ret_chk) - vmsg(fs.format(h=ret_chk,r=ret,p=len(hexstr))) + if type(pad) == int: + pad = len(hexstr) + ret = baseconv.tohex(ret_chk,wl_id=base,pad=pad) + if pad == None: + assert int(ret,16) == int(hexstr,16), rerr.format(ret,ret_chk) + else: + assert ret == hexstr, rerr.format(ret,ret_chk) + vmsg(fs.format(h=ret_chk,r=ret,p=str(pad))) # msg("(('{h}',{p}),'{r}'),".format(h=hexstr,r=ret_chk,c=ret_chk,p=pad)) qmsg('') + + vmsg('') + qmsg('Checking error handling:') + + b = baseconv + bad_data = ( +('bad hexstr', 'HexadecimalStringError','not a hexadecimal str', lambda:b.fromhex('x','b58')), +('empty hexstr', 'HexadecimalStringError','empty hex strings not', lambda:b.fromhex('','b58')), +('bad b58 data', 'BaseConversionError', 'not in b58', lambda:b.tohex('IfFzZ','b58')), +('empty b58 data', 'BaseConversionError', 'empty b58 data', lambda:b.tohex('','b58')), +('empty b8 data' , 'BaseConversionError', 'empty b8 data', lambda:b.tohex('','b8')), +('bad b32 data', 'BaseConversionError', 'not in b32', lambda:b.tohex('1az','b32')), +('bad pad arg (in)', 'BaseConversionPadError',"illegal value for 'pad'", lambda:b.fromhex('ff','b58',pad='foo')), +('bad pad arg (in)', 'BaseConversionPadError',"illegal value for 'pad'", lambda:b.fromhex('ff','b58',pad=False)), +('bad pad arg (in)', 'BaseConversionPadError',"illegal value for 'pad'", lambda:b.fromhex('ff','b58',pad=True)), +('bad seedlen (in)', 'SeedLengthError', "invalid seed byte length",lambda:b.fromhex('ff','b58',pad='seed')), +('bad pad arg (out)','BaseConversionPadError',"illegal value for 'pad'", lambda:b.tohex('Z','b58',pad='foo')), +('bad pad arg (out)','BaseConversionPadError',"illegal value for 'pad'", lambda:b.tohex('Z','b58',pad=False)), +('bad pad arg (out)','BaseConversionPadError',"illegal value for 'pad'", lambda:b.tohex('Z','b58',pad=True)), +('bad seedlen (out)','BaseConversionError', "invalid length for seed", lambda:b.tohex('Z','b58',pad='seed')), + ) + + ut.process_bad_data(bad_data) + msg('OK') return True