对于密码管理器来说,多少轮哈希就足够了?

信息安全 密码 哈希 密码管理 安全编码
2021-08-19 00:19:12

我目前正在编写自己的小密码管理器,将密钥存储在SHA256哈希中,并带有盐。我通过执行以下操作创建哈希:

def sha256_rounds(raw, rounds=100001):
    obj = hashlib.sha256()
    for _ in xrange(rounds):
        obj.update(raw)
        raw = obj.digest()
    return obj.digest()

创建后,它与以下内容一起存储:

    key = base64.urlsafe_b64encode(provided_key)
    length = len(key)
    with open(key_file, "a+") as key_:
        front_salt, back_salt = os.urandom(16), os.urandom(16)
        key_.write("{}{}{}:{}".format(front_salt, key, back_salt, length))

我的问题/担忧是:

  • 这是存储散列密钥的可接受方式吗?
  • 我应该使用更多的迭代吗?
  • 我的腌制技术够吗?还是我应该使用不同的腌制技术?

如果这不是存储散列密码/密钥的可接受方式,我可以采取哪些其他步骤来使其更安全?


更新:

我听取了你的很多建议,如果你想看看到目前为止我的小密码管理器的结果,你可以在这里找到它。非常感谢大家的建议,我将继续努力使其尽可能出色。(如果该链接不起作用,请使用此链接https://github.com/Ekultek/letmein

4个回答

我目前正在编写自己的小密码管理器

这是你的第一个错误。这种复杂的东西有许多微妙的陷阱,即使是专家有时也会陷入其中,如果在这方面没有足够的经验,你就没有机会做出更接近安全的东西。

将密钥存储在 SHA256 哈希中

哦哦...

这并不一定表明你做错了什么,但我强烈怀疑你是否会做对。我假设您在谈论在这里散列的主密码?主密码应使用PBKDF2、bcrypt 或 Argon2 等KDF转换为密钥,然后此密钥用于加密存储的密码。

如果你想有办法验证密码是否正确,存储密钥的哈希应该没问题,但你不能存储密钥本身......如果你存储密钥,任何可以访问你存储的人都拥有一切他们需要解密所有密码!

如果您不是在谈论散列主密码并且您的意思是实际随机生成的密钥,那么我不知道您要在这里完成什么,但是您不应该使用具有大量迭代的慢速 KDF .

或者,您可以对主密码进行两次哈希处理,一次存储为哈希以稍后验证用户输入的密码是否正确,另一次用作加密密钥。根据如何完成,它的范围可能从设计缺陷到完全泄露密钥。

编辑:在看到完整的代码之后,它似乎是第四个选项:你存储密码的哈希值以便稍后检查输入的密码是否正确,然后你哈希这个哈希值作为密钥,这几乎和刚才一样糟糕存储密钥本身。

我通过执行以下操作创建哈希:

def sha256_rounds(raw, rounds=100001):
    obj = hashlib.sha256()
    for _ in xrange(rounds):
        obj.update(raw)
        raw = obj.digest()
    return obj.digest()

目前还不清楚raw这里有什么,但我假设它是密码。您正在做的是使用 SHA256 的未加盐哈希。不要尝试创建自己的 KDF!

创建后,它与以下内容一起存储:

key = base64.urlsafe_b64encode(provided_key)
length = len(key)
with open(key_file, "a+") as key_:
    front_salt, back_salt = os.urandom(16), os.urandom(16)
    key_.write("{}{}{}:{}".format(front_salt, key, back_salt, length))

那么,您是通过对密码进行哈希处理来创建密钥,然后在前后添加随机盐吗?不仅将 2 种不同的盐连接到前后非标准,而且这里没有完成任何事情,因为它是在 KDF 已经完成之后完成的!你只是为了让它们在那里而添加一些随机值。


为了说明这是多么糟糕(截至提交 609fdb5ce976c7e5aa1832670505da60012b73bc),在不需要任何主密码的情况下转储所有存储的密码所需要的只是:

from encryption.aes_encryption import AESCipher
from lib.settings import store_key, MAIN_DIR, DATABASE_FILE, display_formatted_list_output
from sql.sql import create_connection, select_all_data

conn, cursor = create_connection(DATABASE_FILE)
display_formatted_list_output(select_all_data(cursor, "encrypted_data"), store_key(MAIN_DIR))

虽然尝试创建密码管理器可能是一个很好的学习体验,但不要 将它用于任何远程重要的事情。正如@Xenos 所建议的那样,您似乎没有足够的经验来创建自己的密码管理器确实会有所帮助,这可能是一个更好的学习机会来看看现有的开源密码管理器。

首先让我透露一下我为密码管理器 1Password 工作,尽管这似乎是自私的,但我必须向那些告诉你编写安全密码管理器比最初看起来更难的人表达我的意见。另一方面,您最好在公开场合尝试并询问它。这是了解它比最初看起来更难的好方法。

不认证。改为加密

我快速浏览了您在更新中链接到的来源,很高兴看到您接受了迄今为止提供的一些建议。但是,除非我误读了您的代码,否则您仍然有一个基本的设计错误,使其非常不安全

您似乎将主密码条目视为身份验证问题,而不是加密密钥派生。也就是说,您正在使用存储的内容检查输入的密码(在散列之后),如果该检查通过,您正在使用该存储的密钥来解密数据。

您应该做的是存储加密的加密密钥。我们称它为万能钥匙。这应该在您第一次设置新实例时随机生成。这个主密钥是你用来加密和解密实际数据的。

主密钥永远不会以未加密的方式存储。它应该使用有时称为密钥加密密钥 (KEK) 的东西进行加密。此 KEK 是通过您的密钥派生函数 (KDF) 从用户的主密码和盐派生的。

因此,您使用 PBKDF2 的输出(感谢您使用它而不只是重复散列)将是您的 KEK,您使用 KEK 解密主密钥,然后使用解密的主密钥解密您的数据。

现在也许这就是你正在做的事情,我误读了你的代码。但是,如果您只是将通过 KDF 导出的内容与存储的内容进行比较,然后决定是否解密,那么您的系统将非常不安全。

请让项目自述文件的最大胆最大的第一行尖叫它非常不安全的事实。并将其添加到每个源文件中。

其他几点

以下是我在查看源代码时注意到的其他一些事情

  • 使用经过身份验证的加密模式,例如 GCM 而不是 CBC。

  • 您仅加密整个事物的方法适用于少量数据,但一旦您拥有更大的数据集,它就无法扩展。

    当需要分别加密(部分)记录时,请记住,您将需要一个唯一的随机数(对于 GCM)或唯一的初始化向量(如果您不明智地坚持使用 CBC)。

  • 您在 3 次失败后销毁数据是危险的,并且不会增加安全性。

    一个老练的攻击者只会复制您的数据文件并编写他们自己的脚本来尝试破解密码。他们还将制作自己的捕获数据的副本。您的数据销毁只会让某人很容易意外或恶意破坏您的数据。

多少轮PBKDF2

所以对于你原来的问题。答案是视情况而定。一方面,这取决于主密码的强度以及攻击者将投入什么样的资源来解决问题。这也取决于防守者可以投入什么资源。例如,您是否会在电池容量有限的移动设备上运行 KDF?

但碰巧的是,我们 1Password 正试图通过向破解使用 100,000 轮 PBKDF2-HMAC-SHA256 散列的 42.5 位密码的个人或团体提供奖品来衡量破解成本。

缓慢散列的收益减少

但是,比弄清楚如何权衡所有这些更重要的是要了解慢散列虽然对于密码管理器 KDF 绝对必要,但一旦调整到足够高,就会产生递减的边际收益。

假设您正在使用 100K 轮,并且您有一些主密码P如果您选择一个随机数字 0-9 并将其附加到P中,您将获得 10 倍的抗裂性增加。要通过增加 PBKDF2 的轮数来获得相同的增长,您需要达到 100 万轮。关键是,一旦你有相当数量的慢散列,你通过增加密码的强度比增加轮数获得更多的防御力量。

几年前我在Bcrypt 中写过这个很棒,但是密码破解是不可行的吗?

不要使用原始 SHA256 - 它可以使用 GPU 进行加速,因此容易受到暴力破解。它没有被破坏,无论如何,它不再是最佳实践。PBKDF2 可以在一定程度上抵消这种情况,但您应该优先使用 bcrypt 或 scrypt。

重新发明轮子(如果您不使用 PBKDF2-SHA256、bcrypt 或 scrypt,这几乎就是您正在做的事情)在加密领域从来都不是一个好主意。

查看您发布的链接中的代码(部分转载如下),我有点困惑。

如果我阅读正确,main()调用store_key()从磁盘上的文件加载密钥,然后用于compare()将用户给定的密钥与该密钥进行比较。两者都在sha256_rounds()其中运行 PBKDF2。

然后你用同样stored_key的,从文件中加载的那个,做加密?

那是完全完全倒退的。任何可以访问密钥文件的人都可以加载存储的密钥,并使用它来解密存储的数据,或者存储在同一密钥下加密的新数据,而无需通过脚本来“验证”密码。

充其量,我认为您会混淆使用用户给定的密码/密码来生成用于加密的密钥,并根据存储的密码哈希对用户进行身份验证您不能同时使用相同的哈希值,如果您将加密密钥与加密数据一起存储,则加密完全无关紧要。

当然,密码在用于身份验证和用于生成加密密钥时都使用某种哈希/KDF 进行处理。但这更像是一个必要的预处理步骤,因为人类无法记住 256 位密钥,而我们不得不依赖低熵密码。您对所获得的哈希/密钥所做的操作在两个用例之间确实有所不同。


然后是你有一个硬编码的盐sha256_rounds(),这几乎违背了盐的一般用途。我也可以更一般地评论代码,但这不是 codereview.SE。

请告诉我我读错了代码。


我查看的代码部分:

def main():
    stored_key = store_key(MAIN_DIR)    
    if not compare(stored_key):
        ...
    else:
            ... # later
            password = prompt("enter the password to store: ", hide=True)
            encrypted_password = AESCipher(stored_key).encrypt(password)



def store_key(path):
    key_file = "{}/.key".format(path)
    if not os.path.exists(key_file):
        ...
    else:
        retval = open(key_file).read()
        amount = retval.split(":")[-1]
        edited = retval[16:]
        edited = edited[:int(amount)]
        return edited