【问题标题】:PyCrypto problem using AES+CTR使用 AES+CTR 的 PyCrypto 问题
【发布时间】:2011-03-10 11:01:38
【问题描述】:

我正在编写一段代码来使用对称加密来加密文本。但它并没有以正确的结果回来......

from Crypto.Cipher import AES
import os

crypto = AES.new(os.urandom(32), AES.MODE_CTR, counter = lambda : os.urandom(16))
encrypted = crypto.encrypt("aaaaaaaaaaaaaaaa")
print crypto.decrypt(encrypted)

这里,解密后的文字与原文不同。

我不太了解密码学,所以请多多包涵。我知道 CTR 模式需要一个“计数器”功能来每次提供一个随机计数器,但是当我的密钥是 32 个字节并且它坚持我的消息也是 16 个字节的倍数时,为什么它需要它是 16 个字节?这正常吗?

我猜它不会回到原始消息,因为计数器在加密和解密之间发生了变化。但是,无论如何,它在理论上应该如何工作?我究竟做错了什么?无论如何,我不得不求助于欧洲央行,直到我弄清楚这一点:(

【问题讨论】:

    标签: python cryptography aes encryption-symmetric pycrypto


    【解决方案1】:

    AES 是block cipher:它是一种算法(更准确地说,是一对算法),它接受一个密钥和一个消息块并加密或解密该块。无论密钥大小如何,块的大小始终为 16 字节。

    点击率是mode of operation。它是一种基于分组密码生成流密码的算法,可以对任意长度的消息进行加密和解密。

    CTR 通过将连续消息块与计数器连续值的加密相结合来工作。计数器的大小必须是一个块,以便它是块密码的有效输入。

    • 从功能上讲,计数器的连续值是什么并不重要,只要加密和解密端使用相同的序列即可。通常计数器被视为一个 256 位的数字,并为每个连续的块递增,并随机选择一个初始值。因此,通常情况下,递增的方法被嵌入到代码中,但解密方需要知道初始值是什么,因此加密方在加密消息的开头发送或存储初始计数器值。
    • 为了安全起见,切勿使用给定的密钥重复相同的计数器值,这一点至关重要。因此,对于一次性密钥,以'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' 开头是可以的。但是,如果密钥被多次使用,则第二条消息不允许重用第一条消息使用的任何计数器值,最简单的方法是随机生成初始计数器值(使用 2^128空间,碰撞的可能性可以忽略不计)。

    通过让调用者选择一个计数器函数,PyCrypto 库为您提供了很多可以吊死自己的绳索。您应该使用Crypto.Util.Counter,而不仅仅是文档中所说的“以获得更好的性能”,而是因为构建安全的东西比您自己可能想出的东西更容易。即便如此,请注意使用随机初始值,这不是默认值。

    import binascii
    import os
    from Crypto.Cipher import AES
    from Crypto.Util import Counter
    def int_of_string(s):
        return int(binascii.hexlify(s), 16)
    def encrypt_message(key, plaintext):
        iv = os.urandom(16)
        ctr = Counter.new(128, initial_value=int_of_string(iv))
        aes = AES.new(key, AES.MODE_CTR, counter=ctr)
        return iv + aes.encrypt(plaintext)
    def decrypt_message(key, ciphertext):
        iv = ciphertext[:16]
        ctr = Counter.new(128, initial_value=int_of_string(iv))
        aes = AES.new(key, AES.MODE_CTR, counter=ctr)
        return aes.decrypt(ciphertext[16:])
    

    【讨论】:

    • int_of_string(iv) 在 Python 3 中可以替换为 int.from_bytes(iv, byteorder='big')
    【解决方案2】:

    为什么我的 key 是 32 字节时它需要是 16 字节

    它必须与密码的块大小相同。 CTR 模式只是对计数器进行加密,并将明文与加密的计数器块进行异或。

    注意事项:

    1. 计数器值必须是唯一的——如果您曾经使用相同的计数器值在同一个密钥下加密两个不同的明文,那么您只是泄露了您的密钥。
    2. 与 IV 一样,计数器不是秘密的 - 只需将其与密文一起发送即可。如果您试图将代码保密,从而使代码变得更加复杂,您可能会自取其辱。
    3. 计数器值不必是不可预测的——从零开始,为每个块加一就可以了。但是请注意,如果您加密多条消息,则需要跟踪已经使用的计数器值,即您需要跟踪已经使用该密钥加密了多少块(并且您不能使用相同的键入程序的不同实例或在不同的机器上)。
    4. 纯文本可以是任意长度 -- CTR 模式将分组密码转换为流密码。

    标准免责声明:加密很难。如果你不明白你在做什么,你弄错。

    我只想跨会话存储一些密码。

    使用 scrypt。 scrypt 包括 encryptdecrypt,它们使用 AES-CTR 和密码派生密钥。

    $ pip install scrypt
    
    $ python
    >>> import scrypt
    >>> import getpass
    >>> pw = getpass.getpass("enter password:")
    enter password:
    >>> encrypted = scrypt.encrypt("Guido is a space alien.",pw)
    >>> out = scrypt.decrypt(encrypted,pw)
    >>> out
    'Guido is a space alien.'
    

    【讨论】:

      【解决方案3】:

      counter 必须在解密时返回与加密时相同的结果,正如您的直觉,因此,一种(根本不安全)方法是:

      >>> secret = os.urandom(16)
      >>> crypto = AES.new(os.urandom(32), AES.MODE_CTR, counter=lambda: secret)
      >>> encrypted = crypto.encrypt("aaaaaaaaaaaaaaaa")
      >>> print crypto.decrypt(encrypted)
      aaaaaaaaaaaaaaaa
      

      CTR 是一个 block 密码,因此让您感到惊讶的“一次 16 个”约束是一个很自然的约束。

      当然,所谓的“计数器”会在每次调用is grossly insecure 时返回相同 值。不需要太多就可以做得更好,例如....:

      import array
      
      class Secret(object):
        def __init__(self, secret=None):
          if secret is None: secret = os.urandom(16)
          self.secret = secret
          self.reset()
        def counter(self):
          for i, c in enumerate(self.current):
            self.current[i] = c + 1
            if self.current: break
          return self.current.tostring()
        def reset(self):
          self.current = array.array('B', self.secret)
      
      secret = Secret()
      crypto = AES.new(os.urandom(32), AES.MODE_CTR, counter=secret.counter)
      encrypted = crypto.encrypt(16*'a' + 16*'b' + 16*'c')
      secret.reset()
      print crypto.decrypt(encrypted)
      

      【讨论】:

      • 太棒了。好的我明白了。所以本质上,如果我只想加密一件或很少的东西,CTR 对 ECB 没有优势?我只想跨会话存储一些密码。我什至需要 AES 还是应该使用更简单的东西?
      • 其实CTR可以加密任意数量的文本;它将分组密码转换为密钥流生成器。在这种情况下,没有实际理由限制输入是块大小的倍数。
      • 然而,当输入不是 16 字节的倍数时,PyCrypto 似乎会出错
      • 是的,所以如果你想使用 pycrypto,你只需要填充输入(到 16 的下一个最接近的倍数)。
      • 但如果我使用 AES 跨会话加密密码,我还必须将计数器保存在某个地方以便在下次运行时解密?这不会破坏随机计数器的安全性吗?
      【解决方案4】:

      我可能肯定迟到了,我可能忽略了以前的答案,但我没有找到一个明确的说明,说明应该如何(至少恕我直言)根据 PyCrypto 包完成。

      Crypto.Util.Counter 包提供了可调用的有状态计数器,它们非常有用,但至少我很容易不正确地使用它们。

      您必须创建一个计数器,例如ctr = Counter.new('parameters here')。每次您的计数器模式密码对象调用您的计数器来加密消息时,它都会递增。这是良好的加密实践所必需的,否则有关相等块的信息可能会从密文中泄漏。

      现在你不能在同一个密码对象上调用解密函数,因为它会再次调用同一个计数器,而这个计数器同时已经增加了,可能会增加几次。您需要做的是创建一个新的密码对象,该对象具有使用相同参数初始化的不同计数器。通过这种方式,解密工作正常,从加密完成的同一点开始计数器。

      下面的工作示例:

      # Import modules
      from Crypto.Cipher import AES
      from Crypto import Random
      from Crypto.Util import Counter
      
      
      # Pad for short keys
      pad = '# constant pad for short keys ##'
      
      # Generate a random initialization vector, to be used by both encryptor and decryptor
      # This may be sent in clear in a real communication
      
      random_generator = Random.new()
      IV = random_generator.read(8)
      
      
      # Encryption steps
      
      # Ask user for input and pad or truncate to a 32 bytes (256 bits) key
      prompt = 'Input your key. It will padded or truncated at 32 bytes (256 bits).\n-: '
      user_keye = raw_input(prompt)
      keye = (user_keye + pad)[:32]
      
      # Create counter for encryptor
      ctr_e = Counter.new(64, prefix=IV)
      
      # Create encryptor, ask for plaintext to encrypt, then encrypt and print ciphertext
      encryptor = AES.new(keye, AES.MODE_CTR, counter=ctr_e)
      plaintext = raw_input('Enter message to cipher: ')
      ciphertext = encryptor.encrypt(plaintext)
      print ciphertext
      print
      
      
      # Decryption steps
      
      # Ask user for key: it must be equal to that used for encryption
      prompt = 'Input your key. It will padded or truncated at 32 bytes (256 bits).\n-: '
      user_keyd = raw_input(prompt)
      keyd = (user_keyd + pad)[:32]
      
      # Create counter for decryptor: it is equal to the encryptor, but restarts from the beginning
      
      ctr_d = Counter.new(64, prefix=IV)
      
      # Create decryptor, then decrypt and print decoded text
      decryptor = AES.new(keyd, AES.MODE_CTR, counter=ctr_d)
      decoded_text = decryptor.decrypt(ciphertext)
      print decoded_text
      

      【讨论】:

        【解决方案5】:

        初始化向量(“计数器”)需要在加密和解密之间保持不变,就像密钥一样。使用它可以使您可以对相同的文本进行一百万次编码,并且每次都获得不同的密文(防止一些已知的明文攻击和模式匹配/攻击)。解密时仍需要使用与加密时相同的 IV。通常,当您开始解密流时,会将 IV 初始化为与开始加密该流时相同的值。

        有关初始化向量的信息,请参阅 http://en.wikipedia.org/wiki/Initialization_vector

        请注意,os.urandom(16) 不是“确定性的”,这是计数器功能的要求。我建议您使用增量功能,因为这就是 CTR 模式的设计方式。初始计数器值应该是随机的,但后续值应该可以从初始值完全预测(确定性)。初始值甚至可以为你处理(我不知道细节)

        关于密钥、IV 和输入大小,听起来您选择的密码的块大小为 16 字节。你所描述的一切都符合这一点,对我来说似乎很正常。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2012-07-24
          相关资源
          最近更新 更多