Комментарии 30
Спасибо, интересно. У вас не очень идиоматичный 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'е.
По поводу паттерна 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.
А так да, вполне можно и нужно.
Но здесь шифрование, а не аутентификация по хэшу пароля. Разве в шифровании необходимо использовать специальные функции, затрудняющие подбор пароля по хэшу? Этот хэш нигде не хранится, а если бы хранился, его было бы достаточно для расшифровки и без пароля.
В обоих случаях это выработка ключевого материала на основе пароля, разница только в том как этот ключевой материал используется дальше. Обратите внимание, что 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
реализует эти типажи. Разумеется, во втором случае мы не можем инициализировать структуру по ключу, только по ссылке на уже инициализированный блочный шифр.
Улучшаем Кузнечик на Rust