使用AES+CTR时的PyCrypto问题

12 投票
5 回答
41459 浏览
提问于 2025-04-16 00:39

我正在写一段代码,用对称加密来加密一段文本。但是结果总是不对……

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模式需要一个“计数器”函数,每次提供一个随机的计数器,但为什么它需要16个字节,而我的密钥是32个字节,而且它还坚持我的消息也必须是16个字节的倍数?这是正常的吗?

我猜解不回原始消息是因为在加密和解密之间计数器发生了变化。但是,从理论上讲,这应该怎么运作呢?我哪里做错了?总之,我现在不得不先用ECB模式,直到我搞清楚这个问题 :(

5 个回答

2

为什么我的密钥是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.'
11

AES是一种块密码,简单来说,它是一种算法(更准确地说,是一对算法),可以用来加密或解密一段消息。无论密钥的大小如何,消息块的大小总是16字节。

CTR是一种工作模式。它是基于块密码的一对算法,可以生成流密码,从而加密和解密任意长度的消息。

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:])
13

在解密时,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是一种密码,所以“每次16个字节”的限制其实是很自然的。

当然,如果一个所谓的“计数器”在每次调用时返回相同的值,那是非常不安全的。其实只要稍微改进一下就能做到更好,比如……:

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)

撰写回答