Cryptography/Software Engineering Problem: How to make LW 1.0 logins work on LW 2.0

by habryka1 min read16th Mar 201817 comments

8

Site Meta
Personal Blog

So, as part of making the transition to LessWrong.com smooth, I've been working on trying to get the old passwords to work on the new site. Now I want to be very careful with this, since it's super easy to mess up crypto and I have no interest in leaking everyone's private info from the old site (or messing up authentication for the new site)

The current situation is that we have the old password hashes (SHA1 hashes) from the LW 1.0 database, as well as their corresponding salts. And we are running on the Meteor Accounts system on the new page, which uses bCrypt for hashing.

The Meteor accounts system is designed so that you can't really override any of its login procedures, probably to avoid users shooting themselves in the foot. Meteor also hashes the passwords on the client, so on the server we have no access to what password the user typed into the password field. This puts us into a bit of a dilemma about how to deal with the old passwords.

So to summarize, we have the following variables:

lw1_hash_function(password, salt); lw_1_password_hash(user); lw_1_password_salt(user); authenticate_lw2_client(plaintext_password); authenticate_lw2_server(password_hash); set_lw2_password(plaintext_password, user)

Besides setting passwords, these are really all the functions we can access. Now, there is one thing we could do that would work, but that seems like a somewhat hacky solution to me, so I wanted to get input from people who are better at crypto than me:

First, we set everyone's password to be equivalent to their old LW1 hashes (i.e. they could log in if they would type in their old LW1 password hash into the password field). This means their original plaintext password would now have gone through two hashing functions, once the LW1 hash function, and then again through the LW2 hash function.

Then when the login request arrives on the server, we check whether the user is using one of the old LW1 accounts, and if they do, we send back an error to the client (and this is where I am hesitant), together with the lw_1_password_salt for the user they are trying to login with. On receiving that error, the client uses the old lesswrong hash function (lw1_hash_function()), and combines the password they just typed in with the salt they just received, and then tries to login again with the resulting hash (which would now succeed for legacy users).

A quick prototype I've hacked together for this seems to work, but the part I am hesitant about is to release the salt of the original user to whoever is trying to login into that user's account.

It seems that at least a few other authentication systems seem to do something like this, and from my best understanding it is not important for salts to be secret and doing so does not improve security in any significant way. But this seems exactly like the kind of thing you would want to run by a bunch of people with more crypto expertise, so this is me doing that.

8

17 comments, sorted by Highlighting new comments since Today at 9:44 AM
New Comment

I can't immediately think whether there's a better scheme given the described constraints, although I would somewhat snarkily comment that usually "my framework makes this inconvenient" is not a good reason to avoid doing something more secure. ("Inconvenient" and "impossible" are not the same.)

But let me suggest this, if you do implement this scheme: Immediately force a password change after a user logs in with this method. Don't allow them to interact with the site in any way until they perform a password change, which resets them to a 'new-style' password hash. This ensures that active users will be switched over in short order, and -- assuming that a hash leaks and the attacker doesn't know the underlying password -- ensures that an attacker can't have control of an account also used by a legitimate user, because the attacker changing the password will lock out the legitimate user, who will be likely to complain.

You should consider the question of whether these converted accounts get access to any secret information from the corresponding LW1 account. For example, does a converted account get access to the user's LW1 sent or received private messages? If so, I think you have a duty to your users to come up with a secure scheme, and fight with your framework if necessary to make that happen. Otherwise, if the main possibility is impersonation or use of a converted account for spamming, it seems less critical.

It's less about "my framework makes this inconvenient" and more intuitions along the lines of "don't roll your own crypto". The setup described above (that is now active on LW2) doesn't change anything about the password verification process and basically just adds an additional processing step on the client, which ensures that I am not introducing any bugs into our authentication process, which strikes me as much more risky. The risk of accidentally introducing a bug in the authentication protocol strikes me as bigger than the risk of someone being able to impersonate a user if they somehow got access to the old password hashes, and so I am very hesitant to mess with any authentication protocols on a deeper level, and so am actually quite happy about how inconvenient our framework makes it to mess up something in that space.

The only private data you get access to are old drafts, none of the other private data is copied over (we might port over PMs at some later point in time, but that will take a while).

The now implemented solution is only vulnerable to someone having gotten access to the old LW1 password hashes, at which point you very likely already have access to the rest of the private data for the user (not guaranteed, but given that the old database was structured as a giant key-value store for all user data almost any vulnerability would have very likely exposed both). I do agree that as soon as you log in, we should strongly encourage the user to create a new password. I will try to implement that soon.

I do agree that as soon as you log in, we should strongly encourage the user to create a new password. I will try to implement that soon.

