summaryrefslogtreecommitdiff
path: root/src/leap/bitmask/crypto/srpauth.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/leap/bitmask/crypto/srpauth.py')
-rw-r--r--src/leap/bitmask/crypto/srpauth.py606
1 files changed, 606 insertions, 0 deletions
diff --git a/src/leap/bitmask/crypto/srpauth.py b/src/leap/bitmask/crypto/srpauth.py
new file mode 100644
index 00000000..fc0533fc
--- /dev/null
+++ b/src/leap/bitmask/crypto/srpauth.py
@@ -0,0 +1,606 @@
+# -*- coding: utf-8 -*-
+# srpauth.py
+# Copyright (C) 2013 LEAP
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import binascii
+import logging
+
+import requests
+import srp
+import json
+
+#this error is raised from requests
+from simplejson.decoder import JSONDecodeError
+from functools import partial
+
+from PySide import QtCore
+from twisted.internet import threads
+
+from leap.common.check import leap_assert
+from leap.util.constants import REQUEST_TIMEOUT
+from leap.util import request_helpers as reqhelper
+from leap.common.events import signal as events_signal
+from leap.common.events import events_pb2 as proto
+
+logger = logging.getLogger(__name__)
+
+
+class SRPAuthenticationError(Exception):
+ """
+ Exception raised for authentication errors
+ """
+ pass
+
+
+class SRPAuthConnectionError(SRPAuthenticationError):
+ """
+ Exception raised when there's a connection error
+ """
+ pass
+
+
+class SRPAuthUnknownUser(SRPAuthenticationError):
+ """
+ Exception raised when trying to authenticate an unknown user
+ """
+ pass
+
+
+class SRPAuthBadStatusCode(SRPAuthenticationError):
+ """
+ Exception raised when we received an unknown bad status code
+ """
+ pass
+
+
+class SRPAuthNoSalt(SRPAuthenticationError):
+ """
+ Exception raised when we don't receive the salt param at a
+ specific point in the auth process
+ """
+ pass
+
+
+class SRPAuthNoB(SRPAuthenticationError):
+ """
+ Exception raised when we don't receive the B param at a specific
+ point in the auth process
+ """
+ pass
+
+
+class SRPAuthBadDataFromServer(SRPAuthenticationError):
+ """
+ Generic exception when we receive bad data from the server.
+ """
+ pass
+
+
+class SRPAuthJSONDecodeError(SRPAuthenticationError):
+ """
+ Exception raised when there's a problem decoding the JSON content
+ parsed as received from th e server.
+ """
+ pass
+
+
+class SRPAuthBadPassword(SRPAuthenticationError):
+ """
+ Exception raised when the user provided a bad password to auth.
+ """
+ pass
+
+
+class SRPAuthVerificationFailed(SRPAuthenticationError):
+ """
+ Exception raised when we can't verify the SRP data received from
+ the server.
+ """
+ pass
+
+
+class SRPAuthNoSessionId(SRPAuthenticationError):
+ """
+ Exception raised when we don't receive a session id from the
+ server.
+ """
+ pass
+
+
+class SRPAuth(QtCore.QObject):
+ """
+ SRPAuth singleton
+ """
+
+ class __impl(QtCore.QObject):
+ """
+ Implementation of the SRPAuth interface
+ """
+
+ LOGIN_KEY = "login"
+ A_KEY = "A"
+ CLIENT_AUTH_KEY = "client_auth"
+ SESSION_ID_KEY = "_session_id"
+
+ def __init__(self, provider_config):
+ """
+ Constructor for SRPAuth implementation
+
+ :param server: Server to which we will authenticate
+ :type server: str
+ """
+ QtCore.QObject.__init__(self)
+
+ leap_assert(provider_config,
+ "We need a provider config to authenticate")
+
+ self._provider_config = provider_config
+
+ # **************************************************** #
+ # 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._session = self._fetcher.session()
+ self._session_id = None
+ self._session_id_lock = QtCore.QMutex()
+ self._uid = None
+ self._uid_lock = QtCore.QMutex()
+ self._token = None
+ self._token_lock = QtCore.QMutex()
+
+ self._srp_user = None
+ self._srp_a = None
+
+ 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,
+ password,
+ 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
+ SRPAuthUnknownUser
+ 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")
+ init_session = self._session.post(sessions_url,
+ data=auth_data,
+ verify=self._provider_config.
+ get_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): %r" %
+ (e,))
+ raise SRPAuthConnectionError("Could not establish a "
+ "connection")
+ except Exception as e:
+ logger.error("Unknown error: %r" % (e,))
+ raise SRPAuthenticationError("Unknown error: %r" %
+ (e,))
+
+ 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:
+ raise SRPAuthUnknownUser(self.tr("Unknown user"))
+
+ raise SRPAuthBadStatusCode(self.tr("There was a problem with"
+ " authentication"))
+
+ json_content = json.loads(content)
+ salt = json_content.get("salt", None)
+ B = json_content.get("B", None)
+
+ if salt is None:
+ logger.error("No salt parameter sent")
+ raise SRPAuthNoSalt(self.tr("The server did not send "
+ "the salt parameter"))
+ if B is None:
+ logger.error("No B parameter sent")
+ raise SRPAuthNoB(self.tr("The server did not send "
+ "the B parameter"))
+
+ 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
+ SRPAuthBadPassword
+
+ :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(
+ self.tr("The data sent from the server had errors"))
+ 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(self.tr("Could not connect to "
+ "the server"))
+
+ try:
+ content, mtime = reqhelper.get_content(auth_result)
+ except JSONDecodeError:
+ raise SRPAuthJSONDecodeError("Bad JSON content in auth result")
+
+ 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 SRPAuthBadPassword(self.tr("Wrong password"))
+
+ 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(self.tr("Unknown error (%s)") %
+ (auth_result.status_code,))
+
+ 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)
+ uid = json_content.get("id", None)
+ token = json_content.get("token", None)
+ except Exception as e:
+ logger.error(e)
+ raise SRPAuthBadDataFromServer("Something went wrong with the "
+ "login")
+
+ self.set_uid(uid)
+ self.set_token(token)
+
+ if M2 is None or self.get_uid() is None:
+ logger.error("Something went wrong. Content = %r" %
+ (json_content,))
+ raise SRPAuthBadDataFromServer(self.tr("Problem getting data "
+ "from server"))
+
+ events_signal(
+ proto.CLIENT_UID, content=uid,
+ reqcbk=lambda req, res: None) # 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.tr("Bad data from server"))
+
+ self._srp_user.verify_session(unhex_M2)
+
+ if not self._srp_user.authenticated():
+ logger.error("Auth verification failed")
+ raise SRPAuthVerificationFailed(self.tr("Auth verification "
+ "failed"))
+ 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(self.tr("Session cookie "
+ "verification "
+ "failed"))
+
+ events_signal(
+ proto.CLIENT_SESSION_ID, content=session_id,
+ reqcbk=lambda req, res: None) # make the rpc call async
+
+ self.set_session_id(session_id)
+
+ def _threader(self, cb, res, *args, **kwargs):
+ return threads.deferToThread(cb, res, *args, **kwargs)
+
+ def authenticate(self, username, password):
+ """
+ Executes the whole authentication process for a user
+
+ Might raise SRPAuthenticationError
+
+ :param username: username for this session
+ :type username: str
+ :param password: password for this user
+ :type password: str
+
+ :returns: A defer on a different thread
+ :rtype: twisted.internet.defer.Deferred
+ """
+ leap_assert(self.get_session_id() is None, "Already logged in")
+
+ 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))
+
+ return d
+
+ def logout(self):
+ """
+ Logs out the current session.
+ Expects a session_id to exists, might raise AssertionError
+ """
+ logger.debug("Starting logout...")
+
+ leap_assert(self.get_session_id(),
+ "Cannot logout an unexisting session")
+
+ logout_url = "%s/%s/%s/" % (self._provider_config.get_api_uri(),
+ self._provider_config.
+ get_api_version(),
+ "sessions")
+ 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,))
+
+ self.set_session_id(None)
+ self.set_uid(None)
+ # Also reset the session
+ self._session = self._fetcher.session()
+ logger.debug("Successfully logged out.")
+
+ def set_session_id(self, session_id):
+ QtCore.QMutexLocker(self._session_id_lock)
+ self._session_id = session_id
+
+ def get_session_id(self):
+ QtCore.QMutexLocker(self._session_id_lock)
+ return self._session_id
+
+ def set_uid(self, uid):
+ QtCore.QMutexLocker(self._uid_lock)
+ self._uid = uid
+
+ def get_uid(self):
+ QtCore.QMutexLocker(self._uid_lock)
+ return self._uid
+
+ def set_token(self, token):
+ QtCore.QMutexLocker(self._token_lock)
+ self._token = token
+
+ def get_token(self):
+ QtCore.QMutexLocker(self._token_lock)
+ return self._token
+
+ __instance = None
+
+ authentication_finished = QtCore.Signal(bool, str)
+ logout_finished = QtCore.Signal(bool, str)
+
+ def __init__(self, provider_config):
+ """
+ Creates a singleton instance if needed
+ """
+ QtCore.QObject.__init__(self)
+
+ # Check whether we already have an instance
+ if SRPAuth.__instance is None:
+ # Create and remember instance
+ SRPAuth.__instance = SRPAuth.__impl(provider_config)
+
+ # Store instance reference as the only member in the handle
+ self.__dict__['_SRPAuth__instance'] = SRPAuth.__instance
+
+ def authenticate(self, username, password):
+ """
+ Executes the whole authentication process for a user
+
+ Might raise SRPAuthenticationError based
+
+ :param username: username for this session
+ :type username: str
+ :param password: password for this user
+ :type password: str
+ """
+
+ d = self.__instance.authenticate(username, password)
+ d.addCallback(self._gui_notify)
+ d.addErrback(self._errback)
+ return d
+
+ def _gui_notify(self, _):
+ """
+ Callback that notifies the UI with the proper signal.
+
+ :param _: IGNORED, output from the previous callback (None)
+ :type _: IGNORED
+ """
+ logger.debug("Successful login!")
+ self.authentication_finished.emit(True, self.tr("Succeeded"))
+
+ def _errback(self, failure):
+ """
+ General errback for the whole login process. Will notify the
+ UI with the proper signal.
+
+ :param failure: Failure object captured from a callback.
+ :type failure: twisted.python.failure.Failure
+ """
+ logger.error("Error logging in %s" % (failure,))
+ self.authentication_finished.emit(False, "%s" % (failure.value,))
+ failure.trap(Exception)
+
+ def get_session_id(self):
+ return self.__instance.get_session_id()
+
+ def get_uid(self):
+ return self.__instance.get_uid()
+
+ def get_token(self):
+ return self.__instance.get_token()
+
+ def logout(self):
+ """
+ Logs out the current session.
+ Expects a session_id to exists, might raise AssertionError
+ """
+ try:
+ self.__instance.logout()
+ self.logout_finished.emit(True, self.tr("Succeeded"))
+ return True
+ except Exception as e:
+ self.logout_finished.emit(False, "%s" % (e,))
+ return False