【问题标题】:How AES in CTR works for Python with PyCrypto?CTR 中的 AES 如何用于 Python 和 PyCrypto?
【发布时间】:2012-10-02 13:24:53
【问题描述】:

我正在使用 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 与我的明文字节大小一样多的次数,才能正确获取解密数据。即:

encrypted = crypto.encrypt("test")
print crypto.decrypt(encrypted)
print crypto.decrypt(encrypted)
print crypto.decrypt(encrypted)
print crypto.decrypt(encrypted)

最后一次解密调用会将明文返回给我。解密的其他输出是一些乱码字符串。 我想知道这是否正常?我是否必须每次都包含在一个大小等于我的明文的循环中,或者我弄错了什么?

【问题讨论】:

    标签: python encryption cryptography aes pycrypto


    【解决方案1】:

    我将详细说明@gertvdijk 对密码为何表现出原始问题中的行为方式的解释(我的编辑被拒绝),但还要指出将计数器设置为返回静态值是一个主要问题缺陷并展示如何正确设置。

    为新操作重置计数器

    这样做的原因与您在问题中描述的那样是因为您的纯文本(4 字节/32 位)是 CTR 密码输出用于加密的密钥流块大小的四倍(16 字节/ 128 位)。

    因为您一遍又一遍地使用相同的固定值而不是实际的计数器,所以密码会不断输出相同的 16 字节密钥流块。您可以通过重复加密 16 个空字节来观察这一点:

     >>> crypto.encrypt('\x00'*16)
    '?\\-\xdc\x16`\x05p\x0f\xa7\xca\x82\xdbE\x7f/'
    >>> crypto.encrypt('\x00'*16)
    '?\\-\xdc\x16`\x05p\x0f\xa7\xca\x82\xdbE\x7f/'
    

    您也不会在执行解密之前重置密码的状态,因此 4 字节的密文将根据来自第一个输出流块的接下来的 4 字节 XOR 密钥进行解密。这也可以通过加密和解密空字节来观察:

     >>> crypto.encrypt('\x00' * 4)
    '?\\-\xdc'
    >>> crypto.decrypt('\x00' * 4)
    '\x16`\x05p'
    

    如果这按您想要的方式工作,那么这两个操作的结果应该是相同的。相反,您可以在第一个结果中看到 16 字节块的前四个字节,在第二个结果中看到后四个字节。

    在您通过对 4 字节值执行四次操作(总共 16 字节)用完 16 字节的 XOR 键块后,会生成一个新的 XOR 键块。每个 XOR 密钥块的前四个字节(以及所有其他字节)都是相同的,所以这次调用解密时,它会返回明文。

    这真的很糟糕!您不应该以这种方式使用 AES-CTR - 它等同于使用 16 字节重复密钥的简单 XOR 加密,很容易被破解。

    解决方案

    在对新的数据流执行操作(或对其进行其他操作)之前,您必须重置密码的状态,因为原始实例将不再处于正确的初始状态。您的问题将通过为解密实例化一个新的crypto 对象以及重置计数器和密钥流位置来解决。

    您还需要使用适当的计数器函数,该函数将随机数与每次生成新的密钥流块时增加的计数器值相结合。 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"
    

    【讨论】:

      【解决方案2】:

      从一个新的加密对象开始进行新的操作

      这样做的原因与您在问题中描述的那样是因为您的纯文本(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 进行填充

      这个答案最初是对the answer by Marcus 的回复,他最初表示使用填充可以解决这个问题。虽然我知道这看起来像是填充问题的症状,但它肯定不是。

      AES-CTR 的全部意义在于您不需要填充,因为它是一个流密码(与 ECB/CBC 等不同)!流密码处理数据流,而不是将数据分块并在实际的密码计算中链接它们。

      【讨论】:

        【解决方案3】:

        除了 Marcus 所说的,Crypto.Util.Counter 类可用于构建您的计数器块功能。

        【讨论】:

          【解决方案4】:

          根据@gertvdijk 的说法,AES_CTR 是一种不需要需要填充的流密码。所以我删除了相关代码。

          这是我知道的。

          1. 加密和解密必须使用相同的密钥(AES.new(...)中的第一个参数),并保持密钥的私密性。

          2. 加密/解密方法是有状态的,这意味着crypto.en(de)crypt("abcd")==crypto.en(de)crypt("abcd")并非始终为真。在您的 CTR 中,您的计数器回调始终返回相同的内容,因此在加密时它变得无状态(我不是 100% 确定这是原因),但我们仍然发现它在解密时有点有状态。作为结论,我们应该始终使用新对象来完成它们。

          3. 加密和解密中的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

          【讨论】:

          • 我无法理解您的填充函数...为什么需要与预定义表达式的 chr() 相乘?
          • @curious,我们需要知道unpad时增加的长度,我们用char存储(不会大于16,所以一个字节就够了,我们用它的ascii码)。 (BS - len(s) % BS)是添加的长度,chr(...)返回一个字节(实际上是python中的字符串),然后我们添加一个X长度的字符串,其中所有的char都是chr(X)。
          • 但这是一种安全的填充技术吗?
          • 我在考虑这个limited-entropy.com/padding-oracle-attacks,但它仅适用于 CBC 模式
          • @curious,到目前为止我认为攻击的关键点是pad-oracle,而不是padding函数,所以两种可能的解决方案可以是1.让我们的pad oracle永远不会拒绝任何输入,或 2. 在加密数据中添加身份验证信息,即enc=secret+md5sum(secret+private_key)+enc_raw,以便始终对无效输入说不。
          猜你喜欢
          • 1970-01-01
          • 2011-03-10
          • 1970-01-01
          • 1970-01-01
          • 2020-08-13
          • 1970-01-01
          • 2012-07-24
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多