Превью
Превью

После долгого перерыва мы возвращаемся в мир криптографических алгоритмов. В этот раз мы рассмотрим некоторые широко известные режимы шифрования блочных шифров, такие как ECB, CBC, CFB, OFB, CTR и подготовим небольшую архитектурную задумку, о которой я расскажу под катом.

Как всегда надеюсь, что эта статья будет полезна другим студентам. Более опытные разработчики в комментариях ожидаются (и приветствуются) — поделитесь своим опытом.

Весь код сохранен в репозитории GitVerse.

Если вы еще не видели мои предыдущие статьи по алгоритмам хэширования "Streebog" и "SHA", советую ознакомиться — в этот раз будет сложнее.

Архитектурная задумка

Логичными продолжением развития программиста становится расширение своих познаний в области архитектуры. Синтаксис языка, как и сам язык, всего лишь инструменты — самое сложное, это верно составить архитектуру программы, библиотеки или фреймворка.

Говоря: "Верно", я имею в виду составить ее так, чтобы дальнейшее написание модулей становилось легче, а не сложнее.

Почему я упоминаю об этом сейчас ? — Да потому что сам недавно начал пытаться составить что-то сложнее пары функций и для меня это оказалось сложным.

Мы не будет разбирать различные паттерны программирования и кучу готовых решений нашей проблемы, а придумаем все сами.

Итак, с чего начать ? — Например с того как связаны режимы шифрования и алгоритмы шифрования.

Напомню, что:

  • Режимы шифрования: ECB, CBC, CFB, OFB и CTR.

  • Алгоритмы шифрования: "Магма", "Кузнечик", "AES".

Если говорить простыми словами — режим шифрования управляет тем что будет подаваться на вход алгоритму шифрования. Эдакая абстракция получается — нас особо не волнует что за алгоритм шифрования используется, мы просто знаем, что это коробка, в которую нужно подать сами данные и ключ, а на выходе получить шифротекст.

Если еще проще, то режиму шифрования нужно дать "что-то", что умеет шифровать и расшифровывать. Я говорю об абстрактном алгоритме шифрования. Главное слово: "абстрактный".

Размышляя таким образом над частью возникающих вопросов мы придем к тому, что:

  1. Большинство алгоритмов шифрования можно задать абстрактно, главное чтобы это "что-то" умело: шифровать блок, расшифровывать блок и говорить какой размер блока использует.

  2. Большинство режимов шифрования тоже можно задать абстрактно, необходимо чтобы режим умел: шифровать, расшифровывать и говорить, используется ли вектор инициализации или нет.

  3. Большинство алгоритмов дополнения (padding) тоже можно задать абстрактно.

Думаю задумку вы понять смогли. Нам нужно предъявить общие требования для алгоритмов.

Часть из того, что написано, в этой статье использоваться не будет, это задача следующей. В этой мы зададим требования для объектов: "Шифратор", "Шифр", "Алгоритм дополнения" и "Режим шифрования" и реализуем часть из них.

Проектирование

Структура шифратора
Структура шифратора

Чтобы спроектировать объект "Шифратор", нам понадобятся все остальные объекты. Начнем с шифра.

Шифр

Структура шифратора — шифр
Структура шифратора — шифр

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

Код трейта блочного шифра
use std::fmt::Debug;
use super::CipherError;

pub trait BlockCipher: Send + Sync + Debug {
    fn block_size(&self) -> usize;
    fn encrypt_block(&self, block: &[u8]) -> Result<Vec<u8>, CipherError>;
    fn decrypt_block(&self, block: &[u8]) -> Result<Vec<u8>, CipherError>;
}

Да, вот так же коротко выглядит и определение того, что нам нужно от шифра.

Мы создаем "трейт", аналог интерфейса из других языков программирования, который также накладывает требования "супертрейтов": Send, Sync, Debug. Первые два мы трогать не будем, они используются для потокобезопасных взаимодействий и не понадобятся в этом проекте, но оставить их можно, чтобы в будущем реализовать что-то многопоточное. Debug понадобится нам для того, чтобы все объекты, что реализуют этот "трейт" всегда реализовывали и возможность отладки — вывод в консоль в каком-то виде.

Эти вещи мы будем задавать в каждом трейте, поэтому особо не обращайте внимания.

Далее мы задаем 3 функции: для шифрования, расшифровывания и взятия значения размера блока. Все просто:

  1. Размер блока — это число в байтах.

  2. Шифрование блока принимает блок открытого текста.

  3. Расшифровывание блока принимает блок шифротекста.

Функции, связанные с шифрованием и расшифровыванием возвращают объект Result. Мы не проверяем все возможные исключения до начала процесса шифрования/расшифровывания, поэтому на этом этапе могут возникнуть ошибки и мы должны иметь возможность их обработать. Ошибки у нас тоже кастомные — определяем мы их в другом месте.

Код перечисления ошибок
#[derive(Debug)]
pub enum CipherError {
    InvalidKeyLenght,
    InvalidIVLenght,
    InvalidBlockSize,
    DataTooShort,
    DataNotAligned,
    InvalidPadding,
    InvalidKeyFormat,
}

Здесь все ошибки, что понадобились мне при написании шифра "Магма".

Внимательные читатели заметили, что мы не передаем ключ шифрования. Да, код не лишен ошибок, но в нашем случае это не мешает. Мы не определяем функцию конструктора, а значит можем это сделать уже в конкретной структуре шифра, например в "Магме".

Режим шифрования

Структура шифратора — режим шифрования
Структура шифратора — режим шифрования

От режима шифрования нам нужно почти то же самое, что и от шифра.

Код трейта режима шифрования
use std::fmt::Debug;
use super::{BlockCipher, CipherError};

pub trait BlockMode: Send + Sync + Debug {
    fn encrypt(&self, cipher: &dyn BlockCipher, data: &[u8]) -> Result<Vec<u8>, CipherError>;
    fn decrypt(&self, cipher: &dyn BlockCipher, data: &[u8]) -> Result<Vec<u8>, CipherError>;
    fn iv_len(&self) -> Option<usize>;
}

Тут почти такие же функции — шифрование, расшифровывание и взятия значения размера вектора инициализации.

Отличий немного:

  • Вектор инициализации опциональный и нужен не в каждом режиме шифрования — теорию по режимам шифрования мы рассмотрим дальше.

  • В функцию шифрования/дешифрования передается объект шифра. Тоже возможное упущение — можно было шифр сделать частью структуры, но это создаст проблемы для "супертрейтов" Send и Sync.

Алгоритм дополнения

Структура шифратора — алгоритм дополнения
Структура шифратора — алгоритм дополнения

Как известно — блочные алгоритмы шифрования работают, собственно, с блоками данных. Иногда данных не хватает, чтобы разделить их на равные блоки, поэтому на помощь приходят разные алгоритмы дополнения данных, например "PKCS7".

Код трейта алгоритма дополнения
use std::fmt::Debug;
use super::CipherError;

pub trait Padding: Send + Sync + Debug {
    fn pad(&self, data: &[u8], block_size: usize) -> Result<Vec<u8>, CipherError>;
    fn unpad(&self, data: &[u8]) -> Result<Vec<u8>, CipherError>;
}

Нам достаточно, чтобы алгоритм мог дополнить данные и убрать свое дополнение.

Шифратор

Структура шифратора — шифратор
Структура шифратора — шифратор

Воссоединим же 3 части механизма:

Структура "Шифратора"
#[derive(Debug)]
pub struct Encryptor {
    cipher: Box<dyn BlockCipher>,
    mode: Box<dyn BlockMode>,
    padding: Box<dyn Padding>,
}

Ладно, небольшая шутка. Для структуры этого достаточно, но нам нужные еще и методы:

Методы структуры "Шифратора"
impl Encryptor {
    pub fn new(
        cipher: Box<dyn BlockCipher>,
        mode: Box<dyn BlockMode>,
        padding: Box<dyn Padding>,
    ) -> Result<Self, CipherError> {
        if let Some(iv_len) = mode.iv_len() {
            if iv_len != cipher.block_size() {
                return Err(CipherError::InvalidIVLenght)
            }
        }

        Ok(Self { cipher, mode, padding })
    }

