10 Commits

Author SHA1 Message Date
lash
acccaa84dd Add custom cafile, correctly detect 404 2022-01-26 10:22:19 +00:00
65b3d4d409 Merge pull request 'feat: Add cache encryption' (#9) from lash/encrypt into master
Reviewed-on: #9
2022-01-23 06:59:16 +00:00
lash
0bfe054b90 Auto-complete origin on missing port, magic shutil terminal size 2022-01-21 14:38:06 +00:00
lash
fe410e0fc6 Merge remote-tracking branch 'origin/master' into lash/encrypt 2022-01-21 11:29:41 +00:00
lash
3663665d91 Update packaging and deps 2022-01-21 11:28:22 +00:00
b0f8f39d15 usability: WIP Friendly progress output (#4)
This MR adds colorized progress statements when resolving and retrieving data for user. It disables logging if loglevel is set to default (logging.WARNING at this time).

It also skips metadata lookups that have failed in the same session, speeding up retrievals when same address repeatedly occurs in transaction list.

closes #6
closes #5

Co-authored-by: lash <dev@holbrook.no>
Reviewed-on: #4
Co-authored-by: lash <accounts-grassrootseconomics@holbrook.no>
Co-committed-by: lash <accounts-grassrootseconomics@holbrook.no>
2022-01-21 11:11:23 +00:00
lash
140df0d1c6 Add pycryptodome dep 2022-01-21 11:09:17 +00:00
lash
c5b4c41db0 Bump version 2022-01-21 11:04:17 +00:00
lash
d302c5754c Add AES CTR with key as iv 2022-01-21 10:59:27 +00:00
lash
36b4fcab93 Add back crypt module 2022-01-21 10:15:43 +00:00
11 changed files with 118 additions and 25 deletions

View File

@@ -1,3 +1,5 @@
- 0.0.6
* Add cache encryption, with AES-CTR-128
- 0.0.5
* Replace logs with colorized progress output on default loglevel
* Do not repeat already failed metadata lookups

View File

@@ -1,7 +1,7 @@
# import notifier
from clicada.cli.notify import NotifyWriter
notifier = NotifyWriter()
notifier.notify('loading script')
#notifier.notify('loading script')
# standard imports
import os
@@ -22,6 +22,7 @@ from clicada.cli.http import (
HTTPSession,
PGPClientSession,
)
from clicada.crypt.aes import AESCTREncrypt
logg = logging.getLogger()
@@ -72,8 +73,19 @@ class CmdCtrl:
self.remote_openers = {}
if self.get('META_URL') != None:
sctx = None
if self.cmd_args.cafile != None:
import ssl
sctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
sctx.load_verify_locations(self.cmd_args.cafile)
auth_client_session = PGPClientSession(self.__auth)
self.remote_openers['meta'] = HTTPSession(self.get('META_URL'), auth=auth_client_session, origin=self.config.get('META_HTTP_ORIGIN'))
self.remote_openers['meta'] = HTTPSession(
self.get('META_URL'),
auth=auth_client_session,
origin=self.config.get('META_HTTP_ORIGIN'),
ssl_context=sctx,
)
def blockchain(self):
@@ -150,6 +162,7 @@ class CmdCtrl:
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'))
self.encrypter = AESCTREncrypt(auth_db_path, self.__auth.secret)
def get(self, k, default=None):

View File

@@ -26,6 +26,7 @@ class PGPAuthCrypt:
raise AuthError('invalid key {}'.format(auth_key))
self.auth_key = auth_key
self.gpg = gnupg.GPG(gnupghome=pgp_dir)
self.secret = None
def get_secret(self, passphrase=''):
@@ -49,10 +50,11 @@ class PGPAuthCrypt:
f.write(secret.data)
f.close()
f = open(p, 'rb')
self.secret = self.gpg.decrypt_file(f, passphrase=passphrase)
if not self.secret.ok:
secret = self.gpg.decrypt_file(f, passphrase=passphrase)
if not secret.ok:
raise AuthError('could not decrypt encryption secret. wrong password?')
f.close()
self.secret = secret.data
self.__passphrase = passphrase

View File

@@ -3,6 +3,7 @@ import hashlib
import urllib.parse
import os
import logging
from socket import getservbyname
# external imports
from usumbufu.client.base import (
@@ -11,6 +12,7 @@ from usumbufu.client.base import (
)
from usumbufu.client.bearer import BearerClientSession
from usumbufu.client.hoba import HobaClientSession
from urlybird.host import url_apply_port_string
logg = logging.getLogger(__name__)
@@ -20,6 +22,7 @@ class PGPClientSession(HobaClientSession):
alg = '969'
def __init__(self, auth):
super(PGPClientSession, self).__init__()
self.auth = auth
self.origin = None
self.fingerprint = self.auth.fingerprint()
@@ -45,15 +48,12 @@ class HTTPSession:
token_dir = '/run/user/{}/clicada/usumbufu/.token'.format(os.getuid())
def __init__(self, url, auth=None, origin=None):
def __init__(self, url, auth=None, origin=None, ssl_context=None):
self.base_url = url
url_parts = urllib.parse.urlsplit(self.base_url)
url_parts_origin = (url_parts[0], url_parts[1], '', '', '',)
if origin == None:
origin = url_apply_port_string(url, as_origin=True)
self.origin = origin
if self.origin == None:
self.origin = urllib.parse.urlunsplit(url_parts_origin)
else:
logg.debug('overriding http origin for {} with {}'.format(url, self.origin))
h = hashlib.sha256()
h.update(self.base_url.encode('utf-8'))
@@ -63,7 +63,7 @@ class HTTPSession:
os.makedirs(token_store_dir, exist_ok=True)
self.token_store = BaseTokenStore(path=token_store_dir)
self.session = ClientSession(self.origin, token_store=self.token_store)
self.session = ClientSession(self.origin, token_store=self.token_store, ssl_context=ssl_context)
bearer_handler = BearerClientSession(self.origin, token_store=self.token_store)
self.session.add_subhandler(bearer_handler)
@@ -79,6 +79,9 @@ class HTTPSession:
url = urllib.parse.urljoin(self.base_url, endpoint)
logg.debug('open {} with opener {}'.format(url, self))
r = self.opener.open(url)
logg.debug('response code {} for {}'.format(r.code, endpoint))
if r.code == 404:
raise FileNotFoundError()
return r.read().decode('utf-8')

View File

@@ -1,12 +1,13 @@
# standard imports
import os
import sys
import shutil
class NotifyWriter:
def __init__(self, writer=sys.stdout):
(c, r) = os.get_terminal_size()
(c, r) = shutil.get_terminal_size()
self.cols = c
self.fmt = "\r{:" + "<{}".format(c) + "}"
self.w = writer

View File

@@ -29,6 +29,7 @@ tx_normalizer = TxHexNormalizer()
def process_args(argparser):
argparser.add_argument('-m', '--method', type=str, help='lookup method')
argparser.add_argument('--meta-url', dest='meta_url', type=str, help='Url to retrieve metadata from')
argparser.add_argument('--cafile', type=str, help='CA certificate chain file to use for verifying SSL session')
argparser.add_argument('-f', '--force-update', dest='force_update', action='store_true', help='Update records of mutable entries')
argparser.add_argument('identifier', type=str, help='user identifier')
@@ -56,7 +57,7 @@ def execute(ctrl):
store_path = '.clicada'
user_phone_file_label = 'phone'
user_phone_store = FileUserStore(ctrl.opener('meta'), ctrl.chain(), user_phone_file_label, store_path, int(ctrl.get('FILESTORE_TTL')))
user_phone_store = FileUserStore(ctrl.opener('meta'), ctrl.chain(), user_phone_file_label, store_path, int(ctrl.get('FILESTORE_TTL')), encrypter=ctrl.encrypter)
ctrl.notify('resolving identifier {} to wallet address'.format(ctrl.get('_IDENTIFIER')))
user_address = user_phone_store.by_phone(ctrl.get('_IDENTIFIER'), update=ctrl.get('_FORCE'))
@@ -78,7 +79,7 @@ def execute(ctrl):
token_store = FileTokenStore(ctrl.chain(), ctrl.conn(), 'token', store_path)
user_address_file_label = 'address'
user_address_store = FileUserStore(ctrl.opener('meta'), ctrl.chain(), user_address_file_label, store_path, int(ctrl.get('FILESTORE_TTL')))
user_address_store = FileUserStore(ctrl.opener('meta'), ctrl.chain(), user_address_file_label, store_path, int(ctrl.get('FILESTORE_TTL')), encrypter=ctrl.encrypter)
ctrl.notify('resolving metadata for address {}'.format(user_address_normal))
try:

42
clicada/crypt/aes.py Normal file
View File

@@ -0,0 +1,42 @@
# standard imports
import os
import logging
import hashlib
from Crypto.Cipher import AES
from Crypto.Util import Counter
from .base import Encrypter
logg = logging.getLogger(__name__)
class AESCTREncrypt(Encrypter):
aes_block_size = 1 << 7
counter_bytes = int(128 / 8)
def __init__(self, db_dir, secret):
self.secret = secret
def key_to_iv(self, k):
h = hashlib.sha256()
h.update(k.encode('utf-8'))
h.update(self.secret)
z = h.digest()
return int.from_bytes(z[:self.counter_bytes], 'big')
def encrypt(self, k, v):
iv = self.key_to_iv(k)
ctr = Counter.new(self.aes_block_size, initial_value=iv)
cipher = AES.new(self.secret, AES.MODE_CTR, counter=ctr)
return cipher.encrypt(v)
def decrypt(self, k, v):
iv = self.key_to_iv(k)
ctr = Counter.new(self.aes_block_size, initial_value=iv)
cipher = AES.new(self.secret, AES.MODE_CTR, counter=ctr)
return cipher.decrypt(v)

8
clicada/crypt/base.py Normal file
View File

@@ -0,0 +1,8 @@
class Encrypter:
def encrypt(self, v):
raise NotImplementedError()
def decrypt(self, v):
raise NotImplementedError()

View File

@@ -65,7 +65,7 @@ class Account(Person):
class FileUserStore:
def __init__(self, metadata_opener, chain_spec, label, store_base_path, ttl):
def __init__(self, metadata_opener, chain_spec, label, store_base_path, ttl, encrypter=None):
invalidate_before = datetime.datetime.now() - datetime.timedelta(seconds=ttl)
self.invalidate_before = int(invalidate_before.timestamp())
self.have_xattr = False
@@ -82,6 +82,7 @@ class FileUserStore:
self.__validate_dir()
self.metadata_opener = metadata_opener
self.failed_entities = {}
self.encrypter = encrypter
def __validate_dir(self):
@@ -108,8 +109,14 @@ class FileUserStore:
if have_file and not its_time and not force:
raise FileExistsError('user resolution already exists for {}'.format(k))
f = open(p, 'w')
f.write(v)
ve = v
f = None
if self.encrypter != None:
ve = self.encrypter.encrypt(k, ve.encode('utf-8'))
f = open(p, 'wb')
else:
f = open(p, 'w')
f.write(ve)
f.close()
logg.info('added user store {} record {} -> {}'.format(self.label, k, v))
@@ -174,12 +181,21 @@ class FileUserStore:
self.__unstick(p)
self.check_expiry(p)
f = open(p, 'r')
r = f.read()
f = None
if self.encrypter != None:
f = open(p, 'rb')
else:
f = open(p, 'r')
v = f.read()
f.close()
if self.encrypter != None:
v = self.encrypter.decrypt(k, v)
logg.debug('>>>>>>>>>>>>< v decoded {}'.format(v))
v = v.decode('utf-8')
logg.debug('retrieved {} from {}'.format(k, p))
return r.strip()
return v.strip()
def by_phone(self, phone, update=False):
@@ -252,7 +268,7 @@ class FileUserStore:
r = getter.open(ptr)
except Exception as e:
logg.debug('no metadata found for {}: {}'.format(address, e))
if r == None:
self.failed_entities[address] = True
raise MetadataNotFoundError()

View File

@@ -1,7 +1,11 @@
usumbufu~=0.3.4
confini~=0.5.1
usumbufu~=0.3.6
confini~=0.5.4
cic-eth-registry~=0.6.1
cic-types~=0.2.1a8
phonenumbers==8.12.12
eth-erc20~=0.1.2
hexathon~=0.1.0
pycryptodome~=3.10.1
chainlib-eth~=0.0.21
chainlib~=0.0.17
urlybird~=0.0.2

View File

@@ -1,6 +1,6 @@
[metadata]
name = clicada
version = 0.0.5a1
version = 0.0.6a2
description = CLI CRM tool for the cic-stack custodial wallet system
author = Louis Holbrook
author_email = dev@holbrook.no
@@ -34,3 +34,4 @@ packages =
clicada.cli
clicada.tx
clicada.user
clicada.crypt