Commits

Robert Leftwich committed 9d2195b

Added PyFilesystem and Google Docs backends to latest version

Comments (0)

Files changed (3)

keyring/backend.py

 import getpass
 import os
 import sys
-import ConfigParser
 import base64
 import StringIO
+import copy
+import ConfigParser
+import cPickle
+import codecs
 
 from keyring.util.escape import escape as escape_for_ini
 import keyring.util.escape
 import keyring.util.platform
 import keyring.util.loc_compat
 import keyring.py25compat
+try:
+    from keyczar import keyczar
+except ImportError:
+    pass
 
 # use abstract base classes from the compat module
 abc = keyring.py25compat.abc
     """Raised when the password can't be set.
     """
 
+class InitError(Exception):
+    """Raised when the keyring could not be initialised
+    """
+
 class KeyringBackend(object):
     """The abstract base class of the keyring, every backend must implement
     this interface.
         pass
     return None
 
+class GoogleDocsKeyring(KeyringBackend):
+    """Backend that stores keyring on Google Docs.
+       Note that login and any other initialisation is deferred until it is
+       actually required to allow this keyring class to be added to the
+       global _all_keyring list.
+    """
+
+    keyring_title = 'GoogleKeyring'
+    # status enums
+    OK = 1
+    FAIL = 0
+    CONFLICT = -1
+
+    def __init__(self, credential, source, crypter, 
+                 collection=None, client=None, 
+                 can_create=True, input_getter=raw_input
+                ):
+        from gdata.docs.service import DocsService
+
+        self.credential = credential
+        self.crypter = crypter
+        self.source = source
+        self._collection = collection
+        self.can_create = can_create
+        self.input_getter = input_getter
+        self._keyring_dict = None
+
+        if not client:
+            self._client = DocsService()
+        else:
+            self._client = client
+
+        self._client.source = source
+        self._client.ssl = True
+        self._login_reqd = True
+
+    def supported(self):
+        """Return if this keyring supports current environment:
+        -1: not applicable
+         0: suitable
+         1: recommended
+        """
+        try:
+            from gdata.docs.service import DocsService
+        except ImportError:
+            return -1
+        return 0
+
+    def get_password(self, service, username):
+        """Get password of the username for the service
+        """
+        result = self._get_entry(self._keyring, service, username)
+        if result:
+            result = self._decrypt(result)
+        return result
+
+    def set_password(self, service, username, password):
+        """Set password for the username of the service
+        """
+        password = self._encrypt(password or '')
+        keyring_working_copy = copy.deepcopy(self._keyring)
+        service_entries = keyring_working_copy.get(service)
+        if not service_entries:
+            service_entries = {}
+            keyring_working_copy[service] = service_entries
+        service_entries[username] = password
+        save_result = self._save_keyring(keyring_working_copy)
+        if save_result == self.OK:
+            self._keyring_dict = keyring_working_copy
+            return
+        elif save_result == self.CONFLICT:
+            # check if we can avoid updating
+            self.docs_entry, keyring_dict = self._read()
+            existing_pwd = self._get_entry(self._keyring, service, username)
+            conflicting_pwd = self._get_entry(keyring_dict, service, username)
+            if conflicting_pwd == password:
+                # if someone else updated it to the same value then we are done
+                self._keyring_dict = keyring_working_copy
+                return
+            elif conflicting_pwd is None or conflicting_pwd == existing_pwd:
+                # if doesn't already exist or is unchanged then update it
+                new_service_entries = keyring_dict.get(service, {})
+                new_service_entries[username] = password
+                keyring_dict[service] = new_service_entries
+                save_result = self._save_keyring(keyring_dict)
+                if save_result == self.OK:
+                    self._keyring_dict = keyring_dict
+                    return
+                else:
+                    raise PasswordSetError(
+                        'Failed write after conflict detected')
+            else:
+                raise PasswordSetError(
+                    'Conflict detected, service:%s and username:%s was '\
+                    'set to a different value by someone else' %(service,
+                                                                 username))
+            
+        raise PasswordSetError('Could not save keyring')
+
+    @property
+    def client(self):
+        if not self._client.GetClientLoginToken():
+            import gdata
+            try:
+                self._client.ClientLogin(self.credential.username,
+                                         self.credential.password,
+                                         self._client.source)
+            except gdata.service.CaptchaRequired:
+                sys.stdout.write('Please visit ' + self._client.captcha_url)
+                answer = self.input_getter('Answer to the challenge? ')
+                self._client.email = self.credential.username
+                self._client.password = self.credential.password
+                self._client.ClientLogin(
+                    self.credential.username,
+                    self.credential.password,
+                    self._client.source,
+                    captcha_token=self._client.captcha_token,
+                    captcha_response=answer)
+            except gdata.service.BadAuthentication:
+                raise InitError('Users credential were unrecognized')
+            except gdata.service.Error:
+                raise InitError('Login Error')
+
+        return self._client
+
+    @property
+    def collection(self):
+        return self._collection or self.credential.username.split('@')[0]
+
+    @property
+    def _keyring(self):
+        if self._keyring_dict is None:
+            self.docs_entry, self._keyring_dict = self._read()
+        return self._keyring_dict
+
+    def _get_entry(self, keyring_dict, service, username):
+        result = None
+        service_entries = keyring_dict.get(service)
+        if service_entries:
+            result = service_entries.get(username)
+        return result
+
+    def _decrypt(self, value):
+        if not value:
+            return ''
+        return self.crypter.decrypt(value)
+
+    def _encrypt(self, value):
+        if not value:
+            return ''
+        return self.crypter.encrypt(value)
+
+    def _get_doc_title(self):
+        return '%s' %self.keyring_title
+
+    def _read(self):
+        from gdata.docs.service import DocumentQuery
+        import gdata
+        title_query = DocumentQuery(categories=[self.collection])
+        title_query['title'] = self._get_doc_title()
+        title_query['title-exact'] = 'true'
+        docs = self.client.QueryDocumentListFeed(title_query.ToUri())
+
+        if not docs.entry:
+            if self.can_create:
+                docs_entry = None
+                keyring_dict = {}
+            else:
+                raise InitError(
+                    '%s not found in %s and create not permitted'
+                    %(self._get_doc_title(), self.collection))
+        else:
+            docs_entry = docs.entry[0]
+            file_contents = ''
+            try:
+                url = docs_entry.content.src
+                url += '&exportFormat=txt'
+                server_response = self.client.request('GET', url)
+                if server_response.status != 200:
+                    raise InitError(
+                        'Could not read existing Google Docs keyring')
+                file_contents = server_response.read()
+                if file_contents.startswith(codecs.BOM_UTF8):
+                    file_contents = file_contents[len(codecs.BOM_UTF8):]
+                keyring_dict = cPickle.loads(base64.urlsafe_b64decode(
+                    file_contents.decode('string-escape')))
+            except cPickle.UnpicklingError, ex:
+                raise InitError(
+                    'Could not unpickle existing Google Docs keyring', ex)
+            except TypeError, ex:
+                raise InitError(
+                    'Could not decode existing Google Docs keyring', ex)
+
+        return docs_entry, keyring_dict
+
+    def _save_keyring(self, keyring_dict):
+        """Helper to actually write the keyring to Google"""
+        import gdata
+        result = self.OK
+        file_contents = base64.urlsafe_b64encode(cPickle.dumps(keyring_dict))
+        try:
+            if self.docs_entry:
+                extra_headers = {'Content-Type': 'text/plain',
+                                 'Content-Length': len(file_contents)}
+                self.docs_entry = self.client.Put(
+                    file_contents,
+                    self.docs_entry.GetEditMediaLink().href,
+                    extra_headers=extra_headers
+                    )
+            else:
+                from gdata.docs.service import DocumentQuery
+                # check for existence of folder, create if required
+                folder_query = DocumentQuery(categories=['folder'])
+                folder_query['title'] = self.collection
+                folder_query['title-exact'] = 'true'
+                docs = self.client.QueryDocumentListFeed(folder_query.ToUri())
+                if docs.entry:
+                    folder_entry = docs.entry[0]
+                else:
+                    folder_entry = self.client.CreateFolder(self.collection)
+                file_handle = StringIO.StringIO(file_contents)
+                media_source = gdata.MediaSource(
+                    file_handle=file_handle,
+                    content_type='text/plain',
+                    content_length=len(file_contents),
+                    file_name='temp')
+                self.docs_entry = self.client.Upload(
+                    media_source,
+                    self._get_doc_title(),
+                    folder_or_uri=folder_entry
+                )
+        except gdata.service.RequestError, ex:
+            try:
+                if ex.message['reason'].lower().find('conflict') != -1:
+                    result = self.CONFLICT
+                else:
+                    # Google docs has a bug when updating a shared document
+                    # using PUT from any account other that the owner.
+                    # It returns an error 400 "Sorry, there was an error saving the file. Please try again"
+                    # *despite* actually updating the document!
+                    # Workaround by re-reading to see if it actually updated
+                    if ex.message['body'].find(
+                        'Sorry, there was an error saving the file') != -1:
+                        new_docs_entry, new_keyring_dict = self._read()
+                        if new_keyring_dict == keyring_dict:
+                            result = self.OK
+                        else:
+                            result = self.FAIL
+                    else:
+                        result = self.FAIL
+            except:
+                result = self.FAIL
+
+        return result
+
+class Credential(object):
+    """Abstract class to manage credentials
+    """
+
+    __metaclass__ = abc.ABCMeta
+
+    @abc.abstractproperty
+    def username(self):
+        return None
+
+    @abc.abstractproperty
+    def password(self):
+        return None
+
+class BaseCredential(Credential):
+    """Simple credentials implementation
+    """
+
+    def __init__(self, username, password):
+        self._username = username
+        self._password = password
+
+    @property
+    def username(self):
+        return self._username
+
+    @property
+    def password(self):
+        return self._password
+
+class EnvironCredential(Credential):
+    """Source credentials from environment variables.
+       Actual sourcing is deferred until requested.
+    """
+
+    def __init__(self, user_env_var, pwd_env_var):
+        self.user_env_var = user_env_var
+        self.pwd_env_var = pwd_env_var
+
+    def _get_env(self, env_var):
+        """Helper to read an environment variable
+        """
+        value = os.environ.get(env_var)
+        if not value:
+            raise ValueError('Missing environment variable:%s' %env_var)
+        return value
+
+    @property
+    def username(self):
+        return self._get_env(self.user_env_var)
+
+    @property
+    def password(self):
+        return self._get_env(self.pwd_env_var)
+
+class GoogleEnvironCredential(EnvironCredential):
+    """Retrieve credentials from specifically named environment variables
+    """
+
+    def __init__(self):
+        super(GoogleEnvironCredential, self).__init__(
+            'GOOGLE_KEYRING_USER',
+            'GOOGLE_KEYRING_PASSWORD')
+
+class Crypter(object):
+    """Base class providing encryption and decryption
+    """
+
+    @abc.abstractmethod
+    def encrypt(self, value):
+        """Encrypt the value.
+        """
+        pass
+
+    @abc.abstractmethod
+    def decrypt(self, value):
+        """Decrypt the value.
+        """
+        pass
+
+class NullCrypter(Crypter):
+    """A crypter that does nothing
+    """
+
+    def encrypt(self, value):
+        return value
+
+    def decrypt(self, value):
+        return value
+
+class BaseKeyczarCrypter(Crypter):
+    """Base Keyczar keyset encryption and decryption.
+       The keyset initialisation is deferred until required.
+    """
+
+    @abc.abstractproperty
+    def keyset_location(self):
+        """Location for the main keyset that may be encrypted or not"""
+        pass
+
+    @abc.abstractproperty
+    def encrypting_keyset_location(self):
+        """Location for the encrypting keyset.
+           Use None to indicate that the main keyset is not encrypted
+        """
+        pass
+
+    @property
+    def crypter(self):
+        """The actual keyczar crypter"""
+        if not hasattr(self, '_crypter'):
+            # initialise the Keyczar keysets
+            if not self.keyset_location:
+                raise ValueError('No encrypted keyset location!')
+            reader = keyczar.readers.CreateReader(self.keyset_location)
+            if self.encrypting_keyset_location:
+                encrypting_keyczar = keyczar.Crypter.Read(
+                    self.encrypting_keyset_location)
+                reader = keyczar.readers.EncryptedReader(reader,
+                                                         encrypting_keyczar)
+            self._crypter = keyczar.Crypter(reader)
+        return self._crypter
+
+    def encrypt(self, value):
+        """Encrypt the value.
+        """
+        if not value:
+            return ''
+        return self.crypter.Encrypt(value)
+
+    def decrypt(self, value):
+        """Decrypt the value.
+        """
+        if not value:
+            return ''
+        return self.crypter.Decrypt(value)
+
+class KeyczarCrypter(BaseKeyczarCrypter):
+    """A Keyczar crypter using locations specified in the constructor
+    """
+
+    def __init__(self, keyset_location, encrypting_keyset_location=None):
+        self._keyset_location = keyset_location
+        self._encrypting_keyset_location = encrypting_keyset_location
+
+    @property
+    def keyset_location(self):
+        return self._keyset_location
+
+    @property
+    def encrypting_keyset_location(self):
+        return self._encrypting_keyset_location
+
+class EnvironKeyczarCrypter(BaseKeyczarCrypter):
+    """A Keyczar crypter using locations specified by environment vars
+    """
+
+    KEYSET_ENV_VAR = 'KEYRING_KEYCZAR_ENCRYPTED_LOCATION'
+    ENC_KEYSET_ENV_VAR = 'KEYRING_KEYCZAR_ENCRYPTING_LOCATION'
+
+    @property
+    def keyset_location(self):
+        val = os.environ.get(self.KEYSET_ENV_VAR)
+        if not val:
+            raise ValueError('%s environment value not set' %
+                             self.KEYSET_ENV_VAR)
+        return val
+
+    @property
+    def encrypting_keyset_location(self):
+        return os.environ.get(self.ENC_KEYSET_ENV_VAR)
+
+class EnvironGoogleDocsKeyring(GoogleDocsKeyring):
+    """Google Docs keyring using keyczar initialized from environment
+    variables
+    """
+
+    def __init__(self):
+        crypter = EnvironKeyczarCrypter()
+        credential = GoogleEnvironCredential()
+        source = os.environ.get('GOOGLE_KEYRING_SOURCE')
+        super(EnvironGoogleDocsKeyring, self).__init__(
+            credential, source, crypter)
+
+    def supported(self):
+        """Return if this keyring supports current environment:
+        -1: not applicable
+         0: suitable
+         1: recommended
+        """
+        try:
+            from keyczar import keyczar
+            return super(EnvironGoogleDocsKeyring, self).supported()
+        except ImportError:
+            return -1
+
+class BasicPyfilesystemKeyring(KeyringBackend):
+    """BasicPyfilesystemKeyring is a Pyfilesystem-based implementation of
+    keyring.
+
+    It stores the password directly in the file, and supports
+    encryption and decryption. The encrypted password is stored in base64
+    format. 
+    Being based on Pyfilesystem the file can be local or network-based and
+    served by any of the filesystems supported by Pyfilesystem including Amazon
+    S3, FTP, WebDAV, memory and more.
+    """
+
+    default_filename = '~/keyring_pyf_pass.cfg'
+
+    def __init__(self, crypter, filename=None, can_create=True,
+                 cache_timeout=None):
+        super(BasicPyfilesystemKeyring, self).__init__()
+        self._crypter = crypter
+        self._filename = filename or self.default_filename
+        self._can_create = can_create
+        self._cache_timeout = cache_timeout
+
+    @property
+    def filename(self):
+        """The filename used to store the passwords.
+        """
+        return self._filename
+
+    def encrypt(self, password):
+        """Encrypt the password.
+        """
+        if not password or not self._crypter:
+            return password or ''
+        return self._crypter.encrypt(password)
+
+    def decrypt(self, password_encrypted):
+        """Decrypt the password.
+        """
+        if not password_encrypted or not self._crypter:
+            return password_encrypted or ''
+        return self._crypter.decrypt(password_encrypted)
+
+    def _open(self, mode='rb'):
+        """Open the password file in the specified mode
+        """
+        import fs.opener
+        import fs.errors
+        import fs.path
+        import fs.remote
+        open_file = None
+        writeable = 'w' in mode or 'a' in mode or '+' in mode
+        try:
+            # NOTE: currently the MemOpener does not split off any filename
+            #       which causes errors on close()
+            #       so we add a dummy name and open it separately
+            if (self.filename.startswith('mem://') or
+                self.filename.startswith('ram://')):
+                open_file = fs.opener.fsopendir(self.filename).open('kr.cfg',
+                                                                    mode)
+            else:
+                if not hasattr(self, '_pyfs'):
+                    # reuse the pyfilesystem and path
+                    self._pyfs, self._path = fs.opener.opener.parse(self.filename, 
+                                               writeable=writeable)
+                    # cache if permitted
+                    if self._cache_timeout is not None:
+                        self._pyfs = fs.remote.CacheFS(
+                            self._pyfs, cache_timeout=self._cache_timeout)
+                open_file = self._pyfs.open(self._path, mode)
+        except fs.errors.ResourceNotFoundError:
+            if self._can_create:
+                segments = fs.opener.opener.split_segments(self.filename)
+                if segments:
+                    # this seems broken, but pyfilesystem uses it, so we must
+                    fs_name, credentials, url1, url2, path = segments.groups()
+                    assert fs_name, 'Should be a remote filesystem'
+                    host = ''
+                    # allow for domain:port
+                    if ':' in url2:
+                        split_url2 = url2.split('/', 1)
+                        if len(split_url2) > 1:
+                            url2 = split_url2[1]
+                        else:
+                            url2 = ''
+                        host = split_url2[0]
+                    pyfs = fs.opener.opener.opendir('%s://%s' %(fs_name, host))
+                    # cache if permitted
+                    if self._cache_timeout is not None:
+                        pyfs = fs.remote.CacheFS(
+                            pyfs, cache_timeout=self._cache_timeout)
+                    url2_path, url2_filename = fs.path.split(url2)
+                    if url2_path and not pyfs.exists(url2_path):
+                        pyfs.makedir(url2_path, recursive=True)
+                else:
+                    # assume local filesystem
+                    import fs.osfs
+                    full_url = fs.opener._expand_syspath(self.filename)
+                    url2_path, url2 = fs.path.split(full_url)
+                    pyfs = fs.osfs.OSFS(url2_path)
+
+                try:
+                    # reuse the pyfilesystem and path
+                    self._pyfs = pyfs
+                    self._path = url2
+                    return pyfs.open(url2, mode)
+                except fs.errors.ResourceNotFoundError:
+                    if writeable:
+                        raise
+                    else:
+                        pass
+            # NOTE: ignore read errors as the underlying caller can fail safely
+            if writeable:
+                raise
+            else:
+                pass
+        return open_file
+
+    @property
+    def config(self):
+        """load the passwords from the config file
+        """
+        if not hasattr(self, '_config'):
+            raw_config = ConfigParser.RawConfigParser()
+            f = self._open()
+            if f:
+                raw_config.readfp(f)
+                f.close()
+            self._config = raw_config
+        return self._config
+
+    def get_password(self, service, username):
+        """Read the password from the file.
+        """
+        service = escape_for_ini(service)
+        username = escape_for_ini(username)
+
+        # fetch the password
+        try:
+            password_base64 = self.config.get(service, username).encode()
+            # decode with base64
+            password_encrypted = base64.decodestring(password_base64)
+            # decrypted the password
+            password = self.decrypt(password_encrypted).decode('utf-8')
+        except (ConfigParser.NoOptionError, ConfigParser.NoSectionError):
+            password = None
+        return password
+
+    def set_password(self, service, username, password):
+        """Write the password in the file.
+        """
+        service = escape_for_ini(service)
+        username = escape_for_ini(username)
+
+        # encrypt the password
+        password = password or ''
+        password_encrypted = self.encrypt(password.encode('utf-8'))
+
+        # encode with base64
+        password_base64 = base64.encodestring(password_encrypted).decode()
+        # write the modification
+        if not self.config.has_section(service):
+            self.config.add_section(service)
+        self.config.set(service, username, password_base64)
+        config_file = self._open('w')
+        self.config.write(config_file)
+        config_file.close()
+
+    def supported(self):
+        """Applicable when Pyfilesystem installed, but do not recommend.
+        """
+        try:
+            from fs.opener import fsopen
+            return 0
+        except ImportError:
+            return -1
+
+class UnencryptedPyfilesystemKeyring(BasicPyfilesystemKeyring):
+    """Unencrypted Pyfilesystem Keyring
+    """
+
+    def __init__(self, filename=BasicPyfilesystemKeyring.default_filename,
+                 can_create=True, cache_timeout=None):
+        super(UnencryptedPyfilesystemKeyring, self).__init__(
+            NullCrypter(), filename=filename, can_create=can_create,
+            cache_timeout=cache_timeout)
+
+class EncryptedPyfilesystemKeyring(BasicPyfilesystemKeyring):
+    """Encrypted Pyfilesystem Keyring
+    """
+
+    default_filename = '~/crypted_pyf_pass.cfg'
+
+    def __init__(self, crypter, filename=default_filename, can_create=True,
+                 cache_timeout=None):
+        super(EncryptedPyfilesystemKeyring, self).__init__(
+            crypter, filename=filename, can_create=can_create,
+            cache_timeout=cache_timeout)
+
+class EnvironEncryptedPyfilesystemKeyring(EncryptedPyfilesystemKeyring):
+    """Encrypted Pyfilesystem Keyring using Keyczar keysets specified in
+    environment vars
+    """
+
+    def __init__(self):
+        super(EnvironEncryptedPyfilesystemKeyring, self).__init__(
+            EnvironKeyczarCrypter())
 
 _all_keyring = None
 
+class MultipartKeyringWrapper(KeyringBackend):
+
+    """A wrapper around an existing keyring that breaks the password into
+    smaller parts to handle implementations that have limits on the maximum
+    length of passwords i.e. Windows Vault
+    """
+
+    def __init__(self, keyring, max_password_size=512):
+        self._keyring = keyring
+        self._max_password_size = max_password_size
+
+    def supported(self):
+        """Return if this keyring supports current environment:
+        -1: not applicable
+         0: suitable
+         1: recommended
+        """
+        return self._keyring.supported()
+
+    def get_password(self, service, username):
+        """Get password of the username for the service
+        """
+        init_part = self._keyring.get_password(service, username)
+        if init_part:
+            parts = [init_part,]
+            i = 1
+            while True:
+                next_part = self._keyring.get_password(
+                    service,
+                    '%s{{part_%d}}' %(username, i))
+                if next_part:
+                    parts.append(next_part)
+                    i += 1
+                else:
+                    break
+            return ''.join(parts)
+        return None
+
+    def set_password(self, service, username, password):
+        """Set password for the username of the service
+        """
+        password_parts = [
+            password[i:i + self._max_password_size] for i in range(0,
+                                                                   len(password),
+                                                                   self._max_password_size)]
+        for i, password_part in enumerate(password_parts):
+            curr_username = username
+            if i > 0:
+                curr_username += '{{part_%d}}' %i
+            self._keyring.set_password(service, curr_username, password_part)
+
 def get_all_keyring():
     """Return the list of all keyrings in the lib
     """
         _all_keyring = [OSXKeychain(), GnomeKeyring(), KDEKWallet(),
                         CryptedFileKeyring(), UncryptedFileKeyring(),
                         Win32CryptoKeyring(), Win32CryptoRegistry(),
-                        WinVaultKeyring(), SecretServiceKeyring()]
+                        WinVaultKeyring(), SecretServiceKeyring(),
+                        EnvironGoogleDocsKeyring(),
+                        UnencryptedPyfilesystemKeyring(),
+                        EnvironEncryptedPyfilesystemKeyring()]
     return _all_keyring

keyring/tests/mocks.py

+"""
+mocks.py
+
+Various mock objects for testing
+"""
+
+import cPickle
+import base64
+import codecs
+from StringIO import StringIO
+
+class MockAtom(object):
+    """ Mocks an atom in the GData service. """
+    def __init__(self, value):
+        self.text = value
+        
+class MockEntry(object):
+    """ Mocks and entry returned from the GData service. """       
+    def __init__(self, title, ID):
+        self.title = MockAtom(title)
+        self.id = MockAtom('http://mock.example.com/%s' % ID)
+        self.ID = ID # simpler lookup for key value
+
+    def GetEditMediaLink(self):
+        return MockLink()
+    
+
+class MockHTTPClient(object):
+    """ Mocks the functionality of an http client. """
+    def request(*args, **kwargs):
+        pass
+    
+class MockGDataService(object):
+    """ Provides the common functionality of a Google Service. """
+    http_client = MockHTTPClient()
+    def __init__(self, email=None, password=None,
+                 account_type='HOSTED_OR_GOOGLE', service=None,
+                 auth_service_url=None, source=None, server=None,
+                 additional_headers=None, handler=None, tokens=None,
+                 http_client=None, token_store=None):
+        """ Create the Service with the default parameters. """
+        self.email = email
+        self.password = password
+        self.account_type = account_type
+        self.service = service
+        self.auth_service_url = auth_service_url
+        self.server = server
+        self.login_token = None
+    
+    def GetClientLoginToken(self):
+        return self.login_token
+        
+    def SetClientLoginToken(self, token):
+        self.login_token = token
+        
+    def ClientLogin(self, username, password, account_type=None, service=None,
+                    auth_service_url=None, source=None, captcha_token=None, 
+                    captcha_response=None):
+        
+        """ Client side login to the service. """
+        if hasattr(self, '_login_err'):
+            raise self._login_err()
+
+class MockDocumentService(MockGDataService):
+    """ 
+    Implements the minimum functionality of the Google Document service. 
+    """
+
+    def Upload(self, media_source, title, folder_or_uri=None, label=None):
+        """ 
+        Upload a document.  
+        """
+        if hasattr(self, '_upload_err'):
+            raise self._upload_err()
+        if not hasattr(self, '_upload_count'):
+            self._upload_count = 0
+        # save the data for asserting against
+        self._upload_data =  dict(media_source=media_source, title=title,
+                               folder_or_uri=folder_or_uri, label=label)
+        self._upload_count += 1
+        return MockEntry(title, 'mockentry%3A' + title)
+    
+    def QueryDocumentListFeed(self, uri):
+        if hasattr(self, '_listfeed'):
+            return self._listfeed
+        return MockListFeed()
+                              
+    def CreateFolder(self, title, folder_or_uri=None):
+        if hasattr(self, '_create_folder_err'):
+            raise self._create_folder_err()
+        if hasattr(self, '_create_folder'):
+            return self._create_folder
+        return MockListEntry()
+
+    def Put(self, data, uri, extra_headers=None, url_params=None, 
+            escape_params=True, redirects_remaining=3, media_source=None,
+            converter=None):
+        import gdata
+        self._put_data = None
+        if not hasattr(self, '_put_count'):
+            self._put_count = 0
+        if hasattr(self, '_put_err'):
+            # allow for a list of errors
+            if type(self._put_err) == list:
+                put_err = self._put_err.pop(0)
+                if not len(self._put_err):
+                    delattr(self, '_put_err')
+            else:
+                put_err = self._put_err
+            if type(put_err) == tuple:
+                raise put_err[0], put_err[1]
+            else:
+                raise put_err()
+        # save the data for asserting against
+        assert isinstance(data, basestring), \
+            'Should be a string'
+        self._put_data =  cPickle.loads(base64.urlsafe_b64decode(data))
+        self._put_count += 1
+        return MockEntry('', 'mockentry%3A' + '')
+
+    def Export(self, entry_or_id_or_url, file_path, gid=None, extra_params=None):
+        if hasattr(self, '_export_err'):
+            raise self._export_err()
+        if hasattr(self, '_export_data'):
+            export_file = open(file_path, 'wb')
+            export_file.write(self._export_data)
+            export_file.close()
+
+    def request(self, data, uri):
+        if hasattr(self, '_request_err'):
+            if type(self._request_err) == tuple:
+                raise self._request_err[0], self._request_err[1]
+            else:
+                raise self._request_err()
+        if hasattr(self, '_request_response'):
+            return MockHttpResponse(self._request_response)
+
+class MockHttpResponse(StringIO, object):
+
+    def __init__(self, response_dict):
+        super(MockHttpResponse, self).__init__(response_dict.get('data', ''))
+        self.status = response_dict.get('status', 200)
+        self.reason = response_dict.get('reason', '')
+
+class MockListFeed(object):
+
+    @property
+    def entry(self):
+        if hasattr(self, '_entry'):
+            return self._entry
+        return []
+
+class MockListEntry(object):
+
+    pass
+
+class MockLink(object):
+
+    @property
+    def href(self):
+        return ''
+
+class MockContent(object):
+
+    @property
+    def src(self):
+        return 'src'
+
+class MockDocumentListEntry(object):
+
+    @property
+    def content(self):
+        return MockContent()
+
+    def GetEditMediaLink(self):
+        return MockLink()
+
+class MockKeyczarReader(object):
+
+    def __init__(self, location):
+        self.location = location
+
+class MockKeyczarEncryptedReader(object):
+
+    def __init__(self, reader, crypter):
+        self._reader = reader
+        self._crypter = crypter
+
+class MockKeyczarReaders(object):
+
+    @staticmethod
+    def CreateReader(location):
+        return MockKeyczarReader(location)
+
+    @staticmethod
+    def EncryptedReader(reader, crypter):
+        return MockKeyczarEncryptedReader(reader, crypter)
+
+class MockKeyczarCrypter(object):
+
+    def __init__(self, reader):
+        self.reader = reader
+
+    @staticmethod
+    def Read(location):
+        return MockKeyczarCrypter(MockKeyczarReader(location))
+
+class MockKeyczar(object):
+
+    @property
+    def readers(self):
+       return MockKeyczarReaders
+
+    @property
+    def Crypter(self):
+        return MockKeyczarCrypter

keyring/tests/test_backend.py

 created by Kang Zhang 2009-07-14
 """
 
