diff --git a/mmgen/data/version b/mmgen/data/version index 30205390..f3303951 100644 --- a/mmgen/data/version +++ b/mmgen/data/version @@ -1 +1 @@ -13.1.dev016 +13.1.dev017 diff --git a/mmgen/filename.py b/mmgen/filename.py index 3fcb3912..e7bf69f6 100755 --- a/mmgen/filename.py +++ b/mmgen/filename.py @@ -30,7 +30,7 @@ class Filename(MMGenObject): def __init__(self,fn,base_class=None,subclass=None,proto=None,write=False): """ - 'base_class' - a base class with an 'ext_to_type' method + 'base_class' - a base class with an 'ext_to_cls' method 'subclass' - a subclass with an 'ext' attribute One or the other must be provided, but not both. @@ -52,7 +52,7 @@ class Filename(MMGenObject): die(3,f'Class {(subclass or base_class).__name__!r} does not support the Filename API') if base_class: - subclass = base_class.ext_to_type(self.ext,proto) + subclass = base_class.ext_to_cls( self.ext, proto ) if not subclass: die( 'BadFileExtension', f'{self.ext!r}: not a recognized file extension for {base_class}' ) diff --git a/mmgen/fileutil.py b/mmgen/fileutil.py index 898f8d69..7a1a6861 100755 --- a/mmgen/fileutil.py +++ b/mmgen/fileutil.py @@ -116,9 +116,9 @@ def get_seed_file(cmd_args,nargs,invoked_as=None): from .opts import opt from .filename import find_file_in_dir - from .wallet import MMGenWallet + from .wallet.mmgen import wallet - wf = find_file_in_dir(MMGenWallet,g.data_dir) + wf = find_file_in_dir(wallet,g.data_dir) wd_from_opt = bool(opt.hidden_incog_input_params or opt.in_fmt) # have wallet data from opt? diff --git a/mmgen/help.py b/mmgen/help.py index 2334386e..7df0d348 100755 --- a/mmgen/help.py +++ b/mmgen/help.py @@ -81,8 +81,8 @@ def help_notes_func(proto,po,k): ]) def fmt_codes(): - from .wallet import Wallet - return '\n '.join( Wallet.format_fmt_codes().splitlines() ) + from .wallet import format_fmt_codes + return '\n '.join( format_fmt_codes().splitlines() ) def coin_id(): return proto.coin_id diff --git a/mmgen/main_wallet.py b/mmgen/main_wallet.py index c13b2168..07451c37 100755 --- a/mmgen/main_wallet.py +++ b/mmgen/main_wallet.py @@ -22,8 +22,7 @@ mmgen/main_wallet: Entry point for MMGen wallet-related scripts import os from .common import * -from .wallet import Wallet,MMGenWallet -from .filename import find_file_in_dir +from .wallet import Wallet,get_wallet_cls usage = '[opts] [infile]' nargs = 1 @@ -234,7 +233,7 @@ if invoked_as == 'passchg' and ss_in.infile.dirname == g.data_dir: elif invoked_as == 'gen' and not opt.outdir and not opt.stdout: from .filename import find_file_in_dir if ( - not find_file_in_dir( MMGenWallet, g.data_dir ) + not find_file_in_dir( get_wallet_cls('mmgen'), g.data_dir ) and keypress_confirm( 'Make this wallet your default and move it to the data directory?', default_yes = True ) ): diff --git a/mmgen/opts.py b/mmgen/opts.py index 01e5af1e..c05f2b5a 100755 --- a/mmgen/opts.py +++ b/mmgen/opts.py @@ -469,23 +469,23 @@ def check_usr_opts(usr_opts): # Raises an exception if any check fails end )) def chk_in_fmt(key,val,desc): - from .wallet import Wallet,IncogWallet,Brainwallet,IncogWalletHidden - sstype = Wallet.fmt_code_to_type(val) - if not sstype: + from .wallet import get_wallet_data + wd = get_wallet_data(fmt_code=val) + if not wd: opt_unrecognized(key,val) if key == 'out_fmt': p = 'hidden_incog_output_params' - if sstype == IncogWalletHidden and not getattr(opt,p): + if wd.type == 'incog_hidden' and not getattr(opt,p): die( 'UserOptError', 'Hidden incog format output requested. ' + f'You must supply a file and offset with the {fmt_opt(p)!r} option' ) - if issubclass(sstype,IncogWallet) and opt.old_incog_fmt: + if wd.base_type == 'incog_base' and opt.old_incog_fmt: opt_display(key,val,beg='Selected',end=' ') opt_display('old_incog_fmt',beg='conflicts with',end=':\n') die( 'UserOptError', 'Export to old incog wallet format unsupported' ) - elif issubclass(sstype,Brainwallet): + elif wd.type == 'brain': die( 'UserOptError', 'Output to brainwallet format unsupported' ) chk_out_fmt = chk_in_fmt @@ -515,8 +515,9 @@ def check_usr_opts(usr_opts): # Raises an exception if any check fails if hasattr(opt,key2): val2 = getattr(opt,key2) - from .wallet import IncogWalletHidden - if val2 and val2 not in IncogWalletHidden.fmt_codes: + from .wallet import get_wallet_data + wd = get_wallet_data('incog_hidden') + if val2 and val2 not in wd.fmt_codes: die( 'UserOptError', f'Option conflict:\n {fmt_opt(key)}, with\n {fmt_opt(key2)}={val2}' ) chk_hidden_incog_output_params = chk_hidden_incog_input_params diff --git a/mmgen/tx/__init__.py b/mmgen/tx/__init__.py index 6cfbd7b7..2e5fffa4 100755 --- a/mmgen/tx/__init__.py +++ b/mmgen/tx/__init__.py @@ -44,7 +44,7 @@ def _get_cls_info(clsname,modname,args,kwargs): from ..util import get_extension from .completed import Completed ext = get_extension( kwargs['filename'] ) - cls = Completed.ext_to_type( ext, proto ) + cls = Completed.ext_to_cls( ext, proto ) if not cls: die(1,f'{ext!r}: unrecognized file extension for CompletedTX') clsname = cls.__name__ diff --git a/mmgen/tx/completed.py b/mmgen/tx/completed.py index 31e8dd2a..570ddddf 100755 --- a/mmgen/tx/completed.py +++ b/mmgen/tx/completed.py @@ -53,7 +53,7 @@ class Completed(Base): return MMGenTxFile(self) @classmethod - def ext_to_type(cls,ext,proto): + def ext_to_cls(cls,ext,proto): """ see twctl:import_token() """ diff --git a/mmgen/txsign.py b/mmgen/txsign.py index c262a12c..7fd02333 100755 --- a/mmgen/txsign.py +++ b/mmgen/txsign.py @@ -24,7 +24,7 @@ from .common import * from .obj import MMGenList from .addr import MMGenAddrType from .addrlist import AddrIdxList,KeyAddrList -from .wallet import Wallet,WalletUnenc,WalletEnc,MMGenWallet +from .wallet import Wallet,get_wallet_extensions,get_wallet_cls saved_seeds = {} @@ -117,13 +117,12 @@ def get_tx_files(opt,args): def get_seed_files(opt,args): # favor unencrypted seed sources first, as they don't require passwords - u,e = WalletUnenc,WalletEnc - ret = _pop_matching_fns(args,u.get_extensions()) + ret = _pop_matching_fns( args, get_wallet_extensions('unenc') ) from .filename import find_file_in_dir - wf = find_file_in_dir(MMGenWallet,g.data_dir) # Make this the first encrypted ss in the list + wf = find_file_in_dir(get_wallet_cls('mmgen'),g.data_dir) # Make this the first encrypted ss in the list if wf: ret.append(wf) - ret += _pop_matching_fns(args,e.get_extensions()) + ret += _pop_matching_fns( args, get_wallet_extensions('enc') ) if not (ret or opt.mmgen_keys_from_file or opt.keys_from_file): # or opt.use_wallet_dat die(1,'You must specify a seed or key source!') return ret diff --git a/mmgen/util.py b/mmgen/util.py index cf8d155c..d7714eab 100755 --- a/mmgen/util.py +++ b/mmgen/util.py @@ -453,9 +453,8 @@ def compare_or_die(val1, desc1, val2, desc2, e='Error'): return True def check_wallet_extension(fn): - from .wallet import Wallet - if not Wallet.ext_to_type(get_extension(fn)): - die( 'BadFileExtension', f'{fn!r}: unrecognized seed source file extension' ) + from .wallet import get_wallet_data + get_wallet_data( ext=get_extension(fn), die_on_fail=True ) # raises exception on failure def make_full_path(outdir,outfile): return os.path.normpath(os.path.join(outdir, os.path.basename(outfile))) diff --git a/mmgen/wallet.py b/mmgen/wallet.py deleted file mode 100755 index 224961e2..00000000 --- a/mmgen/wallet.py +++ /dev/null @@ -1,1176 +0,0 @@ -#!/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 . - -""" -wallet.py: Wallet classes and methods for the MMGen suite -""" - -import os - -from .common import * -from .obj import * -from .baseconv import * -from .seed import Seed -import mmgen.crypto as crypto - -def check_usr_seed_len(seed_len): - if opt.seed_len and opt.seed_len != seed_len: - die(1,f'ERROR: requested seed length ({opt.seed_len}) doesn’t match seed length of source ({seed_len})') - -class WalletMeta(type): - wallet_classes = set() # one-instance class, so store data in class attr - def __init__(cls,name,bases,namespace): - cls.wallet_classes.add(cls) - cls.wallet_classes -= set(bases) - -class Wallet(MMGenObject,metaclass=WalletMeta): - - desc = g.proj_name + ' seed source' - file_mode = 'text' - filename_api = True - stdin_ok = False - ask_tty = True - no_tty = False - op = None - _msg = {} - - class WalletData(MMGenObject): pass - - def __new__(cls, - fn = None, - ss = None, - seed_bin = None, - seed = None, - passchg = False, - in_data = None, - ignore_in_fmt = False, - in_fmt = None, - passwd_file = None ): - - in_fmt = in_fmt or opt.in_fmt - - if opt.out_fmt: - out_cls = cls.fmt_code_to_type(opt.out_fmt) - if not out_cls: - die(1,f'{opt.out_fmt!r}: unrecognized output format') - else: - out_cls = None - - def die_on_opt_mismatch(opt,sstype): - compare_or_die( - cls.fmt_code_to_type(opt).__name__, 'input format requested on command line', - sstype.__name__, 'input file format' ) - - if seed or seed_bin: - me = super(cls,cls).__new__(out_cls or MMGenWallet) # default to MMGenWallet - me.seed = seed or Seed(seed_bin=seed_bin) - me.op = 'new' - elif ss: - me = super(cls,cls).__new__((ss.__class__ if passchg else out_cls) or MMGenWallet) - me.seed = ss.seed - me.ss_in = ss - me.op = ('conv','pwchg_new')[bool(passchg)] - elif fn or opt.hidden_incog_input_params: - from .filename import Filename - if fn: - f = Filename(fn,base_class=cls) - else: - # permit comma in filename - fn = ','.join(opt.hidden_incog_input_params.split(',')[:-1]) - f = Filename(fn,subclass=IncogWalletHidden) - if in_fmt and not ignore_in_fmt: - die_on_opt_mismatch(in_fmt,f.subclass) - me = super(cls,cls).__new__(f.subclass) - me.infile = f - me.op = ('old','pwchg_old')[bool(passchg)] - elif in_fmt: - me = super(cls,cls).__new__(cls.fmt_code_to_type(in_fmt)) - me.op = ('old','pwchg_old')[bool(passchg)] - else: # called with no arguments: initialize with random seed - me = super(cls,cls).__new__(out_cls or MMGenWallet) - me.seed = Seed(None) - me.op = 'new' - - return me - - def __init__(self, - fn = None, - ss = None, - seed_bin = None, - seed = None, - passchg = False, - in_data = None, - ignore_in_fmt = False, - in_fmt = None, - passwd_file = None ): - - self.passwd_file = passwd_file or opt.passwd_file - self.ssdata = self.WalletData() - self.msg = {} - self.in_data = in_data - - for c in reversed(self.__class__.__mro__): - if hasattr(c,'_msg'): - self.msg.update(c._msg) - - if hasattr(self,'seed'): - self._encrypt() - return - elif hasattr(self,'infile') or self.in_data or not g.stdin_tty: - self._deformat_once() - self._decrypt_retry() - else: - if not self.stdin_ok: - die(1,f'Reading from standard input not supported for {self.desc} format') - self._deformat_retry() - self._decrypt_retry() - - qmsg('Valid {} for Seed ID {}{}'.format( - self.desc, - self.seed.sid.hl(), - (f', seed length {self.seed.bitlen}' if self.seed.bitlen != 256 else '') - )) - - def _get_data(self): - if hasattr(self,'infile'): - from .fileutil import get_data_from_file - self.fmt_data = get_data_from_file(self.infile.name,self.desc,binary=self.file_mode=='binary') - elif self.in_data: - self.fmt_data = self.in_data - else: - self.fmt_data = self._get_data_from_user(self.desc) - - def _get_data_from_user(self,desc): - return get_data_from_user(desc) - - def _deformat_once(self): - self._get_data() - if not self._deformat(): - die(2,'Invalid format for input data') - - def _deformat_retry(self): - while True: - self._get_data() - if self._deformat(): - break - msg('Trying again...') - - def _decrypt_retry(self): - while True: - if self._decrypt(): - break - if self.passwd_file: - die(2,'Passphrase from password file, so exiting') - msg('Trying again...') - - @classmethod - def get_extensions(cls): - return [c.ext for c in cls.wallet_classes if hasattr(c,'ext')] - - @classmethod - def fmt_code_to_type(cls,fmt_code): - if fmt_code: - for c in cls.wallet_classes: - if fmt_code in getattr(c,'fmt_codes',[]): - return c - return None - - @classmethod - def ext_to_type(cls,ext,proto=None): - for c in cls.wallet_classes: - if ext == getattr(c,'ext',None): - return c - - @classmethod - def format_fmt_codes(cls): - d = [(c.__name__,('.'+c.ext if c.ext else str(c.ext)),','.join(c.fmt_codes)) - for c in cls.wallet_classes - if hasattr(c,'fmt_codes')] - w = max(len(i[0]) for i in d) - ret = [f'{a:<{w}} {b:<9} {c}' for a,b,c in [ - ('Format','FileExt','Valid codes'), - ('------','-------','-----------') - ] + sorted(d)] - return '\n'.join(ret) + ('','-α')[g.debug_utf8] + '\n' - - def get_fmt_data(self): - self._format() - return self.fmt_data - - def write_to_file(self,outdir='',desc=''): - self._format() - kwargs = { - 'desc': desc or self.desc, - 'ask_tty': self.ask_tty, - 'no_tty': self.no_tty, - 'binary': self.file_mode == 'binary' - } - # write_data_to_file(): outfile with absolute path overrides opt.outdir - if outdir: - of = os.path.abspath(os.path.join(outdir,self._filename())) - from .fileutil import write_data_to_file - write_data_to_file(of if outdir else self._filename(),self.fmt_data,**kwargs) - -class WalletUnenc(Wallet): - - def _decrypt_retry(self): pass - def _encrypt(self): pass - - def _filename(self): - s = self.seed - return '{}[{}]{x}.{}'.format( - s.fn_stem, - s.bitlen, - self.ext, - x='-α' if g.debug_utf8 else '') - - def _choose_seedlen(self,desc,ok_lens,subtype): - - from .term import get_char - def choose_len(): - prompt = self.choose_seedlen_prompt - while True: - r = get_char('\r'+prompt) - if is_int(r) and 1 <= int(r) <= len(ok_lens): - break - msg_r(('\r','\n')[g.test_suite] + ' '*len(prompt) + '\r') - return ok_lens[int(r)-1] - - msg('{} {}'.format( - blue(f'{capfirst(desc)} type:'), - yellow(subtype) - )) - - while True: - usr_len = choose_len() - prompt = self.choose_seedlen_confirm.format(usr_len) - if keypress_confirm(prompt,default_yes=True,no_nl=not g.test_suite): - return usr_len - -class WalletEnc(Wallet): - - _msg = { - 'choose_passphrase': """ - You must choose a passphrase to encrypt your new {} with. - A key will be generated from your passphrase using a hash preset of '{}'. - Please note that no strength checking of passphrases is performed. - For an empty passphrase, just hit ENTER twice. - """ - } - - def _get_hash_preset_from_user(self,hp,add_desc=''): - prompt = 'Enter {}hash preset for {}{}{},\nor hit ENTER to {} value ({!r}): '.format( - ('old ' if self.op=='pwchg_old' else 'new ' if self.op=='pwchg_new' else ''), - ('','new ')[self.op=='new'], - self.desc, - ('',' '+add_desc)[bool(add_desc)], - ('accept the default','reuse the old')[self.op=='pwchg_new'], - hp ) - return crypto.get_hash_preset_from_user( hash_preset=hp, prompt=prompt ) - - def _get_hash_preset(self,add_desc=''): - if hasattr(self,'ss_in') and hasattr(self.ss_in.ssdata,'hash_preset'): - old_hp = self.ss_in.ssdata.hash_preset - if opt.keep_hash_preset: - hp = old_hp - qmsg(f'Reusing hash preset {hp!r} at user request') - elif opt.hash_preset: - hp = opt.hash_preset - qmsg(f'Using hash preset {hp!r} requested on command line') - else: # Prompt, using old value as default - hp = self._get_hash_preset_from_user(old_hp,add_desc) - if (not opt.keep_hash_preset) and self.op == 'pwchg_new': - qmsg('Hash preset {}'.format( 'unchanged' if hp == old_hp else f'changed to {hp!r}' )) - elif opt.hash_preset: - hp = opt.hash_preset - qmsg(f'Using hash preset {hp!r} requested on command line') - else: - hp = self._get_hash_preset_from_user(g.dfl_hash_preset,add_desc) - self.ssdata.hash_preset = hp - - def _get_new_passphrase(self): - self.ssdata.passwd = crypto.get_new_passphrase( - data_desc = ('new ' if self.op in ('new','conv') else '') + self.desc, - hash_preset = self.ssdata.hash_preset, - passwd_file = self.passwd_file, - pw_desc = ('new ' if self.op=='pwchg_new' else '') + 'passphrase' ) - return self.ssdata.passwd - - def _get_passphrase(self,add_desc=''): - self.ssdata.passwd = crypto.get_passphrase( - data_desc = self.desc + (f' {add_desc}' if add_desc else ''), - passwd_file = self.passwd_file, - pw_desc = ('old ' if self.op == 'pwchg_old' else '') + 'passphrase' ) - - def _get_first_pw_and_hp_and_encrypt_seed(self): - d = self.ssdata - self._get_hash_preset() - - if hasattr(self,'ss_in') and hasattr(self.ss_in.ssdata,'passwd'): - old_pw = self.ss_in.ssdata.passwd - if opt.keep_passphrase: - d.passwd = old_pw - qmsg('Reusing passphrase at user request') - else: - pw = self._get_new_passphrase() - if self.op == 'pwchg_new': - qmsg('Passphrase {}'.format( 'unchanged' if pw == old_pw else 'changed' )) - else: - self._get_new_passphrase() - - from hashlib import sha256 - d.salt = sha256( crypto.get_random(128) ).digest()[:crypto.salt_len] - key = crypto.make_key( d.passwd, d.salt, d.hash_preset ) - d.key_id = make_chksum_8(key) - d.enc_seed = crypto.encrypt_seed( self.seed.data, key ) - -class Mnemonic(WalletUnenc): - - stdin_ok = True - wclass = 'mnemonic' - conv_cls = baseconv - choose_seedlen_prompt = 'Choose a mnemonic length: 1) 12 words, 2) 18 words, 3) 24 words: ' - choose_seedlen_confirm = 'Mnemonic length of {} words chosen. OK?' - - @property - def mn_lens(self): - return sorted(self.conv_cls(self.wl_id).seedlen_map_rev) - - def _get_data_from_user(self,desc): - - if not g.stdin_tty: - return get_data_from_user(desc) - - from .mn_entry import mn_entry # import here to catch cfg var errors - mn_len = self._choose_seedlen(self.wclass,self.mn_lens,self.mn_type) - return mn_entry(self.wl_id).get_mnemonic_from_user(mn_len) - - def _format(self): - - hexseed = self.seed.hexdata - - bc = self.conv_cls(self.wl_id) - mn = bc.fromhex( hexseed, 'seed' ) - rev = bc.tohex( mn, 'seed' ) - - # Internal error, so just die on fail - compare_or_die( rev, 'recomputed seed', hexseed, 'original', e='Internal error' ) - - self.ssdata.mnemonic = mn - self.fmt_data = ' '.join(mn) + '\n' - - def _deformat(self): - - bc = self.conv_cls(self.wl_id) - mn = self.fmt_data.split() - - if len(mn) not in self.mn_lens: - msg('Invalid mnemonic ({} words). Valid numbers of words: {}'.format( - len(mn), - ', '.join(map(str,self.mn_lens)) )) - return False - - for n,w in enumerate(mn,1): - if w not in bc.digits: - msg(f'Invalid mnemonic: word #{n} is not in the {self.wl_id.upper()} wordlist') - return False - - hexseed = bc.tohex( mn, 'seed' ) - rev = bc.fromhex( hexseed, 'seed' ) - - if len(hexseed) * 4 not in Seed.lens: - msg('Invalid mnemonic (produces too large a number)') - return False - - # Internal error, so just die - compare_or_die( ' '.join(rev), 'recomputed mnemonic', ' '.join(mn), 'original', e='Internal error' ) - - self.seed = Seed(bytes.fromhex(hexseed)) - self.ssdata.mnemonic = mn - - check_usr_seed_len(self.seed.bitlen) - - return True - -class MMGenMnemonic(Mnemonic): - - fmt_codes = ('mmwords','words','mnemonic','mnem','mn','m') - desc = 'MMGen native mnemonic data' - mn_type = 'MMGen native' - ext = 'mmwords' - wl_id = 'mmgen' - -class BIP39Mnemonic(Mnemonic): - - fmt_codes = ('bip39',) - desc = 'BIP39 mnemonic data' - mn_type = 'BIP39' - ext = 'bip39' - wl_id = 'bip39' - - def __init__(self,*args,**kwargs): - from .bip39 import bip39 - self.conv_cls = bip39 - super().__init__(*args,**kwargs) - -class MMGenSeedFile(WalletUnenc): - - stdin_ok = True - fmt_codes = ('mmseed','seed','s') - desc = 'seed data' - ext = 'mmseed' - - def _format(self): - b58seed = baseconv('b58').frombytes(self.seed.data,pad='seed',tostr=True) - self.ssdata.chksum = make_chksum_6(b58seed) - self.ssdata.b58seed = b58seed - self.fmt_data = '{} {}\n'.format( - self.ssdata.chksum, - split_into_cols(4,b58seed) ) - - def _deformat(self): - desc = self.desc - ld = self.fmt_data.split() - - if not (7 <= len(ld) <= 12): # 6 <= padded b58 data (ld[1:]) <= 11 - msg(f'Invalid data length ({len(ld)}) in {desc}') - return False - - a,b = ld[0],''.join(ld[1:]) - - if not is_chksum_6(a): - msg(f'{a!r}: invalid checksum format in {desc}') - return False - - if not is_b58_str(b): - msg(f'{b!r}: not a base 58 string, in {desc}') - return False - - vmsg_r(f'Validating {desc} checksum...') - - if not compare_chksums(a,'file',make_chksum_6(b),'computed',verbose=True): - return False - - ret = baseconv('b58').tobytes(b,pad='seed') - - if ret == False: - msg(f'Invalid base-58 encoded seed: {val}') - return False - - self.seed = Seed(ret) - self.ssdata.chksum = a - self.ssdata.b58seed = b - - check_usr_seed_len(self.seed.bitlen) - - return True - -class DieRollSeedFile(WalletUnenc): - - stdin_ok = True - fmt_codes = ('b6d','die','dieroll') - desc = 'base6d die roll seed data' - ext = 'b6d' - conv_cls = baseconv - wclass = 'dieroll' - wl_id = 'b6d' - mn_type = 'base6d' - choose_seedlen_prompt = 'Choose a seed length: 1) 128 bits, 2) 192 bits, 3) 256 bits: ' - choose_seedlen_confirm = 'Seed length of {} bits chosen. OK?' - user_entropy_prompt = 'Would you like to provide some additional entropy from the keyboard?' - interactive_input = False - - def _format(self): - d = baseconv('b6d').frombytes(self.seed.data,pad='seed',tostr=True) + '\n' - self.fmt_data = block_format(d,gw=5,cols=5) - - def _deformat(self): - - d = remove_whitespace(self.fmt_data) - bc = baseconv('b6d') - rmap = bc.seedlen_map_rev - - if not len(d) in rmap: - die( 'SeedLengthError', '{!r}: invalid length for {} (must be one of {})'.format( - len(d), - self.desc, - list(rmap) )) - - # truncate seed to correct length, discarding high bits - seed_len = rmap[len(d)] - seed_bytes = bc.tobytes( d, pad='seed' )[-seed_len:] - - if self.interactive_input and opt.usr_randchars: - if keypress_confirm(self.user_entropy_prompt): - seed_bytes = crypto.add_user_random( - rand_bytes = seed_bytes, - desc = 'gathered from your die rolls' ) - self.desc += ' plus user-supplied entropy' - - self.seed = Seed(seed_bytes) - self.ssdata.hexseed = seed_bytes.hex() - - check_usr_seed_len(self.seed.bitlen) - return True - - def _get_data_from_user(self,desc): - - if not g.stdin_tty: - return get_data_from_user(desc) - - bc = baseconv('b6d') - - seed_bitlens = [ n*8 for n in sorted(bc.seedlen_map) ] - seed_bitlen = self._choose_seedlen( self.wclass, seed_bitlens, self.mn_type ) - nDierolls = bc.seedlen_map[seed_bitlen // 8] - - m = """ - For a {sb}-bit seed you must roll the die {nd} times. After each die roll, - enter the result on the keyboard as a digit. If you make an invalid entry, - you'll be prompted to re-enter it. - """ - msg('\n'+fmt(m.strip()).format(sb=seed_bitlen,nd=nDierolls)+'\n') - - CUR_HIDE = '\033[?25l' - CUR_SHOW = '\033[?25h' - cr = '\n' if g.test_suite else '\r' - prompt_fs = f'\b\b\b {cr}Enter die roll #{{}}: {CUR_SHOW}' - clear_line = '' if g.test_suite else '\r' + ' ' * 25 - invalid_msg = CUR_HIDE + cr + 'Invalid entry' + ' ' * 11 - - from .term import get_char - def get_digit(n): - p = prompt_fs - sleep = g.short_disp_timeout - while True: - ch = get_char(p.format(n),num_chars=1,sleep=sleep) - if ch in bc.digits: - msg_r(CUR_HIDE + ' OK') - return ch - else: - msg_r(invalid_msg) - sleep = g.err_disp_timeout - p = clear_line + prompt_fs - - dierolls,n = [],1 - while len(dierolls) < nDierolls: - dierolls.append(get_digit(n)) - n += 1 - - msg('Die rolls successfully entered' + CUR_SHOW) - self.interactive_input = True - - return ''.join(dierolls) - -class PlainHexSeedFile(WalletUnenc): - - stdin_ok = True - fmt_codes = ('hex','rawhex','plainhex') - desc = 'plain hexadecimal seed data' - ext = 'hex' - - def _format(self): - self.fmt_data = self.seed.hexdata + '\n' - - def _deformat(self): - desc = self.desc - d = self.fmt_data.strip() - - if not is_hex_str_lc(d): - msg(f'{d!r}: not a lowercase hexadecimal string, in {desc}') - return False - - if not len(d)*4 in Seed.lens: - msg(f'Invalid data length ({len(d)}) in {desc}') - return False - - self.seed = Seed(bytes.fromhex(d)) - self.ssdata.hexseed = d - - check_usr_seed_len(self.seed.bitlen) - - return True - -class MMGenHexSeedFile(WalletUnenc): - - stdin_ok = True - fmt_codes = ('seedhex','hexseed','mmhex') - desc = 'hexadecimal seed data with checksum' - ext = 'mmhex' - - def _format(self): - h = self.seed.hexdata - self.ssdata.chksum = make_chksum_6(h) - self.ssdata.hexseed = h - self.fmt_data = '{} {}\n'.format( - self.ssdata.chksum, - split_into_cols(4,h) ) - - def _deformat(self): - desc = self.desc - d = self.fmt_data.split() - try: - d[1] - chk,hstr = d[0],''.join(d[1:]) - except: - msg(f'{self.fmt_data.strip()!r}: invalid {desc}') - return False - - if not len(hstr)*4 in Seed.lens: - msg(f'Invalid data length ({len(hstr)}) in {desc}') - return False - - if not is_chksum_6(chk): - msg(f'{chk!r}: invalid checksum format in {desc}') - return False - - if not is_hex_str(hstr): - msg(f'{hstr!r}: not a hexadecimal string, in {desc}') - return False - - vmsg_r(f'Validating {desc} checksum...') - - if not compare_chksums(chk,'file',make_chksum_6(hstr),'computed',verbose=True): - return False - - self.seed = Seed(bytes.fromhex(hstr)) - self.ssdata.chksum = chk - self.ssdata.hexseed = hstr - - check_usr_seed_len(self.seed.bitlen) - - return True - -class MMGenWallet(WalletEnc): - - fmt_codes = ('wallet','w') - desc = g.proj_name + ' wallet' - ext = 'mmdat' - - def __init__(self,*args,**kwargs): - if opt.label: - self.label = MMGenWalletLabel( - opt.label, - msg = "Error in option '--label'" ) - else: - self.label = None - super().__init__(*args,**kwargs) - - # logic identical to _get_hash_preset_from_user() - def _get_label_from_user(self,old_lbl=''): - prompt = 'Enter a wallet label, or hit ENTER {}: '.format( - 'to reuse the label {}'.format(old_lbl.hl(encl="''")) if old_lbl else - 'for no label' ) - while True: - ret = line_input(prompt) - if ret: - lbl = get_obj(MMGenWalletLabel,s=ret) - if lbl: - return lbl - else: - msg('Invalid label. Trying again...') - else: - return old_lbl or MMGenWalletLabel('No Label') - - # logic identical to _get_hash_preset() - def _get_label(self): - if hasattr(self,'ss_in') and hasattr(self.ss_in.ssdata,'label'): - old_lbl = self.ss_in.ssdata.label - if opt.keep_label: - lbl = old_lbl - qmsg('Reusing label {} at user request'.format( lbl.hl(encl="''") )) - elif self.label: - lbl = self.label - qmsg('Using label {} requested on command line'.format( lbl.hl(encl="''") )) - else: # Prompt, using old value as default - lbl = self._get_label_from_user(old_lbl) - if (not opt.keep_label) and self.op == 'pwchg_new': - qmsg('Label {}'.format( 'unchanged' if lbl == old_lbl else f'changed to {lbl!r}' )) - elif self.label: - lbl = self.label - qmsg('Using label {} requested on command line'.format( lbl.hl(encl="''") )) - else: - lbl = self._get_label_from_user() - self.ssdata.label = lbl - - def _encrypt(self): - self._get_first_pw_and_hp_and_encrypt_seed() - self._get_label() - d = self.ssdata - d.pw_status = ('NE','E')[len(d.passwd)==0] - d.timestamp = make_timestamp() - - def _format(self): - d = self.ssdata - s = self.seed - bc = baseconv('b58') - slt_fmt = bc.frombytes(d.salt,pad='seed',tostr=True) - es_fmt = bc.frombytes(d.enc_seed,pad='seed',tostr=True) - lines = ( - d.label, - '{} {} {} {} {}'.format( s.sid.lower(), d.key_id.lower(), s.bitlen, d.pw_status, d.timestamp ), - '{}: {} {} {}'.format( d.hash_preset, *crypto.get_hash_params(d.hash_preset) ), - '{} {}'.format( make_chksum_6(slt_fmt), split_into_cols(4,slt_fmt) ), - '{} {}'.format( make_chksum_6(es_fmt), split_into_cols(4,es_fmt) ) - ) - chksum = make_chksum_6(' '.join(lines).encode()) - self.fmt_data = '\n'.join((chksum,)+lines) + '\n' - - def _deformat(self): - - def check_master_chksum(lines,desc): - - if len(lines) != 6: - msg(f'Invalid number of lines ({len(lines)}) in {desc} data') - return False - - if not is_chksum_6(lines[0]): - msg(f'Incorrect master checksum ({lines[0]}) in {desc} data') - return False - - chk = make_chksum_6(' '.join(lines[1:])) - if not compare_chksums(lines[0],'master',chk,'computed', - hdr='For wallet master checksum',verbose=True): - return False - - return True - - lines = self.fmt_data.splitlines() - if not check_master_chksum(lines,self.desc): - return False - - d = self.ssdata - d.label = MMGenWalletLabel(lines[1]) - - d1,d2,d3,d4,d5 = lines[2].split() - d.seed_id = d1.upper() - d.key_id = d2.upper() - check_usr_seed_len(int(d3)) - d.pw_status,d.timestamp = d4,d5 - - hpdata = lines[3].split() - - d.hash_preset = hp = hpdata[0][:-1] # a string! - qmsg(f'Hash preset of wallet: {hp!r}') - if opt.hash_preset and opt.hash_preset != hp: - qmsg(f'Warning: ignoring user-requested hash preset {opt.hash_preset!r}') - - hash_params = tuple(map(int,hpdata[1:])) - - if hash_params != crypto.get_hash_params(d.hash_preset): - msg(f'Hash parameters {" ".join(hash_params)!r} don’t match hash preset {d.hash_preset!r}') - return False - - lmin,foo,lmax = sorted(baseconv('b58').seedlen_map_rev) # 22,33,44 - for i,key in (4,'salt'),(5,'enc_seed'): - l = lines[i].split(' ') - chk = l.pop(0) - b58_val = ''.join(l) - - if len(b58_val) < lmin or len(b58_val) > lmax: - msg(f'Invalid format for {key} in {self.desc}: {l}') - return False - - if not compare_chksums(chk,key, - make_chksum_6(b58_val),'computed checksum',verbose=True): - return False - - val = baseconv('b58').tobytes(b58_val,pad='seed') - if val == False: - msg(f'Invalid base 58 number: {b58_val}') - return False - - setattr(d,key,val) - - return True - - def _decrypt(self): - d = self.ssdata - # Needed for multiple transactions with {}-txsign - self._get_passphrase( - add_desc = os.path.basename(self.infile.name) if opt.quiet else '' ) - key = crypto.make_key( d.passwd, d.salt, d.hash_preset ) - ret = crypto.decrypt_seed( d.enc_seed, key, d.seed_id, d.key_id ) - if ret: - self.seed = Seed(ret) - return True - else: - return False - - def _filename(self): - s = self.seed - d = self.ssdata - return '{}-{}[{},{}]{x}.{}'.format( - s.fn_stem, - d.key_id, - s.bitlen, - d.hash_preset, - self.ext, - x='-α' if g.debug_utf8 else '') - -class Brainwallet(WalletEnc): - - stdin_ok = True - fmt_codes = ('mmbrain','brainwallet','brain','bw') - desc = 'brainwallet' - ext = 'mmbrain' - # brainwallet warning message? TODO - - def get_bw_params(self): - # already checked - a = opt.brain_params.split(',') - return int(a[0]),a[1] - - def _deformat(self): - self.brainpasswd = ' '.join(self.fmt_data.split()) - return True - - def _decrypt(self): - d = self.ssdata - if opt.brain_params: - """ - Don't set opt.seed_len! When using multiple wallets, BW seed len might differ from others - """ - bw_seed_len,d.hash_preset = self.get_bw_params() - else: - if not opt.seed_len: - qmsg(f'Using default seed length of {yellow(str(Seed.dfl_len))} bits\n' - + 'If this is not what you want, use the --seed-len option' ) - self._get_hash_preset() - bw_seed_len = opt.seed_len or Seed.dfl_len - qmsg_r('Hashing brainwallet data. Please wait...') - # Use buflen arg of scrypt.hash() to get seed of desired length - seed = crypto.scrypt_hash_passphrase( - self.brainpasswd.encode(), - b'', - d.hash_preset, - buflen = bw_seed_len // 8 ) - qmsg('Done') - self.seed = Seed(seed) - msg(f'Seed ID: {self.seed.sid}') - qmsg('Check this value against your records') - return True - - def _format(self): - raise NotImplementedError('Brainwallet not supported as an output format') - - def _encrypt(self): - raise NotImplementedError('Brainwallet not supported as an output format') - -class IncogWalletBase(WalletEnc): - - _msg = { - 'check_incog_id': """ - Check the generated Incog ID above against your records. If it doesn't - match, then your incognito data is incorrect or corrupted. - """, - 'record_incog_id': """ - Make a record of the Incog ID but keep it secret. You will use it to - identify your incog wallet data in the future. - """, - 'incorrect_incog_passphrase_try_again': """ -Incorrect passphrase, hash preset, or maybe old-format incog wallet. -Try again? (Y)es, (n)o, (m)ore information: -""".strip(), - 'confirm_seed_id': """ -If the Seed ID above is correct but you're seeing this message, then you need -to exit and re-run the program with the '--old-incog-fmt' option. -""".strip(), - 'dec_chk': " {} hash preset" - } - - def _make_iv_chksum(self,s): - from hashlib import sha256 - return sha256(s).hexdigest()[:8].upper() - - def _get_incog_data_len(self,seed_len): - return ( - crypto.aesctr_iv_len - + crypto.salt_len - + (0 if opt.old_incog_fmt else crypto.hincog_chk_len) - + seed_len//8 ) - - def _incog_data_size_chk(self): - # valid sizes: 56, 64, 72 - dlen = len(self.fmt_data) - seed_len = opt.seed_len or Seed.dfl_len - valid_dlen = self._get_incog_data_len(seed_len) - if dlen == valid_dlen: - return True - else: - if opt.old_incog_fmt: - msg('WARNING: old-style incognito format requested. Are you sure this is correct?') - msg(f'Invalid incognito data size ({dlen} bytes) for this seed length ({seed_len} bits)') - msg(f'Valid data size for this seed length: {valid_dlen} bytes') - for sl in Seed.lens: - if dlen == self._get_incog_data_len(sl): - die(1,f'Valid seed length for this data size: {sl} bits') - msg(f'This data size ({dlen} bytes) is invalid for all available seed lengths') - return False - - def _encrypt (self): - self._get_first_pw_and_hp_and_encrypt_seed() - if opt.old_incog_fmt: - die(1,'Writing old-format incog wallets is unsupported') - d = self.ssdata - # IV is used BOTH to initialize counter and to salt password! - d.iv = crypto.get_random( crypto.aesctr_iv_len ) - d.iv_id = self._make_iv_chksum(d.iv) - msg(f'New Incog Wallet ID: {d.iv_id}') - qmsg('Make a record of this value') - vmsg(self.msg['record_incog_id']) - - d.salt = crypto.get_random( crypto.salt_len ) - key = crypto.make_key( d.passwd, d.salt, d.hash_preset, 'incog wallet key' ) - from hashlib import sha256 - chk = sha256(self.seed.data).digest()[:8] - d.enc_seed = crypto.encrypt_data( - chk + self.seed.data, - key, - crypto.aesctr_dfl_iv, - 'seed' ) - - d.wrapper_key = crypto.make_key( d.passwd, d.iv, d.hash_preset, 'incog wrapper key' ) - d.key_id = make_chksum_8(d.wrapper_key) - vmsg(f'Key ID: {d.key_id}') - d.target_data_len = self._get_incog_data_len(self.seed.bitlen) - - def _format(self): - d = self.ssdata - self.fmt_data = d.iv + crypto.encrypt_data( - d.salt + d.enc_seed, - d.wrapper_key, - d.iv, - self.desc ) - - def _filename(self): - s = self.seed - d = self.ssdata - return '{}-{}-{}[{},{}]{x}.{}'.format( - s.fn_stem, - d.key_id, - d.iv_id, - s.bitlen, - d.hash_preset, - self.ext, - x='-α' if g.debug_utf8 else '') - - def _deformat(self): - - if not self._incog_data_size_chk(): - return False - - d = self.ssdata - d.iv = self.fmt_data[0:crypto.aesctr_iv_len] - d.incog_id = self._make_iv_chksum(d.iv) - d.enc_incog_data = self.fmt_data[crypto.aesctr_iv_len:] - msg(f'Incog Wallet ID: {d.incog_id}') - qmsg('Check this value against your records') - vmsg(self.msg['check_incog_id']) - - return True - - def _verify_seed_newfmt(self,data): - chk,seed = data[:8],data[8:] - from hashlib import sha256 - if sha256(seed).digest()[:8] == chk: - qmsg('Passphrase{} are correct'.format( self.msg['dec_chk'].format('and') )) - return seed - else: - msg('Incorrect passphrase{}'.format( self.msg['dec_chk'].format('or') )) - return False - - def _verify_seed_oldfmt(self,seed): - m = f'Seed ID: {make_chksum_8(seed)}. Is the Seed ID correct?' - if keypress_confirm(m, True): - return seed - else: - return False - - def _decrypt(self): - d = self.ssdata - self._get_hash_preset(add_desc=d.incog_id) - self._get_passphrase(add_desc=d.incog_id) - - # IV is used BOTH to initialize counter and to salt password! - key = crypto.make_key( d.passwd, d.iv, d.hash_preset, 'wrapper key' ) - dd = crypto.decrypt_data( d.enc_incog_data, key, d.iv, 'incog data' ) - - d.salt = dd[0:crypto.salt_len] - d.enc_seed = dd[crypto.salt_len:] - - key = crypto.make_key( d.passwd, d.salt, d.hash_preset, 'main key' ) - qmsg(f'Key ID: {make_chksum_8(key)}') - - verify_seed = getattr(self,'_verify_seed_'+ - ('newfmt','oldfmt')[bool(opt.old_incog_fmt)]) - - seed = verify_seed( crypto.decrypt_seed(d.enc_seed, key, '', '') ) - - if seed: - self.seed = Seed(seed) - msg(f'Seed ID: {self.seed.sid}') - return True - else: - return False - -class IncogWallet(IncogWalletBase): - - desc = 'incognito data' - fmt_codes = ('mmincog','incog','icg','i') - ext = 'mmincog' - file_mode = 'binary' - no_tty = True - -class IncogWalletHex(IncogWalletBase): - - desc = 'hex incognito data' - fmt_codes = ('mmincox','incox','incog_hex','xincog','ix','xi') - ext = 'mmincox' - file_mode = 'text' - no_tty = False - - def _deformat(self): - ret = decode_pretty_hexdump(self.fmt_data) - if ret: - self.fmt_data = ret - return super()._deformat() - else: - return False - - def _format(self): - super()._format() - self.fmt_data = pretty_hexdump(self.fmt_data) - -class IncogWalletHidden(IncogWalletBase): - - desc = 'hidden incognito data' - fmt_codes = ('incog_hidden','hincog','ih','hi') - ext = None - file_mode = 'binary' - no_tty = True - - _msg = { - 'choose_file_size': """ -You must choose a size for your new hidden incog data. The minimum size is -{} bytes, which puts the incog data right at the end of the file. Since you -probably want to hide your data somewhere in the middle of the file where it's -harder to find, you're advised to choose a much larger file size than this. - """.strip(), - 'check_incog_id': """ - Check generated Incog ID above against your records. If it doesn't - match, then your incognito data is incorrect or corrupted, or you - may have specified an incorrect offset. - """, - 'record_incog_id': """ - Make a record of the Incog ID but keep it secret. You will used it to - identify the incog wallet data in the future and to locate the offset - where the data is hidden in the event you forget it. - """, - 'dec_chk': ', hash preset, offset {} seed length' - } - - def _get_hincog_params(self,wtype): - a = getattr(opt,'hidden_incog_'+ wtype +'_params').split(',') - return ','.join(a[:-1]),int(a[-1]) # permit comma in filename - - def _check_valid_offset(self,fn,action): - d = self.ssdata - m = ('Input','Destination')[action=='write'] - if fn.size < d.hincog_offset + d.target_data_len: - die(1,'{} file {!r} has length {}, too short to {} {} bytes of data at offset {}'.format( - m, - fn.name, - fn.size, - action, - d.target_data_len, - d.hincog_offset )) - - def _get_data(self): - d = self.ssdata - d.hincog_offset = self._get_hincog_params('input')[1] - - qmsg(f'Getting hidden incog data from file {self.infile.name!r}') - - # Already sanity-checked: - d.target_data_len = self._get_incog_data_len(opt.seed_len or Seed.dfl_len) - self._check_valid_offset(self.infile,'read') - - flgs = os.O_RDONLY|os.O_BINARY if g.platform == 'win' else os.O_RDONLY - fh = os.open(self.infile.name,flgs) - os.lseek(fh,int(d.hincog_offset),os.SEEK_SET) - self.fmt_data = os.read(fh,d.target_data_len) - os.close(fh) - qmsg(f'Data read from file {self.infile.name!r} at offset {d.hincog_offset}') - - # overrides method in Wallet - def write_to_file(self): - d = self.ssdata - self._format() - compare_or_die(d.target_data_len, 'target data length', - len(self.fmt_data),'length of formatted ' + self.desc) - - k = ('output','input')[self.op=='pwchg_new'] - fn,d.hincog_offset = self._get_hincog_params(k) - - if opt.outdir and not os.path.dirname(fn): - fn = os.path.join(opt.outdir,fn) - - check_offset = True - try: - os.stat(fn) - except: - if keypress_confirm( - f'Requested file {fn!r} does not exist. Create?', - default_yes = True ): - min_fsize = d.target_data_len + d.hincog_offset - msg(self.msg['choose_file_size'].format(min_fsize)) - while True: - fsize = parse_bytespec(line_input('Enter file size: ')) - if fsize >= min_fsize: - break - msg(f'File size must be an integer no less than {min_fsize}') - - from .tool.fileutil import tool_cmd - tool_cmd().rand2file(fn,str(fsize)) - check_offset = False - else: - die(1,'Exiting at user request') - - from .filename import Filename - f = Filename(fn,subclass=type(self),write=True) - - dmsg('{} data len {}, offset {}'.format( - capfirst(self.desc), - d.target_data_len, - d.hincog_offset )) - - if check_offset: - self._check_valid_offset(f,'write') - if not opt.quiet: - confirm_or_raise( '', f'alter file {f.name!r}' ) - - flgs = os.O_RDWR|os.O_BINARY if g.platform == 'win' else os.O_RDWR - fh = os.open(f.name,flgs) - os.lseek(fh, int(d.hincog_offset), os.SEEK_SET) - os.write(fh, self.fmt_data) - os.close(fh) - msg('{} written to file {!r} at offset {}'.format( - capfirst(self.desc), - f.name, - d.hincog_offset )) diff --git a/mmgen/wallet/__init__.py b/mmgen/wallet/__init__.py new file mode 100755 index 00000000..864c7c61 --- /dev/null +++ b/mmgen/wallet/__init__.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python3 +# +# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet +# Copyright (C)2013-2022 The MMGen Project +# Licensed under the GNU General Public License, Version 3: +# https://www.gnu.org/licenses +# Public project repositories: +# https://github.com/mmgen/mmgen +# https://gitlab.com/mmgen/mmgen + +""" +wallet.__init__: wallet class initializer +""" + +import importlib +from collections import namedtuple + +from ..globalvars import g +from ..opts import opt +from ..util import die,get_extension +from ..objmethods import MMGenObject +from ..seed import Seed + +_wd = namedtuple('wallet_data', ['type','name','ext','base_type','enc','fmt_codes']) +_pd = namedtuple('partial_wallet_data',['name','ext','base_type','enc','fmt_codes']) + +wallet_data = { + 'bip39': _pd('BIP39Mnemonic', 'bip39', 'mnemonic', False,('bip39',)), + 'brain': _pd('Brainwallet', 'mmbrain',None, True, ('mmbrain','brainwallet','brain','bw')), + 'dieroll': _pd('DieRollWallet', 'b6d', None, False,('b6d','die','dieroll')), + 'incog': _pd('IncogWallet', 'mmincog','incog_base',True, ('mmincog','incog','icg','i')), + 'incog_hex': _pd('IncogWalletHex', 'mmincox','incog_base',True, ('mmincox','incox','incog_hex','ix','xi')), + 'incog_hidden':_pd('IncogWalletHidden',None, 'incog_base',True, ('incog_hidden','hincog','ih','hi')), + 'mmgen': _pd('MMGenWallet', 'mmdat', None, True, ('wallet','w')), + 'mmhex': _pd('MMGenHexSeedFile', 'mmhex', None, False,('seedhex','hexseed','mmhex')), + 'plainhex': _pd('PlainHexSeedFile', 'hex', None, False,('hex','rawhex','plainhex')), + 'seed': _pd('MMGenSeedFile', 'mmseed', None, False,('mmseed','seed','s')), + 'words': _pd('MMGenMnemonic', 'mmwords','mnemonic', False,('mmwords','words','mnemonic','mn','m')), +} + +def get_wallet_data(*args,**kwargs): + + if args: + return _wd( args[0], *wallet_data[args[0]] ) + + for key in ('fmt_code','ext'): + if key in kwargs: + val = kwargs[key] + break + else: + die('{!r}: unrecognized argument'.format( list(kwargs.keys())[0] )) + + if key == 'fmt_code': + for k,v in wallet_data.items(): + if val in v.fmt_codes: + return _wd(k,*v) + else: + for k,v in wallet_data.items(): + if val == getattr(v,key): + return _wd(k,*v) + + if 'die_on_fail' in kwargs: + die( *{ + 'ext': ('BadFileExtension', f'{val!r}: unrecognized wallet file extension'), + 'fmt_code': (3, f'{val!r}: unrecognized wallet format code'), + 'type': (3, f'{val!r}: unrecognized wallet type'), + }[key] ) + +def get_wallet_cls(*args,**kwargs): + return getattr( + importlib.import_module( 'mmgen.wallet.{}'.format( + args[0] if args else get_wallet_data(*args,**kwargs).type) + ), + 'wallet' ) + +def get_wallet_extensions(key): + return { + 'enc': [v.ext for v in wallet_data.values() if v.enc], + 'unenc': [v.ext for v in wallet_data.values() if not v.enc] + }[key] + +def format_fmt_codes(): + d = [( + v.name, + ('.' + v.ext if v.ext else 'None'), + ','.join(v.fmt_codes) + ) for v in wallet_data.values()] + w = max(len(i[0]) for i in d) + ret = [f'{a:<{w}} {b:<9} {c}' for a,b,c in [ + ('Format','FileExt','Valid codes'), + ('------','-------','-----------') + ] + sorted(d) ] + return '\n'.join(ret) + ('','-α')[g.debug_utf8] + '\n' + +def _get_me(modname): + return MMGenObject.__new__( getattr( importlib.import_module(f'mmgen.wallet.{modname}'), 'wallet' ) ) + +def Wallet( + fn = None, + ss = None, + seed_bin = None, + seed = None, + passchg = False, + in_data = None, + ignore_in_fmt = False, + in_fmt = None, + passwd_file = None ): + + in_fmt = in_fmt or opt.in_fmt + + if opt.out_fmt: + ss_out = get_wallet_data(fmt_code=opt.out_fmt) + if not ss_out: + die(1,f'{opt.out_fmt!r}: unrecognized output format') + else: + ss_out = None + + if seed or seed_bin: + me = _get_me( ss_out.type if ss_out else 'mmgen' ) # default to native wallet format + me.seed = seed or Seed(seed_bin=seed_bin) + me.op = 'new' + elif ss: + me = _get_me( ss.type if passchg else ss_out.type if ss_out else 'mmgen' ) + me.seed = ss.seed + me.ss_in = ss + me.op = 'pwchg_new' if passchg else 'conv' + elif fn or opt.hidden_incog_input_params: + if fn: + wd = get_wallet_data(ext=get_extension(fn),die_on_fail=True) + if in_fmt and (not ignore_in_fmt) and in_fmt not in wd.fmt_codes: + die(1,f'{in_fmt}: --in-fmt parameter does not match extension of input file') + me = _get_me( wd.type ) + else: + fn = ','.join(opt.hidden_incog_input_params.split(',')[:-1]) # permit comma in filename + me = _get_me( 'incog_hidden' ) + from ..filename import Filename + me.infile = Filename( fn, subclass=type(me) ) + me.op = 'pwchg_old' if passchg else 'old' + elif in_fmt: + me = _get_me( get_wallet_data(fmt_code=in_fmt).type ) + me.op = 'pwchg_old' if passchg else 'old' + else: # called with no arguments: initialize with random seed + me = _get_me( ss_out.type if ss_out else 'mmgen' ) # default to native wallet format + me.seed = Seed() + me.op = 'new' + + me.__init__( + fn = fn, + ss = ss, + seed_bin = seed_bin, + seed = seed, + passchg = passchg, + in_data = in_data, + ignore_in_fmt = ignore_in_fmt, + in_fmt = in_fmt, + passwd_file = passwd_file ) + + return me diff --git a/mmgen/wallet/base.py b/mmgen/wallet/base.py new file mode 100755 index 00000000..cb1b410c --- /dev/null +++ b/mmgen/wallet/base.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python3 +# +# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet +# Copyright (C)2013-2022 The MMGen Project +# Licensed under the GNU General Public License, Version 3: +# https://www.gnu.org/licenses +# Public project repositories: +# https://github.com/mmgen/mmgen +# https://gitlab.com/mmgen/mmgen + +""" +wallet.base: wallet base class +""" + +import os + +from ..globalvars import g +from ..opts import opt +from ..util import msg,qmsg,die,get_data_from_user +from ..objmethods import MMGenObject +from . import Wallet,wallet_data,get_wallet_cls + +class WalletMeta(type): + + def __init__(cls,name,bases,namespace): + cls.type = cls.__module__.split('.')[-1] + if cls.type in wallet_data: + for k,v in wallet_data[cls.type]._asdict().items(): + setattr(cls,k,v) + +class wallet(MMGenObject,metaclass=WalletMeta): + + desc = 'MMGen seed source' + file_mode = 'text' + filename_api = True + stdin_ok = False + ask_tty = True + no_tty = False + op = None + + class WalletData(MMGenObject): + pass + + def __init__(self, + fn = None, + ss = None, + seed_bin = None, + seed = None, + passchg = False, + in_data = None, + ignore_in_fmt = False, + in_fmt = None, + passwd_file = None ): + + self.passwd_file = passwd_file or opt.passwd_file + self.ssdata = self.WalletData() + self.msg = {} + self.in_data = in_data + + for c in reversed(self.__class__.__mro__): + if hasattr(c,'_msg'): + self.msg.update(c._msg) + + if hasattr(self,'seed'): + self._encrypt() + return + elif hasattr(self,'infile') or self.in_data or not g.stdin_tty: + self._deformat_once() + self._decrypt_retry() + else: + if not self.stdin_ok: + die(1,f'Reading from standard input not supported for {self.desc} format') + self._deformat_retry() + self._decrypt_retry() + + qmsg('Valid {} for Seed ID {}{}'.format( + self.desc, + self.seed.sid.hl(), + (f', seed length {self.seed.bitlen}' if self.seed.bitlen != 256 else '') + )) + + def _get_data(self): + if hasattr(self,'infile'): + from ..fileutil import get_data_from_file + self.fmt_data = get_data_from_file(self.infile.name,self.desc,binary=self.file_mode=='binary') + elif self.in_data: + self.fmt_data = self.in_data + else: + self.fmt_data = self._get_data_from_user(self.desc) + + def _get_data_from_user(self,desc): + return get_data_from_user(desc) + + def _deformat_once(self): + self._get_data() + if not self._deformat(): + die(2,'Invalid format for input data') + + def _deformat_retry(self): + while True: + self._get_data() + if self._deformat(): + break + msg('Trying again...') + + @classmethod + def ext_to_cls(cls,ext,proto): + return get_wallet_cls(ext=ext) + + def get_fmt_data(self): + self._format() + return self.fmt_data + + def write_to_file(self,outdir='',desc=''): + self._format() + kwargs = { + 'desc': desc or self.desc, + 'ask_tty': self.ask_tty, + 'no_tty': self.no_tty, + 'binary': self.file_mode == 'binary' + } + + if outdir: + # write_data_to_file(): outfile with absolute path overrides opt.outdir + of = os.path.abspath(os.path.join(outdir,self._filename())) + + from ..fileutil import write_data_to_file + write_data_to_file( + of if outdir else self._filename(), + self.fmt_data, + **kwargs ) + + def check_usr_seed_len(self,bitlen=None): + chk = bitlen or self.seed.bitlen + if opt.seed_len and opt.seed_len != chk: + die(1,f'ERROR: requested seed length ({opt.seed_len}) doesn’t match seed length of source ({chk})') diff --git a/mmgen/wallet/bip39.py b/mmgen/wallet/bip39.py new file mode 100755 index 00000000..ebeb63a9 --- /dev/null +++ b/mmgen/wallet/bip39.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +# +# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet +# Copyright (C)2013-2022 The MMGen Project +# Licensed under the GNU General Public License, Version 3: +# https://www.gnu.org/licenses +# Public project repositories: +# https://github.com/mmgen/mmgen +# https://gitlab.com/mmgen/mmgen + +""" +wallet.bip39: BIP39 mnemonic wallet class +""" + +from .mnemonic import wallet + +class wallet(wallet): + + desc = 'BIP39 mnemonic data' + mn_type = 'BIP39' + wl_id = 'bip39' + + def __init__(self,*args,**kwargs): + from ..bip39 import bip39 + self.conv_cls = bip39 + super().__init__(*args,**kwargs) diff --git a/mmgen/wallet/brain.py b/mmgen/wallet/brain.py new file mode 100755 index 00000000..3824f6ff --- /dev/null +++ b/mmgen/wallet/brain.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +# +# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet +# Copyright (C)2013-2022 The MMGen Project +# Licensed under the GNU General Public License, Version 3: +# https://www.gnu.org/licenses +# Public project repositories: +# https://github.com/mmgen/mmgen +# https://gitlab.com/mmgen/mmgen + +""" +wallet.brain: brainwallet wallet class +""" + +from ..opts import opt +from ..util import msg,qmsg,qmsg_r +from ..color import yellow +from .enc import wallet +from .seed import Seed +import mmgen.crypto as crypto + +class wallet(wallet): + + stdin_ok = True + desc = 'brainwallet' + # brainwallet warning message? TODO + + def get_bw_params(self): + # already checked + a = opt.brain_params.split(',') + return int(a[0]),a[1] + + def _deformat(self): + self.brainpasswd = ' '.join(self.fmt_data.split()) + return True + + def _decrypt(self): + d = self.ssdata + if opt.brain_params: + """ + Don't set opt.seed_len! When using multiple wallets, BW seed len might differ from others + """ + bw_seed_len,d.hash_preset = self.get_bw_params() + else: + if not opt.seed_len: + qmsg(f'Using default seed length of {yellow(str(Seed.dfl_len))} bits\n' + + 'If this is not what you want, use the --seed-len option' ) + self._get_hash_preset() + bw_seed_len = opt.seed_len or Seed.dfl_len + qmsg_r('Hashing brainwallet data. Please wait...') + # Use buflen arg of scrypt.hash() to get seed of desired length + seed = crypto.scrypt_hash_passphrase( + self.brainpasswd.encode(), + b'', + d.hash_preset, + buflen = bw_seed_len // 8 ) + qmsg('Done') + self.seed = Seed(seed) + msg(f'Seed ID: {self.seed.sid}') + qmsg('Check this value against your records') + return True + + def _format(self): + raise NotImplementedError('Brainwallet not supported as an output format') + + def _encrypt(self): + raise NotImplementedError('Brainwallet not supported as an output format') diff --git a/mmgen/wallet/dieroll.py b/mmgen/wallet/dieroll.py new file mode 100755 index 00000000..c0e4bca0 --- /dev/null +++ b/mmgen/wallet/dieroll.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +# +# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet +# Copyright (C)2013-2022 The MMGen Project +# Licensed under the GNU General Public License, Version 3: +# https://www.gnu.org/licenses +# Public project repositories: +# https://github.com/mmgen/mmgen +# https://gitlab.com/mmgen/mmgen + +""" +wallet.dieroll: dieroll wallet class +""" + +from ..globalvars import g +from ..opts import opt +from ..util import msg,msg_r,die,fmt,block_format,remove_whitespace,keypress_confirm +from ..seed import Seed +from ..baseconv import baseconv +from .unenc import wallet + +class wallet(wallet): + + stdin_ok = True + desc = 'base6d die roll seed data' + conv_cls = baseconv + wl_id = 'b6d' + mn_type = 'base6d' + choose_seedlen_prompt = 'Choose a seed length: 1) 128 bits, 2) 192 bits, 3) 256 bits: ' + choose_seedlen_confirm = 'Seed length of {} bits chosen. OK?' + user_entropy_prompt = 'Would you like to provide some additional entropy from the keyboard?' + interactive_input = False + + def _format(self): + d = baseconv('b6d').frombytes(self.seed.data,pad='seed',tostr=True) + '\n' + self.fmt_data = block_format(d,gw=5,cols=5) + + def _deformat(self): + + d = remove_whitespace(self.fmt_data) + bc = baseconv('b6d') + rmap = bc.seedlen_map_rev + + if not len(d) in rmap: + die( 'SeedLengthError', '{!r}: invalid length for {} (must be one of {})'.format( + len(d), + self.desc, + list(rmap) )) + + # truncate seed to correct length, discarding high bits + seed_len = rmap[len(d)] + seed_bytes = bc.tobytes( d, pad='seed' )[-seed_len:] + + if self.interactive_input and opt.usr_randchars: + if keypress_confirm(self.user_entropy_prompt): + from ..crypto import add_user_random + seed_bytes = add_user_random( + rand_bytes = seed_bytes, + desc = 'gathered from your die rolls' ) + self.desc += ' plus user-supplied entropy' + + self.seed = Seed(seed_bytes) + self.ssdata.hexseed = seed_bytes.hex() + + self.check_usr_seed_len() + return True + + def _get_data_from_user(self,desc): + + if not g.stdin_tty: + return get_data_from_user(desc) + + bc = baseconv('b6d') + + seed_bitlen = self._choose_seedlen([ n*8 for n in sorted(bc.seedlen_map) ]) + nDierolls = bc.seedlen_map[seed_bitlen // 8] + + message = """ + For a {sb}-bit seed you must roll the die {nd} times. After each die roll, + enter the result on the keyboard as a digit. If you make an invalid entry, + you'll be prompted to re-enter it. + """ + msg('\n'+fmt(message.strip()).format(sb=seed_bitlen,nd=nDierolls)+'\n') + + CUR_HIDE = '\033[?25l' + CUR_SHOW = '\033[?25h' + cr = '\n' if g.test_suite else '\r' + prompt_fs = f'\b\b\b {cr}Enter die roll #{{}}: {CUR_SHOW}' + clear_line = '' if g.test_suite else '\r' + ' ' * 25 + invalid_msg = CUR_HIDE + cr + 'Invalid entry' + ' ' * 11 + + from ..term import get_char + def get_digit(n): + p = prompt_fs + sleep = g.short_disp_timeout + while True: + ch = get_char(p.format(n),num_chars=1,sleep=sleep) + if ch in bc.digits: + msg_r(CUR_HIDE + ' OK') + return ch + else: + msg_r(invalid_msg) + sleep = g.err_disp_timeout + p = clear_line + prompt_fs + + dierolls,n = [],1 + while len(dierolls) < nDierolls: + dierolls.append(get_digit(n)) + n += 1 + + msg('Die rolls successfully entered' + CUR_SHOW) + self.interactive_input = True + + return ''.join(dierolls) diff --git a/mmgen/wallet/enc.py b/mmgen/wallet/enc.py new file mode 100755 index 00000000..58358c2a --- /dev/null +++ b/mmgen/wallet/enc.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 +# +# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet +# Copyright (C)2013-2022 The MMGen Project +# Licensed under the GNU General Public License, Version 3: +# https://www.gnu.org/licenses +# Public project repositories: +# https://github.com/mmgen/mmgen +# https://gitlab.com/mmgen/mmgen + +""" +wallet.enc: encrypted wallet base class +""" + +from ..globalvars import g +from ..opts import opt +from ..util import msg,qmsg,make_chksum_8 + +import mmgen.crypto as crypto + +from .base import wallet + +class wallet(wallet): + + def _decrypt_retry(self): + while True: + if self._decrypt(): + break + if self.passwd_file: + die(2,'Passphrase from password file, so exiting') + msg('Trying again...') + + def _get_hash_preset_from_user(self,hp,add_desc=''): + prompt = 'Enter {}hash preset for {}{}{},\nor hit ENTER to {} value ({!r}): '.format( + ('old ' if self.op=='pwchg_old' else 'new ' if self.op=='pwchg_new' else ''), + ('','new ')[self.op=='new'], + self.desc, + ('',' '+add_desc)[bool(add_desc)], + ('accept the default','reuse the old')[self.op=='pwchg_new'], + hp ) + return crypto.get_hash_preset_from_user( hash_preset=hp, prompt=prompt ) + + def _get_hash_preset(self,add_desc=''): + if hasattr(self,'ss_in') and hasattr(self.ss_in.ssdata,'hash_preset'): + old_hp = self.ss_in.ssdata.hash_preset + if opt.keep_hash_preset: + hp = old_hp + qmsg(f'Reusing hash preset {hp!r} at user request') + elif opt.hash_preset: + hp = opt.hash_preset + qmsg(f'Using hash preset {hp!r} requested on command line') + else: # Prompt, using old value as default + hp = self._get_hash_preset_from_user(old_hp,add_desc) + if (not opt.keep_hash_preset) and self.op == 'pwchg_new': + qmsg('Hash preset {}'.format( 'unchanged' if hp == old_hp else f'changed to {hp!r}' )) + elif opt.hash_preset: + hp = opt.hash_preset + qmsg(f'Using hash preset {hp!r} requested on command line') + else: + hp = self._get_hash_preset_from_user(g.dfl_hash_preset,add_desc) + self.ssdata.hash_preset = hp + + def _get_new_passphrase(self): + return crypto.get_new_passphrase( + data_desc = ('new ' if self.op in ('new','conv') else '') + self.desc, + hash_preset = self.ssdata.hash_preset, + passwd_file = self.passwd_file, + pw_desc = ('new ' if self.op=='pwchg_new' else '') + 'passphrase' ) + + def _get_passphrase(self,add_desc=''): + return crypto.get_passphrase( + data_desc = self.desc + (f' {add_desc}' if add_desc else ''), + passwd_file = self.passwd_file, + pw_desc = ('old ' if self.op == 'pwchg_old' else '') + 'passphrase' ) + + def _get_first_pw_and_hp_and_encrypt_seed(self): + d = self.ssdata + self._get_hash_preset() + + if hasattr(self,'ss_in') and hasattr(self.ss_in.ssdata,'passwd'): + old_pw = self.ss_in.ssdata.passwd + if opt.keep_passphrase: + d.passwd = old_pw + qmsg('Reusing passphrase at user request') + else: + d.passwd = self._get_new_passphrase() + if self.op == 'pwchg_new': + qmsg('Passphrase {}'.format( 'unchanged' if d.passwd == old_pw else 'changed' )) + else: + d.passwd = self._get_new_passphrase() + + from hashlib import sha256 + d.salt = sha256( crypto.get_random(128) ).digest()[:crypto.salt_len] + key = crypto.make_key( d.passwd, d.salt, d.hash_preset ) + d.key_id = make_chksum_8(key) + d.enc_seed = crypto.encrypt_seed( self.seed.data, key ) diff --git a/mmgen/wallet/incog.py b/mmgen/wallet/incog.py new file mode 100755 index 00000000..a7c05de7 --- /dev/null +++ b/mmgen/wallet/incog.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +# +# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet +# Copyright (C)2013-2022 The MMGen Project +# Licensed under the GNU General Public License, Version 3: +# https://www.gnu.org/licenses +# Public project repositories: +# https://github.com/mmgen/mmgen +# https://gitlab.com/mmgen/mmgen + +""" +wallet.incog: incognito wallet class +""" + +from .incog_base import wallet + +class wallet(wallet): + + desc = 'incognito data' + file_mode = 'binary' + no_tty = True diff --git a/mmgen/wallet/incog_base.py b/mmgen/wallet/incog_base.py new file mode 100755 index 00000000..df53a4a5 --- /dev/null +++ b/mmgen/wallet/incog_base.py @@ -0,0 +1,169 @@ +#!/usr/bin/env python3 +# +# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet +# Copyright (C)2013-2022 The MMGen Project +# Licensed under the GNU General Public License, Version 3: +# https://www.gnu.org/licenses +# Public project repositories: +# https://github.com/mmgen/mmgen +# https://gitlab.com/mmgen/mmgen + +""" +wallet.incog_base: incognito wallet base class +""" + +from ..globalvars import g +from ..opts import opt +from ..seed import Seed +from ..util import msg,vmsg,qmsg,make_chksum_8,keypress_confirm +from .enc import wallet +import mmgen.crypto as crypto + +class wallet(wallet): + + _msg = { + 'check_incog_id': """ + Check the generated Incog ID above against your records. If it doesn't + match, then your incognito data is incorrect or corrupted. + """, + 'record_incog_id': """ + Make a record of the Incog ID but keep it secret. You will use it to + identify your incog wallet data in the future. + """, + 'decrypt_params': " {} hash preset" + } + + def _make_iv_chksum(self,s): + from hashlib import sha256 + return sha256(s).hexdigest()[:8].upper() + + def _get_incog_data_len(self,seed_len): + return ( + crypto.aesctr_iv_len + + crypto.salt_len + + (0 if opt.old_incog_fmt else crypto.hincog_chk_len) + + seed_len//8 ) + + def _incog_data_size_chk(self): + # valid sizes: 56, 64, 72 + dlen = len(self.fmt_data) + seed_len = opt.seed_len or Seed.dfl_len + valid_dlen = self._get_incog_data_len(seed_len) + if dlen == valid_dlen: + return True + else: + if opt.old_incog_fmt: + msg('WARNING: old-style incognito format requested. Are you sure this is correct?') + msg(f'Invalid incognito data size ({dlen} bytes) for this seed length ({seed_len} bits)') + msg(f'Valid data size for this seed length: {valid_dlen} bytes') + for sl in Seed.lens: + if dlen == self._get_incog_data_len(sl): + die(1,f'Valid seed length for this data size: {sl} bits') + msg(f'This data size ({dlen} bytes) is invalid for all available seed lengths') + return False + + def _encrypt (self): + self._get_first_pw_and_hp_and_encrypt_seed() + if opt.old_incog_fmt: + die(1,'Writing old-format incog wallets is unsupported') + d = self.ssdata + # IV is used BOTH to initialize counter and to salt password! + d.iv = crypto.get_random( crypto.aesctr_iv_len ) + d.iv_id = self._make_iv_chksum(d.iv) + msg(f'New Incog Wallet ID: {d.iv_id}') + qmsg('Make a record of this value') + vmsg('\n ' + self.msg['record_incog_id'].strip()+'\n') + + d.salt = crypto.get_random( crypto.salt_len ) + key = crypto.make_key( d.passwd, d.salt, d.hash_preset, 'incog wallet key' ) + from hashlib import sha256 + chk = sha256(self.seed.data).digest()[:8] + d.enc_seed = crypto.encrypt_data( + chk + self.seed.data, + key, + crypto.aesctr_dfl_iv, + 'seed' ) + + d.wrapper_key = crypto.make_key( d.passwd, d.iv, d.hash_preset, 'incog wrapper key' ) + d.key_id = make_chksum_8(d.wrapper_key) + vmsg(f'Key ID: {d.key_id}') + d.target_data_len = self._get_incog_data_len(self.seed.bitlen) + + def _format(self): + d = self.ssdata + self.fmt_data = d.iv + crypto.encrypt_data( + d.salt + d.enc_seed, + d.wrapper_key, + d.iv, + self.desc ) + + def _filename(self): + s = self.seed + d = self.ssdata + return '{}-{}-{}[{},{}]{x}.{}'.format( + s.fn_stem, + d.key_id, + d.iv_id, + s.bitlen, + d.hash_preset, + self.ext, + x='-α' if g.debug_utf8 else '') + + def _deformat(self): + + if not self._incog_data_size_chk(): + return False + + d = self.ssdata + d.iv = self.fmt_data[0:crypto.aesctr_iv_len] + d.incog_id = self._make_iv_chksum(d.iv) + d.enc_incog_data = self.fmt_data[crypto.aesctr_iv_len:] + msg(f'Incog Wallet ID: {d.incog_id}') + qmsg('Check this value against your records') + vmsg('\n ' + self.msg['check_incog_id'].strip()+'\n') + + return True + + def _verify_seed_newfmt(self,data): + chk,seed = data[:8],data[8:] + from hashlib import sha256 + if sha256(seed).digest()[:8] == chk: + qmsg('Passphrase{} are correct'.format( self.msg['decrypt_params'].format('and') )) + return seed + else: + msg('Incorrect passphrase{}'.format( self.msg['decrypt_params'].format('or') )) + return False + + def _verify_seed_oldfmt(self,seed): + m = f'Seed ID: {make_chksum_8(seed)}. Is the Seed ID correct?' + if keypress_confirm(m, True): + return seed + else: + return False + + def _decrypt(self): + d = self.ssdata + self._get_hash_preset(add_desc=d.incog_id) + d.passwd = self._get_passphrase(add_desc=d.incog_id) + + # IV is used BOTH to initialize counter and to salt password! + key = crypto.make_key( d.passwd, d.iv, d.hash_preset, 'wrapper key' ) + dd = crypto.decrypt_data( d.enc_incog_data, key, d.iv, 'incog data' ) + + d.salt = dd[0:crypto.salt_len] + d.enc_seed = dd[crypto.salt_len:] + + key = crypto.make_key( d.passwd, d.salt, d.hash_preset, 'main key' ) + qmsg(f'Key ID: {make_chksum_8(key)}') + + verify_seed = getattr(self,'_verify_seed_'+ + ('newfmt','oldfmt')[bool(opt.old_incog_fmt)]) + + seed = verify_seed( crypto.decrypt_seed(d.enc_seed, key, '', '') ) + + if seed: + self.seed = Seed(seed) + msg(f'Seed ID: {self.seed.sid}') + return True + else: + return False diff --git a/mmgen/wallet/incog_hex.py b/mmgen/wallet/incog_hex.py new file mode 100755 index 00000000..8a5e0dc9 --- /dev/null +++ b/mmgen/wallet/incog_hex.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 +# +# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet +# Copyright (C)2013-2022 The MMGen Project +# Licensed under the GNU General Public License, Version 3: +# https://www.gnu.org/licenses +# Public project repositories: +# https://github.com/mmgen/mmgen +# https://gitlab.com/mmgen/mmgen + +""" +wallet.incog_hex: hexadecimal incognito wallet class +""" + +from ..util import pretty_hexdump,decode_pretty_hexdump +from .incog_base import wallet + +class wallet(wallet): + + desc = 'hex incognito data' + file_mode = 'text' + no_tty = False + + def _deformat(self): + ret = decode_pretty_hexdump(self.fmt_data) + if ret: + self.fmt_data = ret + return super()._deformat() + else: + return False + + def _format(self): + super()._format() + self.fmt_data = pretty_hexdump(self.fmt_data) diff --git a/mmgen/wallet/incog_hidden.py b/mmgen/wallet/incog_hidden.py new file mode 100755 index 00000000..2976ac5b --- /dev/null +++ b/mmgen/wallet/incog_hidden.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python3 +# +# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet +# Copyright (C)2013-2022 The MMGen Project +# Licensed under the GNU General Public License, Version 3: +# https://www.gnu.org/licenses +# Public project repositories: +# https://github.com/mmgen/mmgen +# https://gitlab.com/mmgen/mmgen + +""" +wallet.incog_hidden: hidden incognito wallet class +""" + +import os + +from ..globalvars import g +from ..opts import opt +from ..seed import Seed +from ..util import ( + msg, + dmsg, + qmsg, + die, + compare_or_die, + keypress_confirm, + parse_bytespec, + line_input, + capfirst, + confirm_or_raise +) +from .incog_base import wallet + +class wallet(wallet): + + desc = 'hidden incognito data' + file_mode = 'binary' + no_tty = True + + _msg = { + 'choose_file_size': """ + You must choose a size for your new hidden incog data. The minimum size + is {} bytes, which puts the incog data right at the end of the file. + Since you probably want to hide your data somewhere in the middle of the + file where it's harder to find, you're advised to choose a much larger file + size than this. + """, + 'check_incog_id': """ + Check generated Incog ID above against your records. If it doesn't match, + then your incognito data is incorrect or corrupted, or you may have speci- + fied an incorrect offset. + """, + 'record_incog_id': """ + Make a record of the Incog ID but keep it secret. You will used it to + identify the incog wallet data in the future and to locate the offset + where the data is hidden in the event you forget it. + """, + 'decrypt_params': ', hash preset, offset {} seed length' + } + + def _get_hincog_params(self,wtype): + a = getattr(opt,'hidden_incog_'+ wtype +'_params').split(',') + return ','.join(a[:-1]),int(a[-1]) # permit comma in filename + + def _check_valid_offset(self,fn,action): + d = self.ssdata + m = ('Input','Destination')[action=='write'] + if fn.size < d.hincog_offset + d.target_data_len: + die(1,'{} file {!r} has length {}, too short to {} {} bytes of data at offset {}'.format( + m, + fn.name, + fn.size, + action, + d.target_data_len, + d.hincog_offset )) + + def _get_data(self): + d = self.ssdata + d.hincog_offset = self._get_hincog_params('input')[1] + + qmsg(f'Getting hidden incog data from file {self.infile.name!r}') + + # Already sanity-checked: + d.target_data_len = self._get_incog_data_len(opt.seed_len or Seed.dfl_len) + self._check_valid_offset(self.infile,'read') + + flgs = os.O_RDONLY|os.O_BINARY if g.platform == 'win' else os.O_RDONLY + fh = os.open(self.infile.name,flgs) + os.lseek(fh,int(d.hincog_offset),os.SEEK_SET) + self.fmt_data = os.read(fh,d.target_data_len) + os.close(fh) + qmsg(f'Data read from file {self.infile.name!r} at offset {d.hincog_offset}') + + # overrides method in Wallet + def write_to_file(self): + d = self.ssdata + self._format() + compare_or_die(d.target_data_len, 'target data length', + len(self.fmt_data),'length of formatted ' + self.desc) + + k = ('output','input')[self.op=='pwchg_new'] + fn,d.hincog_offset = self._get_hincog_params(k) + + if opt.outdir and not os.path.dirname(fn): + fn = os.path.join(opt.outdir,fn) + + check_offset = True + try: + os.stat(fn) + except: + if keypress_confirm( + f'Requested file {fn!r} does not exist. Create?', + default_yes = True ): + min_fsize = d.target_data_len + d.hincog_offset + msg('\n ' + self.msg['choose_file_size'].strip().format(min_fsize)+'\n') + while True: + fsize = parse_bytespec(line_input('Enter file size: ')) + if fsize >= min_fsize: + break + msg(f'File size must be an integer no less than {min_fsize}') + + from ..tool.fileutil import tool_cmd + tool_cmd().rand2file(fn,str(fsize)) + check_offset = False + else: + die(1,'Exiting at user request') + + from ..filename import Filename + f = Filename(fn,subclass=type(self),write=True) + + dmsg('{} data len {}, offset {}'.format( + capfirst(self.desc), + d.target_data_len, + d.hincog_offset )) + + if check_offset: + self._check_valid_offset(f,'write') + if not opt.quiet: + confirm_or_raise( '', f'alter file {f.name!r}' ) + + flgs = os.O_RDWR|os.O_BINARY if g.platform == 'win' else os.O_RDWR + fh = os.open(f.name,flgs) + os.lseek(fh, int(d.hincog_offset), os.SEEK_SET) + os.write(fh, self.fmt_data) + os.close(fh) + msg('{} written to file {!r} at offset {}'.format( + capfirst(self.desc), + f.name, + d.hincog_offset )) diff --git a/mmgen/wallet/mmgen.py b/mmgen/wallet/mmgen.py new file mode 100755 index 00000000..040d3f74 --- /dev/null +++ b/mmgen/wallet/mmgen.py @@ -0,0 +1,191 @@ +#!/usr/bin/env python3 +# +# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet +# Copyright (C)2013-2022 The MMGen Project +# Licensed under the GNU General Public License, Version 3: +# https://www.gnu.org/licenses +# Public project repositories: +# https://github.com/mmgen/mmgen +# https://gitlab.com/mmgen/mmgen + +""" +wallet.mmgen: MMGen native wallet class +""" + +import os + +from ..globalvars import g +from ..opts import opt +from ..seed import Seed +from ..util import msg,qmsg,line_input,make_timestamp,make_chksum_6,split_into_cols,is_chksum_6,compare_chksums +from ..obj import MMGenWalletLabel,get_obj +from ..baseconv import baseconv + +import mmgen.crypto as crypto + +from .enc import wallet + +class wallet(wallet): + + desc = 'MMGen wallet' + + def __init__(self,*args,**kwargs): + if opt.label: + self.label = MMGenWalletLabel( + opt.label, + msg = "Error in option '--label'" ) + else: + self.label = None + super().__init__(*args,**kwargs) + + # logic identical to _get_hash_preset_from_user() + def _get_label_from_user(self,old_lbl=''): + prompt = 'Enter a wallet label, or hit ENTER {}: '.format( + 'to reuse the label {}'.format(old_lbl.hl(encl="''")) if old_lbl else + 'for no label' ) + while True: + ret = line_input(prompt) + if ret: + lbl = get_obj(MMGenWalletLabel,s=ret) + if lbl: + return lbl + else: + msg('Invalid label. Trying again...') + else: + return old_lbl or MMGenWalletLabel('No Label') + + # logic identical to _get_hash_preset() + def _get_label(self): + if hasattr(self,'ss_in') and hasattr(self.ss_in.ssdata,'label'): + old_lbl = self.ss_in.ssdata.label + if opt.keep_label: + lbl = old_lbl + qmsg('Reusing label {} at user request'.format( lbl.hl(encl="''") )) + elif self.label: + lbl = self.label + qmsg('Using label {} requested on command line'.format( lbl.hl(encl="''") )) + else: # Prompt, using old value as default + lbl = self._get_label_from_user(old_lbl) + if (not opt.keep_label) and self.op == 'pwchg_new': + qmsg('Label {}'.format( 'unchanged' if lbl == old_lbl else f'changed to {lbl!r}' )) + elif self.label: + lbl = self.label + qmsg('Using label {} requested on command line'.format( lbl.hl(encl="''") )) + else: + lbl = self._get_label_from_user() + self.ssdata.label = lbl + + def _encrypt(self): + self._get_first_pw_and_hp_and_encrypt_seed() + self._get_label() + d = self.ssdata + d.pw_status = ('NE','E')[len(d.passwd)==0] + d.timestamp = make_timestamp() + + def _format(self): + d = self.ssdata + s = self.seed + bc = baseconv('b58') + slt_fmt = bc.frombytes(d.salt,pad='seed',tostr=True) + es_fmt = bc.frombytes(d.enc_seed,pad='seed',tostr=True) + lines = ( + d.label, + '{} {} {} {} {}'.format( s.sid.lower(), d.key_id.lower(), s.bitlen, d.pw_status, d.timestamp ), + '{}: {} {} {}'.format( d.hash_preset, *crypto.get_hash_params(d.hash_preset) ), + '{} {}'.format( make_chksum_6(slt_fmt), split_into_cols(4,slt_fmt) ), + '{} {}'.format( make_chksum_6(es_fmt), split_into_cols(4,es_fmt) ) + ) + chksum = make_chksum_6(' '.join(lines).encode()) + self.fmt_data = '\n'.join((chksum,)+lines) + '\n' + + def _deformat(self): + + def check_master_chksum(lines,desc): + + if len(lines) != 6: + msg(f'Invalid number of lines ({len(lines)}) in {desc} data') + return False + + if not is_chksum_6(lines[0]): + msg(f'Incorrect master checksum ({lines[0]}) in {desc} data') + return False + + chk = make_chksum_6(' '.join(lines[1:])) + if not compare_chksums(lines[0],'master',chk,'computed', + hdr='For wallet master checksum',verbose=True): + return False + + return True + + lines = self.fmt_data.splitlines() + if not check_master_chksum(lines,self.desc): + return False + + d = self.ssdata + d.label = MMGenWalletLabel(lines[1]) + + d1,d2,d3,d4,d5 = lines[2].split() + d.seed_id = d1.upper() + d.key_id = d2.upper() + self.check_usr_seed_len(int(d3)) + d.pw_status,d.timestamp = d4,d5 + + hpdata = lines[3].split() + + d.hash_preset = hp = hpdata[0][:-1] # a string! + qmsg(f'Hash preset of wallet: {hp!r}') + if opt.hash_preset and opt.hash_preset != hp: + qmsg(f'Warning: ignoring user-requested hash preset {opt.hash_preset!r}') + + hash_params = tuple(map(int,hpdata[1:])) + + if hash_params != crypto.get_hash_params(d.hash_preset): + msg(f'Hash parameters {" ".join(hash_params)!r} don’t match hash preset {d.hash_preset!r}') + return False + + lmin,foo,lmax = sorted(baseconv('b58').seedlen_map_rev) # 22,33,44 + for i,key in (4,'salt'),(5,'enc_seed'): + l = lines[i].split(' ') + chk = l.pop(0) + b58_val = ''.join(l) + + if len(b58_val) < lmin or len(b58_val) > lmax: + msg(f'Invalid format for {key} in {self.desc}: {l}') + return False + + if not compare_chksums(chk,key, + make_chksum_6(b58_val),'computed checksum',verbose=True): + return False + + val = baseconv('b58').tobytes(b58_val,pad='seed') + if val == False: + msg(f'Invalid base 58 number: {b58_val}') + return False + + setattr(d,key,val) + + return True + + def _decrypt(self): + d = self.ssdata + # Needed for multiple transactions with {}-txsign + d.passwd = self._get_passphrase( + add_desc = os.path.basename(self.infile.name) if opt.quiet else '' ) + key = crypto.make_key( d.passwd, d.salt, d.hash_preset ) + ret = crypto.decrypt_seed( d.enc_seed, key, d.seed_id, d.key_id ) + if ret: + self.seed = Seed(ret) + return True + else: + return False + + def _filename(self): + s = self.seed + d = self.ssdata + return '{}-{}[{},{}]{x}.{}'.format( + s.fn_stem, + d.key_id, + s.bitlen, + d.hash_preset, + self.ext, + x='-α' if g.debug_utf8 else '') diff --git a/mmgen/wallet/mmhex.py b/mmgen/wallet/mmhex.py new file mode 100755 index 00000000..c25861e4 --- /dev/null +++ b/mmgen/wallet/mmhex.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +# +# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet +# Copyright (C)2013-2022 The MMGen Project +# Licensed under the GNU General Public License, Version 3: +# https://www.gnu.org/licenses +# Public project repositories: +# https://github.com/mmgen/mmgen +# https://gitlab.com/mmgen/mmgen + +""" +wallet.mmhex: MMGen hexadecimal file wallet class +""" + +from ..util import make_chksum_6,split_into_cols +from ..seed import Seed +from ..util import msg,vmsg_r,is_chksum_6,is_hex_str,compare_chksums +from .unenc import wallet + +class wallet(wallet): + + stdin_ok = True + desc = 'hexadecimal seed data with checksum' + + def _format(self): + h = self.seed.hexdata + self.ssdata.chksum = make_chksum_6(h) + self.ssdata.hexseed = h + self.fmt_data = '{} {}\n'.format( + self.ssdata.chksum, + split_into_cols(4,h) ) + + def _deformat(self): + desc = self.desc + d = self.fmt_data.split() + try: + d[1] + chk,hstr = d[0],''.join(d[1:]) + except: + msg(f'{self.fmt_data.strip()!r}: invalid {desc}') + return False + + if not len(hstr)*4 in Seed.lens: + msg(f'Invalid data length ({len(hstr)}) in {desc}') + return False + + if not is_chksum_6(chk): + msg(f'{chk!r}: invalid checksum format in {desc}') + return False + + if not is_hex_str(hstr): + msg(f'{hstr!r}: not a hexadecimal string, in {desc}') + return False + + vmsg_r(f'Validating {desc} checksum...') + + if not compare_chksums(chk,'file',make_chksum_6(hstr),'computed',verbose=True): + return False + + self.seed = Seed(bytes.fromhex(hstr)) + self.ssdata.chksum = chk + self.ssdata.hexseed = hstr + + self.check_usr_seed_len() + + return True diff --git a/mmgen/wallet/mnemonic.py b/mmgen/wallet/mnemonic.py new file mode 100755 index 00000000..d068877b --- /dev/null +++ b/mmgen/wallet/mnemonic.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 +# +# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet +# Copyright (C)2013-2022 The MMGen Project +# Licensed under the GNU General Public License, Version 3: +# https://www.gnu.org/licenses +# Public project repositories: +# https://github.com/mmgen/mmgen +# https://gitlab.com/mmgen/mmgen + +""" +wallet.mnemonic: MMGen mnemonic wallet base class +""" + +from ..globalvars import g +from ..baseconv import baseconv +from ..util import msg,compare_or_die,get_data_from_user +from ..seed import Seed +from .unenc import wallet + +class wallet(wallet): + + stdin_ok = True + conv_cls = baseconv + choose_seedlen_prompt = 'Choose a mnemonic length: 1) 12 words, 2) 18 words, 3) 24 words: ' + choose_seedlen_confirm = 'Mnemonic length of {} words chosen. OK?' + + @property + def mn_lens(self): + return sorted(self.conv_cls(self.wl_id).seedlen_map_rev) + + def _get_data_from_user(self,desc): + + if not g.stdin_tty: + return get_data_from_user(desc) + + from ..mn_entry import mn_entry # import here to catch cfg var errors + mn_len = self._choose_seedlen( self.mn_lens ) + return mn_entry(self.wl_id).get_mnemonic_from_user(mn_len) + + def _format(self): + + hexseed = self.seed.hexdata + + bc = self.conv_cls(self.wl_id) + mn = bc.fromhex( hexseed, 'seed' ) + rev = bc.tohex( mn, 'seed' ) + + # Internal error, so just die on fail + compare_or_die( rev, 'recomputed seed', hexseed, 'original', e='Internal error' ) + + self.ssdata.mnemonic = mn + self.fmt_data = ' '.join(mn) + '\n' + + def _deformat(self): + + bc = self.conv_cls(self.wl_id) + mn = self.fmt_data.split() + + if len(mn) not in self.mn_lens: + msg('Invalid mnemonic ({} words). Valid numbers of words: {}'.format( + len(mn), + ', '.join(map(str,self.mn_lens)) )) + return False + + for n,w in enumerate(mn,1): + if w not in bc.digits: + msg(f'Invalid mnemonic: word #{n} is not in the {self.wl_id.upper()} wordlist') + return False + + hexseed = bc.tohex( mn, 'seed' ) + rev = bc.fromhex( hexseed, 'seed' ) + + if len(hexseed) * 4 not in Seed.lens: + msg('Invalid mnemonic (produces too large a number)') + return False + + # Internal error, so just die + compare_or_die( ' '.join(rev), 'recomputed mnemonic', ' '.join(mn), 'original', e='Internal error' ) + + self.seed = Seed(bytes.fromhex(hexseed)) + self.ssdata.mnemonic = mn + + self.check_usr_seed_len() + + return True diff --git a/mmgen/wallet/plainhex.py b/mmgen/wallet/plainhex.py new file mode 100755 index 00000000..c27a3596 --- /dev/null +++ b/mmgen/wallet/plainhex.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 +# +# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet +# Copyright (C)2013-2022 The MMGen Project +# Licensed under the GNU General Public License, Version 3: +# https://www.gnu.org/licenses +# Public project repositories: +# https://github.com/mmgen/mmgen +# https://gitlab.com/mmgen/mmgen + +""" +wallet.plainhex: plain hexadecimal wallet class +""" + +from ..util import msg,is_hex_str_lc +from ..seed import Seed +from .unenc import wallet + +class wallet(wallet): + + stdin_ok = True + desc = 'plain hexadecimal seed data' + + def _format(self): + self.fmt_data = self.seed.hexdata + '\n' + + def _deformat(self): + desc = self.desc + d = self.fmt_data.strip() + + if not is_hex_str_lc(d): + msg(f'{d!r}: not a lowercase hexadecimal string, in {desc}') + return False + + if not len(d)*4 in Seed.lens: + msg(f'Invalid data length ({len(d)}) in {desc}') + return False + + self.seed = Seed(bytes.fromhex(d)) + self.ssdata.hexseed = d + + self.check_usr_seed_len() + + return True diff --git a/mmgen/wallet/seed.py b/mmgen/wallet/seed.py new file mode 100755 index 00000000..25255f39 --- /dev/null +++ b/mmgen/wallet/seed.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +# +# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet +# Copyright (C)2013-2022 The MMGen Project +# Licensed under the GNU General Public License, Version 3: +# https://www.gnu.org/licenses +# Public project repositories: +# https://github.com/mmgen/mmgen +# https://gitlab.com/mmgen/mmgen + +""" +wallet.seed: seed file wallet class +""" + +from ..util import msg,vmsg_r,make_chksum_6,split_into_cols,is_chksum_6,compare_chksums +from ..baseconv import baseconv,is_b58_str +from ..seed import Seed +from .unenc import wallet + +class wallet(wallet): + + stdin_ok = True + desc = 'seed data' + + def _format(self): + b58seed = baseconv('b58').frombytes(self.seed.data,pad='seed',tostr=True) + self.ssdata.chksum = make_chksum_6(b58seed) + self.ssdata.b58seed = b58seed + self.fmt_data = '{} {}\n'.format( + self.ssdata.chksum, + split_into_cols(4,b58seed) ) + + def _deformat(self): + desc = self.desc + ld = self.fmt_data.split() + + if not (7 <= len(ld) <= 12): # 6 <= padded b58 data (ld[1:]) <= 11 + msg(f'Invalid data length ({len(ld)}) in {desc}') + return False + + a,b = ld[0],''.join(ld[1:]) + + if not is_chksum_6(a): + msg(f'{a!r}: invalid checksum format in {desc}') + return False + + if not is_b58_str(b): + msg(f'{b!r}: not a base 58 string, in {desc}') + return False + + vmsg_r(f'Validating {desc} checksum...') + + if not compare_chksums(a,'file',make_chksum_6(b),'computed',verbose=True): + return False + + ret = baseconv('b58').tobytes(b,pad='seed') + + if ret == False: + msg(f'Invalid base-58 encoded seed: {val}') + return False + + self.seed = Seed(ret) + self.ssdata.chksum = a + self.ssdata.b58seed = b + + self.check_usr_seed_len() + + return True diff --git a/mmgen/wallet/unenc.py b/mmgen/wallet/unenc.py new file mode 100755 index 00000000..a9ef9472 --- /dev/null +++ b/mmgen/wallet/unenc.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +# +# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet +# Copyright (C)2013-2022 The MMGen Project +# Licensed under the GNU General Public License, Version 3: +# https://www.gnu.org/licenses +# Public project repositories: +# https://github.com/mmgen/mmgen +# https://gitlab.com/mmgen/mmgen + +""" +wallet.unenc: unencrypted wallet base class +""" + +from ..globalvars import g +from ..color import blue,yellow +from ..util import msg,msg_r,capfirst,is_int,keypress_confirm +from .base import wallet + +class wallet(wallet): + + def _decrypt_retry(self): + pass + + def _encrypt(self): + pass + + def _filename(self): + s = self.seed + return '{}[{}]{x}.{}'.format( + s.fn_stem, + s.bitlen, + self.ext, + x='-α' if g.debug_utf8 else '') + + def _choose_seedlen(self,ok_lens): + + from ..term import get_char + def choose_len(): + prompt = self.choose_seedlen_prompt + while True: + r = get_char('\r'+prompt) + if is_int(r) and 1 <= int(r) <= len(ok_lens): + break + msg_r(('\r','\n')[g.test_suite] + ' '*len(prompt) + '\r') + return ok_lens[int(r)-1] + + msg('{} {}'.format( + blue(f'{capfirst(self.base_type or self.type)} type:'), + yellow(self.mn_type) + )) + + while True: + usr_len = choose_len() + prompt = self.choose_seedlen_confirm.format(usr_len) + if keypress_confirm(prompt,default_yes=True,no_nl=not g.test_suite): + return usr_len diff --git a/mmgen/wallet/words.py b/mmgen/wallet/words.py new file mode 100755 index 00000000..7b81a16d --- /dev/null +++ b/mmgen/wallet/words.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +# +# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet +# Copyright (C)2013-2022 The MMGen Project +# Licensed under the GNU General Public License, Version 3: +# https://www.gnu.org/licenses +# Public project repositories: +# https://github.com/mmgen/mmgen +# https://gitlab.com/mmgen/mmgen + +""" +wallet.words: MMGen mnemonic wallet class +""" + +from .mnemonic import wallet + +class wallet(wallet): + + desc = 'MMGen native mnemonic data' + mn_type = 'MMGen native' + wl_id = 'mmgen' diff --git a/setup.cfg b/setup.cfg index 49dd6dbd..81d2fbb4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -54,6 +54,7 @@ packages = mmgen.share mmgen.tool mmgen.tx + mmgen.wallet scripts = cmds/mmgen-addrgen diff --git a/test/overlay/__init__.py b/test/overlay/__init__.py index 30846cfa..0ae3e372 100644 --- a/test/overlay/__init__.py +++ b/test/overlay/__init__.py @@ -51,7 +51,8 @@ def overlay_setup(repo_root): 'mmgen.proto', 'mmgen.share', 'mmgen.tool', - 'mmgen.tx' ): + 'mmgen.tx', + 'mmgen.wallet' ): process_srcdir(d) return overlay_dir diff --git a/test/test_py_d/ts_input.py b/test/test_py_d/ts_input.py index 78a4d29e..0d42661f 100755 --- a/test/test_py_d/ts_input.py +++ b/test/test_py_d/ts_input.py @@ -13,7 +13,7 @@ ts_input.py: user input tests for the MMGen test.py test suite from ..include.common import * from .ts_base import * from .input import * -from mmgen.wallet import Wallet +from mmgen.wallet import get_wallet_cls class TestSuiteInput(TestSuiteBase): 'user input' @@ -207,19 +207,19 @@ class TestSuiteInput(TestSuiteBase): return t def _user_seed_entry(self,fmt,usr_rand=False,out_fmt=None,entry_mode='full',mn=None): - wcls = Wallet.fmt_code_to_type(fmt) + wcls = get_wallet_cls(fmt_code=fmt) wf = os.path.join(ref_dir,f'FE3C6545.{wcls.ext}') - if wcls.wclass == 'mnemonic': + if wcls.base_type == 'mnemonic': mn = mn or read_from_file(wf).strip().split() - elif wcls.wclass == 'dieroll': + elif wcls.type == 'dieroll': mn = mn or list(remove_whitespace(read_from_file(wf))) for idx,val in ((5,'x'),(18,'0'),(30,'7'),(44,'9')): mn.insert(idx,val) t = self.spawn('mmgen-walletconv',['-r10','-S','-i',fmt,'-o',out_fmt or fmt]) - t.expect(f'{capfirst(wcls.wclass)} type:.*{wcls.mn_type}',regex=True) + t.expect(f'{capfirst(wcls.base_type or wcls.type)} type:.*{wcls.mn_type}',regex=True) t.expect(wcls.choose_seedlen_prompt,'1') t.expect('(Y/n): ','y') - if wcls.wclass == 'mnemonic': + if wcls.base_type == 'mnemonic': t.expect('Type a number.*: ','6',regex=True) t.expect('invalid') from mmgen.mn_entry import mn_entry @@ -229,7 +229,7 @@ class TestSuiteInput(TestSuiteBase): mode = strip_ansi_escapes(t.p.match.group(1)).lower() assert mode == mne.em.name.lower(), f'{mode} != {mne.em.name.lower()}' stealth_mnemonic_entry(t,mne,mn,entry_mode=entry_mode) - elif wcls.wclass == 'dieroll': + elif wcls.type == 'dieroll': user_dieroll_entry(t,mn) if usr_rand: t.expect(wcls.user_entropy_prompt,'y') diff --git a/test/test_py_d/ts_main.py b/test/test_py_d/ts_main.py index a889d01c..0481959d 100755 --- a/test/test_py_d/ts_main.py +++ b/test/test_py_d/ts_main.py @@ -23,7 +23,9 @@ ts_main.py: Basic operations tests for the test.py test suite from mmgen.globalvars import g from mmgen.opts import opt from mmgen.fileutil import get_data_from_file,write_data_to_file -from mmgen.wallet import Wallet,MMGenWallet,MMGenMnemonic,IncogWallet,MMGenSeedFile +from mmgen.wallet import get_wallet_cls +from mmgen.wallet.mmgen import wallet as MMGenWallet +from mmgen.wallet.incog import wallet as IncogWallet from mmgen.rpc import rpc_init from ..include.common import * from .common import * @@ -277,8 +279,8 @@ class TestSuiteMain(TestSuiteBase,TestSuiteShared): return t def subwalletgen_mnemonic(self,wf): - icls = Wallet.ext_to_type(get_extension(wf)) - ocls = MMGenMnemonic + icls = get_wallet_cls(ext=get_extension(wf)) + ocls = get_wallet_cls('words') args = [self.usr_rand_arg,'-p1','-d',self.tr.trash_dir,'-o',ocls.fmt_codes[0],wf,'3L'] t = self.spawn('mmgen-subwalletgen', args) t.license() @@ -579,33 +581,34 @@ class TestSuiteMain(TestSuiteBase,TestSuiteShared): t.license() if not pf: - icls = Wallet.ext_to_type(get_extension(wf)) + icls = get_wallet_cls(ext=get_extension(wf)) t.passphrase(icls.desc,self.wpasswd) - ocls = Wallet.fmt_code_to_type(out_fmt) + ocls = get_wallet_cls(fmt_code=out_fmt) - if issubclass(ocls,WalletEnc) and ocls != Brainwallet: + if ocls.enc and ocls.type != 'brain': t.passphrase_new('new '+ocls.desc,self.wpasswd) t.usr_rand(self.usr_rand_chars) - if ocls.__name__.startswith('Incog'): + if ocls.type.startswith('incog'): m = 'Encrypting random data generated by your operating system with key' t.expect(m) t.expect(m) incog_id = t.expect_getend('New Incog Wallet ID: ') t.expect(m) - if ocls == IncogWalletHidden: + if ocls.type == 'incog_hidden': self.write_to_tmpfile(incog_id_fn,incog_id) t.hincog_create(hincog_bytes) - elif ocls == MMGenWallet: + elif ocls.type == 'mmgen': t.label() + return t.written_to_file(capfirst(ocls.desc),oo=True),t def export_seed(self,wf,out_fmt='seed',pf=None): f,t = self._walletconv_export(wf,out_fmt=out_fmt,pf=pf) silence() - wcls = Wallet.fmt_code_to_type(out_fmt) + wcls = get_wallet_cls(fmt_code=out_fmt) msg('==> {}: {}'.format( wcls.desc, cyan(get_data_from_file(f,wcls.desc)) )) @@ -636,8 +639,8 @@ class TestSuiteMain(TestSuiteBase,TestSuiteShared): return self.export_incog(wf,out_fmt='hi',add_args=add_args) def addrgen_seed(self,wf,foo,in_fmt='seed'): - wcls = Wallet.fmt_code_to_type(in_fmt) - stdout = wcls == MMGenSeedFile # capture output to screen once + wcls = get_wallet_cls(fmt_code=in_fmt) + stdout = wcls.type == 'seed' # capture output to screen once t = self.spawn( 'mmgen-addrgen', (['-S'] if stdout else []) + @@ -665,7 +668,7 @@ class TestSuiteMain(TestSuiteBase,TestSuiteShared): ([],[wf])[bool(wf)] + [self.addr_idx_list]) t.license() t.expect_getend('Incog Wallet ID: ') - wcls = Wallet.fmt_code_to_type(in_fmt) + wcls = get_wallet_cls(fmt_code=in_fmt) t.hash_preset(wcls.desc,'1') t.passphrase(rf'{wcls.desc} \w{{8}}',self.wpasswd) vmsg('Comparing generated checksum with checksum from address file') @@ -707,7 +710,7 @@ class TestSuiteMain(TestSuiteBase,TestSuiteShared): t = self.spawn('mmgen-txsign', ['-d',self.tmpdir,txf1,wf1,txf2,wf2]) t.license() for cnum,wf in (('1',wf1),('2',wf2)): - wcls = Wallet.ext_to_type(get_extension(wf)) + wcls = get_wallet_cls(ext=get_extension(wf)) t.view_tx('n') t.passphrase(wcls.desc,self.cfgs[cnum]['wpasswd']) self.txsign_end(t,cnum) @@ -730,7 +733,7 @@ class TestSuiteMain(TestSuiteBase,TestSuiteShared): t.license() t.view_tx('n') for cnum,wf in (('1',wf1),('3',wf2)): - wcls = Wallet.ext_to_type(get_extension(wf)) + wcls = get_wallet_cls(ext=get_extension(wf)) t.passphrase(wcls.desc,self.cfgs[cnum]['wpasswd']) self.txsign_end(t) return t @@ -743,7 +746,7 @@ class TestSuiteMain(TestSuiteBase,TestSuiteShared): bwf = joinpath(self.tmpdir,self.bw_filename) make_brainwallet_file(bwf) seed_len = str(self.seed_len) - args = ['-d',self.tmpdir,'-p1',self.usr_rand_arg,'-l'+seed_len,'-ib'] + args = ['-d',self.tmpdir,'-p1',self.usr_rand_arg,'-l'+seed_len,'-ibw'] t = self.spawn('mmgen-walletconv', args + [bwf]) t.license() wcls = MMGenWallet @@ -830,7 +833,7 @@ class TestSuiteMain(TestSuiteBase,TestSuiteShared): t = self.spawn('mmgen-txsign', add_args + ['-d',self.tmpdir,'-k',non_mm_file,txf,wf]) t.license() t.view_tx('n') - wcls = Wallet.ext_to_type(get_extension(wf)) + wcls = get_wallet_cls(ext=get_extension(wf)) t.passphrase(wcls.desc,self.cfgs['20']['wpasswd']) if bad_vsize: t.expect('Estimated transaction vsize') diff --git a/test/test_py_d/ts_ref.py b/test/test_py_d/ts_ref.py index 409bc779..193ac858 100755 --- a/test/test_py_d/ts_ref.py +++ b/test/test_py_d/ts_ref.py @@ -23,7 +23,7 @@ ts_ref.py: Reference file tests for the test.py test suite import os from mmgen.globalvars import g from mmgen.opts import opt -from mmgen.wallet import MMGenMnemonic +from mmgen.wallet import get_wallet_cls from ..include.common import * from .common import * @@ -163,7 +163,7 @@ class TestSuiteRef(TestSuiteBase,TestSuiteShared): def ref_words_to_subwallet_chk(self,ss_idx): wf = dfl_words_file - ocls = MMGenMnemonic + ocls = get_wallet_cls('words') args = ['-d',self.tr.trash_dir,'-o',ocls.fmt_codes[-1],wf,ss_idx] t = self.spawn('mmgen-subwalletgen',args,extra_desc='(generate subwallet)') diff --git a/test/test_py_d/ts_ref_3seed.py b/test/test_py_d/ts_ref_3seed.py index 808717a8..6c7cf210 100755 --- a/test/test_py_d/ts_ref_3seed.py +++ b/test/test_py_d/ts_ref_3seed.py @@ -23,7 +23,7 @@ ts_ref_3seed.py: Saved and generated reference file tests for 128, 192 and from mmgen.globalvars import g from mmgen.opts import opt -from mmgen.wallet import * +from mmgen.wallet import get_wallet_cls from ..include.common import * from .common import * from .ts_base import * @@ -89,22 +89,22 @@ class TestSuiteRef3Seed(TestSuiteBase,TestSuiteShared): return self.walletchk(wf,pf=None,wcls=ss,sid=self.seed_id) def ref_seed_chk(self): - return self.ref_ss_chk(ss=MMGenSeedFile) + return self.ref_ss_chk(ss=get_wallet_cls('seed')) def ref_hex_chk(self): - return self.ref_ss_chk(ss=MMGenHexSeedFile) + return self.ref_ss_chk(ss=get_wallet_cls('mmhex')) def ref_plainhex_chk(self): - return self.ref_ss_chk(ss=PlainHexSeedFile) + return self.ref_ss_chk(ss=get_wallet_cls('plainhex')) def ref_dieroll_chk(self): - return self.ref_ss_chk(ss=DieRollSeedFile) + return self.ref_ss_chk(ss=get_wallet_cls('dieroll')) def ref_mn_chk(self): - return self.ref_ss_chk(ss=MMGenMnemonic) + return self.ref_ss_chk(ss=get_wallet_cls('words')) def ref_bip39_chk(self): - return self.ref_ss_chk(ss=BIP39Mnemonic) + return self.ref_ss_chk(ss=get_wallet_cls('bip39')) def ref_hincog_chk(self,desc='hidden incognito data'): source = TestSuiteWalletConv.sources[str(self.seed_len)] @@ -141,7 +141,7 @@ class TestSuiteRef3Seed(TestSuiteBase,TestSuiteShared): t = self.spawn('mmgen-walletconv', args + [self.usr_rand_arg]) t.license() t.expect('Enter brainwallet: ', ref_wallet_brainpass+'\n') - ocls = MMGenWallet + ocls = get_wallet_cls('mmgen') t.passphrase_new('new '+ocls.desc,self.wpasswd) t.usr_rand(self.usr_rand_chars) fn = os.path.split(t.written_to_file(capfirst(ocls.desc)))[-1] @@ -160,7 +160,7 @@ class TestSuiteRef3Seed(TestSuiteBase,TestSuiteShared): wf = self.get_file_with_ext('mmdat') pf = joinpath(self.tmpdir,pwfile) t = self.spawn('mmgen-walletconv',extra_args+['-d','test/trash','-o',ofmt,'-P'+pf,wf]) - wcls = Wallet.fmt_code_to_type(ofmt) + wcls = get_wallet_cls(fmt_code=ofmt) fn = os.path.split(t.written_to_file(capfirst(wcls.desc)))[-1] idx = int(self.test_name[-1]) - 1 sid = self.chk_data['sids'][idx] diff --git a/test/test_py_d/ts_regtest.py b/test/test_py_d/ts_regtest.py index 2b0f3492..ff829e47 100755 --- a/test/test_py_d/ts_regtest.py +++ b/test/test_py_d/ts_regtest.py @@ -27,14 +27,15 @@ from mmgen.opts import opt from mmgen.util import die,gmsg from mmgen.protocol import init_proto from mmgen.addrlist import AddrList -from mmgen.wallet import MMGenWallet +from mmgen.wallet import Wallet,get_wallet_cls from ..include.common import * from .common import * pat_date = r'\b\d\d-\d\d-\d\d\b' pat_date_time = r'\b\d\d\d\d-\d\d-\d\d\s+\d\d:\d\d\b' -dfl_wcls = MMGenWallet +dfl_wcls = get_wallet_cls('mmgen') + rt_pw = 'abc-α' rt_data = { 'tx_fee': {'btc':'0.0001','bch':'0.001','ltc':'0.01'}, @@ -338,7 +339,7 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared): return os.path.basename(get_file_with_ext(self._user_dir(user),'mmdat'))[:8] def _get_user_subsid(self,user,subseed_idx): - fn = get_file_with_ext(self._user_dir(user),MMGenWallet.ext) + fn = get_file_with_ext(self._user_dir(user),dfl_wcls.ext) silence() w = Wallet( fn=fn, passwd_file=os.path.join(self.tmpdir,'wallet_password') ) end_silence() diff --git a/test/test_py_d/ts_seedsplit.py b/test/test_py_d/ts_seedsplit.py index 5028e9a3..8533ed79 100755 --- a/test/test_py_d/ts_seedsplit.py +++ b/test/test_py_d/ts_seedsplit.py @@ -22,7 +22,7 @@ ts_seedsplit.py: Seed split/join tests for the test.py test suite from mmgen.globalvars import g from mmgen.opts import opt -from mmgen.wallet import Wallet,MMGenWallet,IncogWallet,IncogWalletHex,IncogWalletHidden,WalletEnc +from mmgen.wallet import get_wallet_cls from .ts_base import * @@ -30,7 +30,7 @@ ref_wf = 'test/ref/98831F3A.bip39' ref_sid = '98831F3A' wpasswd = 'abc' sh1_passwd = 'xyz' -dfl_wcls = MMGenWallet +dfl_wcls = get_wallet_cls('mmgen') class TestSuiteSeedSplit(TestSuiteBase): 'splitting and joining seeds' @@ -111,11 +111,11 @@ class TestSuiteSeedSplit(TestSuiteBase): else: pat = f'master share #{master}' t.expect(pat,regex=True) - ocls = Wallet.fmt_code_to_type(ofmt) - if issubclass(ocls,WalletEnc): + ocls = get_wallet_cls(fmt_code=ofmt) + if ocls.enc: t.hash_preset('new '+ocls.desc,'1') t.passphrase_new('new '+ocls.desc,sh1_passwd) - if ocls == IncogWalletHidden: + if ocls.type == 'incog_hidden': t.hincog_create(1234) t.written_to_file(capfirst(ocls.desc)) return t @@ -134,23 +134,26 @@ class TestSuiteSeedSplit(TestSuiteBase): + shares) if bad_invocation: return t - icls = ( MMGenWallet if 'mmdat' in in_exts - else IncogWallet if 'mmincog' in in_exts - else IncogWalletHex if 'mmincox' in in_exts - else IncogWalletHidden if '-H' in add_args + icls = ( dfl_wcls if 'mmdat' in in_exts + else get_wallet_cls('incog') if 'mmincog' in in_exts + else get_wallet_cls('incog_hex') if 'mmincox' in in_exts + else get_wallet_cls('incog_hidden') if '-H' in add_args else None ) - if icls in (IncogWallet,IncogWalletHex,IncogWalletHidden): + if icls.type.startswith('incog'): t.hash_preset(icls.desc,'1') if icls: t.passphrase(icls.desc,sh1_passwd) if master: fs = "master share #{}, split id.*'{}'.*, share count {}" - pat = fs.format(master,id_str or 'default',len(shares)+(icls==IncogWalletHidden)) + pat = fs.format( + master, + id_str or 'default', + len(shares) + (icls.type=='incog_hidden') ) t.expect(pat,regex=True) sid_cmp = strip_ansi_escapes(t.expect_getend('Joined Seed ID: ')) cmp_or_die(sid,sid_cmp) - ocls = Wallet.fmt_code_to_type(ofmt) - if ocls == MMGenWallet: + ocls = get_wallet_cls(fmt_code=ofmt) + if ocls.type == 'mmgen': t.hash_preset('new '+ocls.desc,'1') t.passphrase_new('new '+ocls.desc,wpasswd) t.written_to_file(capfirst(ocls.desc)) diff --git a/test/test_py_d/ts_shared.py b/test/test_py_d/ts_shared.py index 1e5e6690..6da24cbc 100755 --- a/test/test_py_d/ts_shared.py +++ b/test/test_py_d/ts_shared.py @@ -24,7 +24,8 @@ import os from mmgen.globalvars import g from mmgen.opts import opt from mmgen.util import ymsg -from mmgen.wallet import Wallet,WalletEnc,Brainwallet,MMGenWallet,IncogWalletHidden +from mmgen.wallet import get_wallet_cls + from ..include.common import * from .common import * @@ -167,8 +168,8 @@ class TestSuiteShared(object): t = self.spawn('mmgen-txsign', opts, extra_desc) t.license() t.view_tx(view) - wcls = MMGenWallet if dfl_wallet else Wallet.ext_to_type(get_extension(wf)) - if issubclass(wcls,WalletEnc) and wcls != Brainwallet: + wcls = get_wallet_cls( ext = 'mmdat' if dfl_wallet else get_extension(wf) ) + if wcls.enc and wcls.type != 'brain': t.passphrase(wcls.desc,self.wpasswd) if save: self.txsign_end(t,has_label=has_label) @@ -185,15 +186,15 @@ class TestSuiteShared(object): def walletchk(self,wf,pf,wcls=None,add_args=[],sid=None,extra_desc='',dfl_wallet=False): hp = self.hash_preset if hasattr(self,'hash_preset') else '1' - wcls = wcls or Wallet.ext_to_type(get_extension(wf)) + wcls = wcls or get_wallet_cls(ext=get_extension(wf)) t = self.spawn('mmgen-walletchk', ([] if dfl_wallet else ['-i',wcls.fmt_codes[0]]) + add_args + ['-p',hp] + ([wf] if wf else []), extra_desc=extra_desc) - if wcls != IncogWalletHidden: + if wcls.type != 'incog_hidden': t.expect(f"Getting {wcls.desc} from file '") - if issubclass(wcls,WalletEnc) and wcls != Brainwallet: + if wcls.enc and wcls.type != 'brain': t.passphrase(wcls.desc,self.wpasswd) t.expect(['Passphrase is OK', 'Passphrase.* are correct'],regex=True) chk = t.expect_getend(f'Valid {wcls.desc} for Seed ID ')[:8] @@ -223,7 +224,7 @@ class TestSuiteShared(object): [getattr(self,f'{cmd_pfx}_idx_list')], extra_desc=f'({mmtype})' if mmtype in ('segwit','bech32') else '') t.license() - wcls = MMGenWallet if dfl_wallet else Wallet.ext_to_type(get_extension(wf)) + wcls = get_wallet_cls( ext = 'mmdat' if dfl_wallet else get_extension(wf) ) t.passphrase(wcls.desc,self.wpasswd) t.expect('Passphrase is OK') desc = ('address','password')[passgen] @@ -245,7 +246,7 @@ class TestSuiteShared(object): ([],['--type='+str(mmtype)])[bool(mmtype)] + args, extra_desc=f'({mmtype})' if mmtype in ('segwit','bech32') else '') t.license() - wcls = Wallet.ext_to_type(get_extension(wf)) + wcls = get_wallet_cls(ext=get_extension(wf)) t.passphrase(wcls.desc,self.wpasswd) chk = t.expect_getend(r'Checksum for key-address data .*?: ',regex=True) if check_ref: diff --git a/test/test_py_d/ts_wallet.py b/test/test_py_d/ts_wallet.py index 35811851..1846de9b 100755 --- a/test/test_py_d/ts_wallet.py +++ b/test/test_py_d/ts_wallet.py @@ -22,7 +22,7 @@ ts_wallet.py: Wallet conversion tests for the test.py test suite import os from mmgen.opts import opt -from mmgen.wallet import * +from mmgen.wallet import get_wallet_cls from .common import * from .ts_base import * from .ts_shared import * @@ -107,7 +107,7 @@ class TestSuiteWalletConv(TestSuiteBase,TestSuiteShared): def ref_brain_conv(self): uopts = ['-i','bw','-p','1','-l',str(self.seed_len)] - return self.walletconv_in(None,uopts,oo=True,icls=Brainwallet) + return self.walletconv_in(None,uopts,oo=True,icls=get_wallet_cls('brain')) def ref_incog_conv(self,wfk='ic_wallet',in_fmt='i'): uopts = ['-i',in_fmt,'-p','1','-l',str(self.seed_len)] @@ -125,7 +125,7 @@ class TestSuiteWalletConv(TestSuiteBase,TestSuiteShared): None, uopts + hi_opt, oo = True, - icls = IncogWalletHidden ) + icls = get_wallet_cls('incog_hidden') ) def ref_hincog_conv_old(self): return self.ref_hincog_conv(wfk='hic_wallet_old',add_uopts=['-O']) @@ -173,16 +173,16 @@ class TestSuiteWalletConv(TestSuiteBase,TestSuiteShared): # wallet conversion tests def walletconv_in(self,infile,uopts=[],oo=False,icls=None): - ocls = MMGenMnemonic + ocls = get_wallet_cls('words') opts = ['-d',self.tmpdir,'-o',ocls.fmt_codes[0],self.usr_rand_arg] if_arg = [infile] if infile else [] d = '(convert)' t = self.spawn('mmgen-walletconv',opts+uopts+if_arg,extra_desc=d) t.license() - icls = icls or Wallet.ext_to_type(get_extension(infile)) - if icls == Brainwallet: + icls = icls or get_wallet_cls(ext=get_extension(infile)) + if icls.type == 'brain': t.expect('Enter brainwallet: ',ref_wallet_brainpass+'\n') - if issubclass(icls,WalletEnc) and icls != Brainwallet: + if icls.enc and icls.type != 'brain': t.passphrase(icls.desc,self.wpasswd) if self.test_name[:19] == 'ref_hincog_conv_old': t.expect('Is the Seed ID correct? (Y/n): ','\n') @@ -198,27 +198,27 @@ class TestSuiteWalletConv(TestSuiteBase,TestSuiteShared): sid = self.seed_id ) def walletconv_out(self,out_fmt='w',uopts=[],uopts_chk=[]): - wcls = Wallet.fmt_code_to_type(out_fmt) + wcls = get_wallet_cls(fmt_code=out_fmt) opts = ['-d',self.tmpdir,'-p1','-o',out_fmt] + uopts infile = joinpath(ref_dir,self.seed_id+'.mmwords') t = self.spawn('mmgen-walletconv',[self.usr_rand_arg]+opts+[infile],extra_desc='(convert)') add_args = [f'-l{self.seed_len}'] t.license() - if issubclass(wcls,WalletEnc) and wcls != Brainwallet: + if wcls.enc and wcls.type != 'brain': t.passphrase_new('new '+wcls.desc,self.wpasswd) t.usr_rand(self.usr_rand_chars) - if wcls in (IncogWallet,IncogWalletHex,IncogWalletHidden): + if wcls.type.startswith('incog'): for i in (1,2,3): t.expect('Encrypting random data generated by your operating system with key') - if wcls == IncogWalletHidden: + if wcls.type == 'incog_hidden': t.hincog_create(hincog_bytes) if out_fmt == 'w': t.label() wf = t.written_to_file(capfirst(wcls.desc),oo=True) pf = None - if wcls == IncogWalletHidden: + if wcls.type == 'incog_hidden': add_args += uopts_chk wf = None msg('' if opt.profile else ' OK')