Python AES:如何检测输入了错误的密码?

Python AES:如何检测输入了错误的密码?,python,encryption,cryptography,aes,Python,Encryption,Cryptography,Aes,文本s已通过以下方式加密: s2 = iv + Crypto.Cipher.AES.new(Crypto.Hash.SHA256.new(pwd).digest(), Crypto.Cipher.AES.MODE_CFB, iv).encrypt(s.encode()) 然后,稍后,用户输入密码pwd2,我们用以下方法解密: iv, cipher

文本
s
已通过以下方式加密:

s2 = iv + Crypto.Cipher.AES.new(Crypto.Hash.SHA256.new(pwd).digest(), 
                                    Crypto.Cipher.AES.MODE_CFB, 
                                    iv).encrypt(s.encode())
然后,稍后,用户输入密码
pwd2
,我们用以下方法解密:

iv, cipher = s2[:Crypto.Cipher.AES.block_size], s2[Crypto.Cipher.AES.block_size:]

s3 = Crypto.Cipher.AES.new(Crypto.Hash.SHA256.new(pwd2).digest(),
                           Crypto.Cipher.AES.MODE_CFB, 
                           iv).decrypt(cipher)
问题:即使输入的密码
pw2
错误,最后一行仍然有效。当然,解密的文本将是随机字符,但不会触发错误

问题:如果密码
pw2
不正确,如何使
Crypto.Cipher.AES.new(…).decrypt(Cipher)
失败?或者至少如何检测错误的密码


以下是一个相关问题:
这里讨论的是问题的加密部分(较少编程):.

AES提供了机密性,但不是开箱即用的完整性-要获得完整性,您也有一些选择。最简单也最不容易“射中你自己的脚”的方法就是使用AES-GCM-参见或

您也可以使用HMAC,但这通常需要管理两个不同的关键点,并且还有几个运动部件。如果您有第一种选择,我会推荐它

另外,在将用户创建的密码转换为加密密钥时,SHA-256不是一个很好的KDF。流行的密码散列算法在这方面做得更好——看看Argon2、bcrypt或PBKDF2


编辑:SHA-256是一个坏KDF的原因与它生成一个坏密码哈希函数的原因相同——它太快了。例如,用户创建的128位密码通常比128位随机序列包含的熵小得多——人们喜欢选择单词、有意义的序列等。用SHA-256散列一次并不能真正缓解这个问题。但是,使用类似Argon2的结构对其进行哈希运算,这种结构设计得非常慢,这使得暴力攻击的可行性大大降低。

为了将来的参考,下面是一个可行的解决方案(由@LukeJoshuaPark在其回答中推荐):

当密码确实是错误的时,它会失败,但有一个例外,正如所希望的那样


以下代码使用真实密码派生函数:

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

def cipherAES(pwd, nonce):
    return Crypto.Cipher.AES.new(Crypto.Protocol.KDF.PBKDF2(pwd, nonce, count=100000), Crypto.Cipher.AES.MODE_GCM, nonce=nonce)

# encryption
nonce = Crypto.Random.new().read(16)
cipher = cipherAES(b'pwd1', nonce)
ciphertext, tag = cipher.encrypt_and_digest(b'bonjour')

# decryption
try:
    cipher = cipherAES(b'pwd1', nonce=nonce)
    plaintext = cipher.decrypt_and_verify(ciphertext, tag)
    print("The message was: " + plaintext.decode())
except ValueError:
    print("Wrong password")

@fgrieu的可能更好,因为它使用了
scrypt
作为KDF。

最好的方法是使用经过身份验证的加密,以及一个现代的内存硬熵拉伸密钥派生函数,如scrypt,将密码转换为密钥。密码的名词可以用作密钥派生的盐。这可能是:

from Crypto.Random       import get_random_bytes
from Crypto.Cipher       import AES
from Crypto.Protocol.KDF import scrypt

# initialize an AES-128-GCM cipher from password (derived using scrypt) and nonce
def cipherAES(pwd, nonce):
    # note: the p parameter should allow use of several processors, but did not for me
    # note: changing 16 to 24 or 32 should select AES-192 or AES-256 (not tested)
    return AES.new(scrypt(pwd, nonce, 16, N=2**21, r=8, p=1), AES.MODE_GCM, nonce=nonce)

# encryption
nonce = get_random_bytes(16)
print("deriving key from password and nonce, then encrypting..")
ciphertext, tag = cipherAES(b'pwdHklot2',nonce).encrypt_and_digest(b'bonjour')
print("done")

# decryption of nonce, ciphertext, tag
print("deriving key from password and nonce, then decrypting..")
try:
    plaintext = cipherAES(b'pwdHklot2', nonce).decrypt_and_verify(ciphertext, tag)
    print("The message was: " + plaintext.decode())
except ValueError:
    print("Wrong password or altered nonce, ciphertext, tag")
print("done")
注意:这里的代码是用来说明原理的。具体地说,scrypt参数不应该是固定的,而应该包括在nonce、密文和标记之前的报头中;必须对其进行分组以进行发送,并对其进行解析以进行解密

警告:本文中的任何内容都不应被理解为对PyCryptodome安全性的认可


添加(每个):

我们之所以需要scrypt或其他形式的熵拉伸,仅仅是因为我们使用了密码。我们可以直接使用一个随机的128位密钥

具有100000次迭代的PBKDF2 HMAC SHAn(如OP的第二个代码片段中所示)仅能通过几个GPU抵抗Hashcat。与ASIC辅助攻击的其他障碍相比,这几乎可以忽略不计:最先进的比特币挖掘ASIC每焦耳的功耗超过2*1010 SHA-256,成本低于0.15美元的1 kWh电量为36*105 J。计算这些数字,测试(62(8+1)-1)/(62-1)=22191945157891限制为字母和数字的最多8个字符的密码,专用于哈希部分的能量成本不到47美元

对于合法用户花费的同等时间来说,它更安全,因为它需要大量内存和访问内存,减慢了攻击者的速度,最重要的是使大规模并行攻击的投资成本猛增。

不使用该软件包,但这应能满足您的需要:

导入base64
导入操作系统
从cryptography.fernet导入fernet
从cryptography.hazmat.backends导入默认\u后端
从cryptography.hazmat.primitives.kdf.scrypt导入scrypt
def导出_密码(密码:字节,salt:字节):
"""
根据希望派生所需的时间调整N参数。
scrypt文件建议交互登录的最小值为n=2**14(t<100ms),
或n=2**20表示更敏感的文件(t<5s)。
"""
kdf=Scrypt(salt=salt,长度=32,n=2**16,r=8,p=1,backend=default_backend())
key=kdf.derivate(密码)
返回base64.urlsafe_b64encode(键)
盐=铀氧化物(16)
密码=b'Legoroj'
错误的密码=b'Legoroj2'
#派生密码
密钥=导出密码(密码,salt)
key2=deriver_password(bad_password,salt)#不应重复使用salt,但这仅用于示例目的
#创建Fernet对象
f=Fernet(键)
msg=b“这是一条测试消息”
密文=f.encrypt(msg)
print(msg,flush=True)#flush将其推送到stdout,因此会出现错误
打印(密文,刷新=真)
#Fernet只能使用一次,因此我们需要重新初始化
f=Fernet(键)
明文=f.解密(密文)
打印(纯文本,齐平=真)
#坏键
f=Fernet(键2)
f、 解密(密文)
"""
这将引发InvalidToken和InvalidSignature,这意味着它没有正确解密。
"""

有关文档的链接,请参见my。

谢谢您的回答。PS:为什么SHA256是一个坏的密钥派生函数?PS:在您链接的示例中,您将如何测试完整性?您会使用
nonce
标签
?在这个例子中,你能给出一个完整性测试的例子吗(在代码或伪代码中)?在我链接的例子中,完整性或多或少是“烘焙”的-当你执行解密操作时,只有两件事会发生:1)解密成功,因为密钥/密码、密文、nonce、标记,salt等都是正确的,或者2)解密抛出异常,因为其中一项不正确。您在问题中描述的场景在链接的代码中是不可能的。好吗
from Crypto.Random       import get_random_bytes
from Crypto.Cipher       import AES
from Crypto.Protocol.KDF import scrypt

# initialize an AES-128-GCM cipher from password (derived using scrypt) and nonce
def cipherAES(pwd, nonce):
    # note: the p parameter should allow use of several processors, but did not for me
    # note: changing 16 to 24 or 32 should select AES-192 or AES-256 (not tested)
    return AES.new(scrypt(pwd, nonce, 16, N=2**21, r=8, p=1), AES.MODE_GCM, nonce=nonce)

# encryption
nonce = get_random_bytes(16)
print("deriving key from password and nonce, then encrypting..")
ciphertext, tag = cipherAES(b'pwdHklot2',nonce).encrypt_and_digest(b'bonjour')
print("done")

# decryption of nonce, ciphertext, tag
print("deriving key from password and nonce, then decrypting..")
try:
    plaintext = cipherAES(b'pwdHklot2', nonce).decrypt_and_verify(ciphertext, tag)
    print("The message was: " + plaintext.decode())
except ValueError:
    print("Wrong password or altered nonce, ciphertext, tag")
print("done")