def post(self): now = datetime.now() remote_ip = self.request.remote_ip with session_scope() as session: last = query.get_last_login_attempt(session, remote_ip) if last is None: last = LoginAttempt(None, remote_ip) persist(session, last) else: if (now - last.timestamp) < timedelta(seconds=options.mac_update_interval): LOG.warning("Too frequent attempts to update, remote IP address is %s", remote_ip) raise HTTPError(403, "Too frequent") else: last.timestamp = now persist(session, last) try: password = self.get_argument("password") macs = self.get_argument("macs") except MissingArgumentError: LOG.warning("MAC update received malformed parameters: %s", self.request.arguments) raise HTTPError(400, "Bad parameters list") if not secure_compare(password, options.mac_update_password): LOG.warning("Client provided wrong password for MAC update!") raise HTTPError(403, "Wrong password") LOG.info("Authorized request to update list of checked-in users from IP address %s", remote_ip) macs = json.loads(macs) with session_scope() as session: names = session.\ query(distinct(User.name)).\ filter(User.userid == MACToUser.userid).\ filter(MACToUser.mac_hash .in_ (macs)).\ all() MACUpdateHandler.ROSTER = [n[0] for n in names] LOG.debug("Updated list of checked in users: %s", MACUpdateHandler.ROSTER)
def verify(session, username, supplied_password, ip_address, has_captcha, recaptcha_challenge, recaptcha_response): """Verify user credentials. If the username exists, then the supplied password is hashed and compared with the stored hash. Otherwise, an hash is calculated and discarded. An hash is calculated regardless of the existence of the username, so that the response time is approximately the same whether the username exists or not, mitigating a timing attack to reveal valid usernames. In order to mitigate DoS/bruteforce attacks, two temporal limitations are enforced: 1. Max 1 failed login attempt per IP address each second (regardless of username) 2. Max 1 failed login attempt per each username per IP address each `min_log_retry` seconds (see bitsd.properties), whether the username exists or not. A DoS protection is necessary because password hashing is an expensive operation. """ if has_captcha: solved_captcha = ReCaptcha.is_solution_correct(recaptcha_response, recaptcha_challenge, ip_address) # Exit immediately if wrong answer if not solved_captcha: return False else: solved_captcha = False # Save "now" so that the two timestamp checks are referred to the same instant now = datetime.now() def detect_dos(attempt, timeout): if solved_captcha: return False # Otherwise, check timing if attempt is not None: too_quick = (now - attempt.timestamp) < timeout if too_quick: log_last_login_attempt(session, ip_address, username) return True else: # Clean up if no more relevant session.delete(attempt) return False last_attempt_for_ip = get_last_login_attempt(session, ip_address) last_attempt_for_ip_and_username = get_last_login_attempt(session, ip_address, username) if detect_dos(last_attempt_for_ip, timedelta(seconds=1)): raise DoSError("Too frequent requests from {}".format(ip_address)) if detect_dos(last_attempt_for_ip_and_username, timedelta(seconds=options.min_login_retry)): raise DoSError("Too frequent attempts from {} for username {}".format(ip_address, username)) user = get_user(session, username) if user is None: LOG.warn("Failed attempt for non existent user %r", username) # Calculate hash anyway (see docs for the explanation) Hasher.encrypt(supplied_password) log_last_login_attempt(session, ip_address, username) return False else: valid = Hasher.verify(supplied_password, user.password) if not valid: log_last_login_attempt(session, ip_address, username) return valid