Skip to content

Commit

Permalink
new Lockable class; lock global vars after initialization (amended)
Browse files Browse the repository at this point in the history
  • Loading branch information
mmgen committed May 29, 2020
1 parent 973c7be commit 4c2410e
Show file tree
Hide file tree
Showing 25 changed files with 422 additions and 206 deletions.
2 changes: 1 addition & 1 deletion mmgen/altcoins/eth/tw.py
Expand Up @@ -244,7 +244,7 @@ class EthereumTwUnspentOutputs(TwUnspentOutputs):
'l':'a_lbl_add','D':'a_addr_delete','R':'a_balance_refresh' }

async def __ainit__(self,proto,*args,**kwargs):
if g.use_cached_balances:
if g.cached_balances:
self.hdr_fmt += '\n' + yellow('WARNING: Using cached balances. These may be out of date!')
await TwUnspentOutputs.__ainit__(self,proto,*args,**kwargs)

Expand Down
88 changes: 88 additions & 0 deletions mmgen/base_obj.py
@@ -0,0 +1,88 @@
#!/usr/bin/env python3
#
# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
# Copyright (C)2013-2020 The MMGen Project <mmgen@tuta.io>
#
# 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 <http://www.gnu.org/licenses/>.

"""
base_obj.py: base objects with no internal imports for the MMGen suite
"""

class AttrCtrl:
"""
After instance is locked, forbid setting any attribute if the attribute is not present
in either the class or instance dict.
If _use_class_attr is True, ensure that attribute's type matches that of the class
attribute, unless the class attribute is set to None, in which case no type checking
is performed.
"""
_lock = False
_use_class_attr = False

def lock(self):
self._lock = True

def __setattr__(self,name,value):
if self._lock:
def do_error(name,value,ref_val):
raise AttributeError(
f'{value!r}: invalid value for attribute {name!r}'
+ ' of {} object (must be of type {}, not {})'.format(
type(self).__name__,
type(ref_val).__name__,
type(value).__name__ ) )

if not hasattr(self,name):
raise AttributeError(f'{type(self).__name__} object has no attribute {name!r}')

ref_val = getattr(type(self),name) if self._use_class_attr else getattr(self,name)

if (ref_val is not None) and not isinstance(value,type(ref_val)):
do_error(name,value,ref_val)

return object.__setattr__(self,name,value)

def __delattr__(self,name,value):
raise AttributeError('attribute cannot be deleted')

class Lockable(AttrCtrl):
"""
After instance is locked, its attributes become read-only, with the following exceptions:
- if the attribute's name is in _set_ok, attr can be set once after locking, if unset
- if the attribute's name is in _reset_ok, read-only restrictions are bypassed and only
AttrCtrl checking is performed
To determine whether an attribute is set, it's matched against either None or the class attribute,
if _use_class_attr is True
"""
_set_ok = ()
_reset_ok = ()

def __setattr__(self,name,value):
if self._lock and hasattr(self,name):
if name not in (self._set_ok + self._reset_ok):
raise AttributeError(f'attribute {name!r} of {type(self).__name__} object is read-only')
elif name not in self._reset_ok:
#print(self.__dict__)
if not (
getattr(self,name) is None or
( self._use_class_attr and name not in self.__dict__ ) ):
raise AttributeError(
f'attribute {name!r} of {type(self).__name__} object is already set,'
+ ' and resetting is forbidden' )
# name is in (_set_ok + _reset_ok) -- allow name to be in both lists

return AttrCtrl.__setattr__(self,name,value)
39 changes: 34 additions & 5 deletions mmgen/crypto.py
Expand Up @@ -216,14 +216,44 @@ def get_hash_preset_from_user(hp=g.dfl_hash_preset,desc='data'):
else:
return hp

_salt_len,_sha256_len,_nonce_len = 32,32,32
def get_new_passphrase(desc,passchg=False):

w = '{}passphrase for {}'.format(('','new ')[bool(passchg)], desc)
if opt.passwd_file:
pw = ' '.join(get_words_from_file(opt.passwd_file,w))
elif opt.echo_passphrase:
pw = ' '.join(get_words_from_user(f'Enter {w}: '))
else:
for i in range(g.passwd_max_tries):
pw = ' '.join(get_words_from_user(f'Enter {w}: '))
pw_chk = ' '.join(get_words_from_user('Repeat passphrase: '))
dmsg(f'Passphrases: [{pw}] [{pw_chk}]')
if pw == pw_chk:
vmsg('Passphrases match'); break
else: msg('Passphrases do not match. Try again.')
else:
die(2,f'User failed to duplicate passphrase in {g.passwd_max_tries} attempts')

if pw == '':
qmsg('WARNING: Empty passphrase')

return pw

def get_passphrase(desc,passchg=False):
prompt ='Enter {}passphrase for {}: '.format(('','old ')[bool(passchg)],desc)
if opt.passwd_file:
pwfile_reuse_warning(opt.passwd_file)
return ' '.join(get_words_from_file(opt.passwd_file,'passphrase'))
else:
return ' '.join(get_words_from_user(prompt))

_salt_len,_sha256_len,_nonce_len = (32,32,32)

def mmgen_encrypt(data,desc='data',hash_preset=''):
salt = get_random(_salt_len)
iv = get_random(g.aesctr_iv_len)
nonce = get_random(_nonce_len)
hp = hash_preset or (
opt.hash_preset if 'hash_preset' in opt.set_by_user else get_hash_preset_from_user('3',desc))
hp = hash_preset or opt.hash_preset or get_hash_preset_from_user('3',desc)
m = ('user-requested','default')[hp=='3']
vmsg(f'Encrypting {desc}')
qmsg(f'Using {m} hash preset of {hp!r}')
Expand All @@ -238,8 +268,7 @@ def mmgen_decrypt(data,desc='data',hash_preset=''):
salt = data[:_salt_len]
iv = data[_salt_len:dstart]
enc_d = data[dstart:]
hp = hash_preset or (
opt.hash_preset if 'hash_preset' in opt.set_by_user else get_hash_preset_from_user('3',desc))
hp = hash_preset or opt.hash_preset or get_hash_preset_from_user('3',desc)
m = ('user-requested','default')[hp=='3']
qmsg(f'Using {m} hash preset of {hp!r}')
passwd = get_passphrase(desc)
Expand Down
31 changes: 23 additions & 8 deletions mmgen/globalvars.py
Expand Up @@ -25,19 +25,25 @@
from collections import namedtuple
from .devtools import *

from .base_obj import Lockable

def die(exit_val,s=''):
if s:
sys.stderr.write(s+'\n')
sys.exit(exit_val)

class GlobalContext:
class GlobalContext(Lockable):
"""
Set global vars to default values
Globals are overridden in this order:
1 - config file
2 - environmental vars
3 - command line
"""
_set_ok = ('user_entropy','session')
_reset_ok = ('stdout','stderr','accept_defaults')
_use_class_attr = True

# Constants:
version = '0.12.099'
release_date = 'May 2020'
Expand All @@ -63,12 +69,12 @@ class GlobalContext:
# Variables - these might be altered at runtime:

user_entropy = b''
hash_preset = '3'
dfl_hash_preset = '3'
dfl_seed_len = 256
usr_randchars = 30

tx_fee_adj = Decimal('1.0')
tx_confs = 3
seed_len = 256

# Constant vars - some of these might be overridden in opts.py, but they don't change thereafter

Expand Down Expand Up @@ -97,7 +103,8 @@ class GlobalContext:
monero_wallet_rpc_password = ''
rpc_fail_on_command = ''
aiohttp_rpc_queue_len = 16
use_cached_balances = False
session = None
cached_balances = False

# regtest:
bob = False
Expand Down Expand Up @@ -141,11 +148,12 @@ class GlobalContext:
daemon_data_dir = '' # set by user

# global var sets user opt:
global_sets_opt = ( 'minconf','seed_len','hash_preset','usr_randchars','debug',
'quiet','tx_confs','tx_fee_adj','key_generator' )
global_sets_opt = (
'minconf','usr_randchars','debug', 'quiet','tx_confs','tx_fee_adj','key_generator' )

