Почему мой микротест python aes-ctr-128 с блочным шифрованием работает медленно?

Я разработал алгоритм головоломки, который принимает значение каждого зашифрованного блока, указывающего на следующий зашифрованный блок. Я должен использовать для этого aes-ctr-128 по какой-то особой причине.

Я запускаю фиктивный тест, чтобы увидеть, насколько быстрым или медленным он может быть.

Вот что я сделал. Я тестировал как pycrypto, так и криптографию.

Сначала я создаю файл размером 16 МБ со случайными байтами.

Я пробовал двумя способами:

Метод 1. Загрузите файл в черный список с размером блока 128 бит.

Метод 2. Просто загрузите файл в строку.

Теперь я проверил общее время шифрования каждого 128-битного блока. И я проверил общее время шифрования всего файла.

Вот результат:

крипто:

  1. для шифрования 128-битного блока один за другим: 61 824 aes-ctr-128 в секунду

  2. для шифрования всего файла: 8 843 713 aes-ctr-128 в секунду

криптография

  1. для шифрования 128-битного блока один за другим: 384 959 aes-ctr-128 в секунду

  2. для шифрования всего файла: 113 417 922 aes-ctr-128 в секунду

Мне интересно, почему метод 1 и 2 дали мне результат с такой большой разницей? Предполагается, что эти два метода дают одинаковую скорость?

Вот мой тестовый код:

import os
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
backend = default_backend()

from Crypto.Cipher import AES
from Crypto.Util import Counter
import random
import time

BLOCK_SIZE = 16

def read_block(fname):
    block_list = []
    blobfo = open(fname)
    atEOF = False
    while not atEOF:
        blobdata = blobfo.read(BLOCK_SIZE)
        block_list.append(blobdata)
        if len(blobdata) < BLOCK_SIZE:
        # we should stop after this...
        atEOF = True
    return block_list


print 'loading data'
block_list = read_block('mediumdata')

print 'loading finish'
print len(block_list), 'blocks'

print 'start encryption'
NUM_COUNTER_BITS = 128
# Here I just use a random key
key = os.urandom(16)
t1 = time.time()
for block in block_list:
    ctr = Counter.new(NUM_COUNTER_BITS)
    cipher = AES.new(key, AES.MODE_CTR, counter=ctr)
    cipher.encrypt(block)
t2 = time.time()
print 'finish encryption'

print 'total time:', t2 - t1
print 'time for each aes:', (t2 - t1) / len(block_list)

print 'num of aes per sec:', len(block_list) / (t2 - t1)

print 'now try to encrypt whole file'
block = open('mediumdata').read()
print type(block)
print 'start encryption'
NUM_COUNTER_BITS = 128
key = os.urandom(16)
t1 = time.time()
ctr = Counter.new(NUM_COUNTER_BITS)
cipher = AES.new(key, AES.MODE_CTR, counter=ctr)
cipher.encrypt(block)
t2 = time.time()
print 'finish encryption'

print 'total time:', t2 - t1
print 'time for each aes:', (t2 - t1) / len(block_list)

print 'num of aes per sec:', len(block_list) / (t2 - t1)


print 'now try cryptography'

print 'start encryption'
t1 = time.time()
num = random.randint(1, 65530)
nonce = "".join(chr((num >> (i * 8)) & 0xFF) for i in range(16))
backend = default_backend()
cipher = Cipher(algorithms.AES(key), modes.CTR(nonce), backend=backend)
encryptor = cipher.encryptor()
for block in block_list:
    ciphertext = encryptor.update(block)
encryptor.finalize()
t2 = time.time()
print 'finish encryption'

print 'total time:', t2 - t1
print 'time for each aes:', (t2 - t1) / len(block_list)

print 'num of aes per sec:', len(block_list) / (t2 - t1)

print 'try a whole file'

block = open('mediumdata').read()

print 'start encryption'
t1 = time.time()
num = random.randint(1, 65530)
nonce = "".join(chr((num >> (i * 8)) & 0xFF) for i in range(16))
backend = default_backend()
cipher = Cipher(algorithms.AES(key), modes.CTR(nonce), backend=backend)
encryptor = cipher.encryptor()
ciphertext = encryptor.update(block)# + encryptor.finalize()
encryptor.finalize()
t2 = time.time()
print 'finish encryption'


print 'total time:', t2 - t1
print 'time for each aes:', (t2 - t1) / len(block_list)

print 'num of aes per sec:', len(block_list) / (t2 - t1)

Я что-то пропустил здесь?

Есть ли способ сделать метод 1 быстрее?


