Как стать автором
Обновить

Комментарии 30

Так реализация на Rust не исправляет Гамму шифра, если я правильно понимаю то есть все ранее озвученные недостатки шрифта остаются вместе с ним.
Говоря о SBox, да, здесь используется тот, который приведён с стандарте. У меня есть несколько идей поиска более хорошего SBox, возможно расскажу в будущем.

Спасибо, интересно. У вас не очень идиоматичный Rust.


Довольно стандартным является builder pattern, который в вашем случае будет выглядеть примерно вот так (я могу слегка врать, потому что Вашу библиотеку я не чувствую):


fn hello_world() {
    let gamma = vec![......];
    let mut kuz = Kuznechik::builder().  // do we need mut here?
      .title("Кузнечик на Rust")
      .gamma(gamma)  // Use .clone internally
      .build().unwrap();
    let enc_data = kuz.encrypt("Hello World!");  // Use generics <T: ToBytes>
    println!("Encrypted data:\n{:?}", enc_data);  // Add  AsRef impl to make it work without '&'.
    let dec_data: &str = kuz.decrypt(enc_data); // Use generics to provide different decrypted types
    println!("Decrypted: {}", dec_data);
}

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

я бы даже гамму задавал как некоторый итератор и к примеру делал бы его конструктом.
При написании библиотеки я применил немного не стандартным образом паттерн Strategy, это касается режимов работы шифра (6 структур, реализующих типаж Algorithm). Что касается типа Kuznechik, то это выделенная в отдельную структуру часть кода, которая является неотъемлемой частью каждого из режима шифрования (выполняет роль менеджера ключей).

По поводу паттерна Builder, я думаю он будет смотреться лаконично и дополнять Strategy, а код будет похож на ваш.

Тут основное, даже, не в builder'е, а в правильном описании того, что вы хотите на вход (надо требовать минимально необходимый вам трейт, а не конкретный тип), и что вы можете выдать назад.


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

.gamma(gamma) // Use .clone internally

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


В приведённом автором примере, данные нужны только для assert — в иллюстративных целях, в общем.

Да, в принципе, поинт есть. Но в целом, API, которые хотят владение агрументами, немного смущают. Во-первых, гамму нужно отдавать? as_ref не прокатит? (Я внутрях алгоритма не знаю). Если это mut-буффер, то лучше его скрыть внутри структуры. Если это не mut, а подобие seed, то тогда надо передавать как as_ref. Дальше пользователь сам разберётся, что там передать, Rc, Arc, дать попользоваться и т.д.

Во-первых, гамму нужно отдавать? as_ref не прокатит? (Я внутрях алгоритма не знаю)

Я тоже. (:


Так-то согласен: если владение не нужно, то надо передавать по ссылке. Но вот API "забирающее владение" кажется вполне нормальным.

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

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

Объясню, почему мне кажется, что это не очень дружелюбно.


У вызывающей функции этого владения может не быть. Ей дали slice в середину mmap-файла, например. Его можно читать, но никак нельзя ни модицицировать, ни деаллоцировать (потому что рядом второй тред разбирается с другим диапазоном, например).


В этой ситуации требовать ownership, это большой overreach.


И, даже если мы делаем in-place, зачем нам ownership, если mut ref достаточно?

У вызывающей функции этого владения может не быть.

Ну да, тут надо на контекст смотреть. Если что, я рассуждаю не конкретно о коде автора, а скорее об общем случае. И тут мне кажется, что явные клонирования на вызывающей стороне — это более растовый подход. Хотя для вектора и слайса есть смысл использовать Into<Vec<u8>> и тогда и пользователью будет удобно и лишнего клонирования можно будет избежать.


И, даже если мы делаем in-place, зачем нам ownership, если mut ref достаточно?

С этим не спорю.

Я бы не назвал ваш код идиоматичным тоже. Во-первых, для чего title? Во-вторых, использование векторов, когда массивы вполне выполнят роль и позволят убрать ненужные проверки ошибок. В-третьих, использование owned-типов (предположительно снова векторов), тогда как криптографический код обычно предпочитает работать in-place (т.е. над &mut [u8]) из соображений эффективности. Ну и в-четвёртых, если мы говорим о кастомных S-box'ах, то их лучше делать константными параметрами, например, см. как сделано magma (единственное, тут из-за MSRV тут используется тип с ассоциированными константами в реализации типажа, вместо константы напрямую).


В общем, ваш подход больше характерен для высокоуровневых приложений, но никак не для реализаций низкоуровневых алгоритмов, для которых к тому же желательно быть дружелюбными к no_std.

Vec я скопировал с оригинала, да. Насчёт in-place, всё сложно.


Допустим, у нас есть алгоритм, который шифрует произвольную u8 блоками по 64 байта. На вход подают [u8;22]. И как тут быть шифроалгоритму? Делать Result возвращаемым типом и требовать возиться с паддингом со стороны вызывающего?


Мне кажется, удобная библиотека должна давать возможность и in-place (с Result на неправильные размеры), и в copy-режиме. Мне кажется, что выбор copy/in place с no_std-safe вообще не соотносится никак.


Про S-box вообще ничего не могу сказать, т.к. в сам алгоритм даже не заглядывал.

Допустим, у нас есть алгоритм, который шифрует произвольную u8 блоками по 64 байта. На вход подают [u8;22]. И как тут быть шифроалгоритму?

Есть несколько подходов. Самый простой это передавать буфер и размер сообщения в нём, если размер буфера недостаточен, то возвращать ошибку, другой это использовать интерфейс работающий непосредственно с блоками. Разумеется, ничто не мешает строить на их основе чуть более удобные методы работающие с векторами.


Суть в том, что фундаментом являются no_std-дружелюбные методы не содержащие внутри паник, поверх которых уже надстраиваются более удобные методы.

Ну вот по ссылке, кстати, есть и так, и так. И alloc они хотят. И encrypt/decrypt_vec у них есть, принимающий входные данные immutable.

Если что, этот код писался по большей части мной. :) Суть в том, что методы хотящие alloc опциональны и скрыты за feature gate'ом (и, насколько я знаю, на практике практически не используются), фундаментом же, на котором строятся остальные методы, являются методы работающие над &mut [Block<C>] (по сути &mut [[u8; Self::BLOCK_SIZE]]), т.е. которым ни только аллокатор не нужен, но и для которых принципиально гарантируется успешность исполнения (в идеале хотелось бы ещё и no_panic аннотаций, но чего нет того нет). Аналогичная ситуация с инициализацией, есть фундаментальные методы принимающие ключ и IV в виде ссылок на массивы фиксированных размеров для которых гарантируется успешность исполнения, поверх которых уже построены методы принимающие слайсы. Плюс, если обратите внимание, никакого builder pattern'а ни в этом, ни в большинстве других RustCrypto крейтов нет.


