Python密码盐渍和胡椒

Posted

技术标签:

【中文标题】Python密码盐渍和胡椒【英文标题】:Python Password Salting and Peppering 【发布时间】:2021-10-02 12:20:05 【问题描述】:

我目前正在创建一个处理密码相关功能(散列和验证)的类。我在这个领域的知识非常基础。

经过一些研究,我很明显应该使用已经很好的哈希库。我选择了bycrypt。还建议我应该为每个密码使用一个唯一的盐,以及一个不存储在数据库中的全局胡椒。我的代码运行良好,并按预期运行。

我的问题是,我对密码进行了正确的加盐和加盐吗?现在,我首先使用 sha256 输入密码,然后使用唯一的盐对其运行 bycrypt。我读过 sha256 不是为密码散列而设计的,所以在我们的情况下它并不安全,但我应该用什么来代替它呢?作为对密码散列一无所知的人,这是安全的,还是我应该改变一些东西?

编辑: 如果我的代码太长,这是我要质疑的浓缩部分:

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")

完整代码:

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】:

据我所知,你的表现是正确的。

我听说 sha256 不是为密码散列而设计的,所以它不安全

这意味着,您不应使用 SHA256 对密码进行哈希处理并将其存储在数据库中。

这并不意味着你不能用它来做辣椒。

我可以在这里推荐的一点是,不要使用普通的 SHA-256,而是使用辣椒的组合。可能就像该密码的 SHA-256 + MD-5 或 SHA-1 的一部分。

如果您使用更高的哈希算法,则需要更多的计算。假设您可能会添加更多功能,例如不应该使用旧密码或类似于旧密码,更多的计算会被浪费。

【讨论】:

感谢您的回答。我有一个细节想问。以下情况的可能性有多大:我使用的是 ORM,因此不太可能进行 SQL 注入,但可以假设有人获得了对数据库的访问权限,但没有获得程序源代码的访问权限。他们可以看到散列密码、盐但没有胡椒。如果他们要创建一个密码 X 的帐户,该帐户变成哈希密码 Y 并且他们可以看到前后,他们不能对辣椒进行逆向工程从而获得所有密码的访问权限吗?辣椒是否可以防止这种情况发生,还是需要包括其他东西?这可能吗? 首先,散列是一种单向技术。因此,我们无法取回原始字符串/值。为了攻击或猜测原始,通常使用彩虹表,其中所有类型都是字符串,并使用所有主要类型的算法进行哈希处理。为了避免这种情况,我们添加了一个盐(随机字符串),以便它生成与现有哈希不匹配的新哈希。要回答您的问题,猜测非常困难。因为它可以有很多组合。如上所述,您使用了 SHA-256。我建议与零件的两种组合。有人可以使用前 10 个字符并与其他人一起休息。 我现在在 SHA-256 旁边添加了 MD-5 部分。感谢您指出这一点!

以上是关于Python密码盐渍和胡椒的主要内容,如果未能解决你的问题,请参考以下文章

在 Spring Security 3.1 中,StandardPasswordEncoder 对密码进行盐渍化最合适的用途是啥?

在散列之前加扰加盐密码。好主意?

has_secure_password 是不是使用任何形式的盐渍?

如何使用 OpenCV 在 Python 中为图像添加噪声(高斯/盐和胡椒等)[重复]

使用 Cookie 和 Salted Hashes 的 PHP 登录系统

Angular 制作安全登录/注册页面