One of the core principles underlying libpasta
is that it should be easy to
use best practice password hashing algorithms. Unfortunately, many people are
currently not using these algorithms, and furthermore, “best practice” seems
to be very hard to pin down. To solve this, we include support for painless
migration, which can even be enabled automatically.
Migrating a password hash is a subtle problem. The whole point of password
storage is that you cannot recover the password. To solve this, libpasta
uses
the onion approch.
Suppose we have a password hash H = f(password, salt)
, and wish to migrate from
using algorithm f
to algorithm g
. Clearly we cannot compute H' = g(password, salt)
without first knowing the password.
Hence, we instead compute H' = g(f(password, salt), salt)
, applying the new
hash function on top of the old one.
In libpasta
, this is represented by a hash of the form:
$!$argon2i$m=4096,t=3,p=1$$2y-mcf$cost=12$1hKt7q7c...
Note we have both argon2i
and 2y-mcf
(bcrypt) in the hash value. This is a
bcrypt hash, with cost 12, which has then been further hashed using Argon2i.
Once the user next successfully logs in, this double-hash can simply be updated once again to a standard, single hash.
To change hashing algorithms incurs a one-off cost to go through an re-hash all passwords. Migration tools are coming soon. For example, for Ruby on Rails, the following Rake script could be used for password migration:
namespace :pasta do
desc "Updates all passwords to the default password algorithm"
task migrate_passwords: :environment do
User.all.each do |user|
user.password_digest = Pasta::migrate_hash(user.password_digest)
user.save
end
end
end
On each subsequent user login, there is an additional overhead incurred:
first the cost of computing g(f(password))
, which is at worst the cost
of two long hashes, plus an additional computation of g(password)
. However,
this migration step only need to happen once per customer, providing a
seamless transition experience.
Of course, there is the possibility that a user does not log in for such a long
time that they end up having multiple layers h4(h3(h2(h1(password))))
, which
could potentially take a long time to migrate, and consume too much storage in
the database. Note that the storage only grows linearly in the length of the
parameters of h1...h4
, since the salt/hash size is fixed. This additional time
incurred for an infrequent user is a minor tradeoff for security and
convenience.