使用CBC模式的AES-256与HMAC-SHA256
我最近看到了一段代码示例,讲的是如何用AES-256 CBC加密文件,并使用SHA-256 HMAC来进行身份验证和验证:
aes_key, hmac_key = self.keys
# create a PKCS#7 pad to get us to `len(data) % 16 == 0`
pad_length = 16 - len(data) % 16
data = data + (pad_length * chr(pad_length))
# get IV
iv = os.urandom(16)
# create cipher
cipher = AES.new(aes_key, AES.MODE_CBC, iv)
data = iv + cipher.encrypt(data)
sig = hmac.new(hmac_key, data, hashlib.sha256).digest()
# return the encrypted data (iv, followed by encrypted data, followed by hmac sig):
return data + sig
因为我这次要加密的不只是一个字符串,而是一个相当大的文件,所以我对代码做了一些修改,具体如下:
aes_key, hmac_key = self.keys
iv = os.urandom(16)
cipher = AES.new(aes_key, AES.MODE_CBC, iv)
with open('input.file', 'rb') as infile:
with open('output.file', 'wb') as outfile:
# write the iv to the file:
outfile.write(iv)
# start the loop
end_of_line = True
while True:
input_chunk = infile.read(64 * 1024)
if len(input_chunk) == 0:
# we have reached the end of the input file and it matches `% 16 == 0`
# so pad it with 16 bytes of PKCS#7 padding:
end_of_line = True
input_chunk += 16 * chr(16)
elif len(input_chunk) % 16 > 0:
# we have reached the end of the input file and it doesn't match `% 16 == 0`
# pad it by the remainder of bytes in PKCS#7:
end_of_line = True
input_chunk_remainder = 16 - (len(input_chunk) & 16)
input_chunk += input_chunk_remainder * chr(input_chunk_remainder)
# write out encrypted data and an HMAC of the block
outfile.write(cipher.encrypt(input_chunk) + hmac.new(hmac_key, data,
hashlib.sha256).digest())
if end_of_line:
break
简单来说,这段代码一次读取一个64KB的输入文件块,并对这些块进行加密,同时生成一个HMAC,使用SHA-256对加密后的数据进行处理,并在每个块后面附加这个HMAC。解密时,会读取64KB加32字节的块,计算前64KB的HMAC,并将其与块最后32字节的SHA-256值进行比较。
这样使用HMAC对吗?它能确保数据没有被修改,并且是用正确的密钥解密的吗?
顺便提一下,AES和HMAC的密钥都是从同一个密码短语生成的,这个密码短语是通过先用SHA-512处理输入文本,然后用bcrypt,再用SHA-512处理一次得到的。最后的SHA-512输出会被分成两部分,一部分用作AES密码,另一部分用作HMAC。
2 个回答
我觉得你用HMAC的做法没有什么安全问题(这并不意味着就没有安全隐患),但我不太明白HMAC在密文的子元素中具体有什么用。除非你想在数据被篡改时支持部分恢复明文,否则没必要对64KB的块进行HMAC处理,这样做会增加负担,相比于直接对完整的密文进行处理。
从生成密钥的角度来看,使用一个从密码短语生成的密钥来加密两个随机生成的密钥可能更合理,然后再用这两个随机生成的密钥来进行HMAC和AES操作。我知道用同一个密钥来做块加密和HMAC是不好的,但我不确定用同样方式生成的密钥是否也会有类似的问题。
至少,你应该调整一下你的密钥派生机制。bcrypt是用来处理密码的哈希,而不是用来生成密钥的函数。你应该使用PBKDF2来进行密钥的派生。
是的,这里有两个安全问题。
但首先,我假设你在最后这句话的意思是:
# write out encrypted data and an HMAC of the block
outfile.write(cipher.encrypt(input_chunk) + hmac.new(hmac_key, data, hashlib.sha256).digest())
你实际上是想说:
# write out encrypted data and an HMAC of the block
data = cipher.encrypt(input_chunk)
outfile.write(data + hmac.new(hmac_key, data, hashlib.sha256).digest())
因为 data
在任何地方都没有定义。
第一个安全问题是,你对每一部分的认证都是独立进行的,而不是对整体进行认证。换句话说,攻击者可以重新排列、复制或删除任何一部分,而接收者不会察觉到。
一个更安全的方法是只使用一个 HMAC 实例,把所有加密的数据通过 update
方法传给它,最后只输出一个摘要。
另外,如果你想让接收者在收到整个文件之前就能检测到篡改,可以为每一部分输出中间的 MAC。实际上,调用 digest
并不会改变 HMAC 的状态;你可以在之后继续调用 update
。
第二个安全问题是,你在生成密钥时没有使用盐(我这么说是因为你没有发送盐)。除了密码破解之外,如果你用同一个密码加密了超过两个文件,攻击者也能自由地混合这两个加密文件中的部分内容,因为 HMAC 的密钥是相同的。解决办法是:使用盐。
最后还有一个小问题:infile.read(64 * 1024)
可能返回的字节数少于 64*1024
,但这并不意味着你已经到达文件的末尾。