def test_valid_login_calls_scrypt(self, mock_scrypt_hash, mock_valid_password): Journalist.login(self.user.username, self.user_pw, 'mocked') self.assertTrue( mock_scrypt_hash.called, "Failed to call _scrypt_hash for password w/ valid length")
def test_login_with_invalid_password_doesnt_call_scrypt( self, mock_scrypt_hash): invalid_pw = 'a' * (Journalist.MAX_PASSWORD_LEN + 1) with self.assertRaises(InvalidPasswordLength): Journalist.login(self.user.username, invalid_pw, 'mocked') self.assertFalse(mock_scrypt_hash.called, "Called _scrypt_hash for password w/ invalid length")
def edit_user(user_id): user = Journalist.query.get(user_id) if request.method == 'POST': if request.form.get('username', None): new_username = request.form['username'] try: Journalist.check_username_acceptable(new_username) except InvalidUsernameException as e: flash('Invalid username: '******'error') return redirect(url_for("admin.edit_user", user_id=user_id)) if new_username == user.username: pass elif Journalist.query.filter_by( username=new_username).one_or_none(): flash(gettext( 'Username "{user}" already taken.').format( user=new_username), "error") return redirect(url_for("admin.edit_user", user_id=user_id)) else: user.username = new_username user.is_admin = bool(request.form.get('is_admin')) commit_account_changes(user) password = make_password(config) return render_template("edit_account.html", user=user, password=password)
def test_totp_reuse_protections2(self): """More granular than the preceeding test, we want to make sure the right exception is being raised in the right place.""" valid_token = self.user.totp.now() Journalist.login(self.user.username, self.user_pw, valid_token) with self.assertRaises(BadTokenException): Journalist.login(self.user.username, self.user_pw, valid_token)
def make_password(config): while True: password = crypto_util.genrandomid(7, i18n.get_language(config)) try: Journalist.check_password_acceptable(password) return password except PasswordError: continue
def _make_password(): while True: password = crypto_util.genrandomid(7) try: Journalist.check_password_acceptable(password) return password except PasswordError: continue
def test_totp_reuse_protections2(self): """More granular than the preceeding test, we want to make sure the right exception is being raised in the right place. """ valid_token = self.user.totp.now() Journalist.login(self.user.username, self.user_pw, valid_token) with self.assertRaises(BadTokenException): Journalist.login(self.user.username, self.user_pw, valid_token)
def _get_username(): while True: username = raw_input('Username: '******'Invalid username: ' + str(e)) else: return username
def login(): if request.method == "POST": try: user = Journalist.login(request.form["username"], request.form["password"], request.form["token"]) except Exception as e: app.logger.error("Login for '{}' failed: {}".format(request.form["username"], e)) login_flashed_msg = "Login failed." if isinstance(e, LoginThrottledException): login_flashed_msg += " Please wait at least 60 seconds before logging in again." else: try: user = Journalist.query.filter_by(username=request.form["username"]).one() if user.is_totp: login_flashed_msg += " Please wait for a new two-factor token before logging in again." except: pass flash(login_flashed_msg, "error") else: app.logger.info( "Successful login for '{}' with token {}".format(request.form["username"], request.form["token"]) ) # Update access metadata user.last_access = datetime.utcnow() db_session.add(user) db_session.commit() session["uid"] = user.id return redirect(url_for("index")) return render_template("login.html")
def login(): if request.method == 'POST': try: user = Journalist.login(request.form['username'], request.form['password'], request.form['token']) except Exception as e: app.logger.error("Login for '{}' failed: {}".format( request.form['username'], e)) login_flashed_msg = "Login failed." if isinstance(e, LoginThrottledException): login_flashed_msg += " Please wait at least 60 seconds before logging in again." else: try: user = Journalist.query.filter_by(username=request.form['username']).one() if user.is_totp: login_flashed_msg += " Please wait for a new two-factor token before logging in again." except: pass flash(login_flashed_msg, "error") else: app.logger.info("Successful login for '{}' with token {}".format( request.form['username'], request.form['token'])) # Update access metadata user.last_access = datetime.utcnow() db_session.add(user) db_session.commit() session['uid'] = user.id return redirect(url_for('index')) return render_template("login.html")
def test_max_password_length(self): """Creating a Journalist with a password that is greater than the maximum password length should raise an exception""" overly_long_password = '******' * (Journalist.MAX_PASSWORD_LEN + 1) with self.assertRaises(InvalidPasswordLength): temp_journalist = Journalist(username="******", password=overly_long_password)
def test_min_password_length(self): """Creating a Journalist with a password that is smaller than the minimum password length should raise an exception""" with self.assertRaises(InvalidPasswordLength): temp_journalist = Journalist( username="******", password='******')
def test_min_password_length(self): """Creating a Journalist with a password that is smaller than the minimum password length should raise an exception. This uses the magic number 7 below to get around the "diceware-like" requirement that may cause a failure before the length check. """ password = ('a ' * 7)[0:(Journalist.MIN_PASSWORD_LEN - 1)] with self.assertRaises(InvalidPasswordLength): Journalist(username="******", password=password)
def setUp(self): common.shared_setup() # Patch the two-factor verification to avoid intermittent errors patcher = mock.patch('db.Journalist.verify_token') self.addCleanup(patcher.stop) self.mock_journalist_verify_token = patcher.start() self.mock_journalist_verify_token.return_value = True # Set up test users self.user_pw = "bar" self.user = Journalist(username="******", password=self.user_pw) self.admin_user_pw = "admin" self.admin_user = Journalist(username="******", password=self.admin_user_pw, is_admin=True) db_session.add(self.user) db_session.add(self.admin_user) db_session.commit()
def setUp(self): common.shared_setup() # Patch the two-factor verification so it always succeeds patcher = patch('db.Journalist.verify_token') self.addCleanup(patcher.stop) self.mock_journalist_verify_token = patcher.start() self.mock_journalist_verify_token.return_value = True self.username = "******" self.password = "******" self.user = Journalist(username=self.username, password=self.password) db_session.add(self.user) db_session.commit() # Use a patched login function to avoid dealing with two-factor tokens # (which are being ignored here anyway) self.login = lambda username, password: \ Journalist.login(username, password, "")
def _add_user(is_admin=False): username = _get_username() print("Note: Journalist passwords are now autogenerated.") password = _make_password() print("This journalist's password is: {}".format(password)) is_hotp = _get_yubikey_usage() otp_secret = None if is_hotp: while True: otp_secret = raw_input( 'Please configure your YubiKey and enter the secret: ') if otp_secret: break try: user = Journalist(username=username, password=password, is_admin=is_admin, otp_secret=otp_secret) db_session.add(user) db_session.commit() except Exception as exc: db_session.rollback() if "UNIQUE constraint failed: journalists.username" in str(exc): print('ERROR: That username is already taken!') else: exc_type, exc_value, exc_traceback = sys.exc_info() print( repr( traceback.format_exception(exc_type, exc_value, exc_traceback))) return 1 else: print('User "{}" successfully added'.format(username)) if not otp_secret: # Print the QR code for FreeOTP/ Google Authenticator print( '\nScan the QR code below with FreeOTP or Google ' 'Authenticator:\n') uri = user.totp.provisioning_uri(username, issuer_name='SecureDrop') qr = qrcode.QRCode() qr.add_data(uri) qr.print_ascii(tty=sys.stdout.isatty()) print( '\nIf the barcode does not render correctly, try changing ' "your terminal's font (Monospace for Linux, Menlo for OS " 'X). If you are using iTerm on Mac OS X, you will need to ' 'change the "Non-ASCII Font", which is your profile\'s Text ' "settings.\n\nCan't scan the barcode? Enter following " 'shared secret ' 'manually:\n{}\n'.format(user.formatted_otp_secret)) return 0
def _journalist_logs_in(self): # Create a test user for logging in test_user_info = dict(username='******', password='******') test_user = Journalist(**test_user_info) db_session.add(test_user) db_session.commit() self._login_user(test_user_info['username'], test_user_info['password'], test_user.totp.now()) headline = self.driver.find_element_by_css_selector('span.headline') self.assertIn('Sources', headline.text)
def admin_add_user(): if request.method == 'POST': form_valid = True username = request.form['username'] if len(username) == 0: form_valid = False flash("Missing username", "error") password = request.form['password'] password_again = request.form['password_again'] if password != password_again: form_valid = False flash("Passwords didn't match", "error") is_admin = bool(request.form.get('is_admin')) if form_valid: try: otp_secret = None if request.form.get('is_hotp', False): otp_secret = request.form.get('otp_secret', '') new_user = Journalist(username=username, password=password, is_admin=is_admin, otp_secret=otp_secret) db_session.add(new_user) db_session.commit() except InvalidPasswordLength: form_valid = False flash( "Your password must be between {} and {} characters.". format(Journalist.MIN_PASSWORD_LEN, Journalist.MAX_PASSWORD_LEN), "error") except IntegrityError as e: db_session.rollback() form_valid = False if "UNIQUE constraint failed: journalists.username" in str(e): flash("That username is already in use", "error") else: flash( "An error occurred saving this user to the database." " Please check the application logs.", "error") app.logger.error("Adding user '{}' failed: {}".format( username, e)) if form_valid: return redirect( url_for('admin_new_user_two_factor', uid=new_user.id)) return render_template("admin_add_user.html")
def add_user(): form = NewUserForm() if form.validate_on_submit(): form_valid = True username = request.form['username'] password = request.form['password'] is_admin = bool(request.form.get('is_admin')) try: otp_secret = None if request.form.get('is_hotp', False): otp_secret = request.form.get('otp_secret', '') new_user = Journalist(username=username, password=password, is_admin=is_admin, otp_secret=otp_secret) db_session.add(new_user) db_session.commit() except PasswordError: flash(gettext( 'There was an error with the autogenerated password. ' 'User not created. Please try again.'), 'error') form_valid = False except InvalidUsernameException as e: form_valid = False flash('Invalid username: '******'{}' failed: {}".format( username, e)) if form_valid: return redirect(url_for('admin.new_user_two_factor', uid=new_user.id)) return render_template("admin_add_user.html", password=make_password(config), form=form)
def admin_add_user(): if request.method == 'POST': form_valid = True username = request.form['username'] if len(username) == 0: form_valid = False flash("Missing username", "error") password = request.form['password'] password_again = request.form['password_again'] if password != password_again: form_valid = False flash("Passwords didn't match", "error") is_admin = bool(request.form.get('is_admin')) if form_valid: try: otp_secret = None if request.form.get('is_hotp', False): otp_secret = request.form.get('otp_secret', '') new_user = Journalist(username=username, password=password, is_admin=is_admin, otp_secret=otp_secret) db_session.add(new_user) db_session.commit() except InvalidPasswordLength: form_valid = False flash( "Your password is too long (maximum length {} characters)". format(Journalist.MAX_PASSWORD_LEN), "error") except IntegrityError as e: form_valid = False if "username is not unique" in str(e): flash("That username is already in use", "error") else: flash("An error occurred saving this user to the database", "error") if form_valid: return redirect( url_for('admin_new_user_two_factor', uid=new_user.id)) return render_template("admin_add_user.html")
def validate_user(username, password, token, error_message=None): """ Validates the user by calling the login and handling exceptions :param username: Username :param password: Password :param token: Two-factor authentication token :param error_message: Localized error message string to use on failure :return: Journalist user object if successful, None otherwise. """ try: return Journalist.login(username, password, token) except (InvalidUsernameException, BadTokenException, WrongPasswordException, LoginThrottledException) as e: current_app.logger.error("Login for '{}' failed: {}".format( username, e)) if not error_message: error_message = gettext('Login failed.') login_flashed_msg = error_message if isinstance(e, LoginThrottledException): login_flashed_msg += " " period = Journalist._LOGIN_ATTEMPT_PERIOD # ngettext is needed although we always have period > 1 # see https://github.com/freedomofpress/securedrop/issues/2422 login_flashed_msg += ngettext( "Please wait at least {seconds} second " "before logging in again.", "Please wait at least {seconds} seconds " "before logging in again.", period).format(seconds=period) else: try: user = Journalist.query.filter_by( username=username).one() if user.is_totp: login_flashed_msg += " " login_flashed_msg += gettext( "Please wait for a new two-factor token" " before trying again.") except: pass flash(login_flashed_msg, "error") return None
def add_admin(): while True: username = raw_input("Username: "******"Sorry, that username is already in use." else: break while True: password = getpass("Password: "******"Confirm Password: "******"Your password is too long (maximum length {} characters). " "Please pick a shorter password.".format( Journalist.MAX_PASSWORD_LEN)) continue if password == password_again: break print "Passwords didn't match!" hotp_input = raw_input("Is this admin using a YubiKey [HOTP]? (y/N): ") otp_secret = None if hotp_input.lower() == "y" or hotp_input.lower() == "yes": while True: otp_secret = raw_input( "Please configure your YubiKey and enter the secret: ") if otp_secret: break try: admin = Journalist(username=username, password=password, is_admin=True, otp_secret=otp_secret) db_session.add(admin) db_session.commit() except Exception, e: if "username is not unique" in str(e): print "ERROR: That username is already taken!" else: print "ERROR: An unexpected error occurred, traceback: \n{}".format( e)
def validate_user(username, password, token, error_message=None): """ Validates the user by calling the login and handling exceptions :param username: Username :param password: Password :param token: Two-factor authentication token :param error_message: Localized error message string to use on failure :return: Journalist user object if successful, None otherwise. """ try: return Journalist.login(username, password, token) except (InvalidUsernameException, BadTokenException, WrongPasswordException, LoginThrottledException) as e: current_app.logger.error("Login for '{}' failed: {}".format( username, e)) if not error_message: error_message = gettext('Login failed.') login_flashed_msg = error_message if isinstance(e, LoginThrottledException): login_flashed_msg += " " period = Journalist._LOGIN_ATTEMPT_PERIOD # ngettext is needed although we always have period > 1 # see https://github.com/freedomofpress/securedrop/issues/2422 login_flashed_msg += ngettext( "Please wait at least {seconds} second " "before logging in again.", "Please wait at least {seconds} seconds " "before logging in again.", period).format(seconds=period) else: try: user = Journalist.query.filter_by(username=username).one() if user.is_totp: login_flashed_msg += " " login_flashed_msg += gettext( "Please wait for a new two-factor token" " before trying again.") except: pass flash(login_flashed_msg, "error") return None
def setUp(self): common.shared_setup() # Patch the two-factor verification so it always succeeds patcher = patch('db.Journalist.verify_token') self.addCleanup(patcher.stop) self.mock_journalist_verify_token = patcher.start() self.mock_journalist_verify_token.return_value = True self.username = "******" self.password = "******" self.user = Journalist( username=self.username, password=self.password) db_session.add(self.user) db_session.commit() # Use a patched login function to avoid dealing with two-factor tokens # (which are being ignored here anyway) self.login = lambda username, password: \ Journalist.login(username, password, "")
def setUp(self): utils.env.setup() self.source_app = source.app.test_client() self.journalist_app = journalist.app.test_client() self.gpg = gnupg.GPG(homedir=config.GPG_KEY_DIR) # Patch the two-factor verification to avoid intermittent errors patcher = mock.patch('db.Journalist.verify_token') self.addCleanup(patcher.stop) self.mock_journalist_verify_token = patcher.start() self.mock_journalist_verify_token.return_value = True # Add a test user to the journalist interface and log them in # print Journalist.query.all() self.user_pw = "longpassword" self.user = Journalist(username="******", password=self.user_pw) db_session.add(self.user) db_session.commit() self._login_user()
def add_admin(): while True: username = raw_input("Username: "******"Sorry, that username is already in use." else: break while True: password = getpass("Password: "******"Confirm Password: "******"Passwords didn't match!" hotp_input = raw_input("Is this admin using a YubiKey [HOTP]? (y/N): ") otp_secret = None if hotp_input.lower() == "y" or hotp_input.lower() == "yes": while True: otp_secret = raw_input( "Please configure your YubiKey and enter the secret: ") if otp_secret: break admin = Journalist(username=username, password=password, is_admin=True, otp_secret=otp_secret) try: db_session.add(admin) db_session.commit() except Exception, e: if "username is not unique" in str(e): print "ERROR: That username is already taken!" else: print "ERROR: An unknown error occurred, traceback:" print e
def _admin_logs_in(self): # Create a test admin user for logging in admin_user_info = dict(username='******', password='******', is_admin=True) admin_user = Journalist(**admin_user_info) db_session.add(admin_user) db_session.commit() # Stash the admin user on self so we can use it in later tests self.admin_user = admin_user_info self.admin_user['orm_obj'] = admin_user self._login_user(admin_user_info['username'], admin_user_info['password'], admin_user.totp.now()) # Admin user should log in to the same interface as a normal user, # since there may be users who wish to be both journalists and admins. headline = self.driver.find_element_by_css_selector('span.headline') self.assertIn('Sources', headline.text) # Admin user should have a link that take them to the admin page links = self.driver.find_elements_by_tag_name('a') self.assertIn('Admin', [el.text for el in links])
def _add_user(is_admin=False): # pragma: no cover while True: username = raw_input('Username: '******'Password: '******'Confirm Password: '******'Your password is too long (maximum length {} characters). ' 'Please pick a shorter ' 'password.'.format(Journalist.MAX_PASSWORD_LEN)) continue if len(password) < Journalist.MIN_PASSWORD_LEN: print('Error: Password needs to be at least {} characters.'.format( Journalist.MIN_PASSWORD_LEN )) continue if password == password_again: break print("Passwords didn't match!") hotp_input = raw_input('Will this user be using a YubiKey [HOTP]? (y/N): ') otp_secret = None if hotp_input.lower() in ('y', 'yes'): while True: otp_secret = raw_input( 'Please configure your YubiKey and enter the secret: ') if otp_secret: break try: user = Journalist(username=username, password=password, is_admin=is_admin, otp_secret=otp_secret) db_session.add(user) db_session.commit() except Exception as exc: db_session.rollback() if "UNIQUE constraint failed: journalists.username" in str(exc): print('ERROR: That username is already taken!') else: exc_type, exc_value, exc_traceback = sys.exc_info() print(repr(traceback.format_exception(exc_type, exc_value, exc_traceback))) return 1 else: print('User "{}" successfully added'.format(username)) if not otp_secret: # Print the QR code for FreeOTP/ Google Authenticator print('\nScan the QR code below with FreeOTP or Google ' 'Authenticator:\n') uri = user.totp.provisioning_uri(username, issuer_name='SecureDrop') qr = qrcode.QRCode() qr.add_data(uri) qr.print_ascii(tty=sys.stdout.isatty()) print('\nIf the barcode does not render correctly, try changing ' "your terminal's font (Monospace for Linux, Menlo for OS " 'X). If you are using iTerm on Mac OS X, you will need to ' 'change the "Non-ASCII Font", which is your profile\'s Text ' "settings.\n\nCan't scan the barcode? Enter following " 'shared secret ' 'manually:\n{}\n'.format(user.formatted_otp_secret)) return 0
def test_throttle_login(self): journalist, _ = db_helper.init_journalist() for _ in range(Journalist._MAX_LOGIN_ATTEMPTS_PER_PERIOD): Journalist.throttle_login(journalist) with self.assertRaises(LoginThrottledException): Journalist.throttle_login(journalist)