[Date Prev][Date Next] [Thread Prev][Thread Next] [Date Index] [Thread Index]

Bug#697562: tpu: python-keyring/0.7.1-1+deb7u1



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


Reply to: