В этой статье мы покажем вам, как реализуется криптографическая хэш-функция SipHash, которая дает небольшой по размеру хэш и обладает очень высокой производительностью. Реализация данной функции есть на многих языках: C, C#, Rust и даже Haskell, но нет версии на D — и наша команда решила исправить это недоразумение…
Хэш-функция SipHash — это семейство псевдослучайных функций на основе add-rotate-xor (AXR), созданное Жаном-Филиппом Омассоном и Даниэлем Дж. Бернштейном в 2012 году. Эта функция базируется совершенно на иных принципах, нежели привычные функции наподобие SHA, и применяется в основном для реализации разного рода имитовставок.
Наша реализация была получена путем перевода с языка программирования Swift и также анализом референсной реализации, написанной на C. Для перевода был выбран Swift, поскольку к первоначальному варианту на C был ряд вопросов, а Swift как ни парадоксально очень близок к D и последний даже повлиял на синтаксис этой разработки Apple. Детали реализации рассматривать не будем, упомянем лишь то что метод __toLEUlong и функции-хэлперы hash8 — созданы нашей командой.
Код модуля siphash выглядит так:
module siphash; class SipHash(ubyte NUMBER_OF_COMPRESS_ROUNDS = 2, ubyte NUMBER_OF_FINALIZATION_ROUNDS = 4) { private { /// Secret key: 0 - low part of key, 1 - high of key ulong key0, key1; ulong v0 = 0x736f6d6570736575; ulong v1 = 0x646f72616e646f6d; ulong v2 = 0x6c7967656e657261; ulong v3 = 0x7465646279746573; ulong pendingBytes = 0; ulong pendingByteCount = 0; long byteCount = 0; } private { ulong _toLEUlong(ubyte* ptr) @system { ulong h = 0x0000000000000000; h |= (cast(ulong) ptr[0]); h |= (cast(ulong) ptr[1]) << 8; h |= (cast(ulong) ptr[2]) << 16; h |= (cast(ulong) ptr[3]) << 24; h |= (cast(ulong) ptr[4]) << 32; h |= (cast(ulong) ptr[5]) << 40; h |= (cast(ulong) ptr[6]) << 48; h |= (cast(ulong) ptr[7]) << 56; return h; } ulong _rotateLeft(ulong value, ulong amount) { return (value << amount) | (value >> (64 - amount)); } void _round() { v0 += v1; v1 = _rotateLeft(v1, 13); v1 ^= v0; v0 = _rotateLeft(v0, 32); v2 += v3; v3 = _rotateLeft(v3, 16); v3 ^= v2; v0 += v3; v3 = _rotateLeft(v3, 21); v3 ^= v0; v2 += v1; v1 = _rotateLeft(v1, 17); v1 ^= v2; v2 = _rotateLeft(v2, 32); } void _compress(ulong m) { v3 ^= m; for (ubyte i = 0; i < NUMBER_OF_COMPRESS_ROUNDS; i++) { _round; } v0 ^= m; } ulong _finalize() { pendingBytes |= (cast(ulong) byteCount) << 56; byteCount = -1; _compress(pendingBytes); v2 ^= 0xff; for (ubyte i = 0; i < NUMBER_OF_FINALIZATION_ROUNDS; i++) { _round; } return v0 ^ v1 ^ v2 ^ v3; } } this() { import std.random : Random, uniform, unpredictableSeed; auto rng = Random(unpredictableSeed); this(uniform(0UL, ulong.max, rng), uniform(0UL, ulong.max, rng)); } this(ulong key0, ulong key1) { key0 = key0; key1 = key1; v0 ^= key0; v1 ^= key1; v2 ^= key0; v3 ^= key1; } void append(ubyte[] buffer...) { import std.algorithm : min; auto i = 0; if (pendingByteCount > 0) { ulong readCount = min(buffer.length, 8 - pendingByteCount); ulong m = 0; switch (readCount) { case 7: m |= cast(ulong)(buffer[6]) << 48; goto case; case 6: m |= cast(ulong)(buffer[5]) << 40; goto case; case 5: m |= cast(ulong)(buffer[4]) << 32; goto case; case 4: m |= cast(ulong)(buffer[3]) << 24; goto case; case 3: m |= cast(ulong)(buffer[2]) << 16; goto case; case 2: m |= cast(ulong)(buffer[1]) << 8; goto case; case 1: m |= cast(ulong)(buffer[0]); break; default: break; } pendingBytes |= m << cast(ulong)(pendingByteCount << 3); pendingByteCount += readCount; i += readCount; if (pendingByteCount == 8) { _compress(pendingBytes); pendingBytes = 0; pendingByteCount = 0; } } ulong left = (buffer.length - i) & 7; ulong end = (buffer.length - i) - left; while (i < end) { ulong m = 0; auto ptr = buffer[i .. i + 8].ptr; _compress(_toLEUlong(ptr)); i += 8; } switch (left) { case 7: pendingBytes |= cast(ulong)(buffer[i + 6]) << 48; goto case; case 6: pendingBytes |= cast(ulong)(buffer[i + 5]) << 40; goto case; case 5: pendingBytes |= cast(ulong)(buffer[i + 4]) << 32; goto case; case 4: pendingBytes |= cast(ulong)(buffer[i + 3]) << 24; goto case; case 3: pendingBytes |= cast(ulong)(buffer[i + 2]) << 16; goto case; case 2: pendingBytes |= cast(ulong)(buffer[i + 1]) << 8; goto case; case 1: pendingBytes |= cast(ulong)(buffer[i]); break; default: break; } pendingByteCount = left; byteCount += buffer.length; } ulong finalize() { return _finalize(); } } auto hash8(ubyte[] bytes, ulong[2] key = [0UL, 0UL]) { SipHash!(2, 4) sh = new SipHash!(2, 4)(key[0], key[1]); sh.append(bytes); return sh.finalize; } auto hash8(string s, ulong[2] key = [0UL, 0UL]) { import std.string : representation; SipHash!(2, 4) sh = new SipHash!(2, 4)(key[0], key[1]); sh.append((cast(ubyte[]) s)); return sh.finalize; }
Проверить можно следующим образом:
void main() { import std.stdio; SipHash!(2, 4) s = new SipHash!(2, 4)(0x0000000000000000, 0x0000000000000000); s.append(cast(ubyte[]) `Test`); writefln(`%0.16x`, s.finalize); writefln(`%0.16x`, hash8(`Test`)); writefln(`%0.16x`, hash8(`TesT`)); writefln(`%0.16x`, hash8(`Тест`)); writefln(`%0.16x`, hash8(``)); }
Результат:

В этой реализации класс SipHash параметризуется двумя параметрами: количеством раундов сжатия и количеством раундов финализации, поскольку именно эти два параметра влияют непосредственно на работу алгоритма, который выдает 64-битный хэш. По двум вышеописанным параметрам алгоритм обозначают в литературе, к примеру 2 раунда сжатия и 4 финализации (классический вариант) приводят к алгоритму, который обозначается как SipHash-2-4.
Использование класса предполагает инициализацию экземпляра класса при создании не только внутренним состоянием (это переменные v0, v1, v2, v3), но и некоторым 128-битным ключом key, который разбит на две 64-битные переменные — key0 и key1, представляющие собой младшую и старшую части ключа. Если ключ не указать при создании экземпляра класса, то будет сгенерирован псевдослучайный ключ, который и будет использоваться далее.
После создания экземпляра класса, в полученный объект SipHash с помощью метода append подаются байты потока, который подвергнется дальнейшему хэшированию. Метод append не выдает каких-либо результатов и требует запуска метода finalize, который завершает генерацию хэша и выдает итоговый результат в виде 64-битного беззнакового числа. Последующие функции hash8 избавляют от взаимодействия с логикой класса SipHash и позволяют получить хэш просто вызвав любой из этих двух методов (в зависимости от типа входных данных — массива байтов или строки) и при необходимости указав свой ключ.