Pull to refresh

Comments 28

Самое важное, что в этой статье упущено, это то, что криптографию никогда не нужно писать самому в прикладных программах

Обучение или библиотеки, реализующие криптографию - это другое дело, конечно, а во всех остальных случаях надо себя по рукам бить, когда они тянутся писать свои реализации

UFO just landed and posted this here

если она не используется в коммерческих или государственных продуктах

То есть, во всех продуктах, которыми кто-то пользуется.

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

Вы же наверняка не думаете, что библиотеку openssl пишут глупые программисты? Так вот, heartbleed - это следствие такой "маленькой" ошибки. Но если в библиотеке, которую используют все и везде, существует эта ошибка, не думаете ли вы, что ворвётесь с ноги в криптоалгоритмы с безупречным кодом, лишённым недостатков?

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

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

Но даже если и алгоритм безупречен математически, может оказаться что это абсолютно не важно, т.к. есть side-channel attacks.

UFO just landed and posted this here

С таким подходом самому вообще ничего писать нельзя. А вдруг ошибёшься?

Non sequitur

Лично вам никто ничего не может запретить. Вы спросили "почему", я ответил. Хотите еще глубже понять, гуглите "rolling your own crypto"

UFO just landed and posted this here

Мне кажется тут стоит внести ясность. Реализовать свою криптографическую библиотеку можно, но желательно, чтобы были следующие вещи:

  1. Причина. Например, это какой-то специфический язык, на котором еще не написаны нужные вам алгоритмы. Brainfuck если хотите.

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

  3. Основательные знания в программировании. Вы понимаете как ваша программа утилизирует память, где могут быть опасные места.

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

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

UFO just landed and posted this here

Математики разные бывают, как и программисты

Программист, который ничего не знает о безопасности криптографии тоже практически наверняка понятия не имеет о временных атаках

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

UFO just landed and posted this here

Почему то вспомнилось:

"Ноев ковчег" сделал любитель, а "Титаник" построили профессионалы.

Впрочем, отчасти вы несомненно правы.

Справдливости ради, у любителя был прораб от бога...

Строили профессионалы, а смету на материалы составляли менеджеры.

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

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

Самописная крипта это конечно интересно, однако, кто напишет библиотеку лучше? Профессиональные математики, криптографы или какой нибудь разработчик на скорую руку потому что его постоянно подгоняют с релизом?

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

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

Думайте сами, решайте сами

UFO just landed and posted this here

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

Заметьте, что сами операции у обоих версий 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 чтобы избежать лишних копирований и переаллокаций на следующих шагах - нужный размер вектора вы с самого начала знаете.

Спасибо за подробный комментарий и разъяснения. Внесу изменения в код, а потом и в статью.

Sign up to leave a comment.

Articles