Python Password Salting and Peppering

Viewed 854

I am currently creating a class that handles password related functions (hashing and verification). My knowledge is very basic in this field.

After some research it was obvious to me that I should use an already good hashing library. I chose bycrypt. It was also recommended that I should use a unique salt for each password, and also a global pepper that is not stored inside the database. My code runs fine and does what it is supposed to.

My question is, am I peppering and salting my password correctly? Right now I first pepper the password with sha256 and then I run bycrypt on that with a unique salt. I've read that sha256 is not made for password hashing so it is not secure in our case, but what should I use instead of it? As somebody who has no knowledge in password hashing is this secure, or should I change something?

EDIT: If my code is too long this is the condensed part I am questioning:

import bycrypt
import hashlib
import hmac

password = "vEryC0mPl3X!!!"

PEPPER = "randompepperstring"
salt = bycrypt.gensalt()

peppered_password = hmac.new(PEPPER.encode("utf-8"), password.encode("utf-8"), hashlib.sha256).hexdigest()
salted_peppered_password = bycrypt.hashpw(peppered_password.encode("utf-8"), salt)
heashed_password = salted_peppered_password.decode("utf-8")

Full code:

from bcrypt import hashpw, checkpw, gensalt
from hashlib import sha256
from hmac import new

class EmailNotFoundError(Exception):
    pass

class PasswordError(Exception):
    pass

class PasswordLengthError(PasswordError):
    pass

class PasswordNotComplexError(PasswordError):
    pass

class PasswordManager:

    PEPPER = "randompepperstring"

    def _GenerateSalt() -> bytes:
        return gensalt()

    def _PepperPassword(password: str) -> str:
        return new(PasswordManager.PEPPER.encode("utf-8"), password.encode("utf-8"), sha256).hexdigest()

    def _SaltPassword(password: str, salt: bytes) -> bytes:
        password_bytes = password.encode('utf-8')
        return hashpw(password_bytes, salt)

    def _GetPasswordOfEmail(email: str) -> str:
        # hashed_pw is for testing purposes currently
        # result should come from and sql select, eg: SELECT CASE WHEN COUNT(1) > 0 THEN HashedPassword ELSE NULL END AS [HashedPassword] FROM Users WHERE Email LIKE email
        # result will select SQL NULL if no e-mail is found, which becomes a None in Python
        result = hashed_pw
        if result == None:
            raise EmailNotFoundError("The provided e-mail address does not exist in our database.")
        return result.encode("utf-8")

    def _ValidPasswordFormat(password: str) -> None:
        if 8 > len(password) >= 72:
            raise PasswordLengthError(f"The password should be between 8 and 72 characters long. The provided password '{password}' is {len(password)} characters long.")
        if not any(map(str.isdigit, password)):
            raise PasswordNotComplexError("The password contains no numbers. The password should contain at least 1 number, symbol, uppercase letter and lowercase letter.")
        if not any(map(str.islower, password)):
            raise PasswordNotComplexError("The password contains no lowercase letters. The password should contain at least 1 number, symbol, uppercase letter and lowercase letter.")
        if not any(map(str.isupper, password)):
            raise PasswordNotComplexError("The password contains no uppercase letters. The password should contain at least 1 number, symbol, uppercase letter and lowercase letter.")
        if not any(char in '!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~' for char in password):
            raise PasswordNotComplexError("The password contains no symbols (for example ! or * or = ...). The password should contain at least 1 number, symbol, uppercase letter and lowercase letter.")

    def HashPassword(password: str) -> str:

        PasswordManager._ValidPasswordFormat(password)
        salt =  PasswordManager._GenerateSalt()
        peppered_password = PasswordManager._PepperPassword(password)
        salted_peppered_password = PasswordManager._SaltPassword(peppered_password, salt)
        return salted_peppered_password.decode("utf-8")

    def VerifyLogin(email: str, input_password: str) -> bool:

        stored_password = PasswordManager._GetPasswordOfEmail(email)
        return checkpw(PasswordManager._PepperPassword(input_password).encode('utf-8'), stored_password)

# TESTING
hashed_pw = PasswordManager.HashPassword("vEryC0mPl3X!!!")
print(PasswordManager.VerifyLogin("john@gmail.com", "vEryC0mPl3X!!!"))
print(PasswordManager.VerifyLogin("john@gmail.com", "not"))
print(PasswordManager.VerifyLogin("john@gmail.com", "good"))
print(PasswordManager.VerifyLogin("john@gmail.com", "at all"))
1 Answers

As per my knowledge, what you performed is proper.

I've read that sha256 is not made for password hashing so it is not secure

This is meant, you should not hash the password with SHA256 and store it in the database.

It doesn’t mean that you can’t use it for pepper.

One point I can recommend here is instead of using plain SHA-256, use a combination for pepper. Maybe like part of SHA-256 + MD-5 or SHA-1 for that password.

If you use higher hashing algorithms, it takes more computation. Let’s say you might add further features like should not use old password or similar to old passwords, more computation gets wasted.

Related