Статьи
YouTube-канал

Вычисление контрольной суммы файла

21 марта 2021

Тэги: Java Java 11 алгоритмы ввод-вывод

Содержание

  1. Вычисление md5 с помощью MessageDigest
  2. Вычисление sha-256 и sha-512
  3. Вычисление crc-32
  4. Выводы

Контрольная сумма от набора байт позволяет убедиться в том, что данные на клиенте, полученные от сервера, являются корректными. Для этого вместе с файлом сервер может предоставлять контрольную сумму для проверки на клиентской стороне. Существует несколько алгоритмов вычисления контрольной суммы, рассмотрим самые популярные: md5, sha-256, sha-512 и crc-32.

Вычисление md5 с помощью MessageDigest

В пакете java.security есть такой класс как MessageDigest. Он позволяет получить одну из встроенных реализаций алгоритма вычисления контрольных сумм. Поэтому сначала реализуем метод, который абстрагирован от конкретного алгоритма и работает с любым MessageDigest одинаково.

private static String checksumForDigest(String filename, MessageDigest md) throws IOException {
    try (
            var fis = new FileInputStream(filename);
            var bis = new BufferedInputStream(fis);
            var dis = new DigestInputStream(bis, md)
    ) {
        while (dis.read() != -1) ;
        md = dis.getMessageDigest();
    }

Метод checksumForDigest() получает два параметра: полное имя файла и объект MessageDigest. Затем используем конструкцию try-with-resources, в котором последовательно создаём три потока, «оборачивая» один в другой. По выходу из блока try эти потоки будут закрыты автоматически.

Сначала создаётся поток FileInputStream, связанный непосредственно с целевым файлом, для которого считаем контрольную сумму.

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

Наконец, этот второй буферизованный поток оборачиваем в DigestInputStream, который и реализует механизм подсчёта контрольной сумму в соответствии с заданным в MessageDigest алгоритмом.

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

Кстати, такая «матрёшка» из потоков является типичным примером паттерна «Декоратор», который позволяет наращивать функциональность объекта, не меняя исходный интерфейс.

Затем в цикле читаем файл. Тело цикла при этом должно быть пустым, т.к. каких-то специальных действий на каждой итерации нам делать не нужно - всё необходимое делает DigestInputStream.

После завершения чтения файла получаем новый объект MessageDigest с помощью метода getMessageDigest(). Чтобы получить шестандцатеричную строку хеша, выполним следующее преобразование:

// bytes to hex
var result = new StringBuilder();
for (byte b : md.digest()) {
    result.append(String.format("%02x", b));
}
return result.toString();

Теперь осталось передать абсолютный путь до целевого файла и требуемую реализацию алгоритма md-5.

public static void main(String[] args) throws IOException, NoSuchAlgorithmException {
    var filename = "/home/user/test.txt";
    var messageDigest = MessageDigest.getInstance("MD5");
    System.out.println(checksumForDigest(filename, messageDigest));
}

Объект messageDigest получаем с помощью метода getInstance(). Теперь запустим наш пример и в консоли увидим строку в шестандцатеричном формате. Для проверки в linux-системах можно использовать консольную утилиту md5sum. В результате в консоли увидим нечто подобное:

md5sum test.txt
c21197df738df89c5600e6092146439c  test.txt

Вычисление sha-256 и sha-512

Семейство алгоритмов SHA-2 (Secure Hash Algorithm Version 2 - безопасный алгоритм хеширования, версия 2) включает в себя наиболее популярные sha-256 и sha-512. Мы также можем вычислять их с помощью метода, рассмотренного выше.

public static void main(String[] args) throws IOException, NoSuchAlgorithmException {
    var filename = "/home/user/test.txt";
    var messageDigest = MessageDigest.getInstance("SHA-256"); // или "SHA-512"
    System.out.println(checksumForDigest(filename, messageDigest));
}

Как видите, метод main() почти не изменился. Мы просто поменяли параметр getInstance() на «SHA-256». Вычисление SHA-512 происходит аналогично.

Для проверки полученных значений в консоли можем воспользоваться утилитами sha256sum и sha512sum соответственно:

sha256sum test.txt
24c5e1ce2476011f59fc89c98c221370f424d12c7eac5eaeeea5bcda9b5ae1df  test.txt

sha512sum test.txt
d8f41a39aed21317d92ea86e3c71e6b2f1db4732847914ae11628e2d357e32
28d67c9c559cdc18d2cc6f3c9789cab443e1b33cf6d97e88fc694bdb593076591b  test.txt

Как видите, хэш SHA-512 длинее, а потому безопаснее и рекомендуется использовать именно его.

Вычисление crc-32

Теперь перейдём к хэшу crc-32 (Cyclic redundancy check). Метод его вычисления ещё проще:

private static String getCrc32(String filename, Checksum checksum) throws IOException {
    try (
            var fis = new FileInputStream(filename);
            var bis = new BufferedInputStream(fis);
            var cis = new CheckedInputStream(bis, checksum);
    ) {
        while (cis.read() >= 0) ;
        return Long.toHexString(cis.getChecksum().getValue());
    }
}

Метод на вход получает абсолютное имя файла и объект с интерфейсом Checksum из пакета java.util.zip. Java предлагает несколько реализаций Checksum, одной из которых является класс CRC32.

Здесь мы также используем конструкцию try-with-resources из трёх потоков, как и в предыдущем примере. FileInputStream связан с целевым файлом, BufferedInputStream обеспечивает буферизацию для ускорения обработки больших файлов, а CheckedInputStream как раз позволяет вычислять контрольную сумму с помощью объекта Checksum.

Тут у нас также используется пустой цикл, который считывает файл до конца. После завершения чтения файла мы получаем контрольную сумму в виде целого числа с помощью метода getChecksum().getValue(). Для того, чтобы привести это число в шестнадцатеричный формат, воспользуемся методом Long.toHexString().

Наш метод main() для вычисления контрольной суммы crc-32 будет выглядеть так:

public static void main(String[] args) throws IOException {
    var filename = "/home/user/test.txt";
    System.out.println(getCrc32(filename, new CRC32())); // или CRC32C()
}

Второй реализацией интерфейса Checksum является класс CRC32C - одна из разновидностей алгоритма CRC.

Теперь запустим наш пример и получим контрольную сумму в шестнадцатеричном формате. Для её проверки воспользуемся консольной утилитой crc32:

crc32 test.txt
67b5fa4e

Как видите, в данном случае код довольно компактный.

Выводы

Мы увидели, что «из коробки» Java предоставляет реализации всех популярных алгоритмов для вычисления контрольных сумм, причём от нас не требуется разбираться в их внутреннем устройстве - достаточно лишь использовать соотвествующий поток. Также для ускорения обработки больших файлов нужно не забывать про буферизацию.


Облако тэгов

Kotlin, Java, Java 16, Java 11, Java 10, Java 9, Java 8, Spring, Spring Boot, Spring Data, SQL, PostgreSQL, Oracle, Hibernate, Collections, Stream API, многопоточность, ввод-вывод, Apache, maven, gradle, JUnit, YouTube, новости, ООП, алгоритмы, головоломки, rest, GraphQL, Excel, XML, json, yaml

Последние статьи


Комментарии

Добавить комментарий

×

devmark.ru