Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Force journalists to use diceware passwords #1509

Merged

Conversation

heartsucker
Copy link
Contributor

This is the initial work for fixing #980 (superseding #1493). Forces journalists to use auto-generated diceware passwords and does not allow user input for passwords.

This PR is not ready for merge, but is here for initial functionality review before fixing all of the unit/functional tests.

Copy link
Contributor

@psivesely psivesely left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pretty good overall. I think we can get this one merged after one round of review if you can make the changes below and in-line, as well as add some new specific tests + fix existing ones.

  1. Even though a call to crypto_util.genrandomid(7) will never produce a password of greater than 128 characters, it seems sensible to still check generated passwords against this value as a defense against future regressions.
  2. s/KeePass/KeePassX\ database/g. Also, adding the word "immediately" before save, and maybe even putting "immediately save this new password to your KeePassX database" in bold would help ensure users/admins do not lose their password... Or, we can instruct users to copy the passphrase to their KeePassX database and then copy it from there back into the confirmation prompt--this goes for both ways a password is changed (see Prevent automated submissions (CAPTCHA) #3).
  3. There is an edge case where the server undergoes a regular reboot or otherwise loses connection to the client after a password change via admin_edit_user. It is possible the password is changed, but the flashed message never makes it back to the admin, effectively locking everyone out of the account. Having a confirmation such as how the user is forced to retype their password in new_password is way to prevent this.
  4. I'm not sure "encrypted USB" should be the go-to for transferring passwords. If the admin and journo see each other in person, Diceware passphrases can just be copied by hand w/o introducing another USB and assoc. risks. If they don't see each other in person regularly, then we'll need to recommend an alternative (let's discuss!).
  5. In some of your try/except blocks you put additional statements and/or expressions in the try block than you are trying to protect (e.g., flash). A better place for these is an else block (see https://docs.python.org/3/tutorial/errors.html#handling-exceptions).

securedrop/db.py Outdated
# Enforce a reasonable maximum length for passwords to avoid DoS
if len(password) > self.MAX_PASSWORD_LEN:
if len(password) > Journalist.MAX_PASSWORD_LEN:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we're referencing a class attribute here, it makes sense to make this a classmethod instead of a staticmethod.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this does need to be static so it can be preemptively used without creating a Journalist instance. As in, we check the password N times, then make one instance and set it.

Or is that not a good approach?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this does need to be static so it can be preemptively used without creating a Journalist instance.

You can do the same thing with a class method. The main advantage in this case is that, by writing cls.MAX_PASSWORD_LEN, even if the Journalist class where to change names, the method would still work w/o requiring you to replace Journalist in the method body with the new class name. In this case it's unlikely we'll change the name of the Journalist class, but I still think it's good practice. This SO answer http://stackoverflow.com/a/12179752 might give a little more info. on when it's better to use one or the other.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes, you're totally right.

@@ -214,7 +214,12 @@ class BadTokenException(Exception):
"""Raised when a user logins in with an incorrect TOTP token"""


class InvalidPasswordLength(Exception):
class PasswordError(Exception):

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: I'd get rid of the empty line here and add a period to the docstring.

flash("Passwords didn't match", "error")
while True:
password = crypto_util.genrandomid(7)
if len(password) > Journalist.MAX_PASSWORD_LEN:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should use Journalist.check_password_acceptable here the same way you did in manage.py.

continue

if password == password_again:
while True:
print("This journalist's password is: {}".format(password))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently manage.py is only used to create admin accounts. So your message about both the admin and the Journalist storing this entry doesn't make sense in this context.

@@ -306,24 +306,25 @@ def edit_account():
user = g.user

if request.method == 'POST':
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no more new_password form in edit_account.html (instead you made a new view), so you can basically get rid of the entire body of edit_account except the return statement. And you'll want to remove POST from the app.route decorator as well.

@heartsucker
Copy link
Contributor Author

@fowlslegs In reply to 3, the edge case about a reboot / server failure between the password being changed and the flashed message being send to the client, I think the best thing to do would be to redirect a confirmation page before changing the password (but persist all the other changes). The admin would have to click "yes, confirm password change" on a page with text saying "this is the new password, here's where to save it." Or something to that effect. Then the password change is persisted, and a confirmation is flashed. Done.

I'm only asking because it seems like UI/UX is now becoming a priority, and I figured I should check in before making this change. @ninavizz might want to give input here too.

@psivesely
Copy link
Contributor

Maybe it is possible to use a JS window.confirm popup box if JS is enabled, or otherwise fall back to this confirm password change page? This is part of our larger make things nice easily with JS if it's enabled, but still provide full functionality--"graceful degradation" (see #1476 it's not.

@heartsucker
Copy link
Contributor Author

heartsucker commented Jan 19, 2017

@fowlslegs: I made the changes. One thing doesn't work. I'm trying to use the JS to add a field to the form, but for the life of me I can't get it to work. Do you think you could take a peek? i think everything else works here.

Edit: Just kidding. Got it. :D

@heartsucker heartsucker force-pushed the diceware-for-journalists branch 2 times, most recently from 047d4a3 to 7856d37 Compare January 19, 2017 10:17
@psivesely
Copy link
Contributor

If you think this is ready can you fix the existing tests and write new ones to thoroughly test the new functionality?

@heartsucker heartsucker force-pushed the diceware-for-journalists branch 3 times, most recently from 1489654 to 7f5a0bc Compare January 24, 2017 13:41
@heartsucker
Copy link
Contributor Author

Hey @fowlslegs, can i get some assistance with Selenium. Right now I have correct behavior in the browser, but I can't get it to work in the functional tests. Right now, I'm dying at tests/functional/journalist_navigation_steps.py:291. I have reset the password, but I can only log in with the old password. This does not happen in the browser. I've tried reloading the ORM object and a few other things, but it seems like the old password is... cached somehow?

@heartsucker
Copy link
Contributor Author

Just kidding. Nailed it. :D

@psivesely
Copy link
Contributor

Nice! I'll try to re-review Friday. Prob too busy tomorrow.

@heartsucker heartsucker force-pushed the diceware-for-journalists branch 2 times, most recently from 2594850 to 303a244 Compare January 28, 2017 12:01
@heartsucker
Copy link
Contributor Author

@fowlslegs Whenever you get around to checking this, there was an error on the VM:

OSError: [Errno 39] Directory not empty: '/tmp/securedrop'

I think restarting the build should get this green.

@psivesely
Copy link
Contributor

Build restarted!

@heartsucker heartsucker force-pushed the diceware-for-journalists branch 2 times, most recently from 5fb0e56 to 54c93c0 Compare February 1, 2017 08:01
@heartsucker
Copy link
Contributor Author

@fowlslegs I'm rebasing this at the moment and think there might be a better solution than the JS window.confirm you proposed. I think it would be easier to implement something like the two step form like #delete-all-confirm on the source interface. It would be one less route, one less HTML page, and one less thing to test with Selenium. Good idea, bad idea?

@psivesely
Copy link
Contributor

@heartsucker Don't feel annoying pinging me if it's been a couple of days after I said I would review and I haven't. Everyone forgets.

Anyway, to answer your question, sounds like a good idea to me! Ping me when you've got that in and then I'll finally review.

@heartsucker
Copy link
Contributor Author

@fowlslegs No worries. I was busy with other tickets and would have been just endlessly rebasing this. :)

