diff options
Diffstat (limited to 'src/leap/bitmask/crypto')
-rw-r--r-- | src/leap/bitmask/crypto/srpauth.py | 956 | ||||
-rw-r--r-- | src/leap/bitmask/crypto/srpregister.py | 104 |
2 files changed, 546 insertions, 514 deletions
diff --git a/src/leap/bitmask/crypto/srpauth.py b/src/leap/bitmask/crypto/srpauth.py index c2a5f158..fe177e5a 100644 --- a/src/leap/bitmask/crypto/srpauth.py +++ b/src/leap/bitmask/crypto/srpauth.py @@ -118,475 +118,516 @@ class SRPAuthNoSessionId(SRPAuthenticationError): pass -class SRPAuth(object): +class SRPAuthImpl(object): """ - SRPAuth singleton + Implementation of the SRPAuth interface """ - class __impl(object): + 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 """ - Implementation of the SRPAuth interface + 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 - 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" + :param val: hexlified value + :type val: str - def __init__(self, provider_config, signaler=None): - """ - Constructor for SRPAuth implementation + :rtype: binary hex data + :return: unhexlified val + """ + return binascii.unhexlify(val) \ + if (len(val) % 2 == 0) else binascii.unhexlify('0' + val) - :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") + def _authentication_preprocessing(self, username, password): + """ + Generates the SRP.User to get the A SRP parameter - 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 + :param username: username to login + :type username: str + :param password: password for the username + :type password: str + """ + logger.debug("Authentication preprocessing...") - # User credentials stored for password changing checks - self._username = None - self._password = None + self._srp_user = self._srp.User(username.encode('utf-8'), + password.encode('utf-8'), + self._hashfun, self._ng) + _, A = self._srp_user.start_authentication() - 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) + self._srp_a = A - def _safe_unhexlify(self, val): - """ - Rounds the val to a multiple of 2 and returns the - unhexlified value + 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 - :param val: hexlified value - :type val: 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() - :rtype: binary hex data - :return: unhexlified val - """ - return binascii.unhexlify(val) \ - if (len(val) % 2 == 0) else binascii.unhexlify('0' + val) + content, mtime = reqhelper.get_content(init_session) - def _authentication_preprocessing(self, username, password): - """ - Generates the SRP.User to get the A SRP parameter + 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() - :param username: username to login - :type username: str - :param password: password for the username - :type password: str - """ - logger.debug("Authentication preprocessing...") + logger.error("There was a problem with authentication.") + raise SRPAuthBadStatusCode() - self._srp_user = self._srp.User(username.encode('utf-8'), - password.encode('utf-8'), - self._hashfun, self._ng) - _, A = self._srp_user.start_authentication() + json_content = json.loads(content) + salt = json_content.get("salt", None) + B = json_content.get("B", None) - self._srp_a = A + 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() - def _start_authentication(self, _, username): - """ - Sends the first request for authentication to retrieve the - salt and B parameter + return salt, B - Might raise all SRPAuthenticationError based: - SRPAuthenticationError - SRPAuthConnectionError - SRPAuthBadStatusCode - SRPAuthNoSalt - SRPAuthNoB + 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 - :param _: IGNORED, output from the previous callback (None) - :type _: IGNORED - :param username: username to login - :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) + } - :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) + 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() - auth_data = { - self.CLIENT_AUTH_KEY: binascii.hexlify(M) - } + 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: - 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() + 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) - 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() + Might raise SRPAuthenticationError based: + SRPBadDataFromServer - 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() + :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() - return json.loads(content) + self.set_uuid(uuid) + self.set_token(token) - def _extract_data(self, json_content): - """ - Extracts the necessary parameters from json_content (M2, - id, token) + if M2 is None or self.get_uuid() is None: + logger.error("Something went wrong. Content = %r" % + (json_content,)) + raise SRPAuthBadDataFromServer() - Might raise SRPAuthenticationError based: - SRPBadDataFromServer + events_signal( + proto.CLIENT_UID, content=uuid, + reqcbk=lambda req, res: None) # make the rpc call async - :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() + return M2 - self.set_uuid(uuid) - self.set_token(token) + 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 - if M2 is None or self.get_uuid() is None: - logger.error("Something went wrong. Content = %r" % - (json_content,)) - raise SRPAuthBadDataFromServer() + Might raise SRPAuthenticationError based: + SRPAuthBadDataFromServer + SRPAuthVerificationFailed - events_signal( - proto.CLIENT_UID, content=uuid, - reqcbk=lambda req, res: None) # make the rpc call async + :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() - return M2 + self._srp_user.verify_session(unhex_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 + if not self._srp_user.authenticated(): + logger.error("Auth verification failed.") + raise SRPAuthVerificationFailed() + logger.debug("Session verified.") - Might raise SRPAuthenticationError based: - SRPAuthBadDataFromServer - SRPAuthVerificationFailed + 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() - :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() + events_signal( + proto.CLIENT_SESSION_ID, content=session_id, + reqcbk=lambda req, res: None) # make the rpc call asynch - self._srp_user.verify_session(unhex_M2) + self.set_session_id(session_id) + logger.debug("SUCCESS LOGIN") + return True - if not self._srp_user.authenticated(): - logger.error("Auth verification failed.") - raise SRPAuthVerificationFailed() - logger.debug("Session verified.") + def _threader(self, cb, res, *args, **kwargs): + return threads.deferToThread(cb, res, *args, **kwargs) - 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() + 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. - events_signal( - proto.CLIENT_SESSION_ID, content=session_id, - reqcbk=lambda req, res: None) # make the rpc call async + Might raise: + SRPAuthBadUserOrPassword + requests.exceptions.HTTPError - self.set_session_id(session_id) + :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 _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. - 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 - Might raise: - SRPAuthBadUserOrPassword - requests.exceptions.HTTPError + def authenticate(self, username, password): + """ + Executes the whole authentication process for a user - :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) + Might raise SRPAuthenticationError - if current_password != self._password: - raise SRPAuthBadUserOrPassword + :param username: username for this session + :type username: unicode + :param password: password for this user + :type password: unicode - url = "%s/%s/users/%s.json" % ( - self._provider_config.get_api_uri(), - self._provider_config.get_api_version(), - self.get_uuid()) + :returns: A defer on a different thread + :rtype: twisted.internet.defer.Deferred + """ + leap_assert(self.get_session_id() is None, "Already logged in") - salt, verifier = self._srp.create_salted_verification_key( - self._username.encode('utf-8'), new_password.encode('utf-8'), - self._hashfun, self._ng) + # User credentials stored for password changing checks + self._username = username + self._password = password - 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) - } + self._reset_session() - change_password = self._session.put( - url, data=user_data, - verify=self._provider_config.get_ca_cert_path(), - cookies=cookies, - timeout=REQUEST_TIMEOUT, - headers=headers) + d = threads.deferToThread(self._authentication_preprocessing, + username=username, + password=password) + d.addCallback(partial(self._start_authentication, username=username)) - # In case of non 2xx it raises HTTPError - change_password.raise_for_status() + d.addCallback(partial(self._process_challenge, username=username)) + d.addCallback(self._extract_data) + d.addCallback(self._verify_session) + return d - self._password = new_password + def logout(self): + """ + Logs out the current session. + Expects a session_id to exists, might raise AssertionError + """ + logger.debug("Starting logout...") - 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. + if self.get_session_id() is None: + logger.debug("Already logged out") + return - :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) - d.addCallback(self._change_password_ok) - d.addErrback(self._change_password_error) + 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 _change_password_ok(self, _): - """ - Password change callback. - """ - if self._signaler is not None: - self._signaler.signal(self._signaler.srp_password_change_ok) + def set_session_id(self, session_id): + with self._session_id_lock: + self._session_id = session_id - def _change_password_error(self, failure): - """ - Password change errback. - """ - logger.debug( - "Error changing password. Failure: {0}".format(failure)) - if self._signaler is None: - return + def get_session_id(self): + with self._session_id_lock: + return self._session_id - if failure.check(SRPAuthBadUserOrPassword): - self._signaler.signal(self._signaler.srp_password_change_badpw) - else: - self._signaler.signal(self._signaler.srp_password_change_error) + 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 authenticate(self, username, password): - """ - Executes the whole authentication process for a user + def get_uuid(self): + with self._uuid_lock: + return self._uuid - Might raise SRPAuthenticationError + def set_token(self, token): + with self._token_lock: + self._token = token - :param username: username for this session - :type username: unicode - :param password: password for this user - :type password: unicode + def get_token(self): + with self._token_lock: + return self._token - :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() - - # FIXME --------------------------------------------------------- - # 1. it makes no sense to defer each callback to a thread - # 2. the decision to use threads should be at another level. - # (although it's not really needed, that was a hack around - # the gui blocks) - # it makes very hard to test this. The __impl could be - # separated and decoupled from the provider_config abstraction. - - d = threads.deferToThread(self._authentication_preprocessing, - username=username, - password=password) - - d.addCallback( - partial(self._threader, - self._start_authentication), - username=username) - d.addCallback( - partial(self._threader, - self._process_challenge), - username=username) - d.addCallback( - partial(self._threader, - self._extract_data)) - d.addCallback(partial(self._threader, - self._verify_session)) + 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 SRPAuth(object): + """ + SRPAuth singleton + """ + class __impl(SRPAuthImpl): + + def __init__(self, provider_config, signaler=None): + SRPAuthImpl.__init__(self, provider_config) + self._signaler = signaler + def authenticate(self, username, password): + d = SRPAuthImpl.authenticate(self, username, password) d.addCallback(self._authenticate_ok) d.addErrback(self._authenticate_error) return d @@ -630,82 +671,53 @@ class SRPAuth(object): self._signaler.signal(signal) - def logout(self): + 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 """ - Logs out the current session. - Expects a session_id to exists, might raise AssertionError + d = SRPAuthImpl.change_password(self, current_password, + new_password) + d.addCallback(self._change_password_ok) + d.addErrback(self._change_password_error) + return d + + def _change_password_ok(self, _): """ - logger.debug("Starting logout...") + Password change callback. + """ + if self._signaler is not None: + self._signaler.signal(self._signaler.srp_password_change_ok) - if self.get_session_id() is None: - logger.debug("Already logged out") + def _change_password_error(self, failure): + """ + Password change errback. + """ + logger.debug( + "Error changing password. Failure: {0}".format(failure)) + if self._signaler is None: return - logout_url = "%s/%s/%s/" % (self._provider_config.get_api_uri(), - self._provider_config. - get_api_version(), - "logout") + if failure.check(SRPAuthBadUserOrPassword): + self._signaler.signal(self._signaler.srp_password_change_badpw) + else: + self._signaler.signal(self._signaler.srp_password_change_error) + + def logout(self): 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,)) + SRPAuthImpl.logout(self) + if self._signaler is not None: + self._signaler.signal(self._signaler.srp_logout_ok) + except Exception: if self._signaler is not None: self._signaler.signal(self._signaler.srp_logout_error) 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.") - if self._signaler is not None: - self._signaler.signal(self._signaler.srp_logout_ok) - - 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 __instance = None diff --git a/src/leap/bitmask/crypto/srpregister.py b/src/leap/bitmask/crypto/srpregister.py index 86510de1..e3007b6c 100644 --- a/src/leap/bitmask/crypto/srpregister.py +++ b/src/leap/bitmask/crypto/srpregister.py @@ -33,40 +33,18 @@ from leap.common.check import leap_assert, leap_assert_type logger = logging.getLogger(__name__) -class SRPRegister(QtCore.QObject): - """ - Registers a user to a specific provider using SRP - """ +class SRPRegisterImpl: USER_LOGIN_KEY = 'user[login]' USER_VERIFIER_KEY = 'user[password_verifier]' USER_SALT_KEY = 'user[password_salt]' - - STATUS_OK = (200, 201) - STATUS_TAKEN = 422 STATUS_ERROR = -999 # Custom error status - def __init__(self, signaler=None, - provider_config=None, register_path="users"): - """ - Constructor - - :param signaler: Signaler object used to receive notifications - from the backend - :type signaler: Signaler - :param provider_config: provider configuration instance, - properly loaded - :type privider_config: ProviderConfig - :param register_path: webapp path for registering users - :type register_path; str - """ - QtCore.QObject.__init__(self) + def __init__(self, provider_config, register_path): leap_assert(provider_config, "Please provide a provider") leap_assert_type(provider_config, ProviderConfig) self._provider_config = provider_config - self._signaler = signaler - # **************************************************** # # Dependency injection helpers, override this for more # granular testing @@ -83,25 +61,8 @@ class SRPRegister(QtCore.QObject): self._port = "443" self._register_path = register_path - self._session = self._fetcher.session() - def _get_registration_uri(self): - """ - Returns the URI where the register request should be made for - the provider - - :rtype: str - """ - - uri = "https://%s:%s/%s/%s" % ( - self._provider, - self._port, - self._provider_config.get_api_version(), - self._register_path) - - return uri - def register_user(self, username, password): """ Registers a user with the validator based on the password provider @@ -152,7 +113,6 @@ class SRPRegister(QtCore.QObject): status_code = self.STATUS_ERROR if req is not None: status_code = req.status_code - self._emit_result(status_code) if not ok: try: @@ -165,6 +125,66 @@ class SRPRegister(QtCore.QObject): except Exception as e: logger.error("Unknown error: %r" % (e, )) + return ok, status_code + + def _get_registration_uri(self): + """ + Returns the URI where the register request should be made for + the provider + + :rtype: str + """ + + uri = "https://%s:%s/%s/%s" % ( + self._provider, + self._port, + self._provider_config.get_api_version(), + self._register_path) + + return uri + + +class SRPRegister(QtCore.QObject): + """ + Registers a user to a specific provider using SRP + """ + + STATUS_OK = (200, 201) + STATUS_TAKEN = 422 + + def __init__(self, signaler=None, + provider_config=None, register_path="users"): + """ + Constructor + + :param signaler: Signaler object used to receive notifications + from the backend + :type signaler: Signaler + :param provider_config: provider configuration instance, + properly loaded + :type privider_config: ProviderConfig + :param register_path: webapp path for registering users + :type register_path; str + """ + self._srp_register = SRPRegisterImpl(provider_config, register_path) + QtCore.QObject.__init__(self) + + self._signaler = signaler + + def register_user(self, username, password): + """ + Registers a user with the validator based on the password provider + + :param username: username to register + :type username: str + :param password: password for this username + :type password: str + + :returns: if the registration went ok or not. + :rtype: bool + """ + ok, status_code = self._srp_register.register_user(username, password) + self._emit_result(status_code) return ok def _emit_result(self, status_code): |