# user opt sets global var:
opt_sets_global = ( 'use_internal_keccak_module','subseeds' )
opt_sets_global = (
'use_internal_keccak_module','subseeds','cached_balances' )

# 'long' opts - opt sets global var
common_opts = (
Expand All @@ -160,7 +168,7 @@ class GlobalContext:
'quiet','verbose','debug','outdir','echo_passphrase','passwd_file','stdout',
'show_hash_presets','label','keep_passphrase','keep_hash_preset','yes',
'brain_params','b16','usr_randchars','coin','bob','alice','key_generator',
'hidden_incog_input_params','in_fmt'
'hidden_incog_input_params','in_fmt','hash_preset','seed_len',
)
incompatible_opts = (
('help','longhelp'),
Expand Down Expand Up @@ -229,6 +237,10 @@ class GlobalContext:
if platform == 'win':
autoset_opts['rpc_backend'].choices.remove('aiohttp')

auto_typeset_opts = {
'seed_len': int,
}

min_screen_width = 80
minconf = 1
max_tx_file_size = 100000
Expand Down Expand Up @@ -271,6 +283,9 @@ class GlobalContext:
short_disp_timeout = 0.1
if os.getenv('MMGEN_TEST_SUITE_POPEN_SPAWN'):
stdin_tty = True
if prog_name == 'unit_tests.py':
_set_ok += ('debug_subseed',)
_reset_ok += ('force_standalone_scrypt_module','session')

if os.getenv('MMGEN_DEBUG_ALL'):
for name in env_opts:
Expand Down
4 changes: 2 additions & 2 deletions mmgen/main_addrgen.py
Expand Up @@ -65,9 +65,9 @@
Options: {kgs} (default: {kg})
-l, --seed-len= l Specify wallet seed length of 'l' bits. This option
is required only for brainwallet and incognito inputs
with non-standard (< {g.seed_len}-bit) seed lengths
with non-standard (< {g.dfl_seed_len}-bit) seed lengths
-p, --hash-preset= p Use the scrypt hash parameters defined by preset 'p'
for password hashing (default: '{g.hash_preset}')
for password hashing (default: '{g.dfl_hash_preset}')
-z, --show-hash-presets Show information on available hash presets
-P, --passwd-file= f Get wallet passphrase from file 'f'
-q, --quiet Produce quieter output; suppress some warnings
Expand Down
33 changes: 16 additions & 17 deletions mmgen/main_autosign.py
Expand Up @@ -31,6 +31,8 @@
key_fn = 'autosign.key'

from .common import *
opts.UserOpts._set_ok += ('outdir','passwd_file')

prog_name = os.path.basename(sys.argv[0])
opts_data = {
'sets': [('stealth_led', True, 'led', True)],
Expand Down Expand Up @@ -107,11 +109,22 @@
}
}

cmd_args = opts.init(opts_data,add_opts=['mmgen_keys_from_file','in_fmt'])
cmd_args = opts.init(
opts_data,
add_opts = ['mmgen_keys_from_file','hidden_incog_input_params'],
init_opts = {
'quiet': True,
'in_fmt': 'words',
'out_fmt': 'wallet',
'usr_randchars': 0,
'hash_preset': '1',
'label': 'Autosign Wallet',
})

exit_if_mswin('autosigning')

import mmgen.tx
from .wallet import Wallet
from .txsign import txsign
from .protocol import init_proto
from .rpc import rpc_init
Expand All @@ -123,6 +136,7 @@
mountpoint = opt.mountpoint

opt.outdir = tx_dir = os.path.join(mountpoint,'tx')
opt.passwd_file = os.path.join(tx_dir,key_fn)

async def check_daemons_running():
if opt.coin:
Expand Down Expand Up @@ -220,15 +234,11 @@ async def sign():
return True