@heartsucker heartsucker force-pushed the diceware-for-journalists branch 4 times, most recently from 5c084ca to 6c0e045 Compare August 20, 2017 03:54
@heartsucker
Copy link
Contributor Author

heartsucker commented Aug 20, 2017

Note: the function commit_account_changes is no longer called by the endpoints this PR affects. This is where the most of the coverage decrease comes from. The endpoint for admin_edit_user has no tests. This should be addressed elsewhere.

@@ -188,13 +184,10 @@ def admin_add_user():
otp_secret=otp_secret)
db_session.add(new_user)
db_session.commit()
except InvalidPasswordLength:
except PasswordError:
flash('There was an error with the autogenerated passoword. '
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

passoword -> password

@redshiftzero
Copy link
Contributor

Looks like a bug has crept in - I'm trying to make an admin from the command line and I get:

vagrant@development:/vagrant/securedrop$ ./manage.py add_admin
Traceback (most recent call last):
  File "./manage.py", line 409, in <module>
    _run_from_commandline()
  File "./manage.py", line 402, in _run_from_commandline
    rc = args.func(args)
TypeError: add_admin() takes no arguments (1 given)

(Relevant: #1982)

@heartsucker
Copy link
Contributor Author

Fixed the bug. Travis should be happy in a minute.

@redshiftzero
Copy link
Contributor

Cool - taking another look!

Also writing out here to summarize our discussion in person: since we've had a long back and forth in this PR, once the basic functionality and sufficient testing is down, I'm going to make two followup issues: one for design/UI cleanup and one for documentation. Note that we should tackle updating the documentation today when this is merged (even without updated screenshots, which we can do prior to release) so that any developers trying to work on SecureDrop are not confused by this new functionality.

Copy link
Contributor

@redshiftzero redshiftzero left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving this! Thanks for bearing with us on the long back and forth here @heartsucker . This is a solid improvement and I'm going to merge this in and any modifications we can do in a subsequent PR. I know the documentation updates were not added here, but I've made followup #2167 for that and we should get that done and merged in today.

@redshiftzero
Copy link
Contributor

Staging failure looks like a flake, restarting

@redshiftzero
Copy link
Contributor

redshiftzero commented Aug 22, 2017

Note: Coverage is decreasing here because @heartsucker has removed no cover from functions that were improperly excluded from the coverage computation in manage.py and added some test coverage for them. See coverage output here. I am not going to block merge on this and further test coverage for manage.py should be addressed in #1982.

@redshiftzero redshiftzero merged commit dbd665e into freedomofpress:develop Aug 22, 2017
@heartsucker heartsucker deleted the diceware-for-journalists branch August 22, 2017 21:07
redshiftzero added a commit that referenced this pull request Aug 24, 2017
In PR #2141, we added a flag in pytest to generate automatic screenshots
for each page of the source and journalist interface in each
translated language. At the same time we merged in #1509 which
automatically generated diceware passphrases. Unfortunately there was a
lingering reference to a password (that is now automatically generated)
in one of the functions used to generate the screenshots, which caused
two test failures.
ghost pushed a commit that referenced this pull request Aug 24, 2017
In PR #2141, we added a flag in pytest to generate automatic screenshots
for each page of the source and journalist interface in each
translated language. At the same time we merged in #1509 which
automatically generated diceware passphrases. Unfortunately there was a
lingering reference to a password (that is now automatically generated)
in one of the functions used to generate the screenshots, which caused
two test failures.
ghost pushed a commit that referenced this pull request Aug 24, 2017
In PR #2141, we added a flag in pytest to generate automatic screenshots
for each page of the source and journalist interface in each
translated language. At the same time we merged in #1509 which
automatically generated diceware passphrases. Unfortunately there was a
lingering reference to a password (that is now automatically generated)
in one of the functions used to generate the screenshots, which caused
two test failures.
ghost pushed a commit that referenced this pull request Aug 24, 2017
In PR #2141, we added a flag in pytest to generate automatic screenshots
for each page of the source and journalist interface in each
translated language. At the same time we merged in #1509 which
automatically generated diceware passphrases. Unfortunately there was a
lingering reference to a password (that is now automatically generated)
in one of the functions used to generate the screenshots, which caused
two test failures.
ghost pushed a commit that referenced this pull request Aug 24, 2017
In PR #2141, we added a flag in pytest to generate automatic screenshots
for each page of the source and journalist interface in each
translated language. At the same time we merged in #1509 which
automatically generated diceware passphrases. Unfortunately there was a
lingering reference to a password (that is now automatically generated)
in one of the functions used to generate the screenshots, which caused
two test failures.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants