Index: keyring-5.3/keyring/backends/file.py =================================================================== --- keyring-5.3.orig/keyring/backends/file.py +++ keyring-5.3/keyring/backends/file.py @@ -7,6 +7,8 @@ import sys import json import abc import time +import logging +import shutil from ..py27compat import configparser @@ -14,6 +16,7 @@ from ..errors import PasswordDeleteError from ..backend import KeyringBackend from ..util import platform_, properties from ..util.escape import escape as escape_for_ini +from oslo_concurrency import lockutils class FileBacked(object): @@ -31,6 +34,13 @@ class FileBacked(object): """ return os.path.join(platform_.data_root(), self.filename) + @properties.NonDataProperty + def backup_file_path(self): + """ + The path to the file where passwords are stored. This property + may be overridden by the subclass or at the instance level. + """ + return os.path.join(platform_.data_root(), self.backup_filename) class BaseKeyring(FileBacked, KeyringBackend): """ @@ -78,6 +88,16 @@ class BaseKeyring(FileBacked, KeyringBac password = None return password + + def filecopy(self,src,dest): + """copy file src to dest with default buffer size + """ + with open(src, 'r') as f1: + with open(dest, 'w') as f2: + shutil.copyfileobj(f1,f2) + f2.flush() + + def set_password(self, service, username, password): """Write the password in the file. """ @@ -89,37 +109,56 @@ class BaseKeyring(FileBacked, KeyringBac # encode with base64 password_base64 = base64.encodestring(password_encrypted).decode() - # ensure the file exists - self._ensure_file_path() - # obtain lock for the keyring file - lock = '' - i = 60 - while i: - if not os.path.isfile('/tmp/.keyringlock'): - lock = open('/tmp/.keyringlock', 'w') - break - else: - time.sleep(0.500) - i=i-1 + with lockutils.lock("keyringlock",external=True,lock_path="/tmp"): + # ensure the file exists + self._ensure_file_path() + + config = None + try: + # Load the keyring from the disk + config = configparser.RawConfigParser() + config.read(self.file_path) + except configparser.ParsingError as e: + logging.warning("set_password: keyring file corrupted, Reverting to Backup") + # Revert to the backup file (copy backup over current file) + try: + src = self.backup_file_path + dest = self.file_path + self.filecopy(src,dest) + except shutil.Error as e: + logging.warning("set_password: Revert from Backup failed. Error: %s" % e) + raise + # Load the keyring from the disk, if this fails exception is raised + try: + config = configparser.RawConfigParser() + config.read(self.file_path) + except: + e = sys.exc_info()[0] + logging.warning("set_password: Both keyring files are non useable. Error: %s" % e) + raise - if i: - # Load the keyring from the disk - config = configparser.RawConfigParser() - config.read(self.file_path) # Update the keyring with the password if not config.has_section(service): config.add_section(service) config.set(service, username, password_base64) + # Make a back up of the keyring file here + try: + src = self.file_path + dest = self.backup_file_path + self.filecopy(src,dest) + except shutil.Error as e: + logging.warning("set_password: Backup failed. Error: %s" % e) + # Save the keyring back to the file with open(self.file_path, 'w') as config_file: config.write(config_file) - lock.close() - os.remove('/tmp/.keyringlock') + + def _ensure_file_path(self): @@ -142,17 +181,18 @@ class BaseKeyring(FileBacked, KeyringBac """ service = escape_for_ini(service) username = escape_for_ini(username) - config = configparser.RawConfigParser() - if os.path.exists(self.file_path): - config.read(self.file_path) - try: - if not config.remove_option(service, username): + with lockutils.lock("keyringlock",external=True,lock_path="/tmp"): + config = configparser.RawConfigParser() + if os.path.exists(self.file_path): + config.read(self.file_path) + try: + if not config.remove_option(service, username): + raise PasswordDeleteError("Password not found") + except configparser.NoSectionError: raise PasswordDeleteError("Password not found") - except configparser.NoSectionError: - raise PasswordDeleteError("Password not found") - # update the file - with open(self.file_path, 'w') as config_file: - config.write(config_file) + # update the file + with open(self.file_path, 'w') as config_file: + config.write(config_file) class PlaintextKeyring(BaseKeyring): """Simple File Keyring with no encryption""" @@ -161,6 +201,7 @@ class PlaintextKeyring(BaseKeyring): "Applicable for all platforms, but not recommended" filename = 'keyring_pass.cfg' + backup_filename = 'crypted_pass_backup.cfg' def encrypt(self, password): """Directly return the password itself. @@ -214,6 +255,7 @@ class EncryptedKeyring(Encrypted, BaseKe """PyCrypto File Keyring""" filename = 'crypted_pass.cfg' + backup_filename = 'crypted_pass_backup.cfg' pw_prefix = 'pw:'.encode() @properties.ClassProperty @@ -247,6 +289,19 @@ class EncryptedKeyring(Encrypted, BaseKe self.keyring_key = self._get_new_password() # set a reference password, used to check that the password provided # matches for subsequent checks. + + # try to pre-create the /tmp/keyringlock if it doesn't exist + lockfile = "/tmp/keyringlock" + if os.geteuid() == 0 and (not os.path.exists(lockfile)): + from pwd import getpwnam + import stat + nonrootuser = "wrsroot" + with open(lockfile, 'w'): + pass + # must have the lock file with the correct group permissisions g+rw + os.chmod(lockfile, stat.S_IRWXG | stat.S_IRWXU) + + self.set_password('keyring-setting', 'password reference', 'password reference value') @@ -257,15 +312,41 @@ class EncryptedKeyring(Encrypted, BaseKe if not os.path.exists(self.file_path): return False self._migrate() - config = configparser.RawConfigParser() - config.read(self.file_path) - try: - config.get( - escape_for_ini('keyring-setting'), - escape_for_ini('password reference'), - ) - except (configparser.NoSectionError, configparser.NoOptionError): - return False + + # lock access to the file_path here, make sure it's not being written + # to while while we're checking for keyring-setting + with lockutils.lock("keyringlock",external=True,lock_path="/tmp"): + config = configparser.RawConfigParser() + config.read(self.file_path) + try: + config.get( + escape_for_ini('keyring-setting'), + escape_for_ini('password reference'), + ) + except (configparser.NoSectionError, configparser.NoOptionError): + # The current file doesn't have the keyring-setting, check the backup + logging.warning("_check_file: The current file doesn't have the keyring-setting, check the backup") + if os.path.exists(self.backup_file_path): + config = configparser.RawConfigParser() + config.read(self.backup_file_path) + try: + config.get( + escape_for_ini('keyring-setting'), + escape_for_ini('password reference'), + ) + except (configparser.NoSectionError, configparser.NoOptionError): + return False + # backup file has it, let's use it + try: + src = self.backup_file_path + dest = self.file_path + shutil.copy(src,dest) + except shutil.Error as e: + logging.warning("Revert from Backup failed. Error: %s" % e) + return False + else: + return False + return True def _unlock(self):