    pub fn encrypt(&self, data: &[u8]) -> Result<Vec<u8>, CipherError> {
        let padded = self.padding.pad(data, self.cipher.block_size())?;
        self.mode.encrypt(&*self.cipher, &padded)
    }

    pub fn decrypt(&self, data: &[u8]) -> Result<Vec<u8>, CipherError> {
        let decrypted = self.mode.decrypt(&*self.cipher, data)?;
        self.padding.unpad(&decrypted)
    }
}

Ранее созданные трейты используются для указания объектов, которые должен получить конструктор шифратора. В этом вся прелесть — мы можем комбинировать разные алгоритмы шифрования, режимы шифрования и алгоритмы дополнения сообщения.

В конструкторе происходит проверка на то, чтобы вектор инициализации совпадал размером с блоком алгоритма шифрования.

Далее идут две простые функции шифрования и расшифровывания. Здесь используется то самое дополнение и далее подготовленные данные передаются уже в режим, откуда они попадают собственно в сам шифр.

Вот такая, весьма простая, и иногда косячная реализация библиотеки блочного шифрования у меня получилась. На основе этой оснастки я реализовал шифр "Магма", алгоритм дополнения "PKCS7" и ряд режимов шифрования: ECB, CBC, CFB, OFB и CTR.

Далее подробно про режимы шифрования.

Режимы шифрования

Для каждого режима я приведу диаграммы, для упрощения восприятия.

ECB (Electronic Codebook) — режим электронной кодовой книги

Режим ECB
Режим ECB

Как видно даже из диаграммы — это очень простой режим шифрования. Что вообще нужно, чтобы что-то зашифровать ? Сами данные, какой-то ключ и алгоритм шифрования — все. Кажется, что и придумывать больше ничего не нужно, но, как и все в этом мире, шифр имеет плюсы и минусы.

Плюсы:

  1. Шифрование каждого блока происходит независимо, поэтому задача хорошо распараллеливается.

Минусы:

  1. Процесс шифрования это биективное преобразование, поэтому одному уникальному блоку открытого текста будет соответствовать один блок шифротекста. Это приводит к тому, что мы можем анализировать данные по частотам. Этот недостаток очень хорошо показан на Википедии.

Недостаток режима ECB
Недостаток режима ECB

Мы не получили исходный открытый текст, однако косвенно знаем содержимое.

Код режима шифрования ECB
#[derive(Debug, Clone)]
pub struct ECB;

impl BlockMode for ECB {
    fn encrypt(&self, cipher: &dyn BlockCipher, data: &[u8]) -> Result<Vec<u8>, CipherError> {
        let block_size = cipher.block_size();

        if data.is_empty() {
            return Err(CipherError::DataTooShort);
        }

        if data.len() % block_size != 0 {
            return Err(CipherError::DataNotAligned);
        }

        let mut result = Vec::with_capacity(data.len());

        for chunk in data.chunks(block_size) {
            let encrypted_block = cipher.encrypt_block(chunk)?;
            result.extend_from_slice(&encrypted_block);
        }

        Ok(result)
    }

    fn decrypt(&self, cipher: &dyn BlockCipher, data: &[u8]) -> Result<Vec<u8>, CipherError> {
        let block_size = cipher.block_size();

        if data.is_empty() {
            return Err(CipherError::DataTooShort);
        }

        if data.len() % block_size != 0 {
            return Err(CipherError::DataNotAligned);
        }

        let mut result = Vec::with_capacity(data.len());

        for chunk in data.chunks(block_size) {
            let decrypted_block = cipher.decrypt_block(chunk)?;
            result.extend_from_slice(&decrypted_block);
        }

        Ok(result)
    }

    fn iv_len(&self) -> Option<usize> {
        None
    }
}

Код ECB режима максимально прост — просто отправляем данные в функцию шифрования или расшифровывания и собираем результат.

Проверки, которые здесь выполняются выполняют тривиальные действия — нам просто нужно знать, что данные вообще есть и что их нужное количество.

И поскольку интерфейс BlockMode декларирует нам иметь функцию, опционально возвращающую длину вектора инициализации, мы возвращаем None — ECB не использует вектор инициализации.

CBC (Cipher Block Chaining) — режим сцепления блоков шифротекста

Режим CBC
Режим CBC

Вот здесь уже поинтереснее. Каждый блок шифротекста зависит от предыдущего, поэтому недостаток ECB удалось исправить.

Плюсы:

  1. Невозможно проанализировать частотным способом.

  2. Возможно распараллелить процесс расшифровки. Мы имеем все блоки шифротекста, а поскольку для расшифровки одного блока шифротекста нужен предыдущий (который у нас есть), мы просто и быстро расшифровываем все, что есть.

Минусы:

  1. Невозможно распараллелить шифрование — каждый следующий блок открытого текста шифруется с использованием зашифрованного предыдущего.

  2. Частотный способ здесь уже не работает, но мы все равно можем косвенно определить некоторую информацию. Если мы шифруем, например JSON документ, отправляем на обработку, где вносятся минимальные изменения где-нибудь в конце или середине — мы это узнаем. Неизмененные части открытого текста при шифровании будут идентичны тем, что были до изменения открытого текста. Как минимум мы узнаем первый блок, где произошли изменения.

Недостаток режима CBC
Недостаток режима CBC

На Википедии приводятся еще два минуса, но я думаю, что они относятся уже не к самому режиму.

Код режима шифрования CBC
#[derive(Debug, Clone)]
pub struct CBC {
    iv: Vec<u8>,
}

impl CBC {
    pub fn new(iv: Vec<u8>) -> Self {
        Self { iv }
    }
}

impl BlockMode for CBC {
    fn encrypt(&self, cipher: &dyn BlockCipher, data: &[u8]) -> Result<Vec<u8>, CipherError> {
        let block_size = cipher.block_size();

        if data.is_empty() {
            return Err(CipherError::DataTooShort);
        }

        if data.len() % block_size != 0 {
            return Err(CipherError::DataNotAligned);
        }

        if self.iv.len() != block_size {
            return Err(CipherError::InvalidIVLenght);
        }

        let mut result = Vec::with_capacity(data.len());
        let mut prev_block = self.iv.clone();

        for chunk in data.chunks(block_size) {
            let mut block = chunk.to_vec();

            for i in 0..block_size {
                block[i] ^= prev_block[i];
            }

            let encrypted_block = cipher.encrypt_block(&block)?;
            result.extend_from_slice(&encrypted_block);
            prev_block = encrypted_block;
        }

        Ok(result)
    }

    fn decrypt(&self, cipher: &dyn BlockCipher, data: &[u8]) -> Result<Vec<u8>, CipherError> {
        let block_size = cipher.block_size();

        if data.is_empty() {
            return Err(CipherError::DataTooShort);
        }

        if data.len() % block_size != 0 {
            return Err(CipherError::DataNotAligned);
        }

        if self.iv.len() != block_size {
            return Err(CipherError::InvalidIVLenght);
        }

        let mut result = Vec::with_capacity(data.len());
        let mut prev_block = self.iv.clone();

        for chunk in data.chunks(block_size) {
            let mut decrypted_block = cipher.decrypt_block(chunk)?;

            for i in 0..block_size {
                decrypted_block[i] ^= prev_block[i];
            }

            result.extend_from_slice(&decrypted_block);
            prev_block = chunk.to_vec();
        }

        Ok(result)
    }

    fn iv_len(&self) -> Option<usize> {
        Some(self.iv.len())
    }
}

Да, портянка кажется большой, но если присмотреться, то видно, что процесс в общих чертах один и тот же.

