class NickNym(object): def __init__(self, provider, config, soledad_session, username, token, uuid): nicknym_url = _discover_nicknym_server(provider) self._email = '%s@%s' % (username, provider.domain) self.keymanager = KeyManager('%s@%s' % (username, provider.domain), nicknym_url, soledad_session.soledad, token, which_api_CA_bundle(provider), provider.api_uri, provider.api_version, uuid, config.gpg_binary) def generate_openpgp_key(self): if not self._key_exists(self._email): print "Generating keys - this could take a while..." self._gen_key() self._send_key_to_leap() def _key_exists(self, email): try: self.keymanager.get_key(email, openpgp.OpenPGPKey, private=True, fetch_remote=False) return True except KeyNotFound: return False def _gen_key(self): self.keymanager.gen_key(openpgp.OpenPGPKey) def _send_key_to_leap(self): self.keymanager.send_key(openpgp.OpenPGPKey)
class NickNym(object): def __init__(self, provider, config, soledad_session, srp_session): nicknym_url = _discover_nicknym_server(provider) self._email = '%s@%s' % (srp_session.user_name, provider.domain) self.keymanager = KeyManager('%s@%s' % (srp_session.user_name, provider.domain), nicknym_url, soledad_session.soledad, srp_session.token, which_bundle(provider), provider.api_uri, provider.api_version, srp_session.uuid, config.gpg_binary) def generate_openpgp_key(self): if not self._key_exists(self._email): self._gen_key() self._send_key_to_leap() def _key_exists(self, email): try: self.keymanager.get_key(email, openpgp.OpenPGPKey, private=True, fetch_remote=False) return True except KeyNotFound: return False def _gen_key(self): self.keymanager.gen_key(openpgp.OpenPGPKey) def _send_key_to_leap(self): self.keymanager.send_key(openpgp.OpenPGPKey)
class NickNym(object): def __init__(self, provider, config, soledad_session, email_address, token, uuid): nicknym_url = _discover_nicknym_server(provider) self._email = email_address self.keymanager = KeyManager(self._email, nicknym_url, soledad_session.soledad, token, LeapCertificate(provider).provider_api_cert, provider.api_uri, provider.api_version, uuid, config.gpg_binary) def generate_openpgp_key(self): if not self._key_exists(self._email): print "Generating keys - this could take a while..." self._gen_key() self._send_key_to_leap() def _key_exists(self, email): try: self.keymanager.get_key(email, openpgp.OpenPGPKey, private=True, fetch_remote=False) return True except KeyNotFound: return False def _gen_key(self): self.keymanager.gen_key(openpgp.OpenPGPKey) def _send_key_to_leap(self): self.keymanager.send_key(openpgp.OpenPGPKey)
class NickNym(object): def __init__(self, provider, config, soledad, email_address, token, uuid): nicknym_url = _discover_nicknym_server(provider) self._email = email_address self.keymanager = KeyManager(self._email, nicknym_url, soledad, token=token, ca_cert_path=LeapCertificate(provider).provider_api_cert, api_uri=provider.api_uri, api_version=provider.api_version, uid=uuid, gpgbinary=config.gpg_binary) @defer.inlineCallbacks def generate_openpgp_key(self): key_present = yield self._key_exists(self._email) if not key_present: print "Generating keys - this could take a while..." yield self._gen_key() # Sending it anyway for now. TODO: This can be better with real checking (downloading pubkey from nicknym) yield self._send_key_to_leap() @defer.inlineCallbacks def _key_exists(self, email): try: yield self.fetch_key(email, private=True, fetch_remote=False) defer.returnValue(True) except KeyNotFound: defer.returnValue(False) def fetch_key(self, email, private=False, fetch_remote=True): return self.keymanager.get_key(email, openpgp.OpenPGPKey, private=private, fetch_remote=fetch_remote) def _gen_key(self): return self.keymanager.gen_key(openpgp.OpenPGPKey) def _send_key_to_leap(self): return self.keymanager.send_key(openpgp.OpenPGPKey)
class NickNym(object): def __init__(self, provider, config, soledad, email_address, token, uuid): nicknym_url = _discover_nicknym_server(provider) self._email = email_address self.keymanager = KeyManager(self._email, nicknym_url, soledad, token=token, ca_cert_path=LeapCertificate(provider).provider_api_cert, api_uri=provider.api_uri, api_version=provider.api_version, uid=uuid, gpgbinary=config.gpg_binary) @defer.inlineCallbacks def generate_openpgp_key(self): key_present = yield self._key_exists(self._email) if not key_present: logger.info("Generating keys - this could take a while...") yield self._gen_key() yield self._send_key_to_leap() @defer.inlineCallbacks def _key_exists(self, email): try: yield self.fetch_key(email, private=True, fetch_remote=False) defer.returnValue(True) except KeyNotFound: defer.returnValue(False) def fetch_key(self, email, private=False, fetch_remote=True): return self.keymanager.get_key(email, private=private, fetch_remote=fetch_remote) def _gen_key(self): return self.keymanager.gen_key() def _send_key_to_leap(self): return self.keymanager.send_key()
def test_retrieves_key(self, requests_mock): nickserver_url = 'http://some/nickserver/uri' soledad = MagicMock() soledad.get_from_index.side_effect = [[], [TestDoc(sample_key)]] km = KeyManager('*****@*****.**', nickserver_url, soledad, ca_cert_path='some path') result = km.get_key('*****@*****.**', OpenPGPKey) self.assertEqual(str(OpenPGPKey('*****@*****.**', key_id='key_id')), str(result))
class SoledadBootstrapper(AbstractBootstrapper): """ Soledad init procedure. """ SOLEDAD_KEY = "soledad" KEYMANAGER_KEY = "keymanager" PUBKEY_KEY = "user[public_key]" MAX_INIT_RETRIES = 10 def __init__(self, signaler=None): AbstractBootstrapper.__init__(self, signaler) if signaler is not None: self._cancel_signal = signaler.soledad_cancelled_bootstrap self._provider_config = None self._soledad_config = None self._download_if_needed = False self._user = "" self._password = u"" self._address = "" self._uuid = "" self._srpauth = None self._soledad = None self._keymanager = None @property def srpauth(self): if flags.OFFLINE is True: return None if self._srpauth is None: leap_assert(self._provider_config is not None, "We need a provider config") self._srpauth = SRPAuth(self._provider_config) return self._srpauth @property def soledad(self): return self._soledad @property def keymanager(self): return self._keymanager # initialization def load_offline_soledad(self, username, password, uuid): """ Instantiate Soledad for offline use. :param username: full user id (user@provider) :type username: str or unicode :param password: the soledad passphrase :type password: unicode :param uuid: the user uuid :type uuid: str or unicode """ self._address = username self._password = password self._uuid = uuid def error(failure): # TODO: we should handle more specific exceptions in here logger.exception(failure.value) self._signaler.signal(self._signaler.soledad_offline_failed) d = self.load_and_sync_soledad(uuid, offline=True) d.addCallback( lambda _: self._signaler.signal( self._signaler.soledad_offline_finished)) d.addErrback(error) return d def _get_soledad_local_params(self, uuid, offline=False): """ Return the locals parameters needed for the soledad initialization. :param uuid: the uuid of the user, used in offline mode. :type uuid: unicode, or None. :return: secrets_path, local_db_path, token :rtype: tuple """ # in the future, when we want to be able to switch to # online mode, this should be a proxy object too. # Same for server_url below. if offline is False: token = self.srpauth.get_token() else: token = "" secrets_path, local_db_path = get_db_paths(uuid) logger.debug('secrets_path:%s' % (secrets_path,)) logger.debug('local_db:%s' % (local_db_path,)) return (secrets_path, local_db_path, token) def _get_soledad_server_params(self, uuid, offline): """ Return the remote parameters needed for the soledad initialization. :param uuid: the uuid of the user, used in offline mode. :type uuid: unicode, or None. :return: server_url, cert_file :rtype: tuple """ if offline is True: server_url = "http://localhost:9999/" cert_file = "" else: if uuid is None: uuid = self.srpauth.get_uuid() server_url = self._pick_server(uuid) cert_file = self._provider_config.get_ca_cert_path() return server_url, cert_file def _do_soledad_init(self, uuid, secrets_path, local_db_path, server_url, cert_file, token, syncable): """ Initialize soledad, retry if necessary and raise an exception if we can't succeed. :param uuid: user identifier :type uuid: str :param secrets_path: path to secrets file :type secrets_path: str :param local_db_path: path to local db file :type local_db_path: str :param server_url: soledad server uri :type server_url: str :param cert_file: path to the certificate of the ca used to validate the SSL certificate used by the remote soledad server. :type cert_file: str :param auth token: auth token :type auth_token: str """ init_tries = 1 while init_tries <= self.MAX_INIT_RETRIES: try: logger.debug("Trying to init soledad....") self._try_soledad_init( uuid, secrets_path, local_db_path, server_url, cert_file, token, syncable) logger.debug("Soledad has been initialized.") return except Exception as exc: init_tries += 1 msg = "Init failed, retrying... (retry {0} of {1})".format( init_tries, self.MAX_INIT_RETRIES) logger.warning(msg) continue self._signaler.signal(self._signaler.soledad_bootstrap_failed) logger.exception(exc) raise SoledadInitError() def load_and_sync_soledad(self, uuid=u"", offline=False): """ Once everthing is in the right place, we instantiate and sync Soledad :param uuid: the uuid of the user, used in offline mode. :type uuid: unicode. :param offline: whether to instantiate soledad for offline use. :type offline: bool :return: A Deferred which fires when soledad is sync, or which fails with SoledadInitError or SoledadSyncError :rtype: defer.Deferred """ local_param = self._get_soledad_local_params(uuid, offline) remote_param = self._get_soledad_server_params(uuid, offline) secrets_path, local_db_path, token = local_param server_url, cert_file = remote_param if offline: return self._load_soledad_nosync( uuid, secrets_path, local_db_path, cert_file, token) else: return self._load_soledad_online(uuid, secrets_path, local_db_path, server_url, cert_file, token) def _load_soledad_online(self, uuid, secrets_path, local_db_path, server_url, cert_file, token): syncable = True try: self._do_soledad_init(uuid, secrets_path, local_db_path, server_url, cert_file, token, syncable) except SoledadInitError as e: # re-raise the exceptions from try_init, # we're currently handling the retries from the # soledad-launcher in the gui. return defer.fail(e) leap_assert(not sameProxiedObjects(self._soledad, None), "Null soledad, error while initializing") address = make_address( self._user, self._provider_config.get_domain()) syncer = Syncer(self._soledad, self._signaler) d = self._init_keymanager(address, token) d.addCallback(lambda _: syncer.sync()) d.addErrback(self._soledad_sync_errback) return d def _load_soledad_nosync(self, uuid, secrets_path, local_db_path, cert_file, token): syncable = False self._do_soledad_init(uuid, secrets_path, local_db_path, "", cert_file, token, syncable) d = self._init_keymanager(self._address, token) return d def _soledad_sync_errback(self, failure): failure.trap(InvalidAuthTokenError) # in the case of an invalid token we have already turned off mail and # warned the user def _pick_server(self, uuid): """ Choose a soledad server to sync against. :param uuid: the uuid for the user. :type uuid: unicode :returns: the server url :rtype: unicode """ if not self._soledad_config: self._soledad_config = SoledadConfig() # TODO: Select server based on timezone (issue #3308) server_dict = self._soledad_config.get_hosts() if not server_dict.keys(): # XXX raise more specific exception, and catch it properly! raise Exception("No soledad server found") selected_server = server_dict[first(server_dict.keys())] server_url = "https://%s:%s/user-%s" % ( selected_server["hostname"], selected_server["port"], uuid) logger.debug("Using soledad server url: %s" % (server_url,)) return server_url def _try_soledad_init(self, uuid, secrets_path, local_db_path, server_url, cert_file, auth_token, syncable): """ Try to initialize soledad. :param uuid: user identifier :type uuid: str :param secrets_path: path to secrets file :type secrets_path: str :param local_db_path: path to local db file :type local_db_path: str :param server_url: soledad server uri :type server_url: str :param cert_file: path to the certificate of the ca used to validate the SSL certificate used by the remote soledad server. :type cert_file: str :param auth token: auth token :type auth_token: str """ # TODO: If selected server fails, retry with another host # (issue #3309) encoding = sys.getfilesystemencoding() try: self._soledad = Soledad( uuid, self._password, secrets_path=secrets_path.encode(encoding), local_db_path=local_db_path.encode(encoding), server_url=server_url, cert_file=cert_file.encode(encoding), auth_token=auth_token, defer_encryption=True, syncable=syncable) # XXX All these errors should be handled by soledad itself, # and return a subclass of SoledadInitializationFailed # recoverable, will guarantee retries except (socket.timeout, socket.error, BootstrapSequenceError): logger.warning("Error while initializing Soledad") raise # unrecoverable except (u1db_errors.Unauthorized, u1db_errors.HTTPError): logger.error("Error while initializing Soledad (u1db error).") raise except Exception as exc: logger.exception("Unhandled error while initializating " "Soledad: %r" % (exc,)) raise def _download_config(self): """ Download the Soledad config for the given provider """ leap_assert(self._provider_config, "We need a provider configuration!") logger.debug("Downloading Soledad config for %s" % (self._provider_config.get_domain(),)) self._soledad_config = SoledadConfig() download_service_config( self._provider_config, self._soledad_config, self._session, self._download_if_needed) def _get_gpg_bin_path(self): """ Return the path to gpg binary. :returns: the gpg binary path :rtype: str """ gpgbin = None if flags.STANDALONE: gpgbin = os.path.join( get_path_prefix(), "..", "apps", "mail", "gpg") if IS_WIN: gpgbin += ".exe" else: try: gpgbin_options = which("gpg") # gnupg checks that the path to the binary is not a # symlink, so we need to filter those and come up with # just one option. for opt in gpgbin_options: if not os.path.islink(opt): gpgbin = opt break except IndexError as e: logger.debug("Couldn't find the gpg binary!") logger.exception(e) if IS_MAC: gpgbin = os.path.abspath( os.path.join(here(), "apps", "mail", "gpg")) # During the transition towards gpg2, we can look for /usr/bin/gpg1 # binary, in case it was renamed using dpkg-divert or manually. # We could just pick gpg2, but we need to solve #7564 first. if gpgbin is None: try: gpgbin_options = which("gpg1") for opt in gpgbin_options: if not os.path.islink(opt): gpgbin = opt break except IndexError as e: logger.debug("Couldn't find the gpg1 binary!") logger.exception(e) leap_check(gpgbin is not None, "Could not find gpg1 binary") return gpgbin def _init_keymanager(self, address, token): """ Initialize the keymanager. :param address: the address to initialize the keymanager with. :type address: str :param token: the auth token for accessing webapp. :type token: str :rtype: Deferred """ logger.debug('initializing keymanager...') if flags.OFFLINE: nickserver_uri = "https://localhost" kwargs = { "ca_cert_path": "", "api_uri": "", "api_version": "", "uid": self._uuid, "gpgbinary": self._get_gpg_bin_path() } else: nickserver_uri = "https://nicknym.%s:6425" % ( self._provider_config.get_domain(),) kwargs = { "token": token, "ca_cert_path": self._provider_config.get_ca_cert_path(), "api_uri": self._provider_config.get_api_uri(), "api_version": self._provider_config.get_api_version(), "uid": self.srpauth.get_uuid(), "gpgbinary": self._get_gpg_bin_path() } self._keymanager = KeyManager(address, nickserver_uri, self._soledad, **kwargs) if flags.OFFLINE is False: # make sure key is in server logger.debug('Trying to send key to server...') def send_errback(failure): if failure.check(KeyNotFound): logger.debug( 'No key found for %s, it might be because soledad not ' 'synced yet or it will generate it soon.' % address) else: logger.error("Error sending key to server.") logger.exception(failure.value) # but we do not raise d = self._keymanager.send_key(openpgp.OpenPGPKey) d.addErrback(send_errback) return d else: return defer.succeed(None) def _gen_key(self): """ Generates the key pair if needed, uploads it to the webapp and nickserver :rtype: Deferred """ leap_assert(self._provider_config is not None, "We need a provider configuration!") leap_assert(self._soledad is not None, "We need a non-null soledad to generate keys") address = make_address( self._user, self._provider_config.get_domain()) logger.debug("Retrieving key for %s" % (address,)) def if_not_found_generate(failure): failure.trap(KeyNotFound) logger.debug("Key not found. Generating key for %s" % (address,)) d = self._keymanager.gen_key(openpgp.OpenPGPKey) d.addCallbacks(send_key, log_key_error("generating")) return d def send_key(_): d = self._keymanager.send_key(openpgp.OpenPGPKey) d.addCallbacks( lambda _: logger.debug("Key generated successfully."), log_key_error("sending")) def log_key_error(step): def log_err(failure): logger.error("Error while %s key!", (step,)) logger.exception(failure.value) return failure return log_err d = self._keymanager.get_key( address, openpgp.OpenPGPKey, private=True, fetch_remote=False) d.addErrback(if_not_found_generate) return d def run_soledad_setup_checks(self, provider_config, user, password, download_if_needed=False): """ Starts the checks needed for a new soledad setup :param provider_config: Provider configuration :type provider_config: ProviderConfig :param user: User's login :type user: unicode :param password: User's password :type password: unicode :param download_if_needed: If True, it will only download files if the have changed since the time it was previously downloaded. :type download_if_needed: bool :return: Deferred """ leap_assert_type(provider_config, ProviderConfig) # XXX we should provider a method for setting provider_config self._provider_config = provider_config self._download_if_needed = download_if_needed self._user = user self._password = password if flags.OFFLINE: signal_finished = self._signaler.soledad_offline_finished self._signaler.signal(signal_finished) return defer.succeed(True) signal_finished = self._signaler.soledad_bootstrap_finished signal_failed = self._signaler.soledad_bootstrap_failed try: # XXX FIXME make this async too! (use txrequests) # Also, why the f**k would we want to download it *every time*? # We should be fine by using last-time config, or at least # trying it. self._download_config() uuid = self.srpauth.get_uuid() except Exception as e: # TODO: we should handle more specific exceptions in here self._soledad = None self._keymanager = None logger.exception("Error while bootstrapping Soledad: %r" % (e,)) self._signaler.signal(signal_failed) return defer.succeed(None) # soledad config is ok, let's proceed to load and sync soledad d = self.load_and_sync_soledad(uuid) d.addCallback(lambda _: self._gen_key()) d.addCallback(lambda _: self._signaler.signal(signal_finished)) return d
class SoledadBootstrapper(AbstractBootstrapper): """ Soledad init procedure. """ SOLEDAD_KEY = "soledad" KEYMANAGER_KEY = "keymanager" PUBKEY_KEY = "user[public_key]" MAX_INIT_RETRIES = 10 MAX_SYNC_RETRIES = 10 WAIT_MAX_SECONDS = 600 # WAIT_STEP_SECONDS = 1 WAIT_STEP_SECONDS = 5 def __init__(self, signaler=None): AbstractBootstrapper.__init__(self, signaler) if signaler is not None: self._cancel_signal = signaler.soledad_cancelled_bootstrap self._provider_config = None self._soledad_config = None self._keymanager = None self._download_if_needed = False self._user = "" self._password = "" self._address = "" self._uuid = "" self._srpauth = None self._soledad = None @property def keymanager(self): return self._keymanager @property def soledad(self): return self._soledad @property def srpauth(self): if flags.OFFLINE is True: return None leap_assert(self._provider_config is not None, "We need a provider config") return SRPAuth(self._provider_config) # initialization def load_offline_soledad(self, username, password, uuid): """ Instantiate Soledad for offline use. :param username: full user id (user@provider) :type username: str or unicode :param password: the soledad passphrase :type password: unicode :param uuid: the user uuid :type uuid: str or unicode """ self._address = username self._password = password self._uuid = uuid try: self.load_and_sync_soledad(uuid, offline=True) self._signaler.signal(self._signaler.soledad_offline_finished) except Exception as e: # TODO: we should handle more specific exceptions in here logger.exception(e) self._signaler.signal(self._signaler.soledad_offline_failed) def _get_soledad_local_params(self, uuid, offline=False): """ Return the locals parameters needed for the soledad initialization. :param uuid: the uuid of the user, used in offline mode. :type uuid: unicode, or None. :return: secrets_path, local_db_path, token :rtype: tuple """ # in the future, when we want to be able to switch to # online mode, this should be a proxy object too. # Same for server_url below. if offline is False: token = self.srpauth.get_token() else: token = "" secrets_path, local_db_path = get_db_paths(uuid) logger.debug("secrets_path:%s" % (secrets_path,)) logger.debug("local_db:%s" % (local_db_path,)) return (secrets_path, local_db_path, token) def _get_soledad_server_params(self, uuid, offline): """ Return the remote parameters needed for the soledad initialization. :param uuid: the uuid of the user, used in offline mode. :type uuid: unicode, or None. :return: server_url, cert_file :rtype: tuple """ if uuid is None: uuid = self.srpauth.get_uuid() if offline is True: server_url = "http://localhost:9999/" cert_file = "" else: server_url = self._pick_server(uuid) cert_file = self._provider_config.get_ca_cert_path() return server_url, cert_file def _soledad_sync_errback(self, failure): failure.trap(InvalidAuthTokenError) # in the case of an invalid token we have already turned off mail and # warned the user in _do_soledad_sync() def _do_soledad_init(self, uuid, secrets_path, local_db_path, server_url, cert_file, token): """ Initialize soledad, retry if necessary and raise an exception if we can't succeed. :param uuid: user identifier :type uuid: str :param secrets_path: path to secrets file :type secrets_path: str :param local_db_path: path to local db file :type local_db_path: str :param server_url: soledad server uri :type server_url: str :param cert_file: path to the certificate of the ca used to validate the SSL certificate used by the remote soledad server. :type cert_file: str :param auth token: auth token :type auth_token: str """ init_tries = 1 while init_tries <= self.MAX_INIT_RETRIES: try: logger.debug("Trying to init soledad....") self._try_soledad_init(uuid, secrets_path, local_db_path, server_url, cert_file, token) logger.debug("Soledad has been initialized.") return except Exception as exc: init_tries += 1 msg = "Init failed, retrying... (retry {0} of {1})".format(init_tries, self.MAX_INIT_RETRIES) logger.warning(msg) continue logger.exception(exc) raise SoledadInitError() def load_and_sync_soledad(self, uuid=None, offline=False): """ Once everthing is in the right place, we instantiate and sync Soledad :param uuid: the uuid of the user, used in offline mode. :type uuid: unicode, or None. :param offline: whether to instantiate soledad for offline use. :type offline: bool """ local_param = self._get_soledad_local_params(uuid, offline) remote_param = self._get_soledad_server_params(uuid, offline) secrets_path, local_db_path, token = local_param server_url, cert_file = remote_param try: self._do_soledad_init(uuid, secrets_path, local_db_path, server_url, cert_file, token) except SoledadInitError: # re-raise the exceptions from try_init, # we're currently handling the retries from the # soledad-launcher in the gui. raise leap_assert(not sameProxiedObjects(self._soledad, None), "Null soledad, error while initializing") if flags.OFFLINE: self._init_keymanager(self._address, token) else: try: address = make_address(self._user, self._provider_config.get_domain()) self._init_keymanager(address, token) self._keymanager.get_key(address, openpgp.OpenPGPKey, private=True, fetch_remote=False) d = threads.deferToThread(self._do_soledad_sync) d.addErrback(self._soledad_sync_errback) except KeyNotFound: logger.debug("Key not found. Generating key for %s" % (address,)) self._do_soledad_sync() def _pick_server(self, uuid): """ Choose a soledad server to sync against. :param uuid: the uuid for the user. :type uuid: unicode :returns: the server url :rtype: unicode """ # TODO: Select server based on timezone (issue #3308) server_dict = self._soledad_config.get_hosts() if not server_dict.keys(): # XXX raise more specific exception, and catch it properly! raise Exception("No soledad server found") selected_server = server_dict[first(server_dict.keys())] server_url = "https://%s:%s/user-%s" % (selected_server["hostname"], selected_server["port"], uuid) logger.debug("Using soledad server url: %s" % (server_url,)) return server_url def _do_soledad_sync(self): """ Do several retries to get an initial soledad sync. """ # and now, let's sync sync_tries = self.MAX_SYNC_RETRIES step = self.WAIT_STEP_SECONDS max_wait = self.WAIT_MAX_SECONDS while sync_tries > 0: wait = 0 try: logger.debug("Trying to sync soledad....") self._try_soledad_sync() while self.soledad.syncing: time.sleep(step) wait += step if wait >= max_wait: raise SoledadSyncError("timeout!") logger.debug("Soledad has been synced!") # so long, and thanks for all the fish return except SoledadSyncError: # maybe it's my connection, but I'm getting # ssl handshake timeouts and read errors quite often. # A particularly big sync is a disaster. # This deserves further investigation, maybe the # retry strategy can be pushed to u1db, or at least # it's something worthy to talk about with the # ubuntu folks. sync_tries += 1 msg = "Sync failed, retrying... (retry {0} of {1})".format(sync_tries, self.MAX_SYNC_RETRIES) logger.warning(msg) continue except InvalidAuthTokenError: self._signaler.signal(self._signaler.soledad_invalid_auth_token) raise except Exception as e: # XXX release syncing lock logger.exception("Unhandled error while syncing " "soledad: %r" % (e,)) break raise SoledadSyncError() def _try_soledad_init(self, uuid, secrets_path, local_db_path, server_url, cert_file, auth_token): """ Try to initialize soledad. :param uuid: user identifier :type uuid: str :param secrets_path: path to secrets file :type secrets_path: str :param local_db_path: path to local db file :type local_db_path: str :param server_url: soledad server uri :type server_url: str :param cert_file: path to the certificate of the ca used to validate the SSL certificate used by the remote soledad server. :type cert_file: str :param auth token: auth token :type auth_token: str """ # TODO: If selected server fails, retry with another host # (issue #3309) encoding = sys.getfilesystemencoding() # XXX We should get a flag in soledad itself if flags.OFFLINE is True: Soledad._shared_db = MockSharedDB() try: self._soledad = Soledad( uuid, self._password, secrets_path=secrets_path.encode(encoding), local_db_path=local_db_path.encode(encoding), server_url=server_url, cert_file=cert_file.encode(encoding), auth_token=auth_token, defer_encryption=True, ) # XXX All these errors should be handled by soledad itself, # and return a subclass of SoledadInitializationFailed # recoverable, will guarantee retries except (socket.timeout, socket.error, BootstrapSequenceError): logger.warning("Error while initializing Soledad") raise # unrecoverable except (u1db_errors.Unauthorized, u1db_errors.HTTPError): logger.error("Error while initializing Soledad (u1db error).") raise except Exception as exc: logger.exception("Unhandled error while initializating " "Soledad: %r" % (exc,)) raise def _try_soledad_sync(self): """ Try to sync soledad. Raises SoledadSyncError if not successful. """ try: logger.debug("BOOTSTRAPPER: trying to sync Soledad....") # pass defer_decryption=False to get inline decryption # for debugging. self._soledad.sync(defer_decryption=True) except SSLError as exc: logger.error("%r" % (exc,)) raise SoledadSyncError("Failed to sync soledad") except u1db_errors.InvalidGeneration as exc: logger.error("%r" % (exc,)) raise SoledadSyncError("u1db: InvalidGeneration") except (sqlite_ProgrammingError, sqlcipher_ProgrammingError) as e: logger.exception("%r" % (e,)) raise except InvalidAuthTokenError: # token is invalid, probably expired logger.error("Invalid auth token while trying to sync Soledad") raise except Exception as exc: logger.exception("Unhandled error while syncing " "soledad: %r" % (exc,)) raise SoledadSyncError("Failed to sync soledad") def _download_config(self): """ Download the Soledad config for the given provider """ leap_assert(self._provider_config, "We need a provider configuration!") logger.debug("Downloading Soledad config for %s" % (self._provider_config.get_domain(),)) self._soledad_config = SoledadConfig() download_service_config(self._provider_config, self._soledad_config, self._session, self._download_if_needed) def _get_gpg_bin_path(self): """ Return the path to gpg binary. :returns: the gpg binary path :rtype: str """ gpgbin = None if flags.STANDALONE: gpgbin = os.path.join(get_path_prefix(), "..", "apps", "mail", "gpg") if IS_WIN: gpgbin += ".exe" else: try: gpgbin_options = which("gpg") # gnupg checks that the path to the binary is not a # symlink, so we need to filter those and come up with # just one option. for opt in gpgbin_options: if not os.path.islink(opt): gpgbin = opt break except IndexError as e: logger.debug("Couldn't find the gpg binary!") logger.exception(e) leap_check(gpgbin is not None, "Could not find gpg binary") return gpgbin def _init_keymanager(self, address, token): """ Initialize the keymanager. :param address: the address to initialize the keymanager with. :type address: str :param token: the auth token for accessing webapp. :type token: str """ srp_auth = self.srpauth logger.debug("initializing keymanager...") if flags.OFFLINE is True: args = (address, "https://localhost", self._soledad) kwargs = { "ca_cert_path": "", "api_uri": "", "api_version": "", "uid": self._uuid, "gpgbinary": self._get_gpg_bin_path(), } else: args = (address, "https://nicknym.%s:6425" % (self._provider_config.get_domain(),), self._soledad) kwargs = { "token": token, "ca_cert_path": self._provider_config.get_ca_cert_path(), "api_uri": self._provider_config.get_api_uri(), "api_version": self._provider_config.get_api_version(), "uid": srp_auth.get_uuid(), "gpgbinary": self._get_gpg_bin_path(), } try: self._keymanager = KeyManager(*args, **kwargs) except KeyNotFound: logger.debug("key for %s not found." % address) except Exception as exc: logger.exception(exc) raise if flags.OFFLINE is False: # make sure key is in server logger.debug("Trying to send key to server...") try: self._keymanager.send_key(openpgp.OpenPGPKey) except KeyNotFound: logger.debug("No key found for %s, will generate soon." % address) except Exception as exc: logger.error("Error sending key to server.") logger.exception(exc) # but we do not raise def _gen_key(self): """ Generates the key pair if needed, uploads it to the webapp and nickserver """ leap_assert(self._provider_config is not None, "We need a provider configuration!") leap_assert(self._soledad is not None, "We need a non-null soledad to generate keys") address = make_address(self._user, self._provider_config.get_domain()) logger.debug("Retrieving key for %s" % (address,)) try: self._keymanager.get_key(address, openpgp.OpenPGPKey, private=True, fetch_remote=False) return except KeyNotFound: logger.debug("Key not found. Generating key for %s" % (address,)) # generate key try: self._keymanager.gen_key(openpgp.OpenPGPKey) except Exception as exc: logger.error("Error while generating key!") logger.exception(exc) raise # send key try: self._keymanager.send_key(openpgp.OpenPGPKey) except Exception as exc: logger.error("Error while sending key!") logger.exception(exc) raise logger.debug("Key generated successfully.") def run_soledad_setup_checks(self, provider_config, user, password, download_if_needed=False): """ Starts the checks needed for a new soledad setup :param provider_config: Provider configuration :type provider_config: ProviderConfig :param user: User's login :type user: unicode :param password: User's password :type password: unicode :param download_if_needed: If True, it will only download files if the have changed since the time it was previously downloaded. :type download_if_needed: bool """ leap_assert_type(provider_config, ProviderConfig) # XXX we should provider a method for setting provider_config self._provider_config = provider_config self._download_if_needed = download_if_needed self._user = user self._password = password if flags.OFFLINE: signal_finished = self._signaler.soledad_offline_finished signal_failed = self._signaler.soledad_offline_failed else: signal_finished = self._signaler.soledad_bootstrap_finished signal_failed = self._signaler.soledad_bootstrap_failed try: self._download_config() # soledad config is ok, let's proceed to load and sync soledad uuid = self.srpauth.get_uuid() self.load_and_sync_soledad(uuid) if not flags.OFFLINE: self._gen_key() self._signaler.signal(signal_finished) except Exception as e: # TODO: we should handle more specific exceptions in here self._soledad = None self._keymanager = None logger.exception("Error while bootstrapping Soledad: %r" % (e,)) self._signaler.signal(signal_failed)
class SoledadBootstrapper(AbstractBootstrapper): """ Soledad init procedure. """ SOLEDAD_KEY = "soledad" KEYMANAGER_KEY = "keymanager" PUBKEY_KEY = "user[public_key]" MAX_INIT_RETRIES = 10 MAX_SYNC_RETRIES = 10 WAIT_MAX_SECONDS = 600 # WAIT_STEP_SECONDS = 1 WAIT_STEP_SECONDS = 5 def __init__(self, signaler=None): AbstractBootstrapper.__init__(self, signaler) if signaler is not None: self._cancel_signal = signaler.soledad_cancelled_bootstrap self._provider_config = None self._soledad_config = None self._keymanager = None self._download_if_needed = False self._user = "" self._password = "" self._address = "" self._uuid = "" self._srpauth = None self._soledad = None @property def keymanager(self): return self._keymanager @property def soledad(self): return self._soledad @property def srpauth(self): if flags.OFFLINE is True: return None leap_assert(self._provider_config is not None, "We need a provider config") return SRPAuth(self._provider_config) # initialization def load_offline_soledad(self, username, password, uuid): """ Instantiate Soledad for offline use. :param username: full user id (user@provider) :type username: str or unicode :param password: the soledad passphrase :type password: unicode :param uuid: the user uuid :type uuid: str or unicode """ self._address = username self._password = password self._uuid = uuid try: self.load_and_sync_soledad(uuid, offline=True) self._signaler.signal(self._signaler.soledad_offline_finished) except Exception as e: # TODO: we should handle more specific exceptions in here logger.exception(e) self._signaler.signal(self._signaler.soledad_offline_failed) def _get_soledad_local_params(self, uuid, offline=False): """ Return the locals parameters needed for the soledad initialization. :param uuid: the uuid of the user, used in offline mode. :type uuid: unicode, or None. :return: secrets_path, local_db_path, token :rtype: tuple """ # in the future, when we want to be able to switch to # online mode, this should be a proxy object too. # Same for server_url below. if offline is False: token = self.srpauth.get_token() else: token = "" secrets_path, local_db_path = get_db_paths(uuid) logger.debug('secrets_path:%s' % (secrets_path, )) logger.debug('local_db:%s' % (local_db_path, )) return (secrets_path, local_db_path, token) def _get_soledad_server_params(self, uuid, offline): """ Return the remote parameters needed for the soledad initialization. :param uuid: the uuid of the user, used in offline mode. :type uuid: unicode, or None. :return: server_url, cert_file :rtype: tuple """ if uuid is None: uuid = self.srpauth.get_uuid() if offline is True: server_url = "http://localhost:9999/" cert_file = "" else: server_url = self._pick_server(uuid) cert_file = self._provider_config.get_ca_cert_path() return server_url, cert_file def _soledad_sync_errback(self, failure): failure.trap(InvalidAuthTokenError) # in the case of an invalid token we have already turned off mail and # warned the user in _do_soledad_sync() def _do_soledad_init(self, uuid, secrets_path, local_db_path, server_url, cert_file, token): """ Initialize soledad, retry if necessary and raise an exception if we can't succeed. :param uuid: user identifier :type uuid: str :param secrets_path: path to secrets file :type secrets_path: str :param local_db_path: path to local db file :type local_db_path: str :param server_url: soledad server uri :type server_url: str :param cert_file: path to the certificate of the ca used to validate the SSL certificate used by the remote soledad server. :type cert_file: str :param auth token: auth token :type auth_token: str """ init_tries = 1 while init_tries <= self.MAX_INIT_RETRIES: try: logger.debug("Trying to init soledad....") self._try_soledad_init(uuid, secrets_path, local_db_path, server_url, cert_file, token) logger.debug("Soledad has been initialized.") return except Exception as exc: init_tries += 1 msg = "Init failed, retrying... (retry {0} of {1})".format( init_tries, self.MAX_INIT_RETRIES) logger.warning(msg) continue logger.exception(exc) raise SoledadInitError() def load_and_sync_soledad(self, uuid=None, offline=False): """ Once everthing is in the right place, we instantiate and sync Soledad :param uuid: the uuid of the user, used in offline mode. :type uuid: unicode, or None. :param offline: whether to instantiate soledad for offline use. :type offline: bool """ local_param = self._get_soledad_local_params(uuid, offline) remote_param = self._get_soledad_server_params(uuid, offline) secrets_path, local_db_path, token = local_param server_url, cert_file = remote_param try: self._do_soledad_init(uuid, secrets_path, local_db_path, server_url, cert_file, token) except SoledadInitError: # re-raise the exceptions from try_init, # we're currently handling the retries from the # soledad-launcher in the gui. raise leap_assert(not sameProxiedObjects(self._soledad, None), "Null soledad, error while initializing") if flags.OFFLINE: self._init_keymanager(self._address, token) else: try: address = make_address(self._user, self._provider_config.get_domain()) self._init_keymanager(address, token) self._keymanager.get_key(address, openpgp.OpenPGPKey, private=True, fetch_remote=False) d = threads.deferToThread(self._do_soledad_sync) d.addErrback(self._soledad_sync_errback) except KeyNotFound: logger.debug("Key not found. Generating key for %s" % (address, )) self._do_soledad_sync() def _pick_server(self, uuid): """ Choose a soledad server to sync against. :param uuid: the uuid for the user. :type uuid: unicode :returns: the server url :rtype: unicode """ # TODO: Select server based on timezone (issue #3308) server_dict = self._soledad_config.get_hosts() if not server_dict.keys(): # XXX raise more specific exception, and catch it properly! raise Exception("No soledad server found") selected_server = server_dict[first(server_dict.keys())] server_url = "https://%s:%s/user-%s" % (selected_server["hostname"], selected_server["port"], uuid) logger.debug("Using soledad server url: %s" % (server_url, )) return server_url def _do_soledad_sync(self): """ Do several retries to get an initial soledad sync. """ # and now, let's sync sync_tries = self.MAX_SYNC_RETRIES step = self.WAIT_STEP_SECONDS max_wait = self.WAIT_MAX_SECONDS while sync_tries > 0: wait = 0 try: logger.debug("Trying to sync soledad....") self._try_soledad_sync() while self.soledad.syncing: time.sleep(step) wait += step if wait >= max_wait: raise SoledadSyncError("timeout!") logger.debug("Soledad has been synced!") # so long, and thanks for all the fish return except SoledadSyncError: # maybe it's my connection, but I'm getting # ssl handshake timeouts and read errors quite often. # A particularly big sync is a disaster. # This deserves further investigation, maybe the # retry strategy can be pushed to u1db, or at least # it's something worthy to talk about with the # ubuntu folks. sync_tries += 1 msg = "Sync failed, retrying... (retry {0} of {1})".format( sync_tries, self.MAX_SYNC_RETRIES) logger.warning(msg) continue except InvalidAuthTokenError: self._signaler.signal( self._signaler.soledad_invalid_auth_token) raise except Exception as e: # XXX release syncing lock logger.exception("Unhandled error while syncing " "soledad: %r" % (e, )) break raise SoledadSyncError() def _try_soledad_init(self, uuid, secrets_path, local_db_path, server_url, cert_file, auth_token): """ Try to initialize soledad. :param uuid: user identifier :type uuid: str :param secrets_path: path to secrets file :type secrets_path: str :param local_db_path: path to local db file :type local_db_path: str :param server_url: soledad server uri :type server_url: str :param cert_file: path to the certificate of the ca used to validate the SSL certificate used by the remote soledad server. :type cert_file: str :param auth token: auth token :type auth_token: str """ # TODO: If selected server fails, retry with another host # (issue #3309) encoding = sys.getfilesystemencoding() # XXX We should get a flag in soledad itself if flags.OFFLINE is True: Soledad._shared_db = MockSharedDB() try: self._soledad = Soledad( uuid, self._password, secrets_path=secrets_path.encode(encoding), local_db_path=local_db_path.encode(encoding), server_url=server_url, cert_file=cert_file.encode(encoding), auth_token=auth_token, defer_encryption=True) # XXX All these errors should be handled by soledad itself, # and return a subclass of SoledadInitializationFailed # recoverable, will guarantee retries except (socket.timeout, socket.error, BootstrapSequenceError): logger.warning("Error while initializing Soledad") raise # unrecoverable except (u1db_errors.Unauthorized, u1db_errors.HTTPError): logger.error("Error while initializing Soledad (u1db error).") raise except Exception as exc: logger.exception("Unhandled error while initializating " "Soledad: %r" % (exc, )) raise def _try_soledad_sync(self): """ Try to sync soledad. Raises SoledadSyncError if not successful. """ try: logger.debug("BOOTSTRAPPER: trying to sync Soledad....") # pass defer_decryption=False to get inline decryption # for debugging. self._soledad.sync(defer_decryption=True) except SSLError as exc: logger.error("%r" % (exc, )) raise SoledadSyncError("Failed to sync soledad") except u1db_errors.InvalidGeneration as exc: logger.error("%r" % (exc, )) raise SoledadSyncError("u1db: InvalidGeneration") except (sqlite_ProgrammingError, sqlcipher_ProgrammingError) as e: logger.exception("%r" % (e, )) raise except InvalidAuthTokenError: # token is invalid, probably expired logger.error('Invalid auth token while trying to sync Soledad') raise except Exception as exc: logger.exception("Unhandled error while syncing " "soledad: %r" % (exc, )) raise SoledadSyncError("Failed to sync soledad") def _download_config(self): """ Download the Soledad config for the given provider """ leap_assert(self._provider_config, "We need a provider configuration!") logger.debug("Downloading Soledad config for %s" % (self._provider_config.get_domain(), )) self._soledad_config = SoledadConfig() download_service_config(self._provider_config, self._soledad_config, self._session, self._download_if_needed) def _get_gpg_bin_path(self): """ Return the path to gpg binary. :returns: the gpg binary path :rtype: str """ gpgbin = None if flags.STANDALONE: gpgbin = os.path.join(get_path_prefix(), "..", "apps", "mail", "gpg") if IS_WIN: gpgbin += ".exe" else: try: gpgbin_options = which("gpg") # gnupg checks that the path to the binary is not a # symlink, so we need to filter those and come up with # just one option. for opt in gpgbin_options: if not os.path.islink(opt): gpgbin = opt break except IndexError as e: logger.debug("Couldn't find the gpg binary!") logger.exception(e) leap_check(gpgbin is not None, "Could not find gpg binary") return gpgbin def _init_keymanager(self, address, token): """ Initialize the keymanager. :param address: the address to initialize the keymanager with. :type address: str :param token: the auth token for accessing webapp. :type token: str """ srp_auth = self.srpauth logger.debug('initializing keymanager...') if flags.OFFLINE is True: args = (address, "https://localhost", self._soledad) kwargs = { "ca_cert_path": "", "api_uri": "", "api_version": "", "uid": self._uuid, "gpgbinary": self._get_gpg_bin_path() } else: args = (address, "https://nicknym.%s:6425" % (self._provider_config.get_domain(), ), self._soledad) kwargs = { "token": token, "ca_cert_path": self._provider_config.get_ca_cert_path(), "api_uri": self._provider_config.get_api_uri(), "api_version": self._provider_config.get_api_version(), "uid": srp_auth.get_uuid(), "gpgbinary": self._get_gpg_bin_path() } try: self._keymanager = KeyManager(*args, **kwargs) except KeyNotFound: logger.debug('key for %s not found.' % address) except Exception as exc: logger.exception(exc) raise if flags.OFFLINE is False: # make sure key is in server logger.debug('Trying to send key to server...') try: self._keymanager.send_key(openpgp.OpenPGPKey) except KeyNotFound: logger.debug('No key found for %s, will generate soon.' % address) except Exception as exc: logger.error("Error sending key to server.") logger.exception(exc) # but we do not raise def _gen_key(self): """ Generates the key pair if needed, uploads it to the webapp and nickserver """ leap_assert(self._provider_config is not None, "We need a provider configuration!") leap_assert(self._soledad is not None, "We need a non-null soledad to generate keys") address = make_address(self._user, self._provider_config.get_domain()) logger.debug("Retrieving key for %s" % (address, )) try: self._keymanager.get_key(address, openpgp.OpenPGPKey, private=True, fetch_remote=False) return except KeyNotFound: logger.debug("Key not found. Generating key for %s" % (address, )) # generate key try: self._keymanager.gen_key(openpgp.OpenPGPKey) except Exception as exc: logger.error("Error while generating key!") logger.exception(exc) raise # send key try: self._keymanager.send_key(openpgp.OpenPGPKey) except Exception as exc: logger.error("Error while sending key!") logger.exception(exc) raise logger.debug("Key generated successfully.") def run_soledad_setup_checks(self, provider_config, user, password, download_if_needed=False): """ Starts the checks needed for a new soledad setup :param provider_config: Provider configuration :type provider_config: ProviderConfig :param user: User's login :type user: unicode :param password: User's password :type password: unicode :param download_if_needed: If True, it will only download files if the have changed since the time it was previously downloaded. :type download_if_needed: bool """ leap_assert_type(provider_config, ProviderConfig) # XXX we should provider a method for setting provider_config self._provider_config = provider_config self._download_if_needed = download_if_needed self._user = user self._password = password if flags.OFFLINE: signal_finished = self._signaler.soledad_offline_finished signal_failed = self._signaler.soledad_offline_failed else: signal_finished = self._signaler.soledad_bootstrap_finished signal_failed = self._signaler.soledad_bootstrap_failed try: self._download_config() # soledad config is ok, let's proceed to load and sync soledad uuid = self.srpauth.get_uuid() self.load_and_sync_soledad(uuid) if not flags.OFFLINE: self._gen_key() self._signaler.signal(signal_finished) except Exception as e: # TODO: we should handle more specific exceptions in here self._soledad = None self._keymanager = None logger.exception("Error while bootstrapping Soledad: %r" % (e, )) self._signaler.signal(signal_failed)
class SoledadBootstrapper(AbstractBootstrapper): """ Soledad init procedure """ SOLEDAD_KEY = "soledad" KEYMANAGER_KEY = "keymanager" PUBKEY_KEY = "user[public_key]" MAX_INIT_RETRIES = 10 MAX_SYNC_RETRIES = 10 # All dicts returned are of the form # {"passed": bool, "error": str} download_config = QtCore.Signal(dict) gen_key = QtCore.Signal(dict) soledad_timeout = QtCore.Signal() soledad_failed = QtCore.Signal() def __init__(self): AbstractBootstrapper.__init__(self) self._provider_config = None self._soledad_config = None self._keymanager = None self._download_if_needed = False self._user = "" self._password = "" self._srpauth = None self._soledad = None self._soledad_retries = 0 @property def keymanager(self): return self._keymanager @property def soledad(self): return self._soledad @property def srpauth(self): leap_assert(self._provider_config is not None, "We need a provider config") return SRPAuth(self._provider_config) # retries def cancel_bootstrap(self): self._soledad_retries = self.MAX_INIT_RETRIES def should_retry_initialization(self): """ Returns True if we should retry the initialization. """ logger.debug("current retries: %s, max retries: %s" % ( self._soledad_retries, self.MAX_INIT_RETRIES)) return self._soledad_retries < self.MAX_INIT_RETRIES def increment_retries_count(self): """ Increments the count of initialization retries. """ self._soledad_retries += 1 def _get_db_paths(self, uuid): """ Returns the secrets and local db paths needed for soledad initialization :param uuid: uuid for user :type uuid: str :return: a tuple with secrets, local_db paths :rtype: tuple """ prefix = os.path.join(get_path_prefix(), "leap", "soledad") secrets = "%s/%s.secret" % (prefix, uuid) local_db = "%s/%s.db" % (prefix, uuid) # We remove an empty file if found to avoid complains # about the db not being properly initialized if is_file(local_db) and is_empty_file(local_db): try: os.remove(local_db) except OSError: logger.warning("Could not remove empty file %s" % local_db) return secrets, local_db # initialization def load_and_sync_soledad(self): """ Once everthing is in the right place, we instantiate and sync Soledad """ # TODO this method is still too large uuid = self.srpauth.get_uid() token = self.srpauth.get_token() secrets_path, local_db_path = self._get_db_paths(uuid) # TODO: Select server based on timezone (issue #3308) server_dict = self._soledad_config.get_hosts() if not server_dict.keys(): # XXX raise more specific exception, and catch it properly! raise Exception("No soledad server found") selected_server = server_dict[server_dict.keys()[0]] server_url = "https://%s:%s/user-%s" % ( selected_server["hostname"], selected_server["port"], uuid) logger.debug("Using soledad server url: %s" % (server_url,)) cert_file = self._provider_config.get_ca_cert_path() logger.debug('local_db:%s' % (local_db_path,)) logger.debug('secrets_path:%s' % (secrets_path,)) try: self._try_soledad_init( uuid, secrets_path, local_db_path, server_url, cert_file, token) except: # re-raise the exceptions from try_init, # we're currently handling the retries from the # soledad-launcher in the gui. raise leap_check(self._soledad is not None, "Null soledad, error while initializing") # and now, let's sync sync_tries = self.MAX_SYNC_RETRIES while sync_tries > 0: try: self._try_soledad_sync() # at this point, sometimes the client # gets stuck and does not progress to # the _gen_key step. XXX investigate. logger.debug("Soledad has been synced.") # so long, and thanks for all the fish return except SoledadSyncError: # maybe it's my connection, but I'm getting # ssl handshake timeouts and read errors quite often. # A particularly big sync is a disaster. # This deserves further investigation, maybe the # retry strategy can be pushed to u1db, or at least # it's something worthy to talk about with the # ubuntu folks. sync_tries -= 1 continue # reached bottom, failed to sync # and there's nothing we can do... self.soledad_failed.emit() raise SoledadSyncError() def _try_soledad_init(self, uuid, secrets_path, local_db_path, server_url, cert_file, auth_token): """ Tries to initialize soledad. :param uuid: user identifier :param secrets_path: path to secrets file :param local_db_path: path to local db file :param server_url: soledad server uri :param cert_file: path to the certificate of the ca used to validate the SSL certificate used by the remote soledad server. :type cert_file: str :param auth token: auth token :type auth_token: str """ # TODO: If selected server fails, retry with another host # (issue #3309) try: self._soledad = Soledad( uuid, self._password.encode("utf-8"), secrets_path=secrets_path, local_db_path=local_db_path, server_url=server_url, cert_file=cert_file, auth_token=auth_token) # XXX All these errors should be handled by soledad itself, # and return a subclass of SoledadInitializationFailed # recoverable, will guarantee retries except socket.timeout: logger.debug("SOLEDAD initialization TIMED OUT...") self.soledad_timeout.emit() except socket.error as exc: logger.error("Socket error while initializing soledad") self.soledad_timeout.emit() # unrecoverable except u1db_errors.Unauthorized: logger.error("Error while initializing soledad " "(unauthorized).") self.soledad_failed.emit() except Exception as exc: logger.exception("Unhandled error while initializating " "soledad: %r" % (exc,)) self.soledad_failed.emit() def _try_soledad_sync(self): """ Tries to sync soledad. Raises SoledadSyncError if not successful. """ try: logger.error("trying to sync soledad....") self._soledad.sync() except SSLError as exc: logger.error("%r" % (exc,)) raise SoledadSyncError("Failed to sync soledad") except Exception as exc: logger.exception("Unhandled error while syncing" "soledad: %r" % (exc,)) self.soledad_failed.emit() raise SoledadSyncError("Failed to sync soledad") def _download_config(self): """ Downloads the Soledad config for the given provider """ leap_assert(self._provider_config, "We need a provider configuration!") logger.debug("Downloading Soledad config for %s" % (self._provider_config.get_domain(),)) self._soledad_config = SoledadConfig() download_service_config( self._provider_config, self._soledad_config, self._session, self._download_if_needed) # soledad config is ok, let's proceed to load and sync soledad # XXX but honestly, this is a pretty strange entry point for that. # it feels like it should be the other way around: # load_and_sync, and from there, if needed, call download_config self.load_and_sync_soledad() def _get_gpg_bin_path(self): """ Returns the path to gpg binary. :returns: the gpg binary path :rtype: str """ # TODO: Fix for Windows gpgbin = None if flags.STANDALONE: gpgbin = os.path.join( get_path_prefix(), "..", "apps", "mail", "gpg") else: try: gpgbin_options = which("gpg") # gnupg checks that the path to the binary is not a # symlink, so we need to filter those and come up with # just one option. for opt in gpgbin_options: if not os.path.islink(opt): gpgbin = opt break except IndexError as e: logger.debug("Couldn't find the gpg binary!") logger.exception(e) leap_check(gpgbin is not None, "Could not find gpg binary") return gpgbin def _init_keymanager(self, address): """ Initializes the keymanager. :param address: the address to initialize the keymanager with. :type address: str """ srp_auth = self.srpauth logger.debug('initializing keymanager...') self._keymanager = KeyManager( address, "https://nicknym.%s:6425" % (self._provider_config.get_domain(),), self._soledad, #token=srp_auth.get_token(), # TODO: enable token usage session_id=srp_auth.get_session_id(), ca_cert_path=self._provider_config.get_ca_cert_path(), api_uri=self._provider_config.get_api_uri(), api_version=self._provider_config.get_api_version(), uid=srp_auth.get_uid(), gpgbinary=self._get_gpg_bin_path()) def _gen_key(self, _): """ Generates the key pair if needed, uploads it to the webapp and nickserver """ leap_assert(self._provider_config is not None, "We need a provider configuration!") leap_assert(self._soledad is not None, "We need a non-null soledad to generate keys") address = "%s@%s" % (self._user, self._provider_config.get_domain()) self._init_keymanager(address) logger.debug("Retrieving key for %s" % (address,)) try: self._keymanager.get_key( address, openpgp.OpenPGPKey, private=True, fetch_remote=False) return except KeyNotFound: logger.debug("Key not found. Generating key for %s" % (address,)) # generate key try: self._keymanager.gen_key(openpgp.OpenPGPKey) except Exception as exc: logger.error("error while generating key!") logger.exception(exc) raise # send key try: self._keymanager.send_key(openpgp.OpenPGPKey) except Exception as exc: logger.error("error while sending key!") logger.exception(exc) raise logger.debug("Key generated successfully.") def run_soledad_setup_checks(self, provider_config, user, password, download_if_needed=False): """ Starts the checks needed for a new soledad setup :param provider_config: Provider configuration :type provider_config: ProviderConfig :param user: User's login :type user: str :param password: User's password :type password: str :param download_if_needed: If True, it will only download files if the have changed since the time it was previously downloaded. :type download_if_needed: bool """ leap_assert_type(provider_config, ProviderConfig) # XXX we should provider a method for setting provider_config self._provider_config = provider_config self._download_if_needed = download_if_needed self._user = user self._password = password cb_chain = [ (self._download_config, self.download_config), (self._gen_key, self.gen_key) ] self.addCallbackChain(cb_chain)
class SoledadBootstrapper(AbstractBootstrapper): """ Soledad init procedure. """ SOLEDAD_KEY = "soledad" KEYMANAGER_KEY = "keymanager" PUBKEY_KEY = "user[public_key]" MAX_INIT_RETRIES = 10 def __init__(self, signaler=None): AbstractBootstrapper.__init__(self, signaler) if signaler is not None: self._cancel_signal = signaler.soledad_cancelled_bootstrap self._provider_config = None self._soledad_config = None self._download_if_needed = False self._user = "" self._password = u"" self._address = "" self._uuid = "" self._srpauth = None self._soledad = None self._keymanager = None @property def srpauth(self): if flags.OFFLINE is True: return None if self._srpauth is None: leap_assert(self._provider_config is not None, "We need a provider config") self._srpauth = SRPAuth(self._provider_config) return self._srpauth @property def soledad(self): return self._soledad @property def keymanager(self): return self._keymanager # initialization def load_offline_soledad(self, username, password, uuid): """ Instantiate Soledad for offline use. :param username: full user id (user@provider) :type username: str or unicode :param password: the soledad passphrase :type password: unicode :param uuid: the user uuid :type uuid: str or unicode """ self._address = username self._password = password self._uuid = uuid def error(failure): # TODO: we should handle more specific exceptions in here logger.exception(failure.value) self._signaler.signal(self._signaler.soledad_offline_failed) d = self.load_and_sync_soledad(uuid, offline=True) d.addCallback(lambda _: self._signaler.signal( self._signaler.soledad_offline_finished)) d.addErrback(error) return d def _get_soledad_local_params(self, uuid, offline=False): """ Return the locals parameters needed for the soledad initialization. :param uuid: the uuid of the user, used in offline mode. :type uuid: unicode, or None. :return: secrets_path, local_db_path, token :rtype: tuple """ # in the future, when we want to be able to switch to # online mode, this should be a proxy object too. # Same for server_url below. if offline is False: token = self.srpauth.get_token() else: token = "" secrets_path, local_db_path = get_db_paths(uuid) logger.debug('secrets_path:%s' % (secrets_path, )) logger.debug('local_db:%s' % (local_db_path, )) return (secrets_path, local_db_path, token) def _get_soledad_server_params(self, uuid, offline): """ Return the remote parameters needed for the soledad initialization. :param uuid: the uuid of the user, used in offline mode. :type uuid: unicode, or None. :return: server_url, cert_file :rtype: tuple """ if offline is True: server_url = "http://localhost:9999/" cert_file = "" else: if uuid is None: uuid = self.srpauth.get_uuid() server_url = self._pick_server(uuid) cert_file = self._provider_config.get_ca_cert_path() return server_url, cert_file def _do_soledad_init(self, uuid, secrets_path, local_db_path, server_url, cert_file, token, syncable): """ Initialize soledad, retry if necessary and raise an exception if we can't succeed. :param uuid: user identifier :type uuid: str :param secrets_path: path to secrets file :type secrets_path: str :param local_db_path: path to local db file :type local_db_path: str :param server_url: soledad server uri :type server_url: str :param cert_file: path to the certificate of the ca used to validate the SSL certificate used by the remote soledad server. :type cert_file: str :param auth token: auth token :type auth_token: str """ init_tries = 1 while init_tries <= self.MAX_INIT_RETRIES: try: logger.debug("Trying to init soledad....") self._try_soledad_init(uuid, secrets_path, local_db_path, server_url, cert_file, token, syncable) logger.debug("Soledad has been initialized.") return except Exception as exc: init_tries += 1 msg = "Init failed, retrying... (retry {0} of {1})".format( init_tries, self.MAX_INIT_RETRIES) logger.warning(msg) continue self._signaler.signal(self._signaler.soledad_bootstrap_failed) logger.exception(exc) raise SoledadInitError() def load_and_sync_soledad(self, uuid=u"", offline=False): """ Once everthing is in the right place, we instantiate and sync Soledad :param uuid: the uuid of the user, used in offline mode. :type uuid: unicode. :param offline: whether to instantiate soledad for offline use. :type offline: bool :return: A Deferred which fires when soledad is sync, or which fails with SoledadInitError or SoledadSyncError :rtype: defer.Deferred """ local_param = self._get_soledad_local_params(uuid, offline) remote_param = self._get_soledad_server_params(uuid, offline) secrets_path, local_db_path, token = local_param server_url, cert_file = remote_param if offline: return self._load_soledad_nosync(uuid, secrets_path, local_db_path, cert_file, token) else: return self._load_soledad_online(uuid, secrets_path, local_db_path, server_url, cert_file, token) def _load_soledad_online(self, uuid, secrets_path, local_db_path, server_url, cert_file, token): syncable = True try: self._do_soledad_init(uuid, secrets_path, local_db_path, server_url, cert_file, token, syncable) except SoledadInitError as e: # re-raise the exceptions from try_init, # we're currently handling the retries from the # soledad-launcher in the gui. return defer.fail(e) leap_assert(not sameProxiedObjects(self._soledad, None), "Null soledad, error while initializing") address = make_address(self._user, self._provider_config.get_domain()) syncer = Syncer(self._soledad, self._signaler) d = self._init_keymanager(address, token) d.addCallback(lambda _: syncer.sync()) d.addErrback(self._soledad_sync_errback) return d def _load_soledad_nosync(self, uuid, secrets_path, local_db_path, cert_file, token): syncable = False self._do_soledad_init(uuid, secrets_path, local_db_path, "", cert_file, token, syncable) d = self._init_keymanager(self._address, token) return d def _soledad_sync_errback(self, failure): failure.trap(InvalidAuthTokenError) # in the case of an invalid token we have already turned off mail and # warned the user def _pick_server(self, uuid): """ Choose a soledad server to sync against. :param uuid: the uuid for the user. :type uuid: unicode :returns: the server url :rtype: unicode """ if not self._soledad_config: self._soledad_config = SoledadConfig() # TODO: Select server based on timezone (issue #3308) server_dict = self._soledad_config.get_hosts() if not server_dict.keys(): # XXX raise more specific exception, and catch it properly! raise Exception("No soledad server found") selected_server = server_dict[first(server_dict.keys())] server_url = "https://%s:%s/user-%s" % (selected_server["hostname"], selected_server["port"], uuid) logger.debug("Using soledad server url: %s" % (server_url, )) return server_url def _try_soledad_init(self, uuid, secrets_path, local_db_path, server_url, cert_file, auth_token, syncable): """ Try to initialize soledad. :param uuid: user identifier :type uuid: str :param secrets_path: path to secrets file :type secrets_path: str :param local_db_path: path to local db file :type local_db_path: str :param server_url: soledad server uri :type server_url: str :param cert_file: path to the certificate of the ca used to validate the SSL certificate used by the remote soledad server. :type cert_file: str :param auth token: auth token :type auth_token: str """ # TODO: If selected server fails, retry with another host # (issue #3309) encoding = sys.getfilesystemencoding() try: self._soledad = Soledad( uuid, self._password, secrets_path=secrets_path.encode(encoding), local_db_path=local_db_path.encode(encoding), server_url=server_url, cert_file=cert_file.encode(encoding), auth_token=auth_token, defer_encryption=True, syncable=syncable) # XXX All these errors should be handled by soledad itself, # and return a subclass of SoledadInitializationFailed # recoverable, will guarantee retries except (socket.timeout, socket.error, BootstrapSequenceError): logger.warning("Error while initializing Soledad") raise # unrecoverable except (u1db_errors.Unauthorized, u1db_errors.HTTPError): logger.error("Error while initializing Soledad (u1db error).") raise except Exception as exc: logger.exception("Unhandled error while initializating " "Soledad: %r" % (exc, )) raise def _download_config(self): """ Download the Soledad config for the given provider """ leap_assert(self._provider_config, "We need a provider configuration!") logger.debug("Downloading Soledad config for %s" % (self._provider_config.get_domain(), )) self._soledad_config = SoledadConfig() download_service_config(self._provider_config, self._soledad_config, self._session, self._download_if_needed) def _get_gpg_bin_path(self): """ Return the path to gpg binary. :returns: the gpg binary path :rtype: str """ gpgbin = None if flags.STANDALONE: gpgbin = os.path.join(get_path_prefix(), "..", "apps", "mail", "gpg") if IS_WIN: gpgbin += ".exe" else: try: gpgbin_options = which("gpg") # gnupg checks that the path to the binary is not a # symlink, so we need to filter those and come up with # just one option. for opt in gpgbin_options: if not os.path.islink(opt): gpgbin = opt break except IndexError as e: logger.debug("Couldn't find the gpg binary!") logger.exception(e) if IS_MAC: gpgbin = os.path.abspath( os.path.join(here(), "apps", "mail", "gpg")) # During the transition towards gpg2, we can look for /usr/bin/gpg1 # binary, in case it was renamed using dpkg-divert or manually. # We could just pick gpg2, but we need to solve #7564 first. if gpgbin is None: try: gpgbin_options = which("gpg1") for opt in gpgbin_options: if not os.path.islink(opt): gpgbin = opt break except IndexError as e: logger.debug("Couldn't find the gpg1 binary!") logger.exception(e) leap_check(gpgbin is not None, "Could not find gpg1 binary") return gpgbin def _init_keymanager(self, address, token): """ Initialize the keymanager. :param address: the address to initialize the keymanager with. :type address: str :param token: the auth token for accessing webapp. :type token: str :rtype: Deferred """ logger.debug('initializing keymanager...') if flags.OFFLINE: nickserver_uri = "https://localhost" kwargs = { "ca_cert_path": "", "api_uri": "", "api_version": "", "uid": self._uuid, "gpgbinary": self._get_gpg_bin_path() } else: nickserver_uri = "https://nicknym.%s:6425" % ( self._provider_config.get_domain(), ) kwargs = { "token": token, "ca_cert_path": self._provider_config.get_ca_cert_path(), "api_uri": self._provider_config.get_api_uri(), "api_version": self._provider_config.get_api_version(), "uid": self.srpauth.get_uuid(), "gpgbinary": self._get_gpg_bin_path() } self._keymanager = KeyManager(address, nickserver_uri, self._soledad, **kwargs) if flags.OFFLINE is False: # make sure key is in server logger.debug('Trying to send key to server...') def send_errback(failure): if failure.check(KeyNotFound): logger.debug( 'No key found for %s, it might be because soledad not ' 'synced yet or it will generate it soon.' % address) else: logger.error("Error sending key to server.") logger.exception(failure.value) # but we do not raise d = self._keymanager.send_key(openpgp.OpenPGPKey) d.addErrback(send_errback) return d else: return defer.succeed(None) def _gen_key(self): """ Generates the key pair if needed, uploads it to the webapp and nickserver :rtype: Deferred """ leap_assert(self._provider_config is not None, "We need a provider configuration!") leap_assert(self._soledad is not None, "We need a non-null soledad to generate keys") address = make_address(self._user, self._provider_config.get_domain()) logger.debug("Retrieving key for %s" % (address, )) def if_not_found_generate(failure): failure.trap(KeyNotFound) logger.debug("Key not found. Generating key for %s" % (address, )) d = self._keymanager.gen_key(openpgp.OpenPGPKey) d.addCallbacks(send_key, log_key_error("generating")) return d def send_key(_): d = self._keymanager.send_key(openpgp.OpenPGPKey) d.addCallbacks( lambda _: logger.debug("Key generated successfully."), log_key_error("sending")) def log_key_error(step): def log_err(failure): logger.error("Error while %s key!", (step, )) logger.exception(failure.value) return failure return log_err d = self._keymanager.get_key(address, openpgp.OpenPGPKey, private=True, fetch_remote=False) d.addErrback(if_not_found_generate) return d def run_soledad_setup_checks(self, provider_config, user, password, download_if_needed=False): """ Starts the checks needed for a new soledad setup :param provider_config: Provider configuration :type provider_config: ProviderConfig :param user: User's login :type user: unicode :param password: User's password :type password: unicode :param download_if_needed: If True, it will only download files if the have changed since the time it was previously downloaded. :type download_if_needed: bool :return: Deferred """ leap_assert_type(provider_config, ProviderConfig) # XXX we should provider a method for setting provider_config self._provider_config = provider_config self._download_if_needed = download_if_needed self._user = user self._password = password if flags.OFFLINE: signal_finished = self._signaler.soledad_offline_finished self._signaler.signal(signal_finished) return defer.succeed(True) signal_finished = self._signaler.soledad_bootstrap_finished signal_failed = self._signaler.soledad_bootstrap_failed try: # XXX FIXME make this async too! (use txrequests) # Also, why the f**k would we want to download it *every time*? # We should be fine by using last-time config, or at least # trying it. self._download_config() uuid = self.srpauth.get_uuid() except Exception as e: # TODO: we should handle more specific exceptions in here self._soledad = None self._keymanager = None logger.exception("Error while bootstrapping Soledad: %r" % (e, )) self._signaler.signal(signal_failed) return defer.succeed(None) # soledad config is ok, let's proceed to load and sync soledad d = self.load_and_sync_soledad(uuid) d.addCallback(lambda _: self._gen_key()) d.addCallback(lambda _: self._signaler.signal(signal_finished)) return d
class SoledadBootstrapper(AbstractBootstrapper): """ Soledad init procedure """ SOLEDAD_KEY = "soledad" KEYMANAGER_KEY = "keymanager" PUBKEY_KEY = "user[public_key]" MAX_INIT_RETRIES = 10 # All dicts returned are of the form # {"passed": bool, "error": str} download_config = QtCore.Signal(dict) gen_key = QtCore.Signal(dict) soledad_timeout = QtCore.Signal() soledad_failed = QtCore.Signal() def __init__(self): AbstractBootstrapper.__init__(self) self._provider_config = None self._soledad_config = None self._keymanager = None self._download_if_needed = False self._user = "" self._password = "" self._srpauth = None self._soledad = None self._soledad_retries = 0 @property def keymanager(self): return self._keymanager @property def soledad(self): return self._soledad @property def srpauth(self): leap_assert(self._provider_config is not None, "We need a provider config") return SRPAuth(self._provider_config) # retries def cancel_bootstrap(self): self._soledad_retries = self.MAX_INIT_RETRIES def should_retry_initialization(self): """ Returns True if we should retry the initialization. """ logger.debug("current retries: %s, max retries: %s" % ( self._soledad_retries, self.MAX_INIT_RETRIES)) return self._soledad_retries < self.MAX_INIT_RETRIES def increment_retries_count(self): """ Increments the count of initialization retries. """ self._soledad_retries += 1 # initialization def load_and_sync_soledad(self): """ Once everthing is in the right place, we instantiate and sync Soledad :param srp_auth: SRPAuth object used :type srp_auth: SRPAuth """ srp_auth = self.srpauth uuid = srp_auth.get_uid() prefix = os.path.join(self._soledad_config.get_path_prefix(), "leap", "soledad") secrets_path = "%s/%s.secret" % (prefix, uuid) local_db_path = "%s/%s.db" % (prefix, uuid) # TODO: Select server based on timezone (issue #3308) server_dict = self._soledad_config.get_hosts() if server_dict.keys(): selected_server = server_dict[server_dict.keys()[0]] server_url = "https://%s:%s/user-%s" % ( selected_server["hostname"], selected_server["port"], uuid) logger.debug("Using soledad server url: %s" % (server_url,)) cert_file = self._provider_config.get_ca_cert_path() # TODO: If selected server fails, retry with another host # (issue #3309) try: self._soledad = Soledad( uuid, self._password.encode("utf-8"), secrets_path=secrets_path, local_db_path=local_db_path, server_url=server_url, cert_file=cert_file, auth_token=srp_auth.get_token()) self._soledad.sync() # XXX All these errors should be handled by soledad itself, # and return a subclass of SoledadInitializationFailed except socket.timeout: logger.debug("SOLEDAD TIMED OUT...") self.soledad_timeout.emit() except socket.error as exc: logger.error("Socket error while initializing soledad") self.soledad_failed.emit() except u1db_errors.Unauthorized: logger.error("Error while initializing soledad " "(unauthorized).") self.soledad_failed.emit() except Exception as exc: logger.error("Unhandled error while initializating " "soledad: %r" % (exc,)) raise else: raise Exception("No soledad server found") def _download_config(self): """ Downloads the Soledad config for the given provider """ leap_assert(self._provider_config, "We need a provider configuration!") logger.debug("Downloading Soledad config for %s" % (self._provider_config.get_domain(),)) self._soledad_config = SoledadConfig() headers = {} mtime = get_mtime( os.path.join( self._soledad_config.get_path_prefix(), "leap", "providers", self._provider_config.get_domain(), "soledad-service.json")) if self._download_if_needed and mtime: headers['if-modified-since'] = mtime api_version = self._provider_config.get_api_version() # there is some confusion with this uri, config_uri = "%s/%s/config/soledad-service.json" % ( self._provider_config.get_api_uri(), api_version) logger.debug('Downloading soledad config from: %s' % config_uri) # TODO factor out this srpauth protected get (make decorator) srp_auth = self.srpauth session_id = srp_auth.get_session_id() cookies = None if session_id: cookies = {"_session_id": session_id} res = self._session.get(config_uri, verify=self._provider_config .get_ca_cert_path(), headers=headers, cookies=cookies) res.raise_for_status() self._soledad_config.set_api_version(api_version) # Not modified if res.status_code == 304: logger.debug("Soledad definition has not been modified") self._soledad_config.load( os.path.join( "leap", "providers", self._provider_config.get_domain(), "soledad-service.json")) else: soledad_definition, mtime = get_content(res) self._soledad_config.load(data=soledad_definition, mtime=mtime) self._soledad_config.save(["leap", "providers", self._provider_config.get_domain(), "soledad-service.json"]) self.load_and_sync_soledad() def _gen_key(self, _): """ Generates the key pair if needed, uploads it to the webapp and nickserver """ leap_assert(self._provider_config, "We need a provider configuration!") address = "%s@%s" % (self._user, self._provider_config.get_domain()) logger.debug("Retrieving key for %s" % (address,)) srp_auth = self.srpauth # TODO: use which implementation with known paths # TODO: Fix for Windows gpgbin = "/usr/bin/gpg" if self._standalone: gpgbin = os.path.join(self._provider_config.get_path_prefix(), "..", "apps", "mail", "gpg") self._keymanager = KeyManager( address, "https://nicknym.%s:6425" % (self._provider_config.get_domain(),), self._soledad, #token=srp_auth.get_token(), # TODO: enable token usage session_id=srp_auth.get_session_id(), ca_cert_path=self._provider_config.get_ca_cert_path(), api_uri=self._provider_config.get_api_uri(), api_version=self._provider_config.get_api_version(), uid=srp_auth.get_uid(), gpgbinary=gpgbin) try: self._keymanager.get_key(address, openpgp.OpenPGPKey, private=True, fetch_remote=False) except KeyNotFound: logger.debug("Key not found. Generating key for %s" % (address,)) self._keymanager.gen_key(openpgp.OpenPGPKey) self._keymanager.send_key(openpgp.OpenPGPKey) logger.debug("Key generated successfully.") def run_soledad_setup_checks(self, provider_config, user, password, download_if_needed=False, standalone=False): """ Starts the checks needed for a new soledad setup :param provider_config: Provider configuration :type provider_config: ProviderConfig :param user: User's login :type user: str :param password: User's password :type password: str :param download_if_needed: If True, it will only download files if the have changed since the time it was previously downloaded. :type download_if_needed: bool :param standalone: If True, it'll look for paths inside the bundle (like for gpg) :type standalone: bool """ leap_assert_type(provider_config, ProviderConfig) # XXX we should provider a method for setting provider_config self._provider_config = provider_config self._download_if_needed = download_if_needed self._user = user self._password = password self._standalone = standalone cb_chain = [ (self._download_config, self.download_config), (self._gen_key, self.gen_key) ] self.addCallbackChain(cb_chain)