Castellated: An Adaptable, Robust Password Storage System for Node.js
2020-05-11
If you ask for advice on how to store passwords, you’ll get some responses that are sensible enough. Don’t roll your own crypto. Salted hashes aren’t good enough against GPU attacks. Use a function with a configurable parameter that causes exponential growth in computation, like bcrypt. If the posters are real go-getters that day, they might even bring up timing attacks.
This is fine advice as far as it goes, but I think it’s missing a big picture item: the advice has changed a number of times over the years, and we’re probably not at the final answer yet. When people see the current standard advice, they tend to write systems that can’t be easily changed. This is part of the reason why you still have companies using crypt()
or unsalted MD5 for their passwords, approaches that are two or three generations of advice out of date. Someone has to flag it and then spend the effort needed for a migration. Having run such a migration myself, I’ve seen it reveal some thorny internal issues along the way.
Consider these situations:
- You run bcrypt. Is your cost parameter sufficient against attacks on current hardware? Can your servers afford to push it a little higher? What’s your process for changing the cost parameter? Do you check in every year to see if it’s still sufficient? If you changed it, would only new users receive the upgraded security, or does it upgrade users every time they login?
- You run scrypt. Tomorrow, news comes out showing that scrypt is irrevocably broken. What’s your process for switching to something different? As above, would it only work for new users, or do old users get upgraded when they login?
-
Your system is just plain out of date, running
crypt()
or salted SHA1 or some such. How do you migrate to something else?
Castellated is an approach for Node.js, written in Typescript, that allows password storage to be easily migrated to new methods. Out of the box, it supports encoding in argon2, bcrypt, scrypt, and plaintext (which is there mostly to assist testing). A password encoded this way will look something like this:
ca571e-v1-bcrypt-10-$2b$10$wOWIkiks.tbbftwkJ81BNeuOtq631SzbsVOO7VAHf5ziH.edAAqJi
This stores the encryption type, its parameters, and the encoded string. We pull this string out of a database, check that the user’s password matches, and then see if the encoding is the preferred type. Arguments (such as the cost parameter) are also checked. If these factors don’t match up, then we reencode the password with the new preferred types and store the result. This means that every time a user logs in (meaning we have the plaintext password available to us), the system automatically switches over to the preferred type.
There is also a fallback method which will help in migrating existing systems.
All matching is done using a linear-time algorithm to prevent timing attacks.
It’s all up on npm now.