Написать эту статью меня побудили 2 вещи:

  1. Задание в университете

  2. Слабый пересказ алгоритма SHA256.

Я хотел бы попытаться закрыть пробелы в этой статье своими объяснениями и примерами кода на языке Rust. Будут представлены примеры для алгоритмов SHA256 и SHA512. Надеюсь эта статья будет полезна другим студентам. Более опытные разработчики в комментариях приветствуются.

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

Шаг 1 - заполнение и разделение на блоки

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

Из данных мы формируем блоки, для SHA256 размер блока равен 64 байтам (512 бит), а для SHA512 128 байт (1024 бита). Данные зачастую имеют не кратный блокам размер, поэтому их приходится дополнять. Алгоритм дополнения един:

  1. Добавить к сообщению байт 0x80. В бинарном виде 0x80 выглядит как 10000000.

  2. Добавить к сообщению нулевые байты 0x00.

  3. Добавить к сообщению восемь байт значения длины входных данных.

Теперь внесем конкретику. Сразу после добавленного байта 0x80 нам необходимо заполнить какое-то количество пространства нулевыми байтами 0x00. Это количество пространства определяет следующей формулой:

x = b - m - 1 - 8 \pmod b

Где x - количество нулей для заполнения, b - размер блока данных, m - размер входных данных.

Мы должны точно знать количество нулей в последнем блоке, поэтому вычитаем из размера блока остаток от деления размера входных данных на размер блока. Не стоит забывать и про байт 0x80, который мы добавляем сразу после данных. Кроме того мы должны оставить 8 байт (64 бита) для значения длины входных данных.

Пример блока для алгоритма SHA256
Пример блока для алгоритма SHA256

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

Блок, где почти все место занято данными
Блок, где почти все место занято данными

В статье, на которую я ссылался выше, этот момент упущен. Однако на Википедии все ясно расписано. Мы добавляем байт 0x80 и оставшееся место заполняем нулями. После этого создается новый блок, который содержит в себе только нули заполнителя и 8 байт (64 бита) значения длины входных данных.

Два блока входных данных для алгоритма SHA256
Два блока входных данных для алгоритма SHA256

SHA256:

pub fn sha256(message: &[u8]) -> String {
    let mut m = message.to_vec();
    m.push(0x80);
    if 64 - m.len() % 64 < 8 {
        m.append(&mut vec![0u8; 64 - m.len() % 64])
    }
    m.append(&mut vec![0u8; 64 - m.len() % 64 - 8]);
    m.append(&mut (message.len() as u64 * 8).to_be_bytes().to_vec());
    let blocks = m.chunks_exact(64);

SHA512:

pub fn sha512(message: &[u8]) -> String {
    let mut m = message.to_vec();
    m.push(0x80);
    if 128 - m.len() % 128 < 8 {
        m.append(&mut vec![0u8; 128 - m.len() % 128])
    }
    m.append(&mut vec![0u8; 128 - m.len() % 128 - 8]);
    m.append(&mut (message.len() as u64 * 8).to_be_bytes().to_vec());
    let blocks = m.chunks_exact(128);

Отличие заключается только в том, что в SHA512 размер блок вдвое больше, чем у SHA256, то есть 128 байт (1024 бита).

Шаг 2 - инициализация начальных значений

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

SHA256:

let mut h: [u32; 8] = [
    0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a,
    0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
];
const K: [u32; 64] = [
    0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5,
    0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
    0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3,
    0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
    0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc,
    0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
    0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,
    0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
    0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
    0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
    0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3,
    0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
    0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5,
    0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
    0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,
    0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
];

SHA512:

let mut h: [u64; 8] = [
    0x6a09e667f3bcc908, 0xbb67ae8584caa73b, 0x3c6ef372fe94f82b, 0xa54ff53a5f1d36f1,
    0x510e527fade682d1, 0x9b05688c2b3e6c1f, 0x1f83d9abfb41bd6b, 0x5be0cd19137e2179,
];
const K: [u64; 80] = [
    0x428a2f98d728ae22, 0x7137449123ef65cd, 0xb5c0fbcfec4d3b2f, 0xe9b5dba58189dbbc,
    0x3956c25bf348b538, 0x59f111f1b605d019, 0x923f82a4af194f9b, 0xab1c5ed5da6d8118,
    0xd807aa98a3030242, 0x12835b0145706fbe, 0x243185be4ee4b28c, 0x550c7dc3d5ffb4e2,
    0x72be5d74f27b896f, 0x80deb1fe3b1696b1, 0x9bdc06a725c71235, 0xc19bf174cf692694,
    0xe49b69c19ef14ad2, 0xefbe4786384f25e3, 0x0fc19dc68b8cd5b5, 0x240ca1cc77ac9c65,
    0x2de92c6f592b0275, 0x4a7484aa6ea6e483, 0x5cb0a9dcbd41fbd4, 0x76f988da831153b5,
    0x983e5152ee66dfab, 0xa831c66d2db43210, 0xb00327c898fb213f, 0xbf597fc7beef0ee4,
    0xc6e00bf33da88fc2, 0xd5a79147930aa725, 0x06ca6351e003826f, 0x142929670a0e6e70,
    0x27b70a8546d22ffc, 0x2e1b21385c26c926, 0x4d2c6dfc5ac42aed, 0x53380d139d95b3df,
    0x650a73548baf63de, 0x766a0abb3c77b2a8, 0x81c2c92e47edaee6, 0x92722c851482353b,
    0xa2bfe8a14cf10364, 0xa81a664bbc423001, 0xc24b8b70d0f89791, 0xc76c51a30654be30,
    0xd192e819d6ef5218, 0xd69906245565a910, 0xf40e35855771202a, 0x106aa07032bbd1b8,
    0x19a4c116b8d2d0c8, 0x1e376c085141ab53, 0x2748774cdf8eeb99, 0x34b0bcb5e19b48a8,
    0x391c0cb3c5c95a63, 0x4ed8aa4ae3418acb, 0x5b9cca4f7763e373, 0x682e6ff3d6b2b8a3,
    0x748f82ee5defb2fc, 0x78a5636f43172f60, 0x84c87814a1f0ab72, 0x8cc702081a6439ec,
    0x90befffa23631e28, 0xa4506cebde82bde9, 0xbef9a3f7b2c67915, 0xc67178f2e372532b,
    0xca273eceea26619c, 0xd186b8c721c0c207, 0xeada7dd6cde0eb1e, 0xf57d4f7fee6ed178,
    0x06f067aa72176fba, 0x0a637dc5a2c898a6, 0x113f9804bef90dae, 0x1b710b35131c471b,
    0x28db77f523047d84, 0x32caab7b40c72493, 0x3c9ebe0a15c9bebc, 0x431d67c49c100d4c,
    0x4cc5d4becb3e42b6, 0x597f299cfc657e2a, 0x5fcb6fab3ad6faec, 0x6c44198c4a475817,
];

h хранит значения дробных частей квадратных корней от первых восьми простых чисел. В SHA256 h хранит 32 бита для каждого значения, а в SHA512 64 бита.

Похожий смысл заключен в K. В этой переменной тоже хранятся значения дробных частей, но уже кубических корней от простых чисел. В SHA256 K хранит 64 значения по 32 бита, а в SHA512 80 значений по 64 бита.

Шаг 3 - получение слов

Сейчас начинается этап, где мы будем обрабатывать каждый блок в отдельности. В данный момент мы имеем блок данных и несколько переменных в начальном состоянии. В блоке данные представлены байтами (8 бит), а в переменных h и K 32-х битные и 64-х битные значения. Необходимо произвести приведение типов данных - объединить байты данных в слова w.

SHA256:

for block in blocks {
    let mut w: Vec<u32> = block.chunks_exact(4).map(|chunk| {
        u32::from_be_bytes([chunk[0], chunk[1], chunk[2], chunk[3]])
    }).collect();
    w.append(&mut vec![0u32; 48]);

SHA512:

for block in blocks {
    let mut w: Vec<u64> = block.chunks_exact(8).map(|chunk| {
        u64::from_be_bytes([chunk[0], chunk[1], chunk[2], chunk[3], chunk[4], chunk[5], chunk[6], chunk[7]])
    }).collect();
    w.append(&mut vec![0u64; 64]);

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

SHA256:

for i in 16..64 {
    let s0 = (w[i - 15].rotate_right(7)) ^ (w[i - 15].rotate_right(18)) ^ (w[i - 15] >> 3);
    let s1 = (w[i - 2].rotate_right(17)) ^ (w[i - 2].rotate_right(19)) ^ (w[i - 2] >> 10);
    w[i] = w[i - 16].wrapping_add(s0).wrapping_add(w[i - 7]).wrapping_add(s1);
}

SHA512:

for i in 16..80 {
    let s0 = (w[i - 15].rotate_right(1)) ^ (w[i - 15].rotate_right(8)) ^ (w[i - 15] >> 7);
    let s1 = (w[i - 2].rotate_right(19)) ^ (w[i - 2].rotate_right(61)) ^ (w[i - 2] >> 6);
    w[i] = w[i - 16].wrapping_add(s0).wrapping_add(w[i - 7]).wrapping_add(s1);
}

Здесь мы используем циклические сдвиги, логические сдвиги и операцию исключающего ИЛИ. Когда мы получаем слово, может возникнуть переполнение при сложении, поэтому в Rust мы используем метод wrapping_add().

Шаг 4 - сжатие

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

SHA256:

let mut tmp_h: [u32; 8] = h.clone();

SHA512:

let mut tmp_h: [u64; 8] = h.clone();

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

SHA256:

for i in 0..64 {
    let s1 = (tmp_h[4].rotate_right(6)) ^ (tmp_h[4].rotate_right(11)) ^ (tmp_h[4].rotate_right(25));
    let ch = (tmp_h[4] & tmp_h[5]) ^ (!tmp_h[4] & tmp_h[6]);
    let temp1 = tmp_h[7].wrapping_add(s1).wrapping_add(ch).wrapping_add(K[i]).wrapping_add(w[i]);
    let s0 = (tmp_h[0].rotate_right(2)) ^ (tmp_h[0].rotate_right(13)) ^ (tmp_h[0].rotate_right(22));
    let maj = (tmp_h[0] & tmp_h[1]) ^ (tmp_h[0] & tmp_h[2]) ^ (tmp_h[1] & tmp_h[2]);
    let temp2 = s0.wrapping_add(maj);

SHA512:

for i in 0..80 {
    let s1 = (tmp_h[4].rotate_right(14)) ^ (tmp_h[4].rotate_right(18)) ^ (tmp_h[4].rotate_right(41));
    let ch = (tmp_h[4] & tmp_h[5]) ^ (!tmp_h[4] & tmp_h[6]);
    let temp1 = tmp_h[7].wrapping_add(s1).wrapping_add(ch).wrapping_add(K[i]).wrapping_add(w[i]);
    let s0 = (tmp_h[0].rotate_right(28)) ^ (tmp_h[0].rotate_right(34)) ^ (tmp_h[0].rotate_right(39));
    let maj = (tmp_h[0] & tmp_h[1]) ^ (tmp_h[0] & tmp_h[2]) ^ (tmp_h[1] & tmp_h[2]);
    let temp2 = s0.wrapping_add(maj);

Добавляются новые операции: логическое отрицание и логическое И. Заметьте, что сами операции у обоих версий SHA256 и SHA512 одинаковы, меняются только значения сдвигов.

На каждом раунде мы переопределяем значения tmp_h.

SHA256 и SHA512:

tmp_h[7] = tmp_h[6];
tmp_h[6] = tmp_h[5];
tmp_h[5] = tmp_h[4];
tmp_h[4] = tmp_h[3].wrapping_add(temp1);
tmp_h[3] = tmp_h[2];
tmp_h[2] = tmp_h[1];
tmp_h[1] = tmp_h[0];
tmp_h[0] = temp1.wrapping_add(temp2);

В статье, на которую я ссылался выше допущена ошибка. По сути просто забыли указать, что f = e, но в пояснении этот момент есть.

Шаг 5 - внесение изменений в значения хэша

После всех раундов хэширования мы просто складываем соответствующие значения h и tmp_h.

SHA256 и SHA512:

for i in 0..8 {
    h[i] = h[i].wrapping_add(tmp_h[i]);
}

Шаги 3, 4 и 5 будут повторяться для всех блоков, ни один байт входных данных не останется без внимания.

Шаг 6 - получение хэша

В конце h хранит байты, которые и являются составляющими хэша. Части необходимо соединить и конвертировать в строковый вид.

SHA256:

h.iter()
.map(|byte| format!("{:08x}", byte))
.collect::<Vec<String>>()
.join("")

SHA512:

h.iter()
.map(|byte| format!("{:016x}", byte))
.collect::<Vec<String>>()
.join("")

Ремарка

Есть несколько мест, где могут возникнуть вопросы. Некоторые могут подумать, что алгоритм где-то реализован неправильно. Эта часть предназначена для новичков, люди по-опытнее могут спокойно пропустить.

Этот код был проверен на нескольких примерах с ориентированием на вывод инструментов sha256sum и sha512sum из пакета coreutils версии 9.5.

  1. Не забывайте, что знак переноса строки тоже знак. Он имеет свое байтовое значение, а значит влияет на конечный хэш. "hello world" и "hello world\n" - разные строки и дадут разные значения хэша.
    Используя команду echo "hello world" | sha256sum вы отправляете в утилиту строку "hello world\n" , то есть с переносом строки.

  2. Не забывайте добавлять избыточные ведущие нули. Значения 0x03 и 0x3 идентичны с точки зрения значения, однако когда вы рассчитаете хэш через свою утилиту и забудете добавить избыточные нули, проверка хэша закончится неудачей.

Заключение

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

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

Еще бы я хотел написать здесь про хэш-функцию Стрибог (ГОСТ Р 34.11-2012). Напишите комментарии (или поставьте реакции) если бы хотели такой материал.

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


  1. gudvinr
    01.05.2024 21:50
    +14

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

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


    1. IvanPetrof
      01.05.2024 21:50

      Интересно, сколько людей, которые постоянно повторяют эту фразу и грозятся бить других по рукам, могут аргументированно пояснить в чём опасность самописной криптографии, если она не используется в коммерческих или государственных продуктах?


      1. gudvinr
        01.05.2024 21:50
        +6

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

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

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

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

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

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

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


        1. IvanPetrof
          01.05.2024 21:50
          +4

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

          На самом деле "не боги горшки обжигают".

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


          1. gudvinr
            01.05.2024 21:50
            +1

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

            Non sequitur

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


            1. IvanPetrof
              01.05.2024 21:50
              +3

              Честно говоря, ваш ответ это просто набор демагогических приёмов, не содержащий никакой конкретики. Замени там слово "криптография" на любое другое, ничего не изменится. О чём я и сказал в той фразе, которую вы процитировали.


          1. digit4lsh4d0w Автор
            01.05.2024 21:50
            +4

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

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

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

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

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

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


          1. withkittens
            01.05.2024 21:50

            Люди не способны даже просто правильно сравнить два хэша.


            1. IvanPetrof
              01.05.2024 21:50
              +1

              Вот именно по-этому не стоит слепо доверять алгоритму, реализованному математиками без программистов. Поскольку атака на время сравнения это чисто прикладная вещь, которая не очевидна в идеальном математическом мире формул.


              1. gudvinr
                01.05.2024 21:50

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

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

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


                1. IvanPetrof
                  01.05.2024 21:50
                  +1

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


        1. Vilos
          01.05.2024 21:50

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

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

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


          1. ifap
            01.05.2024 21:50

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


          1. gudvinr
            01.05.2024 21:50
            +1

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

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

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


      1. Kil1J0y
        01.05.2024 21:50
        +1

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

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

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

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


        1. IvanPetrof
          01.05.2024 21:50
          +3

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

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

          С моей точки зрения, реализация готового алгоритма хэш-функции мало чем отличается от реализации готового алгоритма компрессии или сортировки. однако никого не бьют по рукам за свою сортировку (ну если только это не сортировка "пузырьком". Хотя и она имеет право на жизнь на определённых наборах данных)


    1. digit4lsh4d0w Автор
      01.05.2024 21:50

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



  1. Zhuravlev-A-E
    01.05.2024 21:50
    +5

    Используя команду echo "hello world" | sha256sum вы отправляете в утилиту строку "hello world\n" , то есть с переносом строки.

    Добавляйте ключик -n чтобы это не происходило:

    $ echo -n "" | wc -c

    0

    $ echo -n "" | sha256sum

    e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -