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
示例#2
0
    def _check_ca_fingerprint(self, *args):
        """
        Checks the CA cert fingerprint against the one provided in the
        json definition
        """
        leap_assert(self._provider_config, "Cannot check the ca cert "
                    "without a provider config!")

        logger.debug("Checking ca fingerprint for %r and cert %r" %
                     (self._domain, self._provider_config.get_ca_cert_path()))

        if not self._should_proceed_cert():
            return

        parts = self._provider_config.get_ca_cert_fingerprint().split(":")

        error_msg = "Wrong fingerprint format"
        leap_check(len(parts) == 2, error_msg, WrongFingerprint)

        method = parts[0].strip()
        fingerprint = parts[1].strip()
        cert_data = None
        with open(self._provider_config.get_ca_cert_path()) as f:
            cert_data = f.read()

        leap_assert(len(cert_data) > 0, "Could not read certificate data")
        digest = get_digest(cert_data, method)

        error_msg = "Downloaded certificate has a different fingerprint!"
        leap_check(digest == fingerprint, error_msg, WrongFingerprint)
    def get_ca_cert_path(self, about_to_download=False):
        """
        Returns the path to the certificate for the current provider.
        It may raise MissingCACert if
        the certificate does not exists and not about_to_download

        :param about_to_download: defines wether we want the path to
                                  download the cert or not. This helps avoid
                                  checking if the cert exists because we
                                  are about to write it.
        :type about_to_download: bool

        :rtype: unicode
        """

        cert_path = os.path.join(get_path_prefix(), "leap", "providers",
                                 self.get_domain(), "keys", "ca", "cacert.pem")

        if not about_to_download:
            cert_exists = os.path.exists(cert_path)
            error_msg = "You need to download the certificate first"
            leap_check(cert_exists, error_msg, MissingCACert)
            logger.debug("Going to verify SSL against %s" % (cert_path,))

        # OpenSSL does not handle unicode.
        return cert_path.encode('utf-8')
    def _check_ca_fingerprint(self, *args):
        """
        Checks the CA cert fingerprint against the one provided in the
        json definition
        """
        leap_assert(self._provider_config, "Cannot check the ca cert "
                    "without a provider config!")

        logger.debug("Checking ca fingerprint for %r and cert %r" %
                     (self._domain,
                      self._provider_config.get_ca_cert_path()))

        if not self._should_proceed_cert():
            return

        parts = self._provider_config.get_ca_cert_fingerprint().split(":")

        error_msg = "Wrong fingerprint format"
        leap_check(len(parts) == 2, error_msg, WrongFingerprint)

        method = parts[0].strip()
        fingerprint = parts[1].strip()
        cert_data = None
        with open(self._provider_config.get_ca_cert_path()) as f:
            cert_data = f.read()

        leap_assert(len(cert_data) > 0, "Could not read certificate data")
        digest = get_digest(cert_data, method)

        error_msg = "Downloaded certificate has a different fingerprint!"
        leap_check(digest == fingerprint, error_msg, WrongFingerprint)
    def get_ca_cert_path(self, about_to_download=False):
        """
        Returns the path to the certificate for the current provider.
        It may raise MissingCACert if
        the certificate does not exists and not about_to_download

        :param about_to_download: defines wether we want the path to
                                  download the cert or not. This helps avoid
                                  checking if the cert exists because we
                                  are about to write it.
        :type about_to_download: bool
        """

        cert_path = os.path.join(self.get_path_prefix(),
                                 "leap",
                                 "providers",
                                 self.get_domain(),
                                 "keys",
                                 "ca",
                                 "cacert.pem")

        if not about_to_download:
            cert_exists = os.path.exists(cert_path)
            error_msg = "You need to download the certificate first"
            leap_check(cert_exists, error_msg, MissingCACert)
            logger.debug("Going to verify SSL against %s" % (cert_path,))

        return cert_path
示例#6
0
    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
示例#7
0
    def parse_ascii_key(self, key_data):
        """
        Parses an ascii armored key (or key pair) data and returns
        the OpenPGPKey keys.

        :param key_data: the key data to be parsed.
        :type key_data: str or unicode

        :returns: the public key and private key (if applies) for that data.
        :rtype: (public, private) -> tuple(OpenPGPKey, OpenPGPKey)
                the tuple may have one or both components None
        """
        leap_assert_type(key_data, (str, unicode))
        # TODO: add more checks for correct key data.
        leap_assert(key_data is not None, 'Data does not represent a key.')
        mail_regex = '.*<([\w.-]+@[\w.-]+)>.*'

        with self._temporary_gpgwrapper() as gpg:
            # TODO: inspect result, or use decorator
            gpg.import_keys(key_data)
            privkey = None
            pubkey = None

            try:
                privkey = gpg.list_keys(secret=True).pop()
            except IndexError:
                pass
            pubkey = gpg.list_keys(secret=False).pop()  # unitary keyring

            # extract adress from first uid on key
            match = re.match(mail_regex, pubkey['uids'].pop())
            leap_assert(match is not None, 'No user address in key data.')
            address = match.group(1)

            openpgp_privkey = None
            if privkey is not None:
                match = re.match(mail_regex, privkey['uids'].pop())
                leap_assert(match is not None, 'No user address in key data.')
                privaddress = match.group(1)

                # build private key
                openpgp_privkey = _build_key_from_gpg(
                    privaddress, privkey,
                    gpg.export_keys(privkey['fingerprint'], secret=True))

                leap_check(address == privaddress,
                           'Addresses in public and private key differ.',
                           errors.KeyAddressMismatch)
                leap_check(pubkey['fingerprint'] == privkey['fingerprint'],
                           'Fingerprints for public and private key differ.',
                           errors.KeyFingerprintMismatch)

            # build public key
            openpgp_pubkey = _build_key_from_gpg(
                address, pubkey,
                gpg.export_keys(pubkey['fingerprint'], secret=False))

            return (openpgp_pubkey, openpgp_privkey)
示例#8
0
def run_service(store, **kwargs):
    """
    Main entry point to run the service from the client.

    :param store: a soledad instance

    :returns: the port as returned by the reactor when starts listening, and
              the factory for the protocol.
    """
    leap_check(store, "store cannot be None")
    # XXX this can also be a ProxiedObject, FIXME
    # leap_assert_type(store, Soledad)

    port = kwargs.get('port', IMAP_PORT)
    userid = kwargs.get('userid', None)
    leap_check(userid is not None, "need an user id")

    uuid = store.uuid
    factory = LeapIMAPFactory(uuid, userid, store)

    try:
        interface = "localhost"
        # don't bind just to localhost if we are running on docker since we
        # won't be able to access imap from the host
        if os.environ.get("LEAP_DOCKERIZED"):
            interface = ''

        tport = reactor.listenTCP(port, factory,
                                  interface=interface)
    except CannotListenError:
        logger.error("IMAP Service failed to start: "
                     "cannot listen in port %s" % (port,))
    except Exception as exc:
        logger.error("Error launching IMAP service: %r" % (exc,))
    else:
        # all good.

        if DO_MANHOLE:
            # TODO get pass from env var.too.
            manhole_factory = manhole.getManholeFactory(
                {'f': factory,
                 'a': factory.theAccount,
                 'gm': factory.theAccount.getMailbox},
                "boss", "leap")
            reactor.listenTCP(manhole.MANHOLE_PORT, manhole_factory,
                              interface="127.0.0.1")
        logger.debug("IMAP4 Server is RUNNING in port  %s" % (port,))
        emit_async(catalog.IMAP_SERVICE_STARTED, str(port))

        # FIXME -- change service signature
        return tport, factory

    # not ok, signal error.
    emit_async(catalog.IMAP_SERVICE_FAILED_TO_START, str(port))
示例#9
0
def run_service(store, **kwargs):
    """
    Main entry point to run the service from the client.

    :param store: a soledad instance

    :returns: the port as returned by the reactor when starts listening, and
              the factory for the protocol.
    """
    leap_check(store, "store cannot be None")
    # XXX this can also be a ProxiedObject, FIXME
    # leap_assert_type(store, Soledad)

    port = kwargs.get('port', IMAP_PORT)
    userid = kwargs.get('userid', None)
    leap_check(userid is not None, "need an user id")

    uuid = store.uuid
    factory = LeapIMAPFactory(uuid, userid, store)

    try:
        tport = reactor.listenTCP(port, factory, interface="localhost")
    except CannotListenError:
        logger.error("IMAP Service failed to start: "
                     "cannot listen in port %s" % (port, ))
    except Exception as exc:
        logger.error("Error launching IMAP service: %r" % (exc, ))
    else:
        # all good.

        if DO_MANHOLE:
            # TODO get pass from env var.too.
            manhole_factory = manhole.getManholeFactory(
                {
                    'f': factory,
                    'a': factory.theAccount,
                    'gm': factory.theAccount.getMailbox
                }, "boss", "leap")
            reactor.listenTCP(manhole.MANHOLE_PORT,
                              manhole_factory,
                              interface="127.0.0.1")
        logger.debug("IMAP4 Server is RUNNING in port  %s" % (port, ))
        emit(catalog.IMAP_SERVICE_STARTED, str(port))

        # FIXME -- change service signature
        return tport, factory

    # not ok, signal error.
    emit(catalog.IMAP_SERVICE_FAILED_TO_START, str(port))
示例#10
0
    def parse_ascii_key(self, key_data):
        """
        Parses an ascii armored key (or key pair) data and returns
        the OpenPGPKey keys.

        :param key_data: the key data to be parsed.
        :type key_data: str or unicode

        :returns: the public key and private key (if applies) for that data.
        :rtype: (public, private) -> tuple(OpenPGPKey, OpenPGPKey)
                the tuple may have one or both components None
        """
        leap_assert_type(key_data, (str, unicode))
        # TODO: add more checks for correct key data.
        leap_assert(key_data is not None, 'Data does not represent a key.')

        with self._temporary_gpgwrapper() as gpg:
            # TODO: inspect result, or use decorator
            gpg.import_keys(key_data)
            privkey = None
            pubkey = None

            try:
                privkey = gpg.list_keys(secret=True).pop()
            except IndexError:
                pass
            try:
                pubkey = gpg.list_keys(secret=False).pop()  # unitary keyring
            except IndexError:
                return (None, None)

            openpgp_privkey = None
            if privkey is not None:
                # build private key
                openpgp_privkey = self._build_key_from_gpg(
                    privkey,
                    gpg.export_keys(privkey['fingerprint'], secret=True))
                leap_check(pubkey['fingerprint'] == privkey['fingerprint'],
                           'Fingerprints for public and private key differ.',
                           errors.KeyFingerprintMismatch)

            # build public key
            openpgp_pubkey = self._build_key_from_gpg(
                pubkey,
                gpg.export_keys(pubkey['fingerprint'], secret=False))

            return (openpgp_pubkey, openpgp_privkey)
示例#11
0
    def parse_ascii_key(self, key_data):
        """
        Parses an ascii armored key (or key pair) data and returns
        the OpenPGPKey keys.

        :param key_data: the key data to be parsed.
        :type key_data: str or unicode

        :returns: the public key and private key (if applies) for that data.
        :rtype: (public, private) -> tuple(OpenPGPKey, OpenPGPKey)
                the tuple may have one or both components None
        """
        leap_assert_type(key_data, (str, unicode))
        # TODO: add more checks for correct key data.
        leap_assert(key_data is not None, 'Data does not represent a key.')

        with self._temporary_gpgwrapper() as gpg:
            # TODO: inspect result, or use decorator
            gpg.import_keys(key_data)
            privkey = None
            pubkey = None

            try:
                privkey = gpg.list_keys(secret=True).pop()
            except IndexError:
                pass
            try:
                pubkey = gpg.list_keys(secret=False).pop()  # unitary keyring
            except IndexError:
                return (None, None)

            openpgp_privkey = None
            if privkey is not None:
                # build private key
                openpgp_privkey = self._build_key_from_gpg(
                    privkey,
                    gpg.export_keys(privkey['fingerprint'], secret=True))
                leap_check(pubkey['fingerprint'] == privkey['fingerprint'],
                           'Fingerprints for public and private key differ.',
                           errors.KeyFingerprintMismatch)

            # build public key
            openpgp_pubkey = self._build_key_from_gpg(
                pubkey, gpg.export_keys(pubkey['fingerprint'], secret=False))

            return (openpgp_pubkey, openpgp_privkey)
    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 _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
示例#14
0
    def load(self, path="", data=None, mtime=None, relative=True):
        """
        Loads the configuration from disk.
        It may raise NonExistingSchema exception.

        :param path: if relative=True, this is a relative path
                     to configuration. The absolute path
                     will be calculated depending on the platform
        :type path: str

        :param relative: if True, path is relative. If False, it's absolute.
        :type relative: bool

        :return: True if loaded from disk correctly, False otherwise
        :rtype: bool
        """

        if relative is True:
            config_path = os.path.join(
                self.get_path_prefix(), path)
        else:
            config_path = path

        schema = self._get_spec()
        leap_check(schema is not None,
                   "There is no schema to use.", NonExistingSchema)

        self._config_checker = PluggableConfig(format="json")
        self._config_checker.options = copy.deepcopy(schema)

        try:
            if data is None:
                self._config_checker.load(fromfile=config_path, mtime=mtime)
            else:
                self._config_checker.load(data, mtime=mtime)
        except Exception as e:
            logger.error("Something went wrong while loading " +
                         "the config from %s\n%s" % (config_path, e))
            self._config_checker = None
            return False
        return True
示例#15
0
    def load(self, path="", data=None, mtime=None, relative=True):
        """
        Loads the configuration from disk.
        It may raise NonExistingSchema exception.

        :param path: if relative=True, this is a relative path
                     to configuration. The absolute path
                     will be calculated depending on the platform
        :type path: str

        :param relative: if True, path is relative. If False, it's absolute.
        :type relative: bool

        :return: True if loaded from disk correctly, False otherwise
        :rtype: bool
        """

        if relative is True:
            config_path = os.path.join(self.get_path_prefix(), path)
        else:
            config_path = path

        schema = self._get_spec()
        leap_check(schema is not None, "There is no schema to use.",
                   NonExistingSchema)

        self._config_checker = PluggableConfig(format="json")
        self._config_checker.options = copy.deepcopy(schema)

        try:
            if data is None:
                self._config_checker.load(fromfile=config_path, mtime=mtime)
            else:
                self._config_checker.load(data, mtime=mtime)
        except Exception as e:
            logger.error("Something went wrong while loading " +
                         "the config from %s\n%s" % (config_path, e))
            self._config_checker = None
            return False
        return True
示例#16
0
    def parse_ascii_key(self, key_data, address=None):
        """
        Parses an ascii armored key (or key pair) data and returns
        the OpenPGPKey keys.

        :param key_data: the key data to be parsed.
        :type key_data: str or unicode
        :param address: Active address for the key.
        :type address: str

        :returns: the public key and private key (if applies) for that data.
        :rtype: (public, private) -> tuple(OpenPGPKey, OpenPGPKey)
                the tuple may have one or both components None
        """
        leap_assert_type(key_data, (str, unicode))
        # TODO: add more checks for correct key data.
        leap_assert(key_data is not None, 'Data does not represent a key.')

        priv_info, privkey = process_ascii_key(
            key_data, self._gpgbinary, secret=True)
        pub_info, pubkey = process_ascii_key(
            key_data, self._gpgbinary, secret=False)

        if not pubkey:
            return (None, None)

        openpgp_privkey = None
        if privkey:
            # build private key
            openpgp_privkey = self._build_key_from_gpg(priv_info, privkey,
                                                       address)
            leap_check(pub_info['fingerprint'] == priv_info['fingerprint'],
                       'Fingerprints for public and private key differ.',
                       errors.KeyFingerprintMismatch)
        # build public key
        openpgp_pubkey = self._build_key_from_gpg(pub_info, pubkey, address)

        return (openpgp_pubkey, openpgp_privkey)
