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')
