def __init__(self, userid, passwd, mdir=None): """ Initialize the plumber with all that's needed to authenticate against the provider. :param userid: user identifier, foo@bar :type userid: basestring :param passwd: the soledad passphrase :type passwd: basestring :param mdir: a path to a maildir to import :type mdir: str or None """ self.userid = userid self.passwd = passwd user, provider = userid.split('@') self.user = user self.mdir = mdir self.sol = None self._settings = Settings() provider_config_path = os.path.join(get_path_prefix(), get_provider_path(provider)) provider_config = ProviderConfig() loaded = provider_config.load(provider_config_path) if not loaded: print "could not load provider config!" return self.exit()
def get_gateways(kls, eipconfig, providerconfig): """ Return the selected gateways for a given provider, looking at the EIP config file. :param eipconfig: eip configuration object :type eipconfig: EIPConfig :param providerconfig: provider specific configuration :type providerconfig: ProviderConfig :rtype: list """ gateways = [] settings = Settings() domain = providerconfig.get_domain() gateway_conf = settings.get_selected_gateway(domain) gateway_selector = VPNGatewaySelector(eipconfig) if gateway_conf == GATEWAY_AUTOMATIC: gateways = gateway_selector.get_gateways() else: gateways = [gateway_conf] if not gateways: logger.error('No gateway was found!') raise VPNLauncherException('No gateway was found!') logger.debug("Using gateways ips: {0}".format(', '.join(gateways))) return gateways
def __init__(self, bypass_checks=False, frontend_pid=None): """ Constructor for the backend. """ Backend.__init__(self, frontend_pid) self._settings = Settings() # Objects needed by several components, so we make a proxy and pass # them around self._soledad_proxy = zope.proxy.ProxyBase(None) self._keymanager_proxy = zope.proxy.ProxyBase(None) # Component instances creation self._provider = components.Provider(self._signaler, bypass_checks) self._register = components.Register(self._signaler) self._authenticate = components.Authenticate(self._signaler) self._eip = components.EIP(self._signaler) self._soledad = components.Soledad(self._soledad_proxy, self._keymanager_proxy, self._signaler) self._keymanager = components.Keymanager(self._keymanager_proxy, self._signaler) self._mail = components.Mail(self._soledad_proxy, self._keymanager_proxy, self._signaler)
def get_gateways(kls, eipconfig, providerconfig): """ Return a list with the selected gateways for a given provider, looking at the EIP config file. Each item of the list is a tuple containing (gateway, port). :param eipconfig: eip configuration object :type eipconfig: EIPConfig :param providerconfig: provider specific configuration :type providerconfig: ProviderConfig :rtype: list """ gateways = [] settings = Settings() domain = providerconfig.get_domain() gateway_conf = settings.get_selected_gateway(domain) gateway_selector = VPNGatewaySelector(eipconfig) if gateway_conf == GATEWAY_AUTOMATIC: gws = gateway_selector.get_gateways() else: gws = [gateway_conf] if not gws: logger.error('No gateway was found!') raise VPNLauncherException('No gateway was found!') for idx, gw in enumerate(gws): ports = eipconfig.get_gateway_ports(idx) the_port = "1194" # default port # pick the port preferring this order: for port in kls.PREFERRED_PORTS: if port in ports: the_port = port break else: continue gateways.append((gw, the_port)) logger.debug("Using gateways (ip, port): {0!r}".format(gateways)) return gateways
def get_gateway_country_code(self, domain): """ Signal the country code for the currently used gateway for the given provider. :param domain: the domain to get country code. :type domain: str Signals: eip_get_gateway_country_code -> str eip_no_gateway """ settings = Settings() eip_config = eipconfig.EIPConfig() provider_config = ProviderConfig.get_provider_config(domain) api_version = provider_config.get_api_version() eip_config.set_api_version(api_version) eip_config.load(eipconfig.get_eipconfig_path(domain)) gateway_selector = eipconfig.VPNGatewaySelector(eip_config) gateway_conf = settings.get_selected_gateway(domain) if gateway_conf == GATEWAY_AUTOMATIC: gateways = gateway_selector.get_gateways() else: gateways = [gateway_conf] if not gateways: self._signaler.signal(self._signaler.eip_no_gateway) return # this only works for selecting the first gateway, as we're # currently doing. ccodes = gateway_selector.get_gateways_country_code() gateway_ccode = '' # '' instead of None due to needed signal argument if ccodes is not None: gateway_ccode = ccodes[gateways[0]] self._signaler.signal(self._signaler.eip_get_gateway_country_code, gateway_ccode)
def __init__(self, provider_config, signaler=None): """ Constructor for SRPAuth implementation :param provider_config: ProviderConfig needed to authenticate. :type provider_config: ProviderConfig :param signaler: Signaler object used to receive notifications from the backend :type signaler: Signaler """ leap_assert(provider_config, "We need a provider config to authenticate") self._provider_config = provider_config self._signaler = signaler self._settings = Settings() # **************************************************** # # Dependency injection helpers, override this for more # granular testing self._fetcher = requests self._srp = srp self._hashfun = self._srp.SHA256 self._ng = self._srp.NG_1024 # **************************************************** # self._reset_session() self._session_id = None self._session_id_lock = threading.Lock() self._uuid = None self._uuid_lock = threading.Lock() self._token = None self._token_lock = threading.Lock() self._srp_user = None self._srp_a = None # User credentials stored for password changing checks self._username = None self._password = None
class MBOXPlumber(object): """ An class that can fix things inside a soledadbacked account. The idea is to gather in this helper different fixes for mailboxes that can be invoked when data migration in the client is needed. """ def __init__(self, userid, passwd, mdir=None): """ Initialize the plumber with all that's needed to authenticate against the provider. :param userid: user identifier, foo@bar :type userid: basestring :param passwd: the soledad passphrase :type passwd: basestring :param mdir: a path to a maildir to import :type mdir: str or None """ self.userid = userid self.passwd = passwd user, provider = userid.split('@') self.user = user self.mdir = mdir self.sol = None self._settings = Settings() provider_config_path = os.path.join(get_path_prefix(), get_provider_path(provider)) provider_config = ProviderConfig() loaded = provider_config.load(provider_config_path) if not loaded: print "could not load provider config!" return self.exit() def _init_local_soledad(self): """ Initialize local Soledad instance. """ self.uuid = self._settings.get_uuid(self.userid) if not self.uuid: print "Cannot get UUID from settings. Log in at least once." return False print "UUID: %s" % (self.uuid) secrets, localdb = get_db_paths(self.uuid) self.sol = initialize_soledad( self.uuid, self.userid, self.passwd, secrets, localdb, "/tmp", "/tmp") memstore = MemoryStore( permanent_store=SoledadStore(self.sol), write_period=5) self.acct = SoledadBackedAccount(self.userid, self.sol, memstore=memstore) return True # # Account repairing # def repair_account(self, *args): """ Repair mbox uids for all mboxes in this account. """ init = self._init_local_soledad() if not init: return self.exit() for mbox_name in self.acct.mailboxes: self.repair_mbox_uids(mbox_name) print "done." self.exit() def repair_mbox_uids(self, mbox_name): """ Repair indexes for a given mbox. :param mbox_name: mailbox to repair :type mbox_name: basestring """ print print "REPAIRING INDEXES FOR MAILBOX %s" % (mbox_name,) print "----------------------------------------------" mbox = self.acct.getMailbox(mbox_name) len_mbox = mbox.getMessageCount() print "There are %s messages" % (len_mbox,) last_ok = True if mbox.last_uid == len_mbox else False uids_iter = mbox.messages.all_msg_iter() dupes = self._has_dupes(uids_iter) if last_ok and not dupes: print "Mbox does not need repair." return # XXX CHANGE? ---- msgs = mbox.messages.get_all() for zindex, doc in enumerate(msgs): mindex = zindex + 1 old_uid = doc.content['uid'] doc.content['uid'] = mindex self.sol.put_doc(doc) if mindex != old_uid: print "%s -> %s (%s)" % (mindex, doc.content['uid'], old_uid) old_last_uid = mbox.last_uid mbox.last_uid = len_mbox print "LAST UID: %s (%s)" % (mbox.last_uid, old_last_uid) def _has_dupes(self, sequence): """ Return True if the given sequence of ints has duplicates. :param sequence: a sequence of ints :type sequence: sequence :rtype: bool """ d = defaultdict(lambda: 0) for uid in sequence: d[uid] += 1 if d[uid] != 1: return True return False # # Maildir import # def import_mail(self, mail_filename): """ Import a single mail into a mailbox. :param mbox: the Mailbox instance to save in. :type mbox: SoledadMailbox :param mail_filename: the filename to the mail file to save :type mail_filename: basestring :return: a deferred """ def saved(_): print "message added" with open(mail_filename) as f: mail_string = f.read() # uid = self._mbox.getUIDNext() # print "saving with UID: %s" % uid d = self._mbox.messages.add_msg( mail_string, notify_on_disk=True) return d def import_maildir(self, mbox_name="INBOX"): """ Import all mails in a maildir. We will process all subfolders as beloging to the same mailbox (cur, new, tmp). """ # TODO parse hierarchical subfolders into # inferior mailboxes. if not os.path.isdir(self.mdir): print "ERROR: maildir path does not exist." return init = self._init_local_soledad() if not init: return self.exit() mbox = self.acct.getMailbox(mbox_name) self._mbox = mbox len_mbox = mbox.getMessageCount() mail_files_g = flatten( map(partial(os.path.join, f), files) for f, _, files in os.walk(self.mdir)) # we only coerce the generator to give the # len, but we could skip than and inform at the end. mail_files = list(mail_files_g) print "Got %s mails to import into %s (%s)" % ( len(mail_files), mbox_name, len_mbox) def all_saved(_): print "all messages imported" deferreds = [] for f_name in mail_files: deferreds.append(self.import_mail(f_name)) print "deferreds: ", deferreds d1 = defer.gatherResults(deferreds, consumeErrors=False) d1.addCallback(all_saved) d1.addCallback(self._cbExit) def _cbExit(self, ignored): return self.exit() def exit(self): from twisted.internet import reactor try: if self.sol: self.sol.close() reactor.stop() except Exception: pass return
class LeapBackend(Backend): """ Backend server subclass, used to implement the API methods. """ def __init__(self, bypass_checks=False, frontend_pid=None): """ Constructor for the backend. """ Backend.__init__(self, frontend_pid) self._settings = Settings() # Objects needed by several components, so we make a proxy and pass # them around self._soledad_proxy = zope.proxy.ProxyBase(None) self._keymanager_proxy = zope.proxy.ProxyBase(None) # Component instances creation self._provider = components.Provider(self._signaler, bypass_checks) self._register = components.Register(self._signaler) self._authenticate = components.Authenticate(self._signaler) self._eip = components.EIP(self._signaler) self._soledad = components.Soledad(self._soledad_proxy, self._keymanager_proxy, self._signaler) self._keymanager = components.Keymanager(self._keymanager_proxy, self._signaler) self._mail = components.Mail(self._soledad_proxy, self._keymanager_proxy, self._signaler) def _check_type(self, obj, expected_type): """ Check the type of a parameter. :param obj: object to check its type. :type obj: any type :param expected_type: the expected type of the object. :type expected_type: type """ if not isinstance(obj, expected_type): raise TypeError("The parameter type is incorrect.") def provider_setup(self, provider): """ Initiate the setup for a provider. :param provider: URL for the provider :type provider: unicode Signals: prov_unsupported_client prov_unsupported_api prov_name_resolution -> { PASSED_KEY: bool, ERROR_KEY: str } prov_https_connection -> { PASSED_KEY: bool, ERROR_KEY: str } prov_download_provider_info -> { PASSED_KEY: bool, ERROR_KEY: str } """ self._provider.setup_provider(provider) def provider_cancel_setup(self): """ Cancel the ongoing setup provider (if any). """ self._provider.cancel_setup_provider() def provider_bootstrap(self, provider): """ Second stage of bootstrapping for a provider. :param provider: URL for the provider :type provider: unicode Signals: prov_problem_with_provider prov_download_ca_cert -> {PASSED_KEY: bool, ERROR_KEY: str} prov_check_ca_fingerprint -> {PASSED_KEY: bool, ERROR_KEY: str} prov_check_api_certificate -> {PASSED_KEY: bool, ERROR_KEY: str} """ self._provider.bootstrap(provider) def provider_get_supported_services(self, domain): """ Signal a list of supported services provided by the given provider. :param domain: the provider to get the services from. :type domain: str Signals: prov_get_supported_services -> list of unicode """ self._provider.get_supported_services(domain) def provider_get_all_services(self, providers): """ Signal a list of services provided by all the configured providers. :param providers: the list of providers to get the services. :type providers: list Signals: prov_get_all_services -> list of unicode """ self._provider.get_all_services(providers) def provider_get_details(self, domain, lang): """ Signal a dict with the current ProviderConfig settings. :param domain: the domain name of the provider. :type domain: str :param lang: the language to use for localized strings. :type lang: str Signals: prov_get_details -> dict """ self._provider.get_details(domain, lang) def provider_get_pinned_providers(self): """ Signal the pinned providers. Signals: prov_get_pinned_providers -> list of provider domains """ self._provider.get_pinned_providers() def user_register(self, provider, username, password): """ Register a user using the domain and password given as parameters. :param domain: the domain we need to register the user. :type domain: unicode :param username: the user name :type username: unicode :param password: the password for the username :type password: unicode Signals: srp_registration_finished srp_registration_taken srp_registration_failed """ self._register.register_user(provider, username, password) def eip_setup(self, provider, skip_network=False): """ Initiate the setup for a provider :param provider: URL for the provider :type provider: unicode :param skip_network: Whether checks that involve network should be done or not :type skip_network: bool Signals: eip_config_ready -> {PASSED_KEY: bool, ERROR_KEY: str} eip_client_certificate_ready -> {PASSED_KEY: bool, ERROR_KEY: str} eip_cancelled_setup """ self._eip.setup_eip(provider, skip_network) def eip_cancel_setup(self): """ Cancel the ongoing setup EIP (if any). """ self._eip.cancel_setup_eip() def eip_start(self, restart=False): """ Start the EIP service. Signals: backend_bad_call eip_alien_openvpn_already_running eip_connected eip_connection_aborted eip_network_unreachable eip_no_pkexec_error eip_no_polkit_agent_error eip_no_tun_kext_error eip_openvpn_already_running eip_openvpn_not_found_error eip_process_finished eip_process_restart_ping eip_process_restart_tls eip_state_changed -> str eip_status_changed -> tuple of str (download, upload) eip_vpn_launcher_exception :param restart: whether is is a restart. :type restart: bool """ self._eip.start(restart) def eip_stop(self, shutdown=False, restart=False, failed=False): """ Stop the EIP service. :param shutdown: whether this is the final shutdown. :type shutdown: bool :param restart: whether this is part of a restart. :type restart: bool """ self._eip.stop(shutdown, restart) def eip_terminate(self): """ Terminate the EIP service, not necessarily in a nice way. """ self._eip.terminate() def eip_get_gateways_list(self, domain): """ Signal a list of gateways for the given provider. :param domain: the domain to get the gateways. :type domain: str Signals: eip_get_gateways_list -> list of unicode eip_get_gateways_list_error eip_uninitialized_provider """ self._eip.get_gateways_list(domain) def eip_get_gateway_country_code(self, domain): """ Signal a list of gateways for the given provider. :param domain: the domain to get the gateways. :type domain: str Signals: eip_get_gateways_list -> str eip_no_gateway """ self._eip.get_gateway_country_code(domain) def eip_get_initialized_providers(self, domains): """ Signal a list of the given domains and if they are initialized or not. :param domains: the list of domains to check. :type domain: list of str Signals: eip_get_initialized_providers -> list of tuple(unicode, bool) """ self._eip.get_initialized_providers(domains) def eip_can_start(self, domain): """ Signal whether it has everything that is needed to run EIP or not :param domain: the domain for the provider to check :type domain: str Signals: eip_can_start eip_cannot_start """ self._eip.can_start(domain) def eip_check_dns(self, domain): """ Check if we can resolve the given domain name. :param domain: the domain for the provider to check :type domain: str Signals: eip_dns_ok eip_dns_error """ self._eip.check_dns(domain) def tear_fw_down(self): """ Signal the need to tear the fw down. """ self._eip.tear_fw_down() def bitmask_root_vpn_down(self): """ Signal the need to bring vpn down. """ self._eip.bitmask_root_vpn_down() def user_login(self, provider, username, password): """ Execute the whole authentication process for a user :param domain: the domain where we need to authenticate. :type domain: unicode :param username: username for this session :type username: str :param password: password for this user :type password: str Signals: srp_auth_error srp_auth_ok srp_auth_bad_user_or_password srp_auth_server_error srp_auth_connection_error srp_auth_error """ self._authenticate.login(provider, username, password) def user_logout(self): """ Log out the current session. Signals: srp_logout_ok srp_logout_error srp_not_logged_in_error """ self._authenticate.logout() def user_cancel_login(self): """ Cancel the ongoing login (if any). """ self._authenticate.cancel_login() def user_change_password(self, current_password, new_password): """ Change the user's password. :param current_password: the current password of the user. :type current_password: str :param new_password: the new password for the user. :type new_password: str Signals: srp_not_logged_in_error srp_password_change_ok srp_password_change_badpw srp_password_change_error """ self._authenticate.change_password(current_password, new_password) def soledad_change_password(self, new_password): """ Change the database's password. :param new_password: the new password for the user. :type new_password: unicode Signals: srp_not_logged_in_error srp_password_change_ok srp_password_change_badpw srp_password_change_error """ self._soledad.change_password(new_password) def user_get_logged_in_status(self): """ Signal if the user is currently logged in or not. Signals: srp_status_logged_in srp_status_not_logged_in """ self._authenticate.get_logged_in_status() def soledad_bootstrap(self, username, domain, password): """ Bootstrap the soledad database. :param username: the user name :type username: unicode :param domain: the domain that we are using. :type domain: unicode :param password: the password for the username :type password: unicode Signals: soledad_bootstrap_finished soledad_bootstrap_failed soledad_invalid_auth_token """ self._check_type(username, unicode) self._check_type(domain, unicode) self._check_type(password, unicode) self._soledad.bootstrap(username, domain, password) def soledad_load_offline(self, username, password, uuid): """ Load the soledad database in offline mode. :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 Signals: """ self._soledad.load_offline(username, password, uuid) def soledad_cancel_bootstrap(self): """ Cancel the ongoing soledad bootstrapping process (if any). """ self._soledad.cancel_bootstrap() def soledad_close(self): """ Close soledad database. """ self._soledad.close() def keymanager_list_keys(self): """ Signal a list of public keys locally stored. Signals: keymanager_keys_list -> list """ self._keymanager.list_keys() def keymanager_export_keys(self, username, filename): """ Export the given username's keys to a file. :param username: the username whos keys we need to export. :type username: str :param filename: the name of the file where we want to save the keys. :type filename: str Signals: keymanager_export_ok keymanager_export_error """ self._keymanager.export_keys(username, filename) def keymanager_get_key_details(self, username): """ Signal the given username's key details. :param username: the username whos keys we need to get details. :type username: str Signals: keymanager_key_details """ self._keymanager.get_key_details(username) def smtp_start_service(self, full_user_id, download_if_needed=False): """ Start the SMTP service. :param full_user_id: user id, in the form "user@provider" :type full_user_id: str :param download_if_needed: True if it should check for mtime for the file :type download_if_needed: bool """ self._mail.start_smtp_service(full_user_id, download_if_needed) def imap_start_service(self, full_user_id, offline=False): """ Start the IMAP service. :param full_user_id: user id, in the form "user@provider" :type full_user_id: str :param offline: whether imap should start in offline mode or not. :type offline: bool """ self._mail.start_imap_service(full_user_id, offline) def smtp_stop_service(self): """ Stop the SMTP service. """ self._mail.stop_smtp_service() def imap_stop_service(self): """ Stop imap service. Signals: imap_stopped """ self._mail.stop_imap_service() def settings_set_selected_gateway(self, provider, gateway): """ Set the selected gateway for a given provider. :param provider: provider domain :type provider: str :param gateway: gateway to use as default :type gateway: str """ self._settings.set_selected_gateway(provider, gateway)
class SRPAuthImpl(object): """ Implementation of the SRPAuth interface """ LOGIN_KEY = "login" A_KEY = "A" CLIENT_AUTH_KEY = "client_auth" SESSION_ID_KEY = "_session_id" USER_VERIFIER_KEY = 'user[password_verifier]' USER_SALT_KEY = 'user[password_salt]' AUTHORIZATION_KEY = "Authorization" def __init__(self, provider_config): """ Constructor for SRPAuth implementation :param provider_config: ProviderConfig needed to authenticate. :type provider_config: ProviderConfig """ leap_assert(provider_config, "We need a provider config to authenticate") self._provider_config = provider_config self._settings = Settings() # **************************************************** # # Dependency injection helpers, override this for more # granular testing self._fetcher = requests self._srp = srp self._hashfun = self._srp.SHA256 self._ng = self._srp.NG_1024 # **************************************************** # self._reset_session() self._session_id = None self._session_id_lock = threading.Lock() self._uuid = None self._uuid_lock = threading.Lock() self._token = None self._token_lock = threading.Lock() self._srp_user = None self._srp_a = None # User credentials stored for password changing checks self._username = None self._password = None def _reset_session(self): """ Resets the current session and sets max retries to 30. """ self._session = self._fetcher.session() # We need to bump the default retries, otherwise logout # fails most of the times # NOTE: This is a workaround for the moment, the server # side seems to return correctly every time, but it fails # on the client end. if requests_has_max_retries: adapter = HTTPAdapter(max_retries=30) else: adapter = HTTPAdapter() self._session.mount('https://', adapter) def _safe_unhexlify(self, val): """ Rounds the val to a multiple of 2 and returns the unhexlified value :param val: hexlified value :type val: str :rtype: binary hex data :return: unhexlified val """ return binascii.unhexlify(val) \ if (len(val) % 2 == 0) else binascii.unhexlify('0' + val) def _authentication_preprocessing(self, username, password): """ Generates the SRP.User to get the A SRP parameter :param username: username to login :type username: str :param password: password for the username :type password: str """ logger.debug("Authentication preprocessing...") self._srp_user = self._srp.User(username.encode('utf-8'), password.encode('utf-8'), self._hashfun, self._ng) _, A = self._srp_user.start_authentication() self._srp_a = A def _start_authentication(self, _, username): """ Sends the first request for authentication to retrieve the salt and B parameter Might raise all SRPAuthenticationError based: SRPAuthenticationError SRPAuthConnectionError SRPAuthBadStatusCode SRPAuthNoSalt SRPAuthNoB :param _: IGNORED, output from the previous callback (None) :type _: IGNORED :param username: username to login :type username: str :return: salt and B parameters :rtype: tuple """ logger.debug("Starting authentication process...") try: auth_data = { self.LOGIN_KEY: username, self.A_KEY: binascii.hexlify(self._srp_a) } sessions_url = "%s/%s/%s/" % \ (self._provider_config.get_api_uri(), self._provider_config.get_api_version(), "sessions") ca_cert_path = self._provider_config.get_ca_cert_path() ca_cert_path = ca_cert_path.encode(sys.getfilesystemencoding()) init_session = self._session.post(sessions_url, data=auth_data, verify=ca_cert_path, timeout=REQUEST_TIMEOUT) # Clean up A value, we don't need it anymore self._srp_a = None except requests.exceptions.ConnectionError as e: logger.error("No connection made (salt): {0!r}".format(e)) raise SRPAuthConnectionError() except Exception as e: logger.error("Unknown error: %r" % (e,)) raise SRPAuthenticationError() content, mtime = reqhelper.get_content(init_session) if init_session.status_code not in (200,): logger.error("No valid response (salt): " "Status code = %r. Content: %r" % (init_session.status_code, content)) if init_session.status_code == 422: logger.error("Invalid username or password.") raise SRPAuthBadUserOrPassword() logger.error("There was a problem with authentication.") raise SRPAuthBadStatusCode() json_content = json.loads(content) salt = json_content.get("salt", None) B = json_content.get("B", None) if salt is None: logger.error("The server didn't send the salt parameter.") raise SRPAuthNoSalt() if B is None: logger.error("The server didn't send the B parameter.") raise SRPAuthNoB() return salt, B def _process_challenge(self, salt_B, username): """ Given the salt and B processes the auth challenge and generates the M2 parameter Might raise SRPAuthenticationError based: SRPAuthenticationError SRPAuthBadDataFromServer SRPAuthConnectionError SRPAuthJSONDecodeError SRPAuthBadUserOrPassword :param salt_B: salt and B parameters for the username :type salt_B: tuple :param username: username for this session :type username: str :return: the M2 SRP parameter :rtype: str """ logger.debug("Processing challenge...") try: salt, B = salt_B unhex_salt = self._safe_unhexlify(salt) unhex_B = self._safe_unhexlify(B) except (TypeError, ValueError) as e: logger.error("Bad data from server: %r" % (e,)) raise SRPAuthBadDataFromServer() M = self._srp_user.process_challenge(unhex_salt, unhex_B) auth_url = "%s/%s/%s/%s" % (self._provider_config.get_api_uri(), self._provider_config. get_api_version(), "sessions", username) auth_data = { self.CLIENT_AUTH_KEY: binascii.hexlify(M) } try: auth_result = self._session.put(auth_url, data=auth_data, verify=self._provider_config. get_ca_cert_path(), timeout=REQUEST_TIMEOUT) except requests.exceptions.ConnectionError as e: logger.error("No connection made (HAMK): %r" % (e,)) raise SRPAuthConnectionError() try: content, mtime = reqhelper.get_content(auth_result) except JSONDecodeError: logger.error("Bad JSON content in auth result.") raise SRPAuthJSONDecodeError() if auth_result.status_code == 422: error = "" try: error = json.loads(content).get("errors", "") except ValueError: logger.error("Problem parsing the received response: %s" % (content,)) except AttributeError: logger.error("Expecting a dict but something else was " "received: %s", (content,)) logger.error("[%s] Wrong password (HAMK): [%s]" % (auth_result.status_code, error)) raise SRPAuthBadUserOrPassword() if auth_result.status_code not in (200,): logger.error("No valid response (HAMK): " "Status code = %s. Content = %r" % (auth_result.status_code, content)) raise SRPAuthBadStatusCode() return json.loads(content) def _extract_data(self, json_content): """ Extracts the necessary parameters from json_content (M2, id, token) Might raise SRPAuthenticationError based: SRPBadDataFromServer :param json_content: Data received from the server :type json_content: dict """ try: M2 = json_content.get("M2", None) uuid = json_content.get("id", None) token = json_content.get("token", None) except Exception as e: logger.error(e) raise SRPAuthBadDataFromServer() self.set_uuid(uuid) self.set_token(token) if M2 is None or self.get_uuid() is None: logger.error("Something went wrong. Content = %r" % (json_content,)) raise SRPAuthBadDataFromServer() emit(catalog.CLIENT_UID, uuid) # make the rpc call async return M2 def _verify_session(self, M2): """ Verifies the session based on the M2 parameter. If the verification succeeds, it sets the session_id for this session Might raise SRPAuthenticationError based: SRPAuthBadDataFromServer SRPAuthVerificationFailed :param M2: M2 SRP parameter :type M2: str """ logger.debug("Verifying session...") try: unhex_M2 = self._safe_unhexlify(M2) except TypeError: logger.error("Bad data from server (HAWK)") raise SRPAuthBadDataFromServer() self._srp_user.verify_session(unhex_M2) if not self._srp_user.authenticated(): logger.error("Auth verification failed.") raise SRPAuthVerificationFailed() logger.debug("Session verified.") session_id = self._session.cookies.get(self.SESSION_ID_KEY, None) if not session_id: logger.error("Bad cookie from server (missing _session_id)") raise SRPAuthNoSessionId() # make the rpc call async emit(catalog.CLIENT_SESSION_ID, session_id) self.set_session_id(session_id) logger.debug("SUCCESS LOGIN") return True def _threader(self, cb, res, *args, **kwargs): return threads.deferToThread(cb, res, *args, **kwargs) def _change_password(self, current_password, new_password): """ Changes the password for the currently logged user if the current password match. It requires to be authenticated. Might raise: SRPAuthBadUserOrPassword requests.exceptions.HTTPError :param current_password: the current password for the logged user. :type current_password: str :param new_password: the new password for the user :type new_password: str """ leap_assert(self.get_uuid() is not None) if current_password != self._password: raise SRPAuthBadUserOrPassword url = "%s/%s/users/%s.json" % ( self._provider_config.get_api_uri(), self._provider_config.get_api_version(), self.get_uuid()) salt, verifier = self._srp.create_salted_verification_key( self._username.encode('utf-8'), new_password.encode('utf-8'), self._hashfun, self._ng) cookies = {self.SESSION_ID_KEY: self.get_session_id()} headers = { self.AUTHORIZATION_KEY: "Token token={0}".format(self.get_token()) } user_data = { self.USER_VERIFIER_KEY: binascii.hexlify(verifier), self.USER_SALT_KEY: binascii.hexlify(salt) } change_password = self._session.put( url, data=user_data, verify=self._provider_config.get_ca_cert_path(), cookies=cookies, timeout=REQUEST_TIMEOUT, headers=headers) # In case of non 2xx it raises HTTPError change_password.raise_for_status() self._password = new_password def change_password(self, current_password, new_password): """ Changes the password for the currently logged user if the current password match. It requires to be authenticated. :param current_password: the current password for the logged user. :type current_password: str :param new_password: the new password for the user :type new_password: str """ d = threads.deferToThread( self._change_password, current_password, new_password) return d def authenticate(self, username, password): """ Executes the whole authentication process for a user Might raise SRPAuthenticationError :param username: username for this session :type username: unicode :param password: password for this user :type password: unicode :returns: A defer on a different thread :rtype: twisted.internet.defer.Deferred """ leap_assert(self.get_session_id() is None, "Already logged in") # User credentials stored for password changing checks self._username = username self._password = password self._reset_session() d = threads.deferToThread(self._authentication_preprocessing, username=username, password=password) d.addCallback(partial(self._start_authentication, username=username)) d.addCallback(partial(self._process_challenge, username=username)) d.addCallback(self._extract_data) d.addCallback(self._verify_session) return d def logout(self): """ Logs out the current session. Expects a session_id to exists, might raise AssertionError """ logger.debug("Starting logout...") if self.get_session_id() is None: logger.debug("Already logged out") return logout_url = "%s/%s/%s/" % (self._provider_config.get_api_uri(), self._provider_config. get_api_version(), "logout") try: self._session.delete(logout_url, data=self.get_session_id(), verify=self._provider_config. get_ca_cert_path(), timeout=REQUEST_TIMEOUT) except Exception as e: logger.warning("Something went wrong with the logout: %r" % (e,)) raise else: self.set_session_id(None) self.set_uuid(None) self.set_token(None) # Also reset the session self._session = self._fetcher.session() logger.debug("Successfully logged out.") def set_session_id(self, session_id): with self._session_id_lock: self._session_id = session_id def get_session_id(self): with self._session_id_lock: return self._session_id def set_uuid(self, uuid): with self._uuid_lock: full_uid = "%s@%s" % ( self._username, self._provider_config.get_domain()) if uuid is not None: # avoid removing the uuid from settings self._settings.set_uuid(full_uid, uuid) self._uuid = uuid def get_uuid(self): with self._uuid_lock: return self._uuid def set_token(self, token): with self._token_lock: self._token = token def get_token(self): with self._token_lock: return self._token def is_authenticated(self): """ Return whether the user is authenticated or not. :rtype: bool """ user = self._srp_user if user is not None: return user.authenticated() return False
class SRPAuthImpl(object): """ Implementation of the SRPAuth interface """ LOGIN_KEY = "login" A_KEY = "A" CLIENT_AUTH_KEY = "client_auth" SESSION_ID_KEY = "_session_id" USER_VERIFIER_KEY = 'user[password_verifier]' USER_SALT_KEY = 'user[password_salt]' AUTHORIZATION_KEY = "Authorization" def __init__(self, provider_config): """ Constructor for SRPAuth implementation :param provider_config: ProviderConfig needed to authenticate. :type provider_config: ProviderConfig """ leap_assert(provider_config, "We need a provider config to authenticate") self._provider_config = provider_config self._settings = Settings() # **************************************************** # # Dependency injection helpers, override this for more # granular testing self._fetcher = requests self._srp = srp self._hashfun = self._srp.SHA256 self._ng = self._srp.NG_1024 # **************************************************** # self._reset_session() self._session_id = None self._session_id_lock = threading.Lock() self._uuid = None self._uuid_lock = threading.Lock() self._token = None self._token_lock = threading.Lock() self._srp_user = None self._srp_a = None # User credentials stored for password changing checks self._username = None self._password = None def _reset_session(self): """ Resets the current session and sets max retries to 30. """ self._session = self._fetcher.session() # We need to bump the default retries, otherwise logout # fails most of the times # NOTE: This is a workaround for the moment, the server # side seems to return correctly every time, but it fails # on the client end. if requests_has_max_retries: adapter = HTTPAdapter(max_retries=30) else: adapter = HTTPAdapter() self._session.mount('https://', adapter) def _safe_unhexlify(self, val): """ Rounds the val to a multiple of 2 and returns the unhexlified value :param val: hexlified value :type val: str :rtype: binary hex data :return: unhexlified val """ return binascii.unhexlify(val) \ if (len(val) % 2 == 0) else binascii.unhexlify('0' + val) def _authentication_preprocessing(self, username, password): """ Generates the SRP.User to get the A SRP parameter :param username: username to login :type username: str :param password: password for the username :type password: str """ logger.debug("Authentication preprocessing...") self._srp_user = self._srp.User(username.encode('utf-8'), password.encode('utf-8'), self._hashfun, self._ng) _, A = self._srp_user.start_authentication() self._srp_a = A def _start_authentication(self, _, username): """ Sends the first request for authentication to retrieve the salt and B parameter Might raise all SRPAuthenticationError based: SRPAuthenticationError SRPAuthConnectionError SRPAuthBadStatusCode SRPAuthNoSalt SRPAuthNoB :param _: IGNORED, output from the previous callback (None) :type _: IGNORED :param username: username to login :type username: str :return: salt and B parameters :rtype: tuple """ logger.debug("Starting authentication process...") try: auth_data = { self.LOGIN_KEY: username, self.A_KEY: binascii.hexlify(self._srp_a) } sessions_url = "%s/%s/%s/" % \ (self._provider_config.get_api_uri(), self._provider_config.get_api_version(), "sessions") ca_cert_path = self._provider_config.get_ca_cert_path() ca_cert_path = ca_cert_path.encode(sys.getfilesystemencoding()) init_session = self._session.post(sessions_url, data=auth_data, verify=ca_cert_path, timeout=REQUEST_TIMEOUT) # Clean up A value, we don't need it anymore self._srp_a = None except requests.exceptions.ConnectionError as e: logger.error("No connection made (salt): {0!r}".format(e)) raise SRPAuthConnectionError() except Exception as e: logger.error("Unknown error: %r" % (e, )) raise SRPAuthenticationError() content, mtime = reqhelper.get_content(init_session) if init_session.status_code not in (200, ): logger.error("No valid response (salt): " "Status code = %r. Content: %r" % (init_session.status_code, content)) if init_session.status_code == 422: logger.error("Invalid username or password.") raise SRPAuthBadUserOrPassword() logger.error("There was a problem with authentication.") raise SRPAuthBadStatusCode() json_content = json.loads(content) salt = json_content.get("salt", None) B = json_content.get("B", None) if salt is None: logger.error("The server didn't send the salt parameter.") raise SRPAuthNoSalt() if B is None: logger.error("The server didn't send the B parameter.") raise SRPAuthNoB() return salt, B def _process_challenge(self, salt_B, username): """ Given the salt and B processes the auth challenge and generates the M2 parameter Might raise SRPAuthenticationError based: SRPAuthenticationError SRPAuthBadDataFromServer SRPAuthConnectionError SRPAuthJSONDecodeError SRPAuthBadUserOrPassword :param salt_B: salt and B parameters for the username :type salt_B: tuple :param username: username for this session :type username: str :return: the M2 SRP parameter :rtype: str """ logger.debug("Processing challenge...") try: salt, B = salt_B unhex_salt = self._safe_unhexlify(salt) unhex_B = self._safe_unhexlify(B) except (TypeError, ValueError) as e: logger.error("Bad data from server: %r" % (e, )) raise SRPAuthBadDataFromServer() M = self._srp_user.process_challenge(unhex_salt, unhex_B) auth_url = "%s/%s/%s/%s" % (self._provider_config.get_api_uri(), self._provider_config.get_api_version(), "sessions", username) auth_data = {self.CLIENT_AUTH_KEY: binascii.hexlify(M)} try: auth_result = self._session.put( auth_url, data=auth_data, verify=self._provider_config.get_ca_cert_path(), timeout=REQUEST_TIMEOUT) except requests.exceptions.ConnectionError as e: logger.error("No connection made (HAMK): %r" % (e, )) raise SRPAuthConnectionError() try: content, mtime = reqhelper.get_content(auth_result) except JSONDecodeError: logger.error("Bad JSON content in auth result.") raise SRPAuthJSONDecodeError() if auth_result.status_code == 422: error = "" try: error = json.loads(content).get("errors", "") except ValueError: logger.error("Problem parsing the received response: %s" % (content, )) except AttributeError: logger.error( "Expecting a dict but something else was " "received: %s", (content, )) logger.error("[%s] Wrong password (HAMK): [%s]" % (auth_result.status_code, error)) raise SRPAuthBadUserOrPassword() if auth_result.status_code not in (200, ): logger.error("No valid response (HAMK): " "Status code = %s. Content = %r" % (auth_result.status_code, content)) raise SRPAuthBadStatusCode() return json.loads(content) def _extract_data(self, json_content): """ Extracts the necessary parameters from json_content (M2, id, token) Might raise SRPAuthenticationError based: SRPBadDataFromServer :param json_content: Data received from the server :type json_content: dict """ try: M2 = json_content.get("M2", None) uuid = json_content.get("id", None) token = json_content.get("token", None) except Exception as e: logger.error(e) raise SRPAuthBadDataFromServer() self.set_uuid(uuid) self.set_token(token) if M2 is None or self.get_uuid() is None: logger.error("Something went wrong. Content = %r" % (json_content, )) raise SRPAuthBadDataFromServer() emit(catalog.CLIENT_UID, uuid) # make the rpc call async return M2 def _verify_session(self, M2): """ Verifies the session based on the M2 parameter. If the verification succeeds, it sets the session_id for this session Might raise SRPAuthenticationError based: SRPAuthBadDataFromServer SRPAuthVerificationFailed :param M2: M2 SRP parameter :type M2: str """ logger.debug("Verifying session...") try: unhex_M2 = self._safe_unhexlify(M2) except TypeError: logger.error("Bad data from server (HAWK)") raise SRPAuthBadDataFromServer() self._srp_user.verify_session(unhex_M2) if not self._srp_user.authenticated(): logger.error("Auth verification failed.") raise SRPAuthVerificationFailed() logger.debug("Session verified.") session_id = self._session.cookies.get(self.SESSION_ID_KEY, None) if not session_id: logger.error("Bad cookie from server (missing _session_id)") raise SRPAuthNoSessionId() # make the rpc call async emit(catalog.CLIENT_SESSION_ID, session_id) self.set_session_id(session_id) logger.debug("SUCCESS LOGIN") return True def _threader(self, cb, res, *args, **kwargs): return threads.deferToThread(cb, res, *args, **kwargs) def _change_password(self, current_password, new_password): """ Changes the password for the currently logged user if the current password match. It requires to be authenticated. Might raise: SRPAuthBadUserOrPassword requests.exceptions.HTTPError :param current_password: the current password for the logged user. :type current_password: str :param new_password: the new password for the user :type new_password: str """ leap_assert(self.get_uuid() is not None) if current_password != self._password: raise SRPAuthBadUserOrPassword url = "%s/%s/users/%s.json" % (self._provider_config.get_api_uri(), self._provider_config.get_api_version(), self.get_uuid()) salt, verifier = self._srp.create_salted_verification_key( self._username.encode('utf-8'), new_password.encode('utf-8'), self._hashfun, self._ng) cookies = {self.SESSION_ID_KEY: self.get_session_id()} headers = { self.AUTHORIZATION_KEY: "Token token={0}".format(self.get_token()) } user_data = { self.USER_VERIFIER_KEY: binascii.hexlify(verifier), self.USER_SALT_KEY: binascii.hexlify(salt) } change_password = self._session.put( url, data=user_data, verify=self._provider_config.get_ca_cert_path(), cookies=cookies, timeout=REQUEST_TIMEOUT, headers=headers) # In case of non 2xx it raises HTTPError change_password.raise_for_status() self._password = new_password def change_password(self, current_password, new_password): """ Changes the password for the currently logged user if the current password match. It requires to be authenticated. :param current_password: the current password for the logged user. :type current_password: str :param new_password: the new password for the user :type new_password: str """ d = threads.deferToThread(self._change_password, current_password, new_password) return d def authenticate(self, username, password): """ Executes the whole authentication process for a user Might raise SRPAuthenticationError :param username: username for this session :type username: unicode :param password: password for this user :type password: unicode :returns: A defer on a different thread :rtype: twisted.internet.defer.Deferred """ leap_assert(self.get_session_id() is None, "Already logged in") # User credentials stored for password changing checks self._username = username self._password = password self._reset_session() d = threads.deferToThread(self._authentication_preprocessing, username=username, password=password) d.addCallback(partial(self._start_authentication, username=username)) d.addCallback(partial(self._process_challenge, username=username)) d.addCallback(self._extract_data) d.addCallback(self._verify_session) return d def logout(self): """ Logs out the current session. Expects a session_id to exists, might raise AssertionError """ logger.debug("Starting logout...") if self.get_session_id() is None: logger.debug("Already logged out") return logout_url = "%s/%s/%s/" % (self._provider_config.get_api_uri(), self._provider_config.get_api_version(), "logout") cookies = {self.SESSION_ID_KEY: self.get_session_id()} headers = { self.AUTHORIZATION_KEY: "Token token={0}".format(self.get_token()) } try: res = self._session.delete( logout_url, cookies=cookies, headers=headers, verify=self._provider_config.get_ca_cert_path(), timeout=REQUEST_TIMEOUT) except Exception as e: logger.warning("Something went wrong with the logout: %r" % (e, )) raise else: self.set_session_id(None) self.set_uuid(None) self.set_token(None) # Also reset the session self._session = self._fetcher.session() if res.status_code == 204: logger.debug("Successfully logged out.") else: logger.debug("Logout status code: %s" % res.status_code) def set_session_id(self, session_id): with self._session_id_lock: self._session_id = session_id def get_session_id(self): with self._session_id_lock: return self._session_id def set_uuid(self, uuid): with self._uuid_lock: full_uid = "%s@%s" % (self._username, self._provider_config.get_domain()) if uuid is not None: # avoid removing the uuid from settings self._settings.set_uuid(full_uid, uuid) self._uuid = uuid def get_uuid(self): with self._uuid_lock: return self._uuid def set_token(self, token): with self._token_lock: self._token = token def get_token(self): with self._token_lock: return self._token def is_authenticated(self): """ Return whether the user is authenticated or not. :rtype: bool """ user = self._srp_user if user is not None: return user.authenticated() return False
class MBOXPlumber(object): """ An class that can fix things inside a soledadbacked account. The idea is to gather in this helper different fixes for mailboxes that can be invoked when data migration in the client is needed. """ def __init__(self, userid, passwd, mdir=None): """ Initialize the plumber with all that's needed to authenticate against the provider. :param userid: user identifier, foo@bar :type userid: basestring :param passwd: the soledad passphrase :type passwd: basestring :param mdir: a path to a maildir to import :type mdir: str or None """ self.userid = userid self.passwd = passwd user, provider = userid.split('@') self.user = user self.mdir = mdir self.sol = None self._settings = Settings() provider_config_path = os.path.join(get_path_prefix(), get_provider_path(provider)) provider_config = ProviderConfig() loaded = provider_config.load(provider_config_path) if not loaded: print "could not load provider config!" return self.exit() def _init_local_soledad(self): """ Initialize local Soledad instance. """ self.uuid = self._settings.get_uuid(self.userid) if not self.uuid: print "Cannot get UUID from settings. Log in at least once." return False print "UUID: %s" % (self.uuid) secrets, localdb = get_db_paths(self.uuid) self.sol = initialize_soledad(self.uuid, self.userid, self.passwd, secrets, localdb, "/tmp", "/tmp") self.acct = IMAPAccount(self.userid, self.sol) return True # # Account repairing # def repair_account(self, *args): """ Repair mbox uids for all mboxes in this account. """ init = self._init_local_soledad() if not init: return self.exit() for mbox_name in self.acct.mailboxes: self.repair_mbox_uids(mbox_name) print "done." self.exit() def repair_mbox_uids(self, mbox_name): """ Repair indexes for a given mbox. :param mbox_name: mailbox to repair :type mbox_name: basestring """ print print "REPAIRING INDEXES FOR MAILBOX %s" % (mbox_name, ) print "----------------------------------------------" mbox = self.acct.getMailbox(mbox_name) len_mbox = mbox.getMessageCount() print "There are %s messages" % (len_mbox, ) last_ok = True if mbox.last_uid == len_mbox else False uids_iter = mbox.messages.all_msg_iter() dupes = self._has_dupes(uids_iter) if last_ok and not dupes: print "Mbox does not need repair." return # XXX CHANGE? ---- msgs = mbox.messages.get_all() for zindex, doc in enumerate(msgs): mindex = zindex + 1 old_uid = doc.content['uid'] doc.content['uid'] = mindex self.sol.put_doc(doc) if mindex != old_uid: print "%s -> %s (%s)" % (mindex, doc.content['uid'], old_uid) old_last_uid = mbox.last_uid mbox.last_uid = len_mbox print "LAST UID: %s (%s)" % (mbox.last_uid, old_last_uid) def _has_dupes(self, sequence): """ Return True if the given sequence of ints has duplicates. :param sequence: a sequence of ints :type sequence: sequence :rtype: bool """ d = defaultdict(lambda: 0) for uid in sequence: d[uid] += 1 if d[uid] != 1: return True return False # # Maildir import # def import_mail(self, mail_filename): """ Import a single mail into a mailbox. :param mbox: the Mailbox instance to save in. :type mbox: SoledadMailbox :param mail_filename: the filename to the mail file to save :type mail_filename: basestring :return: a deferred """ def saved(_): print "message added" with open(mail_filename) as f: mail_string = f.read() # uid = self._mbox.getUIDNext() # print "saving with UID: %s" % uid d = self._mbox.messages.add_msg(mail_string, notify_on_disk=True) return d def import_maildir(self, mbox_name="INBOX"): """ Import all mails in a maildir. We will process all subfolders as beloging to the same mailbox (cur, new, tmp). """ # TODO parse hierarchical subfolders into # inferior mailboxes. if not os.path.isdir(self.mdir): print "ERROR: maildir path does not exist." return init = self._init_local_soledad() if not init: return self.exit() mbox = self.acct.getMailbox(mbox_name) self._mbox = mbox len_mbox = mbox.getMessageCount() mail_files_g = flatten( map(partial(os.path.join, f), files) for f, _, files in os.walk(self.mdir)) # we only coerce the generator to give the # len, but we could skip than and inform at the end. mail_files = list(mail_files_g) print "Got %s mails to import into %s (%s)" % (len(mail_files), mbox_name, len_mbox) def all_saved(_): print "all messages imported" deferreds = [] for f_name in mail_files: deferreds.append(self.import_mail(f_name)) print "deferreds: ", deferreds d1 = defer.gatherResults(deferreds, consumeErrors=False) d1.addCallback(all_saved) d1.addCallback(self._cbExit) def _cbExit(self, ignored): return self.exit() def exit(self): from twisted.internet import reactor try: if self.sol: self.sol.close() reactor.stop() except Exception: pass return