diff --git a/clicada/cli/arg.py b/clicada/cli/arg.py index 4c7564a..441db69 100644 --- a/clicada/cli/arg.py +++ b/clicada/cli/arg.py @@ -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): diff --git a/clicada/cli/auth.py b/clicada/cli/auth.py index 5954eb9..d5dcb78 100644 --- a/clicada/cli/auth.py +++ b/clicada/cli/auth.py @@ -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 diff --git a/clicada/cli/notify.py b/clicada/cli/notify.py new file mode 100644 index 0000000..6b8d8ef --- /dev/null +++ b/clicada/cli/notify.py @@ -0,0 +1,32 @@ +# standard imports +import os +import sys + + +class NotifyWriter: + + def __init__(self, writer=sys.stdout): + (c, r) = os.get_terminal_size() + self.cols = c + self.fmt = "\r{:" + "<{}".format(c) + "}" + self.w = writer + self.notify_max = self.cols - 4 + + + def notify(self, v): + if len(v) > self.notify_max: + v = v[:self.notify_max] + self.write('\x1b[0;36m... ' + v + '\x1b[0;39m') + + + def ouch(self, v): + if len(v) > self.notify_max: + v = v[:self.notify_max] + self.write('\x1b[0;91m!!! ' + v + '\x1b[0;39m') + + + def write(self, v): + s = str(v) + if len(s) > self.cols: + s = s[:self.cols] + self.w.write(self.fmt.format(s)) diff --git a/clicada/cli/user.py b/clicada/cli/user.py index c4a4794..92a0aa0 100644 --- a/clicada/cli/user.py +++ b/clicada/cli/user.py @@ -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: diff --git a/clicada/crypt.py b/clicada/crypt.py deleted file mode 100644 index b917098..0000000 --- a/clicada/crypt.py +++ /dev/null @@ -1,38 +0,0 @@ -# standard imports -import os -import logging - -from Crypto.Cipher import AES -from Crypto.Util import Counter - -logg = logging.getLogger(__name__) - - -class Encrypt: - - aesBlockSize = 1 << 7 - - def __init__(self, secret, db_dir): - fp = os.path.join(db_dir, '.aes_ctr_iv') - try: - f = open(fp, 'rb') - self.iv = f.read() - except FileNotFoundError: - logg.debug('generating new iv for aes-ctr') - self.iv = os.urandom(8) - f = open(fp, 'wb') - f.write(self.iv) - - f.close() - - iv_num = int.from_bytes(self.iv, 'big') - self.ctr = Counter.new(aesBlockSize, initial_value=iv_num) - self.cipher = AES.new(secret, AES.MODE_CTR, counter=self.ctr) - - - def encrypt(self, v): - return self.cipher.encrypt(v) - - - def decrypt(self, v): - return self.cipher.decrypt(v) diff --git a/clicada/crypt/aes.py b/clicada/crypt/aes.py new file mode 100644 index 0000000..8d8afb4 --- /dev/null +++ b/clicada/crypt/aes.py @@ -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) diff --git a/clicada/crypt/base.py b/clicada/crypt/base.py new file mode 100644 index 0000000..19bc150 --- /dev/null +++ b/clicada/crypt/base.py @@ -0,0 +1,8 @@ +class Encrypter: + + def encrypt(self, v): + raise NotImplementedError() + + + def decrypt(self, v): + raise NotImplementedError() diff --git a/clicada/user/file.py b/clicada/user/file.py index a84a4f9..02bf037 100644 --- a/clicada/user/file.py +++ b/clicada/user/file.py @@ -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):