It is a well-known mantra that when writing a web application or a similar thing, you should never store your users’ passwords unencrypted.
Well, I’m now going to challenge this idea (a bit). Note: I’m definitely not a security expert by any means, and it’s quite possible that I’m completely wrong. But I think I encountered two cases when storing passwords in plain text is actually pretty fine.
I guess what I’m really trying to say here is that security is just a bunch of trade-offs. If your program is somehow exposed on the network (for example, you can access the database from another machine), it is never “absolutely secure”. And even if you only allow access to your data from the machine the data is one, and that machine is air gapped, and all data reside on an encrypted partition with a very strong password, it is still not “absolutely secure”.
That said, an obligatory disclaimer. Let me repeat, it’s quite possible that I am wrong, and if you do not know a lot about security and you use this very article as a justification for storing passwords in plain text, you are doing it wrong. This is just some rambling, food for thought, written by someone who doesn’t like people telling what they think are absolute rules without any justification. And in fact, encrypting, or hashing, or better, key-stretching passwords is so cheap that there is almost no reason not to use it anyway, so even my use-cases I write about below are rather contrived.
So, when I personally consider storing passwords in plain text an acceptable trade-off? Well, some time ago I read an absolutely fascinating article about a software equivalent of a home cooked meal, also known as situated software. That resonated with me a lot, and I will write about it more in the following weeks. The idea is that some software isn’t meant for the general public (which almost inevitably will include some malicious actors), but for small groups of people instead. (In a special case, this small group is actuall a group of one.) The cases I used the simplistic, naive approach of not encrypting passwords fall into exactly this category.
Here is the first case. I once built a very simple app to render a visual representation of my home budget. I use ledger, and while I like it a lot, I wanted something visual to show me whether I am keeping within my budget or not. Also, I wanted it on the web so that I could open it on my phone while shopping and decide – in a rational way – whether I can afford that fancy thing or not. On the other hand, I wanted some kind of authorization so that only me and my family could access it. This is what I did. First and foremost, the app is 100% read-only, the only thing it does with the ledger file is read it (actually, transform it using ledger
itself). And secondly, even if this app had a bug which allows to somehow modify the ledger file, it wouldn’t really matter, since it only has access to a copy of it. (This works in a very simple way: after I update my ledger file, I commit it to Git, and I have a post-commit hook copying it with scp
to the server where the app resides.) Now, what could happen if someone somehow got my password? Nothing could be modified, or even if it could, I wouldn’t bother – the worst case would be that my fancy budget chart would be wrong. Equally importantly, I used a unique password I don’t use anywhere else (this is actually the most important thing!), so even if someone gained access to that password, they would only be able to see my monthly budget (and the current state of my monthly expenses). None of it is secret enough that it would be a problem for me (in fact, I probably could make this app totally open to everyone with the right URL and still nothing bad would happen). The method I settled on is trivially simple – the password is a part of a config file, stored in JSON alongside the app. (Interestingly, there are no usernames at all – just a list of passwords, and any of them can be used to log in. It turned out that this created an unexpected problem – I used Passport to implement authorization, and Passport requires a username to operate. I decided to inject a one-line middleware to the POST
/login
endpoint which just inserts the key username
with the value username
to the body of the request.) By the way, this approach is very similar to what Oddmuse does. In Oddmuse’s case, the rationale for being apparently lax with even admin passwords is that even an admin usually cannot permanently damage an Oddmuse site.
Another case is similar. Almost a decade ago, I built another app, this time not for myself, but for someone I know who needed such a thing. This app also needed some kind of authorization, but it didn’t contain anything crucial like financial or medical data. Again, the set of users would be very limited (this time it would be a bit higher than single digits number, but still not greater than about a dozene). One argument I had against encrypting passwords was that if I did that, I would have to provide the whole infrastructure for dealing with forgotten passwords. That means that I would have to store email addresses and provide “password reset” links via email. This in turn means that I would have to support sending emails, which is simple, but still needs work and perhaps some maintenance. All of that means work and time I didn’t necessarily want to spend on this. (After I wrote this, I discussed this idea with a friend, who suggested a more secure approach without the overhead – I’ll explain it in a minute.)
Instead, I settled down for another approach. First of all, I decided to store my passwords unencrypted. (In fact, encrypting passwords is in fact very little work, so I could actually change that.) More importantly, even if I decided to encrypt passwords, I would not hash them. Why? Because I decided to use usernames which are explicitly not email addresses, and even more importantly, I didn’t let the user choose their usernames and passwords at all. Yes, you heard that right. The username (aka the login) for every user was set by the admin. The password for every user was just set to a random string of characters. There was an option to reset the password to another random string, and that’s it. Here, the idea is that the administrator (for example me) can physically contact all users and give them their passwords via some other channel. Hence one of the main reasons for encrypting (or hashing passwords) – that any breach might expose the users’ data on other systems where they reuse the password – was gone. And even if someone gained access to this system, again – it didn’t contain any crucial (or personal) data.
After I wrote most of this, I consulted a friend who – unlike me – is a security expert, and he tried very hard to convince me that my approach is wrong. He partially succeeded – I admit that in the latter case I could do better with very little effort. There are two things I could have done to increase security (though let me stress that I still think these gains would be marginal). Firstly, I could avoid storing plain-text passwords and key stretch them instead (which is obviously better than just hashing). Of course, that would mean that the password reset procedure would have to be a bit more involved. The admin would initiate it, the system would show them the new password (just this one time), but then it would completely “forget” the password and only store the “enhanced key” (the result of key stretching) from now on. This way the time the system even has the plain-text password would be reduced to a minimum. (I would still disallow setting the passwords by the users.) Secondly, it is much better to concatenate a few words instead of selecting characters at random, since the resulting passphrase can have very high entropy while being easier to remember. (Predictably, a quick search turned out a lot of tools – from web-based, to command-line, to libraries – to generate diceware-like passwords. Also predictably, the quality of these tools seems to have, so to speak, extreme variability.)
So, this is it for today. I hope no security expert died of heart attack while reading this, and I’m curious if someone can prove me wrong (which, as I said, is quite possible, although I really did think my approach through, so I still would be a bit surprised). Generally, I think the main argument for not storing passwords in plain text is “but it is so little work to do it «correctly»”, and while I agree with that, I am aware that it’s still work. (For the record, I decided to no longer store passwords in plain text in my future projects, just in case and to promote better security practices.)