Add encrypted file cache of passwords

This commit is contained in:
Antonio J. Delgado 2024-11-16 22:34:08 +02:00
parent a5c08ab235
commit e7d7a03e18
4 changed files with 118 additions and 16 deletions

View file

@ -21,6 +21,8 @@ import binascii
import click import click
import click_config_file import click_config_file
import passpy import passpy
import secretstorage
from cryptography.fernet import Fernet
try: try:
import requests import requests
HAS_REQUESTS_LIB = True HAS_REQUESTS_LIB = True
@ -101,8 +103,15 @@ class NextcloudHandler:
self.http = 'https' self.http = 'https'
self.timeout = params.get('timeout', 3) self.timeout = params.get('timeout', 3)
self.ssl = True self.ssl = True
self.cached_passwords = [] self.encryption_pass = None
self.last_cache = -1 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() self.session = requests.Session()
proxies = { proxies = {
'http': params.get('https_proxy', ''), 'http': params.get('https_proxy', ''),
@ -190,6 +199,90 @@ class NextcloudHandler:
) )
self.keychain = json.loads(decrypt(text, key)) 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): def _safer_obj(self, obj, fields=None):
if fields is None: if fields is None:
fields = ['password', 'token', 'secret', 'pass', 'passwd', 'hash'] fields = ['password', 'token', 'secret', 'pass', 'passwd', 'hash']
@ -523,18 +616,26 @@ class NextcloudHandler:
def list_passwords(self): def list_passwords(self):
'''List all passwords''' '''List all passwords'''
if self.last_cache < (time.time() - self.cache_duration): if self.cache['last_update'] < (time.time() - self.cache_duration):
self.debug( 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") self.cache['cached_passwords'] = self.get(
if self.cached_passwords: "index.php/apps/passwords/api/1.0/password/list"
self.last_cache = time.time() )
if self.cache['cached_passwords']:
self.cache['last_update'] = time.time()
self._write_cache()
else: else:
self.debug( self.debug(
{ "action": "list_passwords", "message": "Reusing cached list of password" } { "action": "list_passwords", "message": "Reusing cached list of password" }
) )
return self.cached_passwords return self.cache['cached_passwords']
def list_passwords_folders(self): def list_passwords_folders(self):
'''List passwords folders''' '''List passwords folders'''
@ -674,7 +775,7 @@ class NextcloudHandler:
for password in all_passwords: for password in all_passwords:
if self.is_same_password(obj, password): if self.is_same_password(obj, password):
safer_password = dict(password, **{ 'password': '***' }) safer_password = dict(password, **{ 'password': '***' })
self._log.debug( self.debug(
{ {
"action": "test_exists_password", "action": "test_exists_password",
"message": "Same password already exists", "message": "Same password already exists",
@ -683,7 +784,7 @@ class NextcloudHandler:
} }
) )
return True return True
self._log.debug( self.debug(
{ {
"action": "test_exists_password", "action": "test_exists_password",
"message": "No password match found", "message": "No password match found",
@ -1050,7 +1151,7 @@ class NcPasswordClient:
{ "action": "created_password", "object": new_password } { "action": "created_password", "object": new_password }
) )
count += 1 count += 1
self._log.debug( self.debug(
{ {
"action": "migrate_pass", "action": "migrate_pass",
"message": "Migrate Pass summary", "message": "Migrate Pass summary",
@ -1098,7 +1199,7 @@ class NcPasswordClient:
for item in passwords: for item in passwords:
for checked in checked_passwords: for checked in checked_passwords:
if self.nc.is_same_password(checked, item): if self.nc.is_same_password(checked, item):
self._log.debug( self.debug(
{ {
"action": "remove_duplicate", "action": "remove_duplicate",
"object": self.nc.delete_password(item) "object": self.nc.delete_password(item)
@ -1109,7 +1210,7 @@ class NcPasswordClient:
checked_passwords.append(item) checked_passwords.append(item)
if limit != -1 and count >= limit: if limit != -1 and count >= limit:
break break
self._log.debug( self.debug(
{ {
"action": "remove_duplicates", "action": "remove_duplicates",
"message": "Remove duplicates summary", "message": "Remove duplicates summary",

View file

@ -7,7 +7,7 @@ Homepage = "https://repos.susurrando.com/adelgado/nc_password_client"
[project] [project]
name = "nc_password_client" name = "nc_password_client"
version = "0.0.1" version = "0.0.2"
description = "Nextcloud Password client" description = "Nextcloud Password client"
readme = "README.md" readme = "README.md"
authors = [{ name = "Antonio J. Delgado", email = "ad@susurrando.com" }] authors = [{ name = "Antonio J. Delgado", email = "ad@susurrando.com" }]

View file

@ -3,3 +3,4 @@ click_config_file
requests requests
pysodium pysodium
passpy passpy
secretstorage

View file

@ -1,6 +1,6 @@
[metadata] [metadata]
name = nc_password_client name = nc_password_client
version = 0.0.1 version = 0.0.2
[options] [options]
packages = nc_password_client packages = nc_password_client