Example #1
0
def test_throttle_login(journalist_app, test_journo):
    with journalist_app.app_context():
        journalist = test_journo['journalist']
        for _ in range(Journalist._MAX_LOGIN_ATTEMPTS_PER_PERIOD):
            Journalist.throttle_login(journalist)
        with pytest.raises(LoginThrottledException):
            Journalist.throttle_login(journalist)
Example #2
0
def set_diceware_password(user: Journalist, password: Optional[str]) -> bool:
    try:
        user.set_password(password)
    except PasswordError:
        flash(
            gettext(
                'The password you submitted is invalid. Password not changed.'
            ), 'error')
        return False

    try:
        db.session.commit()
    except Exception:
        flash(
            gettext(
                'There was an error, and the new password might not have been '
                'saved correctly. To prevent you from getting locked '
                'out of your account, you should reset your password again.'),
            'error')
        current_app.logger.error('Failed to update a valid password.')
        return False

    # using Markup so the HTML isn't escaped
    flash(
        Markup("<p>" +
               gettext("Password updated. Don't forget to "
                       "save it in your KeePassX database. New password:") +
               ' <span><code>{}</code></span></p>'.format(password)),
        'success')
    return True
Example #3
0
    def new_journalist(self):
        # Make a diceware-like password
        pw = ' '.join([
            random_chars(3, nullable=False, chars=DICEWARE_SAFE_CHARS)
            for _ in range(7)
        ])
        journalist = Journalist(username=random_chars(random.randint(3, 32),
                                                      nullable=False),
                                password=pw,
                                is_admin=random_bool())
        if random_bool():
            # to add legacy passwords back in
            journalist.passphrase_hash = None
            journalist.pw_salt = random_chars(32,
                                              nullable=False).encode('utf-8')
            journalist.pw_hash = random_chars(64,
                                              nullable=False).encode('utf-8')

        journalist.is_admin = bool_or_none()

        journalist.is_totp = bool_or_none()
        journalist.hotp_counter = (random.randint(-1000, 1000)
                                   if random_bool() else None)
        journalist.created_on = random_datetime(nullable=True)
        journalist.last_access = random_datetime(nullable=True)

        db.session.add(journalist)
        db.session.flush()
        self.journalists.append(journalist.id)
Example #4
0
def validate_hotp_secret(user: Journalist, otp_secret: str) -> bool:
    """
    Validates and sets the HOTP provided by a user
    :param user: the change is for this instance of the User object
    :param otp_secret: the new HOTP secret
    :return: True if it validates, False if it does not
    """
    try:
        user.set_hotp_secret(otp_secret)
    except (binascii.Error, TypeError) as e:
        if "Non-hexadecimal digit found" in str(e):
            flash(
                gettext("Invalid secret format: "
                        "please only submit letters A-F and numbers 0-9."),
                "error")
            return False
        elif "Odd-length string" in str(e):
            flash(
                gettext("Invalid secret format: "
                        "odd-length secret. Did you mistype the secret?"),
                "error")
            return False
        else:
            flash(
                gettext("An unexpected error occurred! "
                        "Please inform your admin."), "error")
            current_app.logger.error(
                "set_hotp_secret '{}' (id {}) failed: {}".format(
                    otp_secret, user.id, e))
            return False
    return True
Example #5
0
def _make_password():
    while True:
        password = current_app.crypto_util.genrandomid(7)
        try:
            Journalist.check_password_acceptable(password)
            return password
        except PasswordError:
            continue
Example #6
0
def set_name(user: Journalist, first_name: Optional[str],
             last_name: Optional[str]) -> None:
    try:
        user.set_name(first_name, last_name)
        db.session.commit()
        flash(gettext('Name updated.'), "success")
    except FirstOrLastNameError as e:
        flash(gettext('Name not updated: {}'.format(e)), "error")
Example #7
0
def _get_username() -> str:
    while True:
        username = obtain_input("Username: "******"Invalid username: " + str(e))
        else:
            return username
