VYPR
High severityNVD Advisory· Published Nov 30, 2012· Updated Apr 29, 2026

CVE-2012-4571

CVE-2012-4571

Description

Python Keyring 0.9.1 does not securely initialize the cipher when encrypting passwords for CryptedFileKeyring files, which makes it easier for local users to obtain passwords via a brute-force attack.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
keyringPyPI
< 0.9.20.9.2

Affected products

1

Patches

5
cc1ead78d1e3

Removed test-specific functionality from CryptedFileKeyring - instead have the test runner patch the behavior.

https://github.com/jaraco/keyringJason R. CoombsJun 10, 2012via ghsa
2 files changed · +13 12
  • keyring/backend.py+5 11 modified
    @@ -456,12 +456,6 @@ def supported(self):
                 status = -1
             return status
     
    -    def _getpass(self, *args, **kwargs):
    -        """Wrap getpass.getpass(), so that we can override it when testing.
    -        """
    -
    -        return getpass.getpass(*args, **kwargs)
    -
         @properties.NonDataProperty
         def keyring_key(self):
             # _unlock or _init_file will set the key or raise an exception
    @@ -470,9 +464,9 @@ def keyring_key(self):
     
         def _get_new_password(self):
             while True:
    -            password = self._getpass(
    +            password = getpass.getpass(
                     "Please set a password for your new keyring: ")
    -            confirm = self._getpass('Please confirm the password: ')
    +            confirm = getpass.getpass('Please confirm the password: ')
                 if password != confirm:
                     sys.stderr.write("Error: Your passwords didn't match\n")
                     continue
    @@ -515,7 +509,7 @@ def _unlock(self):
             Unlock this keyring by getting the password for the keyring from the
             user.
             """
    -        self.keyring_key = self._getpass(
    +        self.keyring_key = getpass.getpass(
                 'Please enter password for encrypted keyring: ')
             try:
                 ref_pw = self.get_password('keyring-setting', 'password reference')
    @@ -590,7 +584,7 @@ def __convert_0_9_1(self, keyring_password):
             IV = head[self.block_size:]
     
             if keyring_password is None:
    -            keyring_password = self._getpass(
    +            keyring_password = getpass.getpass(
                     "Please input your password for the keyring: ")
     
             cipher = self._create_cipher(keyring_password, salt, IV)
    @@ -640,7 +634,7 @@ def __convert_0_9_0(self, keyring_password):
             import crypt
     
             if keyring_password is None:
    -            keyring_password = self._getpass(
    +            keyring_password = getpass.getpass(
                     "Please input your password for the keyring: ")
     
             hashed = crypt.crypt(keyring_password, keyring_password)
    
  • keyring/tests/test_backend.py+8 1 modified
    @@ -13,6 +13,7 @@
     import sys
     import tempfile
     import types
    +import getpass
     
     try:
         # Python < 2.7 annd Python >= 3.0 < 3.1
    @@ -364,7 +365,13 @@ class CryptedFileKeyringTestCase(FileKeyringTests, unittest.TestCase):
     
         def setUp(self):
             super(self.__class__, self).setUp()
    -        self.keyring._getpass = lambda *args, **kwargs: "abcdef"
    +        # patch the getpass module to bypass user input
    +        self.getpass_orig = getpass.getpass
    +        getpass.getpass = lambda *args, **kwargs: "abcdef"
    +
    +    def tearDown(self):
    +        getpass.getpass = self.getpass_orig
    +        del self.getpass_orig
     
         def init_keyring(self):
             return keyring.backend.CryptedFileKeyring()
    
56272d908ba7

Now test encrypt/decrypt works on the CryptoFileKeyring again

https://github.com/jaraco/keyringJason R. CoombsJun 10, 2012via ghsa
1 file changed · +0 3
  • keyring/tests/test_backend.py+0 3 modified
    @@ -371,9 +371,6 @@ def setUp(self):
         def init_keyring(self):
             return keyring.backend.CryptedFileKeyring()
     
    -    def test_encrypt_decrypt(self):
    -        pass
    -
     
     @unittest.skipUnless(is_win32_crypto_supported(),
                          "Need Windows")
    
a76942672f6a

CryptedFileKeyring is a BasicFileKeyring again (though it still overrides get_password and set_password and writes an encrypted file)

https://github.com/jaraco/keyringJason R. CoombsJun 10, 2012via ghsa
1 file changed · +1 16
  • keyring/backend.py+1 16 modified
    @@ -433,7 +433,7 @@ def supported(self):
             """
             return 0
     
    -class CryptedFileKeyring(KeyringBackend):
    +class CryptedFileKeyring(BasicFileKeyring):
         """PyCrypto File Keyring"""
     
         # a couple constants
    @@ -442,21 +442,6 @@ class CryptedFileKeyring(KeyringBackend):
     
         filename = 'crypted_pass.cfg'
     
    -    @properties.NonDataProperty
    -    def 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(keyring.util.platform.data_root(), self.filename)
    -
    -    def _relocate_file(self):
    -        old_location = os.path.join(os.path.expanduser('~'), self.filename)
    -        new_location = self.file_path
    -        keyring.util.loc_compat.relocate_file(old_location, new_location)
    -        # disable this function - it only needs to be run once
    -        self._relocate_file = lambda: None
    -
         def supported(self):
             """Applicable for all platforms, but not recommend"
             """
    
cbf509b0386c

CryptedFileKeyring is not a BasicFileKeyring.

https://github.com/jaraco/keyringSebastian RamacherJun 9, 2012via ghsa
2 files changed · +24 5
  • keyring/backend.py+19 4 modified
    @@ -436,11 +436,26 @@ def supported(self):
             """
             return 0
     
    -class CryptedFileKeyring(BasicFileKeyring):
    +class CryptedFileKeyring(KeyringBackend):
         """PyCrypto File Keyring"""
     
    +    @properties.NonDataProperty
    +    def 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(keyring.util.platform.data_root(), self.filename)
    +
         filename = 'crypted_pass.cfg'
     
    +    def _relocate_file(self):
    +        old_location = os.path.join(os.path.expanduser('~'), self.filename)
    +        new_location = self.file_path
    +        keyring.util.loc_compat.relocate_file(old_location, new_location)
    +        # disable this function - it only needs to be run once
    +        self._relocate_file = lambda: None
    +
         def supported(self):
             """Applicable for all platforms, but not recommend"
             """
    @@ -537,7 +552,7 @@ def _convert_old_keyring(self, keyring_password=None):
                 for opt in config.options(section):
                     cipher = AES.new(password, AES.MODE_CFB, '\0' * AES.block_size)
                     p = config.get(section, opt).decode()
    -                p = cipher.decrypt(p.decode('base64'))
    +                p = cipher.decrypt(p.decode('base64')).encode('base64').replace('\n','')
                     config.set(section, opt, p)
     
             self._write_config(config, keyring_password)
    @@ -593,7 +608,7 @@ def get_password(self, service, username):
             # fetch the password
             try:
                 password = config.get(service, username)
    -            password = password.decode('utf-8')
    +            password = password.decode('base64').decode('utf-8')
             except (ConfigParser.NoOptionError, ConfigParser.NoSectionError):
                 password = None
             return password
    @@ -608,7 +623,7 @@ def set_password(self, service, username, password):
     
             config, keyring_password = self._read_config()
     
    -        password = password.encode('utf-8')
    +        password = password.encode('utf-8').encode('base64').replace('\n','')
             # write the modification
             if not config.has_section(service):
                 config.add_section(service)
    
  • keyring/tests/test_backend.py+5 1 modified
    @@ -111,7 +111,8 @@ def is_kwallet_supported():
     def is_crypto_supported():
         try:
             __import__('Crypto.Cipher.AES')
    -        __import__('crypt')
    +        __import__('Crypto.Protocol.KDF')
    +        __import__('Crypto.Random')
         except ImportError:
             return False
         return True
    @@ -370,6 +371,9 @@ def setUp(self):
         def init_keyring(self):
             return keyring.backend.CryptedFileKeyring()
     
    +    def test_encrypt_decrypt(self):
    +        pass
    +
     
     @unittest.skipUnless(is_win32_crypto_supported(),
                          "Need Windows")
    
162f2ed0e39e

Implement new CryptedFileKeyring.

https://github.com/jaraco/keyringSebastian RamacherJun 2, 2012via ghsa
1 file changed · +139 61
  • keyring/backend.py+139 61 modified
    @@ -28,6 +28,11 @@ def abstractmethod(funcobj):
         def abstractproperty(funcobj):
             return property(funcobj)
     
    +try:
    +  from cStringIO import StringIO
    +except ImportError:
    +  from StringIO import StringIO
    +
     _KEYRING_SETTING = 'keyring-setting'
     _CRYPTED_PASSWORD = 'crypted-password'
     _BLOCK_SIZE = 32
    @@ -435,13 +440,14 @@ class CryptedFileKeyring(BasicFileKeyring):
         """PyCrypto File Keyring"""
     
         filename = 'crypted_pass.cfg'
    -    crypted_password = None
     
         def supported(self):
             """Applicable for all platforms, but not recommend"
             """
             try:
                 from Crypto.Cipher import AES
    +            from Crypto.Protocol.KDF import PBKDF2
    +            from Crypto.Random import get_random_bytes
                 status = 0
             except ImportError:
                 status = -1
    @@ -458,92 +464,164 @@ def _init_file(self):
             """
     
             password = None
    -        while 1:
    -            if not password:
    -                password = self._getpass("Please set a password for your new keyring")
    -                password2 = self._getpass('Password (again): ')
    -                if password != password2:
    -                    sys.stderr.write("Error: Your passwords didn't match\n")
    -                    password = None
    -                    continue
    +        while password is None:
    +            password = self._getpass("Please set a password for your new keyring")
    +            password2 = self._getpass('Password (again): ')
    +            if password != password2:
    +                sys.stderr.write("Error: Your passwords didn't match\n")
    +                password = None
    +                continue
                 if '' == password.strip():
                     # forbid the blank password
                     sys.stderr.write("Error: blank passwords aren't allowed.\n")
                     password = None
    -                continue
    -            if len(password) > _BLOCK_SIZE:
    -                # block size of AES is less than 32
    -                sys.stderr.write("Error: password can't be longer than 32.\n")
    -                password = None
    -                continue
    -            break
    -
    -        # hash the password
    -        import crypt
    -        self.crypted_password = crypt.crypt(password, password)
     
             # write down the initialization
             config = ConfigParser.RawConfigParser()
    -        config.add_section(_KEYRING_SETTING)
    -        config.set(_KEYRING_SETTING, _CRYPTED_PASSWORD, self.crypted_password)
    +        self._write_config(config, password)
     
    -        config_file = open(self.file_path,'w')
    -        config.write(config_file)
    +    def _create_cipher(self, password, salt, IV):
    +        """Create the cipher object to encrypt or decrypt the keyring.
    +        """
     
    -        if config_file:
    -            config_file.close()
    +        from Crypto.Protocol.KDF import PBKDF2
    +        from Crypto.Cipher import AES
    +        pw = PBKDF2(password, salt, dkLen=_BLOCK_SIZE)
    +        return AES.new(pw[:_BLOCK_SIZE], AES.MODE_CFB, IV)
     
    -    def _check_file(self):
    -        """Check if the password file has been init properly.
    +    def _write_config(self, config, keyring_password):
    +        """Write the keyring with the given password.
             """
    -        if os.path.exists(self.file_path):
    -            config = ConfigParser.RawConfigParser()
    -            config.read(self.file_path)
    -            try:
    -                self.crypted_password = config.get(_KEYRING_SETTING,
    -                                                    _CRYPTED_PASSWORD)
    -                return self.crypted_password.strip() != ''
    -            except (ConfigParser.NoSectionError, ConfigParser.NoOptionError):
    -                pass
    -        return False
     
    -    def _auth(self, password):
    -        """Return if the password can open the keyring.
    +        config_file = StringIO()
    +        config.write(config_file)
    +        config_file.seek(0)
    +
    +        from Crypto.Random import get_random_bytes
    +        salt = get_random_bytes(_BLOCK_SIZE)
    +        from Crypto.Cipher import AES
    +        IV = get_random_bytes(AES.block_size)
    +        cipher = self._create_cipher(keyring_password, salt, IV)
    +
    +        if not os.path.isdir(os.path.dirname(self.file_path)):
    +            os.makeidrs(os.path.dirname(self.file_path))
    +
    +        encrypted_config_file = open(self.file_path, 'w')
    +        encrypted_config_file.write((salt + IV).encode('base64'))
    +        encrypted_config_file.write(cipher.encrypt(config_file.read()).encode('base64'))
    +        encrypted_config_file.close()
    +
    +    def _convert_old_keyring(self, keyring_password=None):
    +        """Convert keyring to new format.
             """
    +
    +        config_file = open(self.file_path, 'r')
    +        config = ConfigParser.RawConfigParser()
    +        config.readfp(config_file)
    +        config_file.close()
    +
    +        if keyring_password is None:
    +            keyring_password = self._getpass("Please input your password for the keyring: ")
    +
             import crypt
    -        return crypt.crypt(password, password) == self.crypted_password
    +        hashed = crypt.crypt(keyring_password, keyring_password)
    +        if config.get(_KEYRING_SETTING, _CRYPTED_PASSWORD) != hashed:
    +            sys.stderr.write("Wrong password for the keyring.\n")
    +            raise ValueError("Wrong password")
    +
    +        from Crypto.Cipher import AES
    +        password = keyring_password + (_BLOCK_SIZE - len(keyring_password) % _BLOCK_SIZE) * _PADDING
    +
    +        config.remove_option(_KEYRING_SETTING, _CRYPTED_PASSWORD)
    +        for section in config.sections():
    +            for opt in config.options(section):
    +                cipher = AES.new(password, AES.MODE_CFB, '\0' * AES.block_size)
    +                p = config.get(section, opt).decode()
    +                p = cipher.decrypt(p.decode('base64'))
    +                config.set(section, opt, p)
     
    -    def _init_crypter(self):
    -        """Init the crypter(using the password of the keyring).
    +        self._write_config(config, keyring_password)
    +        return (config, keyring_password)
    +
    +
    +    def _read_config(self, keyring_password=None):
    +        """Read the keyring.
             """
    -        # check the password file
    -        if not self._check_file():
    +
    +        # load the passwords from the file
    +        if not os.path.exists(self.file_path):
                 self._init_file()
     
    -        password = self._getpass("Please input your password for the keyring")
    +        encrypted_config_file = open(self.file_path, 'r')
    +        salt = encrypted_config_file.readline()
    +        if salt[0] == '[':
    +          encrypted_config_file.close()
    +          return self._convert_old_keyring(keyring_password)
     
    -        if not self._auth(password):
    +        data = salt.decode('base64')
    +        salt = data[:_BLOCK_SIZE]
    +        IV = data[_BLOCK_SIZE:]
    +        data = encrypted_config_file.read().decode('base64')
    +        encrypted_config_file.close()
    +
    +        if keyring_password is None:
    +            keyring_password = self._getpass("Please input your password for the keyring: ")
    +        cipher = self._create_cipher(keyring_password, salt, IV)
    +
    +        config_file = StringIO(cipher.decrypt(data))
    +        config = ConfigParser.RawConfigParser()
    +        try:
    +            config.readfp(config_file)
    +        except ConfigParser.Error:
                 sys.stderr.write("Wrong password for the keyring.\n")
                 raise ValueError("Wrong password")
    +        return (config, keyring_password)
     
    -        # init the cipher with the password
    -        from Crypto.Cipher import AES
    -        # pad to _BLOCK_SIZE bytes
    -        password = password + (_BLOCK_SIZE - len(password) % _BLOCK_SIZE) * \
    -                                                                    _PADDING
    -        return AES.new(password, AES.MODE_CFB)
    +    def get_password(self, service, username):
    +        """Read the password from the file.
    +        """
     
    -    def encrypt(self, password):
    -        """Encrypt the given password using the pycryto.
    +        self._relocate_file()
    +        service = escape_for_ini(service)
    +        username = escape_for_ini(username)
    +
    +        # load the passwords from the file
    +        if not os.path.exists(self.file_path):
    +            self._init_file()
    +
    +        config, keyring_password = self._read_config()
    +
    +        # fetch the password
    +        try:
    +            password = config.get(service, username)
    +            password = password.decode('utf-8')
    +        except (ConfigParser.NoOptionError, ConfigParser.NoSectionError):
    +            password = None
    +        return password
    +
    +    def set_password(self, service, username, password):
    +        """Save the password in the file.
             """
    -        crypter = self._init_crypter()
    -        return crypter.encrypt(password)
    +
    +        self._relocate_file()
    +        service = escape_for_ini(service)
    +        username = escape_for_ini(username)
    +
    +        config, keyring_password = self._read_config()
    +
    +        password = password.encode('utf-8')
    +        # write the modification
    +        if not config.has_section(service):
    +            config.add_section(service)
    +        config.set(service, username, password)
    +
    +        self._write_config(config, keyring_password)
    +
    +    def encrypt(self, password):
    +        raise NotImplementedError()
     
         def decrypt(self, password_encrypted):
    -        """Decrypt the given password using the pycryto.
    -        """
    -        crypter = self._init_crypter()
    -        return crypter.decrypt(password_encrypted)
    +        raise NotImplementedError()
     
     
     class Win32CryptoKeyring(BasicFileKeyring):
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

12

News mentions

0

No linked articles in our index yet.