【问题标题】:Is this AES GCM file encryption good practice?这是 AES GCM 文件加密的好习惯吗?
【发布时间】:2021-02-28 05:03:42
【问题描述】:

我正在使用它来加密文件,然后使用 AES-GCM 解密文件:

(如果尚未安装,请先执行pip install pycryptodome

import Crypto.Random, Crypto.Protocol.KDF, Crypto.Cipher.AES

def cipherAES_GCM(pwd, nonce):
    key = Crypto.Protocol.KDF.PBKDF2(pwd, nonce, count=100_000)
    return Crypto.Cipher.AES.new(key, Crypto.Cipher.AES.MODE_GCM, nonce=nonce)    

# encrypt
plaintext = b'HelloHelloHelloHelloHelloHelloHello'  # in reality, read from a file
key = b'mykey'
nonce = Crypto.Random.new().read(16)
c, tag = cipherAES_GCM(key, nonce).encrypt_and_digest(plaintext)
ciphertext = nonce + tag + c     # write ciphertext to disk as the "encrypted file"

# decrypt
nonce, tag, c = ciphertext[:16], ciphertext[16:32], ciphertext[32:]  # read from the "encrypted file" on disk
plain = cipherAES_GCM(key, nonce).decrypt_and_verify(c, tag).decode()
print(plain)  # HelloHelloHelloHelloHelloHelloHello

这是否被认为是一种好的加密做法,这种文件加密实施的潜在弱点是什么?


备注:我有 10,000 个文件要加密。如果每次加密文件时,我都会调用 KDF(count 值很高),这将非常低效!
更好的解决方案是:只调用一次 KDF(使用 nonce1),然后对每个文件执行:

nonce2 = Crypto.Random.new().read(16)
cipher, tag = AES.new(key, AES.MODE_GCM, nonce=nonce2).encrypt_and_digest(plain)

但这是否意味着我必须为每个文件将nonce1 | nonce2 | ciphertext | tag 写入磁盘?这会为每个文件添加一个额外的 16 字节 nonce1...

【问题讨论】:

    标签: python encryption cryptography aes aes-gcm


    【解决方案1】:

    改进代码的建议是为 GCM 应用 12 字节的 nonce。目前使用的是 16 字节的 nonce,这应该改变,参见here sec。 注意here

    对于 GCM 的安全性至关重要的是,密钥/随机数对不会被多次使用,here。由于在每次加密的代码中都会生成一个随机随机数,因此可以防止此问题。

    您的代码也将 nonce 用作密钥派生的盐,这原则上没有安全问题,因为这不会导致多次使用相同的密钥/nonce 对 here

    但是,这样做的一个缺点可能是盐长度由随机数长度决定。如果不希望这样做(例如,如果应该使用更大的盐),另一种方法是为每个加密生成一个随机盐,以通过 KDF here 导出密钥和随机数。在这种情况下,连接的数据salt | ciphertext | tag 然后将被传递给接收者。另一种选择是完全分离随机数和密钥生成,并为每个加密生成随机随机数和随机盐以生成密钥。在这种情况下,必须将连接的数据salt | nonce | ciphertext | tag 传递给接收者。注意,和nonce和tag一样,salt也不是秘密,所以可以和密文一起发送。

    代码应用了 100,000 的迭代计数。通常,以下情况适用:迭代计数应尽可能高,同时保持可接受的性能here。如果 100,000 符合您环境的此标准,则可以。

    您使用的串联顺序是nonce | tag | ciphertext。只要双方都知道这一点,这不是问题。通常按照约定,使用nonce | ciphertext | tag 顺序(例如Java 隐式 将标记附加到密文),如果您想遵守此约定,也可以在代码中使用。

    使用最新的、维护的库也很重要,PyCryptodome 就是这种情况(不像它的前身,旧版 PyCrypto,根本不应该使用)。

    编辑:
    PyCryptodome 的 PBKDF2 实现默认使用 16 个字节作为生成密钥的长度,对应于 AES-128。对于摘要,默认应用 HMAC/SHA1。贴出的代码使用了这些标准参数,没有一个是不安全的,但如果需要当然可以更改,here
    注意:虽然 SHA1 本身是不安全的,但这并不适用于上下文 PBKDF2 或 HMAC,here。但是,为了支持 SHA1 从生态系统中消失,可以使用 SHA256。


    编辑:(关于问题的更新):

    已编辑问题中提出的用例是加密 10,000 个文件。为每个文件执行发布的代码,以便通过 KDF 生成相应数量的密钥,这会导致相应的性能损失。这被您描述为非常低效。但是,不应忘记当前代码侧重于安全性而不是性能。在我的回答中,我指出例如迭代次数是一个允许在一定限度内调整性能和安全性的参数。

    PBKDF(基于密码的密钥派生函数)允许从弱密码中派生密钥。为了保证加密安全,有意增加了推导时间,以便攻击者无法比强密钥更快地破解弱密码(理想情况下)。如果推导时间缩短(例如,通过减少迭代次数或多次使用相同的密钥),这通常会导致安全性降低。或者简而言之,性能提升(通过更快的 PBKDF)通常会降低安全性。这为更高性能(但更弱)的解决方案提供了一定的余地。

    您建议的性能更高的解决方案如下:和以前一样,为 每个 文件生成一个 随机 随机数。但是,不是用 自己的 密钥加密 每个 文件,而是用 same 密钥加密 所有 文件。为此目的,生成随机盐一次,通过 KDF 派生此密钥。这确实意味着显着的性能提升。然而,这会自动降低安全性:如果攻击者成功获得密钥,攻击者可以解密所有文件(而不仅仅是一个设想)。但是,如果此缺点在您的安全要求范围内是可接受的(这里似乎就是这种情况),则它不是强制性的排除标准。

    更高效的解决方案要求必须将信息salt | nonce | ciphertext | tag 发送给收件人。 salt 很重要,不能丢失,因为接收者需要 salt 才能通过 PBKDF 派生密钥。一旦接收者确定了密钥,就可以使用标签对密文进行身份验证并使用随机数解密。如果已与接收方商定每个文件将使用相同的密钥,则接收方只需通过 PBKDF 导出密钥一次即可。否则,必须为每个文件派生密钥。

    如果不需要 16 字节的 salt(因为在此方法中它对于所有文件都是相同的),则可以考虑替代架构。例如,可以使用混合方案:使用公钥基础设施生成和交换随机对称密钥。同样在这里,所有文件都可以使用相同的密钥加密,或者每个文件都可以使用自己的密钥加密。

    但对于设计提案的更具体建议,应更详细地描述用例,例如关于文件:文件有多大?是否需要在流/块中进行处理?或者关于收件人:有多少收件人?什么与收件人一致?等等

    【讨论】:

    • 感谢您提供非常详细和有源的答案,真的非常感谢。我会仔细阅读这篇文章。
    • 我的代码有一个问题:我有 10,000 个文件要加密。每次加密文件时,我都会调用 KDF(具有较高的 count 值),这是非常低效的!更好的解决方案是:只调用一次 KDF(使用nonce1),然后为每个文件调用nonce2 = Crypto.Random.new().read(16); Crypto.Cipher.AES.new(key, AES.MODE_GCM, nonce=nonce2).encrypt_and_digest(file_content)。但这是否意味着我必须为每个文件将nonce1 | nonce2 | ciphertext | tag 写入磁盘?这会为每个文件添加一个额外的 16 字节 nonce1...
    • ... 然后,当解密 10k 个 nonce1 | nonce2 | ciphertext | tag 形式的文件(nonce1 可能因文件而异)时,我为每个文件重新创建了一个带有 key = PBKDF2(pwd, nonce1, count=100_000) 的新密钥文件,这是浪费时间!
    • 我已尝试回答新问题。也许它有帮助。我的印象是,您首先需要为您的用例制定一个安全概念,然后才需要具体的实现。只是猜测,不一定是真的。
    【解决方案2】:

    这似乎很好,但我有一个建议,即不要使用相同的随机数进行加密和密钥派生(随机数代表仅使用一次的密钥,使用相同的随机数,因此您可以将随机数的 md5 哈希传递给加密如果您不想使用另一个随机数(IV),请改为使用函数。其次,如果您对更好的安全性感兴趣,我认为您可以切换到cryptography。这是使用cryptography模块进行加密的示例代码,它也具有加密的优点使用安全的128-bit 密钥,它负责其余部分,例如IV(nonce),解密和验证(使用HMAC 完成)。所以上面的所有代码都可以总结在这几行中降低复杂性,因此可以说是更安全的代码。

    from cryptography.fernet import Fernet
    plaintext = b"hello world"
    key = Fernet.generate_key()
    ctx = Fernet(key)
    ciphertext = ctx.encrypt(plaintext)
    print(ciphertext)
    decryption = ctx.decrypt(ciphertext)
    print(decryption)
    

    编辑:请注意,您使用的随机数也会削弱密钥,因为随机数是用密文发送的,现在用于PBKDF 的盐是没有意义的,现在攻击者只需猜测您的密码(假设使用默认计数) 在这种情况下是非常简单的一种,暴力破解的时间不会超过26^5 尝试(总长度为 5 的小写字母总数)。

    【讨论】:

    • 感谢您的回答。如果使用相同的随机数进行加密和密钥派生,会出现什么问题? PS:我已经在原始问题中使用pycryptodome。我也会和 Fernet 一起测试一下。
    • 另外,在您的代码中您不使用密钥派生,您可以更新为使用密码吗?
    • 最后一句话:你确定Fernet使用256位吗?据说是 128:crypto.stackexchange.com/questions/43120/…
    • @Basj 是的,我编辑了我的答案(没注意)。如果您对 2 条相同的消息(在相同的密钥下)使用相同的随机数,那么生成的密文将是相同的。这将导致加密是确定性的,而不是关于ECB 模式的概率性阅读
    • @Basj 看到这个链接是的,它使用了 256 cryptography.io/en/latest/fernet.html
    猜你喜欢
    • 2016-06-27
    • 2011-07-17
    • 2011-09-12
    • 2021-07-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多