def decrypt_wallets():
opt.hash_preset = '1'
opt.set_by_user = ['hash_preset']
opt.passwd_file = os.path.join(tx_dir,key_fn)
from .wallet import Wallet
msg(f'Unlocking wallet{suf(wfs)} with key from {opt.passwd_file!r}')
fails = 0
for wf in wfs:
try:
Wallet(wf)
Wallet(wf,ignore_in_fmt=True)
except SystemExit as e:
if e.code != 0:
fails += 1
Expand Down Expand Up @@ -335,18 +345,7 @@ def create_wallet_dir():
def setup():
remove_wallet_dir()
gen_key(no_unmount=True)
from .wallet import Wallet
opt.hidden_incog_input_params = None
opt.quiet = True
opt.in_fmt = 'words'
ss_in = Wallet()
opt.out_fmt = 'wallet'
opt.usr_randchars = 0
opt.hash_preset = '1'
opt.set_by_user = ['hash_preset']
opt.passwd_file = os.path.join(tx_dir,key_fn)
from .obj import MMGenWalletLabel
opt.label = MMGenWalletLabel('Autosign Wallet')
ss_out = Wallet(ss=ss_in)
ss_out.write_to_file(desc='autosign wallet',outdir=wallet_dir)

Expand Down
4 changes: 2 additions & 2 deletions mmgen/main_passgen.py
Expand Up @@ -54,9 +54,9 @@
generate passwords of half the default length.
-l, --seed-len= l Specify wallet seed length of 'l' bits. This option
is required only for brainwallet and incognito inputs
with non-standard (< {g.seed_len}-bit) seed lengths
with non-standard (< {g.dfl_seed_len}-bit) seed lengths
-p, --hash-preset= p Use the scrypt hash parameters defined by preset 'p'
for password hashing (default: '{g.hash_preset}')
for password hashing (default: '{g.dfl_hash_preset}')
-z, --show-hash-presets Show information on available hash presets
-P, --passwd-file= f Get wallet passphrase from file 'f'
-q, --quiet Produce quieter output; suppress some warnings
Expand Down
5 changes: 1 addition & 4 deletions mmgen/main_seedjoin.py
Expand Up @@ -48,7 +48,7 @@
-L, --label= l Specify a label 'l' for output wallet
-M, --master-share=i Use a master share with index 'i' (min:{ms_min}, max:{ms_max})
-p, --hash-preset= p Use the scrypt hash parameters defined by preset 'p'
for password hashing (default: '{g.hash_preset}')
for password hashing (default: '{g.dfl_hash_preset}')
-z, --show-hash-presets Show information on available hash presets
-P, --passwd-file= f Get wallet passphrase from file 'f'
-q, --quiet Produce quieter output; suppress some warnings
Expand Down Expand Up @@ -110,9 +110,6 @@ def print_shares_info():
if len(cmd_args) + bool(opt.hidden_incog_input_params) < 2:
opts.usage()

if opt.label:
opt.label = MMGenWalletLabel(opt.label,msg="Error in option '--label'")

if opt.master_share:
master_idx = MasterShareIdx(opt.master_share)
id_str = SeedSplitIDString(opt.id_str or 'default')
Expand Down
4 changes: 1 addition & 3 deletions mmgen/main_tool.py
Expand Up @@ -63,7 +63,7 @@ def make_help():
--, --longhelp Print help message for long options (common options)
-k, --use-internal-keccak-module Force use of the internal keccak module
-p, --hash-preset= p Use the scrypt hash parameters defined by preset 'p'
for password hashing (default: '{g.hash_preset}')
for password hashing (default: '{g.dfl_hash_preset}')
-P, --passwd-file= f Get passphrase from file 'f'.
-q, --quiet Produce quieter output
-r, --usr-randchars=n Get 'n' characters of additional randomness from
Expand Down Expand Up @@ -91,8 +91,6 @@ def make_help():

cmd_args = opts.init(opts_data,add_opts=['hidden_incog_input_params','in_fmt','use_old_ed25519'])

g.use_cached_balances = opt.cached_balances

if len(cmd_args) < 1:
opts.usage()

Expand Down

0 comments on commit 4c2410e

Please sign in to comment.