/* See LICENSE for licensing and NOTICE for copyright. */ package org.cryptacular.util; import java.io.IOException; import java.io.InputStream; import java.util.Arrays; import org.bouncycastle.crypto.Digest; import org.bouncycastle.crypto.digests.SHA1Digest; import org.bouncycastle.crypto.digests.SHA256Digest; import org.bouncycastle.crypto.digests.SHA3Digest; import org.bouncycastle.crypto.digests.SHA512Digest; import org.cryptacular.CryptoException; import org.cryptacular.SaltedHash; import org.cryptacular.StreamException; import org.cryptacular.io.Resource; /** * Utility class for computing cryptographic hashes. * * @author Middleware Services */ public final class HashUtil { /** Private constructor of utility class. */ private HashUtil() {} /** * Computes the hash of the given data using the given algorithm. A salted hash may be produced as follows: * * <pre> // data is a byte array containing raw data to digest final byte[] salt = new RBGNonce(16).generate(); final byte[] hash = HashUtil.hash(new SHA1Digest(), data, salt); * </pre> * * @param digest Hash algorithm. * @param data Data to hash. Supported types are <code>byte[]</code>, {@link CharSequence} ,{@link InputStream}, and * {@link Resource}. Character data is processed in the <code>UTF-8</code> character set; if another * character set is desired, the caller should convert to <code>byte[]</code> and provide the resulting * bytes. * * @return Byte array of length {@link Digest#getDigestSize()} containing hash output. * * @throws CryptoException on hash computation errors. * @throws StreamException on stream IO errors. */ public static byte[] hash(final Digest digest, final Object... data) throws CryptoException, StreamException { for (Object o : data) { if (o instanceof byte[]) { final byte[] bytes = (byte[]) o; digest.update(bytes, 0, bytes.length); } else if (o instanceof String) { final byte[] bytes = ByteUtil.toBytes((String) o); digest.update(bytes, 0, bytes.length); } else if (o instanceof InputStream) { hashStream(digest, (InputStream) o); } else if (o instanceof Resource) { final InputStream in; try { in = ((Resource) o).getInputStream(); } catch (IOException e) { throw new StreamException(e); } hashStream(digest, in); } else { throw new IllegalArgumentException("Invalid input data type " + o); } } final byte[] output = new byte[digest.getDigestSize()]; try { digest.doFinal(output, 0); } catch (RuntimeException e) { throw new CryptoException("Hash computation error", e); } return output; } /** * Computes the iterated hash of the given data using the given algorithm. The following example demonstrates a * typical usage pattern, a salted hash with 10 rounds: * * <pre> // data is a byte array containing raw data to digest final byte[] salt = new RBGNonce(16).generate(); final byte[] hash = HashUtil.hash(new SHA1Digest(), 10, data, salt); * </pre> * * @param digest Hash algorithm. * @param iterations Number of hash rounds. Must be positive value. * @param data Data to hash. Supported types are <code>byte[]</code>, {@link CharSequence} ,{@link InputStream}, and * {@link Resource}. Character data is processed in the <code>UTF-8</code> character set; if another * character set is desired, the caller should convert to <code>byte[]</code> and provide the resulting * bytes. * * @return Byte array of length {@link Digest#getDigestSize()} containing hash output. * * @throws CryptoException on hash computation errors. * @throws StreamException on stream IO errors. */ public static byte[] hash(final Digest digest, final int iterations, final Object... data) throws CryptoException, StreamException { if (iterations < 1) { throw new IllegalArgumentException("Iterations must be positive"); } final byte[] output = hash(digest, data); try { for (int i = 1; i < iterations; i++) { digest.update(output, 0, output.length); digest.doFinal(output, 0); } } catch (RuntimeException e) { throw new CryptoException("Hash computation error", e); } return output; } /** * Determines whether the hash of the given input equals a known value. * * @param digest Hash algorithm. * @param hash Hash to compare with. If the length of the array is greater than the length of the digest output, * anything beyond the digest length is considered salt data that is hashed <strong>after</strong> the * input data. * @param iterations Number of hash rounds. * @param data Data to hash. * * @return True if the hash of the data under the given digest is equal to the hash, false otherwise. * * @throws CryptoException on hash computation errors. * @throws StreamException on stream IO errors. */ public static boolean compareHash(final Digest digest, final byte[] hash, final int iterations, final Object... data) throws CryptoException, StreamException { if (hash.length > digest.getDigestSize()) { final byte[] hashPart = Arrays.copyOfRange(hash, 0, digest.getDigestSize()); final byte[] saltPart = Arrays.copyOfRange(hash, digest.getDigestSize(), hash.length); final Object[] dataWithSalt = Arrays.copyOf(data, data.length + 1); dataWithSalt[data.length] = saltPart; return Arrays.equals(hash(digest, iterations, dataWithSalt), hashPart); } return Arrays.equals(hash(digest, iterations, data), hash); } /** * Determines whether the salted hash of the given input equals a known hash value. * * @param digest Hash algorithm. * @param hash Salted hash data. * @param iterations Number of hash rounds. * @param saltAfterData True to apply salt after data, false to apply salt before data. * @param data Data to hash, which should NOT include the salt value. * * @return True if the hash of the data under the given digest is equal to the hash, false otherwise. * * @throws CryptoException on hash computation errors. * @throws StreamException on stream IO errors. */ public static boolean compareHash( final Digest digest, final SaltedHash hash, final int iterations, final boolean saltAfterData, final Object... data) throws CryptoException, StreamException { final Object[] dataWithSalt; if (saltAfterData) { dataWithSalt = Arrays.copyOf(data, data.length + 1); dataWithSalt[data.length] = hash.getSalt(); } else { dataWithSalt = new Object[data.length + 1]; dataWithSalt[0] = hash.getSalt(); System.arraycopy(data, 0, dataWithSalt, 1, data.length); } return Arrays.equals(hash(digest, iterations, dataWithSalt), hash.getHash()); } /** * Produces the SHA-1 hash of the given data. * * @param data Data to hash. See {@link #hash(Digest, Object...)} for supported inputs. * * @return 20-byte array containing hash output. * * @see #hash(Digest, Object...) */ public static byte[] sha1(final Object... data) { return hash(new SHA1Digest(), data); } /** * Produces the SHA-256 hash of the given data. * * @param data Data to hash. See {@link #hash(Digest, Object...)} for supported inputs. * * @return 32-byte array containing hash output. * * @see #hash(Digest, Object...) */ public static byte[] sha256(final Object... data) { return hash(new SHA256Digest(), data); } /** * Produces the SHA-512 hash of the given data. * * @param data Data to hash. See {@link #hash(Digest, Object...)} for supported inputs. * * @return 64-byte array containing hash output. * * @see #hash(Digest, Object...) */ public static byte[] sha512(final Object... data) { return hash(new SHA512Digest(), data); } /** * Produces the SHA-3 hash of the given data. * * @param bitLength One of the supported SHA-3 output bit lengths: 224, 256, 384, or 512. * @param data Data to hash. See {@link #hash(Digest, Object...)} for supported inputs. * * @return Byte array of size <code>bitLength</code> containing hash output. * * @see #hash(Digest, Object...) */ public static byte[] sha3(final int bitLength, final Object... data) { return hash(new SHA3Digest(bitLength), data); } /** * Digests the data in the given stream. Note this method does not finalize the digest process by calling {@link * Digest#doFinal(byte[], int)}. * * @param digest Digest algorithm. * @param in Input stream containing data to hash. */ private static void hashStream(final Digest digest, final InputStream in) { final byte[] buffer = new byte[StreamUtil.CHUNK_SIZE]; int length; try { while ((length = in.read(buffer)) > 0) { digest.update(buffer, 0, length); } } catch (IOException e) { throw new StreamException(e); } } }