我使用的是python 2.7.1我想在CTR模式下使用AES加密某物。我为python安装了PyCrypto库。我写了以下代码:
secret = os.urandom(16)
crypto = AES.new(os.urandom(32), AES.MODE_CTR, counter=lambda: secret)
encrypted = crypto.encrypt("asdk")
print crypto.decrypt(encrypted)
我必须运行crypto.decrypt的次数与明文的字节大小一样多,才能正确地获得解密的数据。I.e:
encrypted = crypto.encrypt("test")
print crypto.decrypt(encrypted)
print crypto.decrypt(encrypted)
print crypto.decrypt(encrypted)
print crypto.decrypt(encrypted)
最后一个解密调用将返回明文。解密的其他输出是一些胡言乱语的字符串。我想知道这是否正常?我每次都必须包含到一个大小等于明文的循环中,还是我做错了什么?
我将详细阐述@gertvdijk对密码为什么会像原来问题中那样表现的解释(我的编辑被拒绝了),但也指出设置计数器返回静态值是一个主要缺陷,并展示如何正确设置它。
为新操作重置计数器
这种情况之所以如您在问题中所述,是因为您的纯文本(4字节/32位)是CTR密码输出用于加密的密钥流块大小(16字节/128位)的四倍。
因为您一次又一次地使用相同的固定值,而不是实际的计数器,所以密码不断地吐出相同的16字节密钥流块。您可以通过重复加密16个空字节来观察到这一点:
>>> crypto.encrypt('x00'*16)
'?\-xdcx16`x05px0fxa7xcax82xdbEx7f/'
>>> crypto.encrypt('x00'*16)
'?\-xdcx16`x05px0fxa7xcax82xdbEx7f/'
在执行解密之前,您也不会重置密码的状态,因此4个字节的密文将根据第一个输出流块中的下4个字节XOR密钥进行解密。这也可以通过加密和解密空字节来观察到:
>>> crypto.encrypt('x00' * 4)
'?\-xdc'
>>> crypto.decrypt('x00' * 4)
'x16`x05p'
如果这是按照你想要的方式工作,那么这两个操作的结果应该是相同的。相反,您可以在第一个结果中看到16字节块的前四个字节,在第二个结果中可以看到第二四个字节。
通过对四个字节的值执行四次操作(总共16个字节),用完了16个字节的XOR密钥块后,将生成一个新的XOR键块。每个XOR密钥块的前四个字节(以及所有其他字节)是相同的,所以当您这次调用decrypt时,它会返回明文。
这真的很糟糕您不应该以这种方式使用AES-CTR——它相当于使用16字节重复密钥的简单XOR加密,很容易被破坏。
解决方案
在对新数据流执行操作(或对其执行其他操作)之前,必须重置密码的状态,因为原始实例将不再处于正确的初始状态。您的问题将通过实例化一个新的crypto
对象进行解密,以及重置计数器和密钥流位置来解决。
您还需要使用适当的计数器函数,该函数将nonce与计数器值相结合,计数器值在每次生成新的密钥流块时都会增加。PyCrypto有一个Counter类,可以为您完成此操作。
from Crypto.Cipher import AES
from Crypto.Util import Counter
from Crypto import Random
# Set up the counter with a nonce.
# 64 bit nonce + 64 bit counter = 128 bit output
nonce = Random.get_random_bytes(8)
countf = Counter.new(64, nonce)
key = Random.get_random_bytes(32) # 256 bits key
# Instantiate a crypto object first for encryption
encrypto = AES.new(key, AES.MODE_CTR, counter=countf)
encrypted = encrypto.encrypt("asdk")
# Reset counter and instantiate a new crypto object for decryption
countf = Counter.new(64, nonce)
decrypto = AES.new(key, AES.MODE_CTR, counter=countf)
print decrypto.decrypt(encrypted) # prints "asdk"
从用于新操作的新加密对象开始
这之所以会像你在问题中描述的那样,是因为你的纯文本(4字节/32位)是加密引擎为你选择的AES模式工作的大小(128位)的四倍,并且还重用了加密对象的相同实例。如果您正在对新的数据流执行操作(或对其执行另一个操作),请不要重用同一个对象。您的问题将通过实例化一个新的crypto
对象进行解密来解决,如下所示:
# *NEVER* USE A FIXED LIKE COUNTER BELOW IN PRODUCTION CODE. READ THE DOCS.
counter = os.urandom(16)
key = os.urandom(32) # 256 bits key
# Instantiate a crypto object first for encryption
encrypto = AES.new(key, AES.MODE_CTR, counter=lambda: counter)
encrypted = encrypto.encrypt("asdk")
# Instantiate a new crypto object for decryption
decrypto = AES.new(key, AES.MODE_CTR, counter=lambda: counter)
print decrypto.decrypt(encrypted) # prints "asdk"
为什么不是用AES-CTR填充
这个答案一开始是对Marcus的回答的回应,他最初表示使用填充可以解决这个问题。虽然我知道这看起来像是填充问题的症状,但事实并非如此。
AES-CTR的全部意义在于,您不需要填充,因为它是流密码(与ECB/CBC等不同)!流密码处理数据流,而不是将数据分块并在实际的加密计算中链接。
除了Marcus所说的,Crypto.Util.Counter
类还可以用于构建计数器块函数。
根据@gertvdijk的说法,AES_CTR是一种流密码,它不需要填充。所以我删除了相关代码。
这是我知道的。
-
在加密和解密时必须使用相同的密钥(
AES.new(...)
中的第一个参数),并保持密钥私有。 -
加密/解密方法是有状态的,这意味着
crypto.en(de)crypt("abcd")==crypto.en(de)crypt("abcd")
不是始终为真。在你的CTR中,你的计数器回调总是返回相同的东西,所以它在加密时变成无状态的(我不能100%确定这是原因),但我们仍然发现它在解密时有一定的状态。总之,我们应该总是用一个新的对象来做它们。 -
加密和解密中的
counter callback
函数的行为应该相同。在你的情况下,这是为了让他们两个都返回相同的秘密。但我不认为secret
是一个"秘密"。您可以使用随机生成的"secret"
,并在通信对等体之间传递它,而无需任何加密,这样对方就可以直接使用它,只要secret
是不可预测的。
所以我会这样写我的密码,希望它能提供一些帮助。
import os
import hashlib
import Crypto.Cipher.AES as AES
class Cipher:
@staticmethod
def md5sum( raw ):
m = hashlib.md5()
m.update(raw)
return m.hexdigest()
BS = AES.block_size
@staticmethod
def pad( s ):
"""note that the padding is no necessary"""
"""return s + (Cipher.BS - len(s) % Cipher.BS) * chr(Cipher.BS - len(s) % Cipher.BS)"""
return s
@staticmethod
def unpad( s ):
"""return s[0:-ord(s[-1])]"""
return s
def __init__(self, key):
self.key = Cipher.md5sum(key)
#the state of the counter callback
self.cnter_cb_called = 0
self.secret = None
def _reset_counter_callback_state( self, secret ):
self.cnter_cb_called = 0
self.secret = secret
def _counter_callback( self ):
"""
this function should be stateful
"""
self.cnter_cb_called += 1
return self.secret[self.cnter_cb_called % Cipher.BS] * Cipher.BS
def encrypt(self, raw):
secret = os.urandom( Cipher.BS ) #random choose a "secret" which is not secret
self._reset_counter_callback_state( secret )
cipher = AES.new( self.key, AES.MODE_CTR, counter = self._counter_callback )
raw_padded = Cipher.pad( raw )
enc_padded = cipher.encrypt( raw_padded )
return secret+enc_padded #yes, it is not secret
def decrypt(self, enc):
secret = enc[:Cipher.BS]
self._reset_counter_callback_state( secret )
cipher = AES.new( self.key, AES.MODE_CTR, counter = self._counter_callback )
enc_padded = enc[Cipher.BS:] #we didn't encrypt the secret, so don't decrypt it
raw_padded = cipher.decrypt( enc_padded )
return Cipher.unpad( raw_padded )
一些测试:
>>> from Cipher import Cipher
>>> x = Cipher("this is key")
>>> "a"==x.decrypt(x.encrypt("a"))
True
>>> "b"==x.decrypt(x.encrypt("b"))
True
>>> "c"==x.decrypt(x.encrypt("c"))
True
>>> x.encrypt("a")==x.encrypt("a")
False #though the input is same, the outputs are different
参考:http://packages.python.org/pycrypto/Crypto.Cipher.blockalgo-module.html#MODE_CTR