Example #8
0
def make_password(config: SDConfig) -> str:
    while True:
        password = current_app.crypto_util.genrandomid(
            7, i18n.get_language(config))
        try:
            Journalist.check_password_acceptable(password)
            return password
        except PasswordError:
            continue
Example #9
0
def _get_last_name() -> Optional[str]:
    while True:
        last_name = obtain_input("Last name: ")
        if not last_name:
            return None
        try:
            Journalist.check_name_acceptable(last_name)
            return last_name
        except FirstOrLastNameError as e:
            print("Invalid name: " + str(e))
Example #10
0
def _get_last_name():
    while True:
        last_name = obtain_input('Last name: ')
        if not last_name:
            return None
        try:
            Journalist.check_name_acceptable(last_name)
            return last_name
        except FirstOrLastNameError as e:
            print('Invalid name: ' + str(e))
Example #11
0
def _get_first_name() -> Optional[str]:
    while True:
        first_name = obtain_input('First name: ')
        if not first_name:
            return None
        try:
            Journalist.check_name_acceptable(first_name)
            return first_name
        except FirstOrLastNameError as e:
            print('Invalid name: ' + str(e))
Example #12
0
def test_totp_reuse_protections3(journalist_app, test_journo, hardening):
    """We want to ensure that padding has no effect on token reuse verification."""

    with totp_window():
        token = TOTP(test_journo["otp_secret"]).now()

        with journalist_app.app_context():
            Journalist.login(test_journo["username"], test_journo["password"],
                             token)
            with pytest.raises(BadTokenException):
                Journalist.login(test_journo["username"],
                                 test_journo["password"], token + " ")
Example #13
0
def add_test_user(username: str, password: str, otp_secret: str, is_admin: bool = False,
                  first_name: str = "", last_name: str = "") -> Journalist:
    user = Journalist(username=username,
                      password=password,
                      is_admin=is_admin,
                      first_name=first_name,
                      last_name=last_name)
    user.otp_secret = otp_secret
    db.session.add(user)
    db.session.commit()
    print('Test user successfully added: '
          'username={}, password={}, otp_secret={}, is_admin={}'
          ''.format(username, password, otp_secret, is_admin))
    return user
def add_test_user(username, password, otp_secret, is_admin=False):
    try:
        user = Journalist(username=username,
                          password=password,
                          is_admin=is_admin)
        user.otp_secret = otp_secret
        db.session.add(user)
        db.session.commit()
        print('Test user successfully added: '
              'username={}, password={}, otp_secret={}, is_admin={}'
              ''.format(username, password, otp_secret, is_admin))
    except IntegrityError:
        print("Test user already added")
        db.session.rollback()
Example #15
0
def test_totp_reuse_protections2(journalist_app, test_journo, hardening):
    """More granular than the preceeding test, we want to make sure the right
    exception is being raised in the right place.
    """

    with totp_window():
        token = TOTP(test_journo["otp_secret"]).now()

        with journalist_app.app_context():
            Journalist.login(test_journo["username"], test_journo["password"],
                             token)
            with pytest.raises(BadTokenException):
                Journalist.login(test_journo["username"],
                                 test_journo["password"], token)
def test_valid_user_can_get_an_api_token(journalist_app, test_journo):
    with journalist_app.test_client() as app:
        valid_token = TOTP(test_journo["otp_secret"]).now()
        response = app.post(
            url_for("api.get_token"),
            data=json.dumps(
                {
                    "username": test_journo["username"],
                    "passphrase": test_journo["password"],
                    "one_time_code": valid_token,
                }
            ),
            headers=get_api_headers(),
        )

        assert response.json["journalist_uuid"] == test_journo["uuid"]
        assert (
            isinstance(
                Journalist.validate_api_token_and_get_user(response.json["token"]), Journalist
            )
            is True
        )
        assert response.status_code == 200
        assert response.json["journalist_first_name"] == test_journo["first_name"]
        assert response.json["journalist_last_name"] == test_journo["last_name"]

        assert_valid_timestamp(response.json["expiration"])
Example #17
0
def get_user_object(request):
    """Helper function to use in token_required views that need a user
    object
    """
    auth_token = request.headers.get('Authorization').split(" ")[1]
    user = Journalist.validate_api_token_and_get_user(auth_token)
    return user
