Комментарии 28
Самое важное, что в этой статье упущено, это то, что криптографию никогда не нужно писать самому в прикладных программах
Обучение или библиотеки, реализующие криптографию - это другое дело, конечно, а во всех остальных случаях надо себя по рукам бить, когда они тянутся писать свои реализации
если она не используется в коммерческих или государственных продуктах
То есть, во всех продуктах, которыми кто-то пользуется.
Если коротко, то "маленькие" баги/допущения/пр. могут пройти незаметно или иметь ничтожные последствия, если это прикладной код. Но в криптоалгоритмах любые "маленькие" проблемы практически гарантированно приводят к упрощению эксплуатации уязвимостей. Понижение энтропии, replay атаки и т.д.
Вы же наверняка не думаете, что библиотеку openssl пишут глупые программисты? Так вот, heartbleed - это следствие такой "маленькой" ошибки. Но если в библиотеке, которую используют все и везде, существует эта ошибка, не думаете ли вы, что ворвётесь с ноги в криптоалгоритмы с безупречным кодом, лишённым недостатков?
Но постойте, скажете вы, heartbleed - это не про плохую реализацию алгоритма. Правильно, но алгоритмы ради алгоритмов никому не нужны кроме обучения, а про это см. выше, и ваш код обязательно будет иметь как минимум прослойку для использования где-то ещё.
И даже если кроме алгоритма ничего нет, то несовершенства в самом алгоритме могут сделать его бесполезным и, следовательно, всё что им было зашифровано, тоже.
Но даже если и алгоритм безупречен математически, может оказаться что это абсолютно не важно, т.к. есть side-channel attacks.
С таким подходом самому вообще ничего писать нельзя. А вдруг ошибёшься?
Non sequitur
Лично вам никто ничего не может запретить. Вы спросили "почему", я ответил. Хотите еще глубже понять, гуглите "rolling your own crypto"
Мне кажется тут стоит внести ясность. Реализовать свою криптографическую библиотеку можно, но желательно, чтобы были следующие вещи:
Причина. Например, это какой-то специфический язык, на котором еще не написаны нужные вам алгоритмы. Brainfuck если хотите.
Основательные знания в криптографии. Вы понимаете причину использования каждой функции, что такое коллизии, что такое прообраз, как их найти и противодействовать этому.
Основательные знания в программировании. Вы понимаете как ваша программа утилизирует память, где могут быть опасные места.
Я, конечно не специалист ни в криптографии, ни в программировании, но мне кажется такого набора достаточно для того, чтобы реализовать криптографическую библиотеку.
Разумеется желательно, чтобы был квалифицированный человек, который сможет провести аудит. Проверка в этой области чрезвычайно важна.
Люди не способны даже просто правильно сравнить два хэша.
Математики разные бывают, как и программисты
Программист, который ничего не знает о безопасности криптографии тоже практически наверняка понятия не имеет о временных атаках
Как и математики не живут в пещерах, иногда выдавая наружу манускрипты. Это такие же люди, у них интернет есть, они читают статьи, посвященные алгоритмам и уязвимостям, с ними связанными, в конце концов
Почему то вспомнилось:
"Ноев ковчег" сделал любитель, а "Титаник" построили профессионалы.
Впрочем, отчасти вы несомненно правы.
Справдливости ради, у любителя был прораб от бога...
Строили профессионалы, а смету на материалы составляли менеджеры.
На резервные шлюпки забили, капитан вообще тусить свалил, а предупреждения погасили.
Вот примерно так же и с криптографией: реализовать может профессионал, но менеджер может навязать требования, которые убирают соломку, а пользователи (другие разработчики) забить на все обертки для большей надежности.
Я с вами полностью согласен, подобные проекты не должны оказаться в прикладном ПО. Это указано в первых строчках в репозитории, здесь решил об этом не упоминать, поскольку код приводился в качестве дополнительного пояснения.
Используя команду
echo "hello world" | sha256sum
вы отправляете в утилиту строку"hello world\n"
, то есть с переносом строки.
Добавляйте ключик -n чтобы это не происходило:
$ echo -n "" | wc -c
0
$ echo -n "" | sha256sum
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -
Заметьте, что сами операции у обоих версий SHA256 и SHA512 одинаковы, меняются только значения сдвигов.
Видимо именно поэтому по всему коду килограммы копипасты из магических значений. Вы хоть бы констант наделали.
.map(|byte| format!("{:08x}", byte))
А тут ещё и имя неудачное ибо вы не по байтам итерируетесь, а по u32/u64.
А вообще вам бы почитать маны к используемым вами API. Код был бы поприятнее.
Спасибо за ваш комментарий! На самом деле я наделся, что кто-то более компетентный оставит пару замечаний к моему коду. Не могли бы вы оставить немного более подробное замечание - я хотел бы разобраться в очевидных глупостях.
Про название переменной в .map(|byte| format!("{:08x}", byte))
вы ловко подметили. Исправлю.
if 128 - m.len() % 128 < 8
...
message.len() as u64 * 8)
Первая восьмёрка 8 байт aka u64
, которые заполняются второй восьмёркой про 8 бит в байте. Неплохо было бы их сделать отдельными константами с разными именами.
m.append(&mut (message.len() as u64 * 8).to_be_bytes().to_vec());
Такие штуки кажется сложнее читать. У Vec
есть метод extend
который принимает итератор на вход, благодаря чему код превращается в что-то чуть более читаемое
m.extend((message.len() * 8).to_be_bytes().into_iter());
u64
иusize
одного размера и кажется явное приведение типа не слишком нужноto_be_bytes
вернёт массив, который проще следом поглотить, нежели тратиться на аллокации из-за.to_vec()
. Потенциально аллокации оптимизирует на этапе компиляции, но это не гарантировано. Поэтому отдаём итератор.аналогично стоит отдавать итераторы в прочих местах.
repeat(0x0).take(штук)
. Штуки считаем перед тем как что-то добавлять в массив. Даже если оставитьappend
.
u32::from_be_bytes
Думал можно сделать как-то превращение байт в число красиво используя функцию read_be_u32
из документации. Но после некоторых изысканий понял, что овчинка выделки не стоит ради пары мест. А std::mem::transmute
обозначен unsafe
.
Выравнивание по границе 64 и 80 слов как минимум добавляет вопросов к магическим числам. Опять же, чтобы не возиться с append, можно просто сделать
use std::iter::repeat;
let w = block
.chunk_exact(/* size_of::<T> */)
.map(|chunk| /*from_be_bytes*/)
// в соответствии с вашим кодом
// sha256 - WORDS = 64, sha512 - WORDS = 80
// PADDING = 16 для обоих
.chain(repeat(0).take(WORDS - PADDING))
.collect();
По идее такой код будет чуть оптимальнее за счёт отсутствия необходимости аллокаций и реаллокаций вектора.
По идее можно tmp_h
вынести в кучку маленьких переменных чтобы код был немного компактнее, но тут главное не запутаться кто есть кто.
let [a,b,c,d,e,f,g,h] = tmp_h[0..8] else {unreachable!()};
let s1 = e.rotate_left(6) ^ e.rotate_right(11) ^ e.rotate_right(25);
let ch = (e & f) ^ (!e & g);
... /* the rest*/
tmp_h[7] = g;
tmp_h[6] = f;
tmp_h[5] = e;
tmp_h[4] = d.wrapping_add(temp1);
tmp_h[3] = c;
tmp_h[2] = b;
tmp_h[1] = a;
tmp_h[0] = temp1.wrapping_add(temp2);
Все рекомендации по рефакторингу моё ИМХО и не являются обязательным. Обязательным стоило бы сделать наличие тестов.
Большое спасибо за ваш подробный комментарий! Я постараюсь учесть все замечания, чтобы внести правки в этот пост и в следующий по Стрибог. Опыта в Rust мне пока не достает, но такие комментарии как ваш, помогают разобраться в некоторых местах. Спасибо!
Забыл добавить про тесты. Они есть в репозитории, здесь их приводить не стал. Поскольку я реализовал по сути всего две функции: sha256 и sha512, то тестирование выполняется только для них.
Тестирование выполнено максимально просто: с помощью утилит из coreutils я посчитал эталонный хэш для текстов и для видео и просто сравниваю с результатом моей функции.
Упоминание в комментарии выше про утечку времени я так и не понял, однако в этом сценарии использования сравнение хэшей работает.
Вы сильно теряете в перформансе на операции vec![0u8; 128 - m.len() % 128])
vec! выделяет массив в куче, но у вас довольно небольшое количество элементов, не более 128 байт, так что можно использовать small vector optimization - сделать массив [0u8;128] (аллокация на стеке) и от него уже взять слайс в 128 - m.len() % 128
элементов(можно взять готовый крейт smallvec если не хотите писать сами). Разница между аллокацией на стеке и в куче примерно в 15 раз, например на моей машине это 30ns vs 2.2ns. Для хэш-функции это может быть значимо. Есть, конечно, шанс что LLVM сделает это за вас, но я бы не закладывался.
let mut m = message.to_vec();
Перед эти надо проверить, вдруг выравнивание не надо делать - тогда у вас лишние копирование. Но даже если надо, то лучше использовать Vec::with_capacity
и extend_from_slice
чтобы избежать лишних копирований и переаллокаций на следующих шагах - нужный размер вектора вы с самого начала знаете.
Реализация SHA256 и SHA512 на языке RUST