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.
| Package | Affected versions | Patched versions |
|---|---|---|
keyringPyPI | < 0.9.2 | 0.9.2 |
Affected products
1- cpe:2.3:a:python:keyring:0.9.1:*:*:*:*:*:*:*
Patches
5cc1ead78d1e3Removed test-specific functionality from CryptedFileKeyring - instead have the test runner patch the behavior.
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()
56272d908ba7Now test encrypt/decrypt works on the CryptoFileKeyring again
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")
a76942672f6aCryptedFileKeyring is a BasicFileKeyring again (though it still overrides get_password and set_password and writes an encrypted file)
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" """
cbf509b0386cCryptedFileKeyring is not a BasicFileKeyring.
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")
162f2ed0e39eImplement new CryptedFileKeyring.
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- github.com/advisories/GHSA-p3h7-3c45-qj4vghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2012-4571ghsaADVISORY
- pypi.python.org/pypi/keyringnvdWEB
- www.openwall.com/lists/oss-security/2012/10/31/8nvdWEB
- www.ubuntu.com/usn/USN-1634-1nvdWEB
- bugs.launchpad.net/ubuntu/+source/python-keyring/+bug/1004845nvdWEB
- github.com/jaraco/keyring/commit/162f2ed0e39e16d561732b9fad8af6cd2341d7bdghsaWEB
- github.com/jaraco/keyring/commit/56272d908ba7a3fe4ebb6d6e87a7cc569f4726acghsaWEB
- github.com/jaraco/keyring/commit/a76942672f6ac85a88bd9b9ed31fd133119b7702ghsaWEB
- github.com/jaraco/keyring/commit/cbf509b0386c3063d8b2879ce72d78ac18023f72ghsaWEB
- github.com/jaraco/keyring/commit/cc1ead78d1e3fab9fa8bb0b4bb334cb82d35db52ghsaWEB
- github.com/pypa/advisory-database/tree/main/vulns/keyring/PYSEC-2012-8.yamlghsaWEB
News mentions
0No linked articles in our index yet.