这是 AES GCM 文件加密的好习惯吗?
Is this AES GCM file encryption good practice?
我正在使用它加密文件,然后使用 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
...
这似乎没问题,但我有一个建议,即不要使用相同的随机数进行加密和密钥派生(随机数代表仅使用一次的密钥使用相同的随机数,因此您可以传递 md5
哈希如果您不想使用另一个 nonce(IV),则将 nonce 改为加密函数。其次,如果您对更好的安全性感兴趣,我认为您可以切换到 cryptography
。这是使用 cryptography
模块的示例代码encrypt 还具有使用安全的 128-bit
密钥进行加密的优点,它负责处理其余部分,例如 IV
(nonces)、解密和验证(使用 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)。
改进代码的建议是为 GCM 应用 12 字节的随机数。当前使用 16 字节的随机数,这应该更改,请参阅 here sec. Note, and here。
对于 GCM 的安全性至关重要的是没有 key/nonce 对被使用超过一次,here。由于在您的代码中每次加密都会生成一个随机数,因此可以避免此问题。
您的代码将 nonce 也用作密钥派生的盐,这在原则上没有安全问题,因为这不会导致多次使用相同的 key/nonce 对,。
然而,这样做的一个缺点可能是盐长度由随机数长度决定。如果不需要这样做(例如,如果应该使用更大的盐),另一种方法是为每次加密生成一个随机盐,以通过 KDF 派生密钥和随机数。在这种情况下,串联的数据 salt | ciphertext | tag
将被传递给接收者。另一种替代方法是将 nonce 和密钥生成完全分开,并为每次加密生成随机 nonce 和用于密钥生成的随机 salt。在这种情况下,必须将串联数据 salt | nonce | ciphertext | tag
传递给接收者。请注意,与随机数和标签一样,盐也不是秘密,因此它可以与密文一起发送。
代码应用 100,000 次迭代计数。通常,以下内容适用:迭代次数应尽可能高,您的环境可以容忍,同时保持可接受的性能,here。如果 100,000 符合您的环境的这个标准,那么这没问题。
您使用的连接顺序是 nonce | tag | ciphertext
。这不是问题,只要双方都知道这一点。通常按照惯例,使用 nonce | ciphertext | tag
顺序(例如 Java 隐式 将标签附加到密文),如果您愿意,也可以在代码中使用遵守这个约定。
使用最新的、维护的库也很重要,PyCryptodome 就是这种情况(不像它的前身,遗留的 PyCrypto,根本不应该使用)。
编辑:
PyCryptodome 的 PBKDF2 实现默认使用 16 个字节作为生成密钥的长度,对应于 AES-128。默认情况下应用摘要 HMAC/SHA1。发布的代码使用这些标准参数,none 其中不安全,但当然可以根据需要更改,here.
注意:虽然 SHA1 本身是不安全的,但这并不适用于上下文
PBKDF2 或 HMAC,here。但是,为了支持 SHA1 从生态系统中消失,可以使用 SHA256。
编辑:(关于问题的更新):
编辑后的问题中出现的用例是 10,000 个文件的加密。为每个文件执行发布的代码,以便通过 KDF 生成相应数量的密钥,从而导致相应的性能损失。这被您描述为 非常低效 。但是,不应忘记当前的代码侧重于安全性而不是性能。在我的回答中,我指出,例如迭代计数是一个参数,它允许在一定限度内调整性能和安全性。
PBKDF(基于密码的密钥派生函数)允许从弱密码派生密钥。为了确保加密安全,推导时间 有意 增加,这样攻击者就无法比强密钥(理想情况下)更快地破解弱密码。如果推导时间缩短(例如,通过减少迭代次数或多次使用相同的密钥),这通常会导致安全性降低。或者简而言之,性能提升(通过更快的 PBKDF)通常会降低安全性。这为更高性能(但更弱)的解决方案留有一定余地。
您建议的更高效的解决方案如下:和以前一样,为 each 文件生成一个 random 随机数。但不是使用 own 密钥加密 each 文件,而是使用 all 密钥加密 all 文件=]相同键。为此,生成随机盐 一次,通过 KDF 派生此密钥。这确实意味着显着的性能提升。但是,这会自动降低安全性:如果攻击者成功获得密钥,攻击者可以解密 所有 个文件(而不仅仅是 一个 与原始场景一样)。但是,如果在您的安全要求范围内可以接受(这里似乎就是这种情况),则此缺点不是强制性排除标准。
性能更高的解决方案要求必须将信息 salt | nonce | ciphertext | tag
发送给收件人。 salt 很重要,不能丢失,因为接收者需要 salt 才能通过 PBKDF 派生密钥。一旦收件人确定了密钥,就可以使用标签对密文进行身份验证,并使用随机数对其进行解密。如果接收方同意每个文件将使用相同的密钥,则接收方通过 PBKDF 导出密钥 一次 就足够了。否则必须为每个文件导出密钥。
如果不需要 16 字节的盐(因为在这种方法中它对所有文件都是相同的),可以考虑替代架构。例如,可以使用混合方案:使用 public 密钥基础设施生成并交换随机对称密钥。同样在这里,所有文件都可以用相同的密钥加密,或者每个文件都可以用自己的密钥加密。
但对于设计方案的更具体建议,应更详细地描述用例,例如关于文件:文件有多大?是否需要在 streams/chunks 中处理?或者关于收件人:有多少收件人?什么与收件人保持一致?等等
我正在使用它加密文件,然后使用 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
...
这似乎没问题,但我有一个建议,即不要使用相同的随机数进行加密和密钥派生(随机数代表仅使用一次的密钥使用相同的随机数,因此您可以传递 md5
哈希如果您不想使用另一个 nonce(IV),则将 nonce 改为加密函数。其次,如果您对更好的安全性感兴趣,我认为您可以切换到 cryptography
。这是使用 cryptography
模块的示例代码encrypt 还具有使用安全的 128-bit
密钥进行加密的优点,它负责处理其余部分,例如 IV
(nonces)、解密和验证(使用 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)。
改进代码的建议是为 GCM 应用 12 字节的随机数。当前使用 16 字节的随机数,这应该更改,请参阅 here sec. Note, and here。
对于 GCM 的安全性至关重要的是没有 key/nonce 对被使用超过一次,here。由于在您的代码中每次加密都会生成一个随机数,因此可以避免此问题。
您的代码将 nonce 也用作密钥派生的盐,这在原则上没有安全问题,因为这不会导致多次使用相同的 key/nonce 对,
然而,这样做的一个缺点可能是盐长度由随机数长度决定。如果不需要这样做(例如,如果应该使用更大的盐),另一种方法是为每次加密生成一个随机盐,以通过 KDF salt | ciphertext | tag
将被传递给接收者。另一种替代方法是将 nonce 和密钥生成完全分开,并为每次加密生成随机 nonce 和用于密钥生成的随机 salt。在这种情况下,必须将串联数据 salt | nonce | ciphertext | tag
传递给接收者。请注意,与随机数和标签一样,盐也不是秘密,因此它可以与密文一起发送。
代码应用 100,000 次迭代计数。通常,以下内容适用:迭代次数应尽可能高,您的环境可以容忍,同时保持可接受的性能,here。如果 100,000 符合您的环境的这个标准,那么这没问题。
您使用的连接顺序是 nonce | tag | ciphertext
。这不是问题,只要双方都知道这一点。通常按照惯例,使用 nonce | ciphertext | tag
顺序(例如 Java 隐式 将标签附加到密文),如果您愿意,也可以在代码中使用遵守这个约定。
使用最新的、维护的库也很重要,PyCryptodome 就是这种情况(不像它的前身,遗留的 PyCrypto,根本不应该使用)。
编辑:
PyCryptodome 的 PBKDF2 实现默认使用 16 个字节作为生成密钥的长度,对应于 AES-128。默认情况下应用摘要 HMAC/SHA1。发布的代码使用这些标准参数,none 其中不安全,但当然可以根据需要更改,here.
注意:虽然 SHA1 本身是不安全的,但这并不适用于上下文
PBKDF2 或 HMAC,here。但是,为了支持 SHA1 从生态系统中消失,可以使用 SHA256。
编辑:(关于问题的更新):
编辑后的问题中出现的用例是 10,000 个文件的加密。为每个文件执行发布的代码,以便通过 KDF 生成相应数量的密钥,从而导致相应的性能损失。这被您描述为 非常低效 。但是,不应忘记当前的代码侧重于安全性而不是性能。在我的回答中,我指出,例如迭代计数是一个参数,它允许在一定限度内调整性能和安全性。
PBKDF(基于密码的密钥派生函数)允许从弱密码派生密钥。为了确保加密安全,推导时间 有意 增加,这样攻击者就无法比强密钥(理想情况下)更快地破解弱密码。如果推导时间缩短(例如,通过减少迭代次数或多次使用相同的密钥),这通常会导致安全性降低。或者简而言之,性能提升(通过更快的 PBKDF)通常会降低安全性。这为更高性能(但更弱)的解决方案留有一定余地。
您建议的更高效的解决方案如下:和以前一样,为 each 文件生成一个 random 随机数。但不是使用 own 密钥加密 each 文件,而是使用 all 密钥加密 all 文件=]相同键。为此,生成随机盐 一次,通过 KDF 派生此密钥。这确实意味着显着的性能提升。但是,这会自动降低安全性:如果攻击者成功获得密钥,攻击者可以解密 所有 个文件(而不仅仅是 一个 与原始场景一样)。但是,如果在您的安全要求范围内可以接受(这里似乎就是这种情况),则此缺点不是强制性排除标准。
性能更高的解决方案要求必须将信息 salt | nonce | ciphertext | tag
发送给收件人。 salt 很重要,不能丢失,因为接收者需要 salt 才能通过 PBKDF 派生密钥。一旦收件人确定了密钥,就可以使用标签对密文进行身份验证,并使用随机数对其进行解密。如果接收方同意每个文件将使用相同的密钥,则接收方通过 PBKDF 导出密钥 一次 就足够了。否则必须为每个文件导出密钥。
如果不需要 16 字节的盐(因为在这种方法中它对所有文件都是相同的),可以考虑替代架构。例如,可以使用混合方案:使用 public 密钥基础设施生成并交换随机对称密钥。同样在这里,所有文件都可以用相同的密钥加密,或者每个文件都可以用自己的密钥加密。
但对于设计方案的更具体建议,应更详细地描述用例,例如关于文件:文件有多大?是否需要在 streams/chunks 中处理?或者关于收件人:有多少收件人?什么与收件人保持一致?等等