feat: Add cache encryption #9
@ -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
 | 
			
		||||
 | 
			
		||||
@ -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()
 | 
			
		||||
 | 
			
		||||
@ -150,6 +151,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):
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -3,6 +3,7 @@ import hashlib
 | 
			
		||||
import urllib.parse
 | 
			
		||||
import os
 | 
			
		||||
import logging
 | 
			
		||||
from socket import getservbyname
 | 
			
		||||
 | 
			
		||||
# external imports
 | 
			
		||||
from usumbufu.client.base import (
 | 
			
		||||
@ -48,7 +49,15 @@ class HTTPSession:
 | 
			
		||||
    def __init__(self, url, auth=None, origin=None):
 | 
			
		||||
        self.base_url = url
 | 
			
		||||
        url_parts = urllib.parse.urlsplit(self.base_url)
 | 
			
		||||
        url_parts_origin = (url_parts[0], url_parts[1], '', '', '',)
 | 
			
		||||
        url_parts_origin_host = url_parts[1].split(":")
 | 
			
		||||
        host = url_parts_origin_host[0]
 | 
			
		||||
        try:
 | 
			
		||||
            host = host + ':' + url_parts_origin_host[1]
 | 
			
		||||
        except IndexError:
 | 
			
		||||
            host = host + ':' + str(getservbyname(url_parts[0]))
 | 
			
		||||
            logg.info('changed origin with missing port number from {} to {}'.format(url_parts[1], host))
 | 
			
		||||
        url_parts_origin = (url_parts[0], host, '', '', '',)
 | 
			
		||||
            
 | 
			
		||||
        self.origin = origin
 | 
			
		||||
        if self.origin == None:
 | 
			
		||||
            self.origin = urllib.parse.urlunsplit(url_parts_origin)
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -56,7 +56,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 +78,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
									
								
							
							
						
						
									
										42
									
								
								clicada/crypt/aes.py
									
									
									
									
									
										Normal 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
									
								
							
							
						
						
									
										8
									
								
								clicada/crypt/base.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,8 @@
 | 
			
		||||
class Encrypter:
 | 
			
		||||
    
 | 
			
		||||
    def encrypt(self, v):
 | 
			
		||||
        raise NotImplementedError()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    def decrypt(self, v):
 | 
			
		||||
        raise NotImplementedError()
 | 
			
		||||
@ -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):
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,10 @@
 | 
			
		||||
usumbufu~=0.3.4
 | 
			
		||||
confini~=0.5.1
 | 
			
		||||
usumbufu~=0.3.5
 | 
			
		||||
confini~=0.5.3
 | 
			
		||||
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
 | 
			
		||||
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user