From e7d7a03e180094c0f3e25d172c6db791f8d59b70 Mon Sep 17 00:00:00 2001 From: "Antonio J. Delgado" Date: Sat, 16 Nov 2024 22:34:08 +0200 Subject: [PATCH] Add encrypted file cache of passwords --- nc_password_client/nc_password_client.py | 129 ++++++++++++++++++++--- pyproject.toml | 2 +- requirements.txt | 1 + setup.cfg | 2 +- 4 files changed, 118 insertions(+), 16 deletions(-) diff --git a/nc_password_client/nc_password_client.py b/nc_password_client/nc_password_client.py index 69a7625..0cae679 100755 --- a/nc_password_client/nc_password_client.py +++ b/nc_password_client/nc_password_client.py @@ -21,6 +21,8 @@ import binascii import click import click_config_file import passpy +import secretstorage +from cryptography.fernet import Fernet try: import requests HAS_REQUESTS_LIB = True @@ -101,8 +103,15 @@ class NextcloudHandler: self.http = 'https' self.timeout = params.get('timeout', 3) self.ssl = True - self.cached_passwords = [] - self.last_cache = -1 + self.encryption_pass = None + self._get_encryption_pass_from_keyring() + self.cache = {} + self.cache_filename = os.path.join( + os.environ['HOME'], + '.cache', + 'nc_password_client.cache' + ) + self._read_cache() self.session = requests.Session() proxies = { 'http': params.get('https_proxy', ''), @@ -190,6 +199,90 @@ class NextcloudHandler: ) self.keychain = json.loads(decrypt(text, key)) + def _get_encryption_pass_from_keyring(self): + connection = secretstorage.dbus_init() + collection = secretstorage.get_default_collection(connection) + if collection.is_locked: + collection.unlock() + for item in collection.get_all_items(): + if item.get_label() == 'nc_password_client': + self.encryption_pass = item.get_secret() + if self.encryption_pass is None: + self.debug( + { + "action": "_get_encryption_pass_from_keyring", + "message": "There wasn't an encryption password in the keyring, will generate one" + } + ) + self.encryption_pass = Fernet.generate_key() + attributes = { + 'application': 'nc_password_client', + } + item = collection.create_item('nc_password_client', attributes, self.encryption_pass) + else: + self.debug( + { + "action": "_get_encryption_pass_from_keyring", + "message": "Encryption password obtained from keyring" + } + ) + collection.lock() + + def _read_cache(self): + if os.path.exists(self.cache_filename): + self.debug( + { + "action": "_read_cache", + "message": "Reading cached passwords" + } + ) + with open(self.cache_filename, 'r', encoding='utf-8') as cache_file: + content = cache_file.read() + if len(content) != 0: + cipher_suite = Fernet(self.encryption_pass) + self.cache = json.loads(cipher_suite.decrypt(content)) + self.debug( + { + "action": "_read_cache", + "last_update": self.cache['last_update'], + "total_cached_password": len(self.cache['cached_passwords']) + } + ) + else: + self.debug( + { + "action": "_read_cache", + "message": "The cache file was empty, so initializing cache" + } + ) + self.cache = { + "last_update": -1, + "cached_passwords": [] + } + else: + self.debug( + { + "action": "_read_cache", + "message": "There wasn't a cache file, so initializing cache" + } + ) + self.cache = { + "last_update": -1, + "cached_passwords": [] + } + + def _write_cache(self): + self.debug( + { + "action": "_write_cache", + "message": "Writing cached passwords to file" + } + ) + with open(self.cache_filename, 'bw') as cache_file: + cipher_suite = Fernet(self.encryption_pass) + encrypted_cache = cipher_suite.encrypt(bytes(json.dumps(self.cache), 'utf-8')) + cache_file.write(encrypted_cache) + def _safer_obj(self, obj, fields=None): if fields is None: fields = ['password', 'token', 'secret', 'pass', 'passwd', 'hash'] @@ -520,21 +613,29 @@ class NextcloudHandler: def close_passwords_session(self): '''Close Passwords API session''' return self.get("index.php/apps/passwords/api/1.0/session/close") - + def list_passwords(self): '''List all passwords''' - if self.last_cache < (time.time() - self.cache_duration): + if self.cache['last_update'] < (time.time() - self.cache_duration): self.debug( - { "action": "list_passwords", "message": "Updating cached list of password" } + { + "action": "list_passwords", + "message": "Updating cached list of password", + "last_update": self.cache['last_update'], + "cache_duration": self.cache_duration + } ) - self.cached_passwords = self.get("index.php/apps/passwords/api/1.0/password/list") - if self.cached_passwords: - self.last_cache = time.time() + self.cache['cached_passwords'] = self.get( + "index.php/apps/passwords/api/1.0/password/list" + ) + if self.cache['cached_passwords']: + self.cache['last_update'] = time.time() + self._write_cache() else: self.debug( { "action": "list_passwords", "message": "Reusing cached list of password" } ) - return self.cached_passwords + return self.cache['cached_passwords'] def list_passwords_folders(self): '''List passwords folders''' @@ -674,7 +775,7 @@ class NextcloudHandler: for password in all_passwords: if self.is_same_password(obj, password): safer_password = dict(password, **{ 'password': '***' }) - self._log.debug( + self.debug( { "action": "test_exists_password", "message": "Same password already exists", @@ -683,7 +784,7 @@ class NextcloudHandler: } ) return True - self._log.debug( + self.debug( { "action": "test_exists_password", "message": "No password match found", @@ -1050,7 +1151,7 @@ class NcPasswordClient: { "action": "created_password", "object": new_password } ) count += 1 - self._log.debug( + self.debug( { "action": "migrate_pass", "message": "Migrate Pass summary", @@ -1098,7 +1199,7 @@ class NcPasswordClient: for item in passwords: for checked in checked_passwords: if self.nc.is_same_password(checked, item): - self._log.debug( + self.debug( { "action": "remove_duplicate", "object": self.nc.delete_password(item) @@ -1109,7 +1210,7 @@ class NcPasswordClient: checked_passwords.append(item) if limit != -1 and count >= limit: break - self._log.debug( + self.debug( { "action": "remove_duplicates", "message": "Remove duplicates summary", diff --git a/pyproject.toml b/pyproject.toml index 188bb0b..d6987a5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ Homepage = "https://repos.susurrando.com/adelgado/nc_password_client" [project] name = "nc_password_client" -version = "0.0.1" +version = "0.0.2" description = "Nextcloud Password client" readme = "README.md" authors = [{ name = "Antonio J. Delgado", email = "ad@susurrando.com" }] diff --git a/requirements.txt b/requirements.txt index 0fb78cc..7d8bdaf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,4 @@ click_config_file requests pysodium passpy +secretstorage diff --git a/setup.cfg b/setup.cfg index cdacdec..6b4e507 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = nc_password_client -version = 0.0.1 +version = 0.0.2 [options] packages = nc_password_client