It's easy to do if you generate the variants from the new password, not the old password.
Imagine an algorithm that takes a password, and generates a few hundred simple variants of it. So for an input like password1
it would generate the following variants:
PASSWORD1
PasSSword1
password2
password
P@aaW0RD2
....
and a few hundred others. (Note that one of those variants is "password"
.)
Password crackers have algorithms like these and use them against word dictionaries to generate guesses.
Using this algorithm, we can perform the following steps:
a user sets their first password to be "password"
.
The system stores hash("password")
, and uses this hash to compare whenever a user logs in.
Several weeks later, the user changes their password to "hunter2"
. The current password hash is changed to hash("hunter2")
. hash("password")
is stored in the list of N historical password hashes.
Several weeks after that, the user attempts to change their password to "password1"
. During this change attempt the following steps are performed.
3a) the new password is available in the clear, because the user just entered it. "password1"
is used as input to the variant generation algorithm described above. A few hundred variant passwords are generated (in the clear).
3b) for each variant password, the hash of that password hash(variant)
is calculated, using the salt for each old password. As noted above, one of the variants of "password1"
is "password"
, so in our list of variant hashes we will have hash("password")
. We end up with V variant hashes to test for each old password, V*N hashes total.
3c) The correctly salted version for each variant hash is tested against the current hash, and the previous hashes, for a total V * N binary array comparisons.
3d) One of the variant hashes is hash("password")
and one of the stored N historical hashes is also hash("password")
. These hashes match exactly, so the password is rejected.
This technique requires V*N hashes to be generated, where V is ~500 and N is ~10. This will only take a few seconds (even with PBKDF2, bcrypt or scrypt) and can be easily performed without disk access.
A note on salt: Changing the salt whenever you change the user's password (which you should definitely do) does make things a bit harder, but you still only need to generate a linear number of hashes, no matter how big your salt is.