Отличия:

  • В структуру добавился вектор инициализации.

  • Функция возврата длины вектора инициализации теперь работает по назначению.

  • Добавилась проверка на длину вектора инициализации. В данном виде это ошибка, поскольку мы проверяем длину вектора инициализации при создании шифратора. Здесь это необходимо оставить, а в коде шифратора убрать для большей унификации.

Следуя спецификациям режима CBC мы XOR'им блок данных с предыдущим. В первой итерации в качестве предыдущего берется вектор инициализации. Далее полученный блок шифруется. Расшифровка аналогична, только в обратном порядке.

CFB (Cipher Feedback) — режим обратной связи по шифротексту

Режим CFB
Режим CFB

Про этот режим шифрования особо нечего сказать. Его последовательность действий не привносит каких-то новых плюсов или минусов.

Плюсы:

  1. Невозможно проанализировать частотным способом.

  2. Возможно распараллелить процесс расшифровки. Мы имеем все блоки шифротекста, а поскольку для расшифровки одного блока шифротекста нужен предыдущий (который у нас есть), зашифрованный еще раз, мы просто и быстро расшифровываем все, что есть.

Минусы:

  1. Невозможно распараллелить шифрование — каждый следующий блок открытого текста шифруется с использованием зашифрованного предыдущего.

  2. Частотный способ здесь уже не работает, но мы все равно можем косвенно определить некоторую информацию. Если мы шифруем, например JSON документ, отправляем на обработку, где вносятся минимальные изменения где-нибудь в конце или середине — мы это узнаем. Неизмененные части открытого текста при шифровании будут идентичны тем, что были до изменения открытого текста. Как минимум мы узнаем первый блок, где произошли изменения. Такой же минус как и у режима CBC.

Код режима шифрования CFB
#[derive(Debug, Clone)]
pub struct CFB {
    iv: Vec<u8>,
}

impl CFB {
    pub fn new(iv: Vec<u8>) -> Self {
        Self { iv }
    }
}

impl BlockMode for CFB {
    fn encrypt(&self, cipher: &dyn BlockCipher, data: &[u8]) -> Result<Vec<u8>, CipherError> {
        let block_size = cipher.block_size();

        if data.is_empty() {
            return Err(CipherError::DataTooShort);
        }

        if data.len() % block_size != 0 {
            return Err(CipherError::DataNotAligned);
        }

        if self.iv.len() != block_size {
            return Err(CipherError::InvalidIVLenght);
        }

        let mut result = Vec::with_capacity(data.len());
        let mut prev_block = self.iv.clone();

        for chunk in data.chunks(block_size) {
            let encrypted_iv = cipher.encrypt_block(&prev_block)?;
            let mut encrypted_block = Vec::with_capacity(block_size);

            for i in 0..block_size {
                encrypted_block.push(encrypted_iv[i] ^ chunk[i]);
            }

            result.extend_from_slice(&encrypted_block);
            prev_block = encrypted_block;
        }

        Ok(result)
    }

    fn decrypt(&self, cipher: &dyn BlockCipher, data: &[u8]) -> Result<Vec<u8>, CipherError> {
        let block_size = cipher.block_size();

        if data.is_empty() {
            return Err(CipherError::DataTooShort);
        }

        if data.len() % block_size != 0 {
            return Err(CipherError::DataNotAligned);
        }

        if self.iv.len() != block_size {
            return Err(CipherError::InvalidIVLenght);
        }

        let mut result = Vec::with_capacity(data.len());
        let mut prev_block = self.iv.clone();

        for chunk in data.chunks(block_size) {
            let encrypted_iv = cipher.encrypt_block(&prev_block)?;
            let mut decrypted_block = Vec::with_capacity(block_size);

            for i in 0..block_size {
                decrypted_block.push(encrypted_iv[i] ^ chunk[i]);
            }

            result.extend_from_slice(&decrypted_block);
            prev_block = chunk.to_vec();
        }

        Ok(result)
    }

    fn iv_len(&self) -> Option<usize> {
        Some(self.iv.len())
    }
}

Следуя спецификациям режима CFB мы шифруем блок данных. В первой итерации в качестве входа берется вектор инициализации. Далее полученный блок XOR'ится с блоком открытого текста. Расшифровка аналогична, только в обратном порядке.

