3

我正在使用 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。IE:

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

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

4

4 回答 4

3

我将详细说明@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"
于 2017-08-11T17:03:26.863 回答
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 填充

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

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

于 2013-07-03T14:35:10.223 回答
1

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

于 2012-10-03T06:06:03.743 回答
0

根据@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

于 2012-10-02T15:44:10.393 回答