person Luke    schedule 20.05.2016    source источник
comment
шифрование имеет тенденцию быть медленным, я не думаю, что вы можете получить такую ​​​​же скорость   -  person YOU    schedule 21.05.2016
comment
@YOU Должен ли метод 1 иметь ту же скорость, что и метод 2?   -  person Luke    schedule 21.05.2016
comment
@YOU Скорость шифрования зависит от устройства, она не обязательно должна быть медленной. iPhone6S работает со скоростью 400+ МБ/с.   -  person zaph    schedule 21.05.2016
comment
Уточните, что такое способ 1 и 2? Сравнивать pycrypto с криптографией или поблочно со всеми сразу? Означает ли 61 824 aes-ctr-128 в секунду 61 824 байта в секунду?   -  person zaph    schedule 21.05.2016
comment
@zaph Метод 1 состоит в том, чтобы зашифровать каждые 128 бит файла один за другим. Способ 2 — загрузить весь файл и вызвать для него шифрование.   -  person Luke    schedule 21.05.2016
comment
Я предполагаю, что замедление связано с частым переключением контекста между криптосредой и интерпретатором Python.   -  person kennytm    schedule 21.05.2016
comment
Метод 1 делает еще 1 000 000 вызовов функций, что требует больше времени, чем простое индексирование в массиве.   -  person zaph    schedule 21.05.2016
comment
@kennytm Я также подозреваю, что это связано с python GIL (глобальная блокировка интерпретатора). Поскольку 128-битное шифрование должно занимать очень мало времени в C, возможно, интервал между выпуском и приобретением GIL намного больше, чем само шифрование. Таким образом, потеря времени переключения вызывает проблему. Однако у меня нет доказательств этого.   -  person Luke    schedule 21.05.2016


Ответы (1)


AES — это блочный шифр с несколькими раундами перестановки. У каждого раунда есть свой ключ раунда, который должен быть получен из «главного» ключа (key в вашем коде). При вызове AES.new(key, mode, ...) будут автоматически получены раундовые ключи, но этот расписание ключей довольно трудоемок. Выполнение расписания ключей для каждого блока значительно замедлит обработку по сравнению с подходом с одним вызовом, особенно если фактический код шифрования использует набор инструкций AES-NI.

Кроме того, как указывает kennytm в комментарии, Python является интерпретируемым языком, поэтому перебор блоков в Python вместо базового нативного криптографического кода (например, pyCrypto использует tomcrypt библиотеки C) обязательно повлечет за собой дополнительные штраф за производительность.


Код метода 1 не работает, потому что вы создаете новый объект Counter для каждого блока, который всегда инициализируется значением 1. Таким образом, вы выполняете операцию XOR для каждого блока с одним и тем же потоком ключей, что создает многоразовый блокнот и, вероятно, позволяет злоумышленник, чтобы вывести открытый текст.

Мы можем исправить это, имея только один объект Counter. Наличие единого ключевого расписания значительно увеличивает производительность.

Улучшенный код метода 1:

ctr = Counter.new(NUM_COUNTER_BITS)
cipher = AES.new(key, AES.MODE_CTR, counter=ctr)
for block in block_list:
    cipher.encrypt(block)

Результаты для pyCrypto:

1049887 blocks

Method 1
total time: 17.31999993324279785156
time for each aes: 1.64970134245e-05
num of aes per sec: 60617.0325662

Improved Method 1
total time: 0.78299999237060546875
time for each aes: 7.45794540146e-07
num of aes per sec: 1340851.86492

Method 2
total time: 0.147000074387
time for each aes: 1.4001513914e-07
num of aes per sec: 7142084.82126

Полный код

person Artjom B.    schedule 21.05.2016
comment
У меня нет опыта работы с модулем cryptography, поэтому я предполагаю, что либо итерация в python является большой проблемой, либо в ней есть ошибка. - person Artjom B.; 21.05.2016
comment
Имеет смысл, что есть стоимость для каждого инициализированного объекта Counter. Таким образом, в методе 1 каждый раз, когда я создаю новый объект счетчика, стоимость увеличивается. У меня есть два вопроса. 1. Метод 2 также требует обновления значения счетчика на 1 для каждого 128-битного блока, почему он быстрее? Это потому, что это просто обновление, а не создание нового объекта Counter? - person Luke; 21.05.2016
comment
2. В моей реальной реализации, согласно моему дизайну, значение счетчика для следующих 128-бит зависит от вывода шифрования для предыдущих 128-бит. Таким образом, есть ли способ, которым я могу только обновить значение счетчика, но не инициализировать новый объект счетчика? Спасибо за вашу помощь. - person Luke; 21.05.2016
comment
Чтобы быть более ясным, когда у меня есть вывод шифрования для первого 128-битного блока, я использую этот вывод, чтобы решить, какой блок следует зашифровать следующим, и каким должно быть значение счетчика. Это отличается от прямого взятия следующего фрагмента блока в файле и его шифрования. Скорее всего, это шифрование с переходом указателя. - person Luke; 21.05.2016
comment
К 1: Да, стоимость счетчика увеличивается, но это не то, что здесь медленно. AES — это блочный шифр, для которого требуется расписание ключей. Если ключ не меняется для нескольких блоков (как в случае с режимом CTR), расписание ключей может быть выполнено один раз, а не для каждого блока снова и снова. AES.new(...) выполняет ключевое расписание, поэтому оно будет медленным, если это будет выполняться снова и снова. - person Artjom B.; 21.05.2016
comment
Re 2: Мне кажется, вы хотите реализовать режим CBC. В любом случае, в этом случае вам следует использовать не аргумент counter, а аргумент nonce и передавать значение пользовательского счетчика внутри цикла, но тогда вы также потеряете преимущество запуска расписания ключей только один раз. Вам следует рассмотреть возможность использования режима ECB в качестве основы для имитации пользовательского режима работы. Я не знаю, можно ли использовать собственный класс Counter, потому что он реализован на C. - person Artjom B.; 21.05.2016