We hash passwords with one-way hash functions to reduce the impact of password database compromise. Passwords hashed with salted SHA or MD5 can ofen be brute forced. Newer password hashhing techniques like Scrypt/Bcrypt/PBKDF2 add computational work to make brute forcing prohibitively expensive.
Your service already has users with salted sha1 password hashes. Since you don't have their original passwords you can't upgrade to a modern password hash without getting each user to log in and enter their password.
You have (salt,sha(salt+password)), you want (salt, scrypt(salt,password))
Instead, feed the existing salted sha1 into scrypt: scrypt(salt,sha1(salt+password)).
To verify a password you'll have to compute a sha1 hash and then an scrypt hash. This is simple to implement, it protects the passwords from brute force attacks, and it does not require action on behalf of your users.
Pro-Tip: If your salt is short or not unique, please create a second salt for the scrypt hashes.
Pro-Tip: Store a password version with each password hash. This will make it easy to incrementally upgrade passwords.
Pro-Tip: Don't forget to purge all sha1's when you're done.
import ssl, scrypt
from base64 import b64encode as b64e
from hmac import compare_digest
from hashlib import sha1
class User(object):
"""Here is toy "User" class with sha1(salt+password) hashes.
Your current password scheme probably looks something like this."""
def __init__(self, name, password):
self.name = name
self.set_password(password)
def set_password(self, password):
salt = str(b64e(ssl.RAND_bytes(9)))
self.enc_pw = (salt, self.hash_password(salt,password),) # (salt, hash,)
def check_password(self, password):
salt, real_hsh = self.enc_pw
return hmac.compare_digest(real_hsh, self.hash_password(salt, password))
@staticmethod
def hash_password(salt, password):
return b64e(sha1((salt+password).encode('utf-8')).digest())
And the new user class:
class SafeUser(User):
"""Proof of concept for how you can upgrade to scrypt without forcing a password reset."""
def __init__(self, name, password=None):
self.name = name
if(password):
self.set_password(password)
@staticmethod
def hash_password(salt, password, sha=None):
sha = sha or User.hash_password(salt, password)
return b64e(scrypt.hash(sha, salt))
@classmethod
def upgrade(cls, old):
salt, sha = old.enc_pw
u = cls(old.name)
u.enc_pw = (salt, u.hash_password(salt, None, sha),)
return u
In action:
# demo
u = User("bob", "horrible-pw")
print("Bob's salt,password:", u.enc_pw)
u2 = SafeUser.upgrade(u)
print("Bob's new encoded password:", u2.enc_pw)
Bob's salt,password: ("b'eFXvv3YHGg9n'", b'4PTlKhAuW6ZAc6vEB6ujoq4Bq6U=')
Bob's new encoded password: ("b'eFXvv3YHGg9n'", b'sDr+X3vDbj2hESc9z5kESDnu5HCwTNqNALjcgJdFZ+CLzE06PL6slx8o0pYePpAsLZtBdIN4/TOGJlcytefz0g==')
(u2salt, u2hsh) = u2.enc_pw
pw = "horrible-pw"
u3 = SafeUser("joe", pw)
assert u.check_password("horrible-pw"), "Bob can log in"
assert not u.check_password("other"), "Everyone can log in"
assert u2.check_password(pw), "Bob can still log in"
assert not u2.check_password("other-password"), "everyone can log in"
assert u2hsh==b64e(scrypt.hash(b64e(sha1((u2salt+pw).encode('utf-8')).digest()),u2salt))
assert u3.check_password(pw), "Bob can still log in"
assert not u3.check_password("other-password"), "everyone can log in"