Содержание
HMAC (от англ. hash-based message authentication code, код проверки подлинности сообщений, использующий односторонние хеш-функции) — один из механизмов проверки целостности информации, позволяющий гарантировать то, что данные, передаваемые или хранящиеся в ненадёжной среде, не были изменены злоумышленниками.
Кратко о принципе работы HMAC
HMAC смешивает секретный ключ с данными сообщения, хэширует результат с хэш-функцией, снова смешивает хэш-значение с секретным ключом, а затем применяет хэш-функцию во второй раз. HMAC можно использовать для определения того, что при отправке сообщения через небезопасный канал оно не было изменено при условии, что отправитель и получатель совместно используют секретный ключ. Отправитель вычисляет хэш-значение для исходных данных и отправляет исходные данные и хэш-значение в виде одного сообщения. Получатель пересчитывает хэш-значение полученного сообщения и проверяет, совпадают ли хэши. Таким образом, если исходные и вычисляемые хэш-значения совпадают, сообщение проходит проверку подлинности.
В C# реализованы следующие алгоритмы для подписи и проверки подлинности данных: HMAC-MD5, HMAC-SHA1, HMAC-SHA256, HMAC-SHA384 и HMAC-SHA512. Как и в случае с хэш-алгоритмами, которые мы разбирали в прошлой части, все алгоритмы HMAC работают также по одним и тем же принципам, поэтому, рассмотрим работу с ними на примере, наверное, самого популярного алгоритма подписи данных в настоящее время — HMAC-SHA256.
Подписываем данные, используя HMAC-SHA256
Для демонстрации, я создал рядом с exe-файлом приложения файл с названием secretfile.txt в который записал одну строку — «Пример работы HMAC-SHA256». Напишем приложение, которое подпишет этот файл. Также, для работы нам потребуется секретный ключ. Пусть это будет «password».
Следующий код демонстрирует пример подписи файла:
static void Main(string[] args)
{
string sourceFileName = "secretfile.txt";
string outputFileName = "secretfile.enc";
string key = "password";
using HMACSHA256 hmac = new HMACSHA256(Encoding.UTF8.GetBytes(key)); //создаем объект для работы и передаем в него ключ
using FileStream inStream = new FileStream(sourceFileName, FileMode.Open); //создаем файловый поток на чтение
using FileStream outStream = new FileStream(outputFileName, FileMode.Create);//создаем файловый поток на запись
byte[] hashData = hmac.ComputeHash(inStream); //вычисляем хэш
inStream.Position = 0; //возвращаемся в начало потока, содержащего исходный файл
outStream.Write(hashData, 0, hashData.Length);//пишем в выходной поток полученный хэш
//копируем данные из исходного файла в подписываемый
int bytesRead;
byte[] buffer = new byte[1024];
do
{
bytesRead = inStream.Read(buffer, 0, 1024);
outStream.Write(buffer, 0, bytesRead);
} while (bytesRead > 0);
}
Что получится в итоге. Вот так выглядит исходный файл:
Вот так будет выглядеть выходной файл (secretfile.enc):

Проверка подписи
Перейдем к следующему шагу — реализуем проверку подписанного файла, чтобы убедиться, что данные не были изменены. Ниже представлен метод проверки подписи:
public static bool VerifyFile(byte[] key, string sourceFile)
{
bool err = false;
using (HMACSHA256 hmac = new HMACSHA256(key))
{
byte[] storedHash = new byte[hmac.HashSize / 8]; //массив для сохранения хэша из файла (32 байта = 256 / 8)
using (FileStream inStream = new FileStream(sourceFile, FileMode.Open)) //теперь исходный файл - это файл с подписью
{
inStream.Read(storedHash, 0, storedHash.Length); //читаем подпись после этой операции курсор в потоке будет находиться ровно в начале содержимого
byte[] computedHash = hmac.ComputeHash(inStream); //считаем хэш оставшегося содержимого (исключая подпись).
err = computedHash.Except(storedHash).Any(); //если, хотя бы по одному байту не совпали - файл подделали
}
}
if (err)
{
Console.WriteLine("Значения хеша отличаются! Подписанный файл был изменен!");
return false;
}
else
{
Console.WriteLine("Хэш-значения совпадают — файл настоящий.");
return true;
}
}
То есть, в двух словах, суть проверки подписи следующая: так как мы вставляли подпись в файл с начала, то и при проверке мы считываем подпись от начала файла. Дополнительно тут никаких хэшей считать не требуется. Далее мы считаем хэш, используя наш секретный ключ, для оставшейся части файла, то есть того, что подписывалось. И полученный хэш побайтово сравниваем с тем, который «вырезали» из начала файла. Чтобы не писать лишний цикл, я просто воспользовался методом LINQ и проверил есть ли хотя бы одно расхождение в двух массивах. Если расхождений нет — значит подписи совпали и файл настоящий.
Ну и остается последний момент — как вернуть файл в исходное состояние.
Возвращаем файл в исходное состояние
Здесь всё достаточно просто — необходимо скопировать, например, в другой поток всё содержимое после подписи. Например, следующим образом:
inStream.Position = storedHash.Length; //поток с подписанным файлом. Смещаемся на позицию ЗА подпись
using FileStream outStream = new FileStream("decrypt.txt", FileMode.Create); //создаем новый поток для записи контента
int bytesRead; //читаем содержимое из одного потока в другой
byte[] buffer = new byte[1024];
do
{
bytesRead = inStream.Read(buffer, 0, 1024);
outStream.Write(buffer, 0, bytesRead);
} while (bytesRead > 0);
В результате получим исходный файл.
Итого
Сегодня мы рассмотрели способ проверки подлинности сообщений с использованием алгоритма HMAC-SHA256. В примере рассмотрен обычный текстовый файл в который добавляется подпись и, затем, подпись проверяется и делается вывод о том, является ли файл подлинным или же он был подделан. Аналогичным образом HMAC-SHA256 может применяться не только к файлам, но и к обычному тексту. Принцип работы останется неизменным. О том, как получать хэш-значение строк можно прочитать в этой статье.
