/******************************************************************************* * Copyright French Prime minister Office/SGMAP/DINSIC/Vitam Program (2015-2019) * * contact.vitam@culture.gouv.fr * * This software is a computer program whose purpose is to implement a digital archiving back-office system managing * high volumetry securely and efficiently. * * This software is governed by the CeCILL 2.1 license under French law and abiding by the rules of distribution of free * software. You can use, modify and/ or redistribute the software under the terms of the CeCILL 2.1 license as * circulated by CEA, CNRS and INRIA at the following URL "http://www.cecill.info". * * As a counterpart to the access to the source code and rights to copy, modify and redistribute granted by the license, * users are provided only with a limited warranty and the software's author, the holder of the economic rights, and the * successive licensors have only limited liability. * * In this respect, the user's attention is drawn to the risks associated with loading, using, modifying and/or * developing or reproducing the software by the user in light of its specific status of free software, that may mean * that it is complicated to manipulate, and that also therefore means that it is reserved for developers and * experienced professionals having in-depth computer knowledge. Users are therefore encouraged to load and test the * software's suitability as regards their requirements in conditions enabling the security of their systems and/or data * to be ensured and, more generally, to use and operate it in the same conditions as regards security. * * The fact that you are presently reading this means that you have had knowledge of the CeCILL 2.1 license and that you * accept its terms. *******************************************************************************/ package fr.gouv.vitam.common.digest; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.security.DigestInputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import fr.gouv.vitam.common.BaseXx; import fr.gouv.vitam.common.CharsetUtils; import fr.gouv.vitam.common.ParametersChecker; import fr.gouv.vitam.common.logging.VitamLogger; import fr.gouv.vitam.common.logging.VitamLoggerFactory; /** * Digest implementation */ public class Digest { private static final String IGNORE = "Ignore"; private static final String ARGUMENT_MUST_NOT_BE_NULL = "Argument must not be null"; private static final VitamLogger LOGGER = VitamLoggerFactory.getInstance(Digest.class); private static final int BUFFER_SIZE = 65536; MessageDigest messageDigest; DigestType type; volatile byte[] finalized = null; private byte[] reusableBytes = null; /** * Create one DigestLight * * @param algo the algorithm to use * @throws IllegalArgumentException if null or unknown algorithm */ public Digest(DigestType algo) { ParametersChecker.checkParameter(ARGUMENT_MUST_NOT_BE_NULL, algo); try { messageDigest = MessageDigest.getInstance(algo.getName()); type = algo; } catch (final NoSuchAlgorithmException e) { throw new IllegalArgumentException("Algo unknown", e); } } /** * Create one DigestLight from parameter * * @param digest * @param algo the algorithm to use * @throws IllegalArgumentException if null or unknown algorithm or if digest is null or empty */ public Digest(String digest, DigestType algo) { this(algo); ParametersChecker.checkParameter(ARGUMENT_MUST_NOT_BE_NULL, digest); finalized = BaseXx.getFromBase16(digest); } /** * * @return the associated digest type */ public final DigestType type() { return type; } /** * * @param bytes the bytes from which to update * @return this * @throws IllegalArgumentException if bytes null */ public final Digest update(byte[] bytes) { ParametersChecker.checkParameter(ARGUMENT_MUST_NOT_BE_NULL, bytes); return update(bytes, 0, bytes.length); } /** * * @param bytes the bytes from which to update * @param offset the offset position * @param length the length * @return this * @throws IllegalArgumentException if bytes null, offset < 0, length < 0 */ public final Digest update(byte[] bytes, int offset, int length) { ParametersChecker.checkParameter(ARGUMENT_MUST_NOT_BE_NULL, bytes); ParametersChecker.checkValue("offset", offset, 0); ParametersChecker.checkValue("length", length, 0); if (offset + length > bytes.length) { throw new IllegalArgumentException("Range is incorrect: " + offset + ":" + length + " while length is " + bytes.length); } if (length == 0) { return this; } finalized = null; messageDigest.update(bytes, offset, length); return this; } /** * * @param buffer * @return this * @throws IllegalArgumentException if buffer null */ public final Digest update(ByteBuffer buffer) { ParametersChecker.checkParameter(ARGUMENT_MUST_NOT_BE_NULL, buffer); final int length = buffer.remaining(); if (length == 0) { return this; } byte[] newbuf; int start = 0; if (buffer.hasArray()) { start = buffer.arrayOffset(); newbuf = buffer.array(); } else { newbuf = new byte[length]; buffer.get(newbuf); } return update(newbuf, start, length); } /** * * @param value the String value from which to update * @return this * @throws IllegalArgumentException value null */ public final Digest update(String value) { ParametersChecker.checkParameter(ARGUMENT_MUST_NOT_BE_NULL, value); return update(value.getBytes(CharsetUtils.UTF8)); } /** * @param in the file from which to update * @return this * @throws IOException if any IO error occurs * @throws IllegalArgumentException in null */ public final Digest update(File in) throws IOException { return update(in, 0, -1); } /** * @param in the file from which to update * @param start the position to start * @param limit if less than 0, means all * @return this * @throws IOException if any IO error occurs * @throws IllegalArgumentException in null, start < 0 */ public final Digest update(File in, long start, long limit) throws IOException { ParametersChecker.checkParameter(ARGUMENT_MUST_NOT_BE_NULL, in); if (!in.isFile()) { throw new IllegalArgumentException("File not found"); } ParametersChecker.checkValue("start", start, 0); if (limit == 0) { return this; } try (FileInputStream inputStream = new FileInputStream(in)) { try (FileChannel fcin = inputStream.getChannel()) { return update(fcin, start, BUFFER_SIZE, limit); } } } /** * @param inputStream the inputstream from which to update using default chunksize * @return this * @throws IOException if any IO error occurs * @throws IllegalArgumentException inputstream null */ public final Digest update(InputStream inputStream) throws IOException { return update(inputStream, BUFFER_SIZE, -1); } /** * @param inputStream the inputstream from which to update * @param chunkSize the chunksize to use * @return this * @throws IOException if any IO error occurs * @throws IllegalArgumentException inputstream null, chunksize < 1 */ public final Digest update(InputStream inputStream, int chunkSize) throws IOException { return update(inputStream, chunkSize, -1); } /** * @param inputStream the inputstream from which to update * @param chunkSize the chunksize to use * @param limit if less than 0, means all * @return this * @throws IOException if any IO error occurs * @throws IllegalArgumentException inputstream null, chunksize < 1 */ public final Digest update(InputStream inputStream, int chunkSize, long limit) throws IOException { ParametersChecker.checkParameter(ARGUMENT_MUST_NOT_BE_NULL, inputStream); ParametersChecker.checkValue("chunkSize", chunkSize, 1); if (limit == 0) { return this; } finalized = null; int size = 0; final byte[] buf = setReusableByte(chunkSize); long toRead = limit > 0 ? limit : Long.MAX_VALUE; int chunk = chunkSize; if (chunk > toRead) { chunk = (int) toRead; } try { while ((size = inputStream.read(buf, 0, chunk)) >= 0) { if (size == 0) { continue; } messageDigest.update(buf, 0, size); toRead -= size; if (toRead <= 0) { break; } if (chunk > toRead) { chunk = (int) toRead; } } return this; } finally { try { inputStream.close(); } catch (final Exception e) { LOGGER.debug(IGNORE, e); // ignore } } } /** * @param fileChannelInputStream the FileChannel inputstream from which to update * @return this * @throws IOException if any IO error occurs * @throws IllegalArgumentException fileChannelIinputStream null */ public final Digest update(FileChannel fileChannelInputStream) throws IOException { return update(fileChannelInputStream, 0, BUFFER_SIZE, -1); } /** * @param fileChannelInputStream the FileChannel inputstream from which to update * @param start the position to start * @param chunkSize the chunksize to use * @param limit if less than 0, means all * @return this * @throws IOException if any IO error occurs * @throws IllegalArgumentException fileChannelIinputStream null, start < 0, chunksize < 1 */ public final Digest update(FileChannel fileChannelInputStream, long start, int chunkSize, long limit) throws IOException { ParametersChecker.checkParameter(ARGUMENT_MUST_NOT_BE_NULL, fileChannelInputStream); ParametersChecker.checkValue("chunkSize", chunkSize, 1); ParametersChecker.checkValue("start", start, 0); if (limit == 0) { return this; } finalized = null; int size = 0; final byte[] buf = setReusableByte(chunkSize); final long toRead = limit > 0 ? limit : Long.MAX_VALUE; long read = 0; int chunk = chunkSize; if (chunk > toRead) { chunk = (int) toRead; } try { final ByteBuffer bb = ByteBuffer.wrap(buf); if (start > 0) { fileChannelInputStream.position(start); } while ((size = fileChannelInputStream.read(bb)) >= 0) { if (size == 0) { continue; } if (read + size > toRead) { size = (int) (toRead - read); } read += size; messageDigest.update(buf, 0, size); bb.clear(); if (read >= toRead) { break; } } return this; } finally { try { fileChannelInputStream.close(); } catch (final Exception e) { LOGGER.debug(IGNORE, e); // ignore } } } /** * Will update the Digest while the returned InputStream will be read * * @param inputStream from which the data to digest will be done * @return the new InputStream to use instead of the given one as parameter */ public InputStream getDigestInputStream(InputStream inputStream) { return new DigestInputStream(inputStream, messageDigest); } /** * Reset the DigestLight * * @return this */ public final Digest reset() { messageDigest.reset(); finalized = null; return this; } /** * * @return the digest */ public final byte[] digest() { if (finalized == null) { MessageDigest dclone; try { dclone = (MessageDigest) messageDigest.clone(); finalized = dclone.digest(); } catch (final CloneNotSupportedException e) { LOGGER.debug(IGNORE, e); // ignore finalized = messageDigest.digest(); } } return finalized; } /** * * @return the digest in Base16 format */ public final String digestHex() { if (finalized == null) { MessageDigest dclone; try { dclone = (MessageDigest) messageDigest.clone(); finalized = dclone.digest(); } catch (final CloneNotSupportedException e) { LOGGER.debug(IGNORE, e); // ignore finalized = messageDigest.digest(); } } return BaseXx.getBase16(finalized); } @Override public final String toString() { return digestHex(); } @Override public final boolean equals(Object obj) { if (obj == null) { return false; } if (obj instanceof Digest) { final Digest digest = (Digest) obj; return digest.type == type && MessageDigest.isEqual(digest(), digest.digest()); } if (obj instanceof String) { return ((String) obj).equalsIgnoreCase(digestHex()); } if (obj instanceof byte[]) { return MessageDigest.isEqual(digest(), (byte[]) obj); } return false; } @Override public final int hashCode() { return digestHex().hashCode(); } /** * @param digest the digest to compare to * @param algo the associated algorithm * @return True if the 2 digests are of the same type and same value */ public final boolean equalsWithType(String digest, DigestType algo) { if (digest == null || algo == null) { return false; } return algo == type && digest.equalsIgnoreCase(digestHex()); } /** * @param digest the digest in byte to use * @param algo the associated algorithm * @return True if the 2 digests are of the same type and same value */ public final boolean equalsWithType(byte[] digest, DigestType algo) { if (digest == null || algo == null) { return false; } return algo == type && MessageDigest.isEqual(digest(), digest); } private final byte[] setReusableByte(int length) { if (reusableBytes == null || reusableBytes.length != length) { reusableBytes = new byte[length]; } return reusableBytes; } /** * * @param in the inputstream from which the digest will be computed * @param algo the algorithm to use * @return the digest for this inputStream * @throws IOException if any IO error occurs * @throws IllegalArgumentException in or algo null */ public static final Digest digest(InputStream in, DigestType algo) throws IOException { return new Digest(algo).update(in); } /** * @param in the file from which the digest will be computed * @param algo the algorithm to use * @return the digest for this File * @throws IOException if any IO error occurs * @throws IllegalArgumentException in or algo null */ public static final Digest digest(File in, DigestType algo) throws IOException { return new Digest(algo).update(in); } }