在 Python 中加盐和散列密码
Posted
技术标签:
【中文标题】在 Python 中加盐和散列密码【英文标题】:Salt and hash a password in Python 【发布时间】:2012-03-24 13:07:25 【问题描述】:此代码应该使用盐对密码进行哈希处理。盐和散列密码被保存在数据库中。密码本身不是。
鉴于操作的敏感性,我想确保一切都符合犹太教规。
import hashlib
import base64
import uuid
password = 'test_password'
salt = base64.urlsafe_b64encode(uuid.uuid4().bytes)
t_sha = hashlib.sha512()
t_sha.update(password+salt)
hashed_password = base64.urlsafe_b64encode(t_sha.digest())
【问题讨论】:
你为什么要对盐进行 b64 编码?直接使用盐然后 b64 将两者一起编码t_sha.digest() + salt
会更简单。当您解码盐分哈希密码后,您可以再次拆分盐分,因为您知道解码后的哈希密码正好是 32 个字节。
@Duncan - 我对 salt 进行了 base64 编码,因此我可以对其进行强大的操作,而不必担心奇怪的问题。 “字节”版本会作为字符串工作吗?如果是这种情况,那么我也不需要 base64 编码 t_sha.digest() 。我可能不会将散列密码和 salt 保存在一起,因为这看起来有点复杂且可读性差一些。
如果您使用的是 Python 2.x,那么 bytes 对象将作为字符串很好地工作。 Python 对字符串中可以包含的内容没有任何限制。但是,如果您将字符串传递给任何外部代码(例如数据库),则可能不适用。 Python 3.x 区分了字节类型和字符串,因此在这种情况下,您不希望在 salt 上使用字符串操作。
我不能告诉你如何在 python 中做到这一点,但普通的 SHA-512 是一个糟糕的选择。使用慢速散列,例如 PBKDF2、bcrypt 或 scrypt。
旁注:我建议不要使用 UUID 作为加密随机性的来源。是的,CPython is cryptographically secure 使用的实现,但这不是由 Python 的规范 nor the UUID spec 和 vulnerable implementations exist 规定的。如果您的代码库在没有安全 UUID4 的情况下使用 Python 实现运行,您将削弱安全性。这可能不太可能发生,但改用secrets
并不需要任何成本。
【参考方案1】:
根据这个问题的其他答案,我使用 bcrypt 实现了一种新方法。
为什么要使用 bcrypt
如果我理解正确,使用bcrypt
而不是SHA512
的论点是bcrypt
的设计速度很慢。 bcrypt
还可以选择在第一次生成散列密码时调整您希望它有多慢:
# The '12' is the number that dictates the 'slowness'
bcrypt.hashpw(password, bcrypt.gensalt( 12 ))
缓慢是可取的,因为如果恶意方获得了包含散列密码的表,那么暴力破解它们会困难得多。
实施
def get_hashed_password(plain_text_password):
# Hash a password for the first time
# (Using bcrypt, the salt is saved into the hash itself)
return bcrypt.hashpw(plain_text_password, bcrypt.gensalt())
def check_password(plain_text_password, hashed_password):
# Check hashed password. Using bcrypt, the salt is saved into the hash itself
return bcrypt.checkpw(plain_text_password, hashed_password)
注意事项
我可以很容易地在 linux 系统中安装这个库:
pip install py-bcrypt
但是,我在 Windows 系统上安装它时遇到了更多麻烦。看来需要补丁了。请参阅此 Stack Overflow 问题:py-bcrypt installing on win 7 64bit python
【讨论】:
12 是 gensalt 的默认值 根据pypi.python.org/pypi/bcrypt/3.1.0,bcrypt 的最大密码长度为 72 字节。 除此之外的任何字符都将被忽略。 因此,他们建议首先使用加密散列函数进行散列,然后对散列进行 base64 编码(有关详细信息,请参阅链接)。旁注:py-bcrypt
似乎是旧的 pypi 包,此后已重命名为 bcrypt
。
散列是可以的,但 check_password 在 djano 中总是给出 false
这在 Python 3.8.5 中对我有用,但我必须将密码编码为字节,如下所示:plain_text_password.encode('utf-8')
【参考方案2】:
编辑:这个答案是错误的。 SHA512 的单次迭代是fast,这使得它不适合用作密码散列函数。请改用此处的其他答案之一。
我觉得很好。但是,我很确定您实际上并不需要 base64。你可以这样做:
import hashlib, uuid
salt = uuid.uuid4().hex
hashed_password = hashlib.sha512(password + salt).hexdigest()
如果不造成困难,您可以通过将盐和散列密码存储为原始字节而不是十六进制字符串,在数据库中获得更有效的存储。为此,请将hex
替换为bytes
,将hexdigest
替换为digest
。
【讨论】:
是的,十六进制可以正常工作。我更喜欢 base64,因为字符串更短一些。在较短的字符串上传递和执行操作更有效。 你不能反转它,你永远不能反转密码。这就是为什么我们散列它而不加密它。如果您需要将输入密码与存储的密码进行比较,请对输入进行哈希处理并比较哈希值。如果您对密码进行加密,任何拥有该密钥的人都可以解密并看到它。不安全 uuid.uuid4().hex 每次生成都不一样。如果您无法取回相同的 uuid,您将如何比较密码以进行检查? @LittleBobbyTables 我认为salt
存储在数据库中,并且也存储了咸散列密码。
@LittleBobbyTables ,盐值与用户名以及他/她相应密码的哈希值一起存储在数据库中。当用户键入密码时,它实际上是经过哈希处理的,这个哈希值与盐值连接起来,存储在数据库中。如果匹配,则认为登录成功。【参考方案3】:
编辑:
这个答案中建议的库现在已经过时了,这个答案中提到的hashlib key derivation functionality:https://***.com/a/56915300/893857是现在使用的一个很好的建议。
原始答案 明智的做法是不要自己编写加密货币,而是使用 passlib 之类的东西:https://passlib.readthedocs.io/en/stable/#
以安全的方式编写加密代码很容易搞砸。令人讨厌的是,对于非加密代码,您经常会在程序崩溃后不工作时立即注意到它。使用加密代码时,您通常只有在为时已晚并且您的数据已被泄露时才发现。因此,我认为最好使用由其他人编写的包,该包由熟悉该主题并基于实战测试协议的人编写。
passlib 也有一些不错的功能,这些功能使其易于使用,并且如果旧协议被破坏,也可以轻松升级到更新的密码哈希协议。
此外,单轮 sha512 更容易受到字典攻击。 sha512 的设计速度很快,当尝试安全地存储密码时,这实际上是一件坏事。其他人已经对所有此类问题进行了长期而认真的思考,因此您最好利用这一点。
【讨论】:
我认为使用 crypo 库的建议很好,但是 OP 已经在使用 hashlib,这是一个也在 Python 标准库中的加密库(与 passlib 不同)。如果我处于 OP 的情况,我会继续使用 hashlib。 @dghubblehashlib
用于加密哈希函数。 passlib
用于安全存储密码。它们不是一回事(尽管很多人似乎都这么认为……然后他们的用户密码就被破解了)。
如果有人想知道:passlib
生成自己的盐,存储在返回的哈希字符串中(至少对于某些方案,例如 BCrypt+SHA256) - 所以你不需要担心它。【参考方案4】:
为了在 Python 3 中工作,您需要使用 UTF-8 编码,例如:
hashed_password = hashlib.sha512(password.encode('utf-8') + salt.encode('utf-8')).hexdigest()
否则你会得到:
回溯(最近一次通话最后一次): 文件“”,第 1 行,在 hashed_password = hashlib.sha512(密码 + salt).hexdigest() TypeError: Unicode 对象必须在散列之前进行编码
【讨论】:
没有。不要使用任何 sha 散列函数来散列密码。使用类似 bcrypt 的东西。原因见其他问题的 cmets。 @josch 哪个 cmets? @CoolCloud ctrl+f "为什么要使用 bcrypt" @josch 太好了,谢谢 :)【参考方案5】:从 Python 3.4 开始,标准库中的 hashlib
模块包含 key derivation 函数,这些函数“为安全密码散列而设计”。
所以使用其中一个,例如 hashlib.pbkdf2_hmac
,使用 os.urandom
生成的盐:
from typing import Tuple
import os
import hashlib
import hmac
def hash_new_password(password: str) -> Tuple[bytes, bytes]:
"""
Hash the provided password with a randomly-generated salt and return the
salt and hash to store in the database.
"""
salt = os.urandom(16)
pw_hash = hashlib.pbkdf2_hmac('sha256', password.encode(), salt, 100000)
return salt, pw_hash
def is_correct_password(salt: bytes, pw_hash: bytes, password: str) -> bool:
"""
Given a previously-stored salt and hash, and a password provided by a user
trying to log in, check whether the password is correct.
"""
return hmac.compare_digest(
pw_hash,
hashlib.pbkdf2_hmac('sha256', password.encode(), salt, 100000)
)
# Example usage:
salt, pw_hash = hash_new_password('correct horse battery staple')
assert is_correct_password(salt, pw_hash, 'correct horse battery staple')
assert not is_correct_password(salt, pw_hash, 'Tr0ub4dor&3')
assert not is_correct_password(salt, pw_hash, 'rosebud')
注意:
使用 16 字节盐和 100000 次 PBKDF2 迭代符合 Python 文档中建议的最小数量。进一步增加迭代次数将使您的哈希计算速度变慢,因此更安全。os.urandom
始终使用加密安全的随机源
hmac.compare_digest
,在is_correct_password
中使用,基本上只是字符串的==
运算符,但没有短路能力,这使其免受定时攻击。那个probably doesn't really provide any extra security value,但它也没有伤害,所以我继续使用它。
关于什么是好的密码散列的理论以及适合散列密码的其他函数列表,请参阅https://security.stackexchange.com/q/211/29805。
【讨论】:
【参考方案6】: 如果您需要使用现有系统存储的哈希,passlib 似乎很有用。如果您可以控制格式,请使用现代哈希,例如 bcrypt 或 scrypt。这时候,bcrypt 似乎从 python 中使用起来要容易得多。
passlib 支持 bcrypt,建议安装 py-bcrypt 作为后端:http://pythonhosted.org/passlib/lib/passlib.hash.bcrypt.html
如果您不想安装 passlib,也可以直接使用py-bcrypt。自述文件有基本使用示例。
另见:How to use scrypt to generate hash for password and salt in Python
【讨论】:
【参考方案7】:我不想恢复旧线程,但是...任何想要使用现代最新安全解决方案的人,请使用 argon2。
https://pypi.python.org/pypi/argon2_cffi
它赢得了密码哈希竞赛。 (https://password-hashing.net/) 比 bcrypt 好用,比 bcrypt 更安全。
【讨论】:
【参考方案8】:首先导入:-
import hashlib, uuid
然后在你的方法中根据这个改变你的代码:
uname = request.form["uname"]
pwd=request.form["pwd"]
salt = hashlib.md5(pwd.encode())
然后在你的数据库sql查询中传递这个salt和uname,login下面是一个表名:
sql = "insert into login values ('"+uname+"','"+email+"','"+salt.hexdigest()+"')"
【讨论】:
uname = request.form["uname"] pwd=request.form["pwd"] salt = hashlib.md5(pwd.encode()) 然后在你的数据库sql中传递这个盐和uname查询,login 下面是一个表名:- sql = "insert into login values ('"+uname+"','"+email+"','"+salt.hexdigest()+"')" -1 因为 md5 非常快,这使得使用 md5 的单次迭代成为密码散列函数的糟糕选择。 您是否真的能够在“答案”中同时包含糟糕的密码哈希和 SQL 注入?【参考方案9】:这是我的建议:
for i in range(len(rserver.keys())):
salt = uuid.uuid4().hex
print(salt)
mdp_hash = rserver.get(rserver.keys()[i])
rserver.set(rserver.keys()[i], hashlib.sha256(salt.encode() + mdp_hash.encode()).hexdigest() + salt)
rsalt.set(rserver.keys()[i], salt)
【讨论】:
以上是关于在 Python 中加盐和散列密码的主要内容,如果未能解决你的问题,请参考以下文章