+import base64
+import codecs
+import cPickle
 import contextlib
+import getpass
 import os
 import random
 import string
 import sys
 import tempfile
 import types
-import getpass
 
 try:
     # Python < 2.7 annd Python >= 3.0 < 3.1
 
 import keyring.backend
 
+from keyring.tests import mocks
+
 ALPHABET = string.ascii_letters + string.digits
 DIFFICULT_CHARS = string.whitespace + string.punctuation
 
         return False
     return True
 
+def is_keyczar_supported():
+    try:
+        __import__('keyczar')
+    except ImportError:
+        print 'NO KEYCZAR'
+        return False
+    return True
+
+def is_gdata_supported():
+    try:
+        __import__('gdata.service')
+    except ImportError:
+        print 'NO GDAT'
+        return False
+    return True
+
+def is_pyfilesystem_supported():
+    try:
+        __import__('fs.opener')
+    except ImportError:
+        print 'NO FS'
+        return False
+    return True
+
 class BackendBasicTests(object):
     """Test for the keyring's basic funtions. password_set and password_get
     """
                 self.assertEqual(-1, self.keyring.supported())
 
 
+def init_google_docs_keyring(client, can_create=True,
+                             input_getter=raw_input):
+    credentials = keyring.backend.BaseCredential('foo', 'bar')
+    return keyring.backend.GoogleDocsKeyring(credentials, 
+                                             'test_src',
+                                             keyring.backend.NullCrypter(),
+                                             client=client,
+                                             can_create=can_create,
+                                             input_getter=input_getter
+                                            )
+
+@unittest.skipUnless(is_gdata_supported(),
+                     "Need Google Docs (gdata)")
+class GoogleDocsKeyringTestCase(BackendBasicTests, unittest.TestCase):
+    """Run all the standard tests on a new keyring"""
+
+    def init_keyring(self):
+        client = mocks.MockDocumentService()
+        client.SetClientLoginToken('foo')
+        return init_google_docs_keyring(client)
+
+@unittest.skipUnless(is_gdata_supported(),
+                     "Need Google Docs (gdata)")
+class GoogleDocsKeyringInteractionTestCase(unittest.TestCase):
+    """Additional tests for Google Doc interactions"""
+
+    def _init_client(self, set_token=True):
+        client = mocks.MockDocumentService()
+        if set_token:
+            client.SetClientLoginToken('interaction')
+        return client
+
+    def _init_keyring(self, client):
+        self.keyring = init_google_docs_keyring(client)
+
+    def _init_listfeed(self):
+        listfeed = mocks.MockListFeed()
+        listfeed._entry = [mocks.MockDocumentListEntry(),
+                           mocks.MockDocumentListEntry()
+                          ]
+        return listfeed
+    
+    def _encode_data(self, data):
+        return base64.urlsafe_b64encode(cPickle.dumps(data))
+
+    def test_handles_auth_failure(self):
+        import gdata
+        client = self._init_client(set_token=False)
+        client._login_err = gdata.service.BadAuthentication
+        self._init_keyring(client)
+        try:
+            google_client = self.keyring.client
+            self.assertTrue(False, 'Should throw InitError')
+        except keyring.backend.InitError:
+            pass
+
+    def test_handles_auth_error(self):
+        import gdata
+        client = self._init_client(set_token=False)
+        client._login_err = gdata.service.Error
+        self._init_keyring(client)
+        try:
+            google_client = self.keyring.client
+            self.assertTrue(False, 'Should throw InitError')
+        except keyring.backend.InitError:
+            pass
+
+    def test_handles_login_captcha(self):
+        import gdata
+        client = self._init_client(set_token=False)
+        client._login_err = gdata.service.CaptchaRequired
+        client.captcha_url = 'a_captcha_url'
+        client.captcha_token = 'token'
+        self.get_input_called = False
+        def _get_input(prompt):
+            self.get_input_called = True
+            delattr(client, '_login_err')
+            return 'Foo'
+        self.keyring = init_google_docs_keyring(client, input_getter=_get_input)
+        google_client = self.keyring.client
+        self.assertTrue(self.get_input_called, 'Should have got input')
+
+    def test_retrieves_existing_keyring_with_and_without_bom(self):
+        client = self._init_client()
+        dummy_entries = dict(section1=dict(user1='pwd1'))
+        no_utf8_bom_entries = self._encode_data(dummy_entries)
+        client._request_response = dict(status=200, data=no_utf8_bom_entries)
+        client._listfeed = self._init_listfeed()
+        self._init_keyring(client)
+        self.assertEqual(self.keyring.get_password('section1', 'user1'), 'pwd1')
+
+        utf8_bom_entries = codecs.BOM_UTF8 + no_utf8_bom_entries
+        client._request_response = dict(status=200, data=utf8_bom_entries)
+        self._init_keyring(client)
+        self.assertEqual(self.keyring.get_password('section1', 'user1'), 'pwd1')
+
+    def test_handles_retrieve_failure(self):
+        import gdata
+        client = self._init_client()
+        client._listfeed = self._init_listfeed()
+        client._request_response = dict(status=400,
+                                        reason='Data centre explosion')
+        self._init_keyring(client)
+        try:
+            self.keyring.get_password('any', 'thing')
+            self.assertTrue(False, 'Should throw InitError')
+        except keyring.backend.InitError:
+            pass
+
+    def test_handles_corrupt_retrieve(self):
+        client = self._init_client()
+        dummy_entries = dict(section1=dict(user1='pwd1'))
+        client._request_response = dict(status=200, data='broken' + self._encode_data(dummy_entries))
+        client._listfeed = self._init_listfeed()
+        self._init_keyring(client)
+        try:
+            self.keyring.get_password('any', 'thing')
+            self.assertTrue(False, 'Should throw InitError')
+        except keyring.backend.InitError:
+            pass
+
+    def test_no_create_if_requested(self):
+        client = self._init_client()
+        self.keyring = init_google_docs_keyring(client, can_create=False)
+        try:
+            self.keyring.get_password('any', 'thing')
+            self.assertTrue(False, 'Should throw InitError')
+        except keyring.backend.InitError:
+            pass
+
+    def test_no_set_if_create_folder_fails_on_new_keyring(self):
+        import gdata
+        client = self._init_client()
+        client._create_folder_err = gdata.service.RequestError
+        self._init_keyring(client)
+        self.assertEqual(self.keyring.get_password('service-a', 'user-A'), None,
+                        'No password should be set in new keyring')
+        try:
+            self.keyring.set_password('service-a', 'user-A', 'password-A')
+            self.assertTrue(False, 'Should throw PasswordSetError')
+        except keyring.backend.PasswordSetError:
+            pass
+        self.assertEqual(self.keyring.get_password('service-a', 'user-A'), None,
+                        'No password should be set after write fail')
+
+    def test_no_set_if_write_fails_on_new_keyring(self):
+        import gdata
+        client = self._init_client()
+        client._upload_err = gdata.service.RequestError
+        self._init_keyring(client)
+        self.assertEqual(self.keyring.get_password('service-a', 'user-A'), None,
+                        'No password should be set in new keyring')
+        try:
+            self.keyring.set_password('service-a', 'user-A', 'password-A')
+            self.assertTrue(False, 'Should throw PasswordSetError')
+        except keyring.backend.PasswordSetError:
+            pass
+        self.assertEqual(self.keyring.get_password('service-a', 'user-A'), None,
+                        'No password should be set after write fail')
+
+    def test_no_set_if_write_fails_on_existing_keyring(self):
+        import gdata
+        client = self._init_client()
+        dummy_entries = dict(sectionB=dict(user9='pwd9'))
+        client._request_response = dict(status=200, data=self._encode_data(dummy_entries)) 
+        client._put_err = gdata.service.RequestError
+        client._listfeed = self._init_listfeed()
+        self._init_keyring(client)
+        self.assertEqual(self.keyring.get_password('sectionB', 'user9'), 'pwd9',
+                        'Correct password should be set in existing keyring')
+        try:
+            self.keyring.set_password('sectionB', 'user9', 'Not the same pwd')
+            self.assertTrue(False, 'Should throw PasswordSetError')
+        except keyring.backend.PasswordSetError:
+            pass
+        self.assertEqual(self.keyring.get_password('sectionB', 'user9'), 'pwd9',
+                        'Password should be unchanged after write fail')
+
+    def test_writes_correct_data_to_google_docs(self):
+        import gdata
+        client = self._init_client()
+        dummy_entries = dict(sectionWriteChk=dict(userWriteChk='pwd'))
+        client._request_response = dict(status=200, data=self._encode_data(dummy_entries))
+        client._listfeed = self._init_listfeed()
+        self._init_keyring(client)
+        self.keyring.set_password('sectionWriteChk',
+                                  'userWritechk',
+                                  'new_pwd')
+        self.assertIsNotNone(client._put_data, 'Should have written data')
+        self.assertEquals(
+            'new_pwd',
+            client._put_data.get('sectionWriteChk').get('userWritechk'),
+            'Did not write updated password!')
+
+    def test_handles_write_conflict_on_different_service(self):
+        import gdata
+        client = self._init_client()
+        dummy_entries = dict(sectionWriteConflictA=dict(
+            userwriteConflictA='pwdwriteConflictA'))
+        client._request_response = dict(status=200, data=self._encode_data(dummy_entries))
+        client._put_err = [(gdata.service.RequestError,
+                               {'status': '406',
+                                'reason': 'Conflict'}),]
+        client._listfeed = self._init_listfeed()
+        self._init_keyring(client)
+        self.assertEqual(
+            self.keyring.get_password('sectionWriteConflictA',
+                                      'userwriteConflictA'),
+            'pwdwriteConflictA',
+            'Correct password should be set in existing keyring')
+        dummy_entries['diffSection'] = dict(foo='bar')
+        client._request_response = dict(status=200, data=self._encode_data(dummy_entries))
+        new_pwd = 'Not the same pwd'
+        self.keyring.set_password('sectionWriteConflictA',
+                                  'userwriteConflictA',
+                                  new_pwd)
+
+        self.assertEquals(self.keyring.get_password('sectionWriteConflictA',
+                                                    'userwriteConflictA'),
+                          new_pwd
+        )
+        self.assertEqual(1, client._put_count,
+                         'Write not called after conflict resolution')
+
+    def test_handles_write_conflict_on_same_service_and_username(self):
+        import gdata
+        client = self._init_client()
+        dummy_entries = dict(sectionWriteConflictB=dict(
+            userwriteConflictB='pwdwriteConflictB'))
+        client._request_response = dict(status=200, data=self._encode_data(dummy_entries))
+        client._put_err = (gdata.service.RequestError,
+                               {'status': '406',
+                                'reason': 'Conflict'})
+        client._listfeed = self._init_listfeed()
+        self._init_keyring(client)
+        self.assertEqual(
+            self.keyring.get_password('sectionWriteConflictB',
+                                      'userwriteConflictB'),
+            'pwdwriteConflictB',
+            'Correct password should be set in existing keyring')
+        conflicting_dummy_entries = dict(sectionWriteConflictB=dict(
+            userwriteConflictB='pwdwriteConflictC'))
+        client._request_response = dict(status=200, data=self._encode_data(conflicting_dummy_entries))
+        try:
+            self.keyring.set_password('sectionWriteConflictB',
+                                      'userwriteConflictB',
+                                      'new_pwd')
+            self.assertTrue(False, 'Should throw PasswordSetError')
+        except keyring.backend.PasswordSetError:
+            pass
+
+    def test_handles_write_conflict_with_identical_change(self):
+        import gdata
+        client = self._init_client()
+        dummy_entries = dict(sectionWriteConflictC=dict(
+            userwriteConflictC='pwdwriteConflictC'))
+        client._request_response = dict(status=200, data=self._encode_data(dummy_entries))
+        client._put_err = [(gdata.service.RequestError,
+                               {'status': '406',
+                                 'reason': 'Conflict'}),]
+        client._listfeed = self._init_listfeed()
+        self._init_keyring(client)
+        self.assertEqual(
+            self.keyring.get_password('sectionWriteConflictC',
+                                      'userwriteConflictC'),
+            'pwdwriteConflictC',
+            'Correct password should be set in existing keyring')
+        new_pwd = 'Not the same pwd'
+        conflicting_dummy_entries = dict(sectionWriteConflictC=dict(
+            userwriteConflictC=new_pwd))
+        client._request_response = dict(status=200, data=self._encode_data(conflicting_dummy_entries))
+        self.keyring.set_password('sectionWriteConflictC',
+                                  'userwriteConflictC',
+                                  new_pwd)
+        self.assertEquals(self.keyring.get_password('sectionWriteConflictC',
+                                                    'userwriteConflictC'),
+                          new_pwd
+        )
+
+    def test_handles_broken_google_put_when_non_owner_update_fails(self):
+        """Google Docs has a bug when putting to a non-owner
+           see  GoogleDocsKeyring._save_keyring()
+        """
+        import gdata
+        client = self._init_client()
+        dummy_entries = dict(sectionBrokenPut=dict(
+            userBrokenPut='pwdBrokenPut'))
+        client._request_response = dict(status=200, data=self._encode_data(dummy_entries))
+        client._put_err = [(
+            gdata.service.RequestError,
+                { 'status': '400',
+                  'body': 'Sorry, there was an error saving the file. Please try again.',
+                  'reason': 'Bad Request'}),]
+        client._listfeed = self._init_listfeed()
+        self._init_keyring(client)
+        new_pwd = 'newPwdBrokenPut'
+        correct_read_entries = dict(sectionBrokenPut=dict(
+            userBrokenPut='pwdBrokenPut'))
+        client._request_response = dict(status=200,
+                                        data=self._encode_data(correct_read_entries))
+        try:
+            self.keyring.set_password('sectionBrokenPut',
+                                      'userBrokenPut',
+                                      new_pwd)
+            self.assertTrue(False, 'Should throw PasswordSetError')
+        except keyring.backend.PasswordSetError:
+            pass
+
+    def test_handles_broken_google_put_when_non_owner_update(self):
+        """Google Docs has a bug when putting to a non-owner
+           see  GoogleDocsKeyring._save_keyring()
+        """
+        import gdata
+        client = self._init_client()
+        dummy_entries = dict(sectionBrokenPut=dict(
+            userBrokenPut='pwdBrokenPut'))
+        client._request_response = dict(status=200, data=self._encode_data(dummy_entries))
+        client._put_err = [(
+            gdata.service.RequestError,
+                { 'status': '400',
+                  'body': 'Sorry, there was an error saving the file. Please try again.',
+                  'reason': 'Bad Request'}),]
+        client._listfeed = self._init_listfeed()
+        self._init_keyring(client)
+        new_pwd = 'newPwdBrokenPut'
+        correct_read_entries = dict(sectionBrokenPut=dict(
+            userBrokenPut=new_pwd))
+        client._request_response = dict(status=200,
+                                        data=self._encode_data(correct_read_entries))
+        self.keyring.set_password('sectionBrokenPut',
+                                  'userBrokenPut',
+                                  new_pwd)
+        self.assertEquals(self.keyring.get_password('sectionBrokenPut',
+                                                    'userBrokenPut'),
+                          new_pwd)
+
+    def test_uses_existing_folder(self):
+        import gdata
+        client = self._init_client()
+        # should not happen
+        client._create_folder_err = gdata.service.RequestError
+
+        self._init_keyring(client)
+        self.assertEqual(self.keyring.get_password('service-a', 'user-A'), None,
+                         'No password should be set in new keyring')
+        client._listfeed = self._init_listfeed()
+        self.keyring.set_password('service-a', 'user-A', 'password-A')
+        self.assertIsNotNone(client._upload_data, 'Should have written data')
+        self.assertEqual(self.keyring.get_password('service-a', 'user-A'),
+                         'password-A',
+                         'Correct password should be set')
+
+class ReverseCrypter(keyring.backend.Crypter):
+    """Very silly crypter class"""
+
+    def encrypt(self, value):
+        return value[::-1]
+
+    def decrypt(self, value):
+        return value[::-1]
+
+class PyfilesystemKeyringTests(BackendBasicTests):
+    """Base class for Pyfilesystem tests"""
+
+    def setUp(self):
+        super(PyfilesystemKeyringTests, self).setUp()
+        self.keyring = self.init_keyring()
+
+    def tearDown(self):
+        del self.keyring
+
+    def test_encrypt_decrypt(self):
+        password = random_string(20)
+        encrypted = self.keyring.encrypt(password)
+
+        self.assertEqual(password, self.keyring.decrypt(encrypted))
+
+@unittest.skipUnless(is_pyfilesystem_supported(),
+                     "Need Pyfilesystem")
+class UnencryptedMemoryPyfilesystemKeyringNoSubDirTestCase(
+    PyfilesystemKeyringTests,
+    unittest.TestCase):
+    """Test in memory with no encryption"""
+
+    keyring_filename = 'mem://unencrypted'
+
+    def init_keyring(self):
+        return keyring.backend.UnencryptedPyfilesystemKeyring(
+            filename=self.keyring_filename)
+
+@unittest.skipUnless(is_pyfilesystem_supported(),
+                     "Need Pyfilesystem")
+class UnencryptedMemoryPyfilesystemKeyringSubDirTestCase(
+    PyfilesystemKeyringTests,
+    unittest.TestCase):
+    """Test in memory with no encryption"""
+
+    keyring_filename = 'mem://some/sub/dir/unencrypted'
+
+    def init_keyring(self):
+        return keyring.backend.UnencryptedPyfilesystemKeyring(
+            filename=self.keyring_filename)
+
+@unittest.skipUnless(is_pyfilesystem_supported(),
+                     "Need Pyfilesystem")
+class UnencryptedLocalPyfilesystemKeyringNoSubDirTestCase(
+    PyfilesystemKeyringTests,
+    unittest.TestCase):
+    """Test using local temp files with no encryption"""
+
+    keyring_filename = '%s/keyring.cfg' %tempfile.mkdtemp()
+
+    def init_keyring(self):
+        return keyring.backend.UnencryptedPyfilesystemKeyring(
+            filename=self.keyring_filename)
+
+    def test_handles_preexisting_keyring(self):
+        from fs.opener import opener
+        fs, path = opener.parse(self.keyring_filename, writeable=True)
+        keyring_file = fs.open(path, 'wb')
+        keyring_file.write(
+            """[svc1]
+user1 = cHdkMQ==
+            """)
+        keyring_file.close()
+        pyf_keyring = keyring.backend.UnencryptedPyfilesystemKeyring(
+            filename=self.keyring_filename)
+        self.assertEquals('pwd1', pyf_keyring.get_password('svc1', 'user1'))
+
+    def tearDown(self):
+        del self.keyring
+        if os.path.exists(self.keyring_filename):
+            os.remove(self.keyring_filename)
+
+@unittest.skipUnless(is_pyfilesystem_supported(),
+                     "Need Pyfilesystem")
+class UnencryptedLocalPyfilesystemKeyringSubDirTestCase(
+    PyfilesystemKeyringTests,
+    unittest.TestCase):
+    """Test using local temp files with no encryption"""
+
+    keyring_dir = '%s//more/sub/dirs' %tempfile.mkdtemp()
+    keyring_filename = '%s/keyring.cfg' %keyring_dir
+
+    def init_keyring(self):
+
+        if not os.path.exists(self.keyring_dir):
+            os.makedirs(self.keyring_dir)
+        return keyring.backend.UnencryptedPyfilesystemKeyring(
+            filename=self.keyring_filename)
+
+@unittest.skipUnless(is_pyfilesystem_supported(),
+                     "Need Pyfilesystem")
+class EncryptedMemoryPyfilesystemKeyringTestCase(PyfilesystemKeyringTests,
+                                                 unittest.TestCase):
+    """Test in memory with encryption"""
+
+    def init_keyring(self):
+        return keyring.backend.EncryptedPyfilesystemKeyring(
+            ReverseCrypter(),
+            filename='mem://encrypted/keyring.cfg')
+
+@unittest.skipUnless(is_pyfilesystem_supported(),
+                     "Need Pyfilesystem")
+class EncryptedLocalPyfilesystemKeyringNoSubDirTestCase(
+    PyfilesystemKeyringTests,
+    unittest.TestCase):
+    """Test using local temp files with encryption"""
+
+    def init_keyring(self):
+        return keyring.backend.EncryptedPyfilesystemKeyring(
+            ReverseCrypter(),
+            filename='temp://keyring.cfg')
+
+@unittest.skipUnless(is_pyfilesystem_supported(),
+                     "Need Pyfilesystem")
+class EncryptedLocalPyfilesystemKeyringSubDirTestCase(
+    PyfilesystemKeyringTests,
+    unittest.TestCase):
+    """Test using local temp files with encryption"""
+
+    def init_keyring(self):
+        return keyring.backend.EncryptedPyfilesystemKeyring(
+            ReverseCrypter(),
+            filename='temp://a/sub/dir/hierarchy/keyring.cfg')
+
+@unittest.skipUnless(is_keyczar_supported(),
+                     "Need Keyczar")
+class KeyczarCrypterTestCase(unittest.TestCase):
+
+    """Test the keyczar crypter"""
+
+    def setUp(self):
+        self._orig_keyczar = keyring.backend.keyczar
+        keyring.backend.keyczar = mocks.MockKeyczar()
+
+    def tearDown(self):
+        keyring.backend.keyczar = self._orig_keyczar
+        if keyring.backend.EnvironKeyczarCrypter.KEYSET_ENV_VAR in os.environ:
+            del os.environ[keyring.backend.EnvironKeyczarCrypter.KEYSET_ENV_VAR]
+        if keyring.backend.EnvironKeyczarCrypter.ENC_KEYSET_ENV_VAR in os\
+        .environ:
+            del os.environ[
+                keyring.backend.EnvironKeyczarCrypter.ENC_KEYSET_ENV_VAR]
+
+    def testKeyczarCrypterWithUnencryptedReader(self):
+        """
+        """
+        location = 'bar://baz'
+        kz_crypter = keyring.backend.KeyczarCrypter(location)
+        self.assertEquals(location, kz_crypter.keyset_location)
+        self.assertIsNone(kz_crypter.encrypting_keyset_location)
+        self.assertIsInstance(kz_crypter.crypter, mocks.MockKeyczarCrypter)
+        self.assertIsInstance(kz_crypter.crypter.reader, mocks.MockKeyczarReader)
+        self.assertEquals(location, kz_crypter.crypter.reader.location)
+
+    def testKeyczarCrypterWithEncryptedReader(self):
+        """
+        """
+        location = 'foo://baz'
+        encrypting_location = 'castle://aaargh'
+        kz_crypter = keyring.backend.KeyczarCrypter(location, encrypting_location)
+        self.assertEquals(location, kz_crypter.keyset_location)
+        self.assertEquals(encrypting_location,
+                          kz_crypter.encrypting_keyset_location)
+        self.assertIsInstance(kz_crypter.crypter, mocks.MockKeyczarCrypter)
+        self.assertIsInstance(kz_crypter.crypter.reader,
+                              mocks.MockKeyczarEncryptedReader)
+        self.assertEquals(location, kz_crypter.crypter.reader._reader.location)
+        self.assertEquals(encrypting_location,
+                          kz_crypter.crypter.reader._crypter.reader.location)
+
+    def testKeyczarCrypterEncryptDecryptHandlesEmptyNone(self):
+        location = 'castle://aargh'
+        kz_crypter = keyring.backend.KeyczarCrypter(location)
+        self.assertEquals('', kz_crypter.encrypt(''))
+        self.assertEquals('', kz_crypter.encrypt(None))
+        self.assertEquals('', kz_crypter.decrypt(''))
+        self.assertEquals('', kz_crypter.decrypt(None))
+
+    def testEnvironCrypterReadsCorrectValues(self):
+        location = 'foo://baz'
+        encrypting_location = 'castle://aaargh'
+        kz_crypter = keyring.backend.EnvironKeyczarCrypter()
+        os.environ[kz_crypter.KEYSET_ENV_VAR] = location
+        self.assertEqual(location, kz_crypter.keyset_location)
+        self.assertIsNone(kz_crypter.encrypting_keyset_location)
+        os.environ[kz_crypter.ENC_KEYSET_ENV_VAR] = encrypting_location
+        self.assertEqual(encrypting_location, kz_crypter.encrypting_keyset_location)
+
+    def testEnvironCrypterThrowsExceptionOnMissingValues(self):
+        location = 'foo://baz'
+        encrypting_location = 'castle://aaargh'
+        kz_crypter = keyring.backend.EnvironKeyczarCrypter()
+        try:
+            locn = kz_crypter.keyset_location
+            self.assertTrue(False, msg="Should have thrown ValueError")
+        except ValueError:
+            # expected
+            pass
+        self.assertIsNone(kz_crypter.encrypting_keyset_location)
+
+class MultipartKeyringWrapperTestCase(unittest.TestCase):
+
+    """Test the wrapper that breaks passwords into smaller chunks"""
+
+    class MockKeyring(keyring.backend.KeyringBackend):
+
+        def __init__(self):
+            self.passwords = {}
+
+        def supported(self):
+            return 'yes'
+
+        def get_password(self, service, username):
+            return self.passwords.get(service+username)
+
+        def set_password(self, service, username, password):
+            self.passwords[service+username] = password
+
+    def testSupportedPassThru(self):
+        kr = keyring.backend.MultipartKeyringWrapper(self.MockKeyring())
+        self.assertEquals(kr.supported(), 'yes')
+
+    def testMissingPassword(self):
+        wrapped_kr = self.MockKeyring()
+        kr = keyring.backend.MultipartKeyringWrapper(wrapped_kr)
+        self.assertIsNone(kr.get_password('s1', 'u1'))
+
+    def testSmallPasswordSetInSinglePart(self):
+        wrapped_kr = self.MockKeyring()
+        kr = keyring.backend.MultipartKeyringWrapper(wrapped_kr)
+        kr.set_password('s1', 'u1', 'p1')
+        self.assertEquals(wrapped_kr.passwords, {'s1u1':'p1'})
+        # should be able to read it back
+        self.assertEquals(kr.get_password('s1', 'u1'), 'p1')
+
+    def testLargePasswordSetInMultipleParts(self):
+        wrapped_kr = self.MockKeyring()
+        kr = keyring.backend.MultipartKeyringWrapper(wrapped_kr,
+                                                     max_password_size=2)
+        kr.set_password('s2', 'u2', '0123456')
+        self.assertEquals(wrapped_kr.passwords, {'s2u2':'01',
+                                                 's2u2{{part_1}}':'23',
+                                                 's2u2{{part_2}}':'45',
+                                                 "s2u2{{part_3}}":'6'})
+
+        # should be able to read it back
+        self.assertEquals(kr.get_password('s2', 'u2'), '0123456')
+
 def test_suite():
     suite = unittest.TestSuite()
     suite.addTest(unittest.makeSuite(OSXKeychainTestCase))
     suite.addTest(unittest.makeSuite(CryptedFileKeyringTestCase))
     suite.addTest(unittest.makeSuite(Win32CryptoKeyringTestCase))
     suite.addTest(unittest.makeSuite(WinVaultKeyringTestCase))
+    suite.addTest(unittest.makeSuite(GoogleDocsKeyringTestCase))
+    suite.addTest(unittest.makeSuite(GoogleDocsKeyringInteractionTestCase))
+    suite.addTest(unittest.makeSuite(UnencryptedMemoryPyfilesystemKeyringNoSubDirTestCase))
+    suite.addTest(unittest.makeSuite(UnencryptedMemoryPyfilesystemKeyringSubDirTestCase))
+    suite.addTest(unittest.makeSuite(UnencryptedLocalPyfilesystemKeyringNoSubDirTestCase))
+    suite.addTest(unittest.makeSuite(UnencryptedLocalPyfilesystemKeyringSubDirTestCase))
+    suite.addTest(unittest.makeSuite(EncryptedMemoryPyfilesystemKeyringTestCase))
+    suite.addTest(unittest.makeSuite(EncryptedLocalPyfilesystemKeyringNoSubDirTestCase))
+    suite.addTest(unittest.makeSuite(EncryptedLocalPyfilesystemKeyringSubDirTestCase))
+    suite.addTest(unittest.makeSuite(KeyczarCrypterTestCase))
+    suite.addTest(unittest.makeSuite(MultipartKeyringWrapperTestCase))
     return suite
 
 if __name__ == '__main__':