С вашими утверждениями выше про типажи (трейты) я в целом согласен.

Большое спасибо за разъяснения.


… Кстати, мне кажется, что аннотации про no_panic немного более сложные, чем кажется. С точки зрения автора библиотеки — no_panic, это обещание, что внутри библиотеки нет операций, которые вызывают панику.


Но с точки зрения потребителя, это обещание, что библиотека не вызывает панику. Это разные вещи.


например,


fn foo(){ foo() }

не содержит вызовов паники, но панику вызывает (т.к. stack overflow). А вот проверять обещание no_panic в настоящем смысле уже невозможно, потому что мы уходим в частичные общерекурсивные функции и т.д.

но панику вызывает (т.к. stack overflow)

Так ведь это не паника.


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

Очень любопытное разногласие. Мне кажется, что вот это — вполне себе паника.


thread 'main' has overflowed its stack
fatal runtime error: stack overflow
Aborted (core dumped)

Могу предложить попытаться перехватить эту панику при помощи panic::catch_unwind. (:


Или сравнить вывод с реальной паникой, который выглядит как "thread 'main' panicked at...". Или добавить на стек объект и проверить, что деструктор (дроп) не будет вызван. Или установить свой обработчик паники через panic::set_hook. К сожалению, не смог быстро найти официальное описание поведения, но по всем наблюдаемым признакам — это не паника. Как и OOM.

Зачем превращать пароль в ключ с помощью SHA-3? для этого давным-давно есть Argon2, bcrypt, pbkdf, наконец
Использование пароля, на данном этапе, было опциональным, поэтому я использовал одну из самых распространённых хеш-функций с длиной выхода 256 бит.
А так да, вполне можно и нужно.

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

В обоих случаях это выработка ключевого материала на основе пароля, разница только в том как этот ключевой материал используется дальше. Обратите внимание, что PBKDF это аббревиатура от Password-Based Key Derivation Function. Без использования подобной функции вы существенно облегчаете задачу подбора пароля даже в сценарии с шифрованием файла, т.е. условно злоумышленник может проводит атаку по словарю и смотреть получается ли при расшифровке первых блоков что-то осознанное или нет.

Неплохо.
Хотелось бы увидеть сравнение производительности с С/C++ версией.

Спасибо. Можно устроить.

Вот, да, хотел поинтересоваться насколько хорошо оно автовекторизуется в текущем виде.
Второй вопрос — насколько ваша имплементация уязвима к атакам по времени.

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

Примечание: я один из мейнтейнеров проекта RustCrypto.


Подобные начинания безусловно похвальны, но я бы рекомендовал не забывать о хотя бы поверхностном обзоре prior art, ибо это позволит в свой "велосипед" позаимствовать те или иные удачные идеи. Например, в крейте kuznyechik реализация принципиально идентична описанной в статье (что вполне ожидаемо), но уделяется отдельное внимание разворачиванию циклов, что позволяет получить дополнительную производительность в ущерб размеру бинарника (это разворачивание при необходимости можно отключить флагом no_unroll).


Но более существенное отличие заключается в том, что kuznyechik интегрируется в остальную экосистему RustCrypto посредством реализации типажей из крейта cipher, тем самым блочный шифр легко может использоваться с обобщённой реализацией CMAC, блочных режимов и другими крейтами (например, MGM). Это означает, что реализации подобных алгоритмов не привязаны к конкретным блочным шифрам, а значит мы можем использовать их например с Магмой или с кастомным Кузнейчиком с исправленным S-box'ом.


Ну и напоследок, упомяну о дополнительной фиче, полезной в отдельных случаях. В RustCrypto мы можем использовать как Cmac<Kuznyechik>, так и Cmac<&Kuznyechik>, т.к. типажи блочных шифров реализованы для &T если T реализует эти типажи. Разумеется, во втором случае мы не можем инициализировать структуру по ключу, только по ссылке на уже инициализированный блочный шифр.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации