Статьи

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

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



Комментарии

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

×

devmark.ru