Was the "change password" page actually tested to work for users who have logged in with 'legacy' LW1 credentials? E.g. I notice that there's a textfield asking for my 'current' password on that page, and I have no idea how that would interact with your solution. I also edited the 'email' field in my user options, expecting it to set a 'recovery' address, but I have no idea if that worked properly. (I didn't get a 'verification' message at that email, whereas that happened right away when I added it on LW1.) All this makes me quite nervous about fiddling with my LW2 credentials at this time, either by 'changing' or 'resetting' them. I'd rather deal with a theoretically 'insecure' hashing function for a while.

Ah, thanks a lot for pointing this out. I only tested the password-reset functionality, and just realized that the change password thing doesn't work as well. I quickly changed the password-change flow to accommodate the new changes. Will push in the next few hours.

Um, now there is a "reset password" link, but it does not work properly at least in my case. My recovery address from LW1 was not in the version of the database that was imported to Lesser Wrong, and apparently setting that address as an 'email' in the "edit account" page did not help, either - it is not seen by the "reset password" functionality as a recovery address associated with my user. Weird.

Did you submit the form before you pressed the reset account button? Otherwise can you ping us on Intercom and I can debug it in detail.

Um, yes I did? (The email does appear in my 'account' page, after all.) Anyway, this issue is not so critical to me that I need "real-time support" from the site devs or anything like that. I only really care to the extent that other users may be similarly affected (as seen, e.g. from recent discussions on LW1!). So if it turns out that this is not a generalized problem w/ the site that would also hit other people, I'd think it preferable to just wait until other work on LW2 is completed and perhaps revisit the issue at a later time.

Ah, sorry. I suggested pinging us on Intercom because it will make it easier to find out the source of the bug via real-time chat than here in the comments. I am unsure whether other users will have the same problem before I know what the source of the problem is.

Can you tell me what's printed in your browser console when you press the reset button?

I might have the same problem - I managed to log in with my lw1 password, but failed to update the password.

(The user interface for changing password is confusing so I'm not sure if I'm following the correct procedure.)

Originally the "EMAIL" field in "Edit Account"-page was empty (even I had set the recovery email before the lw1 was shut down) and clicking the "RESET PASSWORD" caused popup "Must pass options.email". I set the "EMAIL" field and pressed the SUBMIT-button. After that clicking the "RESET PASSWORD" in "Edit Account"-page caused popup "User not found" to appear. (Nothing appears in browser console.)

Trying to log in to greaterwrong with the lw1 password caused message "LessWrong 1.0 detected, legacy password salt attached" to appear and the Username and Password fields to be erased. Trying to log in again produced the same result. Trying to use greaterwrong's reset password feature gave the same "User not found"-error.

Any progress on this? With the switchover from LW1 now imminent, I've looked at the LessWrong code on github a bit more and from a cursory review, it really does seem that the usecase or 'flow' of a user editing their own "recovery email" address is broken. The code calls the proper Meteor/Vulcan functions when creating a new user, and will in turn create new users when importing them for the first time from a legacy (LW1) database, but aside from that, there is no acknowledgement that Vulcan/Meteor has its own functions in the 'account'-related packages for setting/updating these data. Did you test this user flow (even just in a test instance of the code) and verify that it can be used to set an email address that the "forgot password/reset password" will rely on?

Huh, apologies for not being available for 'realtime chat' then. I just tried this quickly with a JS console, but weirdly enough I don't see any message when pressing that button - all I see is the usual error message "user not found" popping up in the browser webview (This is probably an issue on my end, though - I'm not really familiar with how the browser console works). What I do see (though in the network view, not quite in the console) is the info being updated when I submit the "edit account" form, and I notice that the resulting object has an "email" property with the desired address as a string. (However, from quickly perusing the Meteor source code, it looks like it has all sorts of specialized functions on the server side for creating/updating/setting 'account email' data - e.g. it seems to keep track of 'verified' status and to also support multiple emails per user. I wonder if the problem is that the "edit account" 'flow' is not resulting in these functions being called? Or perhaps it's not providing data in the format they expect? Something to look into, perhaps. I'm not sure how LW2 itself changes what Meteor does by default here)

Were the LW1 passwords salted and hashed on the client or the server?

They were salted and hashed on the server.

This is insecure if LW1 hashes get leaked or were leaked at some point. If you know someone's hash, you can login as them by making some JS changes in your browser. LW1 was secure w.r.t. that, because login required a plaintext password, and guessing it from a salted hash is hard even if you know the salt.

Other than that I don't see any problems, but I know next to nothing about security :-/

This is insecure if LW1 hashes get leaked or were leaked at some point.

Isn't this vulnerability inherent in the whole "hashing passwords on the client" setup? (indeed, it seems to miss the whole point of hashing?) Or am I misunderstanding what Meteor does?

I think this vulnerability is specific to Oliver's scheme, not Meteor. What Meteor does is hash the password on the client (not sure why, might as well send it in plaintext over SSL) and then hash and salt it on the server as well (which is good and right).

Yep, Meteor hashes twice. Not fully sure why. Probably to add an extra layer of security to non SSL connections.