From 9cc0777f5459b12dbec6ef44937f1c2d951ac8c5 Mon Sep 17 00:00:00 2001 From: The MMGen Project Date: Mon, 28 Oct 2019 17:35:45 +0000 Subject: [PATCH] move baseconv class to its own module, baseconv.py --- mmgen/addr.py | 1 + mmgen/baseconv.py | 196 +++++++++++++++++++++++++++++++ mmgen/bip39.py | 3 +- mmgen/protocol.py | 3 +- mmgen/seed.py | 1 + mmgen/tx.py | 1 + mmgen/util.py | 169 -------------------------- setup.py | 1 + test/tooltest2.py | 1 + test/unit_tests_d/ut_baseconv.py | 2 +- 10 files changed, 205 insertions(+), 173 deletions(-) create mode 100755 mmgen/baseconv.py diff --git a/mmgen/addr.py b/mmgen/addr.py index e7229a0a..c54cd016 100755 --- a/mmgen/addr.py +++ b/mmgen/addr.py @@ -23,6 +23,7 @@ addr.py: Address generation/display routines for the MMGen suite from hashlib import sha256,sha512 from mmgen.common import * from mmgen.obj import * +from mmgen.baseconv import * pnm = g.proj_name diff --git a/mmgen/baseconv.py b/mmgen/baseconv.py new file mode 100755 index 00000000..d78ea650 --- /dev/null +++ b/mmgen/baseconv.py @@ -0,0 +1,196 @@ +#!/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 . + +""" +baseconv.py: base conversion class for the MMGen suite +""" + +from hashlib import sha256 +from mmgen.exception import * + +def is_b58_str(s): return set(list(s)) <= set(baseconv.digits['b58']) +def is_b32_str(s): return set(list(s)) <= set(baseconv.digits['b32']) + +class baseconv(object): + + desc = { + 'b58': ('base58', 'base58-encoded data'), + 'b32': ('MMGen base32', 'MMGen base32-encoded data created using simple base conversion'), + 'b16': ('hexadecimal string', 'base16 (hexadecimal) string data'), + 'b10': ('base10 string', 'base10 (decimal) string data'), + 'b8': ('base8 string', 'base8 (octal) string data'), + 'b6d': ('base6d (die roll)', 'base6 data using the digits from one to six'), + 'mmgen': ('MMGen native mnemonic', + 'MMGen native mnemonic seed phrase data created using old Electrum wordlist and simple base conversion'), + } + digits = { + 'b58': tuple('123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'), + 'b32': tuple('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'), + 'b16': tuple('0123456789abcdef'), + 'b10': tuple('0123456789'), + 'b8': tuple('01234567'), + 'b6d': tuple('123456'), + } + mn_base = 1626 # tirosh list is 1633 words long! + mn_ids = ('mmgen','tirosh') + wl_chksums = { + 'mmgen': '5ca31424', + 'tirosh': '48f05e1f', # tirosh truncated to mn_base (1626) + # 'tirosh1633': '1a5faeff' + } + seed_pad_lens = { + 'b58': { 16:22, 24:33, 32:44 }, + 'b6d': { 16:50, 24:75, 32:100 }, + } + seed_pad_lens_rev = { + 'b58': { 22:16, 33:24, 44:32 }, + 'b6d': { 50:16, 75:24, 100:32 }, + } + + @classmethod + def init_mn(cls,mn_id): + assert mn_id in cls.mn_ids + if mn_id == 'mmgen': + from mmgen.mn_electrum import words + cls.digits[mn_id] = words + elif mn_id == 'tirosh': + from mmgen.mn_tirosh import words + cls.digits[mn_id] = words[:cls.mn_base] + else: # bip39 + cls.digits[mn_id] = cls.words + + @classmethod + def get_wordlist(cls,wl_id): + cls.init_mn(wl_id) + return cls.digits[wl_id] + + @classmethod + def get_wordlist_chksum(cls,wl_id): + cls.init_mn(wl_id) + return sha256(' '.join(cls.digits[wl_id]).encode()).hexdigest()[:8] + + @classmethod + def check_wordlists(cls): + for k,v in list(cls.wl_chksums.items()): + res = cls.get_wordlist_chksum(k) + assert res == v,'{}: checksum mismatch for {} (should be {})'.format(res,k,v) + + @classmethod + def check_wordlist(cls,wl_id): + cls.init_mn(wl_id) + + wl = cls.digits[wl_id] + from mmgen.util import qmsg,compare_chksums + qmsg('Wordlist: {}\nLength: {} words'.format(wl_id,len(wl))) + new_chksum = cls.get_wordlist_chksum(wl_id) + + a,b = 'generated','saved' + compare_chksums(new_chksum,a,cls.wl_chksums[wl_id],b,die_on_fail=True) + + 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 baseconv conversion 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 data in seed_pad_lens. + If an integer, the string, hex string or byte output will be padded to this length. + """ + if pad == None: + return 0 + elif type(pad) == int: + 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 'wl_id' to hex string" + return cls.tobytes(words_arg,wl_id,pad//2 if type(pad)==int else pad).hex() + + @classmethod + def tobytes(cls,words_arg,wl_id,pad=None): + "convert string or list data of base 'wl_id' to byte string" + + words = words_arg if isinstance(words_arg,(list,tuple)) else tuple(words_arg.strip()) + desc = cls.desc[wl_id][0] + + if len(words) == 0: + raise BaseConversionError('empty {} data'.format(desc)) + + 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),desc)) + return d[len(words)] + + pad_val = max(cls.get_pad(pad,get_seed_pad),1) + wl = cls.digits[wl_id] + base = len(wl) + + if not set(words) <= set(wl): + m = ('{w!r}:','seed data')[pad=='seed'] + ' not in {d} format' + raise BaseConversionError(m.format(w=words_arg,d=desc)) + + ret = sum([wl.index(words[::-1][i])*(base**i) for i in range(len(words))]) + bl = ret.bit_length() + return ret.to_bytes(max(pad_val,bl//8+bool(bl%8)),'big') + + @classmethod + def fromhex(cls,hexstr,wl_id,pad=None,tostr=False): + "convert hex string to list or string data of base 'wl_id'" + + from mmgen.util import is_hex_str + if not is_hex_str(hexstr): + m = ('{h!r}:','seed data')[pad=='seed'] + ' not a hexadecimal string' + raise HexadecimalStringError(m.format(h=hexstr)) + + return cls.frombytes(bytes.fromhex(hexstr),wl_id,pad,tostr) + + @classmethod + def frombytes(cls,bytestr,wl_id,pad=None,tostr=False): + "convert byte string to list or string data of base 'wl_id'" + + if not bytestr: + raise BaseConversionError('empty data not allowed in base conversion') + + 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] + if not len(bytestr) in d: + m = '{}: invalid byte length for seed data in seed-padded base conversion' + raise SeedLengthError(m.format(len(bytestr))) + return d[len(bytestr)] + + pad = max(cls.get_pad(pad,get_seed_pad),1) + wl = cls.digits[wl_id] + base = len(wl) + + num = int.from_bytes(bytestr,'big') + ret = [] + while num: + ret.append(num % base) + num //= base + o = [wl[n] for n in [0] * (pad-len(ret)) + ret[::-1]] + return ''.join(o) if tostr else o diff --git a/mmgen/bip39.py b/mmgen/bip39.py index 7ea982f9..321c8da5 100755 --- a/mmgen/bip39.py +++ b/mmgen/bip39.py @@ -23,7 +23,8 @@ bip39.py - Data and routines for BIP39 mnemonic seed phrases from hashlib import sha256 from mmgen.exception import * -from mmgen.util import baseconv,is_hex_str +from mmgen.baseconv import * +from mmgen.util import is_hex_str # implements a subset of the baseconv API class bip39(baseconv): diff --git a/mmgen/protocol.py b/mmgen/protocol.py index ad469b76..242e505c 100755 --- a/mmgen/protocol.py +++ b/mmgen/protocol.py @@ -25,6 +25,7 @@ from mmgen.util import msg,ymsg,Msg,ydie from mmgen.obj import MMGenObject,BTCAmt,LTCAmt,BCHAmt,B2XAmt,ETHAmt from mmgen.globalvars import g import mmgen.bech32 as bech32 +from mmgen.baseconv import baseconv,is_b58_str def hash160(hexnum): # take hex, return hex - OP_HASH160 return hashlib.new('ripemd160',hashlib.sha256(bytes.fromhex(hexnum)).digest()).hexdigest() @@ -407,13 +408,11 @@ class MoneroProtocol(DummyWIF,BitcoinProtocolAddrgen): def verify_addr(cls,addr,hex_width,return_dict=False): def b58dec(addr_str): - from mmgen.util import baseconv l = len(addr_str) a = ''.join([baseconv.tohex(addr_str[i*11:i*11+11],'b58',pad=16) for i in range(l//11)]) b = baseconv.tohex(addr_str[-(l%11):],'b58',pad=10) return a + b - from mmgen.util import is_b58_str assert is_b58_str(addr),'Not valid base-58 string' assert len(addr) == cls.addr_width,'Incorrect width' diff --git a/mmgen/seed.py b/mmgen/seed.py index aa9c42e1..b52d0a4d 100755 --- a/mmgen/seed.py +++ b/mmgen/seed.py @@ -25,6 +25,7 @@ import os from mmgen.common import * from mmgen.obj import * from mmgen.crypto import * +from mmgen.baseconv import * pnm = g.proj_name diff --git a/mmgen/tx.py b/mmgen/tx.py index 997bdafe..c4952022 100755 --- a/mmgen/tx.py +++ b/mmgen/tx.py @@ -24,6 +24,7 @@ import sys,os,json from stat import * from mmgen.common import * from mmgen.obj import * +from mmgen.baseconv import * wmsg = lambda k: { 'addr_in_addrfile_only': """ diff --git a/mmgen/util.py b/mmgen/util.py index 27c8ac5e..a1f813bb 100755 --- a/mmgen/util.py +++ b/mmgen/util.py @@ -269,8 +269,6 @@ def is_int(s): def is_hex_str(s): return set(list(s.lower())) <= set(list(hexdigits.lower())) def is_hex_str_lc(s): return set(list(s)) <= set(list(hexdigits.lower())) def is_hex_str_uc(s): return set(list(s)) <= set(list(hexdigits.upper())) -def is_b58_str(s): return set(list(s)) <= set(baseconv.digits['b58']) -def is_b32_str(s): return set(list(s)) <= set(baseconv.digits['b32']) def is_ascii(s,enc='ascii'): try: s.decode(enc) @@ -279,173 +277,6 @@ def is_ascii(s,enc='ascii'): def is_utf8(s): return is_ascii(s,enc='utf8') -class baseconv(object): - - desc = { - 'b58': ('base58', 'base58-encoded data'), - 'b32': ('MMGen base32', 'MMGen base32-encoded data created using simple base conversion'), - 'b16': ('hexadecimal string', 'base16 (hexadecimal) string data'), - 'b10': ('base10 string', 'base10 (decimal) string data'), - 'b8': ('base8 string', 'base8 (octal) string data'), - 'b6d': ('base6d (die roll)', 'base6 data using the digits from one to six'), - 'mmgen': ('MMGen native mnemonic', - 'MMGen native mnemonic seed phrase data created using old Electrum wordlist and simple base conversion'), - } - digits = { - 'b58': tuple('123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'), - 'b32': tuple('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'), - 'b16': tuple('0123456789abcdef'), - 'b10': tuple('0123456789'), - 'b8': tuple('01234567'), - 'b6d': tuple('123456'), - } - mn_base = 1626 # tirosh list is 1633 words long! - mn_ids = ('mmgen','tirosh') - wl_chksums = { - 'mmgen': '5ca31424', - 'tirosh': '48f05e1f', # tirosh truncated to mn_base (1626) - # 'tirosh1633': '1a5faeff' - } - seed_pad_lens = { - 'b58': { 16:22, 24:33, 32:44 }, - 'b6d': { 16:50, 24:75, 32:100 }, - } - seed_pad_lens_rev = { - 'b58': { 22:16, 33:24, 44:32 }, - 'b6d': { 50:16, 75:24, 100:32 }, - } - - @classmethod - def init_mn(cls,mn_id): - assert mn_id in cls.mn_ids - if mn_id == 'mmgen': - from mmgen.mn_electrum import words - cls.digits[mn_id] = words - elif mn_id == 'tirosh': - from mmgen.mn_tirosh import words - cls.digits[mn_id] = words[:cls.mn_base] - else: # bip39 - cls.digits[mn_id] = cls.words - - @classmethod - def get_wordlist(cls,wl_id): - cls.init_mn(wl_id) - return cls.digits[wl_id] - - @classmethod - def get_wordlist_chksum(cls,wl_id): - cls.init_mn(wl_id) - return sha256(' '.join(cls.digits[wl_id]).encode()).hexdigest()[:8] - - @classmethod - def check_wordlists(cls): - for k,v in list(cls.wl_chksums.items()): - res = cls.get_wordlist_chksum(k) - assert res == v,'{}: checksum mismatch for {} (should be {})'.format(res,k,v) - - @classmethod - def check_wordlist(cls,wl_id): - cls.init_mn(wl_id) - - wl = cls.digits[wl_id] - qmsg('Wordlist: {}\nLength: {} words'.format(wl_id,len(wl))) - new_chksum = cls.get_wordlist_chksum(wl_id) - - a,b = 'generated','saved' - compare_chksums(new_chksum,a,cls.wl_chksums[wl_id],b,die_on_fail=True) - - 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 baseconv conversion 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 data in seed_pad_lens. - If an integer, the string, hex string or byte output will be padded to this length. - """ - if pad == None: - return 0 - elif type(pad) == int: - 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 'wl_id' to hex string" - return cls.tobytes(words_arg,wl_id,pad//2 if type(pad)==int else pad).hex() - - @classmethod - def tobytes(cls,words_arg,wl_id,pad=None): - "convert string or list data of base 'wl_id' to byte string" - - words = words_arg if isinstance(words_arg,(list,tuple)) else tuple(words_arg.strip()) - desc = cls.desc[wl_id][0] - - if len(words) == 0: - raise BaseConversionError('empty {} data'.format(desc)) - - 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),desc)) - return d[len(words)] - - pad_val = max(cls.get_pad(pad,get_seed_pad),1) - wl = cls.digits[wl_id] - base = len(wl) - - if not set(words) <= set(wl): - m = ('{w!r}:','seed data')[pad=='seed'] + ' not in {d} format' - raise BaseConversionError(m.format(w=words_arg,d=desc)) - - ret = sum([wl.index(words[::-1][i])*(base**i) for i in range(len(words))]) - bl = ret.bit_length() - return ret.to_bytes(max(pad_val,bl//8+bool(bl%8)),'big') - - @classmethod - def fromhex(cls,hexstr,wl_id,pad=None,tostr=False): - "convert hex string to list or string data of base 'wl_id'" - - if not is_hex_str(hexstr): - m = ('{h!r}:','seed data')[pad=='seed'] + ' not a hexadecimal string' - raise HexadecimalStringError(m.format(h=hexstr)) - - return cls.frombytes(bytes.fromhex(hexstr),wl_id,pad,tostr) - - @classmethod - def frombytes(cls,bytestr,wl_id,pad=None,tostr=False): - "convert byte string to list or string data of base 'wl_id'" - - if not bytestr: - raise BaseConversionError('empty data not allowed in base conversion') - - 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] - if not len(bytestr) in d: - m = '{}: invalid byte length for seed data in seed-padded base conversion' - raise SeedLengthError(m.format(len(bytestr))) - return d[len(bytestr)] - - pad = max(cls.get_pad(pad,get_seed_pad),1) - wl = cls.digits[wl_id] - base = len(wl) - - num = int.from_bytes(bytestr,'big') - ret = [] - while num: - ret.append(num % base) - num //= base - o = [wl[n] for n in [0] * (pad-len(ret)) + ret[::-1]] - return ''.join(o) if tostr else o - def match_ext(addr,ext): return addr.split('.')[-1] == ext diff --git a/setup.py b/setup.py index a19c961b..9ea3d918 100755 --- a/setup.py +++ b/setup.py @@ -83,6 +83,7 @@ setup( 'mmgen.__init__', 'mmgen.addr', 'mmgen.altcoin', + 'mmgen.baseconv', 'mmgen.bech32', 'mmgen.bip39', 'mmgen.color', diff --git a/test/tooltest2.py b/test/tooltest2.py index dab9a775..6694049c 100755 --- a/test/tooltest2.py +++ b/test/tooltest2.py @@ -32,6 +32,7 @@ from mmgen.common import * from test.common import * from mmgen.obj import is_wif,is_coin_addr from mmgen.seed import is_bip39_mnemonic,is_mmgen_mnemonic +from mmgen.baseconv import * NL = ('\n','\r\n')[g.platform=='win'] diff --git a/test/unit_tests_d/ut_baseconv.py b/test/unit_tests_d/ut_baseconv.py index f736a50d..9e853d06 100755 --- a/test/unit_tests_d/ut_baseconv.py +++ b/test/unit_tests_d/ut_baseconv.py @@ -133,7 +133,7 @@ class unit_test(object): msg_r('Testing base conversion routines...') - from mmgen.util import baseconv + from mmgen.baseconv import baseconv perr = "length of {!r} less than pad length ({})" rerr = "return value ({!r}) does not match reference value ({!r})"