Package: release.debian.org Severity: normal User: release.debian.org@packages.debian.org Usertags: tpu Hi release team, please consider the attached debdiff for an upload to testing-proposed-updated. It fixes CVE-2012-4571 (#675379) and CVE-2012-5578 (#696736). To fix CVE-2012-4571 I've backported CryptedFileKeyring from 0.9.3-1. The fix for CVE-2012-5578 is the same as applied in 0.9.2-1.1. Regards -- Sebastian Ramacher
diff -Nru python-keyring-0.7.1/debian/changelog python-keyring-0.7.1/debian/changelog --- python-keyring-0.7.1/debian/changelog 2012-02-14 20:22:10.000000000 +0100 +++ python-keyring-0.7.1/debian/changelog 2013-01-06 21:56:14.000000000 +0100 @@ -1,3 +1,16 @@ +python-keyring (0.7.1-1+deb7u1) testing-proposed-updates; urgency=low + + * Team upload. + * debian/patches: + - CVE-2012-4571.patch: backport CryptedFileKeyring from 0.9.3 to fix + CVE-2012-4571. (Closes: #675379) + - 696736-Fix-insecure-permissions-on-database-files.patch: backport fix + from 0.9.2-1.1 to fix insecure permissions on database files. Fix + CVE-2012-5577 and CVE-2012-5578. Thanks Salvatore Bonaccorso. (Closes: + #696736) + + -- Sebastian Ramacher <sramacher@debian.org> Sun, 06 Jan 2013 21:55:50 +0100 + python-keyring (0.7.1-1) unstable; urgency=low * New upstream version (Closes: #656680, #624690) diff -Nru python-keyring-0.7.1/debian/patches/696736-Fix-insecure-permissions-on-database-files.patch python-keyring-0.7.1/debian/patches/696736-Fix-insecure-permissions-on-database-files.patch --- python-keyring-0.7.1/debian/patches/696736-Fix-insecure-permissions-on-database-files.patch 1970-01-01 01:00:00.000000000 +0100 +++ python-keyring-0.7.1/debian/patches/696736-Fix-insecure-permissions-on-database-files.patch 2013-01-02 17:41:19.000000000 +0100 @@ -0,0 +1,56 @@ +Description: set appropriate file permissions on database file. +Bug: https://bitbucket.org/kang/python-keyring-lib/issue/67/set-go-rwx-on-keyring_passcfg +Bug: https://bitbucket.org/kang/python-keyring-lib/issue/76/insecure-database-file-permissions +Bug-Debian: http://bugs.debian.org/696736 +Bug-Ubuntu: https://bugs.launchpad.net/ubuntu/+source/python-keyring/+bug/1031465 +Forwarded: yes +Author: Marc Deslauriers <marc.deslauriers@canonical.com> +Reviewed-by: Salvatore Bonaccorso <carnil@debian.org> +Last-Update: 2013-01-02 + +--- a/keyring/backend.py ++++ b/keyring/backend.py +@@ -6,6 +6,7 @@ + + import getpass + import os ++import stat + import sys + import ConfigParser + import base64 +@@ -342,6 +343,7 @@ + storage_root = os.path.dirname(self.file_path) + if storage_root and not os.path.isdir(storage_root): + os.makedirs(storage_root) ++ os.chmod(storage_root, stat.S_IWRITE | stat.S_IREAD | stat.S_IEXEC) + + + class UncryptedFileKeyring(BasicFileKeyring): +--- a/keyring/util/loc_compat.py ++++ b/keyring/util/loc_compat.py +@@ -1,5 +1,6 @@ + import os + import shutil ++import stat + import sys + + def relocate_file(old_location, new_location): +@@ -24,4 +25,6 @@ + # ensure the storage path exists + if not os.path.isdir(os.path.dirname(new_location)): + os.makedirs(os.path.dirname(new_location)) ++ os.chmod(os.path.dirname(new_location), ++ stat.S_IWRITE | stat.S_IREAD | stat.S_IEXEC) + shutil.move(old_location, new_location) +--- a/keyring/tests/test_backend.py ++++ b/keyring/tests/test_backend.py +@@ -336,7 +336,8 @@ + def setUp(self): + super(FileKeyringTests, self).setUp() + self.keyring = self.init_keyring() +- self.keyring.file_path = self.tmp_keyring_file = tempfile.mktemp() ++ self.keyring.file_path = self.tmp_keyring_file = os.path.join( ++ tempfile.mkdtemp(), "test_pass.cfg") + + def tearDown(self): + try: diff -Nru python-keyring-0.7.1/debian/patches/CVE-2012-4571.patch python-keyring-0.7.1/debian/patches/CVE-2012-4571.patch --- python-keyring-0.7.1/debian/patches/CVE-2012-4571.patch 1970-01-01 01:00:00.000000000 +0100 +++ python-keyring-0.7.1/debian/patches/CVE-2012-4571.patch 2013-01-06 20:26:46.000000000 +0100 @@ -0,0 +1,508 @@ +Description: backport CryptedFileKeyring from 0.9.3 + Use a random IV to initialize AES cipher. Also use PBKDF2 to derive the AES key + from the user provided password. +Origin: backport, + https://bitbucket.org/kang/python-keyring-lib/commits/576e21a, + https://bitbucket.org/kang/python-keyring-lib/commits/46e94a7, + https://bitbucket.org/kang/python-keyring-lib/commits/2e66ff2, + https://bitbucket.org/kang/python-keyring-lib/commits/6481eb6, + https://bitbucket.org/kang/python-keyring-lib/commits/168830d, + https://bitbucket.org/kang/python-keyring-lib/commits/1cf1b06, + https://bitbucket.org/kang/python-keyring-lib/commits/8751161, + https://bitbucket.org/kang/python-keyring-lib/commits/cd5cdda, + https://bitbucket.org/kang/python-keyring-lib/commits/2e97206, + https://bitbucket.org/kang/python-keyring-lib/commits/7b324f0, + https://bitbucket.org/kang/python-keyring-lib/commits/8881c7d, + https://bitbucket.org/kang/python-keyring-lib/commits/28ed1e5 +Bug-Debian: http://bugs.debian.org/675379 +Last-Update: 2013-01-06 + +--- a/keyring/backend.py ++++ b/keyring/backend.py +@@ -12,6 +12,10 @@ + + from keyring.util.escape import escape as escape_for_ini + from keyring.util import properties ++import keyring.util.escape ++import keyring.util.platform ++import keyring.util.loc_compat ++import json + + try: + from abc import ABCMeta, abstractmethod, abstractproperty +@@ -31,11 +35,6 @@ + except ImportError: + pass + +-_KEYRING_SETTING = 'keyring-setting' +-_CRYPTED_PASSWORD = 'crypted-password' +-_BLOCK_SIZE = 32 +-_PADDING = '0' +- + class PasswordSetError(Exception): + """Raised when the password can't be set. + """ +@@ -264,7 +263,7 @@ + """ + The path to the file where passwords are stored. + """ +- return os.path.join(os.path.expanduser('~'), self.filename) ++ return os.path.join(keyring.util.platform.data_root(), self.filename) + + @abstractproperty + def filename(self): +@@ -284,15 +283,29 @@ + """ + pass + ++ def _migrate(self, keyring_password=None): ++ """Convert older keyrings to the current format." ++ """ ++ pass ++ ++ 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 get_password(self, service, username): + """Read the password from the file. + """ ++ self._relocate_file() + service = escape_for_ini(service) + username = escape_for_ini(username) + + # load the passwords from the file + config = ConfigParser.RawConfigParser() + if os.path.exists(self.file_path): ++ self._migrate() + config.read(self.file_path) + + # fetch the password +@@ -309,6 +322,7 @@ + def set_password(self, service, username, password): + """Write the password in the file. + """ ++ self._relocate_file() + service = escape_for_ini(service) + username = escape_for_ini(username) + +@@ -325,9 +339,17 @@ + if not config.has_section(service): + config.add_section(service) + config.set(service, username, password_base64) ++ self._ensure_file_path() + config_file = open(self.file_path,'w') + config.write(config_file) + ++ def _ensure_file_path(self): ++ """ensure the storage path exists""" ++ storage_root = os.path.dirname(self.file_path) ++ if storage_root and not os.path.isdir(storage_root): ++ os.makedirs(storage_root) ++ ++ + class UncryptedFileKeyring(BasicFileKeyring): + """Uncrypted File Keyring""" + +@@ -351,116 +373,181 @@ + class CryptedFileKeyring(BasicFileKeyring): + """PyCrypto File Keyring""" + ++ # a couple constants ++ block_size = 32 ++ pad_char = '0' ++ + filename = 'crypted_pass.cfg' +- crypted_password = None + + def supported(self): + """Applicable for all platforms, but not recommend" + """ + try: +- from Crypto.Cipher import AES ++ __import__('Crypto.Cipher.AES') ++ __import__('Crypto.Protocol.KDF') ++ __import__('Crypto.Random') + status = 0 + except ImportError: + 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) +- +- def _init_file(self): +- """Init the password file, set the password for it. +- """ ++ @properties.NonDataProperty ++ def keyring_key(self): ++ # _unlock or _init_file will set the key or raise an exception ++ if self._check_file(): ++ self._unlock() ++ else: ++ self._init_file() ++ return self.keyring_key + +- 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 ++ def _get_new_password(self): ++ while True: ++ password = getpass.getpass( ++ "Please set a password for your new keyring: ") ++ confirm = getpass.getpass('Please confirm the password: ') ++ if password != confirm: ++ sys.stderr.write("Error: Your passwords didn't match\n") ++ 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 ++ return password + +- # hash the password +- import crypt +- self.crypted_password = crypt.crypt(password, password) ++ def _init_file(self): ++ """ ++ Initialize a new password file and set the reference password. ++ """ ++ self.keyring_key = self._get_new_password() ++ # set a reference password, used to check that the password provided ++ # matches for subsequent checks. ++ self.set_password('keyring-setting', 'password reference', ++ 'password reference value') + +- # write down the initialization ++ def _check_file(self): ++ """ ++ Check if the file exists and has the expected password reference. ++ """ ++ if not os.path.exists(self.file_path): ++ return False ++ self._migrate() + config = ConfigParser.RawConfigParser() +- config.add_section(_KEYRING_SETTING) +- config.set(_KEYRING_SETTING, _CRYPTED_PASSWORD, self.crypted_password) ++ 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 ++ return True + +- config_file = open(self.file_path,'w') +- config.write(config_file) ++ def _unlock(self): ++ """ ++ Unlock this keyring by getting the password for the keyring from the ++ user. ++ """ ++ self.keyring_key = getpass.getpass( ++ 'Please enter password for encrypted keyring: ') ++ try: ++ ref_pw = self.get_password('keyring-setting', 'password reference') ++ assert ref_pw == 'password reference value' ++ except AssertionError: ++ self._lock() ++ raise ValueError("Incorrect Password") + +- if config_file: +- config_file.close() ++ def _lock(self): ++ """ ++ Remove the keyring key from this instance. ++ """ ++ del self.keyring_key + +- def _check_file(self): +- """Check if the password file has been init properly. ++ def _create_cipher(self, password, salt, IV): + """ +- if os.path.exists(self.file_path): ++ Create the cipher object to encrypt or decrypt a payload. ++ """ ++ from Crypto.Protocol.KDF import PBKDF2 ++ from Crypto.Cipher import AES ++ pw = PBKDF2(password, salt, dkLen=self.block_size) ++ return AES.new(pw[:self.block_size], AES.MODE_CFB, IV) ++ ++ def encrypt(self, password): ++ from Crypto.Random import get_random_bytes ++ salt = get_random_bytes(self.block_size) ++ from Crypto.Cipher import AES ++ IV = get_random_bytes(AES.block_size) ++ cipher = self._create_cipher(self.keyring_key, salt, IV) ++ password_encrypted = cipher.encrypt('pw:' + password) ++ # Serialize the salt, IV, and encrypted password in a secure format ++ data = dict( ++ salt=salt, IV=IV, password_encrypted=password_encrypted, ++ ) ++ for key in data: ++ data[key] = data[key].encode('base64') ++ return json.dumps(data) ++ ++ def decrypt(self, password_encrypted): ++ # unpack the encrypted payload ++ data = json.loads(password_encrypted) ++ for key in data: ++ data[key] = data[key].decode('base64') ++ cipher = self._create_cipher(self.keyring_key, data['salt'], ++ data['IV']) ++ plaintext = cipher.decrypt(data['password_encrypted']) ++ assert plaintext.startswith('pw:') ++ return plaintext[3:] ++ ++ def _migrate(self, keyring_password=None): ++ """ ++ Convert keyring from the 0.9.0 and earlier format to the current ++ format. ++ """ ++ KEYRING_SETTING = 'keyring-setting' ++ CRYPTED_PASSWORD = 'crypted-password' ++ ++ try: + 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 ++ config.get(KEYRING_SETTING, CRYPTED_PASSWORD) ++ except Exception: ++ return + +- def _auth(self, password): +- """Return if the password can open the keyring. +- """ +- import crypt +- return crypt.crypt(password, password) == self.crypted_password ++ print("Keyring from 0.9.0 or earlier detected. Upgrading...") + +- def _init_crypter(self): +- """Init the crypter(using the password of the keyring). +- """ +- # check the password file +- if not self._check_file(): +- self._init_file() ++ import crypt + +- password = self._getpass("Please input your password for the keyring") ++ if keyring_password is None: ++ keyring_password = getpass.getpass( ++ "Please input your password for the keyring: ") + +- if not self._auth(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") + +- # 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) ++ self.keyring_key = keyring_password ++ config.remove_option(KEYRING_SETTING, CRYPTED_PASSWORD) ++ with open(self.file_path, 'w') as f: ++ config.write(f) ++ self.set_password('keyring-setting', 'password reference', ++ 'password reference value') + +- def encrypt(self, password): +- """Encrypt the given password using the pycryto. +- """ +- crypter = self._init_crypter() +- return crypter.encrypt(password) ++ from Crypto.Cipher import AES ++ password = keyring_password + ( ++ self.block_size - len(keyring_password) % self.block_size ++ ) * self.pad_char ++ ++ for service in config.sections(): ++ for user in config.options(service): ++ cipher = AES.new(password, AES.MODE_CFB, ++ '\0' * AES.block_size) ++ password_c = config.get(service, user).decode('base64') ++ service = keyring.util.escape.unescape(service) ++ user = keyring.util.escape.unescape(user) ++ password_p = cipher.decrypt(password_c) ++ self.set_password(service, user, password_p) + +- def decrypt(self, password_encrypted): +- """Decrypt the given password using the pycryto. +- """ +- crypter = self._init_crypter() +- return crypter.decrypt(password_encrypted) ++ print("File upgraded successfully") + + + class Win32CryptoKeyring(BasicFileKeyring): +--- a/keyring/core.py ++++ b/keyring/core.py +@@ -13,6 +13,8 @@ + + from keyring import logger + from keyring import backend ++from keyring.util import platform ++from keyring.util import loc_compat + + def set_keyring(keyring): + """Set current keyring backend. +@@ -111,13 +113,19 @@ + """ + keyring = None + +- # search from current working directory and the home folder +- keyring_cfg_list = [os.path.join(os.getcwd(), "keyringrc.cfg"), +- os.path.join(os.path.expanduser("~"), "keyringrc.cfg")] ++ filename = 'keyringrc.cfg' ++ ++ local_path = os.path.join(os.getcwd(), filename) ++ legacy_path = os.path.join(os.path.expanduser("~"), filename) ++ config_path = os.path.join(platform.data_root(), filename) ++ loc_compat.relocate_file(legacy_path, config_path) ++ ++ # search from current working directory and the data root ++ keyring_cfg_candidates = [local_path, config_path] + + # initialize the keyring_config with the first detected config file + keyring_cfg = None +- for path in keyring_cfg_list: ++ for path in keyring_cfg_candidates: + keyring_cfg = path + if os.path.exists(path): + break +--- a/keyring/tests/test_core.py ++++ b/keyring/tests/test_core.py +@@ -8,9 +8,11 @@ + import sys + import tempfile + import shutil ++import subprocess + + import keyring.backend + import keyring.core ++import keyring.util.platform + + PASSWORD_TEXT = "This is password" + PASSWORD_TEXT_2 = "This is password2" +@@ -105,9 +107,48 @@ + if personal_renamed: + os.rename(personal_cfg+'.old', personal_cfg) + ++class LocationTestCase(unittest.TestCase): ++ legacy_location = os.path.expanduser('~/keyringrc.cfg') ++ new_location = os.path.join(keyring.util.platform.data_root(), ++ 'keyringrc.cfg') ++ ++ @unittest.skipIf(os.path.exists(legacy_location), ++ "Location test requires non-existence of ~/keyringrc.cfg") ++ @unittest.skipIf(os.path.exists(new_location), ++ "Location test requires non-existence of %(new_location)s" ++ % vars()) ++ def test_moves_compat(self): ++ """ ++ When starting the keyring module and ~/keyringrc.cfg exists, it ++ should be moved and the user should be informed that it was ++ moved. ++ """ ++ # create the legacy config ++ with open(self.legacy_location, 'w') as f: ++ f.write('[test config]\n') ++ ++ # invoke load_config in a subprocess ++ cmd = [sys.executable, '-c', 'import keyring.core; keyring.core.load_config()'] ++ proc = subprocess.Popen(cmd, stderr=subprocess.PIPE, stdout=subprocess.PIPE) ++ stdout, stderr = proc.communicate() ++ assert proc.returncode == 0, stderr ++ ++ try: ++ assert not os.path.exists(self.legacy_location) ++ assert os.path.exists(self.new_location) ++ with open(self.new_location) as f: ++ assert 'test config' in f.read() ++ finally: ++ if os.path.exists(self.legacy_location): ++ os.remove(self.legacy_location) ++ if os.path.exists(self.new_location): ++ os.remove(self.new_location) ++ ++ + def test_suite(): + suite = unittest.TestSuite() + suite.addTest(unittest.makeSuite(CoreTestCase)) ++ suite.addTest(unittest.makeSuite(LocationTestCase)) + return suite + + if __name__ == "__main__": +--- /dev/null ++++ b/keyring/util/loc_compat.py +@@ -0,0 +1,27 @@ ++import os ++import shutil ++import sys ++ ++def relocate_file(old_location, new_location): ++ """ ++ keyring 0.8 changes the default location for storage of ++ file-based keyring locations. This function is invoked to move ++ files stored in the old location to the new location. ++ ++ TODO: remove this function for keyring 1.0. ++ """ ++ if not os.path.exists(old_location): ++ # nothing to do; no legacy file found ++ return ++ ++ if os.path.exists(new_location): ++ print >> sys.stderr, ("Password file found in legacy " ++ "location\n %(old_location)s\nand new location\n" ++ " %(new_location)s\nOld location will be ignored." ++ % vars()) ++ return ++ ++ # ensure the storage path exists ++ if not os.path.isdir(os.path.dirname(new_location)): ++ os.makedirs(os.path.dirname(new_location)) ++ shutil.move(old_location, new_location) +--- /dev/null ++++ b/keyring/util/platform.py +@@ -0,0 +1,10 @@ ++import os ++ ++def data_root(): ++ """ ++ Use freedesktop.org Base Dir Specfication to determine storage ++ location. ++ """ ++ fallback = os.path.expanduser('~/.local/share') ++ root = os.environ.get('XDG_DATA_HOME', None) or fallback ++ return os.path.join(root, 'python_keyring') diff -Nru python-keyring-0.7.1/debian/patches/series python-keyring-0.7.1/debian/patches/series --- python-keyring-0.7.1/debian/patches/series 1970-01-01 01:00:00.000000000 +0100 +++ python-keyring-0.7.1/debian/patches/series 2013-01-02 15:51:52.000000000 +0100 @@ -0,0 +1,2 @@ +CVE-2012-4571.patch +696736-Fix-insecure-permissions-on-database-files.patch
Attachment:
signature.asc
Description: Digital signature