Example #18
0
def validate_user(
    username: str,
    password: Optional[str],
    token: Optional[str],
    error_message: Optional[str] = None,
) -> Optional[Journalist]:
    """
    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,
            InvalidOTPSecretException,
            BadTokenException,
            WrongPasswordException,
            LoginThrottledException,
            InvalidPasswordLength,
    ) as e:
        current_app.logger.error("Login for '{}' failed: {}".format(
            username, e))
        login_flashed_msg = error_message if error_message else gettext(
            "Login failed.")

        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 {num} second before logging in again.",
                "Please wait at least {num} seconds before logging in again.",
                period,
            ).format(num=period)
        elif isinstance(e, InvalidOTPSecretException):
            login_flashed_msg += " "
            login_flashed_msg += gettext(
                "Your 2FA details are invalid"
                " - please contact an administrator to reset them.")
        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 code from your two-factor mobile"
                        " app or security key before trying again.")
            except Exception:
                pass

        flash(login_flashed_msg, "error")
        return None
Example #19
0
    def add_user() -> Union[str, werkzeug.Response]:
        form = NewUserForm()
        if form.validate_on_submit():
            form_valid = True
            username = request.form['username']
            first_name = request.form['first_name']
            last_name = request.form['last_name']
            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,
                                      first_name=first_name,
                                      last_name=last_name,
                                      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: '******'Username "{user}" already taken.'.format(
                            user=username)), "error")
                else:
                    flash(
                        gettext("An error occurred saving this user"
                                " to the database."
                                " Please inform your admin."), "error")
                    current_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))

        password = PassphraseGenerator.get_default().generate_passphrase(
            preferred_language=g.localeinfo.language)
        return render_template("admin_add_user.html",
                               password=password,
                               form=form)
Example #20
0
def register():
    form = RegistrationForm(meta={'csrf': False})
    if form.validate_on_submit():
        try:
            user = Journalist(name=form.username.data,
                              email=form.email.data,
                              surname=form.user_surname.data)
            app.logger.info(f'try register user "{form.username.data}" '
                            f'"{form.user_surname.data}" "{form.email.data}"')
            user.set_password(form.password.data)
            db.session.add(user)
            db.session.commit()

        except Exception as e:
            flash("Please enter a valid registration data")
            app.logger.error(f'registration failed: {e}')
        else:
            flash("You successfully register!")
            app.logger.info('registration successful')
    return render_template('register.html', title='Register', form=form)
Example #21
0
def validate_hotp_secret(user: Journalist, otp_secret: str) -> bool:
    """
    Validates and sets the HOTP provided by a user
    :param user: the change is for this instance of the User object
    :param otp_secret: the new HOTP secret
    :return: True if it validates, False if it does not
    """
    strip_whitespace = otp_secret.replace(" ", "")
    secret_length = len(strip_whitespace)

    if secret_length != HOTP_SECRET_LENGTH:
        flash(
            ngettext(
                "HOTP secrets are 40 characters long - you have entered {num}.",
                "HOTP secrets are 40 characters long - you have entered {num}.",
                secret_length,
            ).format(num=secret_length),
            "error",
        )
        return False

    try:
        user.set_hotp_secret(otp_secret)
    except (binascii.Error, TypeError) as e:
        if "Non-hexadecimal digit found" in str(e):
            flash(
                gettext("Invalid HOTP secret format: "
                        "please only submit letters A-F and numbers 0-9."),
                "error",
            )
            return False
        else:
            flash(
                gettext("An unexpected error occurred! "
                        "Please inform your admin."), "error")
            current_app.logger.error(
                "set_hotp_secret '{}' (id {}) failed: {}".format(
                    otp_secret, user.id, e))
            return False
    return True
Example #22
0
    def edit_user(user_id: int) -> Union[str, werkzeug.Response]:
        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(
                        gettext("Invalid username: {message}").format(
                            message=e), "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 "{username}" already taken.').format(
                            username=new_username),
                        "error",
                    )
                    return redirect(url_for("admin.edit_user",
                                            user_id=user_id))
                else:
                    user.username = new_username

            try:
                first_name = request.form["first_name"]
                Journalist.check_name_acceptable(first_name)
                user.first_name = first_name
            except FirstOrLastNameError as e:
                # Translators: Here, "{message}" explains the problem with the name.
                flash(
                    gettext("Name not updated: {message}").format(message=e),
                    "error")
                return redirect(url_for("admin.edit_user", user_id=user_id))

            try:
                last_name = request.form["last_name"]
                Journalist.check_name_acceptable(last_name)
                user.last_name = last_name
            except FirstOrLastNameError as e:
                flash(
                    gettext("Name not updated: {message}").format(message=e),
                    "error")
                return redirect(url_for("admin.edit_user", user_id=user_id))

            user.is_admin = bool(request.form.get("is_admin"))

            commit_account_changes(user)

        password = PassphraseGenerator.get_default().generate_passphrase(
            preferred_language=g.localeinfo.language)
        return render_template("edit_account.html",
                               user=user,
                               password=password)
Example #23
0
def cleanup_expired_revoked_tokens() -> None:
    """Remove tokens that have now expired from the revoked token table."""

    revoked_tokens = db.session.query(RevokedToken).all()

    for revoked_token in revoked_tokens:
        if Journalist.validate_token_is_not_expired_or_invalid(
                revoked_token.token):
            pass  # The token has not expired, we must keep in the revoked token table.
        else:
            # The token is no longer valid, remove from the revoked token table.
            db.session.delete(revoked_token)

    db.session.commit()
Example #24
0
def set_diceware_password(user: Journalist, password: Optional[str]) -> bool:
    try:
        # nosemgrep: python.django.security.audit.unvalidated-password.unvalidated-password
        user.set_password(password)
    except PasswordError:
        flash(
            gettext(
                "The password you submitted is invalid. Password not changed."
            ), "error")
        return False

    try:
        db.session.commit()
    except Exception:
        flash(
            gettext(
                "There was an error, and the new password might not have been "
                "saved correctly. To prevent you from getting locked "
                "out of your account, you should reset your password again."),
            "error",
        )
        current_app.logger.error("Failed to update a valid password.")
        return False

    # using Markup so the HTML isn't escaped
    flash(
        Markup("<p>{message} <span><code>{password}</code></span></p>".format(
            message=Markup.escape(
                gettext(
                    "Password updated. Don't forget to save it in your KeePassX database. "
                    "New password:"******"" if password is None else password),
        )),
        "success",
    )
    return True
Example #25
0
    def decorated_function(*args, **kwargs):
        try:
            auth_header = request.headers['Authorization']
        except KeyError:
            return abort(403, 'API token not found in Authorization header.')

        if auth_header:
            split = auth_header.split(" ")
            if len(split) != 2 or split[0] != 'Token':
                abort(403, 'Malformed authorization header.')
            auth_token = split[1]
        else:
            auth_token = ''
        if not Journalist.validate_api_token_and_get_user(auth_token):
            return abort(403, 'API token is invalid or expired.')
        return f(*args, **kwargs)
Example #26
0
def test_valid_user_can_get_an_api_token(journalist_app, test_journo):
    with journalist_app.test_client() as app:
        valid_token = TOTP(test_journo['otp_secret']).now()
        response = app.post(url_for('api.get_token'),
                            data=json.dumps(
                                {'username': test_journo['username'],
                                 'passphrase': test_journo['password'],
                                 'one_time_code': valid_token}),
                            headers=get_api_headers())

        assert response.json['journalist_uuid'] == test_journo['uuid']
        assert isinstance(Journalist.validate_api_token_and_get_user(
            response.json['token']), Journalist) is True
        assert response.status_code == 200
        assert response.json['journalist_first_name'] == test_journo['first_name']
        assert response.json['journalist_last_name'] == test_journo['last_name']
Example #27
0
    def get_token() -> Tuple[flask.Response, int]:
        creds = json.loads(request.data.decode("utf-8"))

        username = creds.get("username", None)
        passphrase = creds.get("passphrase", None)
        one_time_code = creds.get("one_time_code", None)

        if username is None:
            abort(400, "username field is missing")
        if passphrase is None:
            abort(400, "passphrase field is missing")
        if one_time_code is None:
            abort(400, "one_time_code field is missing")

        try:
            journalist = Journalist.login(username, passphrase, one_time_code)
            token_expiry = datetime.now(
                timezone.utc) + timedelta(seconds=TOKEN_EXPIRATION_MINS * 60)

            response = jsonify({
                "token":
                journalist.generate_api_token(
                    expiration=TOKEN_EXPIRATION_MINS * 60),
                "expiration":
                token_expiry,
                "journalist_uuid":
                journalist.uuid,
                "journalist_first_name":
                journalist.first_name,
                "journalist_last_name":
                journalist.last_name,
            })

            # Update access metadata
            journalist.last_access = datetime.now(timezone.utc)
            db.session.add(journalist)
            db.session.commit()

            return response, 200
        except (
                LoginThrottledException,
                InvalidUsernameException,
                BadTokenException,
                InvalidOTPSecretException,
                WrongPasswordException,
        ):
            return abort(403, "Token authentication failed.")
Example #28
0
def _authenticate_user_from_auth_header(request: flask.Request) -> Journalist:
    try:
        auth_header = request.headers['Authorization']
    except KeyError:
        return abort(403, 'API token not found in Authorization header.')

    if auth_header:
        split = auth_header.split(" ")
        if len(split) != 2 or split[0] != 'Token':
            abort(403, 'Malformed authorization header.')
        auth_token = split[1]
    else:
        auth_token = ''
    authenticated_user = Journalist.validate_api_token_and_get_user(auth_token)
    if not authenticated_user:
        return abort(403, 'API token is invalid or expired.')
    return authenticated_user
Example #29
0
def add_journalist(
    username: str = "",
    is_admin: bool = False,
    first_name: str = "",
    last_name: str = "",
    progress: Optional[Tuple[int, int]] = None,
) -> Journalist:
    """
    Adds a single journalist account.
    """
    test_password = "******"
    test_otp_secret = "JHCOGO7VCER3EJ4L"

    if not username:
        username = current_app.crypto_util.display_id()

    journalist = Journalist(
        username=username,
        password=test_password,
        first_name=first_name,
        last_name=last_name,
        is_admin=is_admin,
    )
    journalist.otp_secret = test_otp_secret
    if random_bool():
        # to add legacy passwords back in
        journalist.passphrase_hash = None
        salt = random_chars(32).encode("utf-8")
        journalist.pw_salt = salt
        journalist.pw_hash = journalist._scrypt_hash(test_password, salt)

    db.session.add(journalist)
    attempt = JournalistLoginAttempt(journalist)
    attempt.timestamp = random_datetime(nullable=True)
    db.session.add(attempt)
    db.session.commit()

    print(
        "Created {}journalist{} (username={}, password={}, otp_secret={}, is_admin={})".format(
            "additional " if progress else "",
            " {}/{}".format(*progress) if progress else "",
            username,
            test_password,
            test_otp_secret,
            is_admin,
        )
    )
    return journalist
Example #30
0
    def edit_user(user_id: int) -> Union[str, werkzeug.Response]:
        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

            try:
                first_name = request.form['first_name']
                Journalist.check_name_acceptable(first_name)
                user.first_name = first_name
            except FirstOrLastNameError as e:
                flash(gettext('Name not updated: {}'.format(e)), "error")
                return redirect(url_for("admin.edit_user", user_id=user_id))

            try:
                last_name = request.form['last_name']
                Journalist.check_name_acceptable(last_name)
                user.last_name = last_name
            except FirstOrLastNameError as e:
                flash(gettext('Name not updated: {}'.format(e)), "error")
                return redirect(url_for("admin.edit_user", user_id=user_id))

            user.is_admin = bool(request.form.get('is_admin'))

            commit_account_changes(user)

        password = PassphraseGenerator.get_default().generate_passphrase(
            preferred_language=g.localeinfo.language)
        return render_template("edit_account.html",
                               user=user,
                               password=password)