示例#17
0
    def parse_key(self, key_data, address=None):
        """
        Parses a key (or key pair) data and returns
        the OpenPGPKey keys.

        :param key_data: the key data to be parsed.
        :type key_data: str or unicode
        :param address: Active address for the key.
        :type address: str

        :returns: the public key and private key (if applies) for that data.
        :rtype: (public, private) -> tuple(OpenPGPKey, OpenPGPKey)
                the tuple may have one or both components None
        """
        leap_assert_type(key_data, (str, unicode))
        # TODO: add more checks for correct key data.
        leap_assert(key_data is not None, 'Data does not represent a key.')

        priv_info, privkey = process_key(key_data,
                                         self._gpgbinary,
                                         secret=True)
        pub_info, pubkey = process_key(key_data, self._gpgbinary, secret=False)

        if not pubkey:
            return (None, None)

        openpgp_privkey = None
        if privkey:
            # build private key
            openpgp_privkey = self._build_key_from_gpg(priv_info, privkey,
                                                       address)
            leap_check(pub_info['fingerprint'] == priv_info['fingerprint'],
                       'Fingerprints for public and private key differ.',
                       errors.KeyFingerprintMismatch)
        # build public key
        openpgp_pubkey = self._build_key_from_gpg(pub_info, pubkey, address)

        return (openpgp_pubkey, openpgp_privkey)
示例#18
0
def run_service(*args, **kwargs):
    """
    Main entry point to run the service from the client.

    :returns: the LoopingCall instance that will have to be stoppped
              before shutting down the client, the port as returned by
              the reactor when starts listening, and the factory for
              the protocol.
    """
    from twisted.internet import reactor
    # it looks like qtreactor does not honor this,
    # but other reactors should.
    reactor.suggestThreadPoolSize(20)

    leap_assert(len(args) == 2)
    soledad, keymanager = args
    leap_assert_type(soledad, Soledad)
    leap_assert_type(keymanager, KeyManager)

    port = kwargs.get('port', IMAP_PORT)
    check_period = kwargs.get('check_period', INCOMING_CHECK_PERIOD)
    userid = kwargs.get('userid', None)
    leap_check(userid is not None, "need an user id")
    offline = kwargs.get('offline', False)

    uuid = soledad._get_uuid()
    factory = LeapIMAPFactory(uuid, userid, soledad)

    try:
        tport = reactor.listenTCP(port, factory,
                                  interface="localhost")
        if not offline:
            fetcher = LeapIncomingMail(
                keymanager,
                soledad,
                factory.theAccount,
                check_period,
                userid)
        else:
            fetcher = None
    except CannotListenError:
        logger.error("IMAP Service failed to start: "
                     "cannot listen in port %s" % (port,))
    except Exception as exc:
        logger.error("Error launching IMAP service: %r" % (exc,))
    else:
        # all good.
        # (the caller has still to call fetcher.start_loop)

        if DO_MANHOLE:
            # TODO get pass from env var.too.
            manhole_factory = manhole.getManholeFactory(
                {'f': factory,
                 'a': factory.theAccount,
                 'gm': factory.theAccount.getMailbox},
                "boss", "leap")
            reactor.listenTCP(manhole.MANHOLE_PORT, manhole_factory,
                              interface="127.0.0.1")
        logger.debug("IMAP4 Server is RUNNING in port  %s" % (port,))
        leap_events.signal(IMAP_SERVICE_STARTED, str(port))
        return fetcher, tport, factory

    # not ok, signal error.
    leap_events.signal(IMAP_SERVICE_FAILED_TO_START, str(port))
示例#19
0
    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()