OFB (Cipher Feedback) — режим обратной связи по выходу

Режим OFB
Режим OFB

В этом режиме шифрования есть интересная особенность — для шифрования и расшифровывания используется одна и та же функция. В два раза меньше работы программиста ! И даже новые плюсы и минусы появились !

Плюсы:

  1. Невозможно проанализировать частотным способом.

  2. Функции шифрования и расшифровывания идентичны.

  3. Можно выполнить пред расчет — поскольку вектор инициализации шифруется рекурсивно, мы можем сделать такой расчет заранее.

Минусы:

  1. Невозможно распараллелить — зависимости в обе стороны.

  2. Частотный способ здесь уже не работает, но мы все равно можем косвенно определить некоторую информацию. Если мы шифруем, например JSON документ, отправляем на обработку, где вносятся минимальные изменения где-нибудь в конце или середине — мы это узнаем. Дело усугубляется еще и тем, что при шифровании блок открытого текста не влияет на следующие блоки шифротекста, а значит мы можем узнать все блоки, где были хоть какие-то изменения.

Код режима шифрования OFB
#[derive(Debug, Clone)]
pub struct OFB {
    iv: Vec<u8>,
}

impl OFB {
    pub fn new(iv: Vec<u8>) -> Self {
        Self { iv }
    }
}

impl BlockMode for OFB {
    fn encrypt(&self, cipher: &dyn BlockCipher, data: &[u8]) -> Result<Vec<u8>, CipherError> {
        let block_size = cipher.block_size();

        if data.is_empty() {
            return Err(CipherError::DataTooShort);
        }

        if data.len() % block_size != 0 {
            return Err(CipherError::DataNotAligned);
        }

        if self.iv.len() != block_size {
            return Err(CipherError::InvalidIVLenght);
        }

        let mut result = Vec::with_capacity(data.len());
        let mut prev_block = self.iv.clone();

        for chunk in data.chunks(block_size) {
            let encrypted_iv = cipher.encrypt_block(&prev_block)?;
            let mut encrypted_block = Vec::with_capacity(block_size);

            for i in 0..block_size {
                encrypted_block.push(encrypted_iv[i] ^ chunk[i]);
            }

            result.extend_from_slice(&encrypted_block);
            prev_block = encrypted_iv;
        }

        Ok(result)
    }

    fn decrypt(&self, cipher: &dyn BlockCipher, data: &[u8]) -> Result<Vec<u8>, CipherError> {
        self.encrypt(cipher, data)
    }

    fn iv_len(&self) -> Option<usize> {
        Some(self.iv.len())
    }
}

Следуя спецификациям режима OFB мы постоянно шифруем вектор инициализации, а потом XOR'им с блоком открытого текста и получаем блок шифротекста. Что может быть проще ?

Очень непривычно видеть конструкцию расшифровывания, выглядит как шутка, но это действительно работает.

CTR (Cipher Feedback) — режим счетчика

Режим CTR
Режим CTR

Вот этот режим еще немного отличается от всех предыдущих. Это режим счетчика и в качестве, собственно, счетчика мы используем вектор инициализации. После каждой итерации шифрования мы просто прибавляем единицу к значению вектора инициализации. Поскольку вектор инициализации XOR'ится с открытым текстом, то размер вектора инициализации должен быть такого же размера.

Если вектор инициализации имеет размер 64 бита, например, как в шифре "Магма", то есть ненулевая вероятность переполнить такое число, тем более, что вектором инициализации вряд ли будет ноль. В случае переполнения мы просто отбрасываем старший бит. Такое поведение по умолчанию имеют операции сложения в CPP. В Rust же для такого сложения необходимо вызывать метод wrapping_add().

Плюсы:

  1. Невозможно проанализировать частотным способом.

  2. Функции шифрования и расшифровывания идентичны.

  3. Можно условно параллельно обрабатывать данные — поскольку вектор инициализации это просто число, которое мы увеличиваем на единицу, мы можем выделить несколько потоков и каждому назначить два значения счетчика от которого и до которого тот считает. Все данные, что имеются, делятся на большие непрерывные куски по несколько блоков за раз и отправляются в такие потоки. Это имеет смысл если данных уже много, например, при шифровании больших архивов.

