From bdfdd0fdd7f48939fba396da0915af1aaa6bfacf Mon Sep 17 00:00:00 2001 From: nolash Date: Sat, 6 Nov 2021 10:34:40 +0100 Subject: [PATCH] Add pgp auth, subject metadata summary in cli user output --- clicada/cli/arg.py | 28 +++++++++++++----- clicada/cli/auth.py | 53 ++++++++++++++++++++++++++++++++++ clicada/cli/user.py | 34 ++++++++++++++++++---- clicada/data/config/config.ini | 7 +++++ clicada/error.py | 4 +++ clicada/tx/tx.py | 6 ++-- clicada/user/file.py | 4 +-- 7 files changed, 118 insertions(+), 18 deletions(-) create mode 100644 clicada/cli/auth.py diff --git a/clicada/cli/arg.py b/clicada/cli/arg.py index c397f7a..508056e 100644 --- a/clicada/cli/arg.py +++ b/clicada/cli/arg.py @@ -12,6 +12,7 @@ from chainlib.chain import ChainSpec # local imports import clicada.cli.user as cmd_user import clicada.cli.tag as cmd_tag +from clicada.cli.auth import PGPAuthCrypt script_dir = os.path.dirname(os.path.realpath(__file__)) data_dir = os.path.join(script_dir, '..', 'data') @@ -25,13 +26,12 @@ class CmdCtrl: 't': 'tag', } - def __init__(self, argv=None, description=None, logger=None, *args, **kwargs): - #self.argparser = argparse.ArgumentParser(description=description, *args, **kwargs) - self.argparser = chainlib.eth.cli.ArgumentParser(chainlib.eth.cli.argflag_std_read) + __auth_for = [ + 'user', + ] - #self.argparser.add_argument('-c', type=str, help='Configuration override directory path') - #self.argparser.add_argument('-v', action='store_true', help='Be verbose') - #self.argparser.add_argument('-vv', action='store_true', help='Be very verbose') + def __init__(self, argv=None, description=None, logger=None, *args, **kwargs): + self.argparser = chainlib.eth.cli.ArgumentParser(chainlib.eth.cli.argflag_std_read) sub = self.argparser.add_subparsers() sub.dest = 'command' @@ -68,6 +68,8 @@ class CmdCtrl: else: self.config = chainlib.eth.cli.Config.from_args(self.cmd_args, base_config_dir=base_config_dir, extra_args=extra_args) + self.__auth = self.auth(self.get('AUTH_TYPE')) + self.config.add(False, '_SEQ') logger.debug('loaded config:\n{}'.format(self.config)) @@ -77,8 +79,18 @@ class CmdCtrl: self.__conn = self.rpc.connect_by_config(self.config) - def get(self, k): - r = self.config.get(k) + + def auth(self, typ): + if typ != 'gnupg': + raise NotImplementedError('Valid aut implementations are: gnupg') + default_auth_db_path = os.path.join(os.environ['HOME'], '.clicada/auth') + auth_db_path = self.get('AUTH_DB_PATH', default_auth_db_path) + self.__auth = PGPAuthCrypt(auth_db_path, self.get('AUTH_KEY'), self.get('AUTH_KEYRING_PATH')) + self.__auth.get_secret(self.get('AUTH_PASSPHRASE')) + + + def get(self, k, default=None): + r = self.config.get(k, default) if k in [ '_FORCE', ]: diff --git a/clicada/cli/auth.py b/clicada/cli/auth.py new file mode 100644 index 0000000..c8a699b --- /dev/null +++ b/clicada/cli/auth.py @@ -0,0 +1,53 @@ +# standard imports +import os +import hashlib +import logging + +# external imports +import gnupg + +# local imports +from clicada.error import AuthError + +logg = logging.getLogger(__name__) + + +class PGPAuthCrypt: + + def __init__(self, db_dir, auth_key, pgp_dir=None): + self.db_dir = db_dir + try: + bytes.fromhex(auth_key) + except TypeError: + raise AuthError('invalid key {}'.format(auth_key)) + except ValueError: + raise AuthError('invalid key {}'.format(auth_key)) + self.auth_key = auth_key + self.gpg = gnupg.GPG(gnupghome=pgp_dir) + + + def get_secret(self, passphrase=''): + if passphrase == None: + passphrase = '' + p = os.path.join(self.db_dir, '.secret') + try: + f = open(p, 'rb') + except FileNotFoundError: + h = hashlib.sha256() + h.update(bytes.fromhex(self.auth_key)) + h.update(passphrase.encode('utf-8')) + z = h.digest() + secret = self.gpg.encrypt(z, [self.auth_key]) + if not secret.ok: + raise AuthError('could not encrypt secret for {}'.format(auth_key)) + + d = os.path.dirname(p) + os.makedirs(d, exist_ok=True) + f = open(p, 'wb') + f.write(secret.data) + f.close() + f = open(p, 'rb') + self.secret = self.gpg.decrypt_file(f, passphrase=passphrase) + if not self.secret.ok: + raise AuthError('could not decrypt encryption secret. wrong password?') + f.close() diff --git a/clicada/cli/user.py b/clicada/cli/user.py index 1254755..6796b0f 100644 --- a/clicada/cli/user.py +++ b/clicada/cli/user.py @@ -1,11 +1,13 @@ # standard imports import sys import logging +import datetime # external imports from cic_eth_registry import CICRegistry from cic_eth_registry.lookup.tokenindex import TokenIndexLookup from cic_types.ext.metadata import MetadataRequestsHandler +from cic_types.models.person import Person from chainlib.eth.address import to_checksum_address # local imports @@ -29,7 +31,7 @@ def extra_args(): 'force_update': '_FORCE', 'method': 'META_LOOKUP_METHOD', 'meta_url': 'META_URL', - 'identifier': '_ARG_USER_IDENTIFIER', + 'identifier': '_IDENTIFIER', } @@ -51,18 +53,17 @@ def execute(ctrl): user_phone_file_label = 'phone' user_phone_store = FileUserStore(ctrl.chain(), user_phone_file_label, store_path, int(ctrl.get('FILESTORE_TTL'))) - user_address = user_phone_store.by_phone(ctrl.get('_ARG_USER_IDENTIFIER'), update=ctrl.get('_FORCE')) + user_address = user_phone_store.by_phone(ctrl.get('_IDENTIFIER'), update=ctrl.get('_FORCE')) if user_address == None: - sys.stderr.write('unknown identifier: {}\n'.format(ctrl.get('_ARG_USER_IDENTIFIER'))) + sys.stderr.write('unknown identifier: {}\n'.format(ctrl.get('_IDENTIFIER'))) sys.exit(1) try: user_address = to_checksum_address(user_address) except ValueError: - sys.stderr.write('invalid response "{}" for {}\n'.format(user_address, ctrl.get('_ARG_USER_IDENTIFIER'))) + sys.stderr.write('invalid response "{}" for {}\n'.format(user_address, ctrl.get('_IDENTIFIER'))) sys.exit(1) - - logg.debug('loaded user address {} for {}'.format(user_address, ctrl.get('_ARG_USER_IDENTIFIER'))) + logg.debug('loaded user address {} for {}'.format(user_address, ctrl.get('_IDENTIFIER'))) txs = tx_getter.get(user_address) @@ -71,6 +72,27 @@ def execute(ctrl): user_address_file_label = 'address' user_address_store = FileUserStore(ctrl.chain(), user_address_file_label, store_path, int(ctrl.get('FILESTORE_TTL'))) + r = user_address_store.by_address(user_address) + + print("""Phone: {} +EVM address: {} +Name: {} +Registered: {} +Gender: {} +Location: {} +Products: {} +""".format( + ctrl.get('_IDENTIFIER'), + user_address, + str(r), + datetime.datetime.fromtimestamp(r.date_registered).ctime(), + r.gender, + r.location['area_name'], + ','.join(r.products), + ) +) + + for tx_src in txs['data']: tx = ResolvedTokenTx.from_dict(tx_src) tx.resolve(token_store, user_address_store, update=ctrl.get('_FORCE')) diff --git a/clicada/data/config/config.ini b/clicada/data/config/config.ini index 6a8a16d..b2621db 100644 --- a/clicada/data/config/config.ini +++ b/clicada/data/config/config.ini @@ -10,3 +10,10 @@ cache_url = [cic] registry_address = + +[auth] +type = gnupg +db_path = +keyring_path = +key = +passphrase = diff --git a/clicada/error.py b/clicada/error.py index 3b67fd8..e405133 100644 --- a/clicada/error.py +++ b/clicada/error.py @@ -1,2 +1,6 @@ class ExpiredRecordError(Exception): pass + + +class AuthError(Exception): + pass diff --git a/clicada/tx/tx.py b/clicada/tx/tx.py index f239fad..d5b6fb4 100644 --- a/clicada/tx/tx.py +++ b/clicada/tx/tx.py @@ -69,14 +69,16 @@ class ResolvedTokenTx(TokenTx): return v if self.tx_type == TokenTxType.faucet_giveto.value: return 'FAUCET' - return user_store.get_label(self.sender) + r = user_store.by_address(self.sender) + return str(r) def resolve_recipient_entity(self, user_store, update=False): v = self.resolve_stored_entity(user_store, self.recipient, update=update) if v != None: return v - return user_store.get_label(self.recipient, update=update) + r = user_store.by_address(self.recipient, update=update) + return str(r) def resolve_entities(self, user_store, update=False): diff --git a/clicada/user/file.py b/clicada/user/file.py index c94e594..28799cd 100644 --- a/clicada/user/file.py +++ b/clicada/user/file.py @@ -169,7 +169,7 @@ class FileUserStore: return user_address - def get_label(self, address, update=False): + def by_address(self, address, update=False): add = tx_normalize.wallet_address(address) ignore_expired = self.sticky(address) @@ -202,4 +202,4 @@ class FileUserStore: person = Person() person_data = person.deserialize(person_data=data) self.put(address, json.dumps(person_data.serialize())) - return str(person_data) + return person_data