Как таковых, выделенных раннее, минусов нет. Косвенно значение узнать уже не выйдет, если данные в каком-то месте изменятся, поскольку счетчик это сможет нивелировать — он постоянно вносит избыточность своими непостоянными данными даже туда, где данные статичны.

Условно распараллелить тоже можно попробовать — мы можем наперед выделять разные потоки с предустановленными счетчиками.

Код режима шифрования CTR
#[derive(Debug, Clone)]
pub struct CTR {
    iv: Vec<u8>,
}

impl CTR {
    pub fn new(iv: Vec<u8>) -> Self {
        Self { iv }
    }

    fn increment_counter(counter: &mut [u8]) {
        for byte in counter.iter_mut().rev() {
            *byte = byte.wrapping_add(1);
            if *byte != 0 {
                break;
            }
        }
    }
}

impl BlockMode for CTR {
    fn encrypt(&self, cipher: &dyn BlockCipher, data: &[u8]) -> Result<Vec<u8>, CipherError> {
        let block_size = cipher.block_size();

        if data.is_empty() {
            return Err(CipherError::DataTooShort);
        }

        if data.len() % block_size != 0 {
            return Err(CipherError::DataNotAligned);
        }

        if self.iv.len() != block_size {
            return Err(CipherError::InvalidIVLenght);
        }

        let mut result = Vec::with_capacity(data.len());
        let mut counter = self.iv.clone();

        for chunk in data.chunks(block_size) {
            let encrypted_counter = cipher.encrypt_block(&counter)?;
            let mut encrypted_block = Vec::with_capacity(block_size);

            for i in 0..block_size {
                encrypted_block.push(encrypted_counter[i] ^ chunk[i]);
            }

            result.extend_from_slice(&encrypted_block);
            Self::increment_counter(&mut counter);
        }

        Ok(result)
    }

    fn decrypt(&self, cipher: &dyn BlockCipher, data: &[u8]) -> Result<Vec<u8>, CipherError> {
        self.encrypt(cipher, data)
    }

    fn iv_len(&self) -> Option<usize> {
        Some(self.iv.len())
    }
}

Следуя спецификациям режима CTR мы XOR'им значение счетчика с блоком открытого текста, а затем шифруем. Алгоритм похож на CBC, по крайней мере визуально, но лишен его недостатков.


Заключение

digit4lsh4d0w

Автор

Эта статья в ретроспективе больше похожа на: "Топ 5 режимов шифрования", но все же стоит отметить, что я написал ее как подводку к материалам по алгоритмам шифрования "Магма" и "Кузнечик". Архитектурная задумка, о которой я писал в начале этого материала, уже используется для реализации "Магмы". Некоторые моменты я исправлю (чтобы удовлетворить свою дотошность) и допишу "Кузнечик".

Почему я не исправил ошибки ? — Потому что этот материал отчасти я посвятил архитектуре моего решения (не смейтесь, карточный домик тоже архитектурное решение). Я упомянул почему стоило сделать "так", а не "вот так" — это тоже опыт.

Невероятно приятно писать материал по вещам, которые только открылись для меня. Еще приятнее становится от того, что этот материал может понадобиться другим студентам — я знаю какого это ходить по сети и собирать по крупицам обрывки (особенно по Китайским сайтам). Конечно, можно пойти и прочитать RFC или официальные документы, но увы — душный язык древних русов, переворачивающих строки задом наперед для хэширования и шифрования, читать не то что бы приятно.

В этот раз я сделал куски кода более цельными, не распыляя по капле на всю публикацию — как вам ? Удобно / неудобно ?

Как всегда — любые правки, советы и исправления можно оставить либо в комментариях, либо на GitVerse.

Увидимся в Telegram или очередной статье на Хабре — пока !